feat(monitor): preserve investigation context
This commit is contained in:
@@ -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()) };
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user