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

@@ -1,7 +1,7 @@
import { IconChevronLeft, IconChevronRight, IconClose, IconFilter, IconList, IconMapPin, IconQrCode, IconRefresh, IconSearch } from '@douyinfe/semi-icons';
import { useQuery } from '@tanstack/react-query';
import { memo, useCallback, useDeferredValue, useEffect, useMemo, useState } from 'react';
import { Link } from 'react-router-dom';
import { memo, useCallback, useDeferredValue, useEffect, useMemo, useRef, useState } from 'react';
import { Link, useSearchParams } from 'react-router-dom';
import { api } from '../../api/client';
import type { Page, VehicleRealtimeRow } from '../../api/types';
import { FleetMap } from '../map/FleetMap';
@@ -10,6 +10,7 @@ import { VehicleSourceEvidencePanel } from '../shared/VehicleSourceEvidencePanel
import { formatNumber, relativeFreshness, statusLabel, vehicleStatus } from '../domain/monitor';
import { MAX_MONITOR_SEARCH_TERMS, MONITOR_REFRESH, monitorFilterScope, monitorQueryParams, parseMonitorSearchTerms, useMonitorData, useMonitorVehicleCard, type MonitorViewport } from '../hooks/useMonitorData';
import { LIVE_QUERY_POLICY, QUERY_MEMORY, retainPreviousPageWithinScope } from '../queryPolicy';
import { buildMonitorPath, parseMonitorRouteContext, withMonitorReturn } from '../routing/monitorContext';
const protocols = ['', 'GB32960', 'JT808', 'YUTONG_MQTT'];
const statuses = ['', 'online', 'offline', 'driving', 'idle', 'no_location'];
@@ -178,10 +179,12 @@ const MemoFleetMap = memo(FleetMap);
function VehicleDetailCard({
vehicle,
monitorReturn,
onCollapse,
onClear
}: {
vehicle: VehicleRealtimeRow;
monitorReturn: string;
onCollapse: () => void;
onClear: () => void;
}) {
@@ -204,9 +207,10 @@ function VehicleDetailCard({
<small>{vehicle.vin}</small>
</div>
<div className="v2-detail-actions">
<Link to={`/vehicles/${encodeURIComponent(vehicle.vin)}`}></Link>
<Link to={`/tracks?vin=${encodeURIComponent(vehicle.vin)}`}></Link>
<Link to={`/history?vin=${encodeURIComponent(vehicle.vin)}`}></Link>
<Link to={withMonitorReturn(`/vehicles/${encodeURIComponent(vehicle.vin)}`, monitorReturn)}></Link>
<Link to={withMonitorReturn(`/tracks?vin=${encodeURIComponent(vehicle.vin)}`, monitorReturn)}></Link>
<Link to={withMonitorReturn(`/history?vin=${encodeURIComponent(vehicle.vin)}`, monitorReturn)}></Link>
<Link to={withMonitorReturn(`/statistics?vins=${encodeURIComponent(vehicle.vin)}`, monitorReturn)}></Link>
</div>
<section>
<h3></h3>
@@ -246,23 +250,33 @@ function VehicleDetailCard({
}
export default function MonitorPage() {
const [mode, setMode] = useState<'map' | 'list'>('map');
const [routeParams, setRouteParams] = useSearchParams();
const initialContextRef = useRef(parseMonitorRouteContext(routeParams));
const initialContext = initialContextRef.current;
const [mode, setMode] = useState<'map' | 'list'>(initialContext.mode);
const [mobileEntryOpen, setMobileEntryOpen] = useState(false);
const [listOffset, setListOffset] = useState(0);
const [listLimit, setListLimit] = useState(50);
const [keyword, setKeyword] = useState('');
const [listOffset, setListOffset] = useState(initialContext.listOffset);
const [listLimit, setListLimit] = useState(initialContext.listLimit);
const [keyword, setKeyword] = useState(initialContext.keyword);
const [batchSearchOpen, setBatchSearchOpen] = useState(false);
const deferredKeyword = useDeferredValue(keyword);
const searchTerms = useMemo(() => parseMonitorSearchTerms(keyword), [keyword]);
const [protocol, setProtocol] = useState('');
const [status, setStatus] = useState('');
const [selectedVin, setSelectedVin] = useState('');
const [detailOpen, setDetailOpen] = useState(false);
const [viewport, setViewport] = useState<MonitorViewport>({ zoom: 5, bounds: '' });
const [protocol, setProtocol] = useState(initialContext.protocol);
const [status, setStatus] = useState(initialContext.status);
const [selectedVin, setSelectedVin] = useState(initialContext.selectedVin);
const [detailOpen, setDetailOpen] = useState(initialContext.detailOpen);
const [viewport, setViewport] = useState<MonitorViewport>(initialContext.viewport);
const updateViewport = useCallback((next: MonitorViewport) => {
setViewport((current) => current.zoom === next.zoom && current.bounds === next.bounds ? current : next);
}, []);
const filters = useMemo(() => ({ keyword: deferredKeyword, protocol, status }), [deferredKeyword, protocol, status]);
const monitorReturn = useMemo(() => buildMonitorPath({
mode, keyword, protocol, status, selectedVin, detailOpen, viewport, listOffset, listLimit
}), [detailOpen, keyword, listLimit, listOffset, mode, protocol, selectedVin, status, viewport]);
useEffect(() => {
const next = new URL(monitorReturn, 'https://vehicle-platform.invalid').searchParams;
if (next.toString() !== routeParams.toString()) setRouteParams(next, { replace: true });
}, [monitorReturn, routeParams, setRouteParams]);
const listScope = useMemo(() => monitorFilterScope(filters), [filters]);
const listParams = useMemo(() => {
const params = monitorQueryParams(filters, listLimit);
@@ -394,10 +408,12 @@ export default function MonitorPage() {
onSelect={selectMapVehicle}
onSelectVin={selectVehicle}
onViewportChange={updateViewport}
initialViewport={initialContext.hasViewport ? initialContext.viewport : undefined}
/>
{selected && detailOpen ? (
<VehicleDetailCard
vehicle={selected}
monitorReturn={monitorReturn}
onCollapse={collapseDetail}
onClear={clearSelection}
/>