diff --git a/vehicle-data-platform/apps/web/src/App.tsx b/vehicle-data-platform/apps/web/src/App.tsx index f262c2ee..af02d016 100644 --- a/vehicle-data-platform/apps/web/src/App.tsx +++ b/vehicle-data-platform/apps/web/src/App.tsx @@ -454,7 +454,7 @@ export default function App() { const pages: Record = { dashboard: , - vehicles: , + vehicles: , map: , realtime: , detail: , diff --git a/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx b/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx index 6da6e99e..c1dc1e28 100644 --- a/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx @@ -31,6 +31,17 @@ function sourceEvidenceText(row: VehicleCoverageRow) { return `${row.onlineSourceCount}/${row.sourceCount} 来源在线`; } +function primaryRowProtocol(row: VehicleCoverageRow, fallbackProtocol?: string) { + return (fallbackProtocol || row.sourceStatus?.find((source) => source.online)?.protocol || row.protocols?.[0] || '').trim(); +} + +function rowWorkflowFilters(row: VehicleCoverageRow, fallbackProtocol?: string) { + return { + keyword: row.vin, + ...(primaryRowProtocol(row, fallbackProtocol) ? { protocol: primaryRowProtocol(row, fallbackProtocol) } : {}) + }; +} + function vehicleArchiveCompleteness(row: VehicleCoverageRow) { const fields = [row.vin, row.plate, row.phone, row.oem]; const completed = fields.filter((item) => Boolean(String(item ?? '').trim())).length; @@ -238,11 +249,15 @@ async function copyVehicleShareURL() { export function Vehicles({ onOpenVehicle, onOpenQuality, + onOpenRealtime, + onOpenHistory, onFiltersChange, initialFilters = {} }: { onOpenVehicle: (vin: string, protocol?: string) => void; onOpenQuality?: (filters: Record) => void; + onOpenRealtime?: (filters: Record) => void; + onOpenHistory?: (filters: Record) => void; onFiltersChange?: (filters: Record) => void; initialFilters?: Record; }) { @@ -472,16 +487,18 @@ export function Vehicles({ { title: '绑定', width: 90, render: (_: unknown, row: VehicleCoverageRow) => {row.bindingStatus === 'bound' ? '已绑定' : '未绑定'} }, { title: '操作', - width: 190, + width: 300, render: (_: unknown, row: VehicleCoverageRow) => ( - - - + + + + + ) } ], - [filters, onOpenQuality, onOpenVehicle] + [filters, onOpenHistory, onOpenQuality, onOpenRealtime, onOpenVehicle] ); return ( 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 92deeb19..0f9ab427 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -9327,13 +9327,81 @@ test('opens vehicle service from source-filtered vehicle list with source eviden render(); expect(await screen.findByText('VIN-LIST-JT808')).toBeInTheDocument(); - fireEvent.click(screen.getByRole('button', { name: '车辆服务' })); + fireEvent.click(screen.getByRole('button', { name: '服务' })); await waitFor(() => { expect(window.location.hash).toBe('#/detail?keyword=VIN-LIST-JT808&protocol=JT808'); }); }); +test('opens realtime and trajectory from vehicle center row', async () => { + 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-LIST-WORKFLOW', + plate: '粤A列表流', + phone: '13307795425', + oem: 'G7s', + protocols: ['GB32960', 'JT808'], + missingProtocols: [], + sourceStatus: [ + { protocol: 'GB32960', online: false, lastSeen: '2026-07-03 18:12:10', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true }, + { protocol: 'JT808', online: true, lastSeen: '2026-07-03 20:12:10', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true } + ], + 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: 20, offset: 0 }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + }); + + window.history.replaceState(null, '', '/#/vehicles'); + render(); + + expect(await screen.findByText('VIN-LIST-WORKFLOW')).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: '实时' })); + + await waitFor(() => { + expect(window.location.hash).toBe('#/realtime?keyword=VIN-LIST-WORKFLOW&protocol=JT808'); + }); + + cleanup(); + window.history.replaceState(null, '', '/#/vehicles'); + render(); + + expect(await screen.findByText('VIN-LIST-WORKFLOW')).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: '轨迹' })); + + await waitFor(() => { + expect(window.location.hash).toBe('#/history?keyword=VIN-LIST-WORKFLOW&protocol=JT808'); + }); +}); + test('opens quality issues from source-filtered vehicle list with source evidence', async () => { window.history.replaceState(null, '', '/#/vehicles?protocol=JT808'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { @@ -9429,7 +9497,7 @@ test('opens quality issues from source-filtered vehicle list with source evidenc render(); expect(await screen.findByText('VIN-LIST-QUALITY')).toBeInTheDocument(); - fireEvent.click(screen.getByRole('button', { name: '质量问题' })); + fireEvent.click(screen.getByRole('button', { name: '告警' })); await waitFor(() => { expect(window.location.hash).toBe('#/alert-events?keyword=VIN-LIST-QUALITY&protocol=JT808');