feat: 实现赞/踩系统(P2)
- 新增 PostLike/PostDislike 模型,复合主键 (user_id, post_id) - Post 表新增 LikesCount/DislikesCount 冗余计数器 - Repository 层:reaction_repo.go,含 WithTx 事务支持 - Service 层:reaction_service.go,赞踩互斥逻辑(6 态切换),事务包裹 - Controller 层:reaction_controller.go,3 个 API(ToggleLike/ToggleDislike/GetReaction) - 接口定义:controller/interfaces.go 新增 reactionUseCase,service/repository.go 新增 ReactionStore - 路由注册 + 依赖注入 + AutoMigrate - 前端:文章详情页赞踩按钮 + 状态查询 + 401 跳转登录 - 样式:posts.css 新增赞踩按钮样式
This commit is contained in:
@ -31,6 +31,17 @@
|
||||
<div class="post-detail-body" id="postContent"></div>
|
||||
</article>
|
||||
|
||||
<!-- 赞/踩按钮 -->
|
||||
<div class="post-reactions" id="postReactions" data-post-id="{{.Post.ID}}">
|
||||
<button class="reaction-btn reaction-like-btn" id="likeBtn" title="点赞">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="20" height="20"><path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3H14zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"/></svg>
|
||||
<span class="reaction-like-count" id="likesCount">{{.Post.LikesCount}}</span>
|
||||
</button>
|
||||
<button class="reaction-btn reaction-dislike-btn" id="dislikeBtn" title="踩">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="20" height="20"><path d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3H10zM17 2h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{if .IsLoggedIn}}
|
||||
<div class="post-actions">
|
||||
{{if and $.Post (or (eq $.Post.UserID $.UID) $.CanAccessAdmin) (ne $.Post.Status "pending") (not $.Post.IsLocked)}}
|
||||
@ -253,6 +264,82 @@
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// ========== 赞/踩系统 ==========
|
||||
(function() {
|
||||
var container = document.getElementById('postReactions');
|
||||
var likeBtn = document.getElementById('likeBtn');
|
||||
var dislikeBtn = document.getElementById('dislikeBtn');
|
||||
var likesCountEl = document.getElementById('likesCount');
|
||||
if (!container || !likeBtn || !dislikeBtn) return;
|
||||
|
||||
var postId = container.dataset.postId;
|
||||
var csrfMeta = document.querySelector('meta[name="csrf-token"]');
|
||||
var csrfToken = csrfMeta ? csrfMeta.getAttribute('content') : '';
|
||||
|
||||
// 页面加载时获取当前反应状态
|
||||
fetch('/api/posts/' + postId + '/reaction')
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
if (!d.success || !d.data) return;
|
||||
updateUI(d.data.reaction, d.data.likes_count);
|
||||
})
|
||||
.catch(function() {});
|
||||
|
||||
function updateUI(reaction, count) {
|
||||
likeBtn.classList.remove('active');
|
||||
dislikeBtn.classList.remove('active');
|
||||
if (reaction === 'liked') likeBtn.classList.add('active');
|
||||
if (reaction === 'disliked') dislikeBtn.classList.add('active');
|
||||
if (typeof count === 'number') likesCountEl.textContent = count;
|
||||
}
|
||||
|
||||
likeBtn.addEventListener('click', function() {
|
||||
likeBtn.disabled = true;
|
||||
fetch('/api/posts/' + postId + '/like', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken }
|
||||
})
|
||||
.then(function(r) {
|
||||
if (r.status === 401) { window.location.href = '/login?redirect=/posts/' + postId; return null; }
|
||||
return r.json();
|
||||
})
|
||||
.then(function(d) {
|
||||
likeBtn.disabled = false;
|
||||
if (!d) return;
|
||||
if (d.success) {
|
||||
updateUI(d.data.liked ? 'liked' : 'none', d.data.likes_count);
|
||||
} else {
|
||||
alert(d.message || '操作失败');
|
||||
}
|
||||
})
|
||||
.catch(function() { likeBtn.disabled = false; });
|
||||
});
|
||||
|
||||
dislikeBtn.addEventListener('click', function() {
|
||||
dislikeBtn.disabled = true;
|
||||
fetch('/api/posts/' + postId + '/dislike', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken }
|
||||
})
|
||||
.then(function(r) {
|
||||
if (r.status === 401) { window.location.href = '/login?redirect=/posts/' + postId; return null; }
|
||||
return r.json();
|
||||
})
|
||||
.then(function(d) {
|
||||
dislikeBtn.disabled = false;
|
||||
if (!d) return;
|
||||
if (d.success) {
|
||||
updateUI(d.data.disliked ? 'disliked' : 'none');
|
||||
} else {
|
||||
alert(d.message || '操作失败');
|
||||
}
|
||||
})
|
||||
.catch(function() { dislikeBtn.disabled = false; });
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// ========== 评论系统 ==========
|
||||
(function() {
|
||||
|
||||
Reference in New Issue
Block a user