diff --git a/vehicle-data-platform/apps/web/src/pages/Realtime.tsx b/vehicle-data-platform/apps/web/src/pages/Realtime.tsx
index 19212f95..55b50ef4 100644
--- a/vehicle-data-platform/apps/web/src/pages/Realtime.tsx
+++ b/vehicle-data-platform/apps/web/src/pages/Realtime.tsx
@@ -43,6 +43,20 @@ function serviceStatusWeight(row: VehicleRealtimeRow) {
return 1;
}
+function sourceIssueTags(row: VehicleRealtimeRow) {
+ const tags = (row.sourceStatus ?? [])
+ .filter((source) => !source.online || !source.hasRealtime)
+ .map((source) => `${source.protocol} ${source.hasRealtime ? '离线' : '未接入'}`) ?? [];
+ if (tags.length > 0) return tags;
+ if (row.onlineSourceCount < row.sourceCount) return ['来源缺失'];
+ return [];
+}
+
+function hasSourceIssue(row: VehicleRealtimeRow) {
+ const severity = row.serviceStatus?.severity;
+ return severity === 'warning' || severity === 'error' || row.onlineSourceCount < row.sourceCount || sourceIssueTags(row).length > 0;
+}
+
function isValidCoordinate(row: VehicleRealtimeRow) {
return Number.isFinite(row.longitude) && Number.isFinite(row.latitude) && row.longitude !== 0 && row.latitude !== 0;
}
@@ -126,6 +140,14 @@ export function Realtime({
const locatedCount = rows.filter(isValidCoordinate).length;
const degradedCount = rows.filter((row) => row.onlineSourceCount < row.sourceCount).length;
const primaryProtocols = new Set(rows.map((row) => row.primaryProtocol).filter(Boolean));
+ const sourceIssueRows = rows
+ .filter((row) => canOpenVehicle(row.vin) && hasSourceIssue(row))
+ .sort((a, b) => {
+ const statusDelta = serviceStatusWeight(b) - serviceStatusWeight(a);
+ if (statusDelta !== 0) return statusDelta;
+ return String(b.lastSeen ?? '').localeCompare(String(a.lastSeen ?? ''));
+ })
+ .slice(0, 4);
const mapServiceRows = rows
.filter((row) => isValidCoordinate(row) && canOpenVehicle(row.vin))
.sort((a, b) => {
@@ -217,6 +239,43 @@ export function Realtime({
高德 Web JS Key 和安全密钥通过运行环境配置,前端只读取公开 Key;安全密钥后续走服务端代理,避免明文下发。
+
地图车辆服务队列
{mapServiceRows.length === 0 ? (
diff --git a/vehicle-data-platform/apps/web/src/styles/global.css b/vehicle-data-platform/apps/web/src/styles/global.css
index f8b1480a..938ed138 100644
--- a/vehicle-data-platform/apps/web/src/styles/global.css
+++ b/vehicle-data-platform/apps/web/src/styles/global.css
@@ -385,6 +385,14 @@ body {
border-top: 1px solid var(--vp-border);
}
+.vp-source-consistency-panel {
+ display: grid;
+ gap: 10px;
+ margin-top: 4px;
+ padding-top: 12px;
+ border-top: 1px solid var(--vp-border);
+}
+
.vp-map-service-queue-title {
color: var(--vp-text);
font-size: 13px;
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 cb93bb06..931cd9e5 100644
--- a/vehicle-data-platform/apps/web/src/test/App.test.tsx
+++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx
@@ -5289,6 +5289,108 @@ test('opens amap coordinate and trajectory playback from realtime map service qu
expect(window.location.hash).toBe('#/history?keyword=VIN-MAP-LINK&protocol=GB32960');
});
+test('surfaces multi-source realtime consistency issues with quality drilldown', async () => {
+ window.history.replaceState(null, '', '/#/realtime');
+ vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
+ const path = String(input);
+ if (path.includes('/api/realtime/vehicles')) {
+ return {
+ ok: true,
+ json: async () => ({
+ data: {
+ items: [{
+ vin: 'VIN-RT-CONSISTENCY',
+ plate: '粤A一致监控',
+ phone: '13307795425',
+ oem: 'G7s',
+ protocols: ['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:05', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true },
+ { protocol: 'YUTONG_MQTT', online: false, lastSeen: '2026-07-03 20:01:40', hasRealtime: true, hasHistory: false, hasRaw: true, hasMileage: false }
+ ],
+ sourceCount: 3,
+ onlineSourceCount: 2,
+ online: true,
+ primaryProtocol: 'GB32960',
+ longitude: 113.2,
+ latitude: 23.1,
+ speedKmh: 30,
+ socPercent: 78,
+ totalMileageKm: 119925,
+ lastSeen: '2026-07-03 20:12:10',
+ bindingStatus: 'bound',
+ serviceStatus: {
+ status: 'degraded',
+ severity: 'warning',
+ title: '来源不完整',
+ detail: 'YUTONG_MQTT 离线,GB32960/JT808 仍可支撑车辆服务',
+ sourceCount: 3,
+ onlineSourceCount: 2
+ }
+ }],
+ total: 1,
+ limit: 50,
+ offset: 0
+ },
+ traceId: 'trace-test',
+ timestamp: 1783094400000
+ })
+ } as Response;
+ }
+ if (path.includes('/api/quality/summary')) {
+ return {
+ ok: true,
+ json: async () => ({
+ data: { issueVehicleCount: 0, issueRecordCount: 0, errorCount: 0, warningCount: 0, protocols: [], issueTypes: [] },
+ traceId: 'trace-test',
+ timestamp: 1783094400000
+ })
+ } as Response;
+ }
+ if (path.includes('/api/quality/issues')) {
+ return {
+ ok: true,
+ json: async () => ({
+ data: { items: [], total: 0, limit: 50, offset: 0 },
+ traceId: 'trace-test',
+ timestamp: 1783094400000
+ })
+ } as Response;
+ }
+ if (path.includes('/api/ops/health')) {
+ return {
+ ok: true,
+ json: async () => ({
+ data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } },
+ 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('多来源一致性检查')).toBeInTheDocument();
+ expect(screen.getAllByText('粤A一致监控').length).toBeGreaterThan(0);
+ expect(screen.getByText('一致性诊断:YUTONG_MQTT 离线,GB32960/JT808 仍可支撑车辆服务')).toBeInTheDocument();
+ expect(screen.getByText('一致性:YUTONG_MQTT 离线')).toBeInTheDocument();
+ fireEvent.click(screen.getByRole('button', { name: '检查质量' }));
+
+ await waitFor(() => {
+ expect(window.location.hash).toBe('#/quality?keyword=VIN-RT-CONSISTENCY&protocol=GB32960');
+ });
+});
+
test('opens quality issues from realtime vehicle row with source evidence', async () => {
window.history.replaceState(null, '', '/#/realtime?protocol=JT808');
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {