feat: 实现关注系统 + 通知侧边栏分类 TAB + 点赞聚合通知

- 新增关注系统:UserFollow 模型、FollowService(toggle/status/列表隐私控制)
- User 新增 FollowersCount/FollowingCount/FollowListPublic/NotifyPrefs 字段
- Space 页面增加四态关注按钮(关注/已关注/回关/已互粉)+ 粉丝/关注数链接
- 新增 /space/:uid/followers 和 /space/:uid/following 列表页
- 新增 NotifyFollow/NotifyLikeAggregated 通知类型
- ReactionService 点赞时写入 daily_like_summary,访问时生成聚合通知
- FollowService 关注时触发 NotifyFollow 通知
- 消息中心侧边栏升级为分类 TAB(全部/系统通知/@艾特/点赞/关注)
- NotificationService 新增 ListByCategory 按分类分页查询
This commit is contained in:
2026-06-01 16:30:46 +08:00
parent 2b47380ac5
commit 680df7371a
26 changed files with 1251 additions and 23 deletions

View File

@ -30,6 +30,15 @@
<span class="space-divider">·</span>
<span class="space-joined">加入于 {{.SpaceUser.CreatedAt.Format "2006-01-02"}}</span>
</div>
<div class="follow-links">
<a href="/space/{{.SpaceUser.ID}}/following"><strong>{{.SpaceUser.FollowingCount}}</strong> 关注</a>
<a href="/space/{{.SpaceUser.ID}}/followers"><strong>{{.SpaceUser.FollowersCount}}</strong> 粉丝</a>
</div>
{{if not .IsOwnSpace}}
<div class="follow-btn-wrapper">
<button id="followBtn" class="follow-btn" data-uid="{{.SpaceUser.ID}}">关注</button>
</div>
{{end}}
</div>
</div>
</div>
@ -89,5 +98,75 @@
{{template "layout/footer.html" .}}
<script src="/static/js/common.js?v={{assetV "/static/js/common.js"}}"></script>
{{if not .IsOwnSpace}}
<script>
(function() {
var followBtn = document.getElementById('followBtn');
if (!followBtn) return;
var targetUid = followBtn.getAttribute('data-uid');
var csrfMeta = document.querySelector('meta[name="csrf-token"]');
var csrfToken = csrfMeta ? csrfMeta.getAttribute('content') : '';
// 四个按钮状态文案
var labels = {
none: '关注',
following: '已关注',
followBack: '回关',
mutual: '已互粉'
};
var classes = {
none: '',
following: 'following',
followBack: '',
mutual: 'mutual'
};
function buttonState(status) {
if (status.i_follow && status.they_follow) return 'mutual';
if (status.i_follow) return 'following';
if (status.they_follow) return 'followBack';
return 'none';
}
function updateBtn(status) {
var state = buttonState(status);
followBtn.textContent = labels[state];
followBtn.className = 'follow-btn ' + (classes[state] || '');
}
// 获取初始状态
fetch('/api/users/' + targetUid + '/follow-status')
.then(function(res) { return res.json(); })
.then(function(data) {
if (data && data.success && data.data) {
updateBtn(data.data);
}
});
// 点击切换
followBtn.addEventListener('click', function() {
followBtn.classList.add('waiting');
var xhr = new XMLHttpRequest();
xhr.open('POST', '/api/users/' + targetUid + '/follow', true);
xhr.setRequestHeader('X-CSRF-Token', csrfToken);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
followBtn.classList.remove('waiting');
if (xhr.status === 200) {
try {
var resp = JSON.parse(xhr.responseText);
if (resp.data && resp.data.status) {
updateBtn(resp.data.status);
}
} catch(e) {}
}
}
};
xhr.send();
});
})();
</script>
{{end}}
</body>
</html>