修复 bug

This commit is contained in:
eafonyang
2026-06-12 15:58:08 +08:00
parent 56a76e68ef
commit 1b0b447eee
10 changed files with 135 additions and 20 deletions
+1
View File
@@ -142,6 +142,7 @@ export interface PeqState {
canDel?: number; canDel?: number;
brand?: string; brand?: string;
model?: string; model?: string;
form?: string;
}>; }>;
} }
+1
View File
@@ -265,6 +265,7 @@
"addPresetNameExists": "That name already exists. Choose another.", "addPresetNameExists": "That name already exists. Choose another.",
"toastAddPresetOk": "Preset added", "toastAddPresetOk": "Preset added",
"toastAddPresetFail": "Failed to add preset", "toastAddPresetFail": "Failed to add preset",
"toastPresetLimitReached": "Preset limit reached ({{max}}). Delete some presets before adding more.",
"cancel": "Cancel", "cancel": "Cancel",
"save": "Save", "save": "Save",
"tabBrands": "Brands", "tabBrands": "Brands",
+1
View File
@@ -201,6 +201,7 @@
"addPresetNameExists": "名稱已存在,請修改後再儲存", "addPresetNameExists": "名稱已存在,請修改後再儲存",
"toastAddPresetOk": "已新增預設", "toastAddPresetOk": "已新增預設",
"toastAddPresetFail": "新增預設失敗", "toastAddPresetFail": "新增預設失敗",
"toastPresetLimitReached": "預設已達上限({{max}} 個),請先清理後再操作",
"cancel": "取消", "cancel": "取消",
"save": "儲存", "save": "儲存",
"tabBrands": "品牌", "tabBrands": "品牌",
+1
View File
@@ -201,6 +201,7 @@
"addPresetNameExists": "名称已存在,请修改后再保存", "addPresetNameExists": "名称已存在,请修改后再保存",
"toastAddPresetOk": "已新增预设", "toastAddPresetOk": "已新增预设",
"toastAddPresetFail": "新增预设失败", "toastAddPresetFail": "新增预设失败",
"toastPresetLimitReached": "预设已达上限({{max}} 个),请先清理后再操作",
"cancel": "取消", "cancel": "取消",
"save": "保存", "save": "保存",
"tabBrands": "品牌", "tabBrands": "品牌",
+70 -19
View File
@@ -52,6 +52,7 @@ import {
DEFAULT_BANDS, DEFAULT_BANDS,
FILTER_TYPES, FILTER_TYPES,
FLAT_PRESET_FILTERS, FLAT_PRESET_FILTERS,
PEQ_PRESET_MAX,
} from "./eq/eqConstants"; } from "./eq/eqConstants";
import { import {
eqInterp, eqInterp,
@@ -64,7 +65,13 @@ import {
normalizeBandFreqHz, normalizeBandFreqHz,
parseBandParamInput, parseBandParamInput,
} from "./eq/eqFormatters"; } from "./eq/eqFormatters";
import { bandToPeqFilter, buildPeqCatalogSyncKey, cloneBands, getUniquePresetName } from "./eq/peqMappers"; import {
bandToPeqFilter,
buildPeqCatalogSyncKey,
cloneBands,
fetchEqSyncPeq,
getUniquePresetName,
} from "./eq/peqMappers";
import type { BandParamKind, PeqCatalogItem, PeqEqUi, PeqPresetLocalCache } from "./eq/types"; import type { BandParamKind, PeqCatalogItem, PeqEqUi, PeqPresetLocalCache } from "./eq/types";
/* ── Main Component ── */ /* ── Main Component ── */
@@ -137,6 +144,7 @@ export default function EQPage() {
gain?: number; gain?: number;
q?: number; q?: number;
} | null>(null); } | null>(null);
const bandSliderDragRef = useRef<BandParamKind | null>(null);
const [isBatchEditDialogOpen, setIsBatchEditDialogOpen] = useState(false); const [isBatchEditDialogOpen, setIsBatchEditDialogOpen] = useState(false);
const [batchEditText, setBatchEditText] = useState(""); const [batchEditText, setBatchEditText] = useState("");
type BrandDrawerTab = "brands" | "models" | "target"; type BrandDrawerTab = "brands" | "models" | "target";
@@ -282,7 +290,17 @@ export default function EQPage() {
} }
}, [eqUi]); }, [eqUi]);
const guardPeqPresetCapacity = () => {
if (peqItems.length < PEQ_PRESET_MAX) return true;
toast.error(
eqInterp(eqUi.toastPresetLimitReached, { max: PEQ_PRESET_MAX })
|| `Preset limit reached (${PEQ_PRESET_MAX}). Delete some presets first.`,
);
return false;
};
const openAddHeadsetCatalog = () => { const openAddHeadsetCatalog = () => {
if (!guardPeqPresetCapacity()) return;
setIsBrandDrawerOpen(true); setIsBrandDrawerOpen(true);
setBrandDrawerTab("brands"); setBrandDrawerTab("brands");
setBrandSearchQuery(""); setBrandSearchQuery("");
@@ -632,7 +650,7 @@ export default function EQPage() {
delete peqPresetCacheRef.current[target.name]; delete peqPresetCacheRef.current[target.name];
await api.removePeq([target.name]); await api.removePeq([target.name]);
const latest = await api.getPeqState(); const latest = await fetchEqSyncPeq(api, "deleteHeadphone");
applyPeqStateToUI(latest); applyPeqStateToUI(latest);
toast.success(eqUi.toastDeleted); toast.success(eqUi.toastDeleted);
} catch { } catch {
@@ -641,6 +659,7 @@ export default function EQPage() {
}; };
const openAddPresetDialog = () => { const openAddPresetDialog = () => {
if (!guardPeqPresetCapacity()) return;
const existingNames = headphoneModels; const existingNames = headphoneModels;
const currentName = headphoneModels[headphoneIdx] ?? "Preset"; const currentName = headphoneModels[headphoneIdx] ?? "Preset";
setCopyPresetName(getUniquePresetName(currentName, existingNames)); setCopyPresetName(getUniquePresetName(currentName, existingNames));
@@ -669,11 +688,13 @@ export default function EQPage() {
const copyBrand = currentPeq?.brand?.trim() ?? ""; const copyBrand = currentPeq?.brand?.trim() ?? "";
const copyModel = currentPeq?.model?.trim() ?? ""; const copyModel = currentPeq?.model?.trim() ?? "";
const copyForm = currentPeq?.form?.trim() ?? "";
const copyPayload: PeqChangePayload = { const copyPayload: PeqChangePayload = {
peqChange: { peqChange: {
name: nextName, name: nextName,
...(copyBrand ? { brand: copyBrand } : {}), ...(copyBrand ? { brand: copyBrand } : {}),
...(copyModel ? { model: copyModel } : {}), ...(copyModel ? { model: copyModel } : {}),
...(copyForm ? { form: copyForm } : {}),
filters: bands.map(bandToPeqFilter), filters: bands.map(bandToPeqFilter),
autoPre: currentPeq?.autoPre ?? 0, autoPre: currentPeq?.autoPre ?? 0,
preamp: currentPeq?.preamp ?? 0, preamp: currentPeq?.preamp ?? 0,
@@ -694,7 +715,7 @@ export default function EQPage() {
try { try {
await upgradePeqChange(addPresetMode === "copy" ? copyPayload : flatPayload); await upgradePeqChange(addPresetMode === "copy" ? copyPayload : flatPayload);
if (api && !isDemoMode) { if (api && !isDemoMode) {
const latest = await api.getPeqState(); const latest = await fetchEqSyncPeq(api, "addPreset");
applyPeqStateToUI(latest as { peq?: Array<{ name: string; filters?: any[] | string }>; peqSelect?: number }); applyPeqStateToUI(latest as { peq?: Array<{ name: string; filters?: any[] | string }>; peqSelect?: number });
const createdIndex = latest.peq?.findIndex((item) => item.name === nextName) ?? -1; const createdIndex = latest.peq?.findIndex((item) => item.name === nextName) ?? -1;
if (createdIndex >= 0) { if (createdIndex >= 0) {
@@ -714,6 +735,7 @@ export default function EQPage() {
name: nextName, name: nextName,
...(addPresetMode === "copy" && copyBrand ? { brand: copyBrand } : {}), ...(addPresetMode === "copy" && copyBrand ? { brand: copyBrand } : {}),
...(addPresetMode === "copy" && copyModel ? { model: copyModel } : {}), ...(addPresetMode === "copy" && copyModel ? { model: copyModel } : {}),
...(addPresetMode === "copy" && copyForm ? { form: copyForm } : {}),
filters: localFilters, filters: localFilters,
autoPre: localAutoPre, autoPre: localAutoPre,
preamp: localPreamp, preamp: localPreamp,
@@ -755,7 +777,7 @@ export default function EQPage() {
try { try {
await upgradePeqChange(payload); await upgradePeqChange(payload);
if (api && !isDemoMode) { if (api && !isDemoMode) {
const latest = await api.getPeqState(); const latest = await fetchEqSyncPeq(api, "saveBAsPreset");
applyPeqStateToUI(latest as { peq?: Array<{ name: string; filters?: any[] | string }>; peqSelect?: number }); applyPeqStateToUI(latest as { peq?: Array<{ name: string; filters?: any[] | string }>; peqSelect?: number });
const createdIndex = latest.peq?.findIndex((item) => item.name === nextName) ?? -1; const createdIndex = latest.peq?.findIndex((item) => item.name === nextName) ?? -1;
if (createdIndex >= 0) { if (createdIndex >= 0) {
@@ -902,7 +924,7 @@ export default function EQPage() {
return; return;
} }
try { try {
const loadedPeq = await api.getPeqState(); const loadedPeq = await fetchEqSyncPeq(api, "loadHeadphones");
if (loadedPeq.peq && loadedPeq.peq.length > 0) { if (loadedPeq.peq && loadedPeq.peq.length > 0) {
syncPeqCatalog(loadedPeq); syncPeqCatalog(loadedPeq);
setHeadphoneModels(loadedPeq.peq.map((h) => h.name)); setHeadphoneModels(loadedPeq.peq.map((h) => h.name));
@@ -1259,6 +1281,37 @@ export default function EQPage() {
[setBands], [setBands],
); );
const clearBandSliderPreviewField = useCallback((kind: BandParamKind) => {
setBandSliderPreview((prev) => {
if (prev?.[kind] === undefined) return prev;
const next = { ...prev };
delete next[kind];
return next.freq !== undefined || next.gain !== undefined || next.q !== undefined ? next : null;
});
}, []);
const beginBandSliderDrag = useCallback((kind: BandParamKind) => {
bandSliderDragRef.current = kind;
}, []);
const endBandSliderDrag = useCallback(
(kind: BandParamKind) => {
if (bandSliderDragRef.current !== kind) return;
bandSliderDragRef.current = null;
clearBandSliderPreviewField(kind);
},
[clearBandSliderPreviewField],
);
const handleBandSliderChange = useCallback(
(kind: BandParamKind, value: number) => {
if (bandSliderDragRef.current !== kind) return;
setBandSliderPreview((prev) => ({ ...prev, [kind]: value }));
updateBand(selectedBand, { [kind]: value });
},
[selectedBand, updateBand],
);
const bandParamDialogMeta = useMemo(() => { const bandParamDialogMeta = useMemo(() => {
if (!bandParamDialog) return null; if (!bandParamDialog) return null;
const rangeMessages = { const rangeMessages = {
@@ -1328,9 +1381,11 @@ export default function EQPage() {
toast.error(parsed.message); toast.error(parsed.message);
return; return;
} }
bandSliderDragRef.current = null;
if (bandParamDialog === "freq") updateBand(selectedBand, { freq: parsed.value }); if (bandParamDialog === "freq") updateBand(selectedBand, { freq: parsed.value });
else if (bandParamDialog === "gain") updateBand(selectedBand, { gain: parsed.value }); else if (bandParamDialog === "gain") updateBand(selectedBand, { gain: parsed.value });
else updateBand(selectedBand, { q: parsed.value }); else updateBand(selectedBand, { q: parsed.value });
setBandSliderPreview(null);
closeBandParamDialog(); closeBandParamDialog();
}; };
@@ -1604,11 +1659,9 @@ export default function EQPage() {
min={Math.log10(BAND_FREQ_MIN)} min={Math.log10(BAND_FREQ_MIN)}
max={Math.log10(BAND_FREQ_MAX)} max={Math.log10(BAND_FREQ_MAX)}
step={0.005} step={0.005}
onChange={(v) => { onInteractionStart={() => beginBandSliderDrag("freq")}
const freq = Math.round(Math.pow(10, v)); onInteractionEnd={() => endBandSliderDrag("freq")}
setBandSliderPreview((prev) => ({ ...prev, freq })); onChange={(v) => handleBandSliderChange("freq", Math.round(Math.pow(10, v)))}
updateBand(selectedBand, { freq });
}}
/> />
</div> </div>
@@ -1630,10 +1683,9 @@ export default function EQPage() {
min={BAND_GAIN_MIN} min={BAND_GAIN_MIN}
max={BAND_GAIN_MAX} max={BAND_GAIN_MAX}
step={0.1} step={0.1}
onChange={(v) => { onInteractionStart={() => beginBandSliderDrag("gain")}
setBandSliderPreview((prev) => ({ ...prev, gain: v })); onInteractionEnd={() => endBandSliderDrag("gain")}
updateBand(selectedBand, { gain: v }); onChange={(v) => handleBandSliderChange("gain", v)}
}}
/> />
</div> </div>
@@ -1655,10 +1707,9 @@ export default function EQPage() {
min={BAND_Q_MIN} min={BAND_Q_MIN}
max={BAND_Q_MAX} max={BAND_Q_MAX}
step={0.01} step={0.01}
onChange={(v) => { onInteractionStart={() => beginBandSliderDrag("q")}
setBandSliderPreview((prev) => ({ ...prev, q: v })); onInteractionEnd={() => endBandSliderDrag("q")}
updateBand(selectedBand, { q: v }); onChange={(v) => handleBandSliderChange("q", v)}
}}
/> />
</div> </div>
</div> </div>
@@ -2189,7 +2240,7 @@ export default function EQPage() {
await upgradePeqChange(postPeq); await upgradePeqChange(postPeq);
const createdName = postPeq.peqChange.name; const createdName = postPeq.peqChange.name;
if (api && !isDemoMode) { if (api && !isDemoMode) {
const latest = await api.getPeqState(); const latest = await fetchEqSyncPeq(api, "addCatalogPreset");
applyPeqStateToUI(latest as { peq?: Array<{ name: string; filters?: any[] | string }>; peqSelect?: number }); applyPeqStateToUI(latest as { peq?: Array<{ name: string; filters?: any[] | string }>; peqSelect?: number });
const createdIndex = latest.peq?.findIndex((item) => item.name === createdName) ?? -1; const createdIndex = latest.peq?.findIndex((item) => item.name === createdName) ?? -1;
if (createdIndex >= 0) { if (createdIndex >= 0) {
@@ -15,6 +15,8 @@ export function CyanSlider({
max, max,
step = 1, step = 1,
onChange, onChange,
onInteractionStart,
onInteractionEnd,
disabled = false, disabled = false,
}: { }: {
value: number; value: number;
@@ -22,6 +24,8 @@ export function CyanSlider({
max: number; max: number;
step?: number; step?: number;
onChange: (v: number) => void; onChange: (v: number) => void;
onInteractionStart?: () => void;
onInteractionEnd?: () => void;
disabled?: boolean; disabled?: boolean;
}) { }) {
const fillPct = Math.max(0, Math.min(100, ((value - min) / (max - min)) * 100)); const fillPct = Math.max(0, Math.min(100, ((value - min) / (max - min)) * 100));
@@ -43,8 +47,12 @@ export function CyanSlider({
value={value} value={value}
className="cyan-slider relative z-10" className="cyan-slider relative z-10"
disabled={disabled} disabled={disabled}
onPointerDown={() => onInteractionStart?.()}
onFocus={() => onInteractionStart?.()}
onPointerUp={() => onInteractionEnd?.()}
onPointerCancel={() => onInteractionEnd?.()}
onBlur={() => onInteractionEnd?.()}
onChange={(e) => onChange(Number(e.target.value))} onChange={(e) => onChange(Number(e.target.value))}
onInput={(e) => onChange(Number(e.currentTarget.value))}
/> />
</div> </div>
); );
+3
View File
@@ -11,6 +11,9 @@ export const BAND_GAIN_MAX = 15;
export const BAND_Q_MIN = 0.1; export const BAND_Q_MIN = 0.1;
export const BAND_Q_MAX = 10; export const BAND_Q_MAX = 10;
/** Maximum headphone PEQ presets stored on device. */
export const PEQ_PRESET_MAX = 50;
export const BAND_PARAM_VALUE_BOX_STYLE: CSSProperties = { export const BAND_PARAM_VALUE_BOX_STYLE: CSSProperties = {
background: "rgba(44,44,46,0.9)", background: "rgba(44,44,46,0.9)",
border: "1px solid rgba(255,255,255,0.08)", border: "1px solid rgba(255,255,255,0.08)",
+15
View File
@@ -6,6 +6,21 @@ export function cloneBands(source: EqBand[]) {
return source.map((band) => ({ ...band })); return source.map((band) => ({ ...band }));
} }
/** Dev: log decoded syncPeq JSON after EQ page fetches device PEQ state. */
export function logEqSyncPeqDev(source: string, data: PeqState): void {
if (!import.meta.env.DEV) return;
console.log(`[EQ syncPeq] ${source}`, data);
}
export async function fetchEqSyncPeq(
api: { getPeqState: () => Promise<PeqState> },
source: string,
): Promise<PeqState> {
const data = await api.getPeqState();
logEqSyncPeqDev(source, data);
return data;
}
export function buildPeqCatalogSyncKey( export function buildPeqCatalogSyncKey(
remote: Pick<PeqState, "peq" | "peqSelect"> | null | undefined, remote: Pick<PeqState, "peq" | "peqSelect"> | null | undefined,
devicePeqSelect?: number, devicePeqSelect?: number,
+18
View File
@@ -8,6 +8,23 @@ import { vitePluginManusRuntime } from "vite-plugin-manus-runtime";
const PROJECT_ROOT = import.meta.dirname; const PROJECT_ROOT = import.meta.dirname;
/** Dev: / → i.modern.html; /i.html → UA router entry */
function vitePluginModernDevEntry(): Plugin {
return {
name: "modern-dev-entry",
apply: "serve",
configureServer(server) {
server.middlewares.use((req, _res, next) => {
const url = (req.url ?? "").split("?")[0];
if (url === "/" || url === "/index.html") {
req.url = "/i.modern.html";
}
next();
});
},
};
}
/** 构建后将统一入口 i.html 复制到 dist/ 根目录 */ /** 构建后将统一入口 i.html 复制到 dist/ 根目录 */
function vitePluginCopyRouterEntry(): Plugin { function vitePluginCopyRouterEntry(): Plugin {
const src = path.resolve(import.meta.dirname, "client", "i.html"); const src = path.resolve(import.meta.dirname, "client", "i.html");
@@ -24,6 +41,7 @@ function vitePluginCopyRouterEntry(): Plugin {
} }
const plugins = [ const plugins = [
vitePluginModernDevEntry(),
react(), react(),
tailwindcss(), tailwindcss(),
jsxLocPlugin(), jsxLocPlugin(),
+16
View File
@@ -79,8 +79,24 @@ export default defineConfig({
input: { input: {
i: path.resolve(PROJECT_ROOT, "client", "i.legacy.html"), i: path.resolve(PROJECT_ROOT, "client", "i.legacy.html"),
}, },
output: {
manualChunks(id) {
if (!id.includes("node_modules")) return;
// Match modern build: echarts is lazy-loaded on EQ/Effects pages.
if (
id.includes("/echarts/") ||
id.includes("/zrender/") ||
id.includes("\\echarts\\") ||
id.includes("\\zrender\\")
) {
return "echarts-vendor";
}
}, },
}, },
},
// streamdown pulls @shikijs/langs + mermaid; both lazy-loaded with AIPage.
chunkSizeWarningLimit: 15000,
},
server: { server: {
port: 5173, port: 5173,
strictPort: false, strictPort: false,