diff --git a/vehicle-data-platform/apps/web/src/pages/Quality.tsx b/vehicle-data-platform/apps/web/src/pages/Quality.tsx index 0f8fc8a6..1a1d19f5 100644 --- a/vehicle-data-platform/apps/web/src/pages/Quality.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Quality.tsx @@ -139,6 +139,9 @@ export function Quality({ } applyFilters({ ...filters, issueType: primaryIssueType }); }; + const drillProtocol = (protocol: string) => { + applyFilters({ ...filters, protocol }); + }; return (
@@ -184,6 +187,24 @@ export function Quality({ + + qualityProtocolLabel(row.name) }, + { title: '问题数', dataIndex: 'count', width: 120 }, + { + title: '操作', + width: 140, + render: (_: unknown, row: { name: string }) => ( + + ) + } + ]} + /> +
{ const nextFilters = values as Record; 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 269d8bf2..28f2a924 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -1587,6 +1587,76 @@ test('drills into quality issues by issue type', async () => { expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/quality/summary?issueType=NO_SOURCE'), undefined); }); +test('drills into quality issues by protocol bucket', async () => { + window.history.replaceState(null, '', '/#/quality'); + const fetchMock = 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: 2, + issueRecordCount: 3, + errorCount: 1, + warningCount: 2, + protocols: [ + { name: 'JT808', count: 2 }, + { name: 'VEHICLE_SERVICE', count: 1 } + ], + issueTypes: [{ name: 'VIN_MISSING', count: 2 }] + }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + if (path.includes('/api/quality/issues')) { + return { + ok: true, + json: async () => ({ + data: { + items: [], + total: 3, + 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(); + + fireEvent.click(await screen.findByRole('button', { name: '查看 JT808 质量问题' })); + + await waitFor(() => { + expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/quality/issues?protocol=JT808&limit=20&offset=0'), undefined); + }); + expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/quality/summary?protocol=JT808'), undefined); + expect(window.location.hash).toBe('#/quality?protocol=JT808'); +}); + test('applies shareable quality filters from hash', async () => { window.history.replaceState(null, '', '/#/quality?keyword=%E7%B2%A4A&protocol=VEHICLE_SERVICE&issueType=NO_SOURCE'); const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {