diff --git a/vehicle-data-platform/apps/web/src/pages/VehicleDetail.tsx b/vehicle-data-platform/apps/web/src/pages/VehicleDetail.tsx index d0c7508c..aeba2d60 100644 --- a/vehicle-data-platform/apps/web/src/pages/VehicleDetail.tsx +++ b/vehicle-data-platform/apps/web/src/pages/VehicleDetail.tsx @@ -87,7 +87,14 @@ export function VehicleDetail({ const qualityCount = detail?.quality?.total ?? 0; const online = resolution?.online || summary?.online || identity?.online || false; const lastSeen = resolution?.lastSeen || summary?.lastSeen || latest?.lastSeen || '-'; + const activeProtocol = query.protocol?.trim() ?? ''; const formKey = `${query.keyword}-${query.protocol ?? ''}`; + const switchSource = (nextProtocol = '') => { + const nextQuery = { keyword: resolvedVIN || query.keyword, protocol: nextProtocol }; + setQuery(nextQuery); + onQueryChange?.(nextQuery.keyword, nextQuery.protocol); + load(nextQuery); + }; const qualityTable = ( {detail?.sourceStatus?.length ? ( -
- {detail.sourceStatus.map((source) => ( -
-
- - {source.protocol} - + <> +
+ + {activeProtocol ? `当前 ${activeProtocol}` : '当前 全部来源'} + + +
+
+ {detail.sourceStatus.map((source) => ( +
+
+ + {source.protocol} + + + {source.lastSeen || '无上报'} +
+ + {sourceCapabilities.map((item) => ( + + {item.label}{source[item.key] ? '有' : '无'} + + ))} - {source.lastSeen || '无上报'} +
+ +
- - {sourceCapabilities.map((item) => ( - - {item.label}{source[item.key] ? '有' : '无'} - - ))} - -
- ))} -
+ ))} +
+ ) : ( 暂未查询到该车辆的数据来源覆盖。 )} diff --git a/vehicle-data-platform/apps/web/src/styles/global.css b/vehicle-data-platform/apps/web/src/styles/global.css index 04b86843..ce1fba77 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -173,6 +173,10 @@ body { gap: 12px; } +.vp-source-toolbar { + margin-bottom: 12px; +} + .vp-source-card { min-height: 86px; padding: 12px; @@ -181,6 +185,11 @@ body { background: #fbfcff; } +.vp-source-card-active { + border-color: rgba(22, 100, 255, 0.5); + background: rgba(22, 100, 255, 0.06); +} + .vp-source-card-head { display: flex; align-items: center; @@ -189,6 +198,10 @@ body { margin-bottom: 12px; } +.vp-source-card-actions { + margin-top: 12px; +} + .vp-realtime-strip { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); 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 dafcec26..470cf988 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -243,3 +243,81 @@ test('keeps primary protocol when opening vehicle service from realtime vehicles expect(window.location.hash).toBe('#/detail?keyword=VIN-MQTT-001&protocol=YUTONG_MQTT'); }); }); + +test('switches vehicle detail to a source from the coverage cards', async () => { + window.history.replaceState(null, '', '/#/detail?keyword=VIN001'); + const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { + const path = String(input); + if (path.includes('/api/vehicles/detail')) { + return { + ok: true, + json: async () => ({ + data: { + vin: 'VIN001', + lookupKey: 'VIN001', + lookupResolved: true, + resolution: { + lookupKey: 'VIN001', + resolved: true, + vin: 'VIN001', + plate: '粤AG18312', + phone: '13307795425', + oem: 'G7s', + protocols: ['GB32960', 'JT808', 'YUTONG_MQTT'], + online: true, + lastSeen: '2026-07-03 20:12:10' + }, + realtimeSummary: { + vin: 'VIN001', + plate: '粤AG18312', + phone: '13307795425', + oem: 'G7s', + protocols: ['GB32960', 'JT808', 'YUTONG_MQTT'], + sourceCount: 3, + onlineSourceCount: 2, + online: true, + primaryProtocol: 'GB32960', + longitude: 113.2, + latitude: 23.1, + speedKmh: 30, + socPercent: 78, + totalMileageKm: 100, + lastSeen: '2026-07-03 20:12:10' + }, + 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:00', hasRealtime: true, hasHistory: false, hasRaw: true, hasMileage: false } + ], + realtime: [], + history: { items: [], total: 0, limit: 20, offset: 0 }, + raw: { items: [], total: 0, limit: 10, offset: 0 }, + mileage: { items: [], total: 0, limit: 20, offset: 0 }, + quality: { items: [], total: 0, limit: 20, 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('VIN001')).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: '仅看 JT808' })); + + await waitFor(() => { + expect(window.location.hash).toBe('#/detail?keyword=VIN001&protocol=JT808'); + }); + expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/vehicles/detail?keyword=VIN001&protocol=JT808'), undefined); +});