From 4ef71c37f2cda7fc5da0946dc88b30238f3de7e0 Mon Sep 17 00:00:00 2001 From: lingniu Date: Sat, 4 Jul 2026 12:06:46 +0800 Subject: [PATCH] feat(platform): link realtime rows to quality evidence --- vehicle-data-platform/apps/web/src/App.tsx | 2 +- .../apps/web/src/pages/Realtime.tsx | 17 ++- .../apps/web/src/test/App.test.tsx | 131 ++++++++++++++++++ 3 files changed, 147 insertions(+), 3 deletions(-) diff --git a/vehicle-data-platform/apps/web/src/App.tsx b/vehicle-data-platform/apps/web/src/App.tsx index b1db0690..f05bdf0d 100644 --- a/vehicle-data-platform/apps/web/src/App.tsx +++ b/vehicle-data-platform/apps/web/src/App.tsx @@ -375,7 +375,7 @@ export default function App() { const pages: Record = { dashboard: , vehicles: , - realtime: , + realtime: , detail: , history: , mileage: , diff --git a/vehicle-data-platform/apps/web/src/pages/Realtime.tsx b/vehicle-data-platform/apps/web/src/pages/Realtime.tsx index 2d9c903e..19212f95 100644 --- a/vehicle-data-platform/apps/web/src/pages/Realtime.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Realtime.tsx @@ -67,11 +67,13 @@ const serviceStatusLabel: Record = { export function Realtime({ onOpenVehicle, onOpenHistory, + onOpenQuality, onFiltersChange, initialFilters = {} }: { onOpenVehicle: (vin: string, protocol?: string) => void; onOpenHistory?: (vin: string, protocol?: string) => void; + onOpenQuality?: (filters: Record) => void; onFiltersChange?: (filters: Record) => void; initialFilters?: Record; }) { @@ -107,6 +109,13 @@ export function Realtime({ onFiltersChange?.(nextFilters); load(nextFilters, 1, pagination.pageSize); }; + const openQualityEvidence = (row: VehicleRealtimeRow) => { + if (!canOpenVehicle(row.vin)) return; + onOpenQuality?.({ + keyword: row.vin, + protocol: filters.protocol || row.primaryProtocol || '' + }); + }; const filterSummary = [ filters.keyword ? `关键词:${filters.keyword}` : '', filters.protocol ? `数据来源:${filters.protocol}` : '', @@ -232,6 +241,7 @@ export function Realtime({ + @@ -293,9 +303,12 @@ export function Realtime({ { title: '最后时间', dataIndex: 'lastSeen', width: 170 }, { title: '操作', - width: 110, + width: 190, render: (_: unknown, row: VehicleRealtimeRow) => ( - + + + + ) } ]} 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 a5412364..b9f39cef 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -5100,6 +5100,137 @@ test('opens amap coordinate and trajectory playback from realtime map service qu expect(window.location.hash).toBe('#/history?keyword=VIN-MAP-LINK&protocol=GB32960'); }); +test('opens quality issues from realtime vehicle row with source evidence', async () => { + window.history.replaceState(null, '', '/#/realtime?protocol=JT808'); + vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { + const path = String(input); + if (path.includes('/api/realtime/vehicles')) { + return { + ok: true, + json: async () => ({ + data: { + items: [{ + vin: 'VIN-RT-QUALITY', + plate: '粤A质量1', + phone: '13307795425', + oem: 'G7s', + protocols: ['JT808'], + sourceStatus: [ + { protocol: 'JT808', online: false, lastSeen: '2026-07-03 20:12:10', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: false } + ], + sourceCount: 1, + onlineSourceCount: 0, + online: false, + primaryProtocol: 'JT808', + longitude: 113.2, + latitude: 23.1, + speedKmh: 0, + socPercent: 70, + totalMileageKm: 100, + lastSeen: '2026-07-03 20:12:10', + bindingStatus: 'bound', + serviceStatus: { + status: 'offline', + severity: 'error', + title: '车辆离线', + detail: 'JT808 离线', + sourceCount: 1, + onlineSourceCount: 0 + } + }], + total: 1, + limit: 50, + 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-RT-QUALITY', + plate: '粤A质量1', + phone: '13307795425', + sourceEndpoint: '115.29.187.205:808', + protocol: 'JT808', + issueType: 'LINK_GAP', + severity: 'error', + title: 'JT808 离线', + detail: '实时来源不可用', + firstSeen: '2026-07-03 20:12:10', + lastSeen: '2026-07-03 20:12:10' + }], + total: 1, + limit: 50, + offset: 0 + }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + if (path.includes('/api/ops/health')) { + return { + ok: true, + json: async () => ({ + data: { + linkHealth: [], + kafkaLag: 0, + activeConnections: 1, + capacityFindings: [], + redisOnlineKeys: 1, + tdengineWritable: true, + mysqlWritable: true, + runtime: { requestTimeoutMs: 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-RT-QUALITY')).length).toBeGreaterThan(0); + fireEvent.click(screen.getAllByRole('button', { name: '质量问题' })[0]); + + await waitFor(() => { + expect(window.location.hash).toBe('#/quality?keyword=VIN-RT-QUALITY&protocol=JT808'); + }); + expect(await screen.findByRole('heading', { name: '告警通知' })).toBeInTheDocument(); +}); + test('loads realtime vehicles from shareable source filter hash', async () => { window.history.replaceState(null, '', '/#/realtime?keyword=%E7%B2%A4ART002&protocol=JT808&online=online&serviceStatus=degraded'); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {