diff --git a/music-houduan/src/main/java/com/test/musichouduan/dto/MusicStatusDTO.java b/music-houduan/src/main/java/com/test/musichouduan/dto/MusicStatusDTO.java new file mode 100644 index 0000000..6baee4c --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/dto/MusicStatusDTO.java @@ -0,0 +1,24 @@ +package com.test.musichouduan.dto; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +/** + * 音乐状态DTO,用于封装收藏和点赞状态 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class MusicStatusDTO { + + /** + * 是否已收藏 + */ + private boolean collected; + + /** + * 是否已点赞 + */ + private boolean liked; +} diff --git a/music-qianduan/src/components/MusicPlayer.vue b/music-qianduan/src/components/MusicPlayer.vue index 945db0e..8b09932 100644 --- a/music-qianduan/src/components/MusicPlayer.vue +++ b/music-qianduan/src/components/MusicPlayer.vue @@ -92,10 +92,10 @@
- + - + @@ -288,6 +288,10 @@ import { usePlayerStore } from '../stores/player' import { useUserStore } from '../stores/user' import { processMusicUrl, toggleCollectMusic, getMusicStatus } from '../api/music' import { ElMessage } from 'element-plus' +import { + VideoPlay, VideoPause, Back, Right, List, Close, Delete, + Mute, VideoCamera, Microphone, Star, StarFilled, ArrowDown +} from '@element-plus/icons-vue' const router = useRouter() const userStore = useUserStore() @@ -320,22 +324,18 @@ const loopMode = computed(() => playerStore.getLoop) // 获取歌手名称 const getSingerNames = computed(() => { if (!currentMusic.value) return '' - if (currentMusic.value.singerNames && currentMusic.value.singerNames.length > 0) { return currentMusic.value.singerNames.join('、') } - return currentMusic.value.singer || '未知歌手' }) // 获取音乐的歌手名称 const getSingerNamesFromMusic = (music) => { if (!music) return '' - if (music.singerNames && music.singerNames.length > 0) { return music.singerNames.join('、') } - return music.singer || '未知歌手' } @@ -353,70 +353,48 @@ const volumeIcon = computed(() => { // 循环模式图标 const loopModeIcon = computed(() => { switch (loopMode.value) { - case 'single': - return 'Refresh' - case 'list': - return 'Sort' - default: - return 'Connection' + case 'single': return 'Refresh' + case 'list': return 'Sort' + default: return 'Connection' } }) // 循环模式文本 const loopModeText = computed(() => { switch (loopMode.value) { - case 'single': - return '单曲循环' - case 'list': - return '列表循环' - default: - return '顺序播放' + case 'single': return '单曲循环' + case 'list': return '列表循环' + default: return '顺序播放' } }) // 解析歌词 const parsedLyric = computed(() => { - if (!currentMusic.value || !currentMusic.value.lyric) { - return [] - } - + if (!currentMusic.value || !currentMusic.value.lyric) return [] const lyric = currentMusic.value.lyric const lines = lyric.split('\n') const timeRegExp = /\[(\d{2}):(\d{2})\.(\d{2,3})\]/g - const result = [] - for (const line of lines) { - // 使用正则表达式的g标志匹配所有时间标记 const timeMatches = [...line.matchAll(timeRegExp)] if (timeMatches.length > 0) { - // 取第一个时间标记作为歌词时间 - const match = timeMatches[0] - const min = parseInt(match[1]) - const sec = parseInt(match[2]) - const ms = parseInt(match[3]) + const match = timeMatches + const min = parseInt(match) + const sec = parseInt(match) + const ms = parseInt(match) const time = min * 60 + sec + ms / 1000 - - // 删除所有时间标记 let text = line.replace(timeRegExp, '').trim() - - // 只有当文本非空时才添加到结果中 if (text) { result.push({ time, text }) } } } - - // 按时间排序 return result.sort((a, b) => a.time - b.time) }) // 格式化时间 const formatTime = (seconds) => { - if (isNaN(seconds) || seconds < 0) { - return '00:00' - } - + if (isNaN(seconds) || seconds < 0) return '00:00' const mins = Math.floor(seconds / 60) const secs = Math.floor(seconds % 60) return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}` @@ -425,10 +403,8 @@ const formatTime = (seconds) => { // 格式化日期 const formatDate = (dateStr) => { if (!dateStr) return '' - try { - const date = new Date(dateStr) - return date.toLocaleDateString() + return new Date(dateStr).toLocaleDateString() } catch (e) { return dateStr } @@ -443,31 +419,19 @@ const togglePlay = () => { } } -// 上一首 -const prevSong = () => { - playerStore.playPrev() -} +// 上一首/下一首 +const prevSong = () => playerStore.playPrev() +const nextSong = () => playerStore.playNext() -// 下一首 -const nextSong = () => { - playerStore.playNext() -} - -// 切换静音 +// 静音切换 const toggleMute = () => { - if (volume.value > 0) { - playerStore.setVolume(0) - } else { - playerStore.setVolume(80) - } + playerStore.setVolume(volume.value > 0 ? 0 : 80) } -// 处理音量变化 -const handleVolumeChange = (val) => { - playerStore.setVolume(val) -} +// 音量变化 +const handleVolumeChange = (val) => playerStore.setVolume(val) -// 切换循环模式 +// 循环模式切换 const toggleLoopMode = () => { const modes = ['none', 'list', 'single'] const currentIndex = modes.indexOf(loopMode.value) @@ -475,23 +439,19 @@ const toggleLoopMode = () => { playerStore.setLoop(modes[nextIndex]) } -// 切换收藏状态 +// 收藏/取消收藏 const toggleCollect = async () => { if (!userStore.isLoggedIn) { ElMessage.warning('请先登录') router.push('/login') return } - - if (!currentMusic.value || !currentMusic.value.id) { - return - } + if (!currentMusic.value || !currentMusic.value.id) return try { const res = await toggleCollectMusic(currentMusic.value.id) if (res.code === 200) { - // 直接使用后端返回的最新状态 - playerStore.updateCurrentMusicStatus({ collected: res.data }); + playerStore.updateCurrentMusicStatus({ collected: res.data }) ElMessage.success(res.message || (res.data ? '收藏成功' : '已取消收藏')) } else { ElMessage.error(res.message || '操作失败') @@ -502,157 +462,61 @@ const toggleCollect = async () => { } } -// 切换播放列表显示 -const togglePlaylist = () => { - playerStore.setShowPlaylist(!showPlaylist.value) -} +// 播放列表相关 +const togglePlaylist = () => playerStore.setShowPlaylist(!showPlaylist.value) +const playFromPlaylist = (index) => playerStore.playFromPlaylist(index) +const removeFromPlaylist = (index) => playerStore.removeFromPlaylist(index) +const clearPlaylist = () => playerStore.clearPlaylist() -// 从播放列表播放 -const playFromPlaylist = (index) => { - playerStore.playFromPlaylist(index) -} - -// 从播放列表移除 -const removeFromPlaylist = (index) => { - playerStore.removeFromPlaylist(index) -} - -// 清空播放列表 -const clearPlaylist = () => { - playerStore.clearPlaylist() -} - -// 切换展开状态 +// 展开/收起播放器 const toggleExpand = () => { isExpanded.value = !isExpanded.value - - // 当展开播放器时,禁止外部页面滚动 - if (isExpanded.value) { - document.body.style.overflow = 'hidden' - } else { - document.body.style.overflow = '' - } + document.body.style.overflow = isExpanded.value ? 'hidden' : '' } -// 跳转到音乐详情页面 -const goToMusicDetail = () => { - if (currentMusic.value && currentMusic.value.id) { - router.push(`/music/${currentMusic.value.id}`) - } -} - -// 处理进度条变化 +// 进度条处理 +const isUserSeeking = ref(false) const handleSliderChange = (val) => { - // 暂停监听时间更新,避免冲突 isUserSeeking.value = true playerStore.setCurrentTime(val) - // 延迟恢复监听,确保播放器有时间响应 - setTimeout(() => { - isUserSeeking.value = false - }, 500) // 增加延迟时间,确保播放器有足够时间响应 + setTimeout(() => { isUserSeeking.value = false }, 500) } +const handleSliderInput = () => { isUserSeeking.value = true } +const formatSliderTooltip = (val) => formatTime(val) -// 处理进度条输入(拖动过程中) -const handleSliderInput = (val) => { - isUserSeeking.value = true -} - -// 格式化进度条提示文本 -const formatSliderTooltip = (val) => { - return formatTime(val) -} - -// 标记用户是否正在拖动进度条 -const isUserSeeking = ref(false) - -// 监听当前时间变化,更新滑块位置 +// 监听 Pinia store 的时间更新 watch(currentTime, (newVal) => { - // 只有当用户不在拖动进度条时才更新滑块位置 if (!isUserSeeking.value) { sliderTime.value = newVal } - - // 更新当前歌词索引 + // 更新歌词 if (parsedLyric.value.length > 0) { - let foundMatch = false - for (let i = 0; i < parsedLyric.value.length; i++) { - // 如果当前时间小于歌词时间,则使用前一行歌词 - if (newVal < parsedLyric.value[i].time) { - if (i > 0 && currentLyricIndex.value !== i - 1) { - currentLyricIndex.value = i - 1 - if (isExpanded.value) { - scrollToActiveLyric() - } - } - foundMatch = true - break - } - // 如果是最后一行歌词 - else if (i === parsedLyric.value.length - 1) { - if (currentLyricIndex.value !== i) { - currentLyricIndex.value = i - if (isExpanded.value) { - scrollToActiveLyric() - } - } - foundMatch = true - } - } - - // 如果没有找到匹配的歌词,则重置到第一行 - if (!foundMatch && currentLyricIndex.value !== -1) { - currentLyricIndex.value = -1 + let newIndex = parsedLyric.value.findIndex(line => line.time > newVal) - 1 + if (newIndex < 0) newIndex = parsedLyric.value.length - 1 + if (currentLyricIndex.value !== newIndex) { + currentLyricIndex.value = newIndex + if (isExpanded.value) scrollToActiveLyric() } } }) // 滚动到当前歌词 const scrollToActiveLyric = async () => { + await nextTick() if (lyricScrollbar.value && currentLyricIndex.value >= 0) { - await nextTick() - try { - // 使用ref获取当前歌词元素 - const element = lyricLines.value[currentLyricIndex.value] - if (element) { - // 尝试多种方法来确保滚动生效 - - // 方法 1: 使用scrollIntoView - element.scrollIntoView({ behavior: 'smooth', block: 'center' }) - - // 方法 2: 使用el-scrollbar的方法 - if (lyricScrollbar.value.setScrollTop) { - lyricScrollbar.value.setScrollTop(element.offsetTop - 150, 300) - } - - // 方法 3: 直接设置scrollTop - if (lyricScrollbar.value.wrap) { - setTimeout(() => { - lyricScrollbar.value.wrap.scrollTop = element.offsetTop - 150 - }, 10) - } - } - } catch (error) { - console.error('Failed to scroll to active lyric:', error) + const element = lyricLines.value[currentLyricIndex.value] + if (element) { + lyricScrollbar.value.setScrollTop(element.offsetTop - 150) } } } -// 监听当前音乐变化,处理音乐URL和状态 +// 监听当前播放歌曲的变化 watch(currentMusic, (newMusic) => { if (newMusic && newMusic.id) { - // 处理音乐URL - const processedUrl = processMusicUrl(newMusic.url) - if (processedUrl !== newMusic.url) { - playerStore.updateMusicUrl(newMusic.id, processedUrl) - } - - // 处理封面URL - if (newMusic.cover) { - const processedCover = processMusicUrl(newMusic.cover) - if (processedCover !== newMusic.cover) { - playerStore.updateMusicCover(newMusic.id, processedCover) - } - } + // 处理 URL + playerStore.updateMusicUrl(newMusic.id, processMusicUrl(newMusic.url)) + playerStore.updateMusicCover(newMusic.id, processMusicUrl(newMusic.cover)) // 主动获取最新的收藏和点赞状态 if (userStore.isLoggedIn) { @@ -663,22 +527,15 @@ watch(currentMusic, (newMusic) => { liked: res.data.liked }); } - }).catch(err => { - console.error("获取播放器音乐状态失败:", err); - }); + }).catch(err => console.error("获取播放器音乐状态失败:", err)); } else { - // 未登录则重置状态 playerStore.updateCurrentMusicStatus({ collected: false, liked: false }); } - // 重置歌词索引和元素引用 + // 重置歌词 currentLyricIndex.value = -1 lyricLines.value = {} - - // 如果展开了播放器,切换到歌词标签页 - if (isExpanded.value) { - expandedActiveTab.value = 'lyric' - } + if (isExpanded.value) expandedActiveTab.value = 'lyric' } }) diff --git a/music-qianduan/src/stores/player.js b/music-qianduan/src/stores/player.js index 2f687ea..0daa8c5 100644 --- a/music-qianduan/src/stores/player.js +++ b/music-qianduan/src/stores/player.js @@ -280,6 +280,18 @@ export const usePlayerStore = defineStore('player', { if (index !== -1) { this.playlist[index].cover = cover } + }, + + // 更新当前音乐的状态(如收藏、点赞) + updateCurrentMusicStatus(status) { + if (this.currentMusic) { + this.currentMusic = { ...this.currentMusic, ...status }; + + const index = this.playlist.findIndex(item => item.id === this.currentMusic.id); + if (index !== -1) { + this.playlist[index] = { ...this.playlist[index], ...status }; + } + } } } })