From 27ebcab94484f9af97514833db07c54ca7bcaaa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=A1=E5=9D=82=E4=BA=91=E8=A7=81?= <447f@misaka.org> Date: Sun, 1 Nov 2026 16:43:07 +0800 Subject: [PATCH 01/14] =?UTF-8?q?feat(front):=20=E8=B0=B1=E9=9D=A2?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=B7=A6=E4=BE=A7=E5=88=97=E8=A1=A8=E5=8A=A0?= =?UTF-8?q?=E6=90=9C=E7=B4=A2=E6=A1=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 按名称 / 作曲 / 谱师 / ID 过滤,命中口径复用 utils/musicSearch 的 matchMusicKeyword, 和批量操作页保持同一口径 - 过滤只作用于渲染列表(displayedMusic),selectedMusic 仍由未过滤的 musicList 派生, 所以选中曲被过滤掉时右侧编辑面板不会变空 - 无匹配显示空态;有内容时显示清空按钮,Esc 也能清空;输入法组字中按 Esc 不清空 - 修既有跳转失效(只选中不滚动):批量页调用方只等 100ms,而 tab 切换是 0.3s 的 out-in 过渡,那时 VList 还没挂载。改成等 ref 就绪(setTimeout 轮询,2s 兜底) - 修既有跳转失效(左侧停在目录管理时):跳转先把 leftPanel 切回列表再定位 - 搜索框占位符去掉「别名」:matchMusicKeyword 本来就不搜别名,且这个 key 被批量操作页 与工具页共用,三处文案随实现一起对齐 AI-assisted-by: DeepSeek-V4-Flash (DeepSeek Harness) --- MaiChartManager/Front/src/locales/en.yaml | 4 +- MaiChartManager/Front/src/locales/zh-TW.yaml | 4 +- MaiChartManager/Front/src/locales/zh.yaml | 4 +- .../src/views/Charts/MusicList/index.tsx | 102 +++++++++++++++--- 4 files changed, 99 insertions(+), 15 deletions(-) diff --git a/MaiChartManager/Front/src/locales/en.yaml b/MaiChartManager/Front/src/locales/en.yaml index 4013bce1..8dae235c 100644 --- a/MaiChartManager/Front/src/locales/en.yaml +++ b/MaiChartManager/Front/src/locales/en.yaml @@ -60,7 +60,7 @@ assetDir: music: list: title: Chart List - searchPlaceholder: Search title / composer / charter / alias / ID + searchPlaceholder: Search title / composer / charter / ID tableTitle: Title tableCover: Cover tableVersion: Version @@ -71,6 +71,8 @@ music: filterStandard: Standard filterDX: DX filterUtage: Utage + noMatch: No matching songs + clearSearch: Clear search unknown: Unknown filterBasic: Basic filterMaster: Master diff --git a/MaiChartManager/Front/src/locales/zh-TW.yaml b/MaiChartManager/Front/src/locales/zh-TW.yaml index c0fac9de..82dd354f 100644 --- a/MaiChartManager/Front/src/locales/zh-TW.yaml +++ b/MaiChartManager/Front/src/locales/zh-TW.yaml @@ -58,7 +58,7 @@ assetDir: music: list: title: 譜面列表 - searchPlaceholder: 搜尋名稱 / 作曲 / 譜師 / 別名 / ID + searchPlaceholder: 搜尋名稱 / 作曲 / 譜師 / ID tableTitle: 標題 tableCover: 封面 tableVersion: 版本 @@ -69,6 +69,8 @@ music: filterStandard: 標準 filterDX: DX filterUtage: 宴會場 + noMatch: 沒有符合的曲目 + clearSearch: 清除搜尋 unknown: 未知 filterBasic: 綠 filterMaster: 紫 diff --git a/MaiChartManager/Front/src/locales/zh.yaml b/MaiChartManager/Front/src/locales/zh.yaml index 5bd1d252..8b519851 100644 --- a/MaiChartManager/Front/src/locales/zh.yaml +++ b/MaiChartManager/Front/src/locales/zh.yaml @@ -58,7 +58,7 @@ assetDir: music: list: title: 谱面列表 - searchPlaceholder: 搜索名称 / 作曲 / 谱师 / 别名 / ID + searchPlaceholder: 搜索名称 / 作曲 / 谱师 / ID tableTitle: 标题 tableCover: 封面 tableVersion: 版本 @@ -69,6 +69,8 @@ music: filterStandard: 标准 filterDX: DX filterUtage: 宴会场 + noMatch: 没有匹配的曲目 + clearSearch: 清空搜索 unknown: 未知 filterBasic: 绿 filterMaster: 紫 diff --git a/MaiChartManager/Front/src/views/Charts/MusicList/index.tsx b/MaiChartManager/Front/src/views/Charts/MusicList/index.tsx index d14f8067..d78bf8ad 100644 --- a/MaiChartManager/Front/src/views/Charts/MusicList/index.tsx +++ b/MaiChartManager/Front/src/views/Charts/MusicList/index.tsx @@ -1,19 +1,69 @@ -import { computed, defineComponent, ref } from "vue"; +import { computed, defineComponent, nextTick, ref } from "vue"; import { MusicXmlWithABJacket } from "@/client/apiGen"; -import { Button, Select, theme } from "@munet/ui"; +import { Button, Select, TextInput, theme } from "@munet/ui"; import { VList } from 'virtua/vue'; import MusicEntry from "@/views/Charts/MusicList/MusicEntry"; import { assetDirs, musicList, musicSortMode, MusicSortMode, selectedADir, selectMusicId } from "@/store/refs"; import { leftPanel } from "@/views/Charts/refs"; +import { matchMusicKeyword } from "@/utils/musicSearch"; import { useI18n } from 'vue-i18n'; export const musicListRef = ref | null>(null); -export const scrollToMusic = (musicId: number) => { - const idx = musicList.value.findIndex(m => m.id === musicId); - if (idx === -1 || !musicListRef.value) return; - musicListRef.value.scrollToIndex(idx, { align: 'center', smooth: true }); +// 左侧列表的搜索词。放在模块作用域,因为跳转逻辑(scrollToMusic)要在组件之外读它。 +// 切 A-dir 时刻意不清空:列表上方的搜索框会把当前词显示出来,保留它可以比较不同目录下的同一首。 +export const musicSearchKeyword = ref(''); +// 过滤、空态、清空按钮都用 trim 后的值,免得只输入空格时三处口径不一致 +const keyword = computed(() => musicSearchKeyword.value.trim()); + +const clearMusicSearch = () => (musicSearchKeyword.value = ''); + +// 实际渲染的曲目。搜索词为空时就是当前 A-dir 的全量。 +// 不能直接过滤 musicList:selectedMusic 由它派生,把列表过滤掉会让右侧编辑面板跟着变空。 +export const displayedMusic = computed(() => + keyword.value ? musicList.value.filter(m => matchMusicKeyword(m, keyword.value)) : musicList.value +); + +// 连续跳转时只让最后一次生效,避免前一次的等待把列表滚到旧位置 +let lastJump = 0; +// 用户手动切到目录管理时,之前那次跳转已经不作数了 +const cancelPendingJump = () => { lastJump++; }; + +export const scrollToMusic = async (musicId: number) => { + const jump = ++lastJump; + + // 目标必须在当前 A-dir 里。跨目录不是这里处理的:调用方(批量页跳转)会先把 selectedADir 切到目标所在目录 + if (!musicList.value.some(m => m.id === musicId)) return; + + // 跳转的含义是「回到曲目列表并看到这首歌」:左侧停在目录管理时也要切回列表 + if (leftPanel.value !== 'musicList') leftPanel.value = 'musicList'; + + // 搜索词把目标挡住了就先清掉,否则 scrollToIndex 找不到它 + if (keyword.value && !displayedMusic.value.some(m => m.id === musicId)) { + clearMusicSearch(); + } + await nextTick(); + if (jump !== lastJump) return; + + const list = await waitForMusicList(); + if (!list || jump !== lastJump) return; + + // 等 VList 挂载的这段时间里列表可能已经变了,下标要按最新数据重算 + const idx = displayedMusic.value.findIndex(m => m.id === musicId); + if (idx === -1) return; + list.scrollToIndex(idx, { align: 'center', smooth: true }); +}; + +// 从批量操作跳过来时这个页面还在做 0.3s 的过渡,VList 尚未挂载,而调用方只等 100ms。 +// 用 setTimeout 轮询而不是 requestAnimationFrame:页面在后台时 rAF 不触发,循环会一直卡住。 +// 注意:改 Index.module.sass 的 tab 过渡时长、或 Charts/index.tsx 的左侧面板切换方式时,要回来确认这个超时还够。 +const waitForMusicList = async (timeout = 2000) => { + const start = Date.now(); + while (!musicListRef.value && Date.now() - start < timeout) { + await new Promise(resolve => setTimeout(resolve, 30)); + } + return musicListRef.value; }; export default defineComponent({ @@ -33,6 +83,8 @@ export default defineComponent({ return <>{dir.dirName}{dir.version ? ` (Ver.${dir.version})` : ''}; }; + const clearSearch = clearMusicSearch; + return () => (
@@ -41,7 +93,7 @@ export default defineComponent({
leftPanel.value = 'assetDirs'} + onClick={() => { cancelPendingJump(); leftPanel.value = 'assetDirs'; }} > {selectedDirLabel()}
@@ -51,11 +103,37 @@ export default defineComponent({ class="w-40! shrink-0" />
- - {({item}: {item: MusicXmlWithABJacket}) => ( - selectMusicId.value = item.id!} key={item.id} /> - )} - +
{ + // 输入法组词时按 Esc 是取消候选,不该顺手清空搜索框 + if (e.isComposing) return; + if (e.key === 'Escape') clearSearch(); + }} + > + + {keyword.value && + } +
+
+ + {({item}: {item: MusicXmlWithABJacket}) => ( + selectMusicId.value = item.id!} key={item.id} /> + )} + + {keyword.value && !displayedMusic.value.length && +
{t('music.list.noMatch')}
} +
) } From eae6f011dd10cdbfbbef11e9d9e36bda4075d425 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=A1=E5=9D=82=E4=BA=91=E8=A7=81?= <447f@misaka.org> Date: Sun, 1 Nov 2026 16:47:58 +0800 Subject: [PATCH 02/14] =?UTF-8?q?fix(front):=20=E4=BF=AE=201440=20?= =?UTF-8?q?=E5=AE=BD=E7=AA=97=E5=8F=A3=E4=B8=8B=E8=B0=B1=E9=9D=A2=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=B7=A6=E4=BE=A7=E9=9D=A2=E6=9D=BF=E4=B8=8D=E5=B1=95?= =?UTF-8?q?=E5=BC=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 抽屉布局原来写 max-[1440px],而汉堡按钮是 min-[1440px]:hidden,两边在正好 1440 宽时 同时命中:左侧被 translate 推出屏幕、打开它的按钮又被隐藏,谱面管理页完全没法用。 1440 宽的屏幕把窗口最大化就会踩到 - 抽屉改成 max-[1439px],与 min-[1440px] 以 1440 为界且不重叠;1440 及以上固定展开 - 1439 及以下行为不变(原来的 max-width:1440 同样覆盖 1439),只有正好 1440 从坏掉变成桌面布局 AI-assisted-by: DeepSeek-V4-Flash (DeepSeek Harness) --- MaiChartManager/Front/src/views/Charts/index.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/MaiChartManager/Front/src/views/Charts/index.tsx b/MaiChartManager/Front/src/views/Charts/index.tsx index 102557eb..f621cfec 100644 --- a/MaiChartManager/Front/src/views/Charts/index.tsx +++ b/MaiChartManager/Front/src/views/Charts/index.tsx @@ -21,13 +21,16 @@ export default defineComponent({ isForward.value = val === 'assetDirs'; }); - return () =>
+ // 抽屉布局用 max-[1439px]:抽屉与下面的 min-[1440px]:hidden(汉堡按钮)必须以 1440 为界、 + // 不能互相重叠。之前两边都写 1440,窗口宽度正好 1440(例如 1440 宽的屏幕最大化)时 + // 左侧既被推出屏幕、汉堡按钮又被隐藏,谱面管理页直接没法用。 + return () =>
Date: Sun, 1 Nov 2026 17:01:43 +0800 Subject: [PATCH 03/14] =?UTF-8?q?feat(front):=20A000=20=E7=9B=AE=E5=BD=95?= =?UTF-8?q?=E4=B8=8B=E7=BC=96=E8=BE=91=E6=8C=89=E9=92=AE=E4=BF=9D=E7=95=99?= =?UTF-8?q?=E4=BD=86=E7=A6=81=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 原来选中 A000(游戏本体目录)时,右上角直接换成一句提示文案,按钮消失,位置跟着跳 - 改成按钮始终在,用 pointer-events-none + op-50 禁用;提示改为灰色小字放在按钮下面 - 复制与导出不在此列,它不改 A000 的内容,保持可用 AI-assisted-by: DeepSeek-V4-Flash (DeepSeek Harness) --- .../Front/src/views/Charts/index.tsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/MaiChartManager/Front/src/views/Charts/index.tsx b/MaiChartManager/Front/src/views/Charts/index.tsx index f621cfec..8a8b8e4f 100644 --- a/MaiChartManager/Front/src/views/Charts/index.tsx +++ b/MaiChartManager/Front/src/views/Charts/index.tsx @@ -17,6 +17,8 @@ export default defineComponent({ const { t } = useI18n(); const mobileShowMenu = ref(false); const isForward = ref(true); + // A000 是游戏本体目录,只读:按钮保留但禁用,下面用灰字说明原因 + const isA000 = computed(() => selectedADir.value === 'A000'); watch(leftPanel, (val) => { isForward.value = val === 'assetDirs'; }); @@ -67,16 +69,16 @@ export default defineComponent({
-
- {!!selectedMusic.value &&
} - {selectedADir.value === 'A000' ? ( - t('assetDir.selectNonA000') - ) : ( - <> +
+
+ {!!selectedMusic.value &&
} + {/* A000 是游戏本体目录,不允许编辑:按钮保留但禁用,下面用灰字说明原因 */} +
- - )} +
+
+ {isA000.value &&
{t('assetDir.selectNonA000')}
}
From 8b2e529ce294d56fc41a2a64e2379ab988320aea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=A1=E5=9D=82=E4=BA=91=E8=A7=81?= <447f@misaka.org> Date: Sun, 1 Nov 2026 17:01:56 +0800 Subject: [PATCH 04/14] =?UTF-8?q?feat(front):=20=E6=9C=AA=E9=80=89?= =?UTF-8?q?=E4=B8=AD=E8=B0=B1=E9=9D=A2=E6=97=B6=E5=8F=B3=E4=BE=A7=E7=BB=99?= =?UTF-8?q?=E5=87=BA=E7=A9=BA=E6=80=81=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 没选中任何曲目时右侧编辑区原本整块空白,看不出是「没选」还是「没加载出来」 - 补一个居中的「请选择谱面以编辑」,0.5 不透明度、稍大字号(三语同步) AI-assisted-by: DeepSeek-V4-Flash (DeepSeek Harness) --- MaiChartManager/Front/src/locales/en.yaml | 1 + MaiChartManager/Front/src/locales/zh-TW.yaml | 1 + MaiChartManager/Front/src/locales/zh.yaml | 1 + MaiChartManager/Front/src/views/Charts/index.tsx | 4 +++- 4 files changed, 6 insertions(+), 1 deletion(-) diff --git a/MaiChartManager/Front/src/locales/en.yaml b/MaiChartManager/Front/src/locales/en.yaml index 8dae235c..2448beb7 100644 --- a/MaiChartManager/Front/src/locales/en.yaml +++ b/MaiChartManager/Front/src/locales/en.yaml @@ -83,6 +83,7 @@ music: sortByName: Name sortByVersion: Release edit: + selectHint: Select a chart to edit name: Song Title artist: Artist bpm: BPM diff --git a/MaiChartManager/Front/src/locales/zh-TW.yaml b/MaiChartManager/Front/src/locales/zh-TW.yaml index 82dd354f..ec8b9f68 100644 --- a/MaiChartManager/Front/src/locales/zh-TW.yaml +++ b/MaiChartManager/Front/src/locales/zh-TW.yaml @@ -81,6 +81,7 @@ music: sortByName: 名稱 sortByVersion: 更新時間 edit: + selectHint: 請選擇譜面以編輯 name: 歌曲名稱 artist: 作者 bpm: BPM diff --git a/MaiChartManager/Front/src/locales/zh.yaml b/MaiChartManager/Front/src/locales/zh.yaml index 8b519851..7e55a9be 100644 --- a/MaiChartManager/Front/src/locales/zh.yaml +++ b/MaiChartManager/Front/src/locales/zh.yaml @@ -81,6 +81,7 @@ music: sortByName: 名称 sortByVersion: 更新时间 edit: + selectHint: 请选择谱面以编辑 name: 名称 artist: 作者 bpm: BPM diff --git a/MaiChartManager/Front/src/views/Charts/index.tsx b/MaiChartManager/Front/src/views/Charts/index.tsx index 8a8b8e4f..ceb4f801 100644 --- a/MaiChartManager/Front/src/views/Charts/index.tsx +++ b/MaiChartManager/Front/src/views/Charts/index.tsx @@ -82,7 +82,9 @@ export default defineComponent({
- + {selectedMusic.value + ? + :
{t('music.edit.selectHint')}
}
)} From 63f164fa01f9822478cb8e15548a1964c291a29f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=A1=E5=9D=82=E4=BA=91=E8=A7=81?= <447f@misaka.org> Date: Sun, 1 Nov 2026 17:41:33 +0800 Subject: [PATCH 05/14] =?UTF-8?q?fix(front):=20A000=20=E5=8F=AA=E8=AF=BB?= =?UTF-8?q?=E6=94=B9=E6=88=90=E7=9C=9F=E6=AD=A3=E6=8B=A6=E4=BD=8F=EF=BC=8C?= =?UTF-8?q?=E4=B8=8D=E5=86=8D=E5=8F=AA=E9=9D=A0=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 浏览器审查指出 pointer-events-none 只挡鼠标:A000 下用 Tab 聚焦后回车、或按 Ctrl+S, 仍能触发导入/保存,会往游戏本体目录写数据。原来的「禁用」只有视觉效果。 - 新增 store.refs 的 isReadOnlyDir(selectedADir === 'A000'),所有写入口先看它 - 保存:按钮 disabled,save() 里再判一次(Ctrl+S 走的就是这个函数) - 删除 / 导入 / 创建:按钮 disabled + 入口函数早退 - 替换谱面(ChartPanel 两个入口)与封面替换(JacketBox,含拖拽路径):入口早退 - 创建曲目对话框在确认时重新判目录,堵住「打开后别处切到 A000 再确认」的竞态 验证:A000 下四个按钮均为 disabled,模拟 Ctrl+S 与点击均未产生任何写请求(用一次 性 mock 观察,未打真实后端);pnpm typecheck / build 通过。 AI-assisted-by: DeepSeek-V4-Flash (DeepSeek Harness) --- .../src/components/DragDropDispatcher/ReplaceChartModal.tsx | 3 +++ MaiChartManager/Front/src/components/JacketBox.tsx | 4 +++- MaiChartManager/Front/src/store/refs.ts | 4 ++++ .../Charts/ImportCreateChartButton/CreateMusicModal.tsx | 4 +++- .../ImportCreateChartButton/ImportChartButton/index.tsx | 6 ++++-- .../src/views/Charts/ImportCreateChartButton/index.tsx | 5 +++-- .../Front/src/views/Charts/MusicEdit/ChartPanel.tsx | 5 +++-- .../MusicSelectedTopRightToolbar/DeleteMusicButton.tsx | 6 ++++-- .../MusicSelectedTopRightToolbar/SaveButton/index.tsx | 6 ++++-- 9 files changed, 31 insertions(+), 12 deletions(-) diff --git a/MaiChartManager/Front/src/components/DragDropDispatcher/ReplaceChartModal.tsx b/MaiChartManager/Front/src/components/DragDropDispatcher/ReplaceChartModal.tsx index 6189581d..5a70a62c 100644 --- a/MaiChartManager/Front/src/components/DragDropDispatcher/ReplaceChartModal.tsx +++ b/MaiChartManager/Front/src/components/DragDropDispatcher/ReplaceChartModal.tsx @@ -4,6 +4,7 @@ import { Button, Modal, Radio, showTransactionalDialog, addToast } from '@munet/ import { computed, defineComponent, ref, shallowRef } from 'vue'; import JacketBox from '../JacketBox'; import { DIFFICULTY } from '@/consts'; +import { isReadOnlyDir } from "@/store/refs"; import api from '@/client/api'; import CheckingModal from "@/views/Charts/ImportCreateChartButton/ImportChartButton/CheckingModal"; import LevelTagsDisplay from "@/components/LevelTagsDisplay"; @@ -40,6 +41,8 @@ export default defineComponent({ // 注:本功能的逻辑是,如果选择的是ma2文件,则只替换指定难度的谱面;如果选择的是maidata,则替换整首歌的所有难度。 prepareReplaceChart = async (fHandle?: FileSystemFileHandle, side?: ChartSide) => { + // A000 只读:替换谱面是写操作,入口处直接挡掉 + if (isReadOnlyDir.value) return; targetSide.value = side; let selectedFile: File; if (!fHandle) { diff --git a/MaiChartManager/Front/src/components/JacketBox.tsx b/MaiChartManager/Front/src/components/JacketBox.tsx index e1b2e4f0..4be59e04 100644 --- a/MaiChartManager/Front/src/components/JacketBox.tsx +++ b/MaiChartManager/Front/src/components/JacketBox.tsx @@ -2,7 +2,7 @@ import { computed, defineComponent, PropType, ref } from "vue"; import noJacket from "@/assets/noJacket.webp"; import api, { getUrl } from "@/client/api"; import { showTransactionalDialog } from "@munet/ui"; -import { globalCapture, selectedADir, selectedMusic } from "@/store/refs"; +import { globalCapture, isReadOnlyDir, selectedADir, selectedMusic } from "@/store/refs"; import { MusicXmlWithABJacket } from "@/client/apiGen"; import { useI18n } from 'vue-i18n'; import { pickFile } from "@/utils/pickFile"; @@ -24,6 +24,8 @@ export default defineComponent({ if (props.upload) upload = async (fileHandle?: FileSystemFileHandle) => { if (!props.upload) return; + // A000 只读:点封面或拖拽换图都会走到这里 + if (isReadOnlyDir.value) return; try { let file: File; if (!fileHandle) { diff --git a/MaiChartManager/Front/src/store/refs.ts b/MaiChartManager/Front/src/store/refs.ts index 1a7af2fe..0c243054 100644 --- a/MaiChartManager/Front/src/store/refs.ts +++ b/MaiChartManager/Front/src/store/refs.ts @@ -77,6 +77,10 @@ export const musicList = computed(() => export const selectedMusic = computed(() => musicList.value.find(m => m.id === selectMusicId.value)); export const selectedLevel = ref(0); +// A000 是游戏本体目录,只读。所有写入入口(保存/删除/导入/创建/替换谱面)都要先看它, +// 只靠禁用样式挡不住键盘和 Ctrl+S 这类快捷操作。 +export const isReadOnlyDir = computed(() => selectedADir.value === 'A000'); + // 如ReplaceChart等后端接口可能会涉及对MusicXml中的信息进行修改后保存。此时前端updateMusicList时会发现相关数据出现变更,触发了MusicEdit/ChartPanel中的watch,造成发送多余的edit请求、同时modified也被错误设置为true。 // 因此,对于涉及内部对xml进行修改后会自动保存的后端接口,可以在updateMusicList期间打开本选项,以阻止MusicEdit/ChartPanel中的sync动作。 export const disableSync = ref(false); diff --git a/MaiChartManager/Front/src/views/Charts/ImportCreateChartButton/CreateMusicModal.tsx b/MaiChartManager/Front/src/views/Charts/ImportCreateChartButton/CreateMusicModal.tsx index 4eea2098..dfc08ec0 100644 --- a/MaiChartManager/Front/src/views/Charts/ImportCreateChartButton/CreateMusicModal.tsx +++ b/MaiChartManager/Front/src/views/Charts/ImportCreateChartButton/CreateMusicModal.tsx @@ -1,6 +1,6 @@ import { computed, defineComponent, ref, watch } from "vue"; import { Button, Modal, NumberInput, Radio } from "@munet/ui"; -import { assetDirs, musicList, selectedADir, selectMusicId, updateMusicList } from "@/store/refs"; +import { assetDirs, isReadOnlyDir, musicList, selectedADir, selectMusicId, updateMusicList } from "@/store/refs"; import dxIcon from "@/assets/dxIcon.png"; import stdIcon from "@/assets/stdIcon.png"; import api from "@/client/api"; @@ -35,6 +35,8 @@ export default defineComponent({ }) const save = async () => { + // 对话框打开期间目录可能被切到只读的 A000,保存前重新判一次 + if (isReadOnlyDir.value) return; show.value = false; await api.AddMusic(id.value, selectedADir.value); await updateMusicList(); diff --git a/MaiChartManager/Front/src/views/Charts/ImportCreateChartButton/ImportChartButton/index.tsx b/MaiChartManager/Front/src/views/Charts/ImportCreateChartButton/ImportChartButton/index.tsx index bee427b5..8e75ad4c 100644 --- a/MaiChartManager/Front/src/views/Charts/ImportCreateChartButton/ImportChartButton/index.tsx +++ b/MaiChartManager/Front/src/views/Charts/ImportCreateChartButton/ImportChartButton/index.tsx @@ -4,7 +4,7 @@ import SelectFileTypeTip from "./SelectFileTypeTip"; import { LicenseStatus, MessageLevel } from "@/client/apiGen"; import CheckingModal from "./CheckingModal"; import api, { getUrl } from "@/client/api"; -import { globalCapture, selectedADir, selectMusicId, updateMusicList, version as appVersion } from "@/store/refs"; +import { globalCapture, isReadOnlyDir, selectedADir, selectMusicId, updateMusicList, version as appVersion } from "@/store/refs"; import { appSettings } from "@/store/settings"; import ErrorDisplayIdInput from "./ErrorDisplayIdInput"; import ImportStepDisplay from "./ImportStepDisplay"; @@ -226,6 +226,8 @@ export default defineComponent({ } startProcess = async (dir?: ImportDirectory | ImportDirectory[]) => { + // A000 只读:拖拽进来的导入也要挡住,不只是点按钮 + if (isReadOnlyDir.value) return; let id = getNextUnusedMusicId(); const usedIds = [] as number[]; errors.value = []; @@ -305,7 +307,7 @@ export default defineComponent({ } } - return () => }} diff --git a/MaiChartManager/Front/src/views/Charts/MusicEdit/ChartPanel.tsx b/MaiChartManager/Front/src/views/Charts/MusicEdit/ChartPanel.tsx index a727067e..56413c23 100644 --- a/MaiChartManager/Front/src/views/Charts/MusicEdit/ChartPanel.tsx +++ b/MaiChartManager/Front/src/views/Charts/MusicEdit/ChartPanel.tsx @@ -8,6 +8,7 @@ import PreviewChartButton from "@/views/Charts/MusicEdit/PreviewChartButton"; import { useI18n } from 'vue-i18n'; import { Button, CheckBox, NumberInput, Select, TextInput } from "@munet/ui"; import { prepareReplaceChart } from "@/components/DragDropDispatcher/ReplaceChartModal"; +import { isReadOnlyDir } from "@/store/refs"; import type { ChartSide } from "@/views/Charts/MusicEdit/PreviewChartButton"; import styles from './index.module.sass'; @@ -46,7 +47,7 @@ export default defineComponent({ const sideRow = (side: ChartSide, label: string) =>
{label} -
; @@ -54,7 +55,7 @@ export default defineComponent({ return () =>
{!props.doublePlayer &&
-
} diff --git a/MaiChartManager/Front/src/views/Charts/MusicSelectedTopRightToolbar/DeleteMusicButton.tsx b/MaiChartManager/Front/src/views/Charts/MusicSelectedTopRightToolbar/DeleteMusicButton.tsx index f5a9d291..5ff789bf 100644 --- a/MaiChartManager/Front/src/views/Charts/MusicSelectedTopRightToolbar/DeleteMusicButton.tsx +++ b/MaiChartManager/Front/src/views/Charts/MusicSelectedTopRightToolbar/DeleteMusicButton.tsx @@ -1,6 +1,6 @@ import { defineComponent, ref } from "vue"; import api from "@/client/api"; -import { globalCapture, selectedADir, selectMusicId, updateMusicList } from "@/store/refs"; +import { globalCapture, isReadOnlyDir, selectedADir, selectMusicId, updateMusicList } from "@/store/refs"; import { Button, showTransactionalDialog } from "@munet/ui"; import { useI18n } from 'vue-i18n'; @@ -11,6 +11,8 @@ export default defineComponent({ const { t } = useI18n(); const del = async () => { + // A000 只读:按钮已禁用,这里再兜一层,防止键盘/程序化触发 + if (isReadOnlyDir.value) return; if (!deleteConfirm.value) { deleteConfirm.value = true; return; @@ -32,7 +34,7 @@ export default defineComponent({ } - return () => ; diff --git a/MaiChartManager/Front/src/views/Charts/MusicSelectedTopRightToolbar/SaveButton/index.tsx b/MaiChartManager/Front/src/views/Charts/MusicSelectedTopRightToolbar/SaveButton/index.tsx index a9e67449..1a1db224 100644 --- a/MaiChartManager/Front/src/views/Charts/MusicSelectedTopRightToolbar/SaveButton/index.tsx +++ b/MaiChartManager/Front/src/views/Charts/MusicSelectedTopRightToolbar/SaveButton/index.tsx @@ -1,5 +1,5 @@ import { defineComponent, ref, Teleport } from "vue"; -import { selectedADir, selectedMusic, selectMusicId, updateMusicList } from "@/store/refs"; +import { isReadOnlyDir, selectedADir, selectedMusic, selectMusicId, updateMusicList } from "@/store/refs"; import api from "@/client/api"; import animation from './animation.module.sass'; import { useMagicKeys, whenever } from '@vueuse/core' @@ -11,6 +11,8 @@ export default defineComponent({ const { t } = useI18n(); const save = async () => { + // A000 只读:Ctrl+S 也要拦住,不能只靠按钮禁用 + if (isReadOnlyDir.value) return; await api.SaveMusic(selectMusicId.value, selectedADir.value); isAnimationShow.value = true; setTimeout(() => { @@ -29,7 +31,7 @@ export default defineComponent({ whenever(ctrl_s, save); return () => selectedMusic.value && ( -
@@ -73,12 +70,12 @@ export default defineComponent({
{!!selectedMusic.value &&
} {/* A000 是游戏本体目录,不允许编辑:按钮保留但禁用,下面用灰字说明原因 */} -
+
- {isA000.value &&
{t('assetDir.selectNonA000')}
} + {isReadOnlyDir.value &&
{t('assetDir.selectNonA000')}
}
diff --git a/MaiChartManager/Front/src/views/Charts/refs.ts b/MaiChartManager/Front/src/views/Charts/refs.ts index 3d28a9ba..55588d0f 100644 --- a/MaiChartManager/Front/src/views/Charts/refs.ts +++ b/MaiChartManager/Front/src/views/Charts/refs.ts @@ -7,6 +7,10 @@ export type RightPanel = 'musicEdit' | 'memoEdit'; export const leftPanel = ref('musicList'); export const rightPanel = ref('musicEdit'); +// 窄屏下左侧列表是抽屉,默认关着。放在这里是因为「批量操作 → 跳转」要在组件之外把它拉开, +// 否则跳过去只是切了面板、抽屉还关着,用户看不到目标曲目。 +export const musicDrawerOpen = ref(false); + // memo editing state export const editingMemoDir = ref(); export const editingMemoName = ref(''); From fb6c198b912c142bdab6dc0cd07e58b9195e56f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=A1=E5=9D=82=E4=BA=91=E8=A7=81?= <447f@misaka.org> Date: Sun, 1 Nov 2026 17:48:44 +0800 Subject: [PATCH 07/14] =?UTF-8?q?fix(front):=20=E5=AF=BC=E5=85=A5=E4=B8=8E?= =?UTF-8?q?=E6=9B=BF=E6=8D=A2=E8=B0=B1=E9=9D=A2=E5=9B=BA=E5=AE=9A=E7=9B=AE?= =?UTF-8?q?=E6=A0=87=E7=9B=AE=E5=BD=95=EF=BC=8C=E4=B8=8D=E5=86=8D=E5=86=99?= =?UTF-8?q?=E5=85=A5=E6=97=B6=E9=87=8D=E8=AF=BB=E9=80=89=E4=B8=AD=E7=9B=AE?= =?UTF-8?q?=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 审查指出:入口的只读判断和真正的写入之间隔着若干异步步骤(选文件、确认弹窗、逐条处理), 而写入时又读了一次 selectedADir。期间目录被切到 A000,写操作就会落到只读目录。 - 导入:startProcess 入口快照 importDir,之后 AddMusic / ImportChart / SetAudio / SetAudioPreview / SetMusicJacket / SetMovie / 回滚用的 DeleteMusic 全部用它 - 替换谱面:prepareReplaceChart 时快照 replaceDir,确认后 ReplaceChart 用它 这样「中途切目录」只会影响下一次导入,不会把正在进行的那次带偏。 AI-assisted-by: DeepSeek-V4-Flash (DeepSeek Harness) --- .../DragDropDispatcher/ReplaceChartModal.tsx | 7 ++++++- .../ImportChartButton/index.tsx | 21 ++++++++++++------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/MaiChartManager/Front/src/components/DragDropDispatcher/ReplaceChartModal.tsx b/MaiChartManager/Front/src/components/DragDropDispatcher/ReplaceChartModal.tsx index 5a70a62c..1389e80b 100644 --- a/MaiChartManager/Front/src/components/DragDropDispatcher/ReplaceChartModal.tsx +++ b/MaiChartManager/Front/src/components/DragDropDispatcher/ReplaceChartModal.tsx @@ -24,6 +24,10 @@ export let prepareReplaceChart = async (fileHandle?: FileSystemFileHandle, side? export default defineComponent({ setup() { + // 替换一旦开始就把目标目录固定下来,确认时不能再读 selectedADir: + // 期间切到只读的 A000 会把写操作带偏 + let replaceDir = selectedADir.value; + const checking = ref(false); const file = shallowRef(null); const show = ref<"" | "ma2" | "maidata" | "failed">(""); @@ -43,6 +47,7 @@ export default defineComponent({ prepareReplaceChart = async (fHandle?: FileSystemFileHandle, side?: ChartSide) => { // A000 只读:替换谱面是写操作,入口处直接挡掉 if (isReadOnlyDir.value) return; + replaceDir = selectedADir.value; targetSide.value = side; let selectedFile: File; if (!fHandle) { @@ -89,7 +94,7 @@ export default defineComponent({ const level = replacingMaidata ? -1 : selectedLevel.value; const side = replacingMaidata ? undefined : targetSide.value; show.value = ""; - const result = (await api.ReplaceChart(selectMusicId.value, level, selectedADir.value, { + const result = (await api.ReplaceChart(selectMusicId.value, level, replaceDir, { file: uploadFile, shift: tempOption.value.shift, side, diff --git a/MaiChartManager/Front/src/views/Charts/ImportCreateChartButton/ImportChartButton/index.tsx b/MaiChartManager/Front/src/views/Charts/ImportCreateChartButton/ImportChartButton/index.tsx index 8e75ad4c..eed1f6f0 100644 --- a/MaiChartManager/Front/src/views/Charts/ImportCreateChartButton/ImportChartButton/index.tsx +++ b/MaiChartManager/Front/src/views/Charts/ImportCreateChartButton/ImportChartButton/index.tsx @@ -97,6 +97,10 @@ export default defineComponent({ return !reject; } + // 导入一旦开始就把目标目录固定下来:中途切目录(尤其是切到只读的 A000) + // 也不能把写操作带偏,所有写入都用这个快照 + let importDir = selectedADir.value; + const uploadMovie = (id: number, movie: File, offset: number) => new Promise((resolve, reject) => { currentMovieProgress.value = 0; const body = new FormData(); @@ -104,7 +108,7 @@ export default defineComponent({ body.append('file', movie); body.append('file', movie); const controller = new AbortController(); - fetchEventSource(getUrl(`SetMovieApi/${selectedADir.value}/${id}`), { + fetchEventSource(getUrl(`SetMovieApi/${importDir}/${id}`), { signal: controller.signal, method: 'PUT', body, @@ -144,7 +148,7 @@ export default defineComponent({ try { music.importStep = IMPORT_STEP.create; - const createRet = (await api.AddMusic(music.id, selectedADir.value)).data; + const createRet = (await api.AddMusic(music.id, importDir)).data; if (createRet) throw new Error(createRet); music.importStep = IMPORT_STEP.chart; @@ -163,13 +167,13 @@ export default defineComponent({ utageRightLevel: music.utageMapping.rightLevel, } : {}), debug: import.meta.env.DEV, - assetDir: selectedADir.value, + assetDir: importDir, })).data; errors.value.push(...res.errors!.map(it => ({ ...it, name: music.name }))); if (res.fatal) { try { - await api.DeleteMusic(music.id, selectedADir.value); + await api.DeleteMusic(music.id, importDir); } catch { } return; @@ -181,14 +185,14 @@ export default defineComponent({ // 音频上应该应用的延迟audioPadding = 谱面上应用的延迟chartPadding - &first let audioPadding = chartPadding - music.first; - await api.SetAudio(music.id, selectedADir.value, { file: music.track, padding: audioPadding, ignoreGapless: !!tempOptions.value.ignoreGapless }); + await api.SetAudio(music.id, importDir, { file: music.track, padding: audioPadding, ignoreGapless: !!tempOptions.value.ignoreGapless }); if (music.previewTime) { // 因为&demo_seek和&demo_len都是相对于原始音源的,所以这里的时间必须要扣掉上面SetAudio时应用的padding。 // 根据SetAudioApi的定义,padding为正表示歌曲前面被加了空白,因此原本的预览段在新的裁剪后的音频中的位置也要相应后移;padding为负反之。 // 因此,直接给startTime和endTime都加上audioPadding即可。 music.previewTime.startTime! += audioPadding; music.previewTime.endTime! += audioPadding - await api.SetAudioPreview(music.id, selectedADir.value, music.previewTime); + await api.SetAudioPreview(music.id, importDir, music.previewTime); } if (music.movie && !tempOptions.value.disableBga) { @@ -202,7 +206,7 @@ export default defineComponent({ } music.importStep = IMPORT_STEP.jacket; - if (music.bg) await api.SetMusicJacket(music.id, selectedADir.value, { file: music.bg }); + if (music.bg) await api.SetMusicJacket(music.id, importDir, { file: music.bg }); music.importStep = IMPORT_STEP.finish; } catch (e) { @@ -218,7 +222,7 @@ export default defineComponent({ // 如果是在创建乐曲这步就挂了,说明乐曲XML没有创建成功,则不需要删除乐曲。 // 否则,在ID冲突的情况下,会把原本的乐曲给删除掉,见 https://github.com/MuNET-OSS/MaiChartManager/issues/34 try { - await api.DeleteMusic(music.id, selectedADir.value); + await api.DeleteMusic(music.id, importDir); } catch { } } @@ -228,6 +232,7 @@ export default defineComponent({ startProcess = async (dir?: ImportDirectory | ImportDirectory[]) => { // A000 只读:拖拽进来的导入也要挡住,不只是点按钮 if (isReadOnlyDir.value) return; + importDir = selectedADir.value; let id = getNextUnusedMusicId(); const usedIds = [] as number[]; errors.value = []; From e2fdbc83f11d21ceb0f110389757c68eead4cca7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=A1=E5=9D=82=E4=BA=91=E8=A7=81?= <447f@misaka.org> Date: Sun, 1 Nov 2026 17:48:44 +0800 Subject: [PATCH 08/14] =?UTF-8?q?fix(front):=20=E5=B0=81=E9=9D=A2=E5=86=99?= =?UTF-8?q?=E5=85=A5=E7=94=A8=E6=9B=B2=E7=9B=AE=E8=87=AA=E8=BA=AB=E7=9B=AE?= =?UTF-8?q?=E5=BD=95=EF=BC=8CA000=20=E4=B8=8D=E6=98=BE=E7=A4=BA=E5=8F=AF?= =?UTF-8?q?=E7=82=B9=E6=89=8B=E5=9E=8B=EF=BC=8C=E6=8A=BD=E5=B1=89=E9=9A=8F?= =?UTF-8?q?=E5=8D=B8=E8=BD=BD=E5=A4=8D=E4=BD=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - JacketBox 的 SetMusicJacket 原来传的是「当前选中目录」;选文件是异步的,期间切目录会 写错地方。改成传曲目自己的 assetDir(本来也该如此) - A000 下封面不再显示 cursor-pointer:现在点了没反应,手型会让人以为操作失败 - musicDrawerOpen 是模块级状态,切到别的侧栏页再回来会保留;Charts 卸载时复位 AI-assisted-by: DeepSeek-V4-Flash (DeepSeek Harness) --- MaiChartManager/Front/src/components/JacketBox.tsx | 6 ++++-- MaiChartManager/Front/src/views/Charts/index.tsx | 5 ++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/MaiChartManager/Front/src/components/JacketBox.tsx b/MaiChartManager/Front/src/components/JacketBox.tsx index 4be59e04..ca0ff4b2 100644 --- a/MaiChartManager/Front/src/components/JacketBox.tsx +++ b/MaiChartManager/Front/src/components/JacketBox.tsx @@ -37,7 +37,8 @@ export default defineComponent({ file = await fileHandle.getFile(); } - const res = await api.SetMusicJacket(props.info.id!, selectedADir.value, { file }); + // 用曲目自己的 assetDir:选文件是异步的,期间目录可能被切走/切到 A000 + const res = await api.SetMusicJacket(props.info.id!, props.info.assetDir ?? selectedADir.value, { file }); if (res.error) { const error = res.error as any; await showTransactionalDialog(t('jacket.setFailed'), error.message || error, undefined, true); @@ -58,6 +59,7 @@ export default defineComponent({ } } - return () => upload() : undefined} /> + const canUpload = computed(() => props.upload && !isReadOnlyDir.value); + return () => upload() : undefined} /> } }) diff --git a/MaiChartManager/Front/src/views/Charts/index.tsx b/MaiChartManager/Front/src/views/Charts/index.tsx index 6ecb509d..5b382de4 100644 --- a/MaiChartManager/Front/src/views/Charts/index.tsx +++ b/MaiChartManager/Front/src/views/Charts/index.tsx @@ -1,4 +1,4 @@ -import { defineComponent, ref, Transition, watch } from 'vue'; +import { defineComponent, onUnmounted, ref, Transition, watch } from 'vue'; import MusicList from './MusicList'; import MusicEdit from './MusicEdit'; import MusicSelectedTopRightToolbar from './MusicSelectedTopRightToolbar'; @@ -20,6 +20,9 @@ export default defineComponent({ isForward.value = val === 'assetDirs'; }); + // 抽屉状态是模块级的,切到别的侧栏页再回来不该还开着 + onUnmounted(() => (musicDrawerOpen.value = false)); + // 抽屉布局用 max-[1439px]:抽屉与下面的 min-[1440px]:hidden(汉堡按钮)必须以 1440 为界、 // 不能互相重叠。之前两边都写 1440,窗口宽度正好 1440(例如 1440 宽的屏幕最大化)时 // 左侧既被推出屏幕、汉堡按钮又被隐藏,谱面管理页直接没法用。 From 0aea67808fbd518428be448f0e1761794bb4dce7 Mon Sep 17 00:00:00 2001 From: Clansty Date: Mon, 2 Nov 2026 02:20:24 +0800 Subject: [PATCH 09/14] =?UTF-8?q?fix(front):=20=E6=90=9C=E7=B4=A2=E6=B8=85?= =?UTF-8?q?=E7=A9=BA=E6=8C=89=E9=92=AE=E6=82=AC=E5=81=9C=E6=97=B6=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E9=94=99=E4=BD=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 绝对定位居中改用 top-0 bottom-0 + my-auto,不再依赖 -translate-y-1/2 - 原因:@munet/ui 全局的 button:hover { transform: scale(1.02) } 特异性更高,会把 UnoCSS 的 translate 整条顶掉,导致悬停时按钮向下掉半个高度 --- MaiChartManager/Front/src/views/Charts/MusicList/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/MaiChartManager/Front/src/views/Charts/MusicList/index.tsx b/MaiChartManager/Front/src/views/Charts/MusicList/index.tsx index 030908cb..91f95ef0 100644 --- a/MaiChartManager/Front/src/views/Charts/MusicList/index.tsx +++ b/MaiChartManager/Front/src/views/Charts/MusicList/index.tsx @@ -120,7 +120,9 @@ export default defineComponent({ /> {keyword.value &&