diff --git a/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx b/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx index 52259c13..4d43c3da 100644 --- a/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx @@ -33,13 +33,26 @@ function sourceEvidenceText(row: VehicleCoverageRow) { return `${row.onlineSourceCount}/${row.sourceCount} 来源在线`; } -function sourceConsistencyTag(row: VehicleCoverageRow) { +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; - return {consistency.title || consistency.status}; + const label = consistency.title || consistency.status; + if ((consistency.missingProtocols ?? []).length > 0) { + return ( + + ); + } + return {label}; } async function copyVehicleShareURL() { @@ -166,7 +179,7 @@ export function Vehicles({ { title: '来源一致性', width: 140, - render: (_: unknown, row: VehicleCoverageRow) => sourceConsistencyTag(row) + render: (_: unknown, row: VehicleCoverageRow) => sourceConsistencyAction(row, (nextFilters) => applyFilters({ ...filters, ...nextFilters })) }, { title: '证据覆盖', width: 120, render: (_: unknown, row: VehicleCoverageRow) => sourceEvidenceText(row) }, { title: '在线', width: 90, render: (_: unknown, row: VehicleCoverageRow) => }, 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 6aab28d0..02abe51c 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -2841,6 +2841,177 @@ test('shows source consistency diagnosis in vehicle service list', async () => { expect(screen.getByText('来源不完整')).toBeInTheDocument(); }); +test('filters vehicle list from source consistency diagnosis', async () => { + window.history.replaceState(null, '', '/#/vehicles'); + const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { + const path = String(input); + if (path.includes('/api/vehicles/coverage/summary')) { + return { + ok: true, + json: async () => ({ + data: { + totalVehicles: 1, + onlineVehicles: 1, + singleSourceVehicles: 0, + multiSourceVehicles: 1, + noDataVehicles: 0, + unboundVehicles: 0, + missingSources: [{ protocol: 'YUTONG_MQTT', count: 1 }] + }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + if (path.includes('/api/vehicles/coverage')) { + return { + ok: true, + json: async () => ({ + data: { + items: [{ + vin: 'VIN-CONSISTENCY-ACTION', + plate: '粤A动作', + phone: '13307795425', + oem: 'G7s', + protocols: ['GB32960', 'JT808'], + missingProtocols: ['YUTONG_MQTT'], + sourceCount: 2, + onlineSourceCount: 2, + online: true, + lastSeen: '2026-07-03 20:12:10', + bindingStatus: 'bound', + sourceConsistency: { + sourceCount: 2, + onlineSourceCount: 2, + locatedSourceCount: 0, + missingProtocols: ['YUTONG_MQTT'], + mileageDeltaKm: 0, + sourceTimeDeltaSeconds: 0, + scope: 'coverage', + status: 'degraded', + severity: 'warning', + title: '来源不完整', + detail: '2/2 个来源在线,车辆服务可用但缺少 YUTONG_MQTT 来源。' + } + }], + 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: 10, offset: 0 }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + }); + + render(); + + expect(await screen.findByText('VIN-CONSISTENCY-ACTION')).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: '来源不完整' })); + + await waitFor(() => { + expect(window.location.hash).toBe('#/vehicles?serviceStatus=degraded&missingProtocol=YUTONG_MQTT'); + }); + expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('serviceStatus=degraded'), undefined); + expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('missingProtocol=YUTONG_MQTT'), undefined); +}); + +test('filters vehicle list from single-source consistency diagnosis', async () => { + window.history.replaceState(null, '', '/#/vehicles'); + const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { + const path = String(input); + if (path.includes('/api/vehicles/coverage/summary')) { + return { + ok: true, + json: async () => ({ + data: { + totalVehicles: 1, + onlineVehicles: 1, + singleSourceVehicles: 1, + multiSourceVehicles: 0, + noDataVehicles: 0, + unboundVehicles: 0, + missingSources: [{ protocol: 'JT808', count: 1 }] + }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + if (path.includes('/api/vehicles/coverage')) { + return { + ok: true, + json: async () => ({ + data: { + items: [{ + vin: 'VIN-SINGLE-SOURCE', + 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: 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: 10, offset: 0 }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + }); + + render(); + + expect(await screen.findByText('VIN-SINGLE-SOURCE')).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: '单一来源' })); + + await waitFor(() => { + expect(window.location.hash).toBe('#/vehicles?serviceStatus=degraded&missingProtocol=JT808'); + }); + expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('missingProtocol=JT808'), undefined); +}); + test('opens vehicle service from source-filtered vehicle list with source evidence', async () => { window.history.replaceState(null, '', '/#/vehicles?protocol=JT808'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {