From 276da591947aaf16cd84dc6f33976caa9405d98b Mon Sep 17 00:00:00 2001 From: lingniu Date: Sat, 4 Jul 2026 12:01:28 +0800 Subject: [PATCH] feat(platform): link vehicle rows to quality evidence --- vehicle-data-platform/apps/web/src/App.tsx | 2 +- .../apps/web/src/pages/Vehicles.tsx | 15 ++- .../apps/web/src/test/App.test.tsx | 103 ++++++++++++++++++ 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 26d9525f..b1db0690 100644 --- a/vehicle-data-platform/apps/web/src/App.tsx +++ b/vehicle-data-platform/apps/web/src/App.tsx @@ -374,7 +374,7 @@ export default function App() { const pages: Record = { dashboard: , - vehicles: , + vehicles: , realtime: , detail: , history: , diff --git a/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx b/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx index e15c93b5..3fa7b740 100644 --- a/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx @@ -177,10 +177,12 @@ async function copyVehicleShareURL() { export function Vehicles({ onOpenVehicle, + onOpenQuality, onFiltersChange, initialFilters = {} }: { onOpenVehicle: (vin: string, protocol?: string) => void; + onOpenQuality?: (filters: Record) => void; onFiltersChange?: (filters: Record) => void; initialFilters?: Record; }) { @@ -415,9 +417,18 @@ export function Vehicles({ { title: '在线', width: 90, render: (_: unknown, row: VehicleCoverageRow) => }, { title: '最后在线', dataIndex: 'lastSeen', width: 170 }, { title: '绑定', width: 90, render: (_: unknown, row: VehicleCoverageRow) => {row.bindingStatus === 'bound' ? '已绑定' : '未绑定'} }, - { title: '操作', width: 110, render: (_: unknown, row: VehicleCoverageRow) => } + { + title: '操作', + width: 190, + render: (_: unknown, row: VehicleCoverageRow) => ( + + + + + ) + } ], - [filters, onOpenVehicle] + [filters, onOpenQuality, onOpenVehicle] ); return ( 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 88a7aa19..a5412364 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -5612,6 +5612,109 @@ test('opens vehicle service from source-filtered vehicle list with source eviden }); }); +test('opens quality issues from source-filtered vehicle list with source evidence', async () => { + window.history.replaceState(null, '', '/#/vehicles?protocol=JT808'); + vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { + const path = String(input); + if (path.includes('/api/vehicles/coverage')) { + return { + ok: true, + json: async () => ({ + data: { + items: [{ + vin: 'VIN-LIST-QUALITY', + plate: '粤A列表质', + phone: '13307795425', + oem: 'G7s', + protocols: ['JT808'], + sourceCount: 1, + onlineSourceCount: 0, + online: false, + lastSeen: '2026-07-03 20:12:10', + bindingStatus: 'bound', + serviceStatus: { + status: 'offline', + severity: 'error', + title: '车辆离线', + detail: 'JT808 来源离线', + sourceCount: 1, + onlineSourceCount: 0 + } + }], + total: 1, + limit: 20, + offset: 0 + }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + if (path.includes('/api/quality/summary')) { + return { + ok: true, + json: async () => ({ + data: { + issueVehicleCount: 1, + issueRecordCount: 1, + errorCount: 1, + warningCount: 0, + protocols: [{ name: 'JT808', count: 1 }], + issueTypes: [{ name: 'LINK_GAP', count: 1 }] + }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + if (path.includes('/api/quality/issues')) { + return { + ok: true, + json: async () => ({ + data: { + items: [{ + vin: 'VIN-LIST-QUALITY', + plate: '粤A列表质', + phone: '13307795425', + sourceEndpoint: '115.231.168.135:43625', + protocol: 'JT808', + issueType: 'LINK_GAP', + severity: 'error', + lastSeen: '2026-07-03 20:12:10', + detail: 'JT808 来源离线' + }], + total: 1, + limit: 20, + offset: 0 + }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + return { + ok: true, + json: async () => ({ + data: path.includes('/api/ops/health') + ? { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } } + : { items: [], total: 0, limit: 20, offset: 0 }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + }); + + render(); + + expect(await screen.findByText('VIN-LIST-QUALITY')).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: '质量问题' })); + + await waitFor(() => { + expect(window.location.hash).toBe('#/quality?keyword=VIN-LIST-QUALITY&protocol=JT808'); + }); + expect(await screen.findByRole('heading', { name: '告警通知' })).toBeInTheDocument(); +}); + test('filters vehicle list by service status', async () => { window.history.replaceState(null, '', '/#/vehicles'); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {