From 3fc25ba9a4666cb557d7c233f9b1cffa043b6608 Mon Sep 17 00:00:00 2001 From: alger Date: Sun, 5 Jul 2026 14:35:00 +0800 Subject: [PATCH] =?UTF-8?q?feat(local-music):=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E4=BB=8E=E6=9C=AC=E5=9C=B0=E5=88=97=E8=A1=A8=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E5=8D=95=E6=9B=B2=E5=B9=B6=E9=98=B2=E6=AD=A2=E6=89=AB=E6=8F=8F?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E8=AF=AF=E5=88=A0=EF=BC=88#713=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 右键菜单新增'从本地列表移除'(本地歌曲自动切换文案,仅移除条目不删文件,5 语言文案) - localMusic store 新增 removeEntry action - 扫描失败的文件夹不再参与'已删除清理',避免移动盘/网络盘暂时不可用时整夹歌曲被误删 - 注:'刷新不清理已删除歌曲'主症状已由 c28368f 修复,本次补齐评论区诉求与防护 Closes #713 --- src/i18n/lang/en-US/localMusic.ts | 4 ++- src/i18n/lang/ja-JP/localMusic.ts | 4 ++- src/i18n/lang/ko-KR/localMusic.ts | 4 ++- src/i18n/lang/zh-CN/localMusic.ts | 4 ++- src/i18n/lang/zh-Hant/localMusic.ts | 4 ++- .../common/songItemCom/SongItemDropdown.vue | 10 +++++- src/renderer/store/modules/localMusic.ts | 31 ++++++++++++++++++- src/renderer/views/local-music/index.vue | 16 ++++++++++ 8 files changed, 70 insertions(+), 7 deletions(-) diff --git a/src/i18n/lang/en-US/localMusic.ts b/src/i18n/lang/en-US/localMusic.ts index 6ee8073..1c94c95 100644 --- a/src/i18n/lang/en-US/localMusic.ts +++ b/src/i18n/lang/en-US/localMusic.ts @@ -9,5 +9,7 @@ export default { emptyState: 'No local music found. Please select a folder to scan.', fileNotFound: 'File not found or has been moved', rescan: 'Rescan', - songCount: '{count} songs' + songCount: '{count} songs', + removeFromLibrary: 'Remove from Library', + removedFromLibrary: 'Removed from library (file not deleted)' }; diff --git a/src/i18n/lang/ja-JP/localMusic.ts b/src/i18n/lang/ja-JP/localMusic.ts index 3291923..64955bc 100644 --- a/src/i18n/lang/ja-JP/localMusic.ts +++ b/src/i18n/lang/ja-JP/localMusic.ts @@ -9,5 +9,7 @@ export default { emptyState: 'ローカル音楽がありません。フォルダを選択してスキャンしてください。', fileNotFound: 'ファイルが見つからないか、移動されました', rescan: '再スキャン', - songCount: '{count} 曲' + songCount: '{count} 曲', + removeFromLibrary: 'ライブラリから削除', + removedFromLibrary: 'ライブラリから削除しました(ファイルは削除されません)' }; diff --git a/src/i18n/lang/ko-KR/localMusic.ts b/src/i18n/lang/ko-KR/localMusic.ts index 0155a92..45ace20 100644 --- a/src/i18n/lang/ko-KR/localMusic.ts +++ b/src/i18n/lang/ko-KR/localMusic.ts @@ -9,5 +9,7 @@ export default { emptyState: '로컬 음악이 없습니다. 폴더를 선택하여 스캔하세요.', fileNotFound: '파일을 찾을 수 없거나 이동되었습니다', rescan: '다시 스캔', - songCount: '{count}곡' + songCount: '{count}곡', + removeFromLibrary: '라이브러리에서 제거', + removedFromLibrary: '라이브러리에서 제거했습니다 (파일은 삭제되지 않음)' }; diff --git a/src/i18n/lang/zh-CN/localMusic.ts b/src/i18n/lang/zh-CN/localMusic.ts index eafaa8c..ffa9f1d 100644 --- a/src/i18n/lang/zh-CN/localMusic.ts +++ b/src/i18n/lang/zh-CN/localMusic.ts @@ -9,5 +9,7 @@ export default { emptyState: '暂无本地音乐,请先选择文件夹进行扫描', fileNotFound: '文件不存在或已被移动', rescan: '重新扫描', - songCount: '{count} 首歌曲' + songCount: '{count} 首歌曲', + removeFromLibrary: '从本地列表移除', + removedFromLibrary: '已从本地列表移除(不删除文件)' }; diff --git a/src/i18n/lang/zh-Hant/localMusic.ts b/src/i18n/lang/zh-Hant/localMusic.ts index 0d155ed..bd6f322 100644 --- a/src/i18n/lang/zh-Hant/localMusic.ts +++ b/src/i18n/lang/zh-Hant/localMusic.ts @@ -9,5 +9,7 @@ export default { emptyState: '暫無本地音樂,請先選擇資料夾進行掃描', fileNotFound: '檔案不存在或已被移動', rescan: '重新掃描', - songCount: '{count} 首歌曲' + songCount: '{count} 首歌曲', + removeFromLibrary: '從本機清單移除', + removedFromLibrary: '已從本機清單移除(不刪除檔案)' }; diff --git a/src/renderer/components/common/songItemCom/SongItemDropdown.vue b/src/renderer/components/common/songItemCom/SongItemDropdown.vue index 4347991..44e92ef 100644 --- a/src/renderer/components/common/songItemCom/SongItemDropdown.vue +++ b/src/renderer/components/common/songItemCom/SongItemDropdown.vue @@ -59,6 +59,12 @@ const userStore = useUserStore(); // 后者不具备响应性,登录/登出后菜单禁用状态不会刷新(#706) const hasRealAuth = computed(() => !!userStore.user && userStore.loginType !== 'uid'); +// 本地歌曲:移除菜单项显示"从本地列表移除"而非"从歌单中删除"(#713) +const isLocalSong = computed( + () => + typeof props.item.playMusicUrl === 'string' && props.item.playMusicUrl.startsWith('local://') +); + // 渲染歌曲预览 const renderSongPreview = () => { return h( @@ -198,7 +204,9 @@ const dropdownOptions = computed(() => { key: 'd2' }, { - label: t('songItem.menu.removeFromPlaylist'), + label: isLocalSong.value + ? t('localMusic.removeFromLibrary') + : t('songItem.menu.removeFromPlaylist'), key: 'remove', icon: () => h('i', { class: 'iconfont ri-delete-bin-line' }) } diff --git a/src/renderer/store/modules/localMusic.ts b/src/renderer/store/modules/localMusic.ts index a4d5ba6..9072383 100644 --- a/src/renderer/store/modules/localMusic.ts +++ b/src/renderer/store/modules/localMusic.ts @@ -127,6 +127,9 @@ export const useLocalMusicStore = defineStore( // 磁盘上实际存在的文件路径集合(扫描时收集) const diskFilePaths = new Set(); + // 扫描失败的文件夹:其下的缓存条目不参与"已删除清理", + // 避免移动盘/网络盘暂时不可用时整个文件夹的歌曲被误删(#713) + const failedFolders: string[] = []; // 遍历每个文件夹进行扫描 for (const folderPath of folderPaths.value) { @@ -138,6 +141,7 @@ export const useLocalMusicStore = defineStore( if ((result as any).error) { console.error(`扫描文件夹失败: ${folderPath}`, (result as any).error); message.error(`扫描失败: ${(result as any).error}`); + failedFolders.push(folderPath); continue; } @@ -178,12 +182,23 @@ export const useLocalMusicStore = defineStore( } catch (error) { console.error(`扫描文件夹出错: ${folderPath}`, error); message.error(`扫描文件夹出错: ${folderPath}`); + failedFolders.push(folderPath); } } + /** 判断文件路径是否位于某个扫描失败的文件夹下 */ + const isUnderFailedFolder = (filePath: string): boolean => + failedFolders.some((folder) => { + if (!filePath.startsWith(folder)) return false; + if (folder.endsWith('/') || folder.endsWith('\\')) return true; + const next = filePath.charAt(folder.length); + return next === '/' || next === '\\'; + }); + // 4. 清理已删除文件:从 IndexedDB 移除磁盘上不存在的条目 + // (扫描失败的文件夹跳过清理,其文件未被枚举并不代表已删除) for (const [filePath, entry] of cachedMap) { - if (!diskFilePaths.has(filePath)) { + if (!diskFilePaths.has(filePath) && !isUnderFailedFolder(filePath)) { await localDB.deleteData(LOCAL_MUSIC_STORE, entry.id); } } @@ -213,6 +228,19 @@ export const useLocalMusicStore = defineStore( } } + /** + * 从本地列表移除单个条目(仅软件层面移除,不删除磁盘文件)(#713) + * @param id 条目 ID(generateId 生成的 hex 字符串) + */ + async function removeEntry(id: string): Promise { + const localDB = await getDB(); + await localDB.deleteData(LOCAL_MUSIC_STORE, id); + const index = musicList.value.findIndex((entry) => entry.id === id); + if (index !== -1) { + musicList.value.splice(index, 1); + } + } + /** * 清理缓存:检查文件存在性,移除已不存在的文件条目 */ @@ -271,6 +299,7 @@ export const useLocalMusicStore = defineStore( removeFolder, scanFolders, loadFromCache, + removeEntry, clearCache }; }, diff --git a/src/renderer/views/local-music/index.vue b/src/renderer/views/local-music/index.vue index 283a6c1..26edb2e 100644 --- a/src/renderer/views/local-music/index.vue +++ b/src/renderer/views/local-music/index.vue @@ -143,7 +143,9 @@ :key="item.id" :index="index" :item="item" + :can-remove="true" @play="handlePlaySong" + @remove-song="handleRemoveSong" /> @@ -283,6 +285,20 @@ async function handlePlaySong(_song: SongResult): Promise { } } +/** + * 从本地列表移除单曲(仅软件层面移除,不删除磁盘文件)(#713) + * @param id SongResult.id,即 LocalMusicEntry.id(hex 字符串) + */ +async function handleRemoveSong(id: number | string): Promise { + try { + await localMusicStore.removeEntry(String(id)); + message.success(t('localMusic.removedFromLibrary')); + } catch (error) { + console.error('移除本地歌曲失败:', error); + message.error(String(error)); + } +} + /** * 播放全部 * 将完整列表转换为 SongResult[] 后设置为播放列表并从第一首开始播放