diff --git a/vehicle-data-platform/apps/web/src/pages/NotificationRules.tsx b/vehicle-data-platform/apps/web/src/pages/NotificationRules.tsx index 9710109a..1adc8991 100644 --- a/vehicle-data-platform/apps/web/src/pages/NotificationRules.tsx +++ b/vehicle-data-platform/apps/web/src/pages/NotificationRules.tsx @@ -1,8 +1,8 @@ -import { Button, Card, Space, Table, Tag, Toast } from '@douyinfe/semi-ui'; +import { Button, Card, Space, Table, Tag, Toast, Typography } from '@douyinfe/semi-ui'; import { IconCopy } from '@douyinfe/semi-icons'; import { useEffect, useState } from 'react'; import { api } from '../api/client'; -import type { OpsHealth, QualityAlertRule, QualityNotificationPlan, QualityNotificationPolicy } from '../api/types'; +import type { OpsHealth, QualityAlertRule, QualityNotificationPlan, QualityNotificationPolicy, QualityPriorityIssue } from '../api/types'; import { PageHeader } from '../components/PageHeader'; import { qualityIssueLabel } from '../domain/qualityIssue'; @@ -56,6 +56,31 @@ function notificationRulesRunbook(plan: QualityNotificationPlan | null, release? return lines.join('\n'); } +function priorityIssueDigest(plan: QualityNotificationPlan | null, release?: string) { + const issues = plan?.priorityIssues ?? []; + return [ + '【当前待通知告警】', + `运行版本:${release?.trim() || '-'}`, + `待通知:${issues.length.toLocaleString()} 条`, + ...issues.map((issue, index) => [ + `${index + 1}. [${issue.priority}] ${issue.vehicleLabel}`, + ` 问题:${qualityIssueLabel(issue.issueType)} / ${issue.protocol}`, + ` SLA:${issue.sla}`, + ` 建议动作:${issue.actionLabel}`, + ` 详情:${issue.detail || '-'}`, + ` 车辆服务:${window.location.origin}${window.location.pathname}${issue.vehicleHash}`, + ` 实时定位:${window.location.origin}${window.location.pathname}${issue.realtimeHash}`, + ` 轨迹证据:${window.location.origin}${window.location.pathname}${issue.historyHash}`, + ` RAW证据:${window.location.origin}${window.location.pathname}${issue.rawHash}` + ].join('\n')) + ].join('\n'); +} + +function navigateHash(hash: string) { + if (!hash) return; + window.location.hash = hash; +} + async function copyText(value: string, label: string) { try { await navigator.clipboard.writeText(value); @@ -90,6 +115,7 @@ export function NotificationRules() { const rules = plan?.rules ?? []; const policies = plan?.policies ?? []; + const priorityIssues = plan?.priorityIssues ?? []; const activeRuleCount = plan?.activeRuleCount ?? rules.filter((rule) => rule.count > 0).length; const p0RuleCount = plan?.p0RuleCount ?? rules.filter((rule) => rule.count > 0 && rule.level === 'P0').length; const release = health?.runtime?.platformRelease ?? ''; @@ -110,6 +136,10 @@ export function NotificationRules() {
{policies.length.toLocaleString()}
通知策略
+ +
{priorityIssues.length.toLocaleString()}
+
待通知告警
+
{release || '-'}
运行版本
@@ -152,6 +182,46 @@ export function NotificationRules() { />
+ 当前待通知告警} + style={{ marginTop: 16 }} + > + + pagination={false} + dataSource={priorityIssues} + rowKey={(row?: QualityPriorityIssue) => `${row?.priority ?? ''}-${row?.protocol ?? ''}-${row?.issueType ?? ''}-${row?.vehicleLabel ?? ''}-${row?.lastSeen ?? ''}`} + columns={[ + { title: '优先级', width: 90, render: (_: unknown, row: QualityPriorityIssue) => {row.priority} }, + { title: '车辆', width: 210, dataIndex: 'vehicleLabel' }, + { title: '问题', width: 130, render: (_: unknown, row: QualityPriorityIssue) => qualityIssueLabel(row.issueType) }, + { title: 'SLA', width: 120, dataIndex: 'sla' }, + { title: '建议动作', width: 160, dataIndex: 'actionLabel' }, + { title: '最后时间', width: 170, dataIndex: 'lastSeen' }, + { + title: '说明', + render: (_: unknown, row: QualityPriorityIssue) => ( + {row.detail || row.actionDetail || '-'} + ) + }, + { + title: '证据', + width: 360, + render: (_: unknown, row: QualityPriorityIssue) => ( + + + + + + + + ) + } + ]} + /> + +
{policies.map((policy: QualityNotificationPolicy) => ( 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 0f9ab427..9edc6f0a 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -3294,7 +3294,27 @@ test('renders notification rules as a standalone operations page', async () => { { name: 'P0 实时中断', target: '接入运维 + 业务责任人', channel: '邮件 / 企业微信', condition: '无来源、VIN 缺失、存储不可写', escalationMinutes: 30, acceptanceCriteria: '来源恢复并持续 10 分钟' }, { name: 'P1 数据质量', target: '协议解析 + 数据治理', channel: '每日汇总邮件', condition: '字段缺失、链路间断', escalationMinutes: 120, acceptanceCriteria: '核心字段恢复解析' } ], - priorityIssues: [], + priorityIssues: [{ + vin: 'VIN-RULES-001', + plate: '粤A规则1', + phone: '', + sourceEndpoint: 'vehicle_identity_binding', + protocol: 'VEHICLE_SERVICE', + issueType: 'NO_SOURCE', + severity: 'error', + lastSeen: '2026-07-03 20:12:10', + detail: '通知规则页待通知告警', + priority: 'P0', + actionLabel: '确认平台转发', + actionDetail: '核对平台转发配置', + sla: '30 分钟确认', + vehicleLabel: '粤A规则1 / VIN-RULES-001', + realtimeHash: '#/realtime?keyword=VIN-RULES-001', + historyHash: '#/history?keyword=VIN-RULES-001', + rawHash: '#/history-query?keyword=VIN-RULES-001&tab=raw', + vehicleHash: '#/detail?keyword=VIN-RULES-001', + notificationText: '【P0 告警通知】通知规则页待通知' + }], activeRuleCount: 2, p0RuleCount: 1 }, @@ -3320,12 +3340,23 @@ test('renders notification rules as a standalone operations page', async () => { expect(screen.getByText('车辆无任何来源证据')).toBeInTheDocument(); expect(screen.getByText('P0 实时中断')).toBeInTheDocument(); expect(screen.getByText('接入运维 + 业务责任人')).toBeInTheDocument(); + expect(screen.getByText('当前待通知告警')).toBeInTheDocument(); + expect(screen.getByText('粤A规则1 / VIN-RULES-001')).toBeInTheDocument(); + expect(screen.getByText('确认平台转发')).toBeInTheDocument(); + expect(screen.getByText('通知规则页待通知告警')).toBeInTheDocument(); expect(screen.getByText('platform-rules-test')).toBeInTheDocument(); expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/alert-events/notification-plan?limit=50'), undefined); fireEvent.click(screen.getByRole('button', { name: '复制通知规则Runbook' })); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【通知规则Runbook】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('无来源规则 / P0 / 平台接入')); + fireEvent.click(screen.getByRole('button', { name: '复制待通知汇总' })); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【当前待通知告警】')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('粤A规则1 / VIN-RULES-001')); + fireEvent.click(screen.getByRole('button', { name: '复制单条告警通知' })); + expect(writeText).toHaveBeenCalledWith('【P0 告警通知】通知规则页待通知'); + fireEvent.click(screen.getByRole('button', { name: '车辆服务' })); + expect(window.location.hash).toBe('#/detail?keyword=VIN-RULES-001'); }); test('renders ops quality as a standalone runtime health page', async () => {