diff --git a/MaiChartManager/Front/src/components/DragDropDispatcher/ReplaceChartModal.tsx b/MaiChartManager/Front/src/components/DragDropDispatcher/ReplaceChartModal.tsx index 6189581d..1389e80b 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"; @@ -23,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">(""); @@ -40,6 +45,9 @@ export default defineComponent({ // 注:本功能的逻辑是,如果选择的是ma2文件,则只替换指定难度的谱面;如果选择的是maidata,则替换整首歌的所有难度。 prepareReplaceChart = async (fHandle?: FileSystemFileHandle, side?: ChartSide) => { + // A000 只读:替换谱面是写操作,入口处直接挡掉 + if (isReadOnlyDir.value) return; + replaceDir = selectedADir.value; targetSide.value = side; let selectedFile: File; if (!fHandle) { @@ -86,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/components/JacketBox.tsx b/MaiChartManager/Front/src/components/JacketBox.tsx index e1b2e4f0..ca0ff4b2 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) { @@ -35,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); @@ -56,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/locales/en.yaml b/MaiChartManager/Front/src/locales/en.yaml index 4013bce1..2448beb7 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 @@ -81,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 c0fac9de..ec8b9f68 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: 紫 @@ -79,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 5bd1d252..7e55a9be 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: 紫 @@ -79,6 +81,7 @@ music: sortByName: 名称 sortByVersion: 更新时间 edit: + selectHint: 请选择谱面以编辑 name: 名称 artist: 作者 bpm: BPM diff --git a/MaiChartManager/Front/src/store/refs.ts b/MaiChartManager/Front/src/store/refs.ts index 1a7af2fe..da4b861f 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); @@ -85,9 +89,13 @@ export { aquaMaiConfig } from "@/views/ModManager/refs"; export const saveMusicIfNeeded = async (id: number) => { if (!id) return; - const music = musicListAll.value.find(m => m.id === id); - if (!music?.modified) return; - await api.SaveMusic(id, selectedADir.value); + // 同 ID 曲目可能同时存在于多个目录(A000 与用户目录),按 id 单独取会命中别目录那条、 + // 因它的 modified 是 false 而误判成「不用保存」。modified 只会标在正在编辑的曲目上,拿它定位即可。 + const music = musicListAll.value.find(m => m.id === id && m.modified); + // 写入用曲目自己的 assetDir:这里是被失焦 / 切歌自动触发的,那一刻 selectedADir 可能已经变了, + // 切到只读的 A000 时更不能把待保存的内容写进去 + if (!music?.assetDir || music.assetDir === 'A000') return; + await api.SaveMusic(id, music.assetDir); await updateMusicList(); } diff --git a/MaiChartManager/Front/src/utils/musicSearch.ts b/MaiChartManager/Front/src/utils/musicSearch.ts index 92aba7d7..b7bffa0f 100644 --- a/MaiChartManager/Front/src/utils/musicSearch.ts +++ b/MaiChartManager/Front/src/utils/musicSearch.ts @@ -23,8 +23,12 @@ export const matchMusicKeyword = (music: MusicXmlWithABJacket, keyword: string) const value = keyword.trim().toLowerCase(); if (!value) return true; + // 列表里的 ID 按 6 位补零显示(000123),所以补零形式也要能搜到 + const rawId = (music.id ?? 0).toString(); + return (music.name ?? '').toLowerCase().includes(value) || (music.artist ?? '').toLowerCase().includes(value) - || (music.id ?? 0).toString().includes(value) + || rawId.includes(value) + || rawId.padStart(6, '0').includes(value) || (music.charts ?? []).some(chart => chart.designer?.toLowerCase().includes(value)); }; 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..eed1f6f0 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"; @@ -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 { } } @@ -226,6 +230,9 @@ 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 = []; @@ -305,7 +312,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..a4ed52fd 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'; @@ -31,7 +32,8 @@ export default defineComponent({ }) const sync = (key: keyof Chart, method: Function) => async () => { - if (disableSync.value || !props.chart) return; + // A000 只读:这里目前靠输入组件的 disabled 注入挡住,入口处再拦一层,和保存 / 删除的口径一致 + if (disableSync.value || isReadOnlyDir.value || !props.chart) return; selectedMusic.value!.modified = true; await method(props.songId, props.chartIndex, selectedADir.value, props.chart[key]!); } @@ -46,7 +48,7 @@ export default defineComponent({ const sideRow = (side: ChartSide, label: string) =>
{label} -
; @@ -54,7 +56,7 @@ export default defineComponent({ return () =>
{!props.doublePlayer &&
-
} diff --git a/MaiChartManager/Front/src/views/Charts/MusicList/index.tsx b/MaiChartManager/Front/src/views/Charts/MusicList/index.tsx index d14f8067..cc4e292c 100644 --- a/MaiChartManager/Front/src/views/Charts/MusicList/index.tsx +++ b/MaiChartManager/Front/src/views/Charts/MusicList/index.tsx @@ -1,19 +1,71 @@ -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 { leftPanel, musicDrawerOpen } 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'; + musicDrawerOpen.value = true; + + // 搜索词把目标挡住了就先清掉,否则 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 +85,8 @@ export default defineComponent({ return <>{dir.dirName}{dir.version ? ` (Ver.${dir.version})` : ''}; }; + const clearSearch = clearMusicSearch; + return () => (
@@ -41,7 +95,7 @@ export default defineComponent({
leftPanel.value = 'assetDirs'} + onClick={() => { cancelPendingJump(); leftPanel.value = 'assetDirs'; }} > {selectedDirLabel()}
@@ -51,11 +105,48 @@ 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}) => ( + { + // 用户自己点了别的曲目,之前那次还没执行的跳转就不再作数了 + cancelPendingJump(); + selectMusicId.value = item.id!; + }} /> + )} + + {keyword.value && !displayedMusic.value.length && +
{t('music.list.noMatch')}
} +
) } 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 && ( -
-
- {!!selectedMusic.value &&
} - {selectedADir.value === 'A000' ? ( - t('assetDir.selectNonA000') - ) : ( - <> +
+
+ {!!selectedMusic.value &&
} + {/* A000 是游戏本体目录,不允许编辑:按钮保留但禁用,下面用灰字说明原因 */} +
- - )} +
+
+ {isReadOnlyDir.value &&
{t('assetDir.selectNonA000')}
}
- + {selectedMusic.value + ? + :
{t('music.edit.selectHint')}
}
)} 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('');