From a0adf05d1e5748680d304a73488c1a4caed903a2 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Sun, 12 Apr 2026 10:44:03 +0800 Subject: [PATCH] Delay background preload until map interaction stops --- frontend/components/dashboard/MapCanvas.tsx | 1 + frontend/hooks/useDashboardStore.tsx | 8 +++- frontend/hooks/useLeafletMap.ts | 44 +++++++++++++++++++++ 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/frontend/components/dashboard/MapCanvas.tsx b/frontend/components/dashboard/MapCanvas.tsx index 9fcf504c..569dadc2 100644 --- a/frontend/components/dashboard/MapCanvas.tsx +++ b/frontend/components/dashboard/MapCanvas.tsx @@ -13,6 +13,7 @@ export function MapCanvas() { citySummariesByName: store.citySummariesByName, onClosePanel: store.closePanel, onEnsureCityDetail: store.ensureCityDetail, + onMapInteractionChange: store.setMapInteractionActive, onRegisterStopMotion: store.registerMapStopMotion, onSelectCity: (cityName) => { void store.selectCity(cityName); diff --git a/frontend/hooks/useDashboardStore.tsx b/frontend/hooks/useDashboardStore.tsx index 6f3a6a87..f06ea6ea 100644 --- a/frontend/hooks/useDashboardStore.tsx +++ b/frontend/hooks/useDashboardStore.tsx @@ -47,6 +47,7 @@ interface DashboardStoreValue extends DashboardState { selectedMarketScan: MarketScan | null; selectedDetail: CityDetail | null; selectCity: (cityName: string) => Promise; + setMapInteractionActive: (active: boolean) => void; setForecastDate: (dateStr: string | null) => void; marketScanByCityName: Record; } @@ -254,6 +255,7 @@ export function DashboardStoreProvider({ string | null >(null); const [futureModalDate, setFutureModalDate] = useState(null); + const [isMapInteracting, setIsMapInteracting] = useState(false); const [loadingState, setLoadingState] = useState( getInitialLoadingState, ); @@ -747,8 +749,9 @@ export function DashboardStoreProvider({ void (async () => { await runQueue(priorityQueue, EAGER_SUMMARY_PRIORITY_CONCURRENCY); if (!active) return; + if (isMapInteracting) return; cancelIdleSchedule = scheduleWhenBrowserIdle(() => { - if (!active) return; + if (!active || isMapInteracting) return; void runQueue( backgroundQueue, EAGER_SUMMARY_BACKGROUND_CONCURRENCY, @@ -760,7 +763,7 @@ export function DashboardStoreProvider({ active = false; cancelIdleSchedule(); }; - }, [cities, loadingState.refresh]); + }, [cities, isMapInteracting, loadingState.refresh]); const selectCity = async (cityName: string) => { setSelectedCity(cityName); @@ -1066,6 +1069,7 @@ export function DashboardStoreProvider({ selectedDetail, selectedForecastDate, selectCity, + setMapInteractionActive: setIsMapInteracting, setForecastDate: (dateStr: string | null) => setSelectedForecastDate(dateStr), marketScanByCityName, diff --git a/frontend/hooks/useLeafletMap.ts b/frontend/hooks/useLeafletMap.ts index 4ca69a9b..c88075fe 100644 --- a/frontend/hooks/useLeafletMap.ts +++ b/frontend/hooks/useLeafletMap.ts @@ -19,6 +19,7 @@ interface UseLeafletMapArgs { cityName: string, force?: boolean, ) => Promise; + onMapInteractionChange: (active: boolean) => void; onRegisterStopMotion: (stopMotion: () => void) => void; onSelectCity: (cityName: string) => void; selectedCity: string | null; @@ -195,6 +196,7 @@ export function useLeafletMap({ citySummariesByName, onClosePanel, onEnsureCityDetail, + onMapInteractionChange, onRegisterStopMotion, onSelectCity, selectedCity, @@ -218,6 +220,10 @@ export function useLeafletMap({ const onRegisterStopMotionRef = useRef(onRegisterStopMotion); const onSelectCityRef = useRef(onSelectCity); const onEnsureCityDetailRef = useRef(onEnsureCityDetail); + const onMapInteractionChangeRef = useRef(onMapInteractionChange); + const interactionIdleTimerRef = useRef | null>( + null, + ); useEffect(() => { onClosePanelRef.current = onClosePanel; @@ -235,6 +241,10 @@ export function useLeafletMap({ onEnsureCityDetailRef.current = onEnsureCityDetail; }, [onEnsureCityDetail]); + useEffect(() => { + onMapInteractionChangeRef.current = onMapInteractionChange; + }, [onMapInteractionChange]); + useEffect(() => { suspendMotionRef.current = suspendMotion; }, [suspendMotion]); @@ -278,8 +288,42 @@ export function useLeafletMap({ }; map.on("click", handleMapClick); + const markInteracting = () => { + if (interactionIdleTimerRef.current) { + clearTimeout(interactionIdleTimerRef.current); + interactionIdleTimerRef.current = null; + } + onMapInteractionChangeRef.current(true); + }; + + const markIdleSoon = () => { + if (interactionIdleTimerRef.current) { + clearTimeout(interactionIdleTimerRef.current); + } + interactionIdleTimerRef.current = setTimeout(() => { + interactionIdleTimerRef.current = null; + onMapInteractionChangeRef.current(false); + }, 700); + }; + + map.on("movestart", markInteracting); + map.on("zoomstart", markInteracting); + map.on("dragstart", markInteracting); + map.on("moveend", markIdleSoon); + map.on("zoomend", markIdleSoon); + return () => { onRegisterStopMotionRef.current(() => {}); + if (interactionIdleTimerRef.current) { + clearTimeout(interactionIdleTimerRef.current); + interactionIdleTimerRef.current = null; + } + onMapInteractionChangeRef.current(false); + map.off("movestart", markInteracting); + map.off("zoomstart", markInteracting); + map.off("dragstart", markInteracting); + map.off("moveend", markIdleSoon); + map.off("zoomend", markIdleSoon); map.off("click", handleMapClick); map.remove(); mapRef.current = null;