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
+48 -5
View File
@@ -137,7 +137,7 @@ function IOSToggle({ checked, onChange }: { checked: boolean; onChange: (v: bool
// ── List row ──
function ListRow({
icon, label, value, onClick, toggle, checked, onToggle,
icon, label, value, onClick, toggle, checked, onToggle, thumbnail,
}: {
icon: React.ReactNode;
label: string;
@@ -146,6 +146,7 @@ function ListRow({
toggle?: boolean;
checked?: boolean;
onToggle?: (v: boolean) => void;
thumbnail?: string;
}) {
return (
<div className="ios-list-row cursor-pointer" onClick={onClick}>
@@ -160,6 +161,13 @@ function ListRow({
<IOSToggle checked={!!checked} onChange={onToggle ?? (() => {})} />
) : (
<div className="flex items-center gap-1">
{thumbnail && (
<img
src={thumbnail}
alt={value ?? label}
className="h-8 w-14 object-contain rounded bg-black/20 border border-white/10"
/>
)}
{value && <span className="ios-row-value">{value}</span>}
<ChevronRight size={16} className="ios-chevron" />
</div>
@@ -217,6 +225,8 @@ export default function Home() {
const [localVol, setLocalVol] = useState(deviceState?.volume ?? 100);
const isDragging = useRef(false);
const volumeSliderRef = useRef<HTMLInputElement>(null);
const volumeSliderDragAllowedRef = useRef(false);
const knobDraggingRef = useRef(false);
const knobActivePointerIdRef = useRef<number | null>(null);
const knobStartAngleRef = useRef(0);
@@ -233,6 +243,16 @@ export default function Home() {
knobLastVolRef.current = localVol;
}, [localVol]);
const isPointerOnVolumeSliderThumb = useCallback((clientX: number) => {
const el = volumeSliderRef.current;
if (!el) return false;
const rect = el.getBoundingClientRect();
if (rect.width <= 0) return false;
const thumbCenterX = rect.left + (localVol / 200) * rect.width;
const hitRadius = Math.max(28, rect.width * 0.05);
return Math.abs(clientX - thumbCenterX) <= hitRadius;
}, [localVol]);
const handlePowerOff = useCallback(async () => {
if (powerActionRef.current) return;
powerActionRef.current = true;
@@ -662,29 +682,51 @@ export default function Home() {
boxShadow: "0 0 8px rgba(0,255,246,0.5)"
}} />
<input
ref={volumeSliderRef}
type="range" min={0} max={200} value={localVol}
disabled={volumePassthroughActive}
className="cyan-slider relative z-10 disabled:opacity-50"
className="cyan-slider cyan-slider-thumb-only relative z-10 disabled:opacity-50"
onChange={(e) => {
if (volumePassthroughActive) return;
if (!volumeSliderDragAllowedRef.current) {
e.target.value = String(localVol);
return;
}
const v = Number(e.target.value);
setLocalVol(v);
}}
onPointerDown={() => {
onPointerDown={(e) => {
if (volumePassthroughActive) return;
const onThumb = isPointerOnVolumeSliderThumb(e.clientX);
volumeSliderDragAllowedRef.current = onThumb;
if (!onThumb) {
e.preventDefault();
return;
}
isDragging.current = true;
}}
onPointerUp={(e) => {
const allowed = volumeSliderDragAllowedRef.current;
volumeSliderDragAllowedRef.current = false;
isDragging.current = false;
if (!allowed || volumePassthroughActive) return;
applyVolume(Number(e.currentTarget.value));
}}
onPointerCancel={(e) => {
const allowed = volumeSliderDragAllowedRef.current;
volumeSliderDragAllowedRef.current = false;
isDragging.current = false;
applyVolume(Number(e.currentTarget.value));
if (!volumePassthroughActive && allowed) {
applyVolume(Number(e.currentTarget.value));
}
}}
onBlur={(e) => {
const allowed = volumeSliderDragAllowedRef.current;
volumeSliderDragAllowedRef.current = false;
isDragging.current = false;
applyVolume(Number(e.currentTarget.value));
if (!volumePassthroughActive && allowed) {
applyVolume(Number(e.currentTarget.value));
}
}}
onKeyUp={(e) => {
const k = e.key;
@@ -916,6 +958,7 @@ export default function Home() {
icon={<Gauge size={16} />}
label={homeText.vu ?? "VU表"}
value={vuLabel}
thumbnail={`${import.meta.env.BASE_URL}vu/vu${(ds.vu ?? 0) + 1}.png`}
onClick={() => setLocation("/vu")}
/>
<ListRow