diff --git a/frontend/components/dashboard/ScanTerminalDashboard.tsx b/frontend/components/dashboard/ScanTerminalDashboard.tsx index 74b263f3..58a5bcb7 100644 --- a/frontend/components/dashboard/ScanTerminalDashboard.tsx +++ b/frontend/components/dashboard/ScanTerminalDashboard.tsx @@ -47,6 +47,7 @@ import { LiveTemperatureThresholdChart } from "@/components/dashboard/scan-termi import { KoyfinRowsTable } from "@/components/dashboard/scan-terminal/KoyfinRowsTable"; import { rowName, pct, money, temp, edgeClass } from "@/components/dashboard/scan-terminal/utils"; import { CitySelectorDropdown } from "@/components/dashboard/scan-terminal/CitySelectorDropdown"; +import { GridLayoutSelector } from "@/components/dashboard/scan-terminal/GridLayoutSelector"; function createEmptyAccess(loading = true): ProAccessState { return { @@ -357,20 +358,120 @@ function PolyWeatherTerminal({ const [navExpanded, setNavExpanded] = useState(false); const [activeNavKey, setActiveNavKey] = useState("contracts"); + const [gridCols, setGridCols] = useState(() => { + if (typeof window !== "undefined") { + try { + const val = localStorage.getItem("polyweather_terminal_grid_cols"); + if (val) { + const num = parseInt(val, 10); + if (num >= 1 && num <= 3) return num; + } + } catch {} + } + return 2; + }); + + const [gridRows, setGridRows] = useState(() => { + if (typeof window !== "undefined") { + try { + const val = localStorage.getItem("polyweather_terminal_grid_rows"); + if (val) { + const num = parseInt(val, 10); + if (num >= 1 && num <= 3) return num; + } + } catch {} + } + return 2; + }); + + const totalSlots = gridCols * gridRows; + const [slots, setSlots] = useState>(() => { - try { - const stored = localStorage.getItem("polyweather_terminal_slots"); - if (stored) { - const parsed = JSON.parse(stored); - if (Array.isArray(parsed) && parsed.length === 4) return parsed; - } - } catch {} - return [null, null, null, null]; + if (typeof window !== "undefined") { + try { + const stored = localStorage.getItem("polyweather_terminal_slots"); + if (stored) { + const parsed = JSON.parse(stored); + if (Array.isArray(parsed)) { + let targetLen = 4; + try { + const cVal = localStorage.getItem("polyweather_terminal_grid_cols"); + const rVal = localStorage.getItem("polyweather_terminal_grid_rows"); + if (cVal && rVal) { + const c = parseInt(cVal, 10); + const r = parseInt(rVal, 10); + if (c >= 1 && c <= 3 && r >= 1 && r <= 3) { + targetLen = c * r; + } + } + } catch {} + + if (parsed.length === targetLen) { + return parsed; + } else if (parsed.length < targetLen) { + return [...parsed, ...Array(targetLen - parsed.length).fill(null)]; + } else { + return parsed.slice(0, targetLen); + } + } + } + } catch {} + } + let defaultLen = 4; + if (typeof window !== "undefined") { + try { + const cVal = localStorage.getItem("polyweather_terminal_grid_cols"); + const rVal = localStorage.getItem("polyweather_terminal_grid_rows"); + if (cVal && rVal) { + const c = parseInt(cVal, 10); + const r = parseInt(rVal, 10); + if (c >= 1 && c <= 3 && r >= 1 && r <= 3) { + defaultLen = c * r; + } + } + } catch {} + } + return Array(defaultLen).fill(null); }); const [activeSlotIndex, setActiveSlotIndex] = useState(0); const [maximizedSlotIndex, setMaximizedSlotIndex] = useState(null); const [activeSearchSlotIndex, setActiveSearchSlotIndex] = useState(null); + const handleSetGridSize = (cols: number, rows: number) => { + const nextTotalSlots = cols * rows; + + setGridCols(cols); + setGridRows(rows); + + try { + localStorage.setItem("polyweather_terminal_grid_cols", String(cols)); + localStorage.setItem("polyweather_terminal_grid_rows", String(rows)); + } catch {} + + let nextSlots = [...slots]; + if (nextSlots.length < nextTotalSlots) { + const diff = nextTotalSlots - nextSlots.length; + nextSlots = [...nextSlots, ...Array(diff).fill(null)]; + } else if (nextSlots.length > nextTotalSlots) { + nextSlots = nextSlots.slice(0, nextTotalSlots); + } + + setSlots(nextSlots); + try { + localStorage.setItem("polyweather_terminal_slots", JSON.stringify(nextSlots)); + } catch {} + + if (activeSlotIndex >= nextTotalSlots) { + setActiveSlotIndex(0); + } + if (maximizedSlotIndex !== null && maximizedSlotIndex >= nextTotalSlots) { + setMaximizedSlotIndex(null); + } + if (activeSearchSlotIndex !== null && activeSearchSlotIndex >= nextTotalSlots) { + setActiveSearchSlotIndex(null); + } + }; + const NAV_ITEMS = [ { key: "contracts", Icon: Table2, labelEn: "Contracts", labelZh: "天气合约" }, { key: "markets", Icon: Activity, labelEn: "Markets", labelZh: "市场概览" }, @@ -390,18 +491,15 @@ function PolyWeatherTerminal({ useEffect(() => { if (filteredRegionRows.length && slots.every((s) => s === null)) { - const next = [ - filteredRegionRows[0]?.city || null, - filteredRegionRows[1]?.city || null, - filteredRegionRows[2]?.city || null, - filteredRegionRows[3]?.city || null, - ]; + const next = Array(totalSlots) + .fill(null) + .map((_, idx) => filteredRegionRows[idx]?.city || null); setSlots(next); try { localStorage.setItem("polyweather_terminal_slots", JSON.stringify(next)); } catch {} } - }, [filteredRegionRows, slots]); + }, [filteredRegionRows, slots, totalSlots]); const handleSelectCityForSlot = (index: number, city: string | null) => { const next = [...slots]; @@ -612,6 +710,14 @@ function PolyWeatherTerminal({
{userLocalTime} +
+ +
+ + {isOpen && ( +
+
{ + setHoveredCols(0); + setHoveredRows(0); + }} + > + {[1, 2, 3].map((r) => + [1, 2, 3].map((c) => { + const isHighlighted = c <= previewCols && r <= previewRows; + return ( +
{ + setHoveredCols(c); + setHoveredRows(r); + }} + onClick={() => handleCellClick(c, r)} + className={clsx( + "h-6 w-6 rounded-sm border cursor-pointer transition-colors", + isHighlighted + ? "bg-blue-500 border-blue-600" + : "bg-slate-100 border-slate-200 hover:bg-slate-200" + )} + /> + ); + }) + )} +
+ +
+ {previewCols} × {previewRows}{" "} + {isEn + ? previewCols * previewRows === 1 + ? "Chart" + : "Charts" + : "图表"} +
+
+ )} +
+ ); +} diff --git a/frontend/components/dashboard/scan-terminal/LiveTemperatureThresholdChart.tsx b/frontend/components/dashboard/scan-terminal/LiveTemperatureThresholdChart.tsx index d914bff4..185f8438 100644 --- a/frontend/components/dashboard/scan-terminal/LiveTemperatureThresholdChart.tsx +++ b/frontend/components/dashboard/scan-terminal/LiveTemperatureThresholdChart.tsx @@ -842,12 +842,9 @@ export function LiveTemperatureThresholdChart({ }) { const [hourly, setHourly] = useState(null); const city = String(row?.city || "").toLowerCase().trim(); - const [timeframe, setTimeframe] = useState<"1D" | "3D" | "5D" | "7D">("1D"); + const [timeframe, setTimeframe] = useState<"1D" | "3D">("1D"); const [hiddenSeriesKeys, setHiddenSeriesKeys] = useState>(new Set()); - - useEffect(() => { - setHiddenSeriesKeys(new Set()); - }, [timeframe]); + const [prevCityAndTf, setPrevCityAndTf] = useState({ city: "", timeframe: "" }); useEffect(() => { if (!city) return; @@ -892,12 +889,6 @@ export function LiveTemperatureThresholdChart({ if (timeframe === "3D") { return build3DayChartData(row, hourly); } - if (timeframe === "5D") { - return buildDailyChartData(row, hourly, 5); - } - if (timeframe === "7D") { - return buildDailyChartData(row, hourly, 7); - } return buildFullDayChartData(row, hourly); }, [row, hourly, timeframe]); @@ -911,11 +902,36 @@ export function LiveTemperatureThresholdChart({ const settlementPlate = useMemo(() => runwayPlates.find((p) => p.isSettlement), [runwayPlates]); const chartSeries = useMemo(() => { - if (timeframe !== "1D") { - return series; + return series; + }, [series]); + + useEffect(() => { + const currentKey = `${city}-${timeframe}`; + const prevKey = `${prevCityAndTf.city}-${prevCityAndTf.timeframe}`; + + if (currentKey !== prevKey) { + const defaults = new Set(); + // Always default all model curves to hidden across all cities and timeframes + series.forEach((s) => { + if (s.key.startsWith("model_curve_")) { + defaults.add(s.key); + } + }); + setHiddenSeriesKeys(defaults); + setPrevCityAndTf({ city, timeframe }); + } else { + setHiddenSeriesKeys((prev) => { + const next = new Set(); + const seriesKeys = new Set(series.map((s) => s.key)); + prev.forEach((k) => { + if (seriesKeys.has(k)) { + next.add(k); + } + }); + return next; + }); } - return series.filter((item) => !hasRunwayData || !item.key.startsWith("model_curve_")); - }, [series, hasRunwayData, timeframe]); + }, [city, timeframe, series, prevCityAndTf]); const activeSeries = useMemo(() => { return chartSeries.filter((s) => !hiddenSeriesKeys.has(s.key)); @@ -1045,7 +1061,7 @@ export function LiveTemperatureThresholdChart({ const timeframeActions = (
- {(["1D", "3D", "5D", "7D"] as const).map((tf) => ( + {(["1D", "3D"] as const).map((tf) => (