diff --git a/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.test.tsx b/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.test.tsx
new file mode 100644
index 00000000..7ebae573
--- /dev/null
+++ b/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.test.tsx
@@ -0,0 +1,69 @@
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { cleanup, render, waitFor } from '@testing-library/react';
+import { afterEach, expect, test, vi } from 'vitest';
+import { MemoryRouter, Route, Routes } from 'react-router-dom';
+import { api } from '../../api/client';
+import type { VehicleDetail, VehicleRealtimeRow } from '../../api/types';
+import { ROUTER_FUTURE } from '../routing/routerConfig';
+import VehiclePage from './VehiclePage';
+
+const fleetMapVehicles = vi.hoisted(() => vi.fn());
+
+vi.mock('../map/FleetMap', () => ({
+ FleetMap: ({ vehicles }: { vehicles: VehicleRealtimeRow[] }) => {
+ fleetMapVehicles(vehicles);
+ return
;
+ }
+}));
+
+vi.mock('../auth/AuthGate', () => ({
+ usePlatformSession: () => ({ session: { name: 'test-viewer', role: 'viewer', authMode: 'enforce' } })
+}));
+
+const initialRealtime = {
+ vin: 'LTEST000000000001', plate: '粤A12345', phone: '', oem: '', protocols: ['JT808'], sourceStatus: [],
+ sourceCount: 1, onlineSourceCount: 1, online: true, bindingStatus: 'bound', primaryProtocol: 'JT808',
+ longitude: 113.26, latitude: 23.13, speedKmh: 20, socPercent: 80, totalMileageKm: 1000,
+ lastSeen: '2026-07-16 10:00:00', reportIntervalMs: 30_000
+} satisfies VehicleRealtimeRow;
+
+const detail = {
+ vin: initialRealtime.vin,
+ lookupKey: initialRealtime.vin,
+ lookupResolved: true,
+ realtimeSummary: initialRealtime,
+ sources: ['JT808'],
+ sourceStatus: [],
+ realtime: [],
+ history: { items: [], total: 0, limit: 20, offset: 0 },
+ raw: { items: [], total: 0, limit: 20, offset: 0 },
+ mileage: { items: [], total: 0, limit: 20, offset: 0 },
+ quality: { items: [], total: 0, limit: 20, offset: 0 }
+} satisfies VehicleDetail;
+
+afterEach(() => {
+ cleanup();
+ vi.restoreAllMocks();
+ fleetMapVehicles.mockReset();
+});
+
+test('polls lightweight single-vehicle realtime data and passes its report interval to the map', async () => {
+ const movedRealtime = { ...initialRealtime, longitude: 113.28, latitude: 23.15, lastSeen: '2026-07-16 10:00:30' };
+ vi.spyOn(api, 'vehicleDetail').mockResolvedValue(detail);
+ const realtimeSpy = vi.spyOn(api, 'vehicleRealtime').mockResolvedValue({ items: [movedRealtime], total: 1, limit: 1, offset: 0 });
+ vi.spyOn(api, 'latestTelemetry').mockResolvedValue({ values: [], categories: [], scannedFrames: 0 } as never);
+ const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
+
+ render(} />);
+
+ await waitFor(() => expect(realtimeSpy).toHaveBeenCalledTimes(1));
+ const realtimeQuery = client.getQueryCache().find({ queryKey: ['vehicle-detail-realtime', initialRealtime.vin] });
+ expect((realtimeQuery?.options as { refetchInterval?: number }).refetchInterval).toBe(10_000);
+ const params = realtimeSpy.mock.calls[0][0];
+ expect(params).toBeDefined();
+ expect(params!.get('keywords')).toBe(initialRealtime.vin);
+ expect(params!.get('limit')).toBe('1');
+ await waitFor(() => expect(fleetMapVehicles).toHaveBeenLastCalledWith([
+ expect.objectContaining({ longitude: 113.28, latitude: 23.15, reportIntervalMs: 30_000 })
+ ]));
+});
diff --git a/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.tsx
index 14de0b0b..67485845 100644
--- a/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.tsx
+++ b/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.tsx
@@ -6,7 +6,7 @@ import {
import { FormEvent, useMemo, useState } from 'react';
import { Link, useNavigate, useParams } from 'react-router-dom';
import { api } from '../../api/client';
-import type { LatestTelemetryResponse, QualityIssueRow, VehicleDetail, VehicleProfileSyncItem, VehicleProfileSyncResult } from '../../api/types';
+import type { LatestTelemetryResponse, QualityIssueRow, VehicleDetail, VehicleProfileSyncItem, VehicleProfileSyncResult, VehicleRealtimeRow } from '../../api/types';
import { usePlatformSession } from '../auth/AuthGate';
import { canAdminister } from '../auth/session';
import { LIVE_QUERY_POLICY, QUERY_MEMORY } from '../queryPolicy';
@@ -23,6 +23,7 @@ function issueTone(issue: QualityIssueRow) { return issue.severity === 'error' ?
function durationHours(seconds?: number | null) { return seconds == null ? '—' : `${formatZhNumber(seconds / 3600, 1)} 小时`; }
function localDateTime(value?: string) { return value ? value.slice(0, 16) : ''; }
const operationStatusLabels = { unknown: '待维护', active: '运营中', inactive: '停运', maintenance: '维保中', retired: '已退役' } as const;
+const SINGLE_VEHICLE_REFRESH_MS = 10_000;
function ProfileSyncPanel({ onClose }: { onClose: () => void }) {
const [sourceSystem, setSourceSystem] = useState('');
@@ -170,9 +171,9 @@ function TelemetryPanel({ data, pending, error }: { data?: LatestTelemetryRespon
;
}
-function VehicleRecord({ detail, telemetry, telemetryPending, telemetryError, onUpdated }: { detail: VehicleDetail; telemetry?: LatestTelemetryResponse; telemetryPending: boolean; telemetryError?: string; onUpdated: () => void }) {
+function VehicleRecord({ detail, liveRealtime, telemetry, telemetryPending, telemetryError, onUpdated }: { detail: VehicleDetail; liveRealtime?: VehicleRealtimeRow; telemetry?: LatestTelemetryResponse; telemetryPending: boolean; telemetryError?: string; onUpdated: () => void }) {
const { session } = usePlatformSession();
- const realtime = detail.realtimeSummary;
+ const realtime = liveRealtime ?? detail.realtimeSummary;
const identity = detail.identity;
const mapVehicles = realtime ? [realtime] : [];
const lastMileage = detail.mileage.items[0];
@@ -198,10 +199,20 @@ function VehicleRecord({ detail, telemetry, telemetryPending, telemetryError, on
export default function VehiclePage() {
const { vin } = useParams();
const query = useQuery({ queryKey: ['vehicle-detail', vin], enabled: Boolean(vin), queryFn: ({ signal }) => api.vehicleDetail(new URLSearchParams({ keyword: vin!, limit: '20' }), signal), gcTime: QUERY_MEMORY.summaryGcTime });
+ const resolvedVin = query.data?.lookupResolved ? query.data.vin : '';
+ const realtime = useQuery({
+ queryKey: ['vehicle-detail-realtime', resolvedVin],
+ queryFn: ({ signal }) => api.vehicleRealtime(new URLSearchParams({ keywords: resolvedVin, limit: '1', offset: '0' }), signal),
+ enabled: Boolean(resolvedVin),
+ staleTime: 5_000,
+ gcTime: QUERY_MEMORY.highVolumeGcTime,
+ refetchInterval: resolvedVin ? SINGLE_VEHICLE_REFRESH_MS : false,
+ ...LIVE_QUERY_POLICY
+ });
const telemetry = useQuery({ queryKey: ['vehicle-latest-telemetry', vin], enabled: Boolean(vin), queryFn: ({ signal }) => api.latestTelemetry(vin!, signal), staleTime: 10_000, gcTime: QUERY_MEMORY.summaryGcTime, refetchInterval: 20_000, ...LIVE_QUERY_POLICY });
if (!vin) return ;
if (query.isPending) return ;
if (query.isError) return query.refetch()} />
;
if (!query.data.lookupResolved) return 未找到车辆
没有匹配“{vin}”的车牌、VIN 或终端记录。
重新查询;
- return { void query.refetch(); void telemetry.refetch(); }} />;
+ return { void query.refetch(); void realtime.refetch(); void telemetry.refetch(); }} />;
}