diff --git a/vehicle-data-platform/apps/web/src/App.tsx b/vehicle-data-platform/apps/web/src/App.tsx index b5372ca0..2c1aba37 100644 --- a/vehicle-data-platform/apps/web/src/App.tsx +++ b/vehicle-data-platform/apps/web/src/App.tsx @@ -138,8 +138,8 @@ export default function App() { vehicles: , realtime: , detail: , - history: , - mileage: , + history: , + mileage: , quality: setLinkIssueCount(health.linkHealth.filter((item) => item.status !== 'ok').length)} /> }; diff --git a/vehicle-data-platform/apps/web/src/pages/History.tsx b/vehicle-data-platform/apps/web/src/pages/History.tsx index aaed864a..46f72a3e 100644 --- a/vehicle-data-platform/apps/web/src/pages/History.tsx +++ b/vehicle-data-platform/apps/web/src/pages/History.tsx @@ -33,8 +33,9 @@ function splitFields(value?: string) { .filter(Boolean); } -export function History({ initialVin, onOpenVehicle }: { initialVin: string; onOpenVehicle: (vin: string) => void }) { - const [filters, setFilters] = useState({ ...defaultFilters, keyword: initialVin || defaultFilters.keyword }); +export function History({ initialVin, initialProtocol, onOpenVehicle }: { initialVin: string; initialProtocol?: string; onOpenVehicle: (vin: string) => void }) { + const initialFilters = { ...defaultFilters, keyword: initialVin || defaultFilters.keyword, protocol: initialProtocol }; + const [filters, setFilters] = useState(initialFilters); const [locations, setLocations] = useState>(defaultPage); const [rawFrames, setRawFrames] = useState>(defaultPage); const [selectedRaw, setSelectedRaw] = useState(null); @@ -103,18 +104,18 @@ export function History({ initialVin, onOpenVehicle }: { initialVin: string; onO }; const reset = () => { - const nextFilters = { ...defaultFilters, keyword: initialVin || defaultFilters.keyword }; + const nextFilters = { ...defaultFilters, keyword: initialVin || defaultFilters.keyword, protocol: initialProtocol }; setFilters(nextFilters); loadLocations(nextFilters, 1, locationPagination.pageSize); loadRawFrames(nextFilters, 1, rawPagination.pageSize); }; useEffect(() => { - const nextFilters = { ...defaultFilters, keyword: initialVin || defaultFilters.keyword }; + const nextFilters = { ...defaultFilters, keyword: initialVin || defaultFilters.keyword, protocol: initialProtocol }; setFilters(nextFilters); loadLocations(nextFilters, 1, locationPagination.pageSize); loadRawFrames(nextFilters, 1, rawPagination.pageSize); - }, [initialVin]); + }, [initialVin, initialProtocol]); const rawFieldCount = useMemo(() => Object.keys(selectedRaw?.parsedFields ?? {}).length, [selectedRaw]); diff --git a/vehicle-data-platform/apps/web/src/pages/Mileage.tsx b/vehicle-data-platform/apps/web/src/pages/Mileage.tsx index 4f41c11d..8b53b78e 100644 --- a/vehicle-data-platform/apps/web/src/pages/Mileage.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Mileage.tsx @@ -32,12 +32,12 @@ function canOpenVehicle(vin?: string) { return Boolean(value && value !== 'unknown'); } -export function Mileage({ initialVin, onOpenVehicle }: { initialVin: string; onOpenVehicle: (vin: string) => void }) { +export function Mileage({ initialVin, initialProtocol, onOpenVehicle }: { initialVin: string; initialProtocol?: string; onOpenVehicle: (vin: string) => void }) { const [rows, setRows] = useState([]); const [summary, setSummary] = useState(emptySummary); const [loading, setLoading] = useState(true); const [summaryLoading, setSummaryLoading] = useState(true); - const [filters, setFilters] = useState>({ keyword: initialVin }); + const [filters, setFilters] = useState>({ keyword: initialVin, protocol: initialProtocol ?? '' }); const [pagination, setPagination] = useState({ currentPage: 1, pageSize: 20, total: 0 }); const loadSummary = (values: Record = filters) => { @@ -60,11 +60,11 @@ export function Mileage({ initialVin, onOpenVehicle }: { initialVin: string; onO }; useEffect(() => { - const nextFilters = { keyword: initialVin }; + const nextFilters = { keyword: initialVin, protocol: initialProtocol ?? '' }; setFilters(nextFilters); loadSummary(nextFilters); load(nextFilters, 1, pagination.pageSize); - }, [initialVin]); + }, [initialVin, initialProtocol]); return ( @@ -87,7 +87,7 @@ export function Mileage({ initialVin, onOpenVehicle }: { initialVin: string; onO 查询 { - const nextFilters = { keyword: initialVin }; + const nextFilters = { keyword: initialVin, protocol: initialProtocol ?? '' }; setFilters(nextFilters); loadSummary(nextFilters); load(nextFilters, 1, pagination.pageSize); 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 1b5a7d5a..0f949c65 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -1,4 +1,4 @@ -import { render, screen } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; import { afterEach, expect, test, vi } from 'vitest'; import App from '../App'; @@ -51,3 +51,39 @@ test('opens vehicle detail from shareable hash', async () => { expect(await screen.findByDisplayValue('粤AG18312')).toBeInTheDocument(); expect(screen.getAllByText('车辆服务').length).toBeGreaterThanOrEqual(1); }); + +test('applies protocol from shareable history hash to API requests', async () => { + window.history.replaceState(null, '', '/#/history?keyword=%E7%B2%A4AG18312&protocol=JT808'); + const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => { + 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; + } + return { + ok: true, + json: async () => ({ + data: { items: [], total: 0, limit: 10, offset: 0 }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + }); + + render(); + + await waitFor(() => { + expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/history/locations?'), undefined); + }); + expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('protocol=JT808'), undefined); + expect(fetchMock).toHaveBeenCalledWith('/api/history/raw-frames/query', expect.objectContaining({ + method: 'POST', + body: expect.stringContaining('"protocol":"JT808"') + })); +});