From 9384d6acf52c3b61523fa6ec5443c4cf82fe69d9 Mon Sep 17 00:00:00 2001 From: lingniu Date: Thu, 16 Jul 2026 10:41:41 +0800 Subject: [PATCH] perf(monitor): isolate follow viewport events --- .../apps/web/src/v2/map/FleetMap.test.tsx | 20 ++++++++++++++++++- .../apps/web/src/v2/map/FleetMap.tsx | 6 ++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/vehicle-data-platform/apps/web/src/v2/map/FleetMap.test.tsx b/vehicle-data-platform/apps/web/src/v2/map/FleetMap.test.tsx index c2ea58c4..e2d3561e 100644 --- a/vehicle-data-platform/apps/web/src/v2/map/FleetMap.test.tsx +++ b/vehicle-data-platform/apps/web/src/v2/map/FleetMap.test.tsx @@ -14,7 +14,7 @@ const markerSetMap = vi.fn(); const markerSetPosition = vi.fn(); const labelSetPosition = vi.fn(); const setZoomAndCenter = vi.fn(); -const setCenter = vi.fn(); +const setCenter = vi.fn(() => mapHandlers.get('moveend')?.({})); const panTo = vi.fn(); const mapOff = vi.fn(); const mapDestroy = vi.fn(); @@ -521,6 +521,24 @@ test('renders one selected plate and smoothly follows it until the map is dragge expect((hiddenFloatingLabels[0].options.text as { content: string }).content).toBe('粤A12345'); }); +test('does not treat intermediate programmatic follow frames as user viewport changes', async () => { + useFastAnimationFrames(); + window.__LINGNIU_APP_CONFIG__ = { amapWebJsKey: 'amap-web-key' }; + window.AMapLoader = { load: vi.fn(async () => amapMock()) }; + const onViewportChange = vi.fn(); + const view = render( undefined} onViewportChange={onViewportChange} />); + await waitFor(() => expect(onViewportChange).toHaveBeenCalled()); + onViewportChange.mockReset(); + + view.rerender( undefined} onViewportChange={onViewportChange} />); + + await waitFor(() => expect(setCenter).toHaveBeenLastCalledWith(wgs84ToGcj02(113.27, 23.14)), { timeout: 1_800 }); + await waitFor(() => expect(onViewportChange).toHaveBeenCalledTimes(1)); +}); + test('renders every nearby plate with staggered positions at maximum zoom', async () => { getZoom.mockReturnValue(20); window.__LINGNIU_APP_CONFIG__ = { amapWebJsKey: 'amap-web-key' }; diff --git a/vehicle-data-platform/apps/web/src/v2/map/FleetMap.tsx b/vehicle-data-platform/apps/web/src/v2/map/FleetMap.tsx index f12e4e07..4ec621b4 100644 --- a/vehicle-data-platform/apps/web/src/v2/map/FleetMap.tsx +++ b/vehicle-data-platform/apps/web/src/v2/map/FleetMap.tsx @@ -302,6 +302,7 @@ export function FleetMap({ vehicles, selectedVin, onSelect, monitorMap, onSelect const selectionPositionRef = useRef(''); const centeredVinRef = useRef(''); const followSelectedRef = useRef(true); + const programmaticFollowRef = useRef(false); const selectedVinRef = useRef(selectedVin); const [state, setState] = useState<'loading' | 'ready' | 'fallback' | 'error'>('loading'); const [loadAttempt, setLoadAttempt] = useState(0); @@ -389,6 +390,7 @@ export function FleetMap({ vehicles, selectedVin, onSelect, monitorMap, onSelect const denseLabels = AMap.LabelsLayer ? new AMap.LabelsLayer({ zooms: [19, 20], zIndex: 110, collision: false, allowCollision: true }) : null; labels?.setMap(map); notifyViewport = () => { + if (programmaticFollowRef.current) return; setMapZoom(map.getZoom?.() ?? 5); window.clearTimeout(viewportTimerRef.current); viewportTimerRef.current = window.setTimeout(() => { @@ -400,6 +402,7 @@ export function FleetMap({ vehicles, selectedVin, onSelect, monitorMap, onSelect map.on?.('zoomend', notifyViewport); stopFollowing = () => { if (!centeredVinRef.current) return; + programmaticFollowRef.current = false; followSelectedRef.current = false; setFollowSelected(false); }; @@ -431,6 +434,7 @@ export function FleetMap({ vehicles, selectedVin, onSelect, monitorMap, onSelect pointAnimationFrameRef.current = undefined; pointAnimationTimerRef.current = undefined; movingPointIDsRef.current.clear(); + programmaticFollowRef.current = false; if (selectMassPoint) massInstance?.off?.('click', selectMassPoint); if (notifyViewport) { mapInstance?.off?.('moveend', notifyViewport); @@ -493,6 +497,7 @@ export function FleetMap({ vehicles, selectedVin, onSelect, monitorMap, onSelect window.clearTimeout(pointAnimationTimerRef.current); pointAnimationFrameRef.current = undefined; pointAnimationTimerRef.current = undefined; + programmaticFollowRef.current = false; const previousData = renderedMassDataRef.current; const previousDataByID = new Map(previousData.map((point) => [point.id, point])); const vehiclePointIDs = new Set(monitorMode ? mapPoints.map((point) => point.vin) : fallbackPoints.map((point) => point.vin)); @@ -531,6 +536,7 @@ export function FleetMap({ vehicles, selectedVin, onSelect, monitorMap, onSelect selectionRef.current?.setPosition?.(motion.point.lnglat); const motionComplete = elapsed >= motion.durationMs; if (followSelectedRef.current && !selectedFollowComplete && (elapsed - lastFollowAt >= MAP_FOLLOW_FRAME_MS || motionComplete)) { + programmaticFollowRef.current = !motionComplete; mapRef.current?.setCenter?.(motion.point.lnglat); lastFollowAt = elapsed; selectedFollowComplete = motionComplete;