diff --git a/vehicle-data-platform/apps/web/src/pages/Quality.tsx b/vehicle-data-platform/apps/web/src/pages/Quality.tsx index c12db50e..e4b30e37 100644 --- a/vehicle-data-platform/apps/web/src/pages/Quality.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Quality.tsx @@ -266,6 +266,26 @@ function priorityIssueNotificationText(row: PriorityIssueRow) { ].join('\n'); } +function priorityIssueDigestText(rows: PriorityIssueRow[], summary: QualitySummary) { + const p0Count = rows.filter((row) => row.priority === 'P0').length; + const p1Count = rows.filter((row) => row.priority === 'P1').length; + const lines = rows.map((row, index) => [ + `${index + 1}. [${row.priority}] ${row.vehicleLabel}`, + ` 来源:${qualityProtocolLabel(row.protocol)} / 问题:${qualityIssueLabel(row.issueType)}`, + ` 建议动作:${row.actionLabel} / SLA:${row.sla}`, + ` 最后时间:${row.lastSeen || '-'} / 详情:${row.detail || '-'}` + ].join('\n')); + return [ + '【告警优先队列汇总】', + `问题车辆:${summary.issueVehicleCount.toLocaleString()},问题记录:${summary.issueRecordCount.toLocaleString()}`, + `P0:${p0Count.toLocaleString()} 条,P1:${p1Count.toLocaleString()} 条`, + '', + ...lines, + '', + `告警筛选:${qualityShareURL()}` + ].join('\n'); +} + function ruleStatusColor(count: number, level: string): 'green' | 'orange' | 'red' | 'grey' { if (count <= 0) return 'green'; if (level === 'P0') return 'red'; @@ -429,6 +449,13 @@ export function Quality({ ...(dateTo ? { dateTo } : {}) }); }; + const copyPriorityDigest = () => { + if (priorityRows.length === 0) { + Toast.warning('当前没有可复制的优先告警'); + return; + } + copyText(priorityIssueDigestText(priorityRows, summary), '优先队列通知汇总'); + }; return (
@@ -491,7 +518,16 @@ export function Quality({ ))}
- + + 处置优先队列 + + + )} + style={{ marginTop: 16 }} + > loading={loadingIssues} pagination={false} 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 b4bad4c1..9493f9ec 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -2536,6 +2536,104 @@ test('copies notification text from quality priority queue', async () => { expect(writeText).toHaveBeenCalledWith(expect.stringContaining('告警筛选:http://localhost:3000/#/quality')); }); +test('copies priority queue notification digest on quality page', async () => { + window.history.replaceState(null, '', '/#/quality'); + const writeText = vi.fn(() => Promise.resolve()); + Object.defineProperty(navigator, 'clipboard', { + configurable: true, + value: { writeText } + }); + 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, activeConnections: 120000, capacityFindings: [], redisOnlineKeys: 368, 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: 2, + errorCount: 1, + warningCount: 1, + protocols: [{ name: 'VEHICLE_SERVICE', count: 1 }, { name: 'JT808', count: 1 }], + issueTypes: [{ name: 'NO_SOURCE', count: 1 }, { name: 'VIN_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-DIGEST-001', + plate: '粤A汇总1', + phone: '13307795425', + sourceEndpoint: '115.231.168.135:43625', + protocol: 'VEHICLE_SERVICE', + issueType: 'NO_SOURCE', + severity: 'error', + lastSeen: '2026-07-03 20:12:10', + detail: '车辆无任何来源' + }, + { + vin: '', + plate: '粤A汇总2', + phone: '13307795426', + sourceEndpoint: '115.231.168.135:43626', + protocol: 'JT808', + issueType: 'VIN_MISSING', + severity: 'warning', + lastSeen: '2026-07-03 20:10:10', + detail: '手机号未映射 VIN' + } + ], + total: 2, + 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('处置优先队列')).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: '复制优先队列通知汇总' })); + + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【告警优先队列汇总】')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('P0:1 条,P1:1 条')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('1. [P0] 粤A汇总1 / VIN-DIGEST-001')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('确认平台转发')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('2. [P1] 粤A汇总2 / 13307795426')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('维护身份绑定')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('告警筛选:http://localhost:3000/#/quality')); +}); + test('opens realtime map context from quality priority queue', async () => { window.history.replaceState(null, '', '/#/quality'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {