feat(comment): 添加音乐评论功能并优化热门音乐相关逻辑
- 在 CommentServiceImpl 中增加针对音乐的评论计数逻辑 - 在 Home.vue 中添加热门音乐推荐模块 - 修改 Music.vue 中的热门音乐获取逻辑,支持分页 - 在 MusicDetail.vue 中实现评论列表展示和评论提交功能 - 更新 MusicList.vue,增加音乐详情查看按钮 - 在 MusicServiceImpl 中同步更新播放次数和点赞数
This commit is contained in:
@@ -88,12 +88,77 @@
|
||||
<p>{{ music.description }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="music && music.lyric" class="music-lyric">
|
||||
<h2>歌词</h2>
|
||||
<pre>{{ music.lyric }}</pre>
|
||||
<div class="content-toggle">
|
||||
<el-tabs v-model="activeTab">
|
||||
<el-tab-pane label="歌词" name="lyric"></el-tab-pane>
|
||||
<el-tab-pane label="评论" name="comment"></el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<div v-if="relatedMusic.length > 0" class="related-music">
|
||||
<div v-if="activeTab === 'lyric'">
|
||||
<div v-if="music && music.lyric" class="music-lyric">
|
||||
<h2>歌词</h2>
|
||||
<pre>{{ music.lyric }}</pre>
|
||||
</div>
|
||||
<el-empty v-else description="暂无歌词" />
|
||||
</div>
|
||||
|
||||
<div v-if="activeTab === 'comment'" class="comment-section">
|
||||
<div class="comment-input">
|
||||
<el-input
|
||||
v-model="commentContent"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="发表你的评论..."
|
||||
/>
|
||||
<div class="comment-submit-actions">
|
||||
<el-button type="primary" @click="submitComment(false)">发表评论</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="comment-list">
|
||||
<div v-for="comment in comments" :key="comment.id" class="comment-item">
|
||||
<div class="comment-avatar">
|
||||
<el-avatar :src="comment.user.avatar" :size="40" />
|
||||
</div>
|
||||
<div class="comment-content">
|
||||
<div class="comment-header">
|
||||
<div class="comment-user">{{ comment.user.username }}</div>
|
||||
<div class="comment-actions">
|
||||
<el-button type="text" size="small" @click="setReplyTo(comment)">回复</el-button>
|
||||
<el-button
|
||||
v-if="userStore.isAdmin || comment.user.id === userStore.user?.id"
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="deleteComment(comment.id)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="comment-text">{{ comment.content }}</div>
|
||||
<div class="comment-footer">
|
||||
<div class="comment-time">{{ comment.createTime }}</div>
|
||||
</div>
|
||||
<div v-if="replyingTo && replyingTo.id === comment.id" class="comment-input child-comment-input">
|
||||
<el-input
|
||||
v-model="replyContent"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
:placeholder="`回复 @${replyingTo.user.username}`"
|
||||
/>
|
||||
<div class="comment-submit-actions">
|
||||
<el-button type="primary" size="small" @click="submitComment(true)">回复</el-button>
|
||||
<el-button size="small" @click="cancelReply">取消</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-if="!comments.length" description="还没有评论,快来抢沙发吧!" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="relatedMusic.length > 0 && activeTab === 'lyric'" class="related-music">
|
||||
<h2>相关推荐</h2>
|
||||
<el-row :gutter="20">
|
||||
<el-col
|
||||
@@ -125,21 +190,34 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { ref, computed, onMounted, watch, onUnmounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { usePlayerStore } from '../stores/player'
|
||||
import { useUserStore } from '../stores/user'
|
||||
import { getMusicById, toggleCollectMusic, getHotMusic } from '../api/music'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getCommentList, addComment, deleteComment as apiDeleteComment } from '../api/comment'
|
||||
import { connect, subscribe, unsubscribe, disconnect } from '../utils/websocket'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { VideoPlay, VideoPause, Star, StarFilled, Plus } from '@element-plus/icons-vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const playerStore = usePlayerStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const music = ref(null)
|
||||
const loading = ref(true)
|
||||
const relatedMusic = ref([])
|
||||
|
||||
// 评论区相关状态
|
||||
const activeTab = ref('lyric') // 'lyric' or 'comment'
|
||||
const comments = ref([])
|
||||
const commentContent = ref('')
|
||||
const replyContent = ref('')
|
||||
const replyingTo = ref(null)
|
||||
const expandedComments = ref([])
|
||||
let commentSubscription = null;
|
||||
|
||||
// 计算属性
|
||||
const isPlaying = computed(() => {
|
||||
return playerStore.getCurrentMusic?.id === music.value?.id && playerStore.getIsPlaying
|
||||
@@ -191,6 +269,117 @@ const fetchRelatedMusic = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取评论列表
|
||||
const fetchComments = async () => {
|
||||
if (!music.value) return;
|
||||
try {
|
||||
const res = await getCommentList(music.value.id, 1, 1, 100); // targetType 1 表示音乐
|
||||
if (res.code === 200) {
|
||||
comments.value = res.data.list || res.data.records || [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取评论列表失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 提交评论
|
||||
const submitComment = async (isReply = false) => {
|
||||
if (!userStore.isLoggedIn) {
|
||||
ElMessage.warning('请先登录');
|
||||
router.push('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
const content = isReply ? replyContent.value : commentContent.value;
|
||||
if (!content.trim()) {
|
||||
ElMessage.warning('评论内容不能为空');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await addComment({
|
||||
targetId: music.value.id,
|
||||
targetType: 1, // 1表示音乐
|
||||
content: content,
|
||||
parentId: isReply ? replyingTo.value.id : null,
|
||||
});
|
||||
|
||||
if (res.code === 200) {
|
||||
if (isReply) {
|
||||
replyContent.value = '';
|
||||
replyingTo.value = null;
|
||||
} else {
|
||||
commentContent.value = '';
|
||||
}
|
||||
ElMessage.success('评论已发送');
|
||||
} else {
|
||||
ElMessage.error(res.message || '评论失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交评论失败:', error);
|
||||
ElMessage.error('评论失败,请稍后重试');
|
||||
}
|
||||
};
|
||||
|
||||
// 删除评论
|
||||
const deleteComment = async (commentId) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除这条评论吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
});
|
||||
|
||||
const res = await apiDeleteComment(commentId);
|
||||
if (res.code === 200) {
|
||||
// 重新获取评论以刷新列表
|
||||
fetchComments();
|
||||
ElMessage.success('评论已删除');
|
||||
} else {
|
||||
ElMessage.error(res.message || '删除失败');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除评论失败:', error);
|
||||
ElMessage.error('删除失败,请稍后重试');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 设置回复对象
|
||||
const setReplyTo = (comment) => {
|
||||
replyingTo.value = comment;
|
||||
};
|
||||
|
||||
// 取消回复
|
||||
const cancelReply = () => {
|
||||
replyingTo.value = null;
|
||||
replyContent.value = '';
|
||||
};
|
||||
|
||||
// WebSocket 订阅
|
||||
const setupWebSocket = () => {
|
||||
if (!music.value) return;
|
||||
connect(() => {
|
||||
const destination = `/topic/comments/1/${music.value.id}`; // 1 代表音乐类型
|
||||
commentSubscription = subscribe(destination, (newComment) => {
|
||||
if (newComment && newComment.id) {
|
||||
// 简单处理:直接重新拉取整个评论列表
|
||||
fetchComments();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 断开 WebSocket
|
||||
const teardownWebSocket = () => {
|
||||
if (commentSubscription) {
|
||||
unsubscribe(commentSubscription);
|
||||
commentSubscription = null;
|
||||
}
|
||||
disconnect();
|
||||
};
|
||||
|
||||
// 播放音乐
|
||||
const playMusic = () => {
|
||||
if (isPlaying.value) {
|
||||
@@ -255,14 +444,25 @@ const formatDuration = (seconds) => {
|
||||
// 监听路由变化,重新获取音乐详情
|
||||
watch(() => route.params.id, (newId, oldId) => {
|
||||
if (newId !== oldId) {
|
||||
fetchMusicDetail()
|
||||
fetchRelatedMusic()
|
||||
teardownWebSocket();
|
||||
fetchMusicDetail().then(() => {
|
||||
fetchComments();
|
||||
setupWebSocket();
|
||||
});
|
||||
fetchRelatedMusic();
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
fetchMusicDetail()
|
||||
fetchRelatedMusic()
|
||||
fetchMusicDetail().then(() => {
|
||||
fetchComments();
|
||||
setupWebSocket();
|
||||
});
|
||||
fetchRelatedMusic();
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
teardownWebSocket();
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -330,10 +530,79 @@ onMounted(() => {
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.music-description, .music-lyric {
|
||||
.music-description {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.content-toggle {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.comment-section {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.comment-input {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.comment-submit-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.comment-list {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.comment-item {
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.comment-avatar {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.comment-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.comment-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.comment-user {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.child-comment-input {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.comment-text {
|
||||
margin-bottom: 5px;
|
||||
color: #333;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.comment-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 0.8rem;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.music-description h2, .music-lyric h2, .related-music h2 {
|
||||
font-size: 20px;
|
||||
margin-bottom: 15px;
|
||||
|
||||
Reference in New Issue
Block a user