From fb6d8c323abba1dc05d0e9c131dca2b2a58d7df9 Mon Sep 17 00:00:00 2001 From: lingniu Date: Sat, 4 Jul 2026 11:57:55 +0800 Subject: [PATCH] feat(platform): link quality issues to mileage evidence --- vehicle-data-platform/apps/web/src/App.tsx | 2 +- .../apps/web/src/pages/Quality.tsx | 14 +++ .../apps/web/src/test/App.test.tsx | 110 ++++++++++++++++++ 3 files changed, 125 insertions(+), 1 deletion(-) diff --git a/vehicle-data-platform/apps/web/src/App.tsx b/vehicle-data-platform/apps/web/src/App.tsx index da9d424a..26d9525f 100644 --- a/vehicle-data-platform/apps/web/src/App.tsx +++ b/vehicle-data-platform/apps/web/src/App.tsx @@ -379,7 +379,7 @@ export default function App() { detail: , history: , mileage: , - quality: setLinkIssueCount(health.linkHealth.filter((item) => item.status !== 'ok').length)} onFiltersChange={updateQualityFilters} initialFilters={qualityFilters} /> + quality: setLinkIssueCount(health.linkHealth.filter((item) => item.status !== 'ok').length)} onFiltersChange={updateQualityFilters} initialFilters={qualityFilters} /> }; return ( diff --git a/vehicle-data-platform/apps/web/src/pages/Quality.tsx b/vehicle-data-platform/apps/web/src/pages/Quality.tsx index 5b139e66..6fde9788 100644 --- a/vehicle-data-platform/apps/web/src/pages/Quality.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Quality.tsx @@ -264,12 +264,14 @@ async function copyQualityShareURL() { export function Quality({ onOpenVehicle, onOpenHistory, + onOpenMileage, onHealthLoaded, onFiltersChange, initialFilters = {} }: { onOpenVehicle: (vin: string, protocol?: string) => void; onOpenHistory?: (filters: Record) => void; + onOpenMileage?: (filters: Record) => void; onHealthLoaded?: (health: OpsHealth) => void; onFiltersChange?: (filters: Record) => void; initialFilters?: Record; @@ -363,6 +365,17 @@ export function Quality({ ...(dateTo ? { dateTo } : {}) }); }; + const openIssueMileage = (row: QualityIssueRow) => { + const lookup = qualityIssueVehicleLookup(row); + const dateFrom = issueEvidenceDate(row.lastSeen); + const dateTo = nextDate(dateFrom); + onOpenMileage?.({ + keyword: lookup.key, + protocol: row.protocol, + ...(dateFrom ? { dateFrom } : {}), + ...(dateTo ? { dateTo } : {}) + }); + }; return (
@@ -606,6 +619,7 @@ export function Quality({ + 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 2c8fb825..88a7aa19 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -2589,6 +2589,116 @@ test('opens same-day history evidence from quality issue row', async () => { }); }); +test('opens same-day mileage statistics from quality issue row', async () => { + window.history.replaceState(null, '', '/#/quality'); + 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/quality/summary')) { + return { + ok: true, + json: async () => ({ + data: { + issueVehicleCount: 1, + issueRecordCount: 1, + errorCount: 0, + warningCount: 1, + protocols: [{ name: 'JT808', count: 1 }], + issueTypes: [{ name: 'FIELD_MISSING', count: 1 }] + }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + if (path.includes('/api/quality/issues')) { + return { + ok: true, + json: async () => ({ + data: { + items: [{ + vin: 'VIN-QUALITY-MILEAGE', + plate: '粤A告警里', + phone: '13307795425', + sourceEndpoint: '115.231.168.135:43625', + protocol: 'JT808', + issueType: 'FIELD_MISSING', + severity: 'warning', + lastSeen: '2026-07-03 20:12:10', + detail: 'JT808 位置帧缺少总里程字段' + }], + total: 1, + limit: 20, + 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-QUALITY-MILEAGE', + plate: '粤A告警里', + source: 'JT808', + date: '2026-07-03', + startMileageKm: 200, + endMileageKm: 218.8, + dailyMileageKm: 18.8, + anomalySeverity: 'warning' + }], + total: 1, + limit: 20, + offset: 0 + }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + return { + ok: true, + json: async () => ({ + data: { items: [], total: 0, limit: 20, offset: 0 }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + }); + + render(); + + expect(await screen.findByText('VIN-QUALITY-MILEAGE')).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: '核对里程' })); + + await waitFor(() => { + expect(window.location.hash).toBe('#/mileage?keyword=VIN-QUALITY-MILEAGE&protocol=JT808&dateFrom=2026-07-03&dateTo=2026-07-04'); + }); +}); + test('drills into quality issues by issue type', async () => { window.history.replaceState(null, '', '/#/quality'); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {