From a960d1fdabe0479a3549b3b0ed3f21cd9861cbcd Mon Sep 17 00:00:00 2001 From: lingniu Date: Sat, 4 Jul 2026 12:28:09 +0800 Subject: [PATCH] feat(platform): link locations to raw evidence --- vehicle-data-platform/apps/web/src/App.tsx | 2 +- .../apps/web/src/pages/History.tsx | 19 +++- .../apps/web/src/test/App.test.tsx | 95 +++++++++++++++++++ 3 files changed, 112 insertions(+), 4 deletions(-) diff --git a/vehicle-data-platform/apps/web/src/App.tsx b/vehicle-data-platform/apps/web/src/App.tsx index 73051c04..4cadc42e 100644 --- a/vehicle-data-platform/apps/web/src/App.tsx +++ b/vehicle-data-platform/apps/web/src/App.tsx @@ -393,7 +393,7 @@ export default function App() { vehicles: , realtime: , detail: , - history: , + history: , 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/History.tsx b/vehicle-data-platform/apps/web/src/pages/History.tsx index e1a25399..b258cda1 100644 --- a/vehicle-data-platform/apps/web/src/pages/History.tsx +++ b/vehicle-data-platform/apps/web/src/pages/History.tsx @@ -118,7 +118,8 @@ export function History({ initialFilters = {}, onFiltersChange, onOpenVehicle, - onOpenMileage + onOpenMileage, + onOpenRaw }: { initialVin: string; initialProtocol?: string; @@ -127,6 +128,7 @@ export function History({ onFiltersChange?: (filters: HistoryFilters, tab?: HistoryTabKey) => void; onOpenVehicle: (vin: string, protocol?: string) => void; onOpenMileage?: (filters: Record) => void; + onOpenRaw?: (filters: Record) => void; }) { const initialHistoryFilters = mergeInitialFilters(initialVin, initialProtocol, initialFilters); const [filters, setFilters] = useState(initialHistoryFilters); @@ -294,6 +296,16 @@ export function History({ ...(day ? { dateFrom: day, dateTo: nextDate(day) } : {}) }); }; + const openLocationRaw = (row: HistoryLocationRow) => { + if (!canOpenVehicle(row.vin)) return; + const day = dateOnly(row.deviceTime || row.serverTime || row.lastSeen); + onOpenRaw?.({ + keyword: row.vin, + protocol: row.protocol, + ...(day ? { dateFrom: day, dateTo: nextDate(day) } : {}), + includeFields: 'true' + }); + }; const openRawMileage = (row: RawFrameRow) => { if (!canOpenVehicle(row.vin)) return; const day = dateOnly(row.deviceTime || row.serverTime); @@ -465,11 +477,12 @@ export function History({ { title: '入库时间', dataIndex: 'serverTime', width: 190 }, { title: '操作', - width: 190, + width: 280, render: (_: unknown, row: HistoryLocationRow) => ( - + + ) } 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 931cd9e5..307b91b2 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -4583,6 +4583,101 @@ test('opens same-day mileage statistics from history location row', async () => }); }); +test('opens same-day raw evidence from history location row', async () => { + window.history.replaceState(null, '', '/#/history?keyword=VIN-HISTORY-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/history/locations')) { + return { + ok: true, + json: async () => ({ + data: { + items: [{ + vin: 'VIN-HISTORY-RAW', + plate: '粤A原帧核', + protocol: 'JT808', + longitude: 113.2, + latitude: 23.1, + speedKmh: 30, + socPercent: 0, + totalMileageKm: 218.8, + lastSeen: '2026-07-03 20:12:10', + deviceTime: '2026-07-03 20:12:10', + serverTime: '2026-07-03 20:12:11' + }], + total: 1, + limit: 10, + offset: 0 + }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + if (path.includes('/api/history/raw-frames/query')) { + return { + ok: true, + json: async () => ({ + data: { + items: [{ + id: 'raw-from-location-001', + vin: 'VIN-HISTORY-RAW', + plate: '粤A原帧核', + protocol: 'JT808', + frameType: '0x0200', + deviceTime: '2026-07-03 20:12:10', + serverTime: '2026-07-03 20:12:11', + rawSizeBytes: 128, + parsedFields: { 'jt808.location.longitude': 113.2 } + }], + total: 1, + limit: 10, + offset: 0 + }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + return { + ok: true, + json: async () => ({ + data: { items: [], total: 0, limit: 10, offset: 0 }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + }); + + render(); + + expect((await screen.findAllByText('VIN-HISTORY-RAW')).length).toBeGreaterThan(0); + 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-HISTORY-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('includeFields')).toBe('true'); + expect(params.get('tab')).toBe('raw'); + expect(await screen.findByRole('tab', { name: 'RAW 帧' })).toHaveAttribute('aria-selected', 'true'); + expect(await screen.findByText('raw-from-location-001')).toBeInTheDocument(); +}); + test('opens vehicle service from raw history rows with row source evidence', async () => { window.history.replaceState(null, '', '/#/history?keyword=%E7%B2%A4AG18312&protocol=GB32960'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {