From b7de38605df01e820eb5e9fa3a633c2b5794d9d4 Mon Sep 17 00:00:00 2001 From: lingniu Date: Sat, 4 Jul 2026 01:50:06 +0800 Subject: [PATCH] feat(platform-web): show vehicle list service status --- .../apps/web/src/pages/Vehicles.tsx | 26 +++++++++- .../apps/web/src/test/App.test.tsx | 49 +++++++++++++++++++ vehicle-data-platform/docs/product-spec.md | 2 + 3 files changed, 76 insertions(+), 1 deletion(-) diff --git a/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx b/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx index 4c51ed61..4659a463 100644 --- a/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx @@ -6,6 +6,22 @@ import { DataEmpty } from '../components/DataEmpty'; import { PageHeader } from '../components/PageHeader'; import { StatusTag } from '../components/StatusTag'; +function vehicleServiceStatus(row: VehicleCoverageRow) { + if (row.bindingStatus !== 'bound') { + return { label: '身份未绑定', color: 'orange' as const }; + } + if (row.sourceCount <= 0) { + return { label: '暂无数据来源', color: 'orange' as const }; + } + if (row.onlineSourceCount <= 0) { + return { label: '车辆离线', color: 'red' as const }; + } + if (row.onlineSourceCount < row.sourceCount) { + return { label: '部分来源离线', color: 'orange' as const }; + } + return { label: '服务正常', color: 'green' as const }; +} + export function Vehicles({ onOpenVehicle }: { onOpenVehicle: (vin: string) => void }) { const [rows, setRows] = useState([]); const [loading, setLoading] = useState(true); @@ -37,9 +53,17 @@ export function Vehicles({ onOpenVehicle }: { onOpenVehicle: (vin: string) => vo { title: '车牌', dataIndex: 'plate', width: 120 }, { title: '手机号', dataIndex: 'phone', width: 130 }, { title: 'OEM', dataIndex: 'oem', width: 120 }, + { + title: '车辆服务状态', + width: 130, + render: (_: unknown, row: VehicleCoverageRow) => { + const status = vehicleServiceStatus(row); + return {status.label}; + } + }, { title: '来源', - width: 260, + width: 240, render: (_: unknown, row: VehicleCoverageRow) => ( {row.protocols.map((protocol) => {protocol})} 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 fc9e6f22..22e36cd5 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -245,6 +245,55 @@ test('keeps primary protocol when opening vehicle service from realtime vehicles }); }); +test('shows vehicle service status in vehicle list', async () => { + window.history.replaceState(null, '', '/#/vehicles'); + vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { + const path = String(input); + if (path.includes('/api/vehicles/coverage')) { + return { + ok: true, + json: async () => ({ + data: { + items: [{ + vin: 'VIN-DEGRADED-001', + plate: '粤AG18312', + phone: '13307795425', + oem: 'G7s', + protocols: ['GB32960', 'JT808'], + sourceCount: 2, + onlineSourceCount: 1, + online: true, + lastSeen: '2026-07-03 20:12:10', + bindingStatus: 'bound' + }], + 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('VIN-DEGRADED-001')).toBeInTheDocument(); + expect(screen.getByText('车辆服务状态')).toBeInTheDocument(); + expect(screen.getByText('部分来源离线')).toBeInTheDocument(); +}); + 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) => { diff --git a/vehicle-data-platform/docs/product-spec.md b/vehicle-data-platform/docs/product-spec.md index 5f9c6a26..51dd513c 100644 --- a/vehicle-data-platform/docs/product-spec.md +++ b/vehicle-data-platform/docs/product-spec.md @@ -20,6 +20,8 @@ The product model is vehicle-first. GB32960, JT808, and Yutong MQTT are ingestio Vehicle service coverage is always shown at VIN level. A vehicle may have one or more ingestion sources, but upper product views should summarize source count, online source count, last seen time, and binding status before drilling into protocol-specific diagnostics. +Vehicle lists should expose a vehicle-level service status derived from binding and source coverage: identity required, no data source, offline, degraded, or healthy. Protocol tags remain attribution and troubleshooting context, not the primary status. + ## Interaction Rules - Tables are the default data surface.