diff --git a/vehicle-data-platform/apps/web/src/App.tsx b/vehicle-data-platform/apps/web/src/App.tsx index 6b5c8a30..fb353157 100644 --- a/vehicle-data-platform/apps/web/src/App.tsx +++ b/vehicle-data-platform/apps/web/src/App.tsx @@ -117,6 +117,11 @@ export default function App() { replaceHash('vehicles', undefined, undefined, filters); }; + const updateVehicleFilters = (filters: Record = {}) => { + setVehicleFilters(filters); + replaceHash('vehicles', undefined, undefined, filters); + }; + const openVehicle = async (keyword: string, protocol?: string) => { const lookupKey = keyword.trim(); const nextProtocol = protocol?.trim() ?? ''; @@ -176,7 +181,7 @@ export default function App() { const pages: Record = { dashboard: navigatePage('quality')} onOpenVehicles={openVehicles} />, - vehicles: , + vehicles: , realtime: , detail: , history: , diff --git a/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx b/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx index ec975853..8a9aaf5c 100644 --- a/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Vehicles.tsx @@ -28,19 +28,28 @@ function vehicleServiceStatus(row: VehicleCoverageRow) { return { label: '服务正常', color: 'green' as const }; } -export function Vehicles({ onOpenVehicle, initialFilters = {} }: { onOpenVehicle: (vin: string) => void; initialFilters?: Record }) { +export function Vehicles({ + onOpenVehicle, + onFiltersChange, + initialFilters = {} +}: { + onOpenVehicle: (vin: string) => void; + onFiltersChange?: (filters: Record) => void; + initialFilters?: Record; +}) { const [rows, setRows] = useState([]); const [summary, setSummary] = useState(null); const [loading, setLoading] = useState(true); const [filters, setFilters] = useState>(initialFilters); const [pagination, setPagination] = useState({ currentPage: 1, pageSize: 20, total: 0 }); - const resultSummary = useMemo(() => { - return [ - { label: '过滤车辆', value: (summary?.totalVehicles ?? pagination.total).toLocaleString() }, - { label: '在线车辆', value: (summary?.onlineVehicles ?? 0).toLocaleString() }, - { label: '多源车辆', value: (summary?.multiSourceVehicles ?? 0).toLocaleString() }, - { label: '待绑定', value: (summary?.unboundVehicles ?? 0).toLocaleString() } + const resultSummary = useMemo }>>(() => { + const items: Array<{ label: string; value: string; filters: Record }> = [ + { label: '过滤车辆', value: (summary?.totalVehicles ?? pagination.total).toLocaleString(), filters: {} }, + { label: '在线车辆', value: (summary?.onlineVehicles ?? 0).toLocaleString(), filters: { online: 'online' } }, + { label: '多源车辆', value: (summary?.multiSourceVehicles ?? 0).toLocaleString(), filters: { coverage: 'multi' } }, + { label: '待绑定', value: (summary?.unboundVehicles ?? 0).toLocaleString(), filters: { bindingStatus: 'unbound' } } ]; + return items; }, [pagination.total, summary]); const load = (values: Record = filters, page = pagination.currentPage, pageSize = pagination.pageSize) => { @@ -65,6 +74,12 @@ export function Vehicles({ onOpenVehicle, initialFilters = {} }: { onOpenVehicle .finally(() => setLoading(false)); }; + const applyFilters = (nextFilters: Record) => { + setFilters(nextFilters); + onFiltersChange?.(nextFilters); + load(nextFilters, 1, pagination.pageSize); + }; + useEffect(() => { setFilters(initialFilters); load(initialFilters, 1, pagination.pageSize); @@ -108,8 +123,7 @@ export function Vehicles({ onOpenVehicle, initialFilters = {} }: { onOpenVehicle
{ const nextFilters = values as Record; - setFilters(nextFilters); - load(nextFilters, 1, pagination.pageSize); + applyFilters(nextFilters); }}> @@ -138,8 +152,7 @@ export function Vehicles({ onOpenVehicle, initialFilters = {} }: { onOpenVehicle @@ -147,10 +160,16 @@ export function Vehicles({ onOpenVehicle, initialFilters = {} }: { onOpenVehicle
{resultSummary.map((item) => ( -
+
+ ))}
diff --git a/vehicle-data-platform/apps/web/src/styles/global.css b/vehicle-data-platform/apps/web/src/styles/global.css index a722548d..7be17f1c 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -213,6 +213,20 @@ body { background: #fbfcff; } +.vp-result-summary-button { + width: 100%; + text-align: left; + font: inherit; + cursor: pointer; +} + +.vp-result-summary-button:hover, +.vp-result-summary-button:focus-visible { + border-color: rgba(22, 100, 255, 0.42); + background: #f5f9ff; + outline: none; +} + .vp-result-summary-value { color: var(--vp-text); font-size: 22px; 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 98247dc0..90aee20b 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -262,6 +262,72 @@ test('shows vehicle service result summary on vehicle list filters', async () => expect(screen.getByText('VIN-MULTI-001')).toBeInTheDocument(); }); +test('filters vehicle list from result summary actions', async () => { + window.history.replaceState(null, '', '/#/vehicles'); + const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { + const path = String(input); + 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; + } + if (path.includes('/api/vehicles/coverage/summary')) { + return { + ok: true, + json: async () => ({ + data: { + totalVehicles: path.includes('online=online') ? 73 : 181, + onlineVehicles: 73, + singleSourceVehicles: 108, + multiSourceVehicles: 73, + unboundVehicles: 9 + }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + if (path.includes('/api/vehicles/coverage')) { + return { + ok: true, + json: async () => ({ + data: { + items: [], + total: path.includes('online=online') ? 73 : 181, + limit: 20, + offset: 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(); + + fireEvent.click(await screen.findByRole('button', { name: /在线车辆 73/ })); + + await waitFor(() => { + expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/vehicles/coverage?limit=20&offset=0&online=online'), undefined); + }); + expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/vehicles/coverage/summary?online=online'), undefined); + expect(window.location.hash).toBe('#/vehicles?online=online'); +}); + test('shows resolved vehicle service status after topbar search', async () => { window.history.replaceState(null, '', '/#/dashboard'); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {