From 598cd17c0bb58e2e6dc0da1c478df814e3e03203 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Tue, 26 May 2026 07:57:38 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9F=8E=E5=B8=82=E5=9B=BE=E8=A1=A8=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=20LoadingSignal=20=E7=BB=84=E4=BB=B6=EF=BC=9Adetail?= =?UTF-8?q?=20=E6=95=B0=E6=8D=AE=E5=8A=A0=E8=BD=BD=E4=B8=AD=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E8=BF=9B=E5=BA=A6=E6=8C=87=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../LiveTemperatureThresholdChart.tsx | 137 +++++++++++++++--- 1 file changed, 114 insertions(+), 23 deletions(-) diff --git a/frontend/components/dashboard/scan-terminal/LiveTemperatureThresholdChart.tsx b/frontend/components/dashboard/scan-terminal/LiveTemperatureThresholdChart.tsx index 99316084..30f98d2e 100644 --- a/frontend/components/dashboard/scan-terminal/LiveTemperatureThresholdChart.tsx +++ b/frontend/components/dashboard/scan-terminal/LiveTemperatureThresholdChart.tsx @@ -2,6 +2,7 @@ import clsx from "clsx"; import { useEffect, useMemo, useState } from "react"; +import { LoadingSignal } from "@/components/dashboard/scan-terminal/LoadingSignal"; import { Brush, CartesianGrid, @@ -199,6 +200,47 @@ const _hourlyCache = new Map(); const _hourlyRequestCache = new Map>(); const RUNWAY_LINE_COLORS = ["#00897b", "#d97706", "#7c3aed", "#0891b2", "#ea580c", "#64748b"]; +const SESSION_CACHE_PREFIX = "polyweather_city_detail_v1:"; +const SESSION_CACHE_TTL_MS = 10 * 60 * 1000; // 10 minutes cache for session storage + +function readSessionCache(city: string): { ts: number; data: HourlyForecast } | null { + if (typeof window === "undefined") return null; + try { + const raw = sessionStorage.getItem(`${SESSION_CACHE_PREFIX}${city}`); + if (!raw) return null; + const item = JSON.parse(raw); + if (item && item.ts && Date.now() - item.ts < SESSION_CACHE_TTL_MS) { + return item; + } + } catch {} + return null; +} + +function writeSessionCache(city: string, data: HourlyForecast) { + if (typeof window === "undefined" || !data) return; + try { + sessionStorage.setItem( + `${SESSION_CACHE_PREFIX}${city}`, + JSON.stringify({ ts: Date.now(), data }) + ); + } catch {} +} + +export function clearCityDetailCache() { + _hourlyCache.clear(); + _hourlyRequestCache.clear(); + if (typeof window !== "undefined") { + try { + for (let i = sessionStorage.length - 1; i >= 0; i--) { + const key = sessionStorage.key(i); + if (key && key.startsWith(SESSION_CACHE_PREFIX)) { + sessionStorage.removeItem(key); + } + } + } catch {} + } +} + function validNumber(value: unknown): number | null { return typeof value === "number" && Number.isFinite(value) ? value : null; } @@ -419,11 +461,16 @@ async function fetchHourlyForecastForCity(city: string): Promise return cached.data; } + const sessionCached = readSessionCache(city); + if (sessionCached) { + _hourlyCache.set(city, sessionCached); + return sessionCached.data; + } + const pending = _hourlyRequestCache.get(city); if (pending) return pending; const request = fetch(`/api/city/${encodeURIComponent(city)}/detail?depth=full&force_refresh=false`, { - cache: "no-store", headers: { Accept: "application/json" }, }) .then(async (res) => { @@ -451,6 +498,7 @@ async function fetchHourlyForecastForCity(city: string): Promise airportPrimaryTodayObs: (json as any)?.official?.airport_primary_today_obs || (json as any)?.airport_primary_today_obs || undefined, }; _hourlyCache.set(city, { ts: Date.now(), data }); + writeSessionCache(city, data); return data; }) .finally(() => { @@ -1049,6 +1097,8 @@ export function LiveTemperatureThresholdChart({ onClose, isMaximized = false, disableClose = false, + isActive = !compact, + slotIndex = 0, }: { isEn: boolean; row: ScanOpportunityRow | null; @@ -1059,8 +1109,11 @@ export function LiveTemperatureThresholdChart({ onClose?: () => void; isMaximized?: boolean; disableClose?: boolean; + isActive?: boolean; + slotIndex?: number; }) { const [hourly, setHourly] = useState(null); + const [isFetching, setIsFetching] = useState(false); const city = String(row?.city || "").toLowerCase().trim(); const [timeframe, setTimeframe] = useState<"1D" | "3D">("1D"); const [userToggledKeys, setUserToggledKeys] = useState>({}); @@ -1071,21 +1124,48 @@ export function LiveTemperatureThresholdChart({ useEffect(() => { if (!city) return; - const cached = _hourlyCache.get(city); + + // Check in-memory cache first + let cached = _hourlyCache.get(city); + if (!cached) { + // Fallback to session cache + const sessionEntry = readSessionCache(city); + if (sessionEntry) { + cached = sessionEntry; + _hourlyCache.set(city, sessionEntry); + } + } + if (cached && Date.now() - cached.ts < HOURLY_CACHE_TTL_MS) { setHourly(cached.data); + setIsFetching(false); return; } + setHourly(seedHourlyForecastFromRow(row)); + setIsFetching(true); let cancelled = false; - fetchHourlyForecastForCity(city) - .then((data) => { - if (cancelled || !data) return; - setHourly(data); - }) - .catch(() => {}); - return () => { cancelled = true; }; - }, [city, row]); + + // Prioritize active slots, stagger/delay background slots to optimize load performance + const delay = isActive ? 0 : (slotIndex ? 300 + slotIndex * 250 : 350); + + const timer = setTimeout(() => { + fetchHourlyForecastForCity(city) + .then((data) => { + if (cancelled || !data) return; + setHourly(data); + setIsFetching(false); + }) + .catch(() => { + if (!cancelled) setIsFetching(false); + }); + }, delay); + + return () => { + cancelled = true; + clearTimeout(timer); + }; + }, [city, row, isActive, slotIndex]); const { data, series } = useMemo(() => { if (timeframe === "3D") { @@ -1125,38 +1205,42 @@ export function LiveTemperatureThresholdChart({ return getVisibleTemperatureSeries(city, chartSeries, userToggledKeys); }, [chartSeries, userToggledKeys, city]); - const cityKey = String(row?.city || "").toLowerCase().trim(); + const normalizedKey = normalizeCityKey(row?.city); const runwaySensorCities = new Set([ 'beijing', 'shanghai', 'guangzhou', 'qingdao', 'chengdu', 'chongqing', 'wuhan', // AMSC runway sensors 'seoul', 'busan', // AMOS runway sensors ]); - const isHKO = cityKey === 'hong kong' || cityKey === 'lau fau shan' || cityKey === 'shenzhen' - || cityKey.includes('hongkong') || cityKey.includes('laufau'); - const isTokyo = cityKey === 'tokyo'; - const isSingapore = cityKey === 'singapore'; - const isParis = cityKey === 'paris'; - const isWeatherStation = !runwaySensorCities.has(cityKey) - && !isHKO && !isTokyo && !isSingapore && !isParis; + const isShenzhen = normalizedKey === 'shenzhen'; + const isHKO = (normalizedKey === 'hongkong' || normalizedKey === 'laufaushan') && !isShenzhen; + const isTokyo = normalizedKey === 'tokyo'; + const isSingapore = normalizedKey === 'singapore'; + const isParis = normalizedKey === 'paris'; + const isWeatherStation = !runwaySensorCities.has(normalizedKey) + && !isHKO && !isShenzhen && !isTokyo && !isSingapore && !isParis; - const runwayHeaderLabel = isHKO ? '参考站点 (1分钟)' + const runwayHeaderLabel = isShenzhen ? '天文台实测 (10分钟)' + : isHKO ? '参考站点 (1分钟)' : isTokyo ? '机场气象站 (10分钟)' : isSingapore ? '航站楼温度' : isParis ? '官方机场观测 (15分钟)' : isWeatherStation ? '气象站实测' : '跑道实测 (1分钟)'; - const metarHeaderLabel = isHKO ? '天文台实测 (10分钟)' + const metarHeaderLabel = isShenzhen ? '天文台实测 (10分钟)' + : isHKO ? '天文台实测 (10分钟)' : 'METAR 结算 (30分钟)'; - const runwayHighLabel = isHKO ? '参考站点' + const runwayHighLabel = isShenzhen ? '天文台实测' + : isHKO ? '参考站点' : isTokyo ? '机场气象站' : isSingapore ? '航站楼' : isParis ? '官方机场观测' : isWeatherStation ? '气象站' : '跑道实测'; - const metarHighLabel = isHKO ? '天文台' + const metarHighLabel = isShenzhen ? '天文台' + : isHKO ? '天文台' : 'METAR 官方'; const { currentRunwayTemp, observedHighMetar, observedHighRunway } = useMemo( @@ -1313,6 +1397,13 @@ export function LiveTemperatureThresholdChart({ return ( + {isFetching && ( + + )}
{/* Compact stats bar */} {compact ? ( @@ -1325,7 +1416,7 @@ export function LiveTemperatureThresholdChart({ | - {isEn ? "METAR" : metarHeaderLabel}:{" "} + {isEn ? "METAR" : (isShenzhen ? "当日最高" : metarHeaderLabel)}:{" "} {temp(observedHighMetar)}