feat(comments): 实现评论点赞功能
- 在数据库和API中添加likes字段支持 - 新增评论点赞接口和前端交互逻辑 - 在前端展示点赞数并处理点赞操作 - 更新相关组件和存储逻辑以支持点赞功能
This commit is contained in:
@@ -105,6 +105,28 @@ export class CommentItem extends Component {
|
||||
},
|
||||
text: '回复'
|
||||
}),
|
||||
this.createElement('span', {
|
||||
className: 'cwd-comment-like',
|
||||
children: [
|
||||
this.createElement('button', {
|
||||
className: 'cwd-comment-like-button',
|
||||
attributes: {
|
||||
type: 'button',
|
||||
onClick: () => this.handleLikeComment()
|
||||
},
|
||||
text: '赞'
|
||||
}),
|
||||
this.createTextElement(
|
||||
'span',
|
||||
String(
|
||||
typeof comment.likes === 'number' && Number.isFinite(comment.likes) && comment.likes >= 0
|
||||
? comment.likes
|
||||
: 0
|
||||
),
|
||||
'cwd-comment-like-count'
|
||||
)
|
||||
]
|
||||
}),
|
||||
this.createTextElement('span', formatRelativeTime(comment.created), 'cwd-comment-time')
|
||||
]
|
||||
})
|
||||
@@ -259,6 +281,12 @@ export class CommentItem extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleLikeComment() {
|
||||
if (this.props.onLikeComment) {
|
||||
this.props.onLikeComment(this.props.comment.id);
|
||||
}
|
||||
}
|
||||
|
||||
handleSubmitReply() {
|
||||
if (this.props.onSubmitReply) {
|
||||
this.props.onSubmitReply(this.props.comment.id);
|
||||
|
||||
@@ -100,7 +100,8 @@ export class CommentList extends Component {
|
||||
onSubmitReply: (commentId) => this.handleSubmitReply(commentId),
|
||||
onCancelReply: () => this.handleCancelReply(),
|
||||
onUpdateReplyContent: (content) => this.handleUpdateReplyContent(content),
|
||||
onClearReplyError: () => this.handleClearReplyError()
|
||||
onClearReplyError: () => this.handleClearReplyError(),
|
||||
onLikeComment: (commentId) => this.handleLikeComment(commentId)
|
||||
});
|
||||
commentItem.render();
|
||||
// 缓存 CommentItem 实例
|
||||
@@ -219,6 +220,12 @@ export class CommentList extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleLikeComment(commentId) {
|
||||
if (this.props.onLikeComment) {
|
||||
this.props.onLikeComment(commentId);
|
||||
}
|
||||
}
|
||||
|
||||
handlePrevPage() {
|
||||
if (this.props.onPrevPage) {
|
||||
this.props.onPrevPage();
|
||||
|
||||
@@ -176,7 +176,8 @@ export class CWDComments {
|
||||
this.store = createCommentStore(
|
||||
this.config,
|
||||
api.fetchComments.bind(api),
|
||||
api.submitComment.bind(api)
|
||||
api.submitComment.bind(api),
|
||||
typeof api.likeComment === 'function' ? api.likeComment.bind(api) : undefined
|
||||
);
|
||||
|
||||
this.unsubscribe = this.store.store.subscribe((state) => {
|
||||
@@ -379,6 +380,11 @@ export class CWDComments {
|
||||
onPrevPage: () => this.store.goToPage(state.pagination.page - 1),
|
||||
onNextPage: () => this.store.goToPage(state.pagination.page + 1),
|
||||
onGoToPage: (page) => this.store.goToPage(page),
|
||||
onLikeComment: (commentId) => {
|
||||
if (this.store && typeof this.store.likeComment === 'function') {
|
||||
this.store.likeComment(commentId);
|
||||
}
|
||||
},
|
||||
});
|
||||
this.commentList.render();
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
* @param {string} config.postSlug - 文章标识符
|
||||
* @param {string} config.postTitle - 文章标题(可选)
|
||||
* @param {string} config.postUrl - 文章 URL(可选)
|
||||
* @returns {Object}
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function createApiClient(config) {
|
||||
const baseUrl = config.apiBaseUrl.replace(/\/$/, '');
|
||||
@@ -178,12 +178,41 @@ export function createApiClient(config) {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
return {
|
||||
async function likeComment(commentId) {
|
||||
const id =
|
||||
typeof commentId === 'number'
|
||||
? commentId
|
||||
: typeof commentId === 'string' && commentId.trim()
|
||||
? Number.parseInt(commentId.trim(), 10)
|
||||
: NaN;
|
||||
if (!Number.isFinite(id) || id <= 0) {
|
||||
throw new Error('Invalid comment id');
|
||||
}
|
||||
const response = await fetch(`${baseUrl}/api/comments/like`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ id })
|
||||
});
|
||||
if (!response.ok) {
|
||||
let msg = response.statusText;
|
||||
try {
|
||||
const json = await response.json();
|
||||
if (json.message) msg = json.message;
|
||||
} catch (e) {}
|
||||
throw new Error(msg);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
return {
|
||||
fetchComments,
|
||||
submitComment,
|
||||
verifyAdminKey,
|
||||
trackVisit,
|
||||
getLikeStatus,
|
||||
likePage
|
||||
likePage,
|
||||
likeComment
|
||||
};
|
||||
}
|
||||
|
||||
@@ -90,9 +90,10 @@ class Store {
|
||||
* @param {Object} config - 配置对象
|
||||
* @param {Function} fetchComments - 获取评论的函数
|
||||
* @param {Function} submitComment - 提交评论的函数
|
||||
* @param {Function} likeCommentFn - 点赞评论的函数
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function createCommentStore(config, fetchComments, submitComment) {
|
||||
export function createCommentStore(config, fetchComments, submitComment, likeCommentFn) {
|
||||
// 从 localStorage 加载用户信息
|
||||
const savedInfo = loadUserInfo();
|
||||
|
||||
@@ -128,6 +129,7 @@ export function createCommentStore(config, fetchComments, submitComment) {
|
||||
replyError: null,
|
||||
likeCount: 0,
|
||||
liked: false,
|
||||
commentLikeLoadingId: null,
|
||||
});
|
||||
|
||||
// 监听用户信息变化,自动保存到 localStorage
|
||||
@@ -175,6 +177,78 @@ export function createCommentStore(config, fetchComments, submitComment) {
|
||||
});
|
||||
}
|
||||
|
||||
async function likeComment(commentId) {
|
||||
const state = store.getState();
|
||||
if (!likeCommentFn || state.commentLikeLoadingId === commentId) {
|
||||
return;
|
||||
}
|
||||
const id =
|
||||
typeof commentId === 'number'
|
||||
? commentId
|
||||
: typeof commentId === 'string' && commentId.trim()
|
||||
? Number.parseInt(commentId.trim(), 10)
|
||||
: NaN;
|
||||
if (!Number.isFinite(id) || id <= 0) {
|
||||
return;
|
||||
}
|
||||
store.setState({
|
||||
commentLikeLoadingId: id,
|
||||
});
|
||||
try {
|
||||
const safeComments = Array.isArray(state.comments) ? state.comments : [];
|
||||
const nextComments = safeComments.map((item) => {
|
||||
if (!item || typeof item.id !== 'number') {
|
||||
return item;
|
||||
}
|
||||
if (item.id === id) {
|
||||
const current =
|
||||
typeof item.likes === 'number' && Number.isFinite(item.likes) && item.likes >= 0
|
||||
? item.likes
|
||||
: 0;
|
||||
return {
|
||||
...item,
|
||||
likes: current + 1,
|
||||
};
|
||||
}
|
||||
if (Array.isArray(item.replies) && item.replies.length > 0) {
|
||||
const updatedReplies = item.replies.map((reply) => {
|
||||
if (!reply || typeof reply.id !== 'number') {
|
||||
return reply;
|
||||
}
|
||||
if (reply.id === id) {
|
||||
const current =
|
||||
typeof reply.likes === 'number' && Number.isFinite(reply.likes) && reply.likes >= 0
|
||||
? reply.likes
|
||||
: 0;
|
||||
return {
|
||||
...reply,
|
||||
likes: current + 1,
|
||||
};
|
||||
}
|
||||
return reply;
|
||||
});
|
||||
return {
|
||||
...item,
|
||||
replies: updatedReplies,
|
||||
};
|
||||
}
|
||||
return item;
|
||||
});
|
||||
store.setState({
|
||||
comments: nextComments,
|
||||
});
|
||||
await likeCommentFn(id);
|
||||
} catch (e) {
|
||||
} finally {
|
||||
const latest = store.getState();
|
||||
if (latest.commentLikeLoadingId === id) {
|
||||
store.setState({
|
||||
commentLikeLoadingId: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交评论
|
||||
*/
|
||||
@@ -395,5 +469,6 @@ export function createCommentStore(config, fetchComments, submitComment) {
|
||||
clearSuccess,
|
||||
goToPage,
|
||||
setLikeState,
|
||||
likeComment,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user