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() {
/>
+ 当前待通知告警} onClick={() => copyText(priorityIssueDigest(plan, release), '待通知告警')}>复制待通知汇总}
+ 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) => (
+
+
+
+
+
+ } onClick={() => copyText(row.notificationText, '告警通知')}>复制通知
+
+ )
+ }
+ ]}
+ />
+
+
{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 () => {