diff --git a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx index 9901bdcc..1f147216 100644 --- a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx @@ -63,6 +63,28 @@ function sourceEvidenceText(row: { onlineSourceCount: number; sourceCount: numbe return `${row.onlineSourceCount}/${row.sourceCount} 来源在线`; } +function sourceConsistencyAction(row: VehicleCoverageRow, onFilter: (filters: Record) => void) { + const consistency = row.sourceConsistency; + if (!consistency) { + return 未诊断; + } + const color = consistency.severity === 'ok' ? 'green' as const : consistency.severity === 'error' ? 'red' as const : 'orange' as const; + const label = consistency.title || consistency.status; + if ((consistency.missingProtocols ?? []).length > 0) { + return ( + + ); + } + return {label}; +} + export function Dashboard({ onOpenVehicle, onOpenQuality, onOpenVehicles }: { onOpenVehicle: (vin: string, protocol?: string) => void; onOpenQuality: () => void; onOpenVehicles: (filters?: Record) => void }) { const [summary, setSummary] = useState(null); const [serviceSummary, setServiceSummary] = useState(null); @@ -315,6 +337,11 @@ export function Dashboard({ onOpenVehicle, onOpenQuality, onOpenVehicles }: { on return {status.label}; } }, + { + title: '来源一致性', + width: 140, + render: (_: unknown, row: VehicleCoverageRow) => sourceConsistencyAction(row, loadCoverage) + }, { title: '在线', width: 90, render: (_: unknown, row: VehicleCoverageRow) => }, { title: '绑定', width: 90, render: (_: unknown, row: VehicleCoverageRow) => {row.bindingStatus === 'bound' ? '已绑定' : '未绑定'} }, { title: '最后时间', dataIndex: 'lastSeen', width: 170 }, 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 02abe51c..8af3db0b 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -788,6 +788,115 @@ test('shows row service status in dashboard vehicle previews', async () => { expect(screen.getByText('实时车辆离线')).toBeInTheDocument(); }); +test('filters dashboard coverage from source consistency diagnosis', async () => { + window.history.replaceState(null, '', '/#/dashboard'); + const fetchMock = 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/vehicle-service/summary')) { + return { + ok: true, + json: async () => ({ + data: { + totalVehicles: 1, + boundVehicles: 1, + onlineVehicles: 1, + singleSourceVehicles: 1, + multiSourceVehicles: 0, + noDataVehicles: 0, + identityRequiredVehicles: 0, + serviceStatuses: [], + protocols: [], + missingSources: [] + }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + if (path.includes('/api/vehicles/coverage')) { + return { + ok: true, + json: async () => ({ + data: { + items: [{ + vin: 'VIN-DASH-SINGLE', + plate: '粤A总览', + phone: '', + oem: '', + protocols: ['GB32960'], + missingProtocols: ['JT808', 'YUTONG_MQTT'], + sourceCount: 1, + onlineSourceCount: 1, + online: true, + lastSeen: '2026-07-04 07:15:28', + bindingStatus: 'bound', + sourceConsistency: { + sourceCount: 1, + onlineSourceCount: 1, + locatedSourceCount: 0, + missingProtocols: ['JT808', 'YUTONG_MQTT'], + mileageDeltaKm: 0, + sourceTimeDeltaSeconds: 0, + scope: 'coverage', + status: 'single_source', + severity: 'warning', + title: '单一来源', + detail: '当前车辆只有一个数据来源,无法做跨来源一致性校验。' + } + }], + total: 1, + limit: 8, + 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: 8, offset: 0 }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + }); + + render(); + + expect(await screen.findByText('VIN-DASH-SINGLE')).toBeInTheDocument(); + expect(screen.getByText('来源一致性')).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: '单一来源' })); + + await waitFor(() => { + expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('serviceStatus=degraded'), undefined); + }); + expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('missingProtocol=JT808'), undefined); + expect(screen.getByText('当前筛选:来源异常 / 缺 JT808')).toBeInTheDocument(); +}); + test('opens vehicle service from dashboard realtime preview with source evidence', async () => { window.history.replaceState(null, '', '/#/dashboard'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {