From 783c8eba5e74a16149a0fe3fa800ce09a8a8d854 Mon Sep 17 00:00:00 2001 From: lingniu Date: Sat, 4 Jul 2026 12:56:46 +0800 Subject: [PATCH] feat(platform): copy quality alert notification --- .../apps/web/src/pages/Quality.tsx | 15 ++++ .../apps/web/src/test/App.test.tsx | 85 +++++++++++++++++++ 2 files changed, 100 insertions(+) diff --git a/vehicle-data-platform/apps/web/src/pages/Quality.tsx b/vehicle-data-platform/apps/web/src/pages/Quality.tsx index 859e8c15..a5fdb3ec 100644 --- a/vehicle-data-platform/apps/web/src/pages/Quality.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Quality.tsx @@ -236,6 +236,20 @@ function priorityIssueRows(rows: QualityIssueRow[]): PriorityIssueRow[] { .slice(0, 5); } +function priorityIssueNotificationText(row: PriorityIssueRow) { + return [ + `【${row.priority} 告警通知】${qualityIssueLabel(row.issueType)}`, + `车辆:${row.vehicleLabel}`, + `数据来源:${qualityProtocolLabel(row.protocol)}`, + `问题:${qualityIssueLabel(row.issueType)}`, + `建议动作:${row.actionLabel}`, + `SLA:${row.sla}`, + `最后时间:${row.lastSeen || '-'}`, + `详情:${row.detail || '-'}`, + `证据入口:${qualityShareURL()}` + ].join('\n'); +} + function ruleStatusColor(count: number, level: string): 'green' | 'orange' | 'red' | 'grey' { if (count <= 0) return 'green'; if (level === 'P0') return 'red'; @@ -485,6 +499,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 6ff3171c..d1874575 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -2413,6 +2413,91 @@ test('shows actionable priority queue on quality page', async () => { }); }); +test('copies notification text from quality priority queue', 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: 1, + issueRecordCount: 1, + errorCount: 1, + warningCount: 0, + protocols: [{ name: 'JT808', count: 1 }], + issueTypes: [{ 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: '', + plate: '粤A通知1', + phone: '13307795425', + sourceEndpoint: '115.231.168.135:43625', + protocol: 'JT808', + issueType: 'VIN_MISSING', + severity: 'error', + lastSeen: '2026-07-03 20:12:10', + detail: '手机号未映射 VIN,需要维护 binding' + }], + 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('处置优先队列')).toBeInTheDocument(); + fireEvent.click(await screen.findByRole('button', { name: '复制通知' })); + + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【P0 告警通知】')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆:粤A通知1 / 13307795425')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('问题:VIN 缺失')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('SLA:2 小时修复')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('最后时间:2026-07-03 20:12:10')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('详情:手机号未映射 VIN,需要维护 binding')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('/#/quality')); +}); + test('opens realtime map context from quality priority queue', async () => { window.history.replaceState(null, '', '/#/quality'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {