diff --git a/.cursor/rules/music-vue-rule.mdc b/.cursor/rules/music-vue-rule.mdc new file mode 100644 index 0000000..4dd9f3d --- /dev/null +++ b/.cursor/rules/music-vue-rule.mdc @@ -0,0 +1,90 @@ +--- +description: 这个规则是项目描述 +globs: +alwaysApply: false +--- +您是 TypeScript、Node.j、Vue3、Electron、naive-ui、VueUse 和 Tailwind 方面的专家。 + +项目结构 +- 这是 Electron 项目,使用 Vue3 和 Vuex 进行开发的第三方网易云音乐播放器。 +- 使用 Vue3 和 Vuex 进行开发。 +- 使用 Vuex 进行状态管理。 +- 使用 VueUse 进行状态管理。 +- 使用 naive-ui 进行 UI 设计。 +- 使用 Tailwind 进行样式设计。 +- 使用 remixicon 进行图标设计。 +- 使用 vite 进行项目构建。 +- 使用 electron-builder 进行项目打包。 +- 使用 electron-vite 进行项目开发。 +- 使用 netease-cloud-music-api 进行网易云音乐接口调用。 +- 使用 electron-store 进行本地数据存储。 +- 使用 axios 进行网络请求。 +- 使用 @unblockneteasemusic/server 进行网易云音乐解锁。 +- 使用 vue-i18n 进行国际化。目录为 src/i18n + +代码风格和结构 +- 编写简洁、技术性的 TypeScript 代码,并提供准确示例。 +- 使用组合 API 和声明性编程模式;避免使用选项 API。 +- 优先使用迭代和模块化,而不是代码重复。 +- 使用带有助动词的描述性变量名称(例如 isLoading、hasError)。 +- 结构文件:导出的组件、可组合项、帮助程序、静态内容、类型。 + +命名约定 +- 使用带破折号的小写字母表示目录(例如 components/auth-wizard)。 +- 使用 PascalCase 表示组件名称(例如 AuthWizard.vue)。 +- 使用 camelCase 表示可组合项(例如 useAuthState.ts)。 + +TypeScript 用法 +- 对所有代码使用 TypeScript;优先使用类型而不是接口。 +- 避免使用枚举;改用 const 对象。 +- 将 Vue 3 与 TypeScript 结合使用,利用 defineComponent 和 PropType。 + +语法和格式 +- 对方法和计算属性使用箭头函数。 +- 避免在条件中使用不必要的花括号;对简单语句使用简洁的语法。 +- 使用模板语法进行声明式渲染。 + +UI 和样式 +- 使用 naive-ui 和 Tailwind 进行组件和样式设计。 +- 使用 Tailwind CSS 实现响应式设计;采用移动优先方法。 + +图标 +- 使用 remixicon 作为图标库。 + +性能优化 +- 对异步组件使用 Suspense。 +- 为路由和组件实现延迟加载。 + +关键约定 +- 对常见可组合项和实用函数使用 VueUse。 +- 使用 Vuex 进行状态管理。 +- 优化 Web Vitals(LCP、CLS、FID)。 + + +Vue 3 和 Composition API 最佳实践 +- 使用 + + diff --git a/src/renderer/components/common/SongItem.vue b/src/renderer/components/common/SongItem.vue index 11ef719..b6b94aa 100644 --- a/src/renderer/components/common/SongItem.vue +++ b/src/renderer/components/common/SongItem.vue @@ -140,7 +140,7 @@ const dropdownY = ref(0); const isDownloading = ref(false); -const openPlaylistDrawer = inject<(songId: number) => void>('openPlaylistDrawer'); +const openPlaylistDrawer = inject<(songId: number | string) => void>('openPlaylistDrawer'); const { navigateToArtist } = useArtist(); @@ -285,7 +285,7 @@ const downloadMusic = async () => { try { isDownloading.value = true; - const data = (await getSongUrl(props.item.id, cloneDeep(props.item), true)) as any; + const data = (await getSongUrl(props.item.id as number, cloneDeep(props.item), true)) as any; if (!data || !data.url) { throw new Error(t('songItem.message.getUrlFailed')); } @@ -358,6 +358,7 @@ const imageLoad = async () => { // 播放音乐 设置音乐详情 打开音乐底栏 const playMusicEvent = async (item: SongResult) => { + // 如果是当前正在播放的音乐,则切换播放/暂停状态 if (playMusic.value.id === item.id) { if (play.value) { playerStore.setPlayMusic(false); @@ -368,23 +369,37 @@ const playMusicEvent = async (item: SongResult) => { } return; } - await playerStore.setPlay(item); - playerStore.isPlay = true; - emits('play', item); + + try { + // 使用store的setPlay方法,该方法已经包含了B站视频URL处理逻辑 + const result = await playerStore.setPlay(item); + if (!result) { + throw new Error('播放失败'); + } + playerStore.isPlay = true; + emits('play', item); + } catch (error) { + console.error('播放出错:', error); + } }; // 判断是否已收藏 const isFavorite = computed(() => { - return playerStore.favoriteList.includes(props.item.id); + // 将id转换为number,兼容B站视频ID + const numericId = typeof props.item.id === 'string' ? parseInt(props.item.id, 10) : props.item.id; + return playerStore.favoriteList.includes(numericId); }); // 切换收藏状态 const toggleFavorite = async (e: Event) => { e.stopPropagation(); + // 将id转换为number,兼容B站视频ID + const numericId = typeof props.item.id === 'string' ? parseInt(props.item.id, 10) : props.item.id; + if (isFavorite.value) { - playerStore.removeFromFavorite(props.item.id); + playerStore.removeFromFavorite(numericId); } else { - playerStore.addToFavorite(props.item.id); + playerStore.addToFavorite(numericId); } }; diff --git a/src/renderer/const/bar-const.ts b/src/renderer/const/bar-const.ts index 3040ac5..49f174d 100644 --- a/src/renderer/const/bar-const.ts +++ b/src/renderer/const/bar-const.ts @@ -45,6 +45,10 @@ export const SEARCH_TYPES = [ { label: 'MV', key: 1004 + }, + { + label: 'B站', + key: 2000 } // { // label: '歌词', @@ -63,3 +67,12 @@ export const SEARCH_TYPES = [ // key: 1018, // }, ]; + +export const SEARCH_TYPE = { + MUSIC: 1, // 单曲 + ALBUM: 10, // 专辑 + ARTIST: 100, // 歌手 + PLAYLIST: 1000, // 歌单 + MV: 1004, // MV + BILIBILI: 2000 // B站视频 +} as const; diff --git a/src/renderer/hooks/MusicHook.ts b/src/renderer/hooks/MusicHook.ts index 6e8bae7..952f8ab 100644 --- a/src/renderer/hooks/MusicHook.ts +++ b/src/renderer/hooks/MusicHook.ts @@ -2,6 +2,7 @@ import { createDiscreteApi } from 'naive-ui'; import { computed, nextTick, onUnmounted, ref, watch } from 'vue'; import i18n from '@/../i18n/renderer'; +import { getBilibiliAudioUrl } from '@/api/bilibili'; import useIndexedDB from '@/hooks/IndexDBHook'; import { audioService } from '@/services/audioService'; import pinia, { usePlayerStore } from '@/store'; @@ -235,6 +236,29 @@ watch( initialPosition = savedProgress.progress; } + // 对于B站视频,检查URL是否有效 + if (playMusic.value.source === 'bilibili' && (!newVal || newVal === 'undefined')) { + console.log('B站视频URL无效,尝试重新获取'); + + // 需要重新获取B站视频URL + if (playMusic.value.bilibiliData) { + try { + const proxyUrl = await getBilibiliAudioUrl( + playMusic.value.bilibiliData.bvid, + playMusic.value.bilibiliData.cid + ); + + // 设置URL到播放器状态 + (playMusic.value as any).playMusicUrl = proxyUrl; + playerStore.playMusicUrl = proxyUrl; + newVal = proxyUrl; + } catch (error) { + console.error('获取B站音频URL失败:', error); + return; + } + } + } + // 播放新音频,传递是否应该播放的状态 const newSound = await audioService.play(newVal, playMusic.value, shouldPlay); sound.value = newSound as Howl; @@ -842,3 +866,97 @@ export const initAudioListeners = async () => { console.error('初始化音频监听器失败:', error); } }; + +// 监听URL过期事件,自动重新获取URL并恢复播放 +audioService.on('url_expired', async (expiredTrack) => { + if (!expiredTrack) return; + + console.log('检测到URL过期事件,准备重新获取URL', expiredTrack.name); + + try { + const currentPosition = nowTime.value; // 保存当前播放进度 + console.log('保存当前播放进度:', currentPosition); + + // 处理B站视频 + if (expiredTrack.source === 'bilibili' && expiredTrack.bilibiliData) { + console.log('重新获取B站视频URL'); + try { + // 使用API中的函数获取B站音频URL + const newUrl = await getBilibiliAudioUrl( + expiredTrack.bilibiliData.bvid, + expiredTrack.bilibiliData.cid + ); + + console.log('成功获取新的B站URL:', newUrl); + + // 更新存储 + (expiredTrack as any).playMusicUrl = newUrl; + playerStore.playMusicUrl = newUrl; + + // 重新播放并设置进度 + const newSound = await audioService.play(newUrl, expiredTrack); + sound.value = newSound as Howl; + + // 恢复播放进度 + if (currentPosition > 0) { + newSound.seek(currentPosition); + nowTime.value = currentPosition; + console.log('恢复播放进度:', currentPosition); + } + + // 如果之前是播放状态,继续播放 + if (playerStore.play) { + newSound.play(); + playerStore.setIsPlay(true); + } + + message.success('已自动恢复播放'); + } catch (error) { + console.error('重新获取B站URL失败:', error); + message.error('重新获取音频地址失败,请手动点击播放'); + } + } else if (expiredTrack.source === 'netease') { + // 处理网易云音乐,重新获取URL + console.log('重新获取网易云音乐URL'); + try { + const { getSongUrl } = await import('@/store/modules/player'); + const newUrl = await getSongUrl(expiredTrack.id, expiredTrack as any); + + if (newUrl) { + console.log('成功获取新的网易云URL:', newUrl); + + // 更新存储 + (expiredTrack as any).playMusicUrl = newUrl; + playerStore.playMusicUrl = newUrl; + + // 重新播放并设置进度 + const newSound = await audioService.play(newUrl, expiredTrack); + sound.value = newSound as Howl; + + // 恢复播放进度 + if (currentPosition > 0) { + newSound.seek(currentPosition); + nowTime.value = currentPosition; + console.log('恢复播放进度:', currentPosition); + } + + // 如果之前是播放状态,继续播放 + if (playerStore.play) { + newSound.play(); + playerStore.setIsPlay(true); + } + + message.success('已自动恢复播放'); + } else { + throw new Error('获取URL失败'); + } + } catch (error) { + console.error('重新获取网易云URL失败:', error); + message.error('重新获取音频地址失败,请手动点击播放'); + } + } + } catch (error) { + console.error('处理URL过期事件失败:', error); + message.error('恢复播放失败,请手动点击播放'); + } +}); diff --git a/src/renderer/hooks/MusicListHook.ts b/src/renderer/hooks/MusicListHook.ts index 79c42a3..574a4a8 100644 --- a/src/renderer/hooks/MusicListHook.ts +++ b/src/renderer/hooks/MusicListHook.ts @@ -12,7 +12,7 @@ import { getImageLinearBackground } from '@/utils/linearColor'; const musicHistory = useMusicHistory(); // 获取歌曲url -export const getSongUrl = async (id: number, songData: any, isDownloaded: boolean = false) => { +export const getSongUrl = async (id: any, songData: any, isDownloaded: boolean = false) => { const { data } = await getMusicUrl(id, isDownloaded); let url = ''; let songDetail = null; @@ -247,7 +247,7 @@ export const useMusicListHook = () => { }; // 异步加载歌词的方法 - const loadLrcAsync = async (state: any, playMusicId: number) => { + const loadLrcAsync = async (state: any, playMusicId: any) => { if (state.playMusic.lyric && state.playMusic.lyric.lrcTimeArray.length > 0) { return; } diff --git a/src/renderer/index.css b/src/renderer/index.css index 34a3616..0555b7e 100644 --- a/src/renderer/index.css +++ b/src/renderer/index.css @@ -11,7 +11,7 @@ } .n-slider-handle-indicator--top { - @apply bg-transparent text-2xl px-2 py-1 shadow-none mb-0 dark:text-[#ffffffdd] text-[#000000dd] !important; + @apply bg-transparent text-2xl px-2 py-1 shadow-none mb-0 text-white bg-dark-300 dark:bg-gray-800 bg-opacity-80 rounded-lg !important; mix-blend-mode: difference !important; } diff --git a/src/renderer/layout/components/MobilePlayBar.vue b/src/renderer/layout/components/MobilePlayBar.vue index 6162626..b0c703d 100644 --- a/src/renderer/layout/components/MobilePlayBar.vue +++ b/src/renderer/layout/components/MobilePlayBar.vue @@ -217,15 +217,15 @@ const scrollToPlayList = (val: boolean) => { // 收藏功能 const isFavorite = computed(() => { - return playerStore.favoriteList.includes(playMusic.value.id); + return playerStore.favoriteList.includes(playMusic.value.id as number); }); const toggleFavorite = () => { console.log('isFavorite.value', isFavorite.value); if (isFavorite.value) { - playerStore.removeFromFavorite(playMusic.value.id); + playerStore.removeFromFavorite(playMusic.value.id as number); } else { - playerStore.addToFavorite(playMusic.value.id); + playerStore.addToFavorite(playMusic.value.id as number); } }; diff --git a/src/renderer/layout/components/MusicFull.vue b/src/renderer/layout/components/MusicFull.vue index bafcb19..b4ff89a 100644 --- a/src/renderer/layout/components/MusicFull.vue +++ b/src/renderer/layout/components/MusicFull.vue @@ -112,7 +112,7 @@ -