diff --git a/vehicle-data-platform/apps/web/src/pages/VehicleDetail.tsx b/vehicle-data-platform/apps/web/src/pages/VehicleDetail.tsx index 46ea1adc..0ca2b029 100644 --- a/vehicle-data-platform/apps/web/src/pages/VehicleDetail.tsx +++ b/vehicle-data-platform/apps/web/src/pages/VehicleDetail.tsx @@ -138,6 +138,11 @@ export function VehicleDetail({ const consistencyTimeDeltaSeconds = consistency?.sourceTimeDeltaSeconds ?? sourceTimeDeltaSeconds; const consistencyTitle = consistency?.title ?? '-'; const consistencyDetail = consistency?.detail ?? '-'; + const evidenceSourceCount = overview?.sourceCount ?? consistencySourceCount; + const evidenceOnlineSourceCount = overview?.onlineSourceCount ?? consistencyOnlineSourceCount; + const evidenceLocatedSourceCount = consistencyLocatedSourceCount; + const evidenceMileageDelta = consistencyMileageDelta; + const evidenceTimeDeltaSeconds = consistencyTimeDeltaSeconds; const activeProtocol = query.protocol?.trim() ?? ''; const scopeText = activeProtocol ? `单一来源:${activeProtocol}` : '全部来源聚合'; const formKey = `${query.keyword}-${query.protocol ?? ''}`; @@ -224,6 +229,27 @@ export function VehicleDetail({ ) : null} + {overview || consistency ? ( + +
+ {[ + { label: '来源证据', value: evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount} 来源在线` : '-' }, + { label: '定位证据', value: evidenceLocatedSourceCount > 0 ? `${evidenceLocatedSourceCount} 个来源有位置` : '暂无位置' }, + { label: '里程差异', value: formatCompactNumber(evidenceMileageDelta, ' km') }, + { label: '时间差异', value: formatSeconds(evidenceTimeDeltaSeconds) }, + { label: '主来源', value: overview?.primaryProtocol || summary?.primaryProtocol || '-' }, + { label: '服务结论', value: consistencyTitle } + ].map((item) => ( +
+
{item.label}
+
{item.value}
+
+ ))} +
+ {consistencyDetail} +
+ ) : null} +
{ expect(screen.getByText('历史 12 / RAW 34 / 里程 7')).toBeInTheDocument(); }); +test('shows vehicle service evidence chain before source details', async () => { + window.history.replaceState(null, '', '/#/detail?keyword=VIN001'); + vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { + const path = String(input); + if (path.includes('/api/vehicle-service')) { + return { + ok: true, + json: async () => ({ + data: { + vin: 'VIN001', + lookupKey: 'VIN001', + lookupResolved: true, + serviceOverview: { + vin: 'VIN001', + plate: '粤AG18312', + phone: '13307795425', + oem: 'G7s', + protocols: ['GB32960', 'JT808', 'YUTONG_MQTT'], + primaryProtocol: 'GB32960', + coverageStatus: 'partial', + sourceCount: 3, + onlineSourceCount: 2, + lastSeen: '2026-07-03 20:12:10', + realtimeCount: 3, + historyCount: 12, + rawCount: 34, + mileageCount: 7, + qualityIssueCount: 1 + }, + serviceStatus: { + status: 'degraded', + severity: 'warning', + title: '部分来源离线', + detail: '2/3 个来源在线', + sourceCount: 3, + onlineSourceCount: 2 + }, + sourceConsistency: { + sourceCount: 3, + onlineSourceCount: 2, + locatedSourceCount: 2, + mileageDeltaKm: 1.4, + sourceTimeDeltaSeconds: 125, + scope: 'vehicle', + status: 'degraded', + severity: 'warning', + title: '部分来源离线', + detail: 'MQTT 离线,32960 与 808 仍可支撑车辆服务' + }, + sources: ['GB32960', 'JT808', 'YUTONG_MQTT'], + sourceStatus: [ + { protocol: 'GB32960', online: true, lastSeen: '2026-07-03 20:12:10', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true }, + { protocol: 'JT808', online: true, lastSeen: '2026-07-03 20:12:08', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true }, + { protocol: 'YUTONG_MQTT', online: false, lastSeen: '2026-07-03 20:10:05', hasRealtime: true, hasHistory: false, hasRaw: true, hasMileage: false } + ], + realtime: [], + history: { items: [], total: 12, limit: 20, offset: 0 }, + raw: { items: [], total: 34, limit: 10, offset: 0 }, + mileage: { items: [], total: 7, limit: 20, offset: 0 }, + quality: { items: [], 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('车辆服务证据链')).toBeInTheDocument(); + expect(screen.getByText('2/3 来源在线')).toBeInTheDocument(); + expect(screen.getAllByText('2 个来源有位置').length).toBeGreaterThan(0); + expect(screen.getAllByText('1.4 km').length).toBeGreaterThan(0); + expect(screen.getAllByText('125 秒').length).toBeGreaterThan(0); + expect(screen.getAllByText('MQTT 离线,32960 与 808 仍可支撑车辆服务').length).toBeGreaterThan(0); +}); + test('opens history with the currently selected vehicle detail source', async () => { window.history.replaceState(null, '', '/#/detail?keyword=VIN001'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { @@ -2223,10 +2311,10 @@ test('shows cross-source consistency for one vehicle service', async () => { expect(await screen.findByText('跨来源一致性')).toBeInTheDocument(); expect(screen.getAllByText('部分来源离线').length).toBeGreaterThanOrEqual(1); - expect(screen.getByText('2/3 个来源在线,车辆服务可用但需要关注离线来源。')).toBeInTheDocument(); + expect(screen.getAllByText('2/3 个来源在线,车辆服务可用但需要关注离线来源。').length).toBeGreaterThan(0); expect(screen.getByText('3 个来源')).toBeInTheDocument(); expect(screen.getByText('2/3 在线')).toBeInTheDocument(); - expect(screen.getByText('3 个来源有位置')).toBeInTheDocument(); - expect(screen.getByText('0.4 km')).toBeInTheDocument(); - expect(screen.getByText('35 秒')).toBeInTheDocument(); + expect(screen.getAllByText('3 个来源有位置').length).toBeGreaterThan(0); + expect(screen.getAllByText('0.4 km').length).toBeGreaterThan(0); + expect(screen.getAllByText('35 秒').length).toBeGreaterThan(0); });