From 3351991fe12ed84717a5a04d675e0db5dec34628 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Sun, 17 May 2026 21:37:51 +0800 Subject: [PATCH] feat: add AiCityTemperatureChart component and useAiPinnedCityWorkspace hook for deep-analysis city tracking --- .../scan-terminal/AiCityTemperatureChart.tsx | 28 +++++++++++++------ .../use-ai-pinned-city-workspace.ts | 7 +++-- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/frontend/components/dashboard/scan-terminal/AiCityTemperatureChart.tsx b/frontend/components/dashboard/scan-terminal/AiCityTemperatureChart.tsx index cf04893e..9667f6cd 100644 --- a/frontend/components/dashboard/scan-terminal/AiCityTemperatureChart.tsx +++ b/frontend/components/dashboard/scan-terminal/AiCityTemperatureChart.tsx @@ -78,14 +78,25 @@ export const AiCityTemperatureChart = memo(function AiCityTemperatureChart({ det cityKey: string; data: TemperatureChartData; } | null>(null); - if (computedChartData) { - lastChartDataRef.current = { cityKey, data: computedChartData }; - } - const chartData = - computedChartData || - (lastChartDataRef.current?.cityKey === cityKey - ? lastChartDataRef.current.data - : null); + // Use a memo so we never mutate refs during render (React anti-pattern). + // When cityKey changes, discard any stale cache; once computedChartData + // arrives, save it into the ref and use it. + const chartData = useMemo(() => { + if (lastChartDataRef.current && lastChartDataRef.current.cityKey !== cityKey) { + // City switched — clear stale cache so the old city's chart cannot + // bleed into the new city card while its detail is still loading. + lastChartDataRef.current = null; + } + if (computedChartData) { + lastChartDataRef.current = { cityKey, data: computedChartData }; + return computedChartData; + } + if (lastChartDataRef.current?.cityKey === cityKey) { + return lastChartDataRef.current.data; + } + return null; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [cityKey, computedChartData]); const forecastLabel = locale === "en-US" ? "DEB baseline" : "DEB 原始路径"; const calibratedLabel = locale === "en-US" @@ -205,6 +216,7 @@ export const AiCityTemperatureChart = memo(function AiCityTemperatureChart({ det }, [ calibratedLabel, chartData, + cityKey, detail.temp_symbol, forecastLabel, hasCalibratedPath, diff --git a/frontend/components/dashboard/scan-terminal/use-ai-pinned-city-workspace.ts b/frontend/components/dashboard/scan-terminal/use-ai-pinned-city-workspace.ts index d13e7a05..742af19d 100644 --- a/frontend/components/dashboard/scan-terminal/use-ai-pinned-city-workspace.ts +++ b/frontend/components/dashboard/scan-terminal/use-ai-pinned-city-workspace.ts @@ -40,7 +40,6 @@ export function useAiPinnedCityWorkspace({ const nextCity = aiHydrationQueueRef.current.shift(); const key = normalizeCityKey(nextCity || ""); if (!nextCity || !key) continue; - const existingDetail = findDetailForCity(store.cityDetailsByName, nextCity); try { const detail = await store.ensureCityDetail( nextCity, @@ -72,7 +71,7 @@ export function useAiPinnedCityWorkspace({ void runAiHydrationQueue(); } } - }, [store.cityDetailsByName, store.ensureCityDetail]); + }, [store.ensureCityDetail]); const queueAiFullHydration = useCallback( (cityName: string) => { @@ -97,6 +96,10 @@ export function useAiPinnedCityWorkspace({ getLocalizedCityName(cleanName, prettyName || cleanName, locale) || prettyName || cleanName; + // Clear the hydration guard so that re-selecting this city always + // triggers a fresh hydration attempt (fixes second-city loading failure). + aiFullHydrationRef.current.delete(key); + aiHydrationRetriesRef.current.delete(key); setAiPinnedCities((current) => { const existing = current.findIndex( (item) => normalizeCityKey(item.cityName) === key,