From 4c27cfa28d741085d5d8fe4ffcdd90013a88d85e Mon Sep 17 00:00:00 2001 From: lingniu Date: Sat, 4 Jul 2026 02:11:32 +0800 Subject: [PATCH] feat(platform-web): show dashboard row service status --- .../apps/web/src/pages/Dashboard.tsx | 31 +++++ .../apps/web/src/test/App.test.tsx | 115 ++++++++++++++++++ 2 files changed, 146 insertions(+) diff --git a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx index 293e8e7e..70ead08b 100644 --- a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx @@ -23,6 +23,22 @@ function formatLag(value?: number | null) { return value == null ? '未接入' : value.toLocaleString(); } +function rowServiceStatus(row: { serviceStatus?: { title: string; severity: string }; onlineSourceCount: number; sourceCount: number }) { + if (row.serviceStatus) { + return { + label: row.serviceStatus.title, + color: row.serviceStatus.severity === 'ok' ? 'green' as const : row.serviceStatus.severity === 'error' ? 'red' as const : 'orange' as const + }; + } + if (row.onlineSourceCount <= 0) { + return { label: '车辆离线', color: 'red' as const }; + } + if (row.onlineSourceCount < row.sourceCount) { + return { label: '部分来源离线', color: 'orange' as const }; + } + return { label: '服务正常', color: 'green' as const }; +} + export function Dashboard({ onOpenVehicle, onOpenQuality }: { onOpenVehicle: (vin: string) => void; onOpenQuality: () => void }) { const [summary, setSummary] = useState(null); const [coverage, setCoverage] = useState([]); @@ -204,6 +220,14 @@ export function Dashboard({ onOpenVehicle, onOpenQuality }: { onOpenVehicle: (vi width: 110, render: (_: unknown, row: VehicleCoverageRow) => `${row.onlineSourceCount}/${row.sourceCount}` }, + { + title: '服务状态', + width: 130, + render: (_: unknown, row: VehicleCoverageRow) => { + const status = rowServiceStatus(row); + return {status.label}; + } + }, { title: '在线', width: 90, render: (_: unknown, row: VehicleCoverageRow) => }, { title: '绑定', width: 90, render: (_: unknown, row: VehicleCoverageRow) => {row.bindingStatus === 'bound' ? '已绑定' : '未绑定'} }, { title: '最后时间', dataIndex: 'lastSeen', width: 170 }, @@ -243,6 +267,13 @@ export function Dashboard({ onOpenVehicle, onOpenQuality }: { onOpenVehicle: (vi ) }, + { + title: '服务状态', + render: (_: unknown, row: VehicleRealtimeRow) => { + const status = rowServiceStatus(row); + return {status.label}; + } + }, { title: '最后时间', dataIndex: 'lastSeen' }, { title: '操作', width: 110, 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 6cd7077d..6a7bbe8f 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -57,6 +57,121 @@ test('shows vehicle service status distribution on dashboard', async () => { expect(screen.getByText('39')).toBeInTheDocument(); }); +test('shows row service status in dashboard vehicle previews', async () => { + window.history.replaceState(null, '', '/#/dashboard'); + vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { + const path = String(input); + if (path.includes('/api/dashboard/summary')) { + return { + ok: true, + json: async () => ({ + data: { + onlineVehicles: 3, + activeToday: 4, + frameToday: 1286320, + issueVehicles: 7, + kafkaLag: 0, + protocols: [], + serviceStatuses: [], + linkHealth: [] + }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + if (path.includes('/api/vehicles/coverage')) { + return { + ok: true, + json: async () => ({ + data: { + items: [{ + vin: 'VIN-COVERAGE-WARN', + plate: '粤A预警1', + phone: '', + oem: 'G7s', + protocols: ['GB32960', 'JT808'], + sourceCount: 2, + onlineSourceCount: 1, + online: true, + lastSeen: '2026-07-03 20:12:10', + bindingStatus: 'bound', + serviceStatus: { + status: 'degraded', + severity: 'warning', + title: '覆盖部分离线', + detail: '覆盖预览由 API 判定', + sourceCount: 2, + onlineSourceCount: 1 + } + }], + total: 1, + limit: 8, + offset: 0 + }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + if (path.includes('/api/realtime/vehicles')) { + return { + ok: true, + json: async () => ({ + data: { + items: [{ + vin: 'VIN-REALTIME-OFFLINE', + plate: '粤A离线1', + phone: '', + oem: 'G7s', + protocols: ['JT808'], + sourceCount: 1, + onlineSourceCount: 0, + online: false, + bindingStatus: 'bound', + serviceStatus: { + status: 'offline', + severity: 'error', + title: '实时车辆离线', + detail: '实时预览由 API 判定', + sourceCount: 1, + onlineSourceCount: 0 + }, + primaryProtocol: 'JT808', + longitude: 113.2, + latitude: 23.1, + speedKmh: 0, + socPercent: 0, + totalMileageKm: 100, + lastSeen: '2026-07-03 20:12:10' + }], + total: 1, + limit: 8, + 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.findByText('VIN-COVERAGE-WARN')).toBeInTheDocument(); + expect(screen.getByText('覆盖部分离线')).toBeInTheDocument(); + expect(screen.getByText('VIN-REALTIME-OFFLINE')).toBeInTheDocument(); + expect(screen.getByText('实时车辆离线')).toBeInTheDocument(); +}); + test('opens vehicle detail from shareable hash', async () => { window.history.replaceState(null, '', '/#/detail?keyword=%E7%B2%A4AG18312'); vi.spyOn(globalThis, 'fetch').mockResolvedValue({