diff --git a/vehicle-data-platform/apps/web/src/App.tsx b/vehicle-data-platform/apps/web/src/App.tsx index 20102e62..2a1872a1 100644 --- a/vehicle-data-platform/apps/web/src/App.tsx +++ b/vehicle-data-platform/apps/web/src/App.tsx @@ -19,6 +19,7 @@ export default function App() { const [activeVin, setActiveVin] = useState(initialVehicleKey); 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 [qualityFilters, setQualityFilters] = useState>( initialRoute.page === 'quality' ? qualityFiltersFromRoute(initialRoute) : {} @@ -94,6 +95,7 @@ export default function App() { setQualityFilters(qualityFiltersFromRoute(route)); } setActiveProtocol(route.protocol ?? ''); + setHistoryTab(route.filters?.tab ?? 'location'); }; window.addEventListener('hashchange', applyHashRoute); return () => window.removeEventListener('hashchange', applyHashRoute); @@ -179,10 +181,24 @@ export default function App() { } setAnalysisVin(nextVin); setActiveProtocol(nextProtocol); + setHistoryTab('location'); setActivePage('history'); replaceHash('history', nextVin, nextProtocol); }; + const openRawForVehicle = (vin: string, protocol?: string) => { + const nextVin = vin.trim(); + const nextProtocol = protocol?.trim() ?? activeProtocol; + if (!nextVin) { + return; + } + setAnalysisVin(nextVin); + setActiveProtocol(nextProtocol); + setHistoryTab('raw'); + setActivePage('history'); + replaceHash('history', nextVin, nextProtocol, { tab: 'raw' }); + }; + const openMileageForVehicle = (vin: string, protocol?: string) => { const nextVin = vin.trim(); const nextProtocol = protocol?.trim() ?? activeProtocol; @@ -210,8 +226,8 @@ export default function App() { dashboard: navigatePage('quality')} onOpenVehicles={openVehicles} />, vehicles: , realtime: , - detail: , - history: , + detail: , + history: , mileage: , quality: setLinkIssueCount(health.linkHealth.filter((item) => item.status !== 'ok').length)} onFiltersChange={updateQualityFilters} initialFilters={qualityFilters} /> }; diff --git a/vehicle-data-platform/apps/web/src/domain/appRoute.test.ts b/vehicle-data-platform/apps/web/src/domain/appRoute.test.ts index 676669a1..221e1e82 100644 --- a/vehicle-data-platform/apps/web/src/domain/appRoute.test.ts +++ b/vehicle-data-platform/apps/web/src/domain/appRoute.test.ts @@ -11,6 +11,17 @@ describe('parseAppHash', () => { }); }); + test('parses history tab from hash query', () => { + expect(parseAppHash('#/history?keyword=VIN001&protocol=JT808&tab=raw')).toEqual({ + page: 'history', + keyword: 'VIN001', + protocol: 'JT808', + filters: { + tab: 'raw' + } + }); + }); + test('parses vehicle list filters from hash query', () => { expect(parseAppHash('#/vehicles?coverage=multi&serviceStatus=degraded&online=online&bindingStatus=bound&missingProtocol=YUTONG_MQTT')).toEqual({ page: 'vehicles', @@ -45,6 +56,10 @@ describe('buildAppHash', () => { expect(buildAppHash({ page: 'history', keyword: '粤AG18312', protocol: 'GB32960' })).toBe('#/history?keyword=%E7%B2%A4AG18312&protocol=GB32960'); }); + test('builds shareable raw history hash', () => { + expect(buildAppHash({ page: 'history', keyword: 'VIN001', protocol: 'JT808', filters: { tab: 'raw' } })).toBe('#/history?keyword=VIN001&protocol=JT808&tab=raw'); + }); + test('builds shareable vehicle list hash with filters', () => { expect(buildAppHash({ page: 'vehicles', filters: { coverage: 'multi', serviceStatus: 'degraded', missingProtocol: 'YUTONG_MQTT' } })).toBe('#/vehicles?coverage=multi&serviceStatus=degraded&missingProtocol=YUTONG_MQTT'); }); diff --git a/vehicle-data-platform/apps/web/src/domain/appRoute.ts b/vehicle-data-platform/apps/web/src/domain/appRoute.ts index e2638d4d..80993c3b 100644 --- a/vehicle-data-platform/apps/web/src/domain/appRoute.ts +++ b/vehicle-data-platform/apps/web/src/domain/appRoute.ts @@ -9,7 +9,7 @@ export type AppRoute = { filters?: Record; }; -const filterKeys = ['coverage', 'serviceStatus', 'online', 'bindingStatus', 'missingProtocol', 'issueType'] as const; +const filterKeys = ['coverage', 'serviceStatus', 'online', 'bindingStatus', 'missingProtocol', 'issueType', 'tab'] as const; export function parseAppHash(hash: string): AppRoute { const normalized = hash.trim().replace(/^#\/?/, ''); diff --git a/vehicle-data-platform/apps/web/src/pages/History.tsx b/vehicle-data-platform/apps/web/src/pages/History.tsx index 237d7732..d68e660c 100644 --- a/vehicle-data-platform/apps/web/src/pages/History.tsx +++ b/vehicle-data-platform/apps/web/src/pages/History.tsx @@ -33,9 +33,26 @@ function splitFields(value?: string) { .filter(Boolean); } -export function History({ initialVin, initialProtocol, onOpenVehicle }: { initialVin: string; initialProtocol?: string; onOpenVehicle: (vin: string, protocol?: string) => void }) { +type HistoryTabKey = 'location' | 'raw'; + +function normalizeHistoryTab(value?: string): HistoryTabKey { + return value === 'raw' ? 'raw' : 'location'; +} + +export function History({ + initialVin, + initialProtocol, + initialTab, + onOpenVehicle +}: { + initialVin: string; + initialProtocol?: string; + initialTab?: string; + onOpenVehicle: (vin: string, protocol?: string) => void; +}) { const initialFilters = { ...defaultFilters, keyword: initialVin || defaultFilters.keyword, protocol: initialProtocol }; const [filters, setFilters] = useState(initialFilters); + const [activeTab, setActiveTab] = useState(normalizeHistoryTab(initialTab)); const [locations, setLocations] = useState>(defaultPage); const [rawFrames, setRawFrames] = useState>(defaultPage); const [selectedRaw, setSelectedRaw] = useState(null); @@ -113,9 +130,10 @@ export function History({ initialVin, initialProtocol, onOpenVehicle }: { initia useEffect(() => { const nextFilters = { ...defaultFilters, keyword: initialVin || defaultFilters.keyword, protocol: initialProtocol }; setFilters(nextFilters); + setActiveTab(normalizeHistoryTab(initialTab)); loadLocations(nextFilters, 1, locationPagination.pageSize); loadRawFrames(nextFilters, 1, rawPagination.pageSize); - }, [initialVin, initialProtocol]); + }, [initialVin, initialProtocol, initialTab]); const rawFieldCount = useMemo(() => Object.keys(selectedRaw?.parsedFields ?? {}).length, [selectedRaw]); const currentVehicleKeyword = filters.keyword?.trim() ?? ''; @@ -164,7 +182,7 @@ export function History({ initialVin, initialProtocol, onOpenVehicle }: { initia - + setActiveTab(normalizeHistoryTab(String(key)))}> void; + onOpenRaw: (vin: string, protocol?: string) => void; onOpenMileage: (vin: string, protocol?: string) => void; onOpenVehicles?: (filters?: Record) => void; onQueryChange?: (keyword: string, protocol?: string) => void; @@ -384,6 +386,15 @@ export function VehicleDetail({ > 查看 {row.protocol} 历史 +