feat(monitor): preserve investigation context

This commit is contained in:
lingniu
2026-07-16 17:08:54 +08:00
parent 38b056f5f1
commit 1cfc9c2bdd
17 changed files with 426 additions and 37 deletions

View File

@@ -296,6 +296,29 @@ test('recovers in place after a transient AMap failure and renders data that arr
expect(screen.queryByText(/地图加载失败/)).not.toBeInTheDocument();
});
test('restores the saved monitor viewport without forcing the selected vehicle zoom', async () => {
window.__LINGNIU_APP_CONFIG__ = { amapWebJsKey: 'amap-web-key' };
window.AMapLoader = { load: vi.fn(async () => amapMock()) };
const bounds = '113.100000,23.000000,113.400000,23.300000';
render(<FleetMap
vehicles={[]}
monitorMap={pointMap}
selectedVin="LTEST000000000001"
initialViewport={{ zoom: 13, bounds }}
onSelect={() => undefined}
/>);
await waitFor(() => expect(mapOptions).toHaveLength(1));
expect(mapOptions[0]).toEqual(expect.objectContaining({
zoom: 13,
center: wgs84ToGcj02(113.25, 23.15)
}));
await waitFor(() => expect(markerSetMap).toHaveBeenCalled());
expect(setZoomAndCenter).not.toHaveBeenCalled();
expect(panTo).not.toHaveBeenCalled();
});
test('detaches AMap listeners and destroys the map on unmount', async () => {
window.__LINGNIU_APP_CONFIG__ = { amapWebJsKey: 'amap-web-key' };
window.AMapLoader = { load: vi.fn(async () => amapMock()) };

View File

@@ -81,6 +81,14 @@ function viewportFromMap(map: AMapMap): MonitorViewport | null {
};
}
function viewportCenter(viewport?: MonitorViewport) {
const values = viewport?.bounds.split(',').map(Number) ?? [];
if (values.length !== 4 || values.some((value) => !Number.isFinite(value))) return undefined;
const [west, south, east, north] = values;
if (west >= east || south >= north) return undefined;
return wgs84ToGcj02((west + east) / 2, (south + north) / 2);
}
function styleIndex(vehicle: VehicleRealtimeRow) {
const status = vehicleStatus(vehicle);
return statusStyleIndex(status);
@@ -270,13 +278,14 @@ function densePlatePlacements(points: PlateLabelPoint[]) {
return placements;
}
export function FleetMap({ vehicles, selectedVin, onSelect, monitorMap, onSelectVin, onViewportChange }: {
export function FleetMap({ vehicles, selectedVin, onSelect, monitorMap, onSelectVin, onViewportChange, initialViewport }: {
vehicles: VehicleRealtimeRow[];
selectedVin?: string;
onSelect: (vehicle: VehicleRealtimeRow) => void;
monitorMap?: MonitorMapResponse;
onSelectVin?: (vin: string) => void;
onViewportChange?: (viewport: MonitorViewport) => void;
initialViewport?: MonitorViewport;
}) {
const containerRef = useRef<HTMLDivElement>(null);
const mapRef = useRef<AMapMap | null>(null);
@@ -302,7 +311,8 @@ export function FleetMap({ vehicles, selectedVin, onSelect, monitorMap, onSelect
const viewportTimerRef = useRef<number | undefined>(undefined);
const selectionKeyRef = useRef('');
const selectionPositionRef = useRef('');
const centeredVinRef = useRef('');
const initialViewportRef = useRef(initialViewport);
const centeredVinRef = useRef(initialViewport?.bounds ? selectedVin ?? '' : '');
const followSelectedRef = useRef(true);
const programmaticFollowRef = useRef(false);
const selectedVinRef = useRef(selectedVin);
@@ -310,7 +320,7 @@ export function FleetMap({ vehicles, selectedVin, onSelect, monitorMap, onSelect
const [loadAttempt, setLoadAttempt] = useState(0);
const [showLabels, setShowLabels] = useState(true);
const [followSelected, setFollowSelected] = useState(true);
const [mapZoom, setMapZoom] = useState(5);
const [mapZoom, setMapZoom] = useState(initialViewport?.zoom ?? 5);
const points = useMemo(() => vehicles.filter((vehicle) => isValidAMapCoordinate(vehicle.longitude, vehicle.latitude)), [vehicles]);
const monitorMode = monitorMap?.mode;
const monitorPoints = monitorMap?.points;
@@ -354,11 +364,12 @@ export function FleetMap({ vehicles, selectedVin, onSelect, monitorMap, onSelect
loadAMap(['AMap.Scale', 'AMap.ToolBar']).then((AMap) => {
if (cancelled || !containerRef.current) return;
const initialSelection = initialSelectionRef.current;
const initialCenter = initialSelection
const restoredCenter = viewportCenter(initialViewportRef.current);
const initialCenter = restoredCenter ?? (initialSelection
? wgs84ToGcj02(initialSelection.longitude, initialSelection.latitude)
: wgs84ToGcj02(105.4, 35.9);
: wgs84ToGcj02(105.4, 35.9));
const map = new AMap.Map(containerRef.current, {
zoom: initialSelection ? 13 : 5,
zoom: initialViewportRef.current?.zoom ?? (initialSelection ? 13 : 5),
center: initialCenter,
viewMode: '2D',
mapStyle: 'amap://styles/whitesmoke',
@@ -648,7 +659,7 @@ export function FleetMap({ vehicles, selectedVin, onSelect, monitorMap, onSelect
}, [fallbackPoints, mapZoom, monitorPoints, selectedTarget, selectedVin, showLabels, state]);
useEffect(() => {
if (!selectedVin || !mapRef.current || !amapRef.current) {
if (!selectedVin) {
selectionRef.current?.setMap?.(null);
selectionRef.current = null;
selectionKeyRef.current = '';
@@ -658,6 +669,7 @@ export function FleetMap({ vehicles, selectedVin, onSelect, monitorMap, onSelect
setFollowSelected(false);
return;
}
if (!mapRef.current || !amapRef.current) return;
const target = selectedTarget;
if (!target) return;
const label = escapeHtml(target.plate || target.vin);