Refactor ConnectScreen to use logo image instead of SVG, enhance EQPage with batch editing functionality for PEQ settings, and implement power off confirmation dialog in Home component. Update localization files for new power off features.
This commit is contained in:
+182
-8
@@ -458,6 +458,8 @@ export default function EQPage() {
|
||||
const [addPresetMode, setAddPresetMode] = useState<"copy" | "flat">("copy");
|
||||
const [copyPresetName, setCopyPresetName] = useState("");
|
||||
const [flatPresetName, setFlatPresetName] = useState("");
|
||||
const [isBatchEditDialogOpen, setIsBatchEditDialogOpen] = useState(false);
|
||||
const [batchEditText, setBatchEditText] = useState("");
|
||||
type BrandDrawerTab = "brands" | "models" | "target";
|
||||
const [isBrandDrawerOpen, setIsBrandDrawerOpen] = useState(false);
|
||||
const [brandDrawerTab, setBrandDrawerTab] = useState<BrandDrawerTab>("brands");
|
||||
@@ -624,6 +626,124 @@ export default function EQPage() {
|
||||
});
|
||||
};
|
||||
|
||||
const toCompactNumber = (value: number) => Number(value.toFixed(3)).toString();
|
||||
|
||||
const buildBatchEditText = useCallback(() => {
|
||||
const preamp = Number(currentPeq?.preamp ?? 0);
|
||||
const source = bands.length > 0 ? bands : DEFAULT_BANDS;
|
||||
const lines = [`Preamp:${toCompactNumber(preamp)}dB`];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const band = source[i] ?? DEFAULT_BANDS[i] ?? DEFAULT_BANDS[0];
|
||||
lines.push(
|
||||
`Filter ${i + 1}: ${band.enabled ? "ON" : "OFF"} ${normalizeFilterType(band.type)} Fc ${toCompactNumber(band.freq)} Hz Gain ${toCompactNumber(band.gain)} dB Q ${toCompactNumber(band.q)}`
|
||||
);
|
||||
}
|
||||
return lines.join("\n");
|
||||
}, [bands, currentPeq?.preamp]);
|
||||
|
||||
const openBatchEditDialog = () => {
|
||||
setBatchEditText(buildBatchEditText());
|
||||
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 filterNo = Number(match[1]);
|
||||
if (filterNo < 1 || filterNo > 10) {
|
||||
throw new Error(`格式错误:Filter 编号 ${filterNo} 超出范围`);
|
||||
}
|
||||
|
||||
const type = normalizeFilterType(match[3].toUpperCase());
|
||||
if (!FILTER_TYPES.includes(type)) {
|
||||
throw new Error(`格式错误:Filter ${filterNo} 的类型无效(${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(`格式错误:Filter ${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)),
|
||||
};
|
||||
}
|
||||
|
||||
if (parsedFilters.some((item) => !item)) {
|
||||
throw new Error("格式错误:Filter 1-10 必须全部提供");
|
||||
}
|
||||
|
||||
return {
|
||||
preamp: Number(preamp.toFixed(2)),
|
||||
filters: parsedFilters as typeof bands,
|
||||
};
|
||||
};
|
||||
|
||||
const handleSaveBatchEdit = () => {
|
||||
try {
|
||||
const parsed = parseBatchEditText(batchEditText);
|
||||
let updatedPeq: { name: string; autoPre?: number; preamp?: number; canDel?: number } | undefined;
|
||||
setPeqItems((prev) =>
|
||||
prev.map((item, i) => {
|
||||
if (i !== headphoneIdx) return item;
|
||||
const merged = {
|
||||
...item,
|
||||
autoPre: 0,
|
||||
preamp: parsed.preamp,
|
||||
filters: parsed.filters.map(bandToPeqFilter),
|
||||
};
|
||||
updatedPeq = merged;
|
||||
return merged;
|
||||
})
|
||||
);
|
||||
setBands(parsed.filters);
|
||||
schedulePeqSync(updatedPeq, parsed.filters, 0);
|
||||
setSelectedBand(0);
|
||||
setIsBatchEditDialogOpen(false);
|
||||
toast.success("批量编辑已应用");
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "批量编辑格式错误");
|
||||
}
|
||||
};
|
||||
|
||||
const applyPeqStateToUI = (
|
||||
peqState: { peq?: Array<{ name: string; filters?: any[] | string }>; peqSelect?: number },
|
||||
) => {
|
||||
@@ -1069,7 +1189,7 @@ export default function EQPage() {
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<button
|
||||
className="ios-list-group flex flex-col items-center justify-center py-3 gap-2 active:opacity-70 transition-opacity"
|
||||
onClick={() => toast.info("批量编辑功能即将推出")}>
|
||||
onClick={openBatchEditDialog}>
|
||||
<div className="w-10 h-10 rounded-[12px] flex items-center justify-center"
|
||||
style={{ background: "rgba(0,255,246,0.12)", border: "1px solid rgba(0,255,246,0.2)" }}>
|
||||
<Edit3 size={20} style={{ color: "#00FFF6" }} />
|
||||
@@ -1394,6 +1514,56 @@ export default function EQPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isBatchEditDialogOpen && (
|
||||
<div className="fixed inset-0 z-[121] flex items-center justify-center bg-black/65 px-4">
|
||||
<div
|
||||
className="w-full max-w-[760px] rounded-[14px] p-5"
|
||||
style={{
|
||||
background: "linear-gradient(180deg, rgba(34,36,40,0.98) 0%, rgba(24,26,30,0.98) 100%)",
|
||||
border: "1px solid rgba(255,255,255,0.12)",
|
||||
boxShadow: "0 18px 48px rgba(0,0,0,0.55)",
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-[28px] text-white/90 font-semibold">批量编辑</h3>
|
||||
<button
|
||||
type="button"
|
||||
className="text-white/45 hover:text-white/75 transition-colors"
|
||||
onClick={() => setIsBatchEditDialogOpen(false)}
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
<p className="mb-3 text-[13px] text-white/55">
|
||||
共 11 行:第 1 行 Preamp,第 2-11 行 Filter 1-10。保存后将覆盖当前耳机参数。
|
||||
</p>
|
||||
<textarea
|
||||
value={batchEditText}
|
||||
onChange={(e) => setBatchEditText(e.target.value)}
|
||||
spellCheck={false}
|
||||
className="h-[360px] w-full resize-none overflow-auto rounded-[10px] border border-white/12 bg-[#15171b] p-3 font-mono text-[13px] leading-6 text-white/90 outline-none focus:border-[#00FFF6]/55"
|
||||
/>
|
||||
<div className="mt-5 flex items-center justify-center gap-16">
|
||||
<button
|
||||
type="button"
|
||||
className="px-5 py-2 rounded-full text-[24px] text-white/80 bg-[#3f4349] hover:bg-[#4a4f56] transition-colors"
|
||||
onClick={() => setIsBatchEditDialogOpen(false)}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="px-5 py-2 rounded-full text-[24px] text-black bg-[#00FFF6] hover:brightness-95 transition-all"
|
||||
onClick={handleSaveBatchEdit}
|
||||
>
|
||||
保存
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isBrandDrawerOpen && (
|
||||
<div className="fixed inset-0 z-[110] flex flex-col justify-end">
|
||||
<button
|
||||
@@ -1403,7 +1573,7 @@ export default function EQPage() {
|
||||
onClick={() => setIsBrandDrawerOpen(false)}
|
||||
/>
|
||||
<div
|
||||
className="relative z-10 flex max-h-[88vh] flex-col rounded-t-[18px] bg-white shadow-[0_-8px_32px_rgba(0,0,0,0.35)]"
|
||||
className="relative z-10 flex h-[58dvh] flex-shrink-0 flex-col overflow-hidden rounded-t-[18px] bg-white shadow-[0_-8px_32px_rgba(0,0,0,0.35)]"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex shrink-0 items-center justify-center pt-2 pb-1">
|
||||
@@ -1428,9 +1598,9 @@ export default function EQPage() {
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="flex min-h-0 flex-1 flex-col bg-white">
|
||||
<div className="flex min-h-0 flex-1 flex-col overflow-hidden bg-white">
|
||||
{brandDrawerTab === "brands" && (
|
||||
<>
|
||||
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<div className="shrink-0 border-b border-black/8 px-3 py-2.5">
|
||||
<div className="flex items-center gap-2 rounded-[10px] border border-black/12 bg-[#f5f5f7] px-3 py-2">
|
||||
<Search size={18} className="shrink-0 text-black/35" />
|
||||
@@ -1443,7 +1613,7 @@ export default function EQPage() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto">
|
||||
<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>
|
||||
)}
|
||||
@@ -1511,10 +1681,11 @@ export default function EQPage() {
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
{brandDrawerTab === "models" && (
|
||||
<div className="min-h-0 flex-1 overflow-y-auto">
|
||||
<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>
|
||||
{selectedCatalogModelFromSearch && (
|
||||
@@ -1553,9 +1724,11 @@ export default function EQPage() {
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{brandDrawerTab === "target" && (
|
||||
<div className="min-h-0 flex-1 overflow-y-auto">
|
||||
<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>
|
||||
@@ -1715,6 +1888,7 @@ export default function EQPage() {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user