fix(monitor): isolate batch search scope
This commit is contained in:
@@ -43,6 +43,13 @@ vi.mock('../hooks/useMonitorData', () => ({
|
||||
if (filters.status) params.set('status', filters.status);
|
||||
return params;
|
||||
},
|
||||
monitorFilterScope: (filters: { keyword: string; protocol: string; status: string }) => {
|
||||
const params = new URLSearchParams();
|
||||
if (filters.keyword) params.set('keyword', filters.keyword);
|
||||
if (filters.protocol) params.set('protocol', filters.protocol);
|
||||
if (filters.status) params.set('status', filters.status);
|
||||
return params.toString();
|
||||
},
|
||||
useMonitorData: () => ({
|
||||
summary: { data: { totalVehicles: 2, onlineVehicles: 2, offlineVehicles: 0, drivingVehicles: 1, idleVehicles: 1, frameToday: 10 } },
|
||||
vehicles: { data: { items: vehicles, total: 2 }, isError: false, ...monitorQueryFlags },
|
||||
@@ -204,7 +211,8 @@ test('opens an explicit batch editor and applies copied plate rows', async () =>
|
||||
});
|
||||
});
|
||||
|
||||
test('hides stale fleet rows while a pasted batch is still loading', () => {
|
||||
test('hides stale fleet rows while a pasted batch scope is still loading', () => {
|
||||
monitorQueryFlags.isLoading = true;
|
||||
monitorQueryFlags.isPlaceholderData = true;
|
||||
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
render(<QueryClientProvider client={queryClient}><MemoryRouter future={ROUTER_FUTURE}><MonitorPage /></MemoryRouter></QueryClientProvider>);
|
||||
@@ -215,6 +223,16 @@ test('hides stale fleet rows while a pasted batch is still loading', () => {
|
||||
expect(screen.queryByRole('button', { name: /粤A12345 LTEST000000000001/ })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('keeps same-filter fleet rows visible while a viewport placeholder refreshes', () => {
|
||||
monitorQueryFlags.isPlaceholderData = true;
|
||||
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
render(<QueryClientProvider client={queryClient}><MemoryRouter future={ROUTER_FUTURE}><MonitorPage /></MemoryRouter></QueryClientProvider>);
|
||||
|
||||
expect(screen.getByRole('button', { name: /粤A12345 LTEST000000000001/ })).toBeInTheDocument();
|
||||
expect(screen.queryByText('加载车辆')).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('fleet-map')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('reports pasted plates that have no exact realtime match', () => {
|
||||
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
render(<QueryClientProvider client={queryClient}><MemoryRouter future={ROUTER_FUTURE}><MonitorPage /></MemoryRouter></QueryClientProvider>);
|
||||
|
||||
@@ -4,12 +4,12 @@ import QRCode from 'qrcode';
|
||||
import { memo, useCallback, useDeferredValue, useEffect, useMemo, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { api } from '../../api/client';
|
||||
import type { VehicleRealtimeRow } from '../../api/types';
|
||||
import type { Page, VehicleRealtimeRow } from '../../api/types';
|
||||
import { FleetMap } from '../map/FleetMap';
|
||||
import { EmptyState, InlineError } from '../shared/AsyncState';
|
||||
import { formatNumber, relativeFreshness, statusLabel, vehicleStatus } from '../domain/monitor';
|
||||
import { MAX_MONITOR_SEARCH_TERMS, MONITOR_REFRESH, monitorQueryParams, parseMonitorSearchTerms, useMonitorData, useMonitorVehicleCard, type MonitorViewport } from '../hooks/useMonitorData';
|
||||
import { QUERY_MEMORY } from '../queryPolicy';
|
||||
import { MAX_MONITOR_SEARCH_TERMS, MONITOR_REFRESH, monitorFilterScope, monitorQueryParams, parseMonitorSearchTerms, useMonitorData, useMonitorVehicleCard, type MonitorViewport } from '../hooks/useMonitorData';
|
||||
import { QUERY_MEMORY, retainPreviousPageWithinScope } from '../queryPolicy';
|
||||
|
||||
const protocols = ['', 'GB32960', 'JT808', 'YUTONG_MQTT'];
|
||||
const statuses = ['', 'online', 'offline', 'driving', 'idle'];
|
||||
@@ -213,16 +213,17 @@ export default function MonitorPage() {
|
||||
setViewport((current) => current.zoom === next.zoom && current.bounds === next.bounds ? current : next);
|
||||
}, []);
|
||||
const filters = useMemo(() => ({ keyword: deferredKeyword, protocol, status }), [deferredKeyword, protocol, status]);
|
||||
const listScope = useMemo(() => monitorFilterScope(filters), [filters]);
|
||||
const listParams = useMemo(() => {
|
||||
const params = monitorQueryParams(filters, listLimit);
|
||||
params.set('offset', String(listOffset));
|
||||
return params;
|
||||
}, [filters, listLimit, listOffset]);
|
||||
const { summary, vehicles, map, selectedVehicle } = useMonitorData(filters, viewport, selectedVin, mode === 'map', mode === 'map');
|
||||
const realtimeListQuery = useQuery({
|
||||
queryKey: ['monitor', 'vehicle-list', listParams.toString()],
|
||||
const realtimeListQuery = useQuery<Page<VehicleRealtimeRow>>({
|
||||
queryKey: ['monitor', 'vehicle-list', listScope, listLimit, listOffset],
|
||||
queryFn: ({ signal }) => api.vehicleRealtime(listParams, signal),
|
||||
enabled: mode === 'list', placeholderData: (previous) => previous, refetchInterval: mode === 'list' ? MONITOR_REFRESH.fleet : false,
|
||||
enabled: mode === 'list', placeholderData: retainPreviousPageWithinScope<Page<VehicleRealtimeRow>>(listScope, 2), refetchInterval: mode === 'list' ? MONITOR_REFRESH.fleet : false,
|
||||
gcTime: QUERY_MEMORY.highVolumeGcTime
|
||||
});
|
||||
const rows = useMemo(() => {
|
||||
@@ -233,9 +234,8 @@ export default function MonitorPage() {
|
||||
const listRows = realtimeListQuery.data?.items ?? EMPTY_VEHICLES;
|
||||
const activeBatchRows = mode === 'map' ? rows : listRows;
|
||||
const activeBatchQuery = mode === 'map' ? vehicles : realtimeListQuery;
|
||||
const batchSearchPending = searchTerms.length > 1 && (
|
||||
keyword !== deferredKeyword || activeBatchQuery.isLoading || activeBatchQuery.isPlaceholderData
|
||||
);
|
||||
const filterTransitionPending = keyword !== deferredKeyword || activeBatchQuery.isLoading;
|
||||
const batchSearchPending = searchTerms.length > 1 && filterTransitionPending;
|
||||
const batchMatch = useMemo(() => {
|
||||
const identities = new Set<string>();
|
||||
for (const vehicle of activeBatchRows) {
|
||||
@@ -245,7 +245,7 @@ export default function MonitorPage() {
|
||||
const missing = searchTerms.filter((term) => !identities.has(term));
|
||||
return { matched: searchTerms.length - missing.length, missing };
|
||||
}, [activeBatchRows, searchTerms]);
|
||||
const visibleRows = batchSearchPending ? [] : rows;
|
||||
const visibleRows = filterTransitionPending ? [] : rows;
|
||||
const batchStatus = batchSearchPending
|
||||
? `正在查找 ${searchTerms.length} 辆`
|
||||
: batchMatch.missing.length
|
||||
@@ -254,6 +254,8 @@ export default function MonitorPage() {
|
||||
const batchStatusTitle = batchMatch.missing.length
|
||||
? `未找到:${batchMatch.missing.join('、')}`
|
||||
: searchTerms.join('、');
|
||||
const visibleListRows = filterTransitionPending ? EMPTY_VEHICLES : listRows;
|
||||
const visibleListTotal = filterTransitionPending ? 0 : (realtimeListQuery.data?.total ?? 0);
|
||||
const selected = selectedVin
|
||||
? rows.find((vehicle) => vehicle.vin === selectedVin) ?? selectedVehicle.data?.items[0]
|
||||
: undefined;
|
||||
@@ -334,7 +336,7 @@ export default function MonitorPage() {
|
||||
</div>
|
||||
<MemoFleetMap
|
||||
vehicles={visibleRows}
|
||||
monitorMap={searchTerms.length > 1 && (batchSearchPending || map.isPlaceholderData) ? undefined : map.data}
|
||||
monitorMap={filterTransitionPending ? undefined : map.data}
|
||||
selectedVin={selectedVin || undefined}
|
||||
onSelect={selectMapVehicle}
|
||||
onSelectVin={selectVehicle}
|
||||
@@ -356,7 +358,7 @@ export default function MonitorPage() {
|
||||
</button>
|
||||
</aside>
|
||||
) : null}
|
||||
</section> : <MonitorVehicleTable rows={realtimeListQuery.data?.items ?? []} total={realtimeListQuery.data?.total ?? 0} page={Math.floor(listOffset / listLimit) + 1} totalPages={Math.max(1, Math.ceil((realtimeListQuery.data?.total ?? 0) / listLimit))} limit={listLimit} loading={realtimeListQuery.isFetching} onSelect={selectVehicle} onPage={(page) => setListOffset((page - 1) * listLimit)} onLimit={(next) => { setListLimit(next); setListOffset(0); }} />}
|
||||
</section> : <MonitorVehicleTable rows={visibleListRows} total={visibleListTotal} page={Math.floor(listOffset / listLimit) + 1} totalPages={Math.max(1, Math.ceil(visibleListTotal / listLimit))} limit={listLimit} loading={filterTransitionPending || realtimeListQuery.isFetching} onSelect={selectVehicle} onPage={(page) => setListOffset((page - 1) * listLimit)} onLimit={(next) => { setListLimit(next); setListOffset(0); }} />}
|
||||
|
||||
<section className="v2-event-strip">
|
||||
<strong>实时数据状态</strong>
|
||||
|
||||
Reference in New Issue
Block a user