diff --git a/vehicle-data-platform/apps/web/src/App.tsx b/vehicle-data-platform/apps/web/src/App.tsx index adb2543c..da9d424a 100644 --- a/vehicle-data-platform/apps/web/src/App.tsx +++ b/vehicle-data-platform/apps/web/src/App.tsx @@ -346,6 +346,21 @@ export default function App() { replaceHash('mileage', nextVin, nextProtocol); }; + const openMileageWithFilters = (filters: Record = {}) => { + const nextFilters = normalizeMileageFilterValues(filters); + const nextVin = nextFilters.keyword?.trim() || analysisVin; + const nextProtocol = nextFilters.protocol?.trim() ?? activeProtocol; + if (!nextVin) { + return; + } + setAnalysisVin(nextVin); + setActiveProtocol(nextProtocol); + setMileageFilters({ ...nextFilters, keyword: nextVin, protocol: nextProtocol }); + setActivePage('mileage'); + const { keyword, protocol, ...restFilters } = nextFilters; + replaceHash('mileage', keyword ?? nextVin, protocol ?? nextProtocol, restFilters); + }; + const updateVehicleDetailQuery = (keyword: string, protocol?: string) => { const nextKeyword = keyword.trim(); const nextProtocol = protocol?.trim() ?? ''; @@ -362,7 +377,7 @@ export default function App() { vehicles: , realtime: , detail: , - history: , + 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/pages/History.tsx b/vehicle-data-platform/apps/web/src/pages/History.tsx index 786b5298..9d21e531 100644 --- a/vehicle-data-platform/apps/web/src/pages/History.tsx +++ b/vehicle-data-platform/apps/web/src/pages/History.tsx @@ -29,6 +29,18 @@ function canOpenVehicle(vin?: string) { return Boolean(value && value !== 'unknown'); } +function dateOnly(value?: string) { + const match = String(value ?? '').match(/^(\d{4})-(\d{2})-(\d{2})/); + return match ? `${match[1]}-${match[2]}-${match[3]}` : ''; +} + +function nextDate(value: string) { + const match = value.match(/^(\d{4})-(\d{2})-(\d{2})$/); + if (!match) return ''; + const date = new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]) + 1)); + return date.toISOString().slice(0, 10); +} + function splitFields(value?: string) { return (value ?? '') .split(/[\n,]/) @@ -105,7 +117,8 @@ export function History({ initialTab, initialFilters = {}, onFiltersChange, - onOpenVehicle + onOpenVehicle, + onOpenMileage }: { initialVin: string; initialProtocol?: string; @@ -113,6 +126,7 @@ export function History({ initialFilters?: Record; onFiltersChange?: (filters: HistoryFilters, tab?: HistoryTabKey) => void; onOpenVehicle: (vin: string, protocol?: string) => void; + onOpenMileage?: (filters: Record) => void; }) { const initialHistoryFilters = mergeInitialFilters(initialVin, initialProtocol, initialFilters); const [filters, setFilters] = useState(initialHistoryFilters); @@ -271,6 +285,15 @@ export function History({ if (!currentPlayback || !canOpenVehicle(currentPlayback.vin)) return; onOpenVehicle(currentPlayback.vin, currentPlayback.protocol); }; + const openLocationMileage = (row: HistoryLocationRow) => { + if (!canOpenVehicle(row.vin)) return; + const day = dateOnly(row.deviceTime || row.serverTime || row.lastSeen); + onOpenMileage?.({ + keyword: row.vin, + protocol: row.protocol, + ...(day ? { dateFrom: day, dateTo: nextDate(day) } : {}) + }); + }; return (
@@ -433,9 +456,12 @@ export function History({ { title: '入库时间', dataIndex: 'serverTime', width: 190 }, { title: '操作', - width: 110, + width: 190, render: (_: unknown, row: HistoryLocationRow) => ( - + + + + ) } ]} 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 6bbf373a..2c8fb825 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -4293,6 +4293,101 @@ test('opens vehicle service from history results with row source evidence', asyn }); }); +test('opens same-day mileage statistics from history location row', async () => { + window.history.replaceState(null, '', '/#/history?keyword=VIN-HISTORY-MILEAGE&protocol=JT808'); + 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/history/locations')) { + return { + ok: true, + json: async () => ({ + data: { + items: [{ + vin: 'VIN-HISTORY-MILEAGE', + plate: '粤A历史账', + protocol: 'JT808', + longitude: 113.2, + latitude: 23.1, + speedKmh: 30, + socPercent: 0, + totalMileageKm: 218.8, + lastSeen: '2026-07-03 20:12:10', + deviceTime: '2026-07-03 20:12:10', + serverTime: '2026-07-03 20:12:11' + }], + total: 1, + limit: 10, + offset: 0 + }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + if (path.includes('/api/mileage/summary')) { + return { + ok: true, + json: async () => ({ + data: { vehicleCount: 1, recordCount: 1, sourceCount: 1, totalMileageKm: 18.8, averageMileagePerVin: 18.8 }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + if (path.includes('/api/mileage/daily')) { + return { + ok: true, + json: async () => ({ + data: { + items: [{ + vin: 'VIN-HISTORY-MILEAGE', + plate: '粤A历史账', + source: 'JT808', + date: '2026-07-03', + startMileageKm: 200, + endMileageKm: 218.8, + dailyMileageKm: 18.8, + anomalySeverity: 'normal' + }], + total: 1, + 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(); + + expect(await screen.findByText('VIN-HISTORY-MILEAGE')).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: '核对里程' })); + + await waitFor(() => { + expect(window.location.hash).toBe('#/mileage?keyword=VIN-HISTORY-MILEAGE&protocol=JT808&dateFrom=2026-07-03&dateTo=2026-07-04'); + }); +}); + test('opens vehicle service from raw history rows with row source evidence', async () => { window.history.replaceState(null, '', '/#/history?keyword=%E7%B2%A4AG18312&protocol=GB32960'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {