feat(comment): 添加歌手详情页评论回复功能
- 新增回复评论功能,支持嵌套回复 - 增加展开/收起子评论的功能 - 优化评论列表展示,支持子评论显示- 调整评论输入框和提交按钮布局 - 优化评论样式,区分主评论和子评论
This commit is contained in:
@@ -32,6 +32,18 @@ const comments = ref([])
|
|||||||
// 评论内容
|
// 评论内容
|
||||||
const commentContent = ref('')
|
const commentContent = ref('')
|
||||||
|
|
||||||
|
// 回复内容
|
||||||
|
const replyContent = ref('')
|
||||||
|
|
||||||
|
// 回复对象
|
||||||
|
const replyTo = ref(null)
|
||||||
|
|
||||||
|
// 正在回复的评论
|
||||||
|
const replyingTo = ref(null)
|
||||||
|
|
||||||
|
// 展开的评论
|
||||||
|
const expandedComments = ref([])
|
||||||
|
|
||||||
// 加载状态
|
// 加载状态
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
@@ -178,15 +190,48 @@ const toggleCollect = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 切换回复可见性
|
||||||
|
const toggleReplies = (comment) => {
|
||||||
|
const commentId = comment.id;
|
||||||
|
const index = expandedComments.value.indexOf(commentId);
|
||||||
|
if (index > -1) {
|
||||||
|
expandedComments.value.splice(index, 1);
|
||||||
|
if (replyingTo.value === commentId) {
|
||||||
|
cancelReply();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
expandedComments.value.push(commentId);
|
||||||
|
setReplyTo(comment);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 设置回复
|
||||||
|
const setReplyTo = (comment) => {
|
||||||
|
replyingTo.value = comment.id;
|
||||||
|
replyTo.value = comment;
|
||||||
|
replyContent.value = `@${comment.user.username} `;
|
||||||
|
if (!expandedComments.value.includes(comment.id)) {
|
||||||
|
expandedComments.value.push(comment.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消回复
|
||||||
|
const cancelReply = () => {
|
||||||
|
replyingTo.value = null
|
||||||
|
replyTo.value = null
|
||||||
|
replyContent.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
// 提交评论
|
// 提交评论
|
||||||
const submitComment = async () => {
|
const submitComment = async (isReply = false) => {
|
||||||
if (!userStore.isLoggedIn) {
|
if (!userStore.isLoggedIn) {
|
||||||
ElMessage.warning('请先登录')
|
ElMessage.warning('请先登录')
|
||||||
router.push('/login')
|
router.push('/login')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!commentContent.value.trim()) {
|
const content = isReply ? replyContent.value : commentContent.value
|
||||||
|
if (!content.trim()) {
|
||||||
ElMessage.warning('评论内容不能为空')
|
ElMessage.warning('评论内容不能为空')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -195,14 +240,17 @@ const submitComment = async () => {
|
|||||||
const res = await addComment({
|
const res = await addComment({
|
||||||
targetId: singerId.value,
|
targetId: singerId.value,
|
||||||
targetType: 3, // 3表示歌手
|
targetType: 3, // 3表示歌手
|
||||||
content: commentContent.value
|
content: content,
|
||||||
|
parentId: isReply ? replyingTo.value : null
|
||||||
})
|
})
|
||||||
|
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
// 添加成功,评论将通过 WebSocket 推送,无需手动刷新
|
if (isReply) {
|
||||||
// await fetchComments() // 移除手动刷新
|
replyContent.value = ''
|
||||||
commentContent.value = ''
|
} else {
|
||||||
ElMessage.success('评论已发送') // 提示语可以调整
|
commentContent.value = ''
|
||||||
|
}
|
||||||
|
ElMessage.success('评论已发送')
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(res.message || '评论失败')
|
ElMessage.error(res.message || '评论失败')
|
||||||
}
|
}
|
||||||
@@ -283,19 +331,26 @@ onMounted(() => {
|
|||||||
connect(() => {
|
connect(() => {
|
||||||
const destination = `/topic/comments/3/${singerId.value}`; // 3 代表歌手类型
|
const destination = `/topic/comments/3/${singerId.value}`; // 3 代表歌手类型
|
||||||
commentSubscription = subscribe(destination, (newComment) => {
|
commentSubscription = subscribe(destination, (newComment) => {
|
||||||
// 收到新评论,添加到列表开头
|
|
||||||
if (newComment && newComment.id) {
|
if (newComment && newComment.id) {
|
||||||
// 处理新评论的用户头像
|
if (newComment.user && newComment.user.avatar) {
|
||||||
if (newComment.user && newComment.user.avatar) {
|
newComment.user.avatar = processImageUrl(newComment.user.avatar);
|
||||||
newComment.user.avatar = processImageUrl(newComment.user.avatar);
|
}
|
||||||
}
|
|
||||||
// 检查是否已存在
|
if (newComment.parentId) {
|
||||||
if (!comments.value.some(c => c.id === newComment.id)) {
|
const parentComment = comments.value.find(c => c.id === newComment.parentId);
|
||||||
comments.value.unshift(newComment); // 添加到开头
|
if (parentComment) {
|
||||||
console.log('New comment received via WebSocket:', newComment);
|
if (!parentComment.children) {
|
||||||
}
|
parentComment.children = [];
|
||||||
|
}
|
||||||
|
parentComment.children.push(newComment);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!comments.value.some(c => c.id === newComment.id)) {
|
||||||
|
comments.value.unshift(newComment);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn("Received invalid comment data via WebSocket:", newComment);
|
console.warn("Received invalid comment data via WebSocket:", newComment);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, (error) => {
|
}, (error) => {
|
||||||
@@ -431,10 +486,11 @@ onUnmounted(() => {
|
|||||||
:rows="3"
|
:rows="3"
|
||||||
placeholder="发表你的评论..."
|
placeholder="发表你的评论..."
|
||||||
/>
|
/>
|
||||||
<el-button type="primary" @click="submitComment">发表评论</el-button>
|
<div class="comment-submit-actions">
|
||||||
|
<el-button type="primary" @click="submitComment(false)">发表评论</el-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 评论列表 -->
|
|
||||||
<!-- 评论列表 -->
|
<!-- 评论列表 -->
|
||||||
<div class="comment-list">
|
<div class="comment-list">
|
||||||
<div v-for="comment in comments" :key="comment.id" class="comment-item">
|
<div v-for="comment in comments" :key="comment.id" class="comment-item">
|
||||||
@@ -445,6 +501,7 @@ onUnmounted(() => {
|
|||||||
<div class="comment-header">
|
<div class="comment-header">
|
||||||
<div class="comment-user">{{ comment.user.username }}</div>
|
<div class="comment-user">{{ comment.user.username }}</div>
|
||||||
<div class="comment-actions">
|
<div class="comment-actions">
|
||||||
|
<el-button type="text" size="small" @click="setReplyTo(comment)">回复</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
v-if="userStore.isAdmin || comment.user.id === userStore.user?.id"
|
v-if="userStore.isAdmin || comment.user.id === userStore.user?.id"
|
||||||
type="danger"
|
type="danger"
|
||||||
@@ -456,7 +513,55 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="comment-text">{{ comment.content }}</div>
|
<div class="comment-text">{{ comment.content }}</div>
|
||||||
<div class="comment-time">{{ comment.createTime }}</div>
|
<div class="comment-footer">
|
||||||
|
<div class="comment-time">{{ comment.createTime }}</div>
|
||||||
|
<el-button
|
||||||
|
v-if="comment.children && comment.children.length"
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
@click="toggleReplies(comment)"
|
||||||
|
>
|
||||||
|
{{ expandedComments.includes(comment.id) ? '收起回复' : `查看 ${comment.children.length} 条回复` }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 子评论 -->
|
||||||
|
<div v-if="expandedComments.includes(comment.id)" class="child-comment-list">
|
||||||
|
<!-- 回复输入框 -->
|
||||||
|
<div v-if="replyingTo === comment.id" class="comment-input child-comment-input">
|
||||||
|
<el-input
|
||||||
|
v-model="replyContent"
|
||||||
|
type="textarea"
|
||||||
|
:rows="2"
|
||||||
|
:placeholder="`回复 @${replyTo.user.username}`"
|
||||||
|
/>
|
||||||
|
<div class="comment-submit-actions">
|
||||||
|
<el-button type="primary" @click="submitComment(true)">回复</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-for="child in [...(comment.children || [])].reverse()" :key="child.id" class="comment-item">
|
||||||
|
<div class="comment-avatar">
|
||||||
|
<el-avatar :src="child.user.avatar" :size="30" />
|
||||||
|
</div>
|
||||||
|
<div class="comment-content">
|
||||||
|
<div class="comment-header">
|
||||||
|
<div class="comment-user">{{ child.user.username }}</div>
|
||||||
|
<div class="comment-actions">
|
||||||
|
<el-button
|
||||||
|
v-if="userStore.isAdmin || child.user.id === userStore.user?.id"
|
||||||
|
type="danger"
|
||||||
|
size="small"
|
||||||
|
@click="deleteComment(child.id)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="comment-text">{{ child.content }}</div>
|
||||||
|
<div class="comment-time">{{ child.createTime }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -578,7 +683,12 @@ onUnmounted(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
align-items: flex-end;
|
}
|
||||||
|
|
||||||
|
.comment-submit-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-list {
|
.comment-list {
|
||||||
@@ -611,13 +721,26 @@ onUnmounted(() => {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.child-comment-list {
|
||||||
|
margin-top: 15px;
|
||||||
|
padding-left: 15px;
|
||||||
|
border-left: 2px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.child-comment-input {
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
.comment-text {
|
.comment-text {
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
color: #333;
|
color: #333;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-time {
|
.comment-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: #999;
|
color: #999;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user