From 2991a01738f008803bbb9f470699961d0a257c56 Mon Sep 17 00:00:00 2001 From: lingniu Date: Sat, 4 Jul 2026 12:14:08 +0800 Subject: [PATCH] feat(platform): link mileage rows to raw evidence --- vehicle-data-platform/apps/web/src/App.tsx | 18 +++- .../apps/web/src/pages/Mileage.tsx | 17 +++- .../apps/web/src/test/App.test.tsx | 85 +++++++++++++++++++ 3 files changed, 117 insertions(+), 3 deletions(-) diff --git a/vehicle-data-platform/apps/web/src/App.tsx b/vehicle-data-platform/apps/web/src/App.tsx index 43d31192..73051c04 100644 --- a/vehicle-data-platform/apps/web/src/App.tsx +++ b/vehicle-data-platform/apps/web/src/App.tsx @@ -319,6 +319,22 @@ export default function App() { replaceHash('history', nextVin, nextProtocol, { tab: 'raw' }); }; + const openRawWithFilters = (filters: Record = {}) => { + const nextFilters = normalizeHistoryFilterValues(filters); + const nextVin = nextFilters.keyword?.trim() || analysisVin; + const nextProtocol = nextFilters.protocol?.trim() ?? activeProtocol; + if (!nextVin) { + return; + } + setAnalysisVin(nextVin); + setActiveProtocol(nextProtocol); + setHistoryTab('raw'); + setHistoryFilters({ ...nextFilters, keyword: nextVin, protocol: nextProtocol }); + setActivePage('history'); + const { keyword, protocol, ...restFilters } = nextFilters; + replaceHash('history', keyword ?? nextVin, protocol ?? nextProtocol, { ...restFilters, tab: 'raw' }); + }; + const openRealtimeForVehicle = (vin: string, protocol?: string) => { const nextVin = vin.trim(); const nextProtocol = protocol?.trim() ?? activeProtocol; @@ -378,7 +394,7 @@ export default function App() { realtime: , detail: , history: , - mileage: , + mileage: , quality: setLinkIssueCount(health.linkHealth.filter((item) => item.status !== 'ok').length)} onFiltersChange={updateQualityFilters} initialFilters={qualityFilters} /> }; diff --git a/vehicle-data-platform/apps/web/src/pages/Mileage.tsx b/vehicle-data-platform/apps/web/src/pages/Mileage.tsx index d0d72f8b..6b9d5dd1 100644 --- a/vehicle-data-platform/apps/web/src/pages/Mileage.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Mileage.tsx @@ -99,7 +99,8 @@ export function Mileage({ initialFilters = {}, onFiltersChange, onOpenVehicle, - onOpenHistory + onOpenHistory, + onOpenRaw }: { initialVin: string; initialProtocol?: string; @@ -107,6 +108,7 @@ export function Mileage({ onFiltersChange?: (filters: Record) => void; onOpenVehicle: (vin: string, protocol?: string) => void; onOpenHistory?: (filters: Record) => void; + onOpenRaw?: (filters: Record) => void; }) { const [rows, setRows] = useState([]); const [summary, setSummary] = useState(emptySummary); @@ -170,6 +172,16 @@ export function Mileage({ ...(dateTo ? { dateTo } : {}) }); }; + const openMileageRawEvidence = (row: DailyMileageRow) => { + const dateFrom = row.date?.trim() ?? ''; + const dateTo = nextDate(dateFrom); + onOpenRaw?.({ + keyword: row.vin, + protocol: row.source, + ...(dateFrom ? { dateFrom } : {}), + ...(dateTo ? { dateTo } : {}) + }); + }; useEffect(() => { const nextFilters = mergeInitialFilters(initialVin, initialProtocol, initialFilters); @@ -338,10 +350,11 @@ export function Mileage({ }, { title: '操作', - width: 190, + width: 270, render: (_: unknown, row: DailyMileageRow) => ( + ) diff --git a/vehicle-data-platform/apps/web/src/test/App.test.tsx b/vehicle-data-platform/apps/web/src/test/App.test.tsx index feb4c454..8c01d9d2 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -4185,6 +4185,91 @@ test('opens same-day trajectory playback from mileage row for evidence review', }); }); +test('opens same-day raw frame evidence from mileage row', async () => { + window.history.replaceState(null, '', '/#/mileage?keyword=VIN-MILEAGE-RAW&protocol=JT808'); + vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { + const path = String(input); + if (path.includes('/api/ops/health')) { + return { + ok: true, + json: async () => ({ + data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + if (path.includes('/api/mileage/summary')) { + return { + ok: true, + json: async () => ({ + data: { vehicleCount: 1, recordCount: 1, sourceCount: 1, totalMileageKm: 18.8, averageMileagePerVin: 18.8 }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + if (path.includes('/api/mileage/daily')) { + return { + ok: true, + json: async () => ({ + data: { + items: [{ + vin: 'VIN-MILEAGE-RAW', + plate: '粤ARAW核', + source: 'JT808', + date: '2026-07-03', + startMileageKm: 200, + endMileageKm: 218.8, + dailyMileageKm: 18.8, + anomalySeverity: 'warning' + }], + total: 1, + limit: 20, + offset: 0 + }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + if (path.includes('/api/history/locations') || path.includes('/api/history/raw-frames')) { + return { + ok: true, + json: async () => ({ + data: { items: [], total: 0, limit: 20, offset: 0 }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + return { + ok: true, + json: async () => ({ + data: { items: [], total: 0, limit: 20, offset: 0 }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + }); + + render(); + + expect(await screen.findByText('VIN-MILEAGE-RAW')).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: '核对 RAW' })); + + await waitFor(() => { + expect(window.location.hash.startsWith('#/history?')).toBe(true); + }); + const params = new URLSearchParams(window.location.hash.slice('#/history?'.length)); + expect(params.get('keyword')).toBe('VIN-MILEAGE-RAW'); + expect(params.get('protocol')).toBe('JT808'); + expect(params.get('dateFrom')).toBe('2026-07-03'); + expect(params.get('dateTo')).toBe('2026-07-04'); + expect(params.get('tab')).toBe('raw'); + expect(await screen.findByRole('heading', { name: '轨迹回放' })).toBeInTheDocument(); +}); + test('shows mileage anomaly action guidance', async () => { window.history.replaceState(null, '', '/#/mileage?keyword=VIN-MILEAGE-ANOMALY&protocol=GB32960'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {