- {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);
+});