From d9faff1bc3e3367b5d7b37aa98ef4680309e3daf Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Wed, 8 Apr 2026 18:52:59 +0800 Subject: [PATCH] Fix dashboard auth and map marker cache updates --- frontend/components/dashboard/HeaderBar.tsx | 36 +----- .../dashboard/PolyWeatherDashboard.tsx | 8 +- frontend/hooks/useDashboardStore.tsx | 114 ++++++++++++------ frontend/hooks/useLeafletMap.ts | 59 +++++---- 4 files changed, 110 insertions(+), 107 deletions(-) diff --git a/frontend/components/dashboard/HeaderBar.tsx b/frontend/components/dashboard/HeaderBar.tsx index 01b715ba..1c531f5b 100644 --- a/frontend/components/dashboard/HeaderBar.tsx +++ b/frontend/components/dashboard/HeaderBar.tsx @@ -1,16 +1,11 @@ "use client"; -import { useEffect, useState } from "react"; import Link from "next/link"; import { usePathname } from "next/navigation"; import clsx from "clsx"; import { LogIn, UserRound } from "lucide-react"; import { useDashboardStore } from "@/hooks/useDashboardStore"; import { useI18n } from "@/hooks/useI18n"; -import { - getSupabaseBrowserClient, - hasSupabasePublicEnv, -} from "@/lib/supabase/client"; function parseExpiryInfo(raw?: string | null) { const text = String(raw || "").trim(); @@ -30,41 +25,12 @@ export function HeaderBar() { const store = useDashboardStore(); const { locale, setLocale, t } = useI18n(); const pathname = usePathname(); - const [isAuthenticated, setIsAuthenticated] = useState(false); - const supabaseReady = hasSupabasePublicEnv(); + const isAuthenticated = store.proAccess.authenticated; const docsHref = "/docs/intro"; const docsActive = pathname?.startsWith("/docs"); const trialPromoLabel = locale === "en-US" ? "New users get 3-day Pro trial" : "新用户可免费体验 3 天 Pro"; - useEffect(() => { - let mounted = true; - - if (!supabaseReady) { - setIsAuthenticated(false); - return; - } - - const supabase = getSupabaseBrowserClient(); - - void supabase.auth.getSession().then(({ data }) => { - if (!mounted) return; - setIsAuthenticated(Boolean(data.session?.user?.id)); - }); - - const { - data: { subscription }, - } = supabase.auth.onAuthStateChange((_event, session) => { - if (!mounted) return; - setIsAuthenticated(Boolean(session?.user?.id)); - }); - - return () => { - mounted = false; - subscription.unsubscribe(); - }; - }, [supabaseReady]); - const accountHref = isAuthenticated ? "/account" : "/auth/login?next=%2Faccount"; diff --git a/frontend/components/dashboard/PolyWeatherDashboard.tsx b/frontend/components/dashboard/PolyWeatherDashboard.tsx index 653c2121..366a509d 100644 --- a/frontend/components/dashboard/PolyWeatherDashboard.tsx +++ b/frontend/components/dashboard/PolyWeatherDashboard.tsx @@ -1,7 +1,6 @@ "use client"; - -import { useEffect } from "react"; import dynamic from "next/dynamic"; +import { useEffect } from "react"; import styles from "./Dashboard.module.css"; import { DashboardStoreProvider, @@ -47,11 +46,6 @@ function DashboardScreen() { const store = useDashboardStore(); const { t } = useI18n(); - useEffect(() => { - void import("@/components/dashboard/HistoryModal"); - void import("@/components/dashboard/FutureForecastModal"); - }, []); - useEffect(() => { const onKeyDown = (event: KeyboardEvent) => { if (event.key !== "Escape") return; diff --git a/frontend/hooks/useDashboardStore.tsx b/frontend/hooks/useDashboardStore.tsx index 311f7e3c..cbf9291f 100644 --- a/frontend/hooks/useDashboardStore.tsx +++ b/frontend/hooks/useDashboardStore.tsx @@ -133,24 +133,19 @@ export function DashboardStoreProvider({ }: { children: React.ReactNode; }) { - const initialCache = dashboardClient.readCityDetailCacheBundle(); + const initialCacheRef = useRef | null>(null); const [cities, setCities] = useState([]); const [cityDetailsByName, setCityDetailsByName] = useState< Record - >(() => initialCache.details); + >({}); const [citySummariesByName, setCitySummariesByName] = useState< Record - >(() => - Object.fromEntries( - Object.entries(initialCache.details).map(([cityName, detail]) => [ - cityName, - toCitySummary(detail), - ]), - ), - ); + >({}); const [cityDetailMetaByName, setCityDetailMetaByName] = useState< Record - >(() => initialCache.meta); + >({}); const [marketScanByCityName, setMarketScanByCityName] = useState< Record >({}); @@ -173,15 +168,9 @@ export function DashboardStoreProvider({ const mapStopMotionRef = useRef<() => void>(() => {}); const hydratedSelectionRef = useRef(false); + const hydratedProCacheRef = useRef(false); const backgroundSummaryCheckAtRef = useRef>({}); - const citySummariesRef = useRef>( - Object.fromEntries( - Object.entries(initialCache.details).map(([cityName, detail]) => [ - cityName, - toCitySummary(detail), - ]), - ), - ); + const citySummariesRef = useRef>({}); const selectedDetail = selectedCity && proAccess.subscriptionActive ? cityDetailsByName[selectedCity] || null @@ -200,11 +189,22 @@ export function DashboardStoreProvider({ : null; useEffect(() => { + if (proAccess.loading) return; + if (!proAccess.authenticated || !proAccess.subscriptionActive) { + dashboardClient.clearCityDetailCache(); + return; + } dashboardClient.writeCityDetailCacheBundle( cityDetailsByName, cityDetailMetaByName, ); - }, [cityDetailMetaByName, cityDetailsByName]); + }, [ + cityDetailMetaByName, + cityDetailsByName, + proAccess.authenticated, + proAccess.loading, + proAccess.subscriptionActive, + ]); useEffect(() => { citySummariesRef.current = citySummariesByName; @@ -214,6 +214,34 @@ export function DashboardStoreProvider({ proAccessRef.current = proAccess; }, [proAccess]); + useEffect(() => { + if (proAccess.loading) return; + if (!proAccess.authenticated || !proAccess.subscriptionActive) { + hydratedProCacheRef.current = false; + initialCacheRef.current = null; + return; + } + if (hydratedProCacheRef.current) return; + + hydratedProCacheRef.current = true; + const cached = + initialCacheRef.current || dashboardClient.readCityDetailCacheBundle(); + initialCacheRef.current = cached; + if (!Object.keys(cached.details).length) return; + + setCityDetailsByName(cached.details); + setCityDetailMetaByName(cached.meta); + setCitySummariesByName((current) => ({ + ...Object.fromEntries( + Object.entries(cached.details).map(([cityName, detail]) => [ + cityName, + toCitySummary(detail), + ]), + ), + ...current, + })); + }, [proAccess.authenticated, proAccess.loading, proAccess.subscriptionActive]); + useEffect(() => { if (proAccess.loading) return; if (proAccess.authenticated && proAccess.subscriptionActive) return; @@ -636,31 +664,39 @@ export function DashboardStoreProvider({ }; const refreshAll = async () => { - const previousSelectedDetail = selectedCity - ? cityDetailsByName[selectedCity] - : undefined; dashboardClient.clearCityDetailCache(); setCityDetailsByName({}); setCityDetailMetaByName({}); if (selectedCity) { + const access = proAccessRef.current; setLoadingState((current) => ({ ...current, refresh: true })); try { - const latestDetail = await dashboardClient.getCityDetail(selectedCity, { - force: true, - }); - const detail = latestDetail; - setCityDetailsByName({ [selectedCity]: detail }); - setCitySummariesByName((current) => ({ - ...current, - [selectedCity]: toCitySummary(detail), - })); - setCityDetailMetaByName({ - [selectedCity]: { - cachedAt: Date.now(), - revision: getCityRevision(detail), - }, - }); - setSelectedForecastDate(detail.local_date); + if (access.authenticated && access.subscriptionActive) { + const latestDetail = await dashboardClient.getCityDetail(selectedCity, { + force: true, + }); + const detail = latestDetail; + setCityDetailsByName({ [selectedCity]: detail }); + setCitySummariesByName((current) => ({ + ...current, + [selectedCity]: toCitySummary(detail), + })); + setCityDetailMetaByName({ + [selectedCity]: { + cachedAt: Date.now(), + revision: getCityRevision(detail), + }, + }); + setSelectedForecastDate(detail.local_date); + } else { + const summary = await dashboardClient.getCitySummary(selectedCity, { + force: true, + }); + setCitySummariesByName((current) => ({ + ...current, + [selectedCity]: summary, + })); + } } finally { setLoadingState((current) => ({ ...current, refresh: false })); } diff --git a/frontend/hooks/useLeafletMap.ts b/frontend/hooks/useLeafletMap.ts index 96b39a23..4ca69a9b 100644 --- a/frontend/hooks/useLeafletMap.ts +++ b/frontend/hooks/useLeafletMap.ts @@ -77,6 +77,20 @@ function createMarkerIcon( }); } +function getMarkerSignature( + city: CityListItem, + snapshot?: Pick | CitySummary, +) { + return [ + city.display_name, + city.risk_level, + city.temp_unit, + city.lat, + city.lon, + snapshot?.current?.temp ?? "", + ].join("|"); +} + function buildNearbyIconHtml(detail: CityDetail, station: NearbyStation) { const sanitizeWindText = (value?: string | null) => { const text = String(value || "").trim(); @@ -295,7 +309,7 @@ export function useLeafletMap({ }, [cities]); const lastCityDataRef = useRef< - Record + Record >({}); // Handle marker synchronization @@ -309,29 +323,32 @@ export function useLeafletMap({ if (canceled) return; const currentMarkers = markersRef.current; - const nextMarkers: typeof currentMarkers = {}; - const nextLastData: typeof lastCityDataRef.current = {}; + const cityNames = new Set(cities.map((city) => city.name)); + + Object.entries(currentMarkers).forEach(([name, entry]) => { + if (cityNames.has(name)) return; + map.removeLayer(entry.marker); + delete currentMarkers[name]; + delete lastCityDataRef.current[name]; + }); cities.forEach((city) => { const detail = cityDetailsByName[city.name]; const summary = citySummariesByName[city.name]; const snapshot = detail || summary; const existing = currentMarkers[city.name]; - - const currentTemp = snapshot?.current?.temp; - const currentRisk = city.risk_level; - const lastData = lastCityDataRef.current[city.name]; - const dataChanged = - !lastData || - lastData.temp !== currentTemp || - lastData.risk !== currentRisk; + const signature = getMarkerSignature(city, snapshot); + const previousSignature = lastCityDataRef.current[city.name]; if (existing) { - if (dataChanged) { + if (existing.city.lat !== city.lat || existing.city.lon !== city.lon) { + existing.marker.setLatLng([city.lat, city.lon]); + } + if (previousSignature !== signature) { existing.marker.setIcon(createMarkerIcon(city, snapshot)); } - nextMarkers[city.name] = { city, marker: existing.marker }; - nextLastData[city.name] = { temp: currentTemp, risk: currentRisk }; + currentMarkers[city.name] = { city, marker: existing.marker }; + lastCityDataRef.current[city.name] = signature; return; } @@ -357,19 +374,9 @@ export function useLeafletMap({ onSelectCityRef.current(city.name); }); - nextMarkers[city.name] = { city, marker }; - nextLastData[city.name] = { temp: currentTemp, risk: currentRisk }; + currentMarkers[city.name] = { city, marker }; + lastCityDataRef.current[city.name] = signature; }); - - // Cleanup removed markers - Object.entries(currentMarkers).forEach(([name, entry]) => { - if (!nextMarkers[name]) { - map.removeLayer(entry.marker); - } - }); - - markersRef.current = nextMarkers; - lastCityDataRef.current = nextLastData; }) : null;