feat(comment): 优化评论加载效率并添加回复功能

- 在 CommentRepository 中添加 findByParentIdInOrderByCreateTimeAsc 方法,实现批量查询子评论
- 重构 CommentServiceImpl 中的 findCommentsByMusicId 方法,提升评论加载效率
- 在前端 MusicDetail 组件中增加回复功能,支持二级评论和回复展开/收起
- 优化 WebSocket订阅逻辑,实现实时更新评论列表
This commit is contained in:
ikmkj
2025-08-10 16:23:12 +08:00
parent e30011cee2
commit a36ef7a561
3 changed files with 128 additions and 16 deletions

View File

@@ -34,6 +34,14 @@ public interface CommentRepository extends JpaRepository<Comment, Long>, JpaSpec
*/ */
List<Comment> findByParentIdOrderByCreateTimeAsc(Long parentId); List<Comment> findByParentIdOrderByCreateTimeAsc(Long parentId);
/**
* 根据父评论ID列表查询所有子评论
*
* @param parentIds 父评论ID列表
* @return 子评论列表
*/
List<Comment> findByParentIdInOrderByCreateTimeAsc(List<Long> parentIds);
/** /**
* 根据用户查询评论 * 根据用户查询评论
* *

View File

@@ -74,16 +74,28 @@ public class CommentServiceImpl implements CommentService {
.map(this::convertToDTO) .map(this::convertToDTO)
.collect(Collectors.toList()); .collect(Collectors.toList());
// 获取子评论 // 优化:一次性获取所有子评论
for (CommentDTO commentDTO : commentDTOs) { // 1. 获取所有父评论的ID
List<Comment> children = commentRepository.findByParentIdOrderByCreateTimeAsc(commentDTO.getId()); List<Long> parentIds = commentDTOs.stream()
if (!children.isEmpty()) { .map(CommentDTO::getId)
commentDTO.setChildren(children.stream() .collect(Collectors.toList());
.map(this::convertToDTO)
.collect(Collectors.toList())); // 2. 如果有父评论,则一次性查询出它们所有的子评论
} else { if (!parentIds.isEmpty()) {
commentDTO.setChildren(new ArrayList<>()); List<Comment> allChildren = commentRepository.findByParentIdInOrderByCreateTimeAsc(parentIds);
}
// 3. 将子评论按其父ID分组
java.util.Map<Long, List<CommentDTO>> childrenMap = allChildren.stream()
.map(this::convertToDTO)
.collect(Collectors.groupingBy(CommentDTO::getParentId));
// 4. 将子评论列表设置到对应的父评论DTO中
commentDTOs.forEach(parentDTO -> parentDTO.setChildren(
childrenMap.getOrDefault(parentDTO.getId(), new ArrayList<>())
));
} else {
// 如果没有父评论确保children字段不是null
commentDTOs.forEach(parentDTO -> parentDTO.setChildren(new ArrayList<>()));
} }
return new PageResult<>( return new PageResult<>(

View File

@@ -129,8 +129,9 @@
<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">
<!-- 一级评论 -->
<div class="comment-avatar"> <div class="comment-avatar">
<el-avatar :src="comment.user.avatar" :size="40" /> <el-avatar :src="processImageUrl(comment.user.avatar)" :size="40" />
</div> </div>
<div class="comment-content"> <div class="comment-content">
<div class="comment-header"> <div class="comment-header">
@@ -150,19 +151,69 @@
<div class="comment-text">{{ comment.content }}</div> <div class="comment-text">{{ comment.content }}</div>
<div class="comment-footer"> <div class="comment-footer">
<div class="comment-time">{{ comment.createTime }}</div> <div class="comment-time">{{ comment.createTime }}</div>
<el-button type="text" size="small" @click="toggleReplies(comment)">
{{ expandedComments.includes(comment.id) ? '收起回复' : `查看回复 (${comment.children ? comment.children.length : 0})` }}
</el-button>
</div> </div>
<div v-if="replyingTo && replyingTo.id === comment.id" class="comment-input child-comment-input">
<!-- 回复输入框 -->
<div v-if="replyingTo === comment.id" class="comment-input child-comment-input">
<el-input <el-input
v-model="replyContent" v-model="replyContent"
type="textarea" type="textarea"
:rows="2" :rows="2"
:placeholder="`回复 @${replyingTo.user.username}`" :placeholder="`回复 @${replyTo.user.username}`"
/> />
<div class="comment-submit-actions"> <div class="comment-submit-actions">
<el-button type="primary" size="small" @click="submitComment(true)">回复</el-button> <el-button type="primary" size="small" @click="submitComment(true)">回复</el-button>
<el-button size="small" @click="cancelReply">取消</el-button> <el-button size="small" @click="cancelReply">取消</el-button>
</div> </div>
</div> </div>
<!-- 二级评论 -->
<div v-if="expandedComments.includes(comment.id) && comment.children && comment.children.length > 0" class="child-comments">
<div v-for="child in comment.children" :key="child.id" class="comment-item">
<div class="comment-avatar">
<el-avatar :src="processImageUrl(child.user.avatar)" :size="32" />
</div>
<div class="comment-content">
<div class="comment-header">
<div class="comment-user">{{ child.user.username }}</div>
<div class="comment-actions">
<el-button type="text" size="small" @click="setReplyTo(child)">回复</el-button>
<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">
<span v-if="child.parentCommentUser" class="reply-to">@{{ child.parentCommentUser.username }}</span>
{{ child.content }}
</div>
<div class="comment-footer">
<div class="comment-time">{{ child.createTime }}</div>
</div>
<!-- 二级回复输入框 -->
<div v-if="replyingTo === child.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" size="small" @click="submitComment(true, child.id)">回复</el-button>
<el-button size="small" @click="cancelReply">取消</el-button>
</div>
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
<el-empty v-if="!comments.length" description="还没有评论,快来抢沙发吧!" /> <el-empty v-if="!comments.length" description="还没有评论,快来抢沙发吧!" />
@@ -210,6 +261,7 @@ import { getCommentList, addComment, deleteComment as apiDeleteComment } from '.
import { connect, subscribe, unsubscribe, disconnect } from '../utils/websocket' import { connect, subscribe, unsubscribe, disconnect } from '../utils/websocket'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { VideoPlay, VideoPause, Star, StarFilled, Plus } from '@element-plus/icons-vue' import { VideoPlay, VideoPause, Star, StarFilled, Plus } from '@element-plus/icons-vue'
import { processImageUrl } from '../utils/imageUtils'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
@@ -226,6 +278,7 @@ const comments = ref([])
const commentContent = ref('') const commentContent = ref('')
const replyContent = ref('') const replyContent = ref('')
const replyingTo = ref(null) const replyingTo = ref(null)
const replyTo = ref(null)
const expandedComments = ref([]) const expandedComments = ref([])
let commentSubscription = null; let commentSubscription = null;
@@ -398,15 +451,34 @@ const deleteComment = async (commentId) => {
// 设置回复对象 // 设置回复对象
const setReplyTo = (comment) => { const setReplyTo = (comment) => {
replyingTo.value = comment; replyingTo.value = comment.id;
replyTo.value = comment;
if (!expandedComments.value.includes(comment.id)) {
expandedComments.value.push(comment.id);
}
}; };
// 取消回复 // 取消回复
const cancelReply = () => { const cancelReply = () => {
replyingTo.value = null; replyingTo.value = null;
replyTo.value = null;
replyContent.value = ''; replyContent.value = '';
}; };
// 切换回复可见性
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);
}
};
// WebSocket 订阅 // WebSocket 订阅
const setupWebSocket = () => { const setupWebSocket = () => {
if (!music.value) return; if (!music.value) return;
@@ -414,10 +486,30 @@ const setupWebSocket = () => {
const destination = `/topic/comments/1/${music.value.id}`; // 1 代表音乐类型 const destination = `/topic/comments/1/${music.value.id}`; // 1 代表音乐类型
commentSubscription = subscribe(destination, (newComment) => { commentSubscription = subscribe(destination, (newComment) => {
if (newComment && newComment.id) { if (newComment && newComment.id) {
// 简单处理:直接重新拉取整个评论列表 if (newComment.user && newComment.user.avatar) {
fetchComments(); newComment.user.avatar = processImageUrl(newComment.user.avatar);
}
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 {
console.warn("Received invalid comment data via WebSocket:", newComment);
} }
}); });
}, (error) => {
console.error("WebSocket connection error:", error);
// ElMessage.error('评论服务连接失败,实时更新不可用');
}); });
}; };