Refactor EQPage component to enhance PEQ preset management by introducing a local cache for presets, improving frequency normalization and display functions, and updating related state handling methods. Add real-time slider preview for better user experience.
This commit is contained in:
+113
-43
@@ -56,13 +56,16 @@ import {
|
|||||||
import {
|
import {
|
||||||
eqInterp,
|
eqInterp,
|
||||||
formatBandFreqDisplay,
|
formatBandFreqDisplay,
|
||||||
|
formatBandGainDisplay,
|
||||||
formatBandParamForInput,
|
formatBandParamForInput,
|
||||||
|
formatBandQDisplay,
|
||||||
freqLabel,
|
freqLabel,
|
||||||
normalizeFilterType,
|
normalizeFilterType,
|
||||||
|
normalizeBandFreqHz,
|
||||||
parseBandParamInput,
|
parseBandParamInput,
|
||||||
} from "./eq/eqFormatters";
|
} from "./eq/eqFormatters";
|
||||||
import { bandToPeqFilter, buildPeqCatalogSyncKey, cloneBands, getUniquePresetName } from "./eq/peqMappers";
|
import { bandToPeqFilter, buildPeqCatalogSyncKey, cloneBands, getUniquePresetName } from "./eq/peqMappers";
|
||||||
import type { BandParamKind, PeqCatalogItem, PeqEqUi } from "./eq/types";
|
import type { BandParamKind, PeqCatalogItem, PeqEqUi, PeqPresetLocalCache } from "./eq/types";
|
||||||
|
|
||||||
/* ── Main Component ── */
|
/* ── Main Component ── */
|
||||||
export default function EQPage() {
|
export default function EQPage() {
|
||||||
@@ -126,6 +129,12 @@ export default function EQPage() {
|
|||||||
const [saveBPresetName, setSaveBPresetName] = useState("");
|
const [saveBPresetName, setSaveBPresetName] = useState("");
|
||||||
const [bandParamDialog, setBandParamDialog] = useState<BandParamKind | null>(null);
|
const [bandParamDialog, setBandParamDialog] = useState<BandParamKind | null>(null);
|
||||||
const [bandParamInput, setBandParamInput] = useState("");
|
const [bandParamInput, setBandParamInput] = useState("");
|
||||||
|
/** 滑块拖动时的实时显示(避免按钮文字滞后于 bands 状态) */
|
||||||
|
const [bandSliderPreview, setBandSliderPreview] = useState<{
|
||||||
|
freq?: number;
|
||||||
|
gain?: number;
|
||||||
|
q?: number;
|
||||||
|
} | 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";
|
||||||
@@ -155,8 +164,8 @@ export default function EQPage() {
|
|||||||
const skipPeqAutoSyncRef = useRef(false);
|
const skipPeqAutoSyncRef = useRef(false);
|
||||||
/** A/B 切换已单独下发,避免 bands 变更触发的 effect 用错 peqChange/peqApply */
|
/** A/B 切换已单独下发,避免 bands 变更触发的 effect 用错 peqChange/peqApply */
|
||||||
const skipBandsSyncFromAbToggleRef = useRef(false);
|
const skipBandsSyncFromAbToggleRef = useRef(false);
|
||||||
/** 本地编辑过的预设 filters(按名称);syncPeq 拉取不会覆盖 */
|
/** 本地编辑过的预设(filters / preamp / autoPre);syncPeq 拉取不会覆盖 */
|
||||||
const peqFiltersCacheRef = useRef<Record<string, PeqFilter[]>>({});
|
const peqPresetCacheRef = useRef<Record<string, PeqPresetLocalCache>>({});
|
||||||
const headphoneMenuRef = useRef<HTMLDivElement | null>(null);
|
const headphoneMenuRef = useRef<HTMLDivElement | null>(null);
|
||||||
const filterMenuRef = useRef<HTMLDivElement | null>(null);
|
const filterMenuRef = useRef<HTMLDivElement | null>(null);
|
||||||
const bands = bandsByMode[abMode];
|
const bands = bandsByMode[abMode];
|
||||||
@@ -325,9 +334,10 @@ export default function EQPage() {
|
|||||||
filters = peq.filters;
|
filters = peq.filters;
|
||||||
}
|
}
|
||||||
return filters.map((f: any) => {
|
return filters.map((f: any) => {
|
||||||
const freq = f.fc || f.freq || f.frequency || 1000;
|
const rawFreq = f.fc ?? f.freq ?? f.frequency ?? 1000;
|
||||||
|
const freq = normalizeBandFreqHz(rawFreq) ?? 1000;
|
||||||
return {
|
return {
|
||||||
freq: Number(freq),
|
freq: Math.round(freq),
|
||||||
gain: Number(f.gain || 0),
|
gain: Number(f.gain || 0),
|
||||||
q: Number(f.q || 1),
|
q: Number(f.q || 1),
|
||||||
type: normalizeFilterType(f.type),
|
type: normalizeFilterType(f.type),
|
||||||
@@ -445,7 +455,7 @@ export default function EQPage() {
|
|||||||
preamp: parsed.preamp,
|
preamp: parsed.preamp,
|
||||||
filters,
|
filters,
|
||||||
};
|
};
|
||||||
rememberPeqFiltersForPreset(item.name, filters);
|
rememberPeqPresetCache(item.name, { filters, preamp: parsed.preamp, autoPre: 0 });
|
||||||
updatedPeq = merged;
|
updatedPeq = merged;
|
||||||
return merged;
|
return merged;
|
||||||
})
|
})
|
||||||
@@ -460,18 +470,27 @@ export default function EQPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const rememberPeqFiltersForPreset = useCallback(
|
const rememberPeqPresetCache = useCallback(
|
||||||
(presetName: string | undefined, filters: PeqFilter[]) => {
|
(presetName: string | undefined, patch: PeqPresetLocalCache) => {
|
||||||
if (!presetName || filters.length === 0) return;
|
if (!presetName) return;
|
||||||
peqFiltersCacheRef.current[presetName] = filters;
|
const prev = peqPresetCacheRef.current[presetName] ?? {};
|
||||||
|
const next: PeqPresetLocalCache = { ...prev, ...patch };
|
||||||
|
if (next.filters?.length === 0) delete next.filters;
|
||||||
|
peqPresetCacheRef.current[presetName] = next;
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const mergeRemotePeqCatalog = useCallback((items: PeqCatalogItem[]) => {
|
const mergeRemotePeqCatalog = useCallback((items: PeqCatalogItem[]) => {
|
||||||
return items.map((item) => {
|
return items.map((item) => {
|
||||||
const cached = peqFiltersCacheRef.current[item.name];
|
const cached = peqPresetCacheRef.current[item.name];
|
||||||
return cached?.length ? { ...item, filters: cached } : item;
|
if (!cached) return item;
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
...(cached.filters?.length ? { filters: cached.filters } : {}),
|
||||||
|
...(cached.preamp !== undefined ? { preamp: cached.preamp } : {}),
|
||||||
|
...(cached.autoPre !== undefined ? { autoPre: cached.autoPre } : {}),
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -496,19 +515,33 @@ export default function EQPage() {
|
|||||||
[mergeRemotePeqCatalog],
|
[mergeRemotePeqCatalog],
|
||||||
);
|
);
|
||||||
|
|
||||||
/** 将当前 A 曲线写回 peqItems 与本地缓存,切换耳机时才能恢复编辑结果 */
|
/** 将当前 A 曲线与总增益写回 peqItems 与本地缓存,切换耳机时才能恢复编辑结果 */
|
||||||
const persistFiltersToPeqItem = useCallback(
|
const persistPresetEditsToPeqItem = useCallback(
|
||||||
(catalogIdx: number, filtersSource: typeof DEFAULT_BANDS) => {
|
(
|
||||||
|
catalogIdx: number,
|
||||||
|
filtersSource: typeof DEFAULT_BANDS,
|
||||||
|
meta?: { preamp?: number; autoPre?: number },
|
||||||
|
) => {
|
||||||
const filters = filtersSource.map(bandToPeqFilter);
|
const filters = filtersSource.map(bandToPeqFilter);
|
||||||
setPeqItems((prev) =>
|
setPeqItems((prev) =>
|
||||||
prev.map((item, i) => {
|
prev.map((item, i) => {
|
||||||
if (i !== catalogIdx) return item;
|
if (i !== catalogIdx) return item;
|
||||||
rememberPeqFiltersForPreset(item.name, filters);
|
const merged = {
|
||||||
return { ...item, filters };
|
...item,
|
||||||
|
filters,
|
||||||
|
...(meta?.preamp !== undefined ? { preamp: meta.preamp } : {}),
|
||||||
|
...(meta?.autoPre !== undefined ? { autoPre: meta.autoPre } : {}),
|
||||||
|
};
|
||||||
|
rememberPeqPresetCache(item.name, {
|
||||||
|
filters,
|
||||||
|
preamp: merged.preamp,
|
||||||
|
autoPre: merged.autoPre,
|
||||||
|
});
|
||||||
|
return merged;
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[rememberPeqFiltersForPreset],
|
[rememberPeqPresetCache],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSelectHeadphone = useCallback(
|
const handleSelectHeadphone = useCallback(
|
||||||
@@ -519,8 +552,15 @@ export default function EQPage() {
|
|||||||
}
|
}
|
||||||
const leaving = peqItems[headphoneIdx];
|
const leaving = peqItems[headphoneIdx];
|
||||||
const leavingFilters = bandsByMode.A.map(bandToPeqFilter);
|
const leavingFilters = bandsByMode.A.map(bandToPeqFilter);
|
||||||
rememberPeqFiltersForPreset(leaving?.name, leavingFilters);
|
rememberPeqPresetCache(leaving?.name, {
|
||||||
persistFiltersToPeqItem(headphoneIdx, bandsByMode.A);
|
filters: leavingFilters,
|
||||||
|
preamp: leaving?.preamp,
|
||||||
|
autoPre: leaving?.autoPre,
|
||||||
|
});
|
||||||
|
persistPresetEditsToPeqItem(headphoneIdx, bandsByMode.A, {
|
||||||
|
preamp: leaving?.preamp,
|
||||||
|
autoPre: leaving?.autoPre,
|
||||||
|
});
|
||||||
if (peqSyncTimerRef.current !== null) {
|
if (peqSyncTimerRef.current !== null) {
|
||||||
window.clearTimeout(peqSyncTimerRef.current);
|
window.clearTimeout(peqSyncTimerRef.current);
|
||||||
peqSyncTimerRef.current = null;
|
peqSyncTimerRef.current = null;
|
||||||
@@ -533,8 +573,8 @@ export default function EQPage() {
|
|||||||
bandsByMode.A,
|
bandsByMode.A,
|
||||||
headphoneIdx,
|
headphoneIdx,
|
||||||
peqItems,
|
peqItems,
|
||||||
persistFiltersToPeqItem,
|
persistPresetEditsToPeqItem,
|
||||||
rememberPeqFiltersForPreset,
|
rememberPeqPresetCache,
|
||||||
updateSetting,
|
updateSetting,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -580,13 +620,13 @@ export default function EQPage() {
|
|||||||
try {
|
try {
|
||||||
if (isDemoMode || !api) {
|
if (isDemoMode || !api) {
|
||||||
const nextItems = peqItems.filter((_, idx) => idx !== headphoneIdx);
|
const nextItems = peqItems.filter((_, idx) => idx !== headphoneIdx);
|
||||||
delete peqFiltersCacheRef.current[target.name];
|
delete peqPresetCacheRef.current[target.name];
|
||||||
applyPeqStateToUI({ peq: nextItems, peqSelect: Math.max(0, headphoneIdx - 1) });
|
applyPeqStateToUI({ peq: nextItems, peqSelect: Math.max(0, headphoneIdx - 1) });
|
||||||
toast.success(eqUi.toastDeleted);
|
toast.success(eqUi.toastDeleted);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
delete peqFiltersCacheRef.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 api.getPeqState();
|
||||||
applyPeqStateToUI(latest);
|
applyPeqStateToUI(latest);
|
||||||
@@ -973,7 +1013,10 @@ export default function EQPage() {
|
|||||||
const usePeqChange =
|
const usePeqChange =
|
||||||
topLevel === "peqChange" || (topLevel === "byMode" && mode === "A");
|
topLevel === "peqChange" || (topLevel === "byMode" && mode === "A");
|
||||||
if (usePeqChange) {
|
if (usePeqChange) {
|
||||||
persistFiltersToPeqItem(idxAtSchedule, filtersSource);
|
persistPresetEditsToPeqItem(idxAtSchedule, filtersSource, {
|
||||||
|
preamp: peq.preamp,
|
||||||
|
autoPre: peq.autoPre,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const submit = usePeqChange
|
const submit = usePeqChange
|
||||||
? () => upgradePeqChange({ peqChange: body } satisfies PeqChangePayload)
|
? () => upgradePeqChange({ peqChange: body } satisfies PeqChangePayload)
|
||||||
@@ -983,7 +1026,7 @@ export default function EQPage() {
|
|||||||
});
|
});
|
||||||
}, delay);
|
}, delay);
|
||||||
},
|
},
|
||||||
[api, headphoneIdx, isDemoMode, persistFiltersToPeqItem, upgradePeqApply, upgradePeqChange],
|
[api, headphoneIdx, isDemoMode, persistPresetEditsToPeqItem, upgradePeqApply, upgradePeqChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
/** 点击 A/B 切换试听:顶层固定 peqApply(A/B 数据结构相同,仅 filters 不同) */
|
/** 点击 A/B 切换试听:顶层固定 peqApply(A/B 数据结构相同,仅 filters 不同) */
|
||||||
@@ -1045,7 +1088,11 @@ export default function EQPage() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const chartRef = useRef<echarts.ECharts | null>(null);
|
const chartRef = useRef<echarts.ECharts | null>(null);
|
||||||
const band = bands[selectedBand];
|
const band = bands[selectedBand] ?? DEFAULT_BANDS[selectedBand] ?? DEFAULT_BANDS[0];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setBandSliderPreview(null);
|
||||||
|
}, [selectedBand, abMode, headphoneSwitchKey]);
|
||||||
|
|
||||||
// Render frequency response chart using ECharts
|
// Render frequency response chart using ECharts
|
||||||
const renderCharts = (peqFilters: typeof bands, raw: number[] | null, changeParam: boolean) => {
|
const renderCharts = (peqFilters: typeof bands, raw: number[] | null, changeParam: boolean) => {
|
||||||
@@ -1146,7 +1193,11 @@ export default function EQPage() {
|
|||||||
skipPeqAutoSyncRef.current = true;
|
skipPeqAutoSyncRef.current = true;
|
||||||
setBandsByMode((prev) => ({ ...prev, A: cloneBands(bBands) }));
|
setBandsByMode((prev) => ({ ...prev, A: cloneBands(bBands) }));
|
||||||
setSelectedBandByMode((prev) => ({ ...prev, A: prev.B }));
|
setSelectedBandByMode((prev) => ({ ...prev, A: prev.B }));
|
||||||
rememberPeqFiltersForPreset(peq.name, filters);
|
rememberPeqPresetCache(peq.name, {
|
||||||
|
filters,
|
||||||
|
preamp: peq.preamp,
|
||||||
|
autoPre: peq.autoPre,
|
||||||
|
});
|
||||||
setPeqItems((prev) =>
|
setPeqItems((prev) =>
|
||||||
prev.map((item, i) => (i !== headphoneIdx ? item : { ...item, filters })),
|
prev.map((item, i) => (i !== headphoneIdx ? item : { ...item, filters })),
|
||||||
);
|
);
|
||||||
@@ -1177,10 +1228,12 @@ export default function EQPage() {
|
|||||||
upgradePeqChange,
|
upgradePeqChange,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const updateBand = (idx: number, patch: Partial<typeof bands[0]>) => {
|
const updateBand = useCallback(
|
||||||
const nextBands = bands.map((b, i) => (i === idx ? { ...b, ...patch } : b));
|
(idx: number, patch: Partial<typeof DEFAULT_BANDS[0]>) => {
|
||||||
setBands(nextBands);
|
setBands((prevBands) => prevBands.map((b, i) => (i === idx ? { ...b, ...patch } : b)));
|
||||||
};
|
},
|
||||||
|
[setBands],
|
||||||
|
);
|
||||||
|
|
||||||
const bandParamDialogMeta = useMemo(() => {
|
const bandParamDialogMeta = useMemo(() => {
|
||||||
if (!bandParamDialog) return null;
|
if (!bandParamDialog) return null;
|
||||||
@@ -1264,6 +1317,7 @@ export default function EQPage() {
|
|||||||
if (i !== headphoneIdx) return item;
|
if (i !== headphoneIdx) return item;
|
||||||
const merged = { ...item, ...patch };
|
const merged = { ...item, ...patch };
|
||||||
updatedPeq = merged;
|
updatedPeq = merged;
|
||||||
|
rememberPeqPresetCache(item.name, patch);
|
||||||
return merged;
|
return merged;
|
||||||
});
|
});
|
||||||
return next;
|
return next;
|
||||||
@@ -1436,7 +1490,12 @@ export default function EQPage() {
|
|||||||
background: "rgba(44,44,46,0.65)",
|
background: "rgba(44,44,46,0.65)",
|
||||||
}}
|
}}
|
||||||
onClick={() => setSelectedBand(i)}>
|
onClick={() => setSelectedBand(i)}>
|
||||||
<span className="text-[12px] font-bold leading-tight lg:text-[11px]">{freqLabel(b.freq)}</span>
|
<span
|
||||||
|
className="notranslate text-[12px] font-bold leading-tight tabular-nums lg:text-[11px]"
|
||||||
|
translate="no"
|
||||||
|
>
|
||||||
|
{freqLabel(b.freq)}
|
||||||
|
</span>
|
||||||
<span className="text-[9px] mt-0.5 opacity-70 leading-tight lg:text-[8px] lg:mt-0">{getFilterShortName(b.type)}</span>
|
<span className="text-[9px] mt-0.5 opacity-70 leading-tight lg:text-[8px] lg:mt-0">{getFilterShortName(b.type)}</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
@@ -1508,18 +1567,23 @@ export default function EQPage() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => openBandParamDialog("freq")}
|
onClick={() => openBandParamDialog("freq")}
|
||||||
className="text-[13px] font-semibold px-2.5 py-1 rounded-[8px] text-white active:scale-95 transition-transform"
|
className="notranslate text-[13px] font-semibold tabular-nums px-2.5 py-1 rounded-[8px] text-white active:scale-95 transition-transform"
|
||||||
style={BAND_PARAM_VALUE_BOX_STYLE}
|
style={BAND_PARAM_VALUE_BOX_STYLE}
|
||||||
|
translate="no"
|
||||||
>
|
>
|
||||||
{formatBandFreqDisplay(band.freq)}
|
{formatBandFreqDisplay(bandSliderPreview?.freq ?? band.freq)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<CyanSlider
|
<CyanSlider
|
||||||
value={Math.log10(band.freq)}
|
value={Math.log10(bandSliderPreview?.freq ?? band.freq)}
|
||||||
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) => updateBand(selectedBand, { freq: Math.round(Math.pow(10, v)) })}
|
onChange={(v) => {
|
||||||
|
const freq = Math.round(Math.pow(10, v));
|
||||||
|
setBandSliderPreview((prev) => ({ ...prev, freq }));
|
||||||
|
updateBand(selectedBand, { freq });
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1533,15 +1597,18 @@ export default function EQPage() {
|
|||||||
className="text-[13px] font-semibold px-2.5 py-1 rounded-[8px] text-white active:scale-95 transition-transform"
|
className="text-[13px] font-semibold px-2.5 py-1 rounded-[8px] text-white active:scale-95 transition-transform"
|
||||||
style={BAND_PARAM_VALUE_BOX_STYLE}
|
style={BAND_PARAM_VALUE_BOX_STYLE}
|
||||||
>
|
>
|
||||||
{band.gain >= 0 ? `+${band.gain.toFixed(1)}` : band.gain.toFixed(1)} dB
|
{formatBandGainDisplay(bandSliderPreview?.gain ?? band.gain)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<CyanSlider
|
<CyanSlider
|
||||||
value={band.gain}
|
value={bandSliderPreview?.gain ?? band.gain}
|
||||||
min={BAND_GAIN_MIN}
|
min={BAND_GAIN_MIN}
|
||||||
max={BAND_GAIN_MAX}
|
max={BAND_GAIN_MAX}
|
||||||
step={0.1}
|
step={0.1}
|
||||||
onChange={(v) => updateBand(selectedBand, { gain: v })}
|
onChange={(v) => {
|
||||||
|
setBandSliderPreview((prev) => ({ ...prev, gain: v }));
|
||||||
|
updateBand(selectedBand, { gain: v });
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1555,15 +1622,18 @@ export default function EQPage() {
|
|||||||
className="text-[13px] font-semibold px-2.5 py-1 rounded-[8px] text-white active:scale-95 transition-transform"
|
className="text-[13px] font-semibold px-2.5 py-1 rounded-[8px] text-white active:scale-95 transition-transform"
|
||||||
style={BAND_PARAM_VALUE_BOX_STYLE}
|
style={BAND_PARAM_VALUE_BOX_STYLE}
|
||||||
>
|
>
|
||||||
{band.q.toFixed(2)}
|
{formatBandQDisplay(bandSliderPreview?.q ?? band.q)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<CyanSlider
|
<CyanSlider
|
||||||
value={band.q}
|
value={bandSliderPreview?.q ?? band.q}
|
||||||
min={BAND_Q_MIN}
|
min={BAND_Q_MIN}
|
||||||
max={BAND_Q_MAX}
|
max={BAND_Q_MAX}
|
||||||
step={0.01}
|
step={0.01}
|
||||||
onChange={(v) => updateBand(selectedBand, { q: v })}
|
onChange={(v) => {
|
||||||
|
setBandSliderPreview((prev) => ({ ...prev, q: v }));
|
||||||
|
updateBand(selectedBand, { q: v });
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export function CyanSlider({
|
|||||||
className="cyan-slider relative z-10"
|
className="cyan-slider relative z-10"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChange={(e) => onChange(Number(e.target.value))}
|
onChange={(e) => onChange(Number(e.target.value))}
|
||||||
|
onInput={(e) => onChange(Number(e.currentTarget.value))}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,10 +18,40 @@ export function eqInterp(template: string | undefined, vars: Record<string, stri
|
|||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatBandFreqDisplay(freq: number) {
|
/** 将设备/表单中的频率统一为 Hz 数值;无法解析时返回 null */
|
||||||
return freq >= 1000
|
export function normalizeBandFreqHz(freq: unknown): number | null {
|
||||||
? `${(freq / 1000).toFixed(2).replace(/\.?0+$/, "")} kHz`
|
if (typeof freq === "number" && Number.isFinite(freq)) return freq;
|
||||||
: `${freq} Hz`;
|
if (typeof freq === "string") {
|
||||||
|
const trimmed = freq.trim().replace(/,/g, "");
|
||||||
|
if (!trimmed) return null;
|
||||||
|
let s = trimmed.toLowerCase().replace(/hz$/, "");
|
||||||
|
const kHz = /k(hz)?$/.test(s);
|
||||||
|
if (kHz) s = s.replace(/k(hz)?$/, "");
|
||||||
|
const num = Number.parseFloat(s);
|
||||||
|
return Number.isFinite(num) ? (kHz ? num * 1000 : num) : null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatBandGainDisplay(gain: unknown) {
|
||||||
|
const db = typeof gain === "number" && Number.isFinite(gain) ? gain : Number(gain);
|
||||||
|
if (!Number.isFinite(db)) return "0.0 dB";
|
||||||
|
const text = db.toFixed(1);
|
||||||
|
return `${db >= 0 ? `+${text}` : text} dB`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatBandQDisplay(q: unknown) {
|
||||||
|
const n = typeof q === "number" && Number.isFinite(q) ? q : Number(q);
|
||||||
|
return Number.isFinite(n) ? n.toFixed(2) : "—";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatBandFreqDisplay(freq: unknown) {
|
||||||
|
const hz = normalizeBandFreqHz(freq);
|
||||||
|
if (hz === null) return "—";
|
||||||
|
if (hz >= 1000) {
|
||||||
|
return `${(hz / 1000).toFixed(2).replace(/\.?0+$/, "")} kHz`;
|
||||||
|
}
|
||||||
|
return `${Math.round(hz)} Hz`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatBandParamForInput(
|
export function formatBandParamForInput(
|
||||||
@@ -106,6 +136,14 @@ export function normalizeFilterType(type: string | number | undefined): string {
|
|||||||
return getFilterShortName(type);
|
return getFilterShortName(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function freqLabel(f: number) {
|
/** 10 段滤波器按钮上的紧凑频率标签(始终为数字,避免被翻译插件改成英文单词) */
|
||||||
return f >= 1000 ? `${(f / 1000).toFixed(1)}K` : `${f}`;
|
export function freqLabel(freq: unknown) {
|
||||||
|
const hz = normalizeBandFreqHz(freq);
|
||||||
|
if (hz === null) return "—";
|
||||||
|
if (hz >= 1000) {
|
||||||
|
const k = hz / 1000;
|
||||||
|
const text = k >= 10 ? String(Math.round(k)) : k.toFixed(1).replace(/\.0$/, "");
|
||||||
|
return `${text}K`;
|
||||||
|
}
|
||||||
|
return String(Math.round(hz));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { PeqState } from "@/lib/luxsinApi";
|
import type { PeqFilter, PeqState } from "@/lib/luxsinApi";
|
||||||
import type localeZh from "@/locales/data-zh.json";
|
import type localeZh from "@/locales/data-zh.json";
|
||||||
|
|
||||||
export type PeqEqUi = NonNullable<NonNullable<(typeof localeZh)["peq"]>["eqUi"]>;
|
export type PeqEqUi = NonNullable<NonNullable<(typeof localeZh)["peq"]>["eqUi"]>;
|
||||||
@@ -15,6 +15,13 @@ export type EqBand = {
|
|||||||
|
|
||||||
export type PeqCatalogItem = NonNullable<PeqState["peq"]>[number];
|
export type PeqCatalogItem = NonNullable<PeqState["peq"]>[number];
|
||||||
|
|
||||||
|
/** 本地编辑缓存(按预设名称);syncPeq 拉取时优先于设备 catalog */
|
||||||
|
export type PeqPresetLocalCache = {
|
||||||
|
filters?: PeqFilter[];
|
||||||
|
preamp?: number;
|
||||||
|
autoPre?: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type CatalogTarget = {
|
export type CatalogTarget = {
|
||||||
name: string;
|
name: string;
|
||||||
bassBoost: { fc: number; q: number; gain: number };
|
bassBoost: { fc: number; q: number; gain: number };
|
||||||
|
|||||||
Reference in New Issue
Block a user