diff --git a/vehicle-data-platform/apps/web/src/App.tsx b/vehicle-data-platform/apps/web/src/App.tsx index 645229e0..adb2543c 100644 --- a/vehicle-data-platform/apps/web/src/App.tsx +++ b/vehicle-data-platform/apps/web/src/App.tsx @@ -364,7 +364,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 08b93819..5b139e66 100644 --- a/vehicle-data-platform/apps/web/src/pages/Quality.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Quality.tsx @@ -121,6 +121,18 @@ function qualityShareURL() { return `${window.location.origin}${window.location.pathname}${window.location.hash}`; } +function issueEvidenceDate(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 = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value.trim()); + 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 qualityActionRecommendation(row: QualityIssueRow) { if (row.issueType === 'NO_SOURCE') { return { @@ -251,11 +263,13 @@ async function copyQualityShareURL() { export function Quality({ onOpenVehicle, + onOpenHistory, onHealthLoaded, onFiltersChange, initialFilters = {} }: { onOpenVehicle: (vin: string, protocol?: string) => void; + onOpenHistory?: (filters: Record) => void; onHealthLoaded?: (health: OpsHealth) => void; onFiltersChange?: (filters: Record) => void; initialFilters?: Record; @@ -338,6 +352,17 @@ export function Quality({ filters.protocol ? `数据来源:${qualityProtocolLabel(filters.protocol)}` : '', filters.issueType ? `问题类型:${qualityIssueLabel(filters.issueType)}` : '' ].filter(Boolean); + const openIssueHistory = (row: QualityIssueRow) => { + const lookup = qualityIssueVehicleLookup(row); + const dateFrom = issueEvidenceDate(row.lastSeen); + const dateTo = nextDate(dateFrom); + onOpenHistory?.({ + keyword: lookup.key, + protocol: row.protocol, + ...(dateFrom ? { dateFrom } : {}), + ...(dateTo ? { dateTo } : {}) + }); + }; return (
@@ -573,13 +598,14 @@ export function Quality({ }, { title: '操作', - width: 250, + width: 330, render: (_: unknown, row: QualityIssueRow) => { const lookup = qualityIssueVehicleLookup(row); return ( + 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 3ba34ca3..6bbf373a 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -2513,6 +2513,82 @@ test('opens vehicle service from quality issue with issue source evidence', asyn }); }); +test('opens same-day history evidence 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: 'LINK_GAP', 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-HISTORY', + plate: '粤A告警证', + phone: '13307795425', + sourceEndpoint: '115.231.168.135:43625', + protocol: 'JT808', + issueType: 'LINK_GAP', + severity: 'warning', + lastSeen: '2026-07-03 20:12:10', + detail: 'JT808 来源存在上报间断' + }], + 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-HISTORY')).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: '核对历史' })); + + await waitFor(() => { + expect(window.location.hash).toBe('#/history?keyword=VIN-QUALITY-HISTORY&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) => {