修复&优化听力补偿模块功能

This commit is contained in:
eafonyang
2026-09-09 18:26:24 +08:00
parent 8d13847668
commit 8db56e2211
6 changed files with 73 additions and 11 deletions
+25 -3
View File
@@ -1,7 +1,7 @@
import { lazy, Suspense } from "react";
import { lazy, Suspense, useEffect, useRef } from "react";
import { Toaster } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip";
import { Route, Switch, Router } from "wouter";
import { Route, Switch, Router, useLocation } from "wouter";
import { useHashLocation } from "wouter/use-hash-location";
import ErrorBoundary from "./components/ErrorBoundary";
import PageLoader from "./components/PageLoader";
@@ -15,7 +15,7 @@ import BluetoothPage from "./pages/BluetoothPage";
import SystemPage from "./pages/SystemPage";
import IOPage from "./pages/IOPage";
import VUPage from "./pages/VUPage";
import SelectPage from "./pages/SelectPage";
import SelectPage, { consumeSelectReturnScroll } from "./pages/SelectPage";
import CommunityPage from "./pages/CommunityPage";
import ChangelogPage from "./pages/ChangelogPage";
import { AIDrawerProvider } from "./contexts/AIDrawerContext";
@@ -54,6 +54,27 @@ function EffectsPageRoute() {
);
}
/**
* Restores the calling page's scroll position when returning from /select.
* The select page is short, so the browser clamps window scroll to 0 while on
* it; navigateToSelect() captures the offset before navigating and we re-apply
* it once the calling page (e.g. Effects) has remounted.
*/
function SelectScrollRestore() {
const [location] = useLocation();
const prevRef = useRef(location);
useEffect(() => {
const prev = prevRef.current;
prevRef.current = location;
if (prev !== "/select" || location === "/select") return;
const y = consumeSelectReturnScroll();
if (y == null || y <= 0) return;
// Wait a frame so the returning page has rendered and the document has height.
requestAnimationFrame(() => window.scrollTo(0, y));
}, [location]);
return null;
}
function AppRouter() {
return (
<Switch>
@@ -85,6 +106,7 @@ function App() {
<Toaster />
<Router hook={useHashLocation}>
<AIDrawerProvider>
<SelectScrollRestore />
<div className={appShellOuterClass}>
<div className={appShellColumnClass}>
<DesktopAIShell>
@@ -1,8 +1,8 @@
import { useEffect, useMemo, useRef } from "react";
import * as echarts from "echarts";
import {
HEARING_FREQ_LABELS,
buildHearingSplineSeries,
resolveHearingFreqLabels,
} from "@/lib/hearingCurve";
const LEFT_COLOR = "rgb(0, 255, 246)";
@@ -29,6 +29,11 @@ export default function HearingCompensationChart({
const leftSeries = useMemo(() => buildHearingSplineSeries(left), [left]);
const rightSeries = useMemo(() => buildHearingSplineSeries(right), [right]);
// X axis adapts to the control-point count: 7-band (legacy) or 10-band (adds 3k/5k/6k).
const pointCount = Math.max(left.length, right.length);
const freqLabels = useMemo(() => resolveHearingFreqLabels(pointCount), [pointCount]);
const xMax = Math.max(0, pointCount - 1);
useEffect(() => {
const el = containerRef.current;
if (!el) return;
@@ -76,13 +81,13 @@ export default function HearingCompensationChart({
xAxis: {
type: "value",
min: 0,
max: HEARING_FREQ_LABELS.length - 1,
max: xMax,
interval: 1,
axisLabel: {
formatter: (value: number) => {
const idx = Math.round(value);
if (Math.abs(value - idx) > 1e-6) return "";
return HEARING_FREQ_LABELS[idx] ?? "";
return freqLabels[idx] ?? "";
},
fontSize: 14,
color: "#999",
@@ -98,7 +103,9 @@ export default function HearingCompensationChart({
yAxis: {
type: "value",
min: -80,
max: 0,
// Top raised to +20 dB: levels above 70 map to positive dB and were
// clipped at the old max of 0, leaving gaps at the top of the curve.
max: 20,
interval: 10,
name: "dB",
nameLocation: "end",
@@ -157,7 +164,7 @@ export default function HearingCompensationChart({
ro.disconnect();
window.removeEventListener("resize", onResize);
};
}, [leftSeries, rightSeries, leftLabel, rightLabel]);
}, [leftSeries, rightSeries, leftLabel, rightLabel, freqLabels, xMax]);
useEffect(() => {
return () => {
@@ -171,7 +178,7 @@ export default function HearingCompensationChart({
ref={containerRef}
className={
className
?? "w-full h-[min(42vw,200px)] min-h-[140px] md:h-[260px] lg:h-[300px] rounded-lg bg-black/30"
?? "w-full h-[min(52vw,240px)] min-h-[180px] md:h-[320px] lg:h-[380px] rounded-lg bg-black/30"
}
aria-hidden
/>
+17 -1
View File
@@ -1,7 +1,22 @@
/** Hearing compensation curve helpers: map levels → dB and cubic-spline smooth. */
/** 7-band control points (legacy): 125/250/500/1k/2k/4k/8k. */
export const HEARING_FREQ_LABELS = ["125", "250", "500", "1k", "2k", "4k", "8k"] as const;
/** 10-band control points: adds 3k/5k/6k → 125/250/500/1k/2k/3k/4k/5k/6k/8k (ascending). */
export const HEARING_FREQ_LABELS_10 = [
"125", "250", "500", "1k", "2k", "3k", "4k", "5k", "6k", "8k",
] as const;
/**
* Resolve the X-axis frequency labels for a given control-point `count`.
* 10 points → the extended 3k/5k/6k set; any other count → the legacy 7-band
* set. Keeps the chart compatible with both 7-point and 10-point hearing data.
*/
export function resolveHearingFreqLabels(count: number): readonly string[] {
return count === HEARING_FREQ_LABELS_10.length ? HEARING_FREQ_LABELS_10 : HEARING_FREQ_LABELS;
}
/** Y = n + (-70). Values outside [-80, 0] are kept as-is (axis still -80…0). */
export function mapHearingLevelToDb(n: number): number {
return n - 70;
@@ -92,7 +107,8 @@ export function createNaturalCubicSpline(
/**
* Build densely sampled [xIndex, dB] pairs for a smooth ECharts line.
* X uses equal spacing 0…6 matching HEARING_FREQ_LABELS.
* X uses equal spacing 0…N-1 where N = `levels.length`, so it adapts to both
* 7-point and 10-point hearing data (see `resolveHearingFreqLabels`).
*/
export function buildHearingSplineSeries(
levels: number[],
+1 -1
View File
@@ -917,7 +917,7 @@ export default function EffectsPage() {
</button>
{hearingOn && hearingProfiles[hearingIdx] && (
<HearingCompensationChart
className="mt-3 w-full h-[min(42vw,200px)] min-h-[140px] md:h-[260px] lg:h-[300px] rounded-lg bg-black/30"
className="mt-3 w-full h-[min(52vw,240px)] min-h-[180px] md:h-[320px] lg:h-[380px] rounded-lg bg-black/30"
left={hearingProfiles[hearingIdx].l}
right={hearingProfiles[hearingIdx].r}
leftLabel={effectText.hearing?.left ?? "Left"}
+13
View File
@@ -185,10 +185,23 @@ export function consumeSelectPageResult() {
// Scroll position of the calling page, captured before navigating to /select.
// The select page is short, so the browser clamps window scroll to 0 while on
// it; without restoring, the calling page (e.g. Effects) reopens at the top.
let selectReturnScroll: number | null = null;
/** Consume the stored scroll position (once). Returns null when absent. */
export function consumeSelectReturnScroll(): number | null {
const y = selectReturnScroll;
selectReturnScroll = null;
return y;
}
// Helper function to set select page state
export function navigateToSelect(title: string, options: string[], selected: number, back: string, key: string) {
selectReturnScroll = window.scrollY;
selectPageState = { title, options, selected, back, key };
}
+4
View File
@@ -6,6 +6,10 @@
set -e
# AWS CLI v2 默认把命令输出送入分页器(less)create-invalidation 结束后会停在
# (END) 提示符等待按键,导致脚本无法返回 shell。禁用分页器直接打印输出。
export AWS_PAGER=""
show_help() {
cat <<EOF
用法: ./scripts/deploy.sh [环境] [选项]