feat: add AiCityTemperatureChart component and useAiPinnedCityWorkspace hook for deep-analysis city tracking

This commit is contained in:
2569718930@qq.com
2026-05-17 21:37:51 +08:00
parent 1645fd88d0
commit 3351991fe1
2 changed files with 25 additions and 10 deletions
@@ -78,14 +78,25 @@ export const AiCityTemperatureChart = memo(function AiCityTemperatureChart({ det
cityKey: string; cityKey: string;
data: TemperatureChartData; data: TemperatureChartData;
} | null>(null); } | null>(null);
if (computedChartData) { // Use a memo so we never mutate refs during render (React anti-pattern).
lastChartDataRef.current = { cityKey, data: computedChartData }; // When cityKey changes, discard any stale cache; once computedChartData
} // arrives, save it into the ref and use it.
const chartData = const chartData = useMemo(() => {
computedChartData || if (lastChartDataRef.current && lastChartDataRef.current.cityKey !== cityKey) {
(lastChartDataRef.current?.cityKey === cityKey // City switched — clear stale cache so the old city's chart cannot
? lastChartDataRef.current.data // bleed into the new city card while its detail is still loading.
: null); 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 forecastLabel = locale === "en-US" ? "DEB baseline" : "DEB 原始路径";
const calibratedLabel = const calibratedLabel =
locale === "en-US" locale === "en-US"
@@ -205,6 +216,7 @@ export const AiCityTemperatureChart = memo(function AiCityTemperatureChart({ det
}, [ }, [
calibratedLabel, calibratedLabel,
chartData, chartData,
cityKey,
detail.temp_symbol, detail.temp_symbol,
forecastLabel, forecastLabel,
hasCalibratedPath, hasCalibratedPath,
@@ -40,7 +40,6 @@ export function useAiPinnedCityWorkspace({
const nextCity = aiHydrationQueueRef.current.shift(); const nextCity = aiHydrationQueueRef.current.shift();
const key = normalizeCityKey(nextCity || ""); const key = normalizeCityKey(nextCity || "");
if (!nextCity || !key) continue; if (!nextCity || !key) continue;
const existingDetail = findDetailForCity(store.cityDetailsByName, nextCity);
try { try {
const detail = await store.ensureCityDetail( const detail = await store.ensureCityDetail(
nextCity, nextCity,
@@ -72,7 +71,7 @@ export function useAiPinnedCityWorkspace({
void runAiHydrationQueue(); void runAiHydrationQueue();
} }
} }
}, [store.cityDetailsByName, store.ensureCityDetail]); }, [store.ensureCityDetail]);
const queueAiFullHydration = useCallback( const queueAiFullHydration = useCallback(
(cityName: string) => { (cityName: string) => {
@@ -97,6 +96,10 @@ export function useAiPinnedCityWorkspace({
getLocalizedCityName(cleanName, prettyName || cleanName, locale) || getLocalizedCityName(cleanName, prettyName || cleanName, locale) ||
prettyName || prettyName ||
cleanName; 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) => { setAiPinnedCities((current) => {
const existing = current.findIndex( const existing = current.findIndex(
(item) => normalizeCityKey(item.cityName) === key, (item) => normalizeCityKey(item.cityName) === key,