feat(comment): 添加音乐评论功能并优化热门音乐相关逻辑
- 在 CommentServiceImpl 中增加针对音乐的评论计数逻辑 - 在 Home.vue 中添加热门音乐推荐模块 - 修改 Music.vue 中的热门音乐获取逻辑,支持分页 - 在 MusicDetail.vue 中实现评论列表展示和评论提交功能 - 更新 MusicList.vue,增加音乐详情查看按钮 - 在 MusicServiceImpl 中同步更新播放次数和点赞数
This commit is contained in:
@@ -8,6 +8,7 @@ import com.test.musichouduan.entity.Comment;
|
|||||||
import com.test.musichouduan.entity.User;
|
import com.test.musichouduan.entity.User;
|
||||||
import com.test.musichouduan.exception.BusinessException;
|
import com.test.musichouduan.exception.BusinessException;
|
||||||
import com.test.musichouduan.repository.CommentRepository;
|
import com.test.musichouduan.repository.CommentRepository;
|
||||||
|
import com.test.musichouduan.repository.MusicRepository;
|
||||||
import com.test.musichouduan.repository.UserRepository;
|
import com.test.musichouduan.repository.UserRepository;
|
||||||
import com.test.musichouduan.service.CommentService;
|
import com.test.musichouduan.service.CommentService;
|
||||||
import org.springframework.messaging.simp.SimpMessagingTemplate; // 添加导入
|
import org.springframework.messaging.simp.SimpMessagingTemplate; // 添加导入
|
||||||
@@ -38,6 +39,9 @@ public class CommentServiceImpl implements CommentService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private UserRepository userRepository;
|
private UserRepository userRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MusicRepository musicRepository;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private SimpMessagingTemplate messagingTemplate; // 注入 SimpMessagingTemplate
|
private SimpMessagingTemplate messagingTemplate; // 注入 SimpMessagingTemplate
|
||||||
|
|
||||||
@@ -100,6 +104,16 @@ public class CommentServiceImpl implements CommentService {
|
|||||||
// 保存评论
|
// 保存评论
|
||||||
Comment savedComment = commentRepository.save(comment);
|
Comment savedComment = commentRepository.save(comment);
|
||||||
|
|
||||||
|
// 如果评论的是音乐 (targetType=1, 这个值需要与前端约定好)
|
||||||
|
// 通常可以定义一个枚举或常量类来管理这些类型值
|
||||||
|
if (savedComment.getTargetType() == 1) {
|
||||||
|
musicRepository.findById(savedComment.getTargetId()).ifPresent(music -> {
|
||||||
|
int currentCommentCount = music.getCommentCount() != null ? music.getCommentCount() : 0;
|
||||||
|
music.setCommentCount(currentCommentCount + 1);
|
||||||
|
musicRepository.save(music);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 将新评论转换为DTO
|
// 将新评论转换为DTO
|
||||||
CommentDTO savedCommentDTO = convertToDTO(savedComment);
|
CommentDTO savedCommentDTO = convertToDTO(savedComment);
|
||||||
|
|
||||||
|
|||||||
@@ -537,7 +537,10 @@ public class MusicServiceImpl implements MusicService {
|
|||||||
Music music = musicOpt.get();
|
Music music = musicOpt.get();
|
||||||
|
|
||||||
// 增加播放次数
|
// 增加播放次数
|
||||||
music.setPlayCount(music.getPlayCount() + 1);
|
long currentPlayCount = music.getPlayCount() != null ? music.getPlayCount() : 0;
|
||||||
|
music.setPlayCount(currentPlayCount + 1);
|
||||||
|
// 根据要求,将点赞数与播放次数同步
|
||||||
|
music.setLikeCount((int) (currentPlayCount + 1));
|
||||||
musicRepository.save(music);
|
musicRepository.save(music);
|
||||||
|
|
||||||
// 如果用户已登录,记录播放历史
|
// 如果用户已登录,记录播放历史
|
||||||
|
|||||||
@@ -65,7 +65,8 @@ export function getMusicById(id) {
|
|||||||
* @returns {Promise<any>}
|
* @returns {Promise<any>}
|
||||||
*/
|
*/
|
||||||
export function getHotMusic(limit = 10) {
|
export function getHotMusic(limit = 10) {
|
||||||
return api.get('/music/hot', {
|
// 更新:调用新的热门音乐接口
|
||||||
|
return api.get('/hot-music/list', {
|
||||||
params: { limit }
|
params: { limit }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,6 +72,15 @@
|
|||||||
<el-icon><Star /></el-icon>
|
<el-icon><Star /></el-icon>
|
||||||
</el-button>
|
</el-button>
|
||||||
|
|
||||||
|
<el-button
|
||||||
|
circle
|
||||||
|
size="small"
|
||||||
|
@click="goToMusicDetail(item.id)"
|
||||||
|
title="查看详情"
|
||||||
|
>
|
||||||
|
<el-icon><InfoFilled /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
|
||||||
<el-dropdown trigger="click" @command="handleCommand($event, item)">
|
<el-dropdown trigger="click" @command="handleCommand($event, item)">
|
||||||
<el-button circle size="small">
|
<el-button circle size="small">
|
||||||
<el-icon><More /></el-icon>
|
<el-icon><More /></el-icon>
|
||||||
@@ -105,7 +114,8 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import { VideoPlay, VideoPause, Star, More } from '@element-plus/icons-vue'
|
import { useRouter } from 'vue-router'
|
||||||
|
import { VideoPlay, VideoPause, Star, More, InfoFilled } from '@element-plus/icons-vue'
|
||||||
import { usePlayerStore } from '../stores/player'
|
import { usePlayerStore } from '../stores/player'
|
||||||
import { toggleCollectMusic } from '../api/music'
|
import { toggleCollectMusic } from '../api/music'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
@@ -139,6 +149,8 @@ const props = defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits(['update:currentPage', 'update:pageSize', 'page-change', 'collect-change'])
|
const emit = defineEmits(['update:currentPage', 'update:pageSize', 'page-change', 'collect-change'])
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
// 使用本地状态来代替直接绑定到props
|
// 使用本地状态来代替直接绑定到props
|
||||||
const localCurrentPage = ref(props.currentPage)
|
const localCurrentPage = ref(props.currentPage)
|
||||||
const localPageSize = ref(props.pageSize)
|
const localPageSize = ref(props.pageSize)
|
||||||
@@ -154,6 +166,11 @@ watch(() => props.pageSize, (newVal) => {
|
|||||||
|
|
||||||
const playerStore = usePlayerStore()
|
const playerStore = usePlayerStore()
|
||||||
|
|
||||||
|
// 跳转到音乐详情页
|
||||||
|
const goToMusicDetail = (id) => {
|
||||||
|
router.push({ name: 'music-detail', params: { id } })
|
||||||
|
}
|
||||||
|
|
||||||
// 检查音乐是否正在播放
|
// 检查音乐是否正在播放
|
||||||
const isPlaying = (id) => {
|
const isPlaying = (id) => {
|
||||||
return playerStore.getCurrentMusic?.id === id && playerStore.getIsPlaying
|
return playerStore.getCurrentMusic?.id === id && playerStore.getIsPlaying
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useRouter } from 'vue-router'
|
|||||||
import { usePlayerStore } from '../stores/player'
|
import { usePlayerStore } from '../stores/player'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import HomeBanner from '../components/HomeBanner.vue'
|
import HomeBanner from '../components/HomeBanner.vue'
|
||||||
import { getLatestMusic } from '../api/music'
|
import { getLatestMusic, getHotMusic } from '../api/music'
|
||||||
import { getLatestPlaylist } from '../api/playlist'
|
import { getLatestPlaylist } from '../api/playlist'
|
||||||
import { getLatestSinger } from '../api/singer'
|
import { getLatestSinger } from '../api/singer'
|
||||||
import { processMusicUrl } from '../api/music'
|
import { processMusicUrl } from '../api/music'
|
||||||
@@ -17,6 +17,10 @@ const playerStore = usePlayerStore()
|
|||||||
const newSongs = ref([])
|
const newSongs = ref([])
|
||||||
const loadingNewSongs = ref(false)
|
const loadingNewSongs = ref(false)
|
||||||
|
|
||||||
|
// 热门音乐
|
||||||
|
const hotSongs = ref([])
|
||||||
|
const loadingHotSongs = ref(false)
|
||||||
|
|
||||||
// 新歌单推荐
|
// 新歌单推荐
|
||||||
const newPlaylists = ref([])
|
const newPlaylists = ref([])
|
||||||
const loadingNewPlaylists = ref(false)
|
const loadingNewPlaylists = ref(false)
|
||||||
@@ -48,6 +52,29 @@ const fetchLatestMusic = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取热门音乐
|
||||||
|
const fetchHotMusic = async () => {
|
||||||
|
loadingHotSongs.value = true
|
||||||
|
try {
|
||||||
|
// 注意:这里调用的是新的 hot-music API
|
||||||
|
const res = await getHotMusic(8) // 获取8首热门音乐
|
||||||
|
if (res.code === 200) {
|
||||||
|
hotSongs.value = res.data.map(item => ({
|
||||||
|
...item,
|
||||||
|
cover: processMusicUrl(item.cover),
|
||||||
|
url: processMusicUrl(item.url)
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || '获取热门音乐失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取热门音乐失败:', error)
|
||||||
|
ElMessage.error('获取热门音乐失败')
|
||||||
|
} finally {
|
||||||
|
loadingHotSongs.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 获取最新歌单
|
// 获取最新歌单
|
||||||
const fetchLatestPlaylist = async () => {
|
const fetchLatestPlaylist = async () => {
|
||||||
loadingNewPlaylists.value = true
|
loadingNewPlaylists.value = true
|
||||||
@@ -121,6 +148,7 @@ const formatPlayCount = (count) => {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 获取数据
|
// 获取数据
|
||||||
fetchLatestMusic()
|
fetchLatestMusic()
|
||||||
|
fetchHotMusic() // 获取热门音乐
|
||||||
fetchLatestPlaylist()
|
fetchLatestPlaylist()
|
||||||
fetchLatestSinger()
|
fetchLatestSinger()
|
||||||
})
|
})
|
||||||
@@ -155,6 +183,28 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 热门音乐推荐 -->
|
||||||
|
<div class="section hot-songs-section" v-loading="loadingHotSongs">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2>热门音乐</h2>
|
||||||
|
<el-button text @click="router.push('/music')">查看更多</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="song-list">
|
||||||
|
<div v-for="song in hotSongs" :key="song.id" class="song-item" @dblclick="playSong(song)">
|
||||||
|
<div class="song-cover">
|
||||||
|
<img :src="song.cover" :alt="song.name">
|
||||||
|
<div class="play-icon" @click="playSong(song)">
|
||||||
|
<el-icon><video-play /></el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="song-info">
|
||||||
|
<div class="song-name">{{ song.name }}</div>
|
||||||
|
<div class="song-singer">{{ song.singer || (song.singers && song.singers.map(s => s.name).join(', ')) }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 新歌单推荐 -->
|
<!-- 新歌单推荐 -->
|
||||||
<div class="section new-playlists-section" v-loading="loadingNewPlaylists">
|
<div class="section new-playlists-section" v-loading="loadingNewPlaylists">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
|
|||||||
@@ -99,25 +99,29 @@ const handleCollectChange = (music) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取热门音乐
|
// 获取热门音乐
|
||||||
const fetchHotMusic = async () => {
|
const fetchHotMusic = async (params = {}) => {
|
||||||
loadingHot.value = true
|
loadingHot.value = true
|
||||||
try {
|
try {
|
||||||
const res = await getHotMusic(20) // 获取20首热门音乐
|
// 从缓存中获取全部50条热门音乐
|
||||||
|
const res = await getHotMusic(50)
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
hotMusicList.value = res.data || []
|
const allHotMusic = res.data || []
|
||||||
|
total.value = allHotMusic.length // 总数是50
|
||||||
|
|
||||||
// 处理音乐URL和封面URL
|
// 处理URL和收藏状态
|
||||||
hotMusicList.value.forEach(music => {
|
allHotMusic.forEach(music => {
|
||||||
if (music.url) {
|
if (music.url) music.url = processMusicUrl(music.url)
|
||||||
music.url = processMusicUrl(music.url)
|
if (music.cover) music.cover = processMusicUrl(music.cover)
|
||||||
}
|
if (music.collected === undefined) music.collected = false
|
||||||
if (music.cover) {
|
|
||||||
music.cover = processMusicUrl(music.cover)
|
|
||||||
}
|
|
||||||
if (music.collected === undefined) {
|
|
||||||
music.collected = false
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 根据当前页码和每页数量进行前端分页
|
||||||
|
const page = params.page || currentPage.value
|
||||||
|
const size = params.size || pageSize.value
|
||||||
|
const startIndex = (page - 1) * size
|
||||||
|
const endIndex = startIndex + size
|
||||||
|
hotMusicList.value = allHotMusic.slice(startIndex, endIndex)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(res.message || '获取热门音乐失败')
|
ElMessage.error(res.message || '获取热门音乐失败')
|
||||||
}
|
}
|
||||||
@@ -223,7 +227,10 @@ onMounted(() => {
|
|||||||
<MusicList
|
<MusicList
|
||||||
:list="hotMusicList"
|
:list="hotMusicList"
|
||||||
:loading="loadingHot"
|
:loading="loadingHot"
|
||||||
:showPagination="false"
|
:total="total"
|
||||||
|
v-model:current-page="currentPage"
|
||||||
|
v-model:page-size="pageSize"
|
||||||
|
@page-change="fetchHotMusic"
|
||||||
@collect-change="handleCollectChange"
|
@collect-change="handleCollectChange"
|
||||||
/>
|
/>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|||||||
@@ -88,12 +88,77 @@
|
|||||||
<p>{{ music.description }}</p>
|
<p>{{ music.description }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="music && music.lyric" class="music-lyric">
|
<div class="content-toggle">
|
||||||
<h2>歌词</h2>
|
<el-tabs v-model="activeTab">
|
||||||
<pre>{{ music.lyric }}</pre>
|
<el-tab-pane label="歌词" name="lyric"></el-tab-pane>
|
||||||
|
<el-tab-pane label="评论" name="comment"></el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
</div>
|
</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>
|
<h2>相关推荐</h2>
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col
|
<el-col
|
||||||
@@ -125,21 +190,34 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, watch } from 'vue'
|
import { ref, computed, onMounted, watch, onUnmounted } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { usePlayerStore } from '../stores/player'
|
import { usePlayerStore } from '../stores/player'
|
||||||
|
import { useUserStore } from '../stores/user'
|
||||||
import { getMusicById, toggleCollectMusic, getHotMusic } from '../api/music'
|
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'
|
import { VideoPlay, VideoPause, Star, StarFilled, Plus } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const playerStore = usePlayerStore()
|
const playerStore = usePlayerStore()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
const music = ref(null)
|
const music = ref(null)
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const relatedMusic = ref([])
|
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(() => {
|
const isPlaying = computed(() => {
|
||||||
return playerStore.getCurrentMusic?.id === music.value?.id && playerStore.getIsPlaying
|
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 = () => {
|
const playMusic = () => {
|
||||||
if (isPlaying.value) {
|
if (isPlaying.value) {
|
||||||
@@ -255,14 +444,25 @@ const formatDuration = (seconds) => {
|
|||||||
// 监听路由变化,重新获取音乐详情
|
// 监听路由变化,重新获取音乐详情
|
||||||
watch(() => route.params.id, (newId, oldId) => {
|
watch(() => route.params.id, (newId, oldId) => {
|
||||||
if (newId !== oldId) {
|
if (newId !== oldId) {
|
||||||
fetchMusicDetail()
|
teardownWebSocket();
|
||||||
fetchRelatedMusic()
|
fetchMusicDetail().then(() => {
|
||||||
|
fetchComments();
|
||||||
|
setupWebSocket();
|
||||||
|
});
|
||||||
|
fetchRelatedMusic();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchMusicDetail()
|
fetchMusicDetail().then(() => {
|
||||||
fetchRelatedMusic()
|
fetchComments();
|
||||||
|
setupWebSocket();
|
||||||
|
});
|
||||||
|
fetchRelatedMusic();
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
teardownWebSocket();
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -330,10 +530,79 @@ onMounted(() => {
|
|||||||
gap: 15px;
|
gap: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.music-description, .music-lyric {
|
.music-description {
|
||||||
margin-bottom: 30px;
|
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 {
|
.music-description h2, .music-lyric h2, .related-music h2 {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
|
|||||||
Reference in New Issue
Block a user