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:
47
templates/MetaLab-2026/html/follow/followers.html
Normal file
47
templates/MetaLab-2026/html/follow/followers.html
Normal file
@ -0,0 +1,47 @@
|
||||
{{template "layout/header.html" .}}
|
||||
<meta name="robots" content="noindex,nofollow">
|
||||
<body>
|
||||
|
||||
{{template "layout/nav.html" .}}
|
||||
|
||||
<div class="follow-list-page">
|
||||
<h1>粉丝列表</h1>
|
||||
|
||||
{{if not .Result.Accessible}}
|
||||
<div class="follow-list-private">
|
||||
<p>该用户设置了关注列表不公开</p>
|
||||
</div>
|
||||
{{else if eq (len .Result.Items) 0}}
|
||||
<div class="follow-list-empty">
|
||||
<p>暂无粉丝</p>
|
||||
</div>
|
||||
{{else}}
|
||||
{{range .Result.Items}}
|
||||
<div class="follow-list-item">
|
||||
<a href="/space/{{.FollowerID}}" class="follow-list-user">
|
||||
<div class="follow-list-avatar">
|
||||
{{slice .FollowerUsername 0 1}}
|
||||
</div>
|
||||
<span class="follow-list-name">{{.FollowerUsername}}</span>
|
||||
</a>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if gt .TotalPages 1}}
|
||||
<div class="pagination">
|
||||
{{if .HasPrev}}
|
||||
<a href="?page={{.PrevPage}}" class="page-link">上一页</a>
|
||||
{{end}}
|
||||
<span class="page-info">第 {{.Page}} / {{.TotalPages}} 页</span>
|
||||
{{if .HasNext}}
|
||||
<a href="?page={{.NextPage}}" class="page-link">下一页</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{template "layout/footer.html" .}}
|
||||
<script src="/static/js/common.js?v={{assetV "/static/js/common.js"}}"></script>
|
||||
</body>
|
||||
</html>
|
||||
47
templates/MetaLab-2026/html/follow/following.html
Normal file
47
templates/MetaLab-2026/html/follow/following.html
Normal file
@ -0,0 +1,47 @@
|
||||
{{template "layout/header.html" .}}
|
||||
<meta name="robots" content="noindex,nofollow">
|
||||
<body>
|
||||
|
||||
{{template "layout/nav.html" .}}
|
||||
|
||||
<div class="follow-list-page">
|
||||
<h1>关注列表</h1>
|
||||
|
||||
{{if not .Result.Accessible}}
|
||||
<div class="follow-list-private">
|
||||
<p>该用户设置了关注列表不公开</p>
|
||||
</div>
|
||||
{{else if eq (len .Result.Items) 0}}
|
||||
<div class="follow-list-empty">
|
||||
<p>暂未关注任何人</p>
|
||||
</div>
|
||||
{{else}}
|
||||
{{range .Result.Items}}
|
||||
<div class="follow-list-item">
|
||||
<a href="/space/{{.FolloweeID}}" class="follow-list-user">
|
||||
<div class="follow-list-avatar">
|
||||
{{slice .FolloweeUsername 0 1}}
|
||||
</div>
|
||||
<span class="follow-list-name">{{.FolloweeUsername}}</span>
|
||||
</a>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if gt .TotalPages 1}}
|
||||
<div class="pagination">
|
||||
{{if .HasPrev}}
|
||||
<a href="?page={{.PrevPage}}" class="page-link">上一页</a>
|
||||
{{end}}
|
||||
<span class="page-info">第 {{.Page}} / {{.TotalPages}} 页</span>
|
||||
{{if .HasNext}}
|
||||
<a href="?page={{.NextPage}}" class="page-link">下一页</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{template "layout/footer.html" .}}
|
||||
<script src="/static/js/common.js?v={{assetV "/static/js/common.js"}}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -10,13 +10,29 @@
|
||||
<h2>消息中心</h2>
|
||||
</div>
|
||||
<nav class="msg-sidebar-nav">
|
||||
<span class="msg-sidebar-item active">
|
||||
<a href="/messages?tab=all" class="msg-sidebar-item{{if or (eq .CurrentTab "all") (eq .CurrentTab "")}} active{{end}}">
|
||||
<svg class="msg-sidebar-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>
|
||||
全部消息
|
||||
{{if .UnreadCount}}
|
||||
<span class="msg-sidebar-count">{{.UnreadCount}}</span>
|
||||
{{end}}
|
||||
</span>
|
||||
</a>
|
||||
<a href="/messages?tab=system" class="msg-sidebar-item{{if eq .CurrentTab "system"}} active{{end}}">
|
||||
<svg class="msg-sidebar-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
|
||||
系统通知
|
||||
</a>
|
||||
<a href="/messages?tab=mention" class="msg-sidebar-item{{if eq .CurrentTab "mention"}} active{{end}}">
|
||||
<svg class="msg-sidebar-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||
@艾特我的
|
||||
</a>
|
||||
<a href="/messages?tab=like" class="msg-sidebar-item{{if eq .CurrentTab "like"}} active{{end}}">
|
||||
<svg class="msg-sidebar-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>
|
||||
点赞通知
|
||||
</a>
|
||||
<a href="/messages?tab=follow" class="msg-sidebar-item{{if eq .CurrentTab "follow"}} active{{end}}">
|
||||
<svg class="msg-sidebar-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="8.5" cy="7" r="4"/><line x1="20" y1="8" x2="20" y2="14"/><line x1="23" y1="11" x2="17" y2="11"/></svg>
|
||||
关注通知
|
||||
</a>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
@ -78,13 +94,13 @@
|
||||
{{if .TotalPages}}
|
||||
<div class="msg-pagination">
|
||||
{{if .HasPrev}}
|
||||
<a href="/messages?page={{.PrevPage}}" class="msg-page-btn">上一页</a>
|
||||
<a href="/messages?tab={{.CurrentTab}}&page={{.PrevPage}}" class="msg-page-btn">上一页</a>
|
||||
{{else}}
|
||||
<span class="msg-page-btn disabled">上一页</span>
|
||||
{{end}}
|
||||
<span class="msg-page-info">{{.Page}} / {{.TotalPages}}</span>
|
||||
{{if .HasNext}}
|
||||
<a href="/messages?page={{.NextPage}}" class="msg-page-btn">下一页</a>
|
||||
<a href="/messages?tab={{.CurrentTab}}&page={{.NextPage}}" class="msg-page-btn">下一页</a>
|
||||
{{else}}
|
||||
<span class="msg-page-btn disabled">下一页</span>
|
||||
{{end}}
|
||||
|
||||
@ -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>
|
||||
|
||||
Reference in New Issue
Block a user