diff --git a/vehicle-data-platform/apps/web/src/pages/NotificationRules.tsx b/vehicle-data-platform/apps/web/src/pages/NotificationRules.tsx index c771009c..265a9bd1 100644 --- a/vehicle-data-platform/apps/web/src/pages/NotificationRules.tsx +++ b/vehicle-data-platform/apps/web/src/pages/NotificationRules.tsx @@ -723,6 +723,44 @@ export function NotificationRules() { onClick: () => copyText(notificationRulesRunbook(plan, release), '通知规则Runbook') } ]; + const notificationCustomerQuestions = [ + { + question: '哪些车受影响?', + value: priorityIssues.length > 0 ? `${priorityIssues.length.toLocaleString()} 辆/次` : '暂无影响', + evidence: p0IssueCount > 0 ? `P0 ${p0IssueCount.toLocaleString()} 条` : `${evidenceCompleteCount}/${priorityIssues.length || 0} 证据完整`, + detail: '先给客户受影响车辆、告警级别和可跳转证据,不让客户从规则配置里反推影响范围。', + action: '影响摘要', + color: p0IssueCount > 0 ? 'red' as const : priorityIssues.length > 0 ? 'orange' as const : 'green' as const, + onClick: () => copyText(priorityIssueDigest(plan, release), '待通知告警') + }, + { + question: '应该通知谁?', + value: primaryOwner, + evidence: policies[0]?.channel || '待配置渠道', + detail: '按责任团队和通知策略生成可复制文本,包含车辆、原因、证据链接和下一步动作。', + action: '复制通知', + color: ownerRows.length > 0 ? 'blue' as const : 'orange' as const, + onClick: () => copyText(priorityIssueDigest(plan, release), '待通知告警') + }, + { + question: '多久没有恢复会升级?', + value: nextSlaRow?.policy?.escalationMinutes ? formatEscalationMinutes(nextSlaRow.policy.escalationMinutes) : `${overdueCount}/${dueSoonCount}`, + evidence: `${overdueCount.toLocaleString()} 超时 / ${dueSoonCount.toLocaleString()} 临近`, + detail: '客户看到的是升级时限和当前风险,后台再按P0/P1策略做超时升级和责任追踪。', + action: 'SLA报告', + color: overdueCount > 0 ? 'red' as const : dueSoonCount > 0 ? 'orange' as const : 'green' as const, + onClick: () => copyText(slaEscalationReport(slaRows, release), 'SLA升级报告') + }, + { + question: '恢复后怎么验收?', + value: policies[0]?.acceptanceCriteria || '按车辆服务证据验收', + evidence: release || '当前版本', + detail: '恢复不是只看服务在线,而是看车辆证据、实时数据、轨迹和规则命中是否一起恢复。', + action: '验收口径', + color: 'blue' as const, + onClick: () => copyText(notificationRulesRunbook(plan, release), '通知规则Runbook') + } + ]; const exportNotificationRules = () => { const rows = notificationExportRows(plan, release); if (rows.length === 0) { @@ -774,6 +812,41 @@ export function NotificationRules() { ))} + +
+
+ + 客户口径 + 0 ? 'orange' : 'green'}> + {priorityIssues.length > 0 ? `${priorityIssues.length.toLocaleString()} 待解释` : '当前稳定'} + + + 把通知规则翻译成客户能理解的影响、责任、升级和恢复口径 + + 客户不需要先理解规则配置,先回答受影响车辆、通知对象、升级时限和恢复验收。 + +
+
+ {notificationCustomerQuestions.map((item) => ( + + ))} +
+
+
{activeRuleCount.toLocaleString()}
diff --git a/vehicle-data-platform/apps/web/src/styles/global.css b/vehicle-data-platform/apps/web/src/styles/global.css index 35163237..c27eb7c7 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -3892,6 +3892,72 @@ button.vp-realtime-command-item:focus-visible { align-self: end; } +.vp-notification-question-board { + display: grid; + grid-template-columns: minmax(260px, 0.58fr) minmax(0, 1.42fr); + gap: 16px; +} + +.vp-notification-question-summary { + min-height: 160px; + padding: 14px; + border: 1px solid rgba(22, 100, 255, 0.24); + border-radius: var(--vp-radius); + background: #f6f9ff; + display: grid; + gap: 10px; + align-content: start; +} + +.vp-notification-question-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; +} + +.vp-notification-question-item { + min-height: 160px; + padding: 12px; + border: 1px solid var(--vp-border); + border-radius: 8px; + background: #fbfcff; + text-align: left; + cursor: pointer; + font: inherit; + display: grid; + gap: 9px; + align-content: start; +} + +.vp-notification-question-item:hover, +.vp-notification-question-item:focus-visible { + border-color: rgba(22, 100, 255, 0.45); + box-shadow: var(--vp-shadow-sm); + outline: none; +} + +.vp-notification-question-item strong { + color: var(--vp-text); + font-size: 18px; + line-height: 24px; + font-weight: 700; + word-break: break-word; +} + +.vp-notification-question-item small { + color: var(--vp-text-muted); + font-size: 12px; + line-height: 18px; +} + +.vp-notification-question-item em { + color: #1664ff; + font-style: normal; + font-size: 12px; + font-weight: 600; + align-self: end; +} + .vp-notification-coverage-board { display: grid; grid-template-columns: minmax(260px, 0.72fr) minmax(0, 1.28fr); @@ -6855,6 +6921,8 @@ button.vp-realtime-command-item:focus-visible { .vp-alert-command-grid, .vp-notification-customer-board .semi-card-body, .vp-notification-customer-steps, + .vp-notification-question-board, + .vp-notification-question-grid, .vp-notification-coverage-board, .vp-notification-coverage-grid, .vp-notification-owner-item, 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 95d001bc..d9086877 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -4072,6 +4072,12 @@ test('renders notification rules as a standalone operations page', async () => { expect(screen.getByRole('button', { name: '客户告警闭环 超时升级 SLA报告' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户告警闭环 验收恢复 运行手册' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '复制值班清单' })).toBeInTheDocument(); + expect(screen.getByText('客户通知常问')).toBeInTheDocument(); + expect(screen.getByText('把通知规则翻译成客户能理解的影响、责任、升级和恢复口径')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户通知常问 哪些车受影响? 影响摘要' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户通知常问 应该通知谁? 复制通知' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户通知常问 多久没有恢复会升级? SLA报告' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户通知常问 恢复后怎么验收? 验收口径' })).toBeInTheDocument(); expect(screen.getByText('无来源规则')).toBeInTheDocument(); expect(screen.getByText('车辆无任何来源证据')).toBeInTheDocument(); expect(screen.getAllByText('P0 实时中断').length).toBeGreaterThan(0);