diff --git a/vehicle-data-platform/apps/web/src/components/SourceStatusTags.tsx b/vehicle-data-platform/apps/web/src/components/SourceStatusTags.tsx index 4ad18e9f..02f97b0b 100644 --- a/vehicle-data-platform/apps/web/src/components/SourceStatusTags.tsx +++ b/vehicle-data-platform/apps/web/src/components/SourceStatusTags.tsx @@ -17,12 +17,12 @@ export function SourceStatusTags({ lastSeen }: { sourceStatus?: VehicleSourceStatus[]; - protocols: string[]; + protocols?: string[]; lastSeen: string; }) { const sources = sourceStatus?.length ? sourceStatus - : protocols.map((protocol) => ({ protocol, online: false, hasRealtime: true, lastSeen })); + : (protocols ?? []).map((protocol) => ({ protocol, online: false, hasRealtime: true, lastSeen })); return ( diff --git a/vehicle-data-platform/apps/web/src/pages/Realtime.tsx b/vehicle-data-platform/apps/web/src/pages/Realtime.tsx index ea0ea595..e2afbe3e 100644 --- a/vehicle-data-platform/apps/web/src/pages/Realtime.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Realtime.tsx @@ -34,6 +34,15 @@ function sourceEvidenceText(row: VehicleRealtimeRow) { return `${row.onlineSourceCount}/${row.sourceCount} 来源在线`; } +function serviceStatusWeight(row: VehicleRealtimeRow) { + const severity = row.serviceStatus?.severity; + if (severity === 'error') return 4; + if (severity === 'warning') return 3; + if (row.onlineSourceCount <= 0) return 4; + if (row.onlineSourceCount < row.sourceCount) return 3; + return 1; +} + function isValidCoordinate(row: VehicleRealtimeRow) { return Number.isFinite(row.longitude) && Number.isFinite(row.latitude) && row.longitude !== 0 && row.latitude !== 0; } @@ -101,13 +110,21 @@ 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 mapServiceRows = rows + .filter((row) => isValidCoordinate(row) && canOpenVehicle(row.vin)) + .sort((a, b) => { + const statusDelta = serviceStatusWeight(b) - serviceStatusWeight(a); + if (statusDelta !== 0) return statusDelta; + return String(b.lastSeen ?? '').localeCompare(String(a.lastSeen ?? '')); + }) + .slice(0, 5); const mapPoints: VehicleMapPoint[] = rows.map((row, index) => ({ id: row.vin || `${row.primaryProtocol}-${index}`, label: row.plate || row.vin || 'unknown', longitude: row.longitude, latitude: row.latitude, online: row.online, - title: `${row.plate || row.vin || '-'} ${row.primaryProtocol || ''} ${row.lastSeen || ''}` + title: `${row.plate || row.vin || '-'} ${vehicleServiceStatus(row).label} ${row.primaryProtocol || ''} ${row.lastSeen || ''}` })); return ( @@ -184,6 +201,34 @@ export function Realtime({ 高德 Web JS Key 和安全密钥通过运行环境配置,前端只读取公开 Key;安全密钥后续走服务端代理,避免明文下发。 +
+
地图车辆服务队列
+ {mapServiceRows.length === 0 ? ( + 暂无可定位车辆 + ) : ( + mapServiceRows.map((row) => { + const status = vehicleServiceStatus(row); + return ( +
+
+
+ {row.plate || row.vin} + {row.vin} +
+ {status.label} +
+ + {row.serviceStatus?.detail || sourceEvidenceText(row)} + +
+ {row.lastSeen || '-'} + +
+
+ ); + }) + )} +
diff --git a/vehicle-data-platform/apps/web/src/styles/global.css b/vehicle-data-platform/apps/web/src/styles/global.css index 5c2d24c1..c9ac620b 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -377,6 +377,42 @@ body { line-height: 30px; } +.vp-map-service-queue { + 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; + font-weight: 700; +} + +.vp-map-service-item { + display: grid; + gap: 8px; + padding: 10px; + border: 1px solid var(--vp-border); + border-radius: var(--vp-radius); + background: #fff; +} + +.vp-map-service-item-main, +.vp-map-service-item-footer { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.vp-map-service-item-main > div { + display: grid; + min-width: 0; +} + .vp-alert-flow { display: grid; grid-template-columns: repeat(3, 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 ba956995..ebc717c4 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -4287,7 +4287,7 @@ test('opens vehicle service from realtime vehicles with primary source evidence' render(); - expect(await screen.findByText('VIN-MQTT-001')).toBeInTheDocument(); + expect((await screen.findAllByText('VIN-MQTT-001')).length).toBeGreaterThan(0); fireEvent.click(screen.getByRole('button', { name: '车辆服务' })); await waitFor(() => { @@ -4370,7 +4370,7 @@ test('opens vehicle service from realtime vehicles with current source filter', render(); - expect(await screen.findByText('VIN-RT-FILTER')).toBeInTheDocument(); + expect((await screen.findAllByText('VIN-RT-FILTER')).length).toBeGreaterThan(0); fireEvent.click(screen.getByRole('button', { name: '车辆服务' })); await waitFor(() => { @@ -4435,8 +4435,8 @@ test('shows canonical service status in realtime vehicles', async () => { render(); - expect(await screen.findByText('VIN-RT-DEGRADED')).toBeInTheDocument(); - expect(screen.getByText('来源不完整')).toBeInTheDocument(); + expect((await screen.findAllByText('VIN-RT-DEGRADED')).length).toBeGreaterThan(0); + expect(screen.getAllByText('来源不完整').length).toBeGreaterThan(0); }); test('frames realtime page as one vehicle service with source evidence', async () => { @@ -4508,7 +4508,99 @@ test('frames realtime page as one vehicle service with source evidence', async ( expect(screen.getByText('GB32960 在线')).toBeInTheDocument(); expect(screen.getByText('JT808 在线')).toBeInTheDocument(); expect(screen.getByText('YUTONG_MQTT 未接入')).toBeInTheDocument(); - expect(screen.getByText('2/2 来源在线')).toBeInTheDocument(); + expect(screen.getAllByText('2/2 来源在线').length).toBeGreaterThan(0); +}); + +test('opens vehicle service from realtime map service queue', 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-MAP-001', + plate: '粤AMAP01', + phone: '', + oem: 'G7s', + protocols: ['GB32960', 'JT808'], + sourceStatus: [ + { protocol: 'GB32960', online: true, lastSeen: '2026-07-03 20:12:10', hasRealtime: true, hasHistory: false, hasRaw: false, hasMileage: false }, + { protocol: 'JT808', online: false, lastSeen: '2026-07-03 19:12:10', hasRealtime: true, hasHistory: false, hasRaw: false, hasMileage: false } + ], + sourceCount: 2, + onlineSourceCount: 1, + 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: 'JT808 离线,GB32960 仍可支撑车辆服务', + sourceCount: 2, + onlineSourceCount: 1 + } + }], + total: 1, + limit: 50, + offset: 0 + }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + if (path.includes('/api/vehicle-service/overview')) { + return { + ok: true, + json: async () => ({ + data: { + vin: 'VIN-MAP-001', + plate: '粤AMAP01', + sourceCount: 2, + onlineSourceCount: 1, + coverageStatus: 'degraded', + primaryProtocol: 'GB32960', + lastSeen: '2026-07-03 20:12:10', + historyCount: 0, + rawCount: 0, + mileageCount: 0, + qualityIssueCount: 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('地图车辆服务队列')).toBeInTheDocument(); + expect(screen.getAllByText('粤AMAP01').length).toBeGreaterThan(0); + expect(screen.getByText('JT808 离线,GB32960 仍可支撑车辆服务')).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: '地图车辆服务' })); + + await waitFor(() => { + expect(window.location.hash).toBe('#/detail?keyword=VIN-MAP-001&protocol=GB32960'); + }); }); test('loads realtime vehicles from shareable source filter hash', async () => { @@ -4559,7 +4651,7 @@ test('loads realtime vehicles from shareable source filter hash', async () => { render(); - expect(await screen.findByText('VIN-RT-FILTERED')).toBeInTheDocument(); + expect((await screen.findAllByText('VIN-RT-FILTERED')).length).toBeGreaterThan(0); expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/realtime/vehicles?limit=50&offset=0&keyword=%E7%B2%A4ART002&protocol=JT808&online=online&serviceStatus=degraded'), undefined); });