Fix dashboard auth and map marker cache updates

This commit is contained in:
2569718930@qq.com
2026-04-08 18:52:59 +08:00
parent f108c9f7df
commit d9faff1bc3
4 changed files with 110 additions and 107 deletions
+1 -35
View File
@@ -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";
@@ -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;
+75 -39
View File
@@ -133,24 +133,19 @@ export function DashboardStoreProvider({
}: {
children: React.ReactNode;
}) {
const initialCache = dashboardClient.readCityDetailCacheBundle();
const initialCacheRef = useRef<ReturnType<
typeof dashboardClient.readCityDetailCacheBundle
> | null>(null);
const [cities, setCities] = useState<CityListItem[]>([]);
const [cityDetailsByName, setCityDetailsByName] = useState<
Record<string, CityDetail>
>(() => initialCache.details);
>({});
const [citySummariesByName, setCitySummariesByName] = useState<
Record<string, CitySummary>
>(() =>
Object.fromEntries(
Object.entries(initialCache.details).map(([cityName, detail]) => [
cityName,
toCitySummary(detail),
]),
),
);
>({});
const [cityDetailMetaByName, setCityDetailMetaByName] = useState<
Record<string, { cachedAt: number; revision: string }>
>(() => initialCache.meta);
>({});
const [marketScanByCityName, setMarketScanByCityName] = useState<
Record<string, MarketScan>
>({});
@@ -173,15 +168,9 @@ export function DashboardStoreProvider({
const mapStopMotionRef = useRef<() => void>(() => {});
const hydratedSelectionRef = useRef(false);
const hydratedProCacheRef = useRef(false);
const backgroundSummaryCheckAtRef = useRef<Record<string, number>>({});
const citySummariesRef = useRef<Record<string, CitySummary>>(
Object.fromEntries(
Object.entries(initialCache.details).map(([cityName, detail]) => [
cityName,
toCitySummary(detail),
]),
),
);
const citySummariesRef = useRef<Record<string, CitySummary>>({});
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 }));
}
+33 -26
View File
@@ -77,6 +77,20 @@ function createMarkerIcon(
});
}
function getMarkerSignature(
city: CityListItem,
snapshot?: Pick<CityDetail, "current" | "temp_symbol"> | 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<string, { temp?: number | null; risk?: string }>
Record<string, string>
>({});
// 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;