diff --git a/client/src/pages/EQPage.tsx b/client/src/pages/EQPage.tsx index d94c5f9..651074e 100644 --- a/client/src/pages/EQPage.tsx +++ b/client/src/pages/EQPage.tsx @@ -190,6 +190,10 @@ export default function EQPage() { const skipBandsSyncFromAbToggleRef = useRef(false); /** 本地编辑过的预设(filters / preamp / autoPre);syncPeq 拉取不会覆盖 */ const peqPresetCacheRef = useRef>({}); + /** Anchor UI selection by preset name so poll/catalog reorder won't mis-switch headphones. */ + const selectedPeqNameRef = useRef(""); + const devicePeqSelectRef = useRef(deviceState?.peqSelect); + devicePeqSelectRef.current = deviceState?.peqSelect; const headphoneMenuRef = useRef(null); const filterMenuRef = useRef(null); const bands = bandsByMode[abMode]; @@ -446,6 +450,11 @@ export default function EQPage() { }); }, []); + const selectHeadphone = useCallback((idx: number, name?: string) => { + selectedPeqNameRef.current = name ?? ""; + setHeadphoneIdx(idx); + }, []); + const syncPeqCatalogFromState = useCallback( ( remote: { peq?: PeqCatalogItem[]; peqSelect?: number }, @@ -455,14 +464,24 @@ export default function EQPage() { setPeqItems(items as typeof peqItems); setHeadphoneModels(items.map((item) => item.name)); if (items.length === 0) { + selectedPeqNameRef.current = ""; setHeadphoneIdx(0); - return; + return 0; } - const nextIdx = Math.min( - Math.max(devicePeqSelect ?? remote.peqSelect ?? 0, 0), - items.length - 1, - ); + const preferred = selectedPeqNameRef.current; + let nextIdx = + preferred !== "" + ? items.findIndex((item) => item.name === preferred) + : -1; + if (nextIdx < 0) { + nextIdx = Math.min( + Math.max(devicePeqSelect ?? remote.peqSelect ?? 0, 0), + items.length - 1, + ); + } + selectedPeqNameRef.current = items[nextIdx]?.name ?? ""; setHeadphoneIdx(nextIdx); + return nextIdx; }, [mergeRemotePeqCatalog], ); @@ -517,7 +536,7 @@ export default function EQPage() { window.clearTimeout(peqSyncTimerRef.current); peqSyncTimerRef.current = null; } - setHeadphoneIdx(idx); + selectHeadphone(idx, peqItems[idx]?.name); updateSetting({ peqSelect: idx }); setIsHeadphoneMenuOpen(false); }, @@ -527,6 +546,7 @@ export default function EQPage() { peqItems, persistPresetEditsToPeqItem, rememberPeqPresetCache, + selectHeadphone, updateSetting, ], ); @@ -545,20 +565,18 @@ export default function EQPage() { }); } lastPeqCatalogSyncKeyRef.current = buildPeqCatalogSyncKey(remote, resolvedPeqSelect); - syncPeqCatalogFromState(remote, resolvedPeqSelect); + const catalogIdx = syncPeqCatalogFromState(remote, resolvedPeqSelect); - const items = mergeRemotePeqCatalog((remote.peq ?? []) as PeqCatalogItem[]); + const items = remote.peq ?? []; if (items.length === 0) { setBandsForBothModes(DEFAULT_BANDS); setSelectedBandByMode({ A: 0, B: 0 }); return; } - const nextIdx = Math.min( - Math.max(resolvedPeqSelect, 0), - items.length - 1, + const nextBands = normalizeFiltersFromPeq( + items[catalogIdx] as { filters?: any[] | string }, ); - const nextBands = normalizeFiltersFromPeq(items[nextIdx] as { filters?: any[] | string }); if (nextBands.length > 0) { setBandsForBothModes(nextBands); } @@ -664,7 +682,7 @@ export default function EQPage() { applyPeqStateToUI(latest as { peq?: Array<{ name: string; filters?: any[] | string }>; peqSelect?: number }); const createdIndex = latest.peq?.findIndex((item) => item.name === nextName) ?? -1; if (createdIndex >= 0) { - setHeadphoneIdx(createdIndex); + selectHeadphone(createdIndex, nextName); updateSetting({ peqSelect: createdIndex }); } } else { @@ -688,7 +706,7 @@ export default function EQPage() { canDel: 1, }, ]); - setHeadphoneIdx(headphoneModels.length); + selectHeadphone(headphoneModels.length, nextName); } setIsAddPresetDialogOpen(false); toast.success(eqUi.toastAddPresetOk); @@ -737,7 +755,7 @@ export default function EQPage() { applyPeqStateToUI(latest as { peq?: Array<{ name: string; filters?: any[] | string }>; peqSelect?: number }); const createdIndex = latest.peq?.findIndex((item) => item.name === nextName) ?? -1; if (createdIndex >= 0) { - setHeadphoneIdx(createdIndex); + selectHeadphone(createdIndex, nextName); updateSetting({ peqSelect: createdIndex }); } } else { @@ -752,7 +770,7 @@ export default function EQPage() { canDel: currentPeq?.canDel ?? 1, }, ]); - setHeadphoneIdx(headphoneModels.length); + selectHeadphone(headphoneModels.length, nextName); } setIsSaveBDialogOpen(false); toast.success(eqUi.toastSaveBOk); @@ -822,7 +840,7 @@ export default function EQPage() { applyPeqStateToUI(latest as { peq?: Array<{ name: string; filters?: any[] | string }>; peqSelect?: number }); const targetIdx = latest.peq?.findIndex((item) => item.name === nextName) ?? -1; if (targetIdx >= 0) { - setHeadphoneIdx(targetIdx); + selectHeadphone(targetIdx, nextName); updateSetting({ peqSelect: targetIdx }); } } else if (isOverwrite) { @@ -844,7 +862,7 @@ export default function EQPage() { : item, ), ); - setHeadphoneIdx(existingIdx); + selectHeadphone(existingIdx, nextName); } else { setHeadphoneModels((prev) => [...prev, nextName]); setPeqItems((prev) => [ @@ -861,7 +879,7 @@ export default function EQPage() { canDel: 1, }, ]); - setHeadphoneIdx(headphoneModels.length); + selectHeadphone(headphoneModels.length, nextName); } toast.success(eqUi.importSuccess); setIsShareDialogOpen(false); @@ -891,56 +909,15 @@ export default function EQPage() { // 加载耳机列表并初始化曲线 useEffect(() => { + let cancelled = false; + async function loadHeadphones() { peqHydrationPendingRef.current = true; allowPeqRemoteSyncRef.current = false; try { - if (isDemoMode || !api) { - // 演示模式使用默认列表 - const defaultModels = [ - "Sennheiser HD 650", - "Sennheiser HD 800", - "Sony WH-1000XM5", - "AKG K701", - "Beyerdynamic DT 990", - "Audio-Technica ATH-M50x", - ]; - setHeadphoneModels(defaultModels); - setPeqItems(defaultModels.map(name => ({ name, filters: [] }))); - setCurrentRawCurve(null); - return; - } - try { - const loadedPeq = await fetchEqSyncPeq(api, "loadHeadphones"); - if (loadedPeq.peq && loadedPeq.peq.length > 0) { - const mergedPeq = mergeRemotePeqCatalog(loadedPeq.peq); - setHeadphoneModels(mergedPeq.map((h) => h.name)); - setPeqItems(mergedPeq); - const nextIdx = Math.min( - Math.max(deviceState?.peqSelect ?? loadedPeq.peqSelect ?? 0, 0), - mergedPeq.length - 1, - ); - setHeadphoneIdx(nextIdx); - lastPeqCatalogSyncKeyRef.current = buildPeqCatalogSyncKey(loadedPeq, deviceState?.peqSelect); - - // 初始化当前选中的耳机曲线 - const currentPeq = mergedPeq[nextIdx]; - if (currentPeq) { - // 修复 filters 数据(兼容不同的字段名) - const fixedFilters = normalizeFiltersFromPeq(currentPeq); - - // 更新 bands 状态 - if (fixedFilters && fixedFilters.length > 0) { - setBandsForBothModes(fixedFilters); - setSelectedBandByMode({ A: 0, B: 0 }); - } - - // raw 与图表渲染统一交给 headphoneIdx/peqItems 监听逻辑处理, - // 避免首次进入页面时重复请求 modelCurve。 - - } - } else { - // 如果没有数据,使用默认列表 + if (isDemoMode || !api) { + if (cancelled) return; + // 演示模式使用默认列表 const defaultModels = [ "Sennheiser HD 650", "Sennheiser HD 800", @@ -951,32 +928,91 @@ export default function EQPage() { ]; setHeadphoneModels(defaultModels); setPeqItems(defaultModels.map(name => ({ name, filters: [] }))); + selectHeadphone(0, defaultModels[0]); + setCurrentRawCurve(null); + return; + } + try { + const loadedPeq = await fetchEqSyncPeq(api, "loadHeadphones"); + if (cancelled) return; + if (loadedPeq.peq && loadedPeq.peq.length > 0) { + const mergedPeq = mergeRemotePeqCatalog(loadedPeq.peq); + setHeadphoneModels(mergedPeq.map((h) => h.name)); + setPeqItems(mergedPeq); + const nextIdx = Math.min( + Math.max( + devicePeqSelectRef.current ?? loadedPeq.peqSelect ?? 0, + 0, + ), + mergedPeq.length - 1, + ); + selectHeadphone(nextIdx, mergedPeq[nextIdx]?.name); + lastPeqCatalogSyncKeyRef.current = buildPeqCatalogSyncKey( + loadedPeq, + devicePeqSelectRef.current ?? loadedPeq.peqSelect, + ); + + // 初始化当前选中的耳机曲线 + const currentPeq = mergedPeq[nextIdx]; + if (currentPeq) { + // 修复 filters 数据(兼容不同的字段名) + const fixedFilters = normalizeFiltersFromPeq(currentPeq); + + // 更新 bands 状态 + if (fixedFilters && fixedFilters.length > 0) { + setBandsForBothModes(fixedFilters); + setSelectedBandByMode({ A: 0, B: 0 }); + } + + // raw 与图表渲染统一交给 headphoneIdx/peqItems 监听逻辑处理, + // 避免首次进入页面时重复请求 modelCurve。 + + } + } else { + // 如果没有数据,使用默认列表 + const defaultModels = [ + "Sennheiser HD 650", + "Sennheiser HD 800", + "Sony WH-1000XM5", + "AKG K701", + "Beyerdynamic DT 990", + "Audio-Technica ATH-M50x", + ]; + setHeadphoneModels(defaultModels); + setPeqItems(defaultModels.map(name => ({ name, filters: [] }))); + selectHeadphone(0, defaultModels[0]); + setCurrentRawCurve(null); + } + } catch { + if (cancelled) return; + // 出错时使用默认列表 + const defaultModels = [ + "Sennheiser HD 650", + "Sennheiser HD 800", + "Sony WH-1000XM5", + "AKG K701", + "Beyerdynamic DT 990", + "Audio-Technica ATH-M50x", + ]; + setHeadphoneModels(defaultModels); + setPeqItems(defaultModels.map(name => ({ name, filters: [] }))); + selectHeadphone(0, defaultModels[0]); setCurrentRawCurve(null); } - } catch { - // 出错时使用默认列表 - const defaultModels = [ - "Sennheiser HD 650", - "Sennheiser HD 800", - "Sony WH-1000XM5", - "AKG K701", - "Beyerdynamic DT 990", - "Audio-Technica ATH-M50x", - ]; - setHeadphoneModels(defaultModels); - setPeqItems(defaultModels.map(name => ({ name, filters: [] }))); - setCurrentRawCurve(null); - } } finally { + if (cancelled) return; allowPeqRemoteSyncRef.current = true; skipPeqAutoSyncRef.current = true; queueMicrotask(() => { - peqHydrationPendingRef.current = false; + if (!cancelled) peqHydrationPendingRef.current = false; }); } } - loadHeadphones(); - }, [api, isDemoMode, loadRawCurveForPeq, mergeRemotePeqCatalog, deviceState?.peqSelect]); + void loadHeadphones(); + return () => { + cancelled = true; + }; + }, [api, isDemoMode, mergeRemotePeqCatalog, selectHeadphone, setCurrentRawCurve]); /** 切换耳机型号时变;不含 filters,避免编辑时写回 peqItems 把 B 试听曲线重置 */ const headphoneSwitchKey = useMemo(() => { @@ -1786,7 +1822,7 @@ export default function EQPage() { applyPeqStateToUI(latest as { peq?: Array<{ name: string; filters?: any[] | string }>; peqSelect?: number }); const targetIdx = latest.peq?.findIndex((item) => item.name === createdName) ?? -1; if (targetIdx >= 0) { - setHeadphoneIdx(targetIdx); + selectHeadphone(targetIdx, createdName); updateSetting({ peqSelect: targetIdx }); } } else if (isOverwrite) { @@ -1808,7 +1844,7 @@ export default function EQPage() { : item, ), ); - setHeadphoneIdx(existingIdx); + selectHeadphone(existingIdx, createdName); } else { let nextIndex = 0; setPeqItems((prev) => [ @@ -1829,7 +1865,7 @@ export default function EQPage() { nextIndex = prev.length; return [...prev, createdName]; }); - setHeadphoneIdx(nextIndex); + selectHeadphone(nextIndex, createdName); } setIsBrandDrawerOpen(false); } catch { @@ -1909,6 +1945,9 @@ export default function EQPage() { }} onRenamePreset={async (oldName, newName, item) => { try { + if (selectedPeqNameRef.current === oldName) { + selectedPeqNameRef.current = newName; + } if (isDemoMode || !api) { const nextItems = peqItems.map(it => it.name === oldName ? { ...it, name: newName } : it