diff --git a/vehicle-data-platform/apps/web/src/App.tsx b/vehicle-data-platform/apps/web/src/App.tsx index 2a1872a1..4a6440f0 100644 --- a/vehicle-data-platform/apps/web/src/App.tsx +++ b/vehicle-data-platform/apps/web/src/App.tsx @@ -20,7 +20,9 @@ export default function App() { const [analysisVin, setAnalysisVin] = useState(initialVehicleKey); const [activeProtocol, setActiveProtocol] = useState(initialRoute.protocol ?? ''); const [historyTab, setHistoryTab] = useState(initialRoute.filters?.tab ?? 'location'); - const [vehicleFilters, setVehicleFilters] = useState>(initialRoute.filters ?? {}); + const [vehicleFilters, setVehicleFilters] = useState>( + initialRoute.page === 'vehicles' ? vehicleFiltersFromRoute(initialRoute) : {} + ); const [qualityFilters, setQualityFilters] = useState>( initialRoute.page === 'quality' ? qualityFiltersFromRoute(initialRoute) : {} ); @@ -89,7 +91,7 @@ export default function App() { } } if (route.page === 'vehicles') { - setVehicleFilters(route.filters ?? {}); + setVehicleFilters(vehicleFiltersFromRoute(route)); } if (route.page === 'quality') { setQualityFilters(qualityFiltersFromRoute(route)); @@ -122,18 +124,27 @@ export default function App() { replaceQualityHash(qualityFilters); return; } - replaceHash(page, undefined, undefined, page === 'vehicles' ? vehicleFilters : undefined); + if (page === 'vehicles') { + replaceVehicleHash(vehicleFilters); + return; + } + replaceHash(page); }; const openVehicles = (filters: Record = {}) => { setVehicleFilters(filters); setActivePage('vehicles'); - replaceHash('vehicles', undefined, undefined, filters); + replaceVehicleHash(filters); }; const updateVehicleFilters = (filters: Record = {}) => { setVehicleFilters(filters); - replaceHash('vehicles', undefined, undefined, filters); + replaceVehicleHash(filters); + }; + + const replaceVehicleHash = (filters: Record = {}) => { + const { keyword, protocol, ...restFilters } = filters; + replaceHash('vehicles', keyword, protocol, restFilters); }; const replaceQualityHash = (filters: Record = {}) => { @@ -247,6 +258,14 @@ function qualityFiltersFromRoute(route: ReturnType): Record }; } +function vehicleFiltersFromRoute(route: ReturnType): Record { + return { + ...(route.keyword ? { keyword: route.keyword } : {}), + ...(route.protocol ? { protocol: route.protocol } : {}), + ...(route.filters ?? {}) + }; +} + function serviceStatusFromOverview(overview: VehicleServiceOverview): VehicleServiceStatus { const sourceCount = overview.sourceCount; const onlineSourceCount = overview.onlineSourceCount; diff --git a/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx b/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx index b2d65883..b61e851c 100644 --- a/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx @@ -47,7 +47,7 @@ export function Vehicles({ onFiltersChange, initialFilters = {} }: { - onOpenVehicle: (vin: string) => void; + onOpenVehicle: (vin: string, protocol?: string) => void; onFiltersChange?: (filters: Record) => void; initialFilters?: Record; }) { @@ -158,7 +158,7 @@ export function Vehicles({ { title: '在线', width: 90, render: (_: unknown, row: VehicleCoverageRow) => }, { title: '最后在线', dataIndex: 'lastSeen', width: 170 }, { title: '绑定', width: 90, render: (_: unknown, row: VehicleCoverageRow) => {row.bindingStatus === 'bound' ? '已绑定' : '未绑定'} }, - { title: '操作', width: 110, render: (_: unknown, row: VehicleCoverageRow) => } + { title: '操作', width: 110, render: (_: unknown, row: VehicleCoverageRow) => } ], [filters, onOpenVehicle] ); 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 acd5004e..4ee06974 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -2447,6 +2447,80 @@ test('shows vehicle service status in vehicle list', async () => { expect(screen.getByText('部分来源离线')).toBeInTheDocument(); }); +test('opens vehicle service from source-filtered vehicle list with source evidence', async () => { + window.history.replaceState(null, '', '/#/vehicles?protocol=JT808'); + 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-JT808', + plate: '粤A列表1', + phone: '13307795425', + oem: 'G7s', + protocols: ['JT808'], + sourceCount: 1, + 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; + } + if (path.includes('/api/vehicle-service/overview')) { + return { + ok: true, + json: async () => ({ + data: { + vin: 'VIN-LIST-JT808', + plate: '粤A列表1', + sourceCount: 1, + onlineSourceCount: 1, + coverageStatus: 'online', + primaryProtocol: 'JT808', + 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: 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; + }); + + render(); + + expect(await screen.findByText('VIN-LIST-JT808')).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: '车辆服务' })); + + await waitFor(() => { + expect(window.location.hash).toBe('#/detail?keyword=VIN-LIST-JT808&protocol=JT808'); + }); +}); + test('filters vehicle list by service status', async () => { window.history.replaceState(null, '', '/#/vehicles'); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {