feat(comment): 添加歌单详情页评论回复与展开功能 - 实现评论回复输入与提交功能,支持@用户名 - 添加回复展开与收起交互,支持查看子评论 - 使用 WebSocket 接收新评论与回复,并处理用户头像图片 - 优化评论展示结构,区分主评论与子评论
This commit is contained in:
@@ -7,6 +7,7 @@ import { ElMessage, ElMessageBox } from 'element-plus'
|
|||||||
import { getPlaylistById, toggleCollectPlaylist, deletePlaylist as apiDeletePlaylist } from '../api/playlist'
|
import { getPlaylistById, toggleCollectPlaylist, deletePlaylist as apiDeletePlaylist } from '../api/playlist'
|
||||||
import { getCommentList, addComment, deleteComment as apiDeleteComment } from '../api/comment'
|
import { getCommentList, addComment, deleteComment as apiDeleteComment } from '../api/comment'
|
||||||
import { connect, subscribe, unsubscribe, disconnect } from '../utils/websocket' // 导入 WebSocket 工具
|
import { connect, subscribe, unsubscribe, disconnect } from '../utils/websocket' // 导入 WebSocket 工具
|
||||||
|
import { processImageUrl } from '../utils/imageUtils'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -25,6 +26,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)
|
||||||
|
|
||||||
@@ -158,15 +171,48 @@ const deleteComment = async (commentId) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 切换回复可见性
|
||||||
|
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
|
||||||
}
|
}
|
||||||
@@ -175,14 +221,17 @@ const submitComment = async () => {
|
|||||||
const res = await addComment({
|
const res = await addComment({
|
||||||
targetId: playlistId.value,
|
targetId: playlistId.value,
|
||||||
targetType: 2, // 2表示歌单
|
targetType: 2, // 2表示歌单
|
||||||
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 || '评论失败')
|
||||||
}
|
}
|
||||||
@@ -259,15 +308,26 @@ onMounted(() => {
|
|||||||
connect(() => {
|
connect(() => {
|
||||||
const destination = `/topic/comments/2/${playlistId.value}`; // 2 代表歌单类型
|
const destination = `/topic/comments/2/${playlistId.value}`; // 2 代表歌单类型
|
||||||
commentSubscription = subscribe(destination, (newComment) => {
|
commentSubscription = subscribe(destination, (newComment) => {
|
||||||
// 收到新评论,添加到列表开头
|
|
||||||
if (newComment && newComment.id) {
|
if (newComment && newComment.id) {
|
||||||
// 检查是否已存在(防止重复添加,虽然理论上不应发生)
|
if (newComment.user && newComment.user.avatar) {
|
||||||
if (!comments.value.some(c => c.id === newComment.id)) {
|
newComment.user.avatar = processImageUrl(newComment.user.avatar);
|
||||||
comments.value.unshift(newComment); // 添加到开头
|
}
|
||||||
console.log('New comment received via WebSocket:', newComment);
|
|
||||||
}
|
if (newComment.parentId) {
|
||||||
|
const parentComment = comments.value.find(c => c.id === newComment.parentId);
|
||||||
|
if (parentComment) {
|
||||||
|
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) => {
|
||||||
@@ -413,7 +473,9 @@ 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>
|
||||||
|
|
||||||
<!-- 评论列表 -->
|
<!-- 评论列表 -->
|
||||||
@@ -426,6 +488,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"
|
||||||
@@ -437,7 +500,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>
|
||||||
|
|||||||
Reference in New Issue
Block a user