refactor(eq): 重构EQ页面及相关代码以优化PEQ管理与同步

- 禁用Vite配置中的manus debug collector插件,减少无用请求
- 对DeviceContext添加syncPeqCatalog方法用于同步PEQ状态
- 重构luxsinApi.ts中PEQ请求相关代码,统一POST请求封装,增强错误处理
- 将EQPage相关常量、工具和组件抽取至独立模块,简化主文件结构
- 使用缓存机制记忆预设滤波器列表,减少不必要的重复计算
- 优化PEQ状态同步逻辑,支持按模式(A/B)分别保存与应用滤波器
- 实现选择耳机型号时自动保存并更新对应滤波器缓存和显示
- 修改A/B切换逻辑,使用防抖同步和固定的peqApply调用
- 为音量滑块添加thumb-only滑动样式及拖拽区域限制,提升交互体验
- Home页面ListRow组件支持显示自定义缩略图
- 增加eq/constants.ts定义滤波器类型、参数范围和样式,统一管理配置
- 解决多处EQ页面组件交互和状态切换的潜在同步问题,提高代码维护性和可读性
This commit is contained in:
yangy
2026-05-28 16:26:38 +08:00
parent 8cde820467
commit 3f9b2f0e7b
10 changed files with 700 additions and 306 deletions
+176
View File
@@ -0,0 +1,176 @@
import type { PeqFilter, PeqState } from "@/lib/luxsinApi";
import { getFilterShortName, getFilterType } from "@/lib/peqAudio";
import {
BAND_FREQ_MIN,
BAND_FREQ_MAX,
BAND_GAIN_MIN,
BAND_GAIN_MAX,
BAND_Q_MIN,
BAND_Q_MAX,
type BandParamKind,
type PeqCatalogItem,
} from "./constants";
/* ── Template interpolation ── */
export function eqInterp(
template: string | undefined,
vars: Record<string, string | number>,
): string {
if (!template) return "";
let s = template;
for (const [k, v] of Object.entries(vars)) {
s = s.split(`{{${k}}}`).join(String(v));
}
return s;
}
/* ── Band frequency display formatting ── */
export function formatBandFreqDisplay(freq: number) {
return freq >= 1000
? `${(freq / 1000).toFixed(2).replace(/\.?0+$/, "")} kHz`
: `${freq} Hz`;
}
/* ── Band param value for input field ── */
export function formatBandParamForInput(
kind: BandParamKind,
band: { freq: number; gain: number; q: number },
) {
switch (kind) {
case "freq":
return String(band.freq);
case "gain":
return band.gain.toFixed(1);
case "q":
return band.q.toFixed(2);
}
}
/* ── Parse band param input with validation ── */
export function parseBandParamInput(
kind: BandParamKind,
raw: string,
messages: { invalid: string; outOfRange: string },
): { ok: true; value: number } | { ok: false; message: string } {
const trimmed = raw.trim();
if (!trimmed) return { ok: false, message: messages.invalid };
if (kind === "freq") {
let s = trimmed.replace(/\s+/g, "").toLowerCase().replace(/hz$/, "");
const kHz = /k(hz)?$/.test(s);
if (kHz) s = s.replace(/k(hz)?$/, "");
const num = Number.parseFloat(s);
if (!Number.isFinite(num)) return { ok: false, message: messages.invalid };
const hz = Math.round(kHz ? num * 1000 : num);
if (hz < BAND_FREQ_MIN || hz > BAND_FREQ_MAX) {
return { ok: false, message: messages.outOfRange };
}
return { ok: true, value: hz };
}
if (kind === "gain") {
const s = trimmed.replace(/\s*dB\s*$/i, "").trim();
const num = Number.parseFloat(s);
if (!Number.isFinite(num)) return { ok: false, message: messages.invalid };
const gain = Number(num.toFixed(1));
if (gain < BAND_GAIN_MIN || gain > BAND_GAIN_MAX) {
return { ok: false, message: messages.outOfRange };
}
return { ok: true, value: gain };
}
const num = Number.parseFloat(trimmed);
if (!Number.isFinite(num)) return { ok: false, message: messages.invalid };
const q = Number(num.toFixed(2));
if (q < BAND_Q_MIN || q > BAND_Q_MAX) {
return { ok: false, message: messages.outOfRange };
}
return { ok: true, value: q };
}
/* ── Normalize filter type from string or number ── */
export function normalizeFilterType(
type: string | number | undefined,
): string {
if (type === undefined || type === null) return "PEAK";
if (typeof type === "number") {
switch (type) {
case 0:
return "LPF";
case 1:
return "HPF";
case 2:
return "BPF";
case 3:
return "NOTCH";
case 4:
return "PEAK";
case 5:
return "LSHELF";
case 6:
return "HSHELF";
case 7:
return "APF";
default:
return "PEAK";
}
}
return getFilterShortName(type);
}
/* ── Clone bands array ── */
export function cloneBands(
source: Array<{
freq: number;
gain: number;
q: number;
type: string;
enabled: boolean;
}>,
) {
return source.map((band) => ({ ...band }));
}
/* ── Frequency label for band grid ── */
export function freqLabel(f: number) {
return f >= 1000 ? `${(f / 1000).toFixed(1)}K` : `${f}`;
}
/* ── Build PEQ catalog sync key ── */
export function buildPeqCatalogSyncKey(
remote: Pick<PeqState, "peq" | "peqSelect"> | null | undefined,
devicePeqSelect?: number,
): string {
const items = remote?.peq;
if (!items?.length) return items ? "empty" : "";
const select = devicePeqSelect ?? remote?.peqSelect ?? 0;
return `${select}|${items.map((p) => p.name).join("\u0001")}`;
}
/* ── Generate unique preset name ── */
export function getUniquePresetName(
base: string,
existingNames: string[],
) {
if (!existingNames.includes(base)) return base;
let index = 1;
while (existingNames.includes(`${base}_${index}`)) {
index += 1;
}
return `${base}_${index}`;
}
/* ── Convert UI band → device PeqFilter ── */
export function bandToPeqFilter(b: {
freq: number;
gain: number;
q: number;
type: string | number;
}): PeqFilter {
return {
fc: b.freq,
gain: b.gain,
q: b.q,
type: getFilterType(b.type),
};
}