修复&优化听力补偿模块功能
This commit is contained in:
+25
-3
@@ -1,7 +1,7 @@
|
|||||||
import { lazy, Suspense } from "react";
|
import { lazy, Suspense, useEffect, useRef } from "react";
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
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 { useHashLocation } from "wouter/use-hash-location";
|
||||||
import ErrorBoundary from "./components/ErrorBoundary";
|
import ErrorBoundary from "./components/ErrorBoundary";
|
||||||
import PageLoader from "./components/PageLoader";
|
import PageLoader from "./components/PageLoader";
|
||||||
@@ -15,7 +15,7 @@ import BluetoothPage from "./pages/BluetoothPage";
|
|||||||
import SystemPage from "./pages/SystemPage";
|
import SystemPage from "./pages/SystemPage";
|
||||||
import IOPage from "./pages/IOPage";
|
import IOPage from "./pages/IOPage";
|
||||||
import VUPage from "./pages/VUPage";
|
import VUPage from "./pages/VUPage";
|
||||||
import SelectPage from "./pages/SelectPage";
|
import SelectPage, { consumeSelectReturnScroll } from "./pages/SelectPage";
|
||||||
import CommunityPage from "./pages/CommunityPage";
|
import CommunityPage from "./pages/CommunityPage";
|
||||||
import ChangelogPage from "./pages/ChangelogPage";
|
import ChangelogPage from "./pages/ChangelogPage";
|
||||||
import { AIDrawerProvider } from "./contexts/AIDrawerContext";
|
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() {
|
function AppRouter() {
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
@@ -85,6 +106,7 @@ function App() {
|
|||||||
<Toaster />
|
<Toaster />
|
||||||
<Router hook={useHashLocation}>
|
<Router hook={useHashLocation}>
|
||||||
<AIDrawerProvider>
|
<AIDrawerProvider>
|
||||||
|
<SelectScrollRestore />
|
||||||
<div className={appShellOuterClass}>
|
<div className={appShellOuterClass}>
|
||||||
<div className={appShellColumnClass}>
|
<div className={appShellColumnClass}>
|
||||||
<DesktopAIShell>
|
<DesktopAIShell>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useEffect, useMemo, useRef } from "react";
|
import { useEffect, useMemo, useRef } from "react";
|
||||||
import * as echarts from "echarts";
|
import * as echarts from "echarts";
|
||||||
import {
|
import {
|
||||||
HEARING_FREQ_LABELS,
|
|
||||||
buildHearingSplineSeries,
|
buildHearingSplineSeries,
|
||||||
|
resolveHearingFreqLabels,
|
||||||
} from "@/lib/hearingCurve";
|
} from "@/lib/hearingCurve";
|
||||||
|
|
||||||
const LEFT_COLOR = "rgb(0, 255, 246)";
|
const LEFT_COLOR = "rgb(0, 255, 246)";
|
||||||
@@ -29,6 +29,11 @@ export default function HearingCompensationChart({
|
|||||||
const leftSeries = useMemo(() => buildHearingSplineSeries(left), [left]);
|
const leftSeries = useMemo(() => buildHearingSplineSeries(left), [left]);
|
||||||
const rightSeries = useMemo(() => buildHearingSplineSeries(right), [right]);
|
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(() => {
|
useEffect(() => {
|
||||||
const el = containerRef.current;
|
const el = containerRef.current;
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
@@ -76,13 +81,13 @@ export default function HearingCompensationChart({
|
|||||||
xAxis: {
|
xAxis: {
|
||||||
type: "value",
|
type: "value",
|
||||||
min: 0,
|
min: 0,
|
||||||
max: HEARING_FREQ_LABELS.length - 1,
|
max: xMax,
|
||||||
interval: 1,
|
interval: 1,
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
formatter: (value: number) => {
|
formatter: (value: number) => {
|
||||||
const idx = Math.round(value);
|
const idx = Math.round(value);
|
||||||
if (Math.abs(value - idx) > 1e-6) return "";
|
if (Math.abs(value - idx) > 1e-6) return "";
|
||||||
return HEARING_FREQ_LABELS[idx] ?? "";
|
return freqLabels[idx] ?? "";
|
||||||
},
|
},
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: "#999",
|
color: "#999",
|
||||||
@@ -98,7 +103,9 @@ export default function HearingCompensationChart({
|
|||||||
yAxis: {
|
yAxis: {
|
||||||
type: "value",
|
type: "value",
|
||||||
min: -80,
|
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,
|
interval: 10,
|
||||||
name: "dB",
|
name: "dB",
|
||||||
nameLocation: "end",
|
nameLocation: "end",
|
||||||
@@ -157,7 +164,7 @@ export default function HearingCompensationChart({
|
|||||||
ro.disconnect();
|
ro.disconnect();
|
||||||
window.removeEventListener("resize", onResize);
|
window.removeEventListener("resize", onResize);
|
||||||
};
|
};
|
||||||
}, [leftSeries, rightSeries, leftLabel, rightLabel]);
|
}, [leftSeries, rightSeries, leftLabel, rightLabel, freqLabels, xMax]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
@@ -171,7 +178,7 @@ export default function HearingCompensationChart({
|
|||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className={
|
className={
|
||||||
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
|
aria-hidden
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,7 +1,22 @@
|
|||||||
/** Hearing compensation curve helpers: map levels → dB and cubic-spline smooth. */
|
/** 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;
|
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). */
|
/** Y = n + (-70). Values outside [-80, 0] are kept as-is (axis still -80…0). */
|
||||||
export function mapHearingLevelToDb(n: number): number {
|
export function mapHearingLevelToDb(n: number): number {
|
||||||
return n - 70;
|
return n - 70;
|
||||||
@@ -92,7 +107,8 @@ export function createNaturalCubicSpline(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Build densely sampled [xIndex, dB] pairs for a smooth ECharts line.
|
* 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(
|
export function buildHearingSplineSeries(
|
||||||
levels: number[],
|
levels: number[],
|
||||||
|
|||||||
@@ -917,7 +917,7 @@ export default function EffectsPage() {
|
|||||||
</button>
|
</button>
|
||||||
{hearingOn && hearingProfiles[hearingIdx] && (
|
{hearingOn && hearingProfiles[hearingIdx] && (
|
||||||
<HearingCompensationChart
|
<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}
|
left={hearingProfiles[hearingIdx].l}
|
||||||
right={hearingProfiles[hearingIdx].r}
|
right={hearingProfiles[hearingIdx].r}
|
||||||
leftLabel={effectText.hearing?.left ?? "Left"}
|
leftLabel={effectText.hearing?.left ?? "Left"}
|
||||||
|
|||||||
@@ -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
|
// Helper function to set select page state
|
||||||
|
|
||||||
export function navigateToSelect(title: string, options: string[], selected: number, back: string, key: string) {
|
export function navigateToSelect(title: string, options: string[], selected: number, back: string, key: string) {
|
||||||
|
|
||||||
|
selectReturnScroll = window.scrollY;
|
||||||
selectPageState = { title, options, selected, back, key };
|
selectPageState = { title, options, selected, back, key };
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,10 @@
|
|||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
# AWS CLI v2 默认把命令输出送入分页器(less),create-invalidation 结束后会停在
|
||||||
|
# (END) 提示符等待按键,导致脚本无法返回 shell。禁用分页器直接打印输出。
|
||||||
|
export AWS_PAGER=""
|
||||||
|
|
||||||
show_help() {
|
show_help() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
用法: ./scripts/deploy.sh [环境] [选项]
|
用法: ./scripts/deploy.sh [环境] [选项]
|
||||||
|
|||||||
Reference in New Issue
Block a user