Refactor BottomNav to use localized labels for navigation items; added support for dynamic language selection based on device state. Updated locale files for English, Traditional Chinese, and Simplified Chinese to include bottom navigation labels.
This commit is contained in:
+166
-108
@@ -39,6 +39,20 @@ import {
|
||||
getFilterShortName,
|
||||
buildPeqSvgCurveData,
|
||||
} from "@/lib/peqAudio";
|
||||
import localeZh from "@/locales/data-zh.json";
|
||||
import localeZhHK from "@/locales/data-zh-HK.json";
|
||||
import localeEn from "@/locales/data-en.json";
|
||||
|
||||
type PeqEqUi = NonNullable<NonNullable<(typeof localeZh)["peq"]>["eqUi"]>;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/* ── iOS Toggle ── */
|
||||
function IOSToggle({ checked, onChange }: { checked: boolean; onChange: (v: boolean) => void }) {
|
||||
@@ -94,7 +108,15 @@ function normalizeFilterType(type: string | number | undefined): string {
|
||||
|
||||
/* ── Frequency Response Chart ── */
|
||||
function FreqChart({
|
||||
bands, rawCurve, selectedBand, abMode, onAbToggle, onCopyMode, onBandDrag, onBandSelect,
|
||||
bands,
|
||||
rawCurve,
|
||||
selectedBand,
|
||||
abMode,
|
||||
onAbToggle,
|
||||
onCopyMode,
|
||||
onBandDrag,
|
||||
onBandSelect,
|
||||
eqUi,
|
||||
}: {
|
||||
bands: Array<{ freq: number; gain: number; q: number; type: string; enabled: boolean }>;
|
||||
rawCurve: number[] | null;
|
||||
@@ -104,6 +126,7 @@ function FreqChart({
|
||||
onCopyMode: (from: "A" | "B", to: "A" | "B") => void;
|
||||
onBandDrag: (idx: number, patch: Partial<{ freq: number; gain: number }>) => void;
|
||||
onBandSelect: (idx: number) => void;
|
||||
eqUi: PeqEqUi;
|
||||
}) {
|
||||
const H = 140;
|
||||
const chartContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
@@ -252,7 +275,7 @@ function FreqChart({
|
||||
});
|
||||
|
||||
const targetMode: "A" | "B" = abMode === "A" ? "B" : "A";
|
||||
const copyButtonText = abMode === "A" ? "复制到 B" : "复制到 A";
|
||||
const copyButtonText = eqInterp(eqUi.chartCopyTo, { mode: targetMode });
|
||||
|
||||
const freqLabels = [20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000];
|
||||
const gainLabels = [20, 15, 10, 5, 0, -5, -10, -15, -20];
|
||||
@@ -315,16 +338,16 @@ function FreqChart({
|
||||
[targetMode]: { ...prev[abMode] },
|
||||
}));
|
||||
onAbToggle(targetMode);
|
||||
toast.success(`已复制到 ${targetMode}`);
|
||||
toast.success(eqInterp(eqUi.toastChartCopied, { mode: targetMode }));
|
||||
}}>
|
||||
{copyButtonText}
|
||||
</button>
|
||||
<button
|
||||
className="px-3 py-1 rounded-[8px] text-[11px] text-white/50 active:text-white transition-colors"
|
||||
style={{ background: "rgba(44,44,46,0.7)", border: "1px solid rgba(255,255,255,0.08)" }}
|
||||
onClick={() => toast.info("已加载至设备")}
|
||||
onClick={() => toast.info(eqUi.toastChartLoadedDevice)}
|
||||
>
|
||||
加载至设备
|
||||
{eqUi.chartLoadToDevice}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -544,6 +567,19 @@ export default function EQPage() {
|
||||
const { deviceState, updateSetting, api, isDemoMode, upgradePeqChange } = useDevice();
|
||||
const eqOn = (deviceState?.peqEnable ?? 0) === 1;
|
||||
|
||||
const eqUi = useMemo((): PeqEqUi => {
|
||||
const lang = deviceState?.language;
|
||||
const pack = lang === 0 ? localeEn : lang === 1 ? localeZhHK : localeZh;
|
||||
const peq = pack.peq as typeof localeZh.peq;
|
||||
return (peq.eqUi ?? (localeZh.peq as typeof localeZh.peq).eqUi) as PeqEqUi;
|
||||
}, [deviceState?.language]);
|
||||
|
||||
const peqCardLabels = useMemo(() => {
|
||||
const lang = deviceState?.language;
|
||||
const pack = lang === 0 ? localeEn : lang === 1 ? localeZhHK : localeZh;
|
||||
return pack.peq ?? localeZh.peq;
|
||||
}, [deviceState?.language]);
|
||||
|
||||
const [bandsByMode, setBandsByMode] = useState<Record<"A" | "B", typeof DEFAULT_BANDS>>({
|
||||
A: cloneBands(DEFAULT_BANDS),
|
||||
B: cloneBands(DEFAULT_BANDS),
|
||||
@@ -660,11 +696,11 @@ export default function EQPage() {
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : "加载失败";
|
||||
setCatalogBrandsError(msg);
|
||||
toast.error("获取品牌列表失败");
|
||||
toast.error(eqUi.toastBrandsFail);
|
||||
} finally {
|
||||
setCatalogBrandsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
}, [eqUi]);
|
||||
|
||||
const searchCatalogByKeyword = useCallback(async (keyword: string) => {
|
||||
const q = keyword.trim();
|
||||
@@ -699,11 +735,11 @@ export default function EQPage() {
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : "加载失败";
|
||||
setCatalogModelsError(msg);
|
||||
toast.error("获取型号列表失败");
|
||||
toast.error(eqUi.toastModelsFail);
|
||||
} finally {
|
||||
setCatalogModelsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
}, [eqUi]);
|
||||
|
||||
const openAddHeadsetCatalog = () => {
|
||||
setIsBrandDrawerOpen(true);
|
||||
@@ -792,76 +828,80 @@ export default function EQPage() {
|
||||
setIsBatchEditDialogOpen(true);
|
||||
};
|
||||
|
||||
const parseBatchEditText = (raw: string) => {
|
||||
const lines = raw
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
if (lines.length < 11) {
|
||||
throw new Error("格式错误:至少需要 11 行(Preamp + 10 个滤波器)");
|
||||
}
|
||||
|
||||
const preampMatch = lines[0].match(/^Preamp\s*:\s*([+-]?\d+(?:\.\d+)?)\s*dB$/i);
|
||||
if (!preampMatch) {
|
||||
throw new Error("格式错误:第 1 行应为 Preamp:-5.96dB");
|
||||
}
|
||||
const preamp = Number(preampMatch[1]);
|
||||
if (!Number.isFinite(preamp)) {
|
||||
throw new Error("格式错误:Preamp 数值无效");
|
||||
}
|
||||
|
||||
const parsedFilters = new Array<{
|
||||
enabled: boolean;
|
||||
type: string;
|
||||
freq: number;
|
||||
gain: number;
|
||||
q: number;
|
||||
}>(10);
|
||||
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
const line = lines[i];
|
||||
const match = line.match(
|
||||
/^Filter\s+(\d+)\s*:\s*(ON|OFF)\s+([A-Za-z]+)\s+Fc\s+([+-]?\d+(?:\.\d+)?)\s+Hz\s+Gain\s+([+-]?\d+(?:\.\d+)?)\s+dB\s+Q\s+([+-]?\d+(?:\.\d+)?)$/i
|
||||
);
|
||||
if (!match) {
|
||||
throw new Error(`格式错误:第 ${i + 1} 行不符合 Filter 格式`);
|
||||
const parseBatchEditText = useCallback(
|
||||
(raw: string) => {
|
||||
const t = eqUi;
|
||||
const lines = raw
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
if (lines.length < 11) {
|
||||
throw new Error(eqInterp(t.errMinLines, {}));
|
||||
}
|
||||
|
||||
const filterNo = Number(match[1]);
|
||||
if (filterNo < 1 || filterNo > 10) {
|
||||
throw new Error(`格式错误:Filter 编号 ${filterNo} 超出范围`);
|
||||
const preampMatch = lines[0].match(/^Preamp\s*:\s*([+-]?\d+(?:\.\d+)?)\s*dB$/i);
|
||||
if (!preampMatch) {
|
||||
throw new Error(eqInterp(t.errPreampFirstLine, {}));
|
||||
}
|
||||
const preamp = Number(preampMatch[1]);
|
||||
if (!Number.isFinite(preamp)) {
|
||||
throw new Error(eqInterp(t.errPreampValue, {}));
|
||||
}
|
||||
|
||||
const type = normalizeFilterType(match[3].toUpperCase());
|
||||
if (!FILTER_TYPES.includes(type)) {
|
||||
throw new Error(`格式错误:Filter ${filterNo} 的类型无效(${match[3]})`);
|
||||
const parsedFilters = new Array<{
|
||||
enabled: boolean;
|
||||
type: string;
|
||||
freq: number;
|
||||
gain: number;
|
||||
q: number;
|
||||
}>(10);
|
||||
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
const line = lines[i];
|
||||
const match = line.match(
|
||||
/^Filter\s+(\d+)\s*:\s*(ON|OFF)\s+([A-Za-z]+)\s+Fc\s+([+-]?\d+(?:\.\d+)?)\s+Hz\s+Gain\s+([+-]?\d+(?:\.\d+)?)\s+dB\s+Q\s+([+-]?\d+(?:\.\d+)?)$/i
|
||||
);
|
||||
if (!match) {
|
||||
throw new Error(eqInterp(t.errFilterLine, { line: String(i + 1) }));
|
||||
}
|
||||
|
||||
const filterNo = Number(match[1]);
|
||||
if (filterNo < 1 || filterNo > 10) {
|
||||
throw new Error(eqInterp(t.errFilterNoRange, { no: String(filterNo) }));
|
||||
}
|
||||
|
||||
const type = normalizeFilterType(match[3].toUpperCase());
|
||||
if (!FILTER_TYPES.includes(type)) {
|
||||
throw new Error(eqInterp(t.errFilterType, { no: String(filterNo), type: match[3] }));
|
||||
}
|
||||
|
||||
const freq = Number(match[4]);
|
||||
const gain = Number(match[5]);
|
||||
const q = Number(match[6]);
|
||||
if (![freq, gain, q].every(Number.isFinite)) {
|
||||
throw new Error(eqInterp(t.errFilterNumbers, { no: String(filterNo) }));
|
||||
}
|
||||
|
||||
parsedFilters[filterNo - 1] = {
|
||||
enabled: match[2].toUpperCase() === "ON",
|
||||
type,
|
||||
freq: Number(freq.toFixed(2)),
|
||||
gain: Number(gain.toFixed(2)),
|
||||
q: Number(q.toFixed(2)),
|
||||
};
|
||||
}
|
||||
|
||||
const freq = Number(match[4]);
|
||||
const gain = Number(match[5]);
|
||||
const q = Number(match[6]);
|
||||
if (![freq, gain, q].every(Number.isFinite)) {
|
||||
throw new Error(`格式错误:Filter ${filterNo} 的数值无效`);
|
||||
if (parsedFilters.some((item) => !item)) {
|
||||
throw new Error(eqInterp(t.errFilterAll, {}));
|
||||
}
|
||||
|
||||
parsedFilters[filterNo - 1] = {
|
||||
enabled: match[2].toUpperCase() === "ON",
|
||||
type,
|
||||
freq: Number(freq.toFixed(2)),
|
||||
gain: Number(gain.toFixed(2)),
|
||||
q: Number(q.toFixed(2)),
|
||||
return {
|
||||
preamp: Number(preamp.toFixed(2)),
|
||||
filters: parsedFilters as typeof bands,
|
||||
};
|
||||
}
|
||||
|
||||
if (parsedFilters.some((item) => !item)) {
|
||||
throw new Error("格式错误:Filter 1-10 必须全部提供");
|
||||
}
|
||||
|
||||
return {
|
||||
preamp: Number(preamp.toFixed(2)),
|
||||
filters: parsedFilters as typeof bands,
|
||||
};
|
||||
};
|
||||
},
|
||||
[eqUi],
|
||||
);
|
||||
|
||||
const handleSaveBatchEdit = () => {
|
||||
try {
|
||||
@@ -884,9 +924,9 @@ export default function EQPage() {
|
||||
schedulePeqSync(updatedPeq, parsed.filters, 0);
|
||||
setSelectedBand(0);
|
||||
setIsBatchEditDialogOpen(false);
|
||||
toast.success("批量编辑已应用");
|
||||
toast.success(eqUi.toastBatchApplied);
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "批量编辑格式错误");
|
||||
toast.error(error instanceof Error ? error.message : eqUi.toastBatchParseError);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -917,23 +957,23 @@ export default function EQPage() {
|
||||
const target = peqItems[headphoneIdx];
|
||||
if (!target?.name) return;
|
||||
|
||||
const confirmed = window.confirm(`是否删除耳机「${target.name}」?`);
|
||||
const confirmed = window.confirm(eqInterp(eqUi.deleteConfirm, { name: target.name }));
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
if (isDemoMode || !api) {
|
||||
const nextItems = peqItems.filter((_, idx) => idx !== headphoneIdx);
|
||||
applyPeqStateToUI({ peq: nextItems, peqSelect: Math.max(0, headphoneIdx - 1) });
|
||||
toast.success("已删除耳机");
|
||||
toast.success(eqUi.toastDeleted);
|
||||
return;
|
||||
}
|
||||
|
||||
await api.removePeq([target.name]);
|
||||
const latest = await api.getPeqState();
|
||||
applyPeqStateToUI(latest as { peq?: Array<{ name: string; filters?: any[] | string }>; peqSelect?: number });
|
||||
toast.success("已删除耳机");
|
||||
toast.success(eqUi.toastDeleted);
|
||||
} catch {
|
||||
toast.error("删除耳机失败");
|
||||
toast.error(eqUi.toastDeleteFail);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1385,7 +1425,7 @@ export default function EQPage() {
|
||||
style={{ background: "rgba(0,255,246,0.12)", border: "1px solid rgba(0,255,246,0.2)" }}>
|
||||
<Edit3 size={20} style={{ color: "#00FFF6" }} />
|
||||
</div>
|
||||
<span className="text-[14px] text-white/80">批量编辑</span>
|
||||
<span className="text-[14px] text-white/80">{peqCardLabels.edit}</span>
|
||||
</button>
|
||||
<button
|
||||
className="ios-list-group flex flex-col items-center justify-center py-3 gap-2 active:opacity-70 transition-opacity"
|
||||
@@ -1394,7 +1434,7 @@ export default function EQPage() {
|
||||
style={{ background: "rgba(0,255,246,0.12)", border: "1px solid rgba(0,255,246,0.2)" }}>
|
||||
<Headphones size={20} style={{ color: "#00FFF6" }} />
|
||||
</div>
|
||||
<span className="text-[14px] text-white/80">添加耳机型号</span>
|
||||
<span className="text-[14px] text-white/80">{peqCardLabels.headset}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1484,6 +1524,7 @@ export default function EQPage() {
|
||||
onCopyMode={copyModeParams}
|
||||
onBandDrag={(idx, patch) => setBands((prev) => prev.map((b, i) => i === idx ? { ...b, ...patch } : b))}
|
||||
onBandSelect={setSelectedBand}
|
||||
eqUi={eqUi}
|
||||
/>
|
||||
|
||||
{/* ── Band grid (2 rows × 5) ── */}
|
||||
@@ -1513,7 +1554,7 @@ export default function EQPage() {
|
||||
<div className="ios-list-group p-4 space-y-5 relative z-30 overflow-visible">
|
||||
{/* Filter type selector */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[14px] text-white/50 font-medium">滤波器</span>
|
||||
<span className="text-[14px] text-white/50 font-medium">{peqCardLabels.filters?.label ?? "滤波器"}</span>
|
||||
<div
|
||||
ref={filterMenuRef}
|
||||
className="relative z-40 w-[136px] rounded-[12px] overflow-visible backdrop-blur-md transition-all"
|
||||
@@ -1596,10 +1637,10 @@ export default function EQPage() {
|
||||
{/* Q 值 */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-[13px] text-white/45 font-medium">Q 值</span>
|
||||
<span className="text-[13px] text-white/45 font-medium">{peqCardLabels.q ?? "Q值"}</span>
|
||||
<span className="text-[13px] font-semibold px-2.5 py-1 rounded-[8px] text-white"
|
||||
style={{ background: "rgba(44,44,46,0.9)", border: "1px solid rgba(255,255,255,0.08)" }}>
|
||||
{band.q.toFixed(2)} dB
|
||||
{band.q.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
<CyanSlider value={band.q} min={0.1} max={10} step={0.01}
|
||||
@@ -1611,7 +1652,7 @@ export default function EQPage() {
|
||||
<div className="ios-list-group p-4 relative z-10">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-baseline gap-1">
|
||||
<span className="text-[13px] text-white/45">总增益</span>
|
||||
<span className="text-[13px] text-white/45">{peqCardLabels.preamp}</span>
|
||||
<span className="text-[28px] font-bold ml-2" style={{ color: "#00FFF6" }}>
|
||||
{preampValue >= 0 ? `+${preampValue.toFixed(1)}` : preampValue.toFixed(1)}
|
||||
</span>
|
||||
@@ -1719,7 +1760,7 @@ export default function EQPage() {
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-[28px] text-white/90 font-semibold">批量编辑</h3>
|
||||
<h3 className="text-[28px] text-white/90 font-semibold">{eqUi.batchEditTitle}</h3>
|
||||
<button
|
||||
type="button"
|
||||
className="text-white/45 hover:text-white/75 transition-colors"
|
||||
@@ -1729,7 +1770,7 @@ export default function EQPage() {
|
||||
</button>
|
||||
</div>
|
||||
<p className="mb-3 text-[13px] text-white/55">
|
||||
共 11 行:第 1 行 Preamp,第 2-11 行 Filter 1-10。保存后将覆盖当前耳机参数。
|
||||
{eqUi.batchEditHint}
|
||||
</p>
|
||||
<textarea
|
||||
value={batchEditText}
|
||||
@@ -1743,14 +1784,14 @@ export default function EQPage() {
|
||||
className="px-5 py-2 rounded-full text-[24px] text-white/80 bg-[#3f4349] hover:bg-[#4a4f56] transition-colors"
|
||||
onClick={() => setIsBatchEditDialogOpen(false)}
|
||||
>
|
||||
取消
|
||||
{eqUi.cancel}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="px-5 py-2 rounded-full text-[24px] text-black bg-[#00FFF6] hover:brightness-95 transition-all"
|
||||
onClick={handleSaveBatchEdit}
|
||||
>
|
||||
保存
|
||||
{eqUi.save}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1762,7 +1803,7 @@ export default function EQPage() {
|
||||
<button
|
||||
type="button"
|
||||
className="absolute inset-0 bg-black/55"
|
||||
aria-label="关闭"
|
||||
aria-label={eqUi.closeDrawer}
|
||||
onClick={() => setIsBrandDrawerOpen(false)}
|
||||
/>
|
||||
<div
|
||||
@@ -1775,7 +1816,8 @@ export default function EQPage() {
|
||||
<div className="flex shrink-0 items-stretch rounded-t-[12px] bg-[#2c2c2e] px-1 pt-1">
|
||||
{(["brands", "models", "target"] as const).map((tab) => {
|
||||
const active = brandDrawerTab === tab;
|
||||
const label = tab === "brands" ? "Brands" : tab === "models" ? "Models" : "Target";
|
||||
const label =
|
||||
tab === "brands" ? eqUi.tabBrands : tab === "models" ? eqUi.tabModels : eqUi.tabTarget;
|
||||
return (
|
||||
<button
|
||||
key={tab}
|
||||
@@ -1801,14 +1843,14 @@ export default function EQPage() {
|
||||
type="search"
|
||||
value={brandSearchQuery}
|
||||
onChange={(e) => setBrandSearchQuery(e.target.value)}
|
||||
placeholder="搜索品牌或型号"
|
||||
placeholder={eqUi.searchPlaceholder}
|
||||
className="min-w-0 flex-1 bg-transparent text-[15px] text-black/80 outline-none placeholder:text-black/35"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto overflow-x-hidden overscroll-contain touch-pan-y">
|
||||
{brandSearchQuery.trim() && catalogSearchLoading && (
|
||||
<div className="py-10 text-center text-[14px] text-black/45">搜索中…</div>
|
||||
<div className="py-10 text-center text-[14px] text-black/45">{eqUi.searching}</div>
|
||||
)}
|
||||
{!!brandSearchQuery.trim() && !catalogSearchLoading && catalogSearchError && (
|
||||
<div className="px-4 py-8 text-center text-[14px] text-red-600/90">{catalogSearchError}</div>
|
||||
@@ -1817,7 +1859,7 @@ export default function EQPage() {
|
||||
!catalogSearchLoading &&
|
||||
!catalogSearchError &&
|
||||
catalogSearchResults.length === 0 && (
|
||||
<div className="py-10 text-center text-[14px] text-black/45">未找到相关品牌或型号</div>
|
||||
<div className="py-10 text-center text-[14px] text-black/45">{eqUi.noSearchResults}</div>
|
||||
)}
|
||||
{!!brandSearchQuery.trim() &&
|
||||
!catalogSearchLoading &&
|
||||
@@ -1844,13 +1886,13 @@ export default function EQPage() {
|
||||
</button>
|
||||
))}
|
||||
{!brandSearchQuery.trim() && catalogBrandsLoading && (
|
||||
<div className="py-10 text-center text-[14px] text-black/45">加载中…</div>
|
||||
<div className="py-10 text-center text-[14px] text-black/45">{eqUi.loading}</div>
|
||||
)}
|
||||
{!brandSearchQuery.trim() && !catalogBrandsLoading && catalogBrandsError && (
|
||||
<div className="px-4 py-8 text-center text-[14px] text-red-600/90">{catalogBrandsError}</div>
|
||||
)}
|
||||
{!brandSearchQuery.trim() && !catalogBrandsLoading && !catalogBrandsError && filteredCatalogBrands.length === 0 && (
|
||||
<div className="py-10 text-center text-[14px] text-black/45">无匹配品牌</div>
|
||||
<div className="py-10 text-center text-[14px] text-black/45">{eqUi.noBrandMatch}</div>
|
||||
)}
|
||||
{!brandSearchQuery.trim() &&
|
||||
!catalogBrandsLoading &&
|
||||
@@ -1880,22 +1922,25 @@ export default function EQPage() {
|
||||
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<div className="min-h-0 flex-1 overflow-y-auto overflow-x-hidden overscroll-contain touch-pan-y">
|
||||
<div className="sticky top-0 z-10 border-b border-black/8 bg-white px-4 py-2.5 text-[13px] text-black/55">
|
||||
品牌:<span className="font-semibold text-black/80">{selectedCatalogBrand || "—"}</span>
|
||||
{eqUi.brandLabel}<span className="font-semibold text-black/80">{selectedCatalogBrand || "—"}</span>
|
||||
{selectedCatalogModelFromSearch && (
|
||||
<span className="ml-2 text-black/45">已选搜索型号:{selectedCatalogModelFromSearch}</span>
|
||||
<span className="ml-2 text-black/45">
|
||||
{eqUi.modelFromSearchLabel}
|
||||
{selectedCatalogModelFromSearch}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{!selectedCatalogBrand && (
|
||||
<div className="py-10 text-center text-[14px] text-black/45">请先在 Brands 中选择品牌</div>
|
||||
<div className="py-10 text-center text-[14px] text-black/45">{eqUi.selectBrandFirst}</div>
|
||||
)}
|
||||
{!!selectedCatalogBrand && catalogModelsLoading && (
|
||||
<div className="py-10 text-center text-[14px] text-black/45">加载中…</div>
|
||||
<div className="py-10 text-center text-[14px] text-black/45">{eqUi.loading}</div>
|
||||
)}
|
||||
{!!selectedCatalogBrand && !catalogModelsLoading && catalogModelsError && (
|
||||
<div className="px-4 py-8 text-center text-[14px] text-red-600/90">{catalogModelsError}</div>
|
||||
)}
|
||||
{!!selectedCatalogBrand && !catalogModelsLoading && !catalogModelsError && catalogModels.length === 0 && (
|
||||
<div className="py-10 text-center text-[14px] text-black/45">该品牌暂无型号</div>
|
||||
<div className="py-10 text-center text-[14px] text-black/45">{eqUi.noModelsForBrand}</div>
|
||||
)}
|
||||
{!!selectedCatalogBrand &&
|
||||
!catalogModelsLoading &&
|
||||
@@ -1923,9 +1968,18 @@ export default function EQPage() {
|
||||
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<div className="min-h-0 flex-1 overflow-y-auto overflow-x-hidden overscroll-contain touch-pan-y">
|
||||
<div className="sticky top-0 z-10 border-b border-black/8 bg-white px-4 py-2.5 text-[13px] text-black/55 space-y-0.5">
|
||||
<div>品牌:<span className="font-semibold text-black/80">{selectedCatalogBrand || "—"}</span></div>
|
||||
<div>型号:<span className="font-semibold text-black/80">{selectedCatalogModelName || "—"}</span></div>
|
||||
<div>Form:<span className="font-semibold text-black/80">{selectedCatalogModelForm || "全部"}</span></div>
|
||||
<div>
|
||||
{eqUi.targetBrand}
|
||||
<span className="font-semibold text-black/80">{selectedCatalogBrand || "—"}</span>
|
||||
</div>
|
||||
<div>
|
||||
{eqUi.targetModel}
|
||||
<span className="font-semibold text-black/80">{selectedCatalogModelName || "—"}</span>
|
||||
</div>
|
||||
<div>
|
||||
{eqUi.targetForm}
|
||||
<span className="font-semibold text-black/80">{selectedCatalogModelForm || eqUi.formAll}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-5 pt-4 pb-3">
|
||||
<button
|
||||
@@ -2024,9 +2078,9 @@ export default function EQPage() {
|
||||
}
|
||||
|
||||
setIsBrandDrawerOpen(false);
|
||||
toast.success("新耳机 EQ 已上报");
|
||||
toast.success(eqUi.toastNewEqOk);
|
||||
} catch {
|
||||
toast.error("获取曲线失败");
|
||||
toast.error(eqUi.toastCurveFail);
|
||||
} finally {
|
||||
setIsConfirmingTarget(false);
|
||||
}
|
||||
@@ -2039,14 +2093,14 @@ export default function EQPage() {
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
{isConfirmingTarget ? "loading..." : "confirm"}
|
||||
{isConfirmingTarget ? eqUi.confirmLoading : eqUi.confirmButton}
|
||||
</button>
|
||||
</div>
|
||||
{!selectedCatalogModelName && (
|
||||
<div className="py-10 text-center text-[14px] text-black/45">请先选择型号</div>
|
||||
<div className="py-10 text-center text-[14px] text-black/45">{eqUi.selectModelFirst}</div>
|
||||
)}
|
||||
{!!selectedCatalogModelName && availableCatalogTargets.length === 0 && (
|
||||
<div className="py-10 text-center text-[14px] text-black/45">暂无可用 Target</div>
|
||||
<div className="py-10 text-center text-[14px] text-black/45">{eqUi.noTargets}</div>
|
||||
)}
|
||||
{!!selectedCatalogModelName &&
|
||||
availableCatalogTargets.map((target) => {
|
||||
@@ -2059,12 +2113,16 @@ export default function EQPage() {
|
||||
style={active ? { background: "rgba(0,255,246,0.14)" } : undefined}
|
||||
onClick={() => {
|
||||
setSelectedCatalogTarget(target.name);
|
||||
toast.success(`已选 Target:${target.name}`);
|
||||
toast.success(eqInterp(eqUi.toastTargetSelected, { name: target.name }));
|
||||
}}
|
||||
>
|
||||
<div className="text-[15px] text-black/85 font-medium">{target.name}</div>
|
||||
<div className="mt-1 text-[12px] text-black/55">
|
||||
Bass boost: fc {target.bassBoost.fc}, q {target.bassBoost.q}, gain {target.bassBoost.gain}dB
|
||||
{eqInterp(eqUi.bassBoost, {
|
||||
fc: String(target.bassBoost.fc),
|
||||
q: String(target.bassBoost.q),
|
||||
gain: String(target.bassBoost.gain),
|
||||
})}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user