From 00d416964b1decdd2aff82b8c8f9ae3a158b698a Mon Sep 17 00:00:00 2001 From: lingniu Date: Mon, 6 Jul 2026 06:05:01 +0800 Subject: [PATCH] feat(platform): add alert closure workbench --- .../apps/web/src/pages/Quality.tsx | 123 ++++++++++++++++++ .../apps/web/src/styles/global.css | 94 +++++++++++++ .../apps/web/src/test/App.test.tsx | 11 ++ 3 files changed, 228 insertions(+) diff --git a/vehicle-data-platform/apps/web/src/pages/Quality.tsx b/vehicle-data-platform/apps/web/src/pages/Quality.tsx index 2e82d6c0..d60d6b3e 100644 --- a/vehicle-data-platform/apps/web/src/pages/Quality.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Quality.tsx @@ -813,6 +813,50 @@ function alertNotificationCommandText({ ].filter(Boolean).join('\n'); } +function alertClosurePackageText({ + summary, + priorityRows, + primaryPolicy, + filters, + platformRelease +}: { + summary: QualitySummary; + priorityRows: PriorityIssueRow[]; + primaryPolicy?: QualityNotificationPolicy; + filters: Record; + platformRelease?: string; +}) { + const release = platformRelease?.trim(); + const p0Rows = priorityRows.filter((row) => row.priority === 'P0'); + const p1Rows = priorityRows.filter((row) => row.priority === 'P1'); + const focusIssue = priorityRows[0]; + const focusLookup = focusIssue ? qualityIssueVehicleLookup(focusIssue) : undefined; + const evidenceKeyword = focusLookup?.key || filters.keyword || ''; + const evidenceProtocol = focusIssue?.protocol || filters.protocol; + const primaryPolicyLine = primaryPolicy + ? [primaryPolicy.name, primaryPolicy.target, primaryPolicy.channel, formatEscalationMinutes(primaryPolicy.escalationMinutes)].filter(Boolean).join(' / ') + : '-'; + return [ + '【客户告警闭环包】', + ...(release ? [`运行版本:${release}`] : []), + '处理路径:影响车辆 -> 通知升级 -> 处置证据 -> 恢复验收', + `影响范围:${Number(summary.issueVehicleCount ?? 0).toLocaleString()} 辆 / ${Number(summary.issueRecordCount ?? 0).toLocaleString()} 条`, + `优先级:P0 ${p0Rows.length.toLocaleString()} / P1 ${p1Rows.length.toLocaleString()}`, + `主通知策略:${primaryPolicyLine}`, + `焦点车辆:${focusIssue ? focusIssue.vehicleLabel : '-'}`, + focusIssue ? `焦点问题:${qualityProtocolLabel(focusIssue.protocol)} / ${qualityIssueLabel(focusIssue.issueType)} / ${focusIssue.actionLabel}` : '', + `恢复标准:${primaryPolicy?.acceptanceCriteria || '实时可看 / 轨迹可回放 / 里程可核对 / 历史证据可导出'}`, + '', + '证据入口:', + `告警事件:${qualityShareURL()}`, + `实时监控:${appURL(buildAppHash({ page: 'realtime', keyword: evidenceKeyword, protocol: evidenceProtocol }))}`, + `轨迹回放:${appURL(buildAppHash({ page: 'history', keyword: evidenceKeyword, protocol: evidenceProtocol }))}`, + `原始记录:${appURL(buildAppHash({ page: 'history-query', keyword: evidenceKeyword, protocol: evidenceProtocol, filters: { tab: 'raw', includeFields: 'true' } }))}`, + `里程统计:${appURL(buildAppHash({ page: 'mileage', keyword: evidenceKeyword, protocol: evidenceProtocol }))}`, + `通知规则:${appURL(buildAppHash({ page: 'notification-rules' }))}` + ].filter(Boolean).join('\n'); +} + async function copyText(value: string, label: string) { const text = value.trim(); if (!text) { @@ -1130,6 +1174,15 @@ export function Quality({ platformRelease: health?.runtime?.platformRelease }), '客户告警通知总控包'); }; + const copyAlertClosurePackage = () => { + copyText(alertClosurePackageText({ + summary: notificationPlan?.summary ?? summary, + priorityRows, + primaryPolicy, + filters, + platformRelease: health?.runtime?.platformRelease + }), '告警闭环包'); + }; const alertNotificationCommandItems = [ { label: '影响车辆', @@ -1360,10 +1413,80 @@ export function Quality({ onClick: copyAlertRecoveryReceipt } ]; + const alertClosureWorkbenchItems = [ + { + step: '1', + label: '影响车辆', + value: `${issueVehicleCount.toLocaleString()} 辆`, + detail: `${issueRecordCount.toLocaleString()} 条告警,先判断是否影响实时地图、轨迹回放、里程和导出。`, + color: issueVehicleCount > 0 ? 'orange' as const : 'green' as const, + onClick: copyBusinessImpact + }, + { + step: '2', + label: '通知升级', + value: primaryPolicy?.name || '策略待配置', + detail: primaryPolicy ? `${primaryPolicy.target} / ${primaryPolicy.channel} / ${formatEscalationMinutes(primaryPolicy.escalationMinutes) || '未配置升级'}` : '需要补齐通知对象、渠道和升级时限。', + color: primaryPolicy ? 'blue' as const : 'orange' as const, + onClick: () => onOpenNotificationRules?.() + }, + { + step: '3', + label: '处置证据', + value: '实时/轨迹/原始记录', + detail: '每次通知都带上车辆实时状态、轨迹证据、原始帧和解析字段,减少来回确认。', + color: 'blue' as const, + onClick: copyNotificationHandoff + }, + { + step: '4', + label: '恢复验收', + value: '实时/轨迹/里程/导出', + detail: primaryPolicy?.acceptanceCriteria || '恢复后确认车辆服务状态、轨迹、历史和里程证据可查询。', + color: primaryPolicy?.acceptanceCriteria ? 'green' as const : 'orange' as const, + onClick: copyAlertRecoveryReceipt + } + ]; return (
+
+
+ + 客户告警闭环台 + 0 ? 'red' : priorityP1Count > 0 ? 'orange' : 'green'}> + P0 {priorityP0Count.toLocaleString()} / P1 {priorityP1Count.toLocaleString()} + + {primaryPolicy?.target || '责任人待配置'} + + 先判断影响车辆,再通知责任人,随后打开实时、轨迹、原始记录完成处置,最后按恢复标准验收。 + + + {onOpenNotificationRules ? : null} + +
+
+ {alertClosureWorkbenchItems.map((item) => ( + + ))} +
+
diff --git a/vehicle-data-platform/apps/web/src/styles/global.css b/vehicle-data-platform/apps/web/src/styles/global.css index 7e720b66..1abcc374 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -8330,6 +8330,98 @@ button.vp-realtime-command-item:focus-visible { gap: 12px; } +.vp-alert-closure-workbench { + margin-top: 16px; + margin-bottom: 16px; + border: 1px solid rgba(2, 132, 199, 0.18); + border-radius: var(--vp-radius); + background: #fff; + display: grid; + grid-template-columns: minmax(320px, 0.48fr) minmax(0, 1.52fr); + overflow: hidden; + box-shadow: var(--vp-shadow-sm); +} + +.vp-alert-closure-workbench-summary { + padding: 18px; + border-right: 1px solid var(--vp-border); + background: #f0f9ff; + display: grid; + gap: 10px; + align-content: center; +} + +.vp-alert-closure-workbench-summary strong { + color: var(--vp-text); + font-size: 18px; + line-height: 26px; + word-break: break-word; +} + +.vp-alert-closure-workbench-grid { + padding: 14px; + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 10px; +} + +.vp-alert-closure-workbench-item { + min-height: 148px; + padding: 12px; + border: 1px solid rgba(2, 132, 199, 0.16); + border-radius: 8px; + background: #fbfcff; + color: inherit; + cursor: pointer; + font: inherit; + text-align: left; + display: grid; + grid-template-columns: 32px minmax(0, 1fr); + gap: 10px; +} + +.vp-alert-closure-workbench-item:hover, +.vp-alert-closure-workbench-item:focus-visible { + border-color: rgba(2, 132, 199, 0.42); + background: #f0f9ff; + box-shadow: 0 0 0 3px rgba(2, 132, 199, 0.08); + outline: none; +} + +.vp-alert-closure-workbench-item > span { + width: 32px; + height: 32px; + border-radius: 8px; + background: #e0f2fe; + color: #075985; + display: inline-flex; + align-items: center; + justify-content: center; + font-weight: 800; +} + +.vp-alert-closure-workbench-item div { + display: grid; + gap: 8px; + align-content: start; + min-width: 0; +} + +.vp-alert-closure-workbench-item strong { + color: var(--vp-text); + font-size: 17px; + line-height: 24px; + font-weight: 800; + word-break: break-word; +} + +.vp-alert-closure-workbench-item small { + color: var(--vp-text-muted); + font-size: 12px; + line-height: 18px; + word-break: break-word; +} + .vp-alert-action-conclusion { margin-top: 16px; margin-bottom: 16px; @@ -15142,6 +15234,8 @@ button.vp-realtime-command-item:focus-visible { .vp-conclusion-grid, .vp-monitor-layout, .vp-alert-flow, + .vp-alert-closure-workbench, + .vp-alert-closure-workbench-grid, .vp-alert-action-conclusion, .vp-alert-action-conclusion-grid, .vp-alert-customer-service-desk, 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 5b66c0be..9d4fa515 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -5030,6 +5030,17 @@ test('shows alert rule and notification policy workspace on quality page', async expect(screen.getByRole('button', { name: '客户告警闭环时间线 3 责任通知 接入运维 + 业务责任人 通知策略' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户告警闭环时间线 4 证据复核 实时/轨迹/里程 证据交接' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户告警闭环时间线 5 恢复验收 有标准 复制回执' })).toBeInTheDocument(); + expect(screen.getByText('客户告警闭环台')).toBeInTheDocument(); + expect(screen.getByText('先判断影响车辆,再通知责任人,随后打开实时、轨迹、原始记录完成处置,最后按恢复标准验收。')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户告警闭环台 1 影响车辆 3 辆' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户告警闭环台 2 通知升级 P0 实时中断' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户告警闭环台 3 处置证据 实时/轨迹/原始记录' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户告警闭环台 4 恢复验收 实时/轨迹/里程/导出' })).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: '复制告警闭环包' })); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【客户告警闭环包】')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('影响范围:3 辆 / 5 条')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('主通知策略:P0 实时中断 / 接入运维 + 业务责任人 / 站内告警 / 邮件 / 企业微信 / 30 分钟升级')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('证据入口:')); fireEvent.click(screen.getByRole('button', { name: '复制告警决策' })); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【客户告警决策说明】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('决策结论:立即处置'));