diff --git a/frontend/components/dashboard/ScanTerminalDashboard.tsx b/frontend/components/dashboard/ScanTerminalDashboard.tsx index 1496bbcd..edd545bf 100644 --- a/frontend/components/dashboard/ScanTerminalDashboard.tsx +++ b/frontend/components/dashboard/ScanTerminalDashboard.tsx @@ -15,7 +15,7 @@ import { UserRound, Users, } from "lucide-react"; -import { Fragment, useCallback, useDeferredValue, useEffect, useMemo, useRef, useState } from "react"; +import { Fragment, memo, useCallback, useDeferredValue, useEffect, useMemo, useRef, useState } from "react"; import type { CityListItem, ProAccessState, ScanOpportunityRow } from "@/lib/dashboard-types"; import { getInitialLocaleFromNavigator } from "@/lib/i18n"; import { isBrowserLocalFullAccess } from "@/lib/local-dev-access"; @@ -87,6 +87,11 @@ const TrainingDashboard = dynamic( ); const ONLINE_USERS_REFRESH_MS = 5 * 60_000; +const TERMINAL_NAV_ITEMS = [ + { key: "thresholds", Icon: Activity, labelEn: "Decision", labelZh: "天气决策" }, + { key: "training", Icon: GraduationCap, labelEn: "Training", labelZh: "训练数据" }, + { key: "guide", Icon: BookOpenCheck, labelEn: "Guide", labelZh: "使用指南" }, +] as const; const AUTH_PROFILE_REQUEST_TIMEOUT_MS = 4500; const AUTH_DECISION_RECOVERY_MS = 10_000; const ACTIVE_ACCESS_CACHE_KEY = "polyweather_terminal_active_access_v1"; @@ -433,6 +438,99 @@ function EmptySlotCard({ ); } +const TerminalSidebar = memo(function TerminalSidebar({ + activeNavKey, + isEn, + onSelectNav, +}: { + activeNavKey: string; + isEn: boolean; + onSelectNav: (key: string) => void; +}) { + const [navExpanded, setNavExpanded] = useState(false); + + return ( + + ); +}); + function PolyWeatherTerminal({ generatedText, isEn, @@ -495,9 +593,11 @@ function PolyWeatherTerminal({ window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [searchInputRef, setSearchQuery]); - const [navExpanded, setNavExpanded] = useState(false); const [activeNavKey, setActiveNavKey] = useState("thresholds"); const [onlineCount, setOnlineCount] = useState(null); + const handleSelectNav = useCallback((key: string) => { + setActiveNavKey(key); + }, []); useEffect(() => { if (activeNavKey !== "thresholds") return; @@ -577,12 +677,6 @@ function PolyWeatherTerminal({ } }; - const NAV_ITEMS = [ - { key: "thresholds", Icon: Activity, labelEn: "Decision", labelZh: "天气决策" }, - { key: "training", Icon: GraduationCap, labelEn: "Training", labelZh: "训练数据" }, - { key: "guide", Icon: BookOpenCheck, labelEn: "Guide", labelZh: "使用指南" }, - ]; - useEffect(() => { setSelectedCity(null); }, [selectedRegionKey, setSelectedCity]); @@ -706,87 +800,7 @@ function PolyWeatherTerminal({ return (
- +
diff --git a/frontend/components/dashboard/scan-terminal/__tests__/terminalGridPolicy.test.ts b/frontend/components/dashboard/scan-terminal/__tests__/terminalGridPolicy.test.ts index b7b893d5..1756c546 100644 --- a/frontend/components/dashboard/scan-terminal/__tests__/terminalGridPolicy.test.ts +++ b/frontend/components/dashboard/scan-terminal/__tests__/terminalGridPolicy.test.ts @@ -165,6 +165,13 @@ export function runTests() { chartCanvasSource.includes("TemperatureChartCanvasComponent"), "temperature chart canvas must be memoized so unrelated terminal state does not remount Recharts", ); + assert( + dashboardSource.includes("const TerminalSidebar = memo(function TerminalSidebar") && + dashboardSource.includes("const [navExpanded, setNavExpanded] = useState(false);") && + dashboardSource.includes("transition-colors duration-150") && + !dashboardSource.includes("transition-all duration-200"), + "terminal sidebar expand state must be isolated from the chart grid and must not animate width through transition-all", + ); assert( __shouldKeepTemperatureChartLoadingForTest({ row: { city: "Moscow" } as any,