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, QualityPriorityIssue } from '../api/types'; import { PageHeader } from '../components/PageHeader'; import { buildCsv, downloadCsv, type CsvColumn } from '../domain/csvExport'; import { qualityIssueLabel } from '../domain/qualityIssue'; function formatEscalationMinutes(minutes?: number) { if (!Number.isFinite(minutes) || Number(minutes) <= 0) return '-'; const value = Number(minutes); if (value < 60) return `${value} 分钟升级`; const hours = Math.floor(value / 60); const rest = value % 60; return rest > 0 ? `${hours} 小时 ${rest} 分钟升级` : `${hours} 小时升级`; } function formatDurationFromMinutes(minutes?: number | null) { if (minutes == null || !Number.isFinite(minutes)) return '-'; const value = Math.max(0, Math.round(minutes)); if (value < 60) return `${value.toLocaleString()} 分钟`; const hours = Math.floor(value / 60); const rest = value % 60; return rest > 0 ? `${hours.toLocaleString()} 小时 ${rest} 分钟` : `${hours.toLocaleString()} 小时`; } function ruleColor(rule: QualityAlertRule): 'green' | 'orange' | 'red' | 'grey' { if ((rule.count ?? 0) <= 0) return 'green'; if (rule.level === 'P0') return 'red'; if (rule.level === 'P1') return 'orange'; return 'grey'; } function ruleTitle(rule: QualityAlertRule) { return rule.title || qualityIssueLabel(rule.issueType); } type NotificationExportRow = { category: string; name: string; level: string; ownerOrTarget: string; triggerOrCondition: string; notifyOrChannel: string; slaOrEscalation: string; acceptance: string; count: number | string; release: string; }; type SlaEscalationRow = { issue: QualityPriorityIssue; policy?: QualityNotificationPolicy; elapsedMinutes?: number; remainingMinutes?: number; status: 'overdue' | 'due_soon' | 'tracking' | 'unknown'; statusLabel: string; statusColor: 'red' | 'orange' | 'green' | 'grey'; }; type NotificationExecutionRow = { key: string; issue: QualityPriorityIssue; rule?: QualityAlertRule; policy?: QualityNotificationPolicy; slaRow: SlaEscalationRow; evidenceText: string; evidenceColor: 'green' | 'orange'; }; type NotificationCoverageOwnerRow = { owner: string; activeRules: number; p0Rules: number; hitCount: number; primaryPolicy?: QualityNotificationPolicy; }; type NotificationReachabilityRow = { owner: string; issueCount: number; p0IssueCount: number; ruleCount: number; channel: string; target: string; escalation: string; acceptance: string; evidenceComplete: number; status: 'ready' | 'risk' | 'blocked'; statusLabel: string; statusColor: 'green' | 'orange' | 'red'; nextAction: string; }; const notificationExportColumns: CsvColumn[] = [ { title: '类型', value: (row) => row.category }, { title: '名称', value: (row) => row.name }, { title: '级别', value: (row) => row.level }, { title: '责任方', value: (row) => row.ownerOrTarget }, { title: '触发条件', value: (row) => row.triggerOrCondition }, { title: '通知方式', value: (row) => row.notifyOrChannel }, { title: 'SLA/升级', value: (row) => row.slaOrEscalation }, { title: '验收口径', value: (row) => row.acceptance }, { title: '当前命中', value: (row) => row.count }, { title: '运行版本', value: (row) => row.release } ]; function parseIssueTime(value?: string) { const normalized = String(value ?? '').trim().replace(' ', 'T'); if (!normalized) return undefined; const ms = Date.parse(normalized); return Number.isFinite(ms) ? ms : undefined; } function policyForPriority(issue: QualityPriorityIssue, policies: QualityNotificationPolicy[]) { const prefix = issue.priority === 'P0' ? 'P0' : 'P1'; return policies.find((policy) => policy.name.startsWith(prefix)) ?? policies[0]; } function slaEscalationRows(issues: QualityPriorityIssue[], policies: QualityNotificationPolicy[], nowMs = Date.now()): SlaEscalationRow[] { return issues.map((issue) => { const policy = policyForPriority(issue, policies); const lastSeenMs = parseIssueTime(issue.lastSeen); if (lastSeenMs == null || !policy?.escalationMinutes) { return { issue, policy, status: 'unknown', statusLabel: '待确认时间', statusColor: 'grey' }; } const elapsedMinutes = Math.max(0, (nowMs - lastSeenMs) / 60000); const remainingMinutes = policy.escalationMinutes - elapsedMinutes; if (remainingMinutes <= 0) { return { issue, policy, elapsedMinutes, remainingMinutes, status: 'overdue', statusLabel: `已超时 ${formatDurationFromMinutes(Math.abs(remainingMinutes))}`, statusColor: 'red' }; } if (remainingMinutes <= 15) { return { issue, policy, elapsedMinutes, remainingMinutes, status: 'due_soon', statusLabel: `${formatDurationFromMinutes(remainingMinutes)} 后升级`, statusColor: 'orange' }; } return { issue, policy, elapsedMinutes, remainingMinutes, status: 'tracking', statusLabel: `${formatDurationFromMinutes(remainingMinutes)} 后升级`, statusColor: 'green' }; }).sort((left, right) => { const statusWeight: Record = { overdue: 3, due_soon: 2, tracking: 1, unknown: 0 }; return statusWeight[right.status] - statusWeight[left.status] || (left.remainingMinutes ?? Number.POSITIVE_INFINITY) - (right.remainingMinutes ?? Number.POSITIVE_INFINITY) || left.issue.vehicleLabel.localeCompare(right.issue.vehicleLabel); }); } function slaEscalationReport(rows: SlaEscalationRow[], release?: string) { const overdue = rows.filter((row) => row.status === 'overdue').length; const dueSoon = rows.filter((row) => row.status === 'due_soon').length; const tracking = rows.filter((row) => row.status === 'tracking').length; const next = rows[0]; return [ '【告警SLA升级报告】', `运行版本:${release?.trim() || '-'}`, `升级态势:已超时 ${overdue.toLocaleString()} / 即将升级 ${dueSoon.toLocaleString()} / 正常跟进 ${tracking.toLocaleString()}`, `下一优先:${next ? `[${next.issue.priority}] ${next.issue.vehicleLabel} / ${next.statusLabel}` : '暂无待通知告警'}`, '', '升级队列:', ...(rows.length > 0 ? rows.slice(0, 10).map((row, index) => [ `${index + 1}. [${row.issue.priority}] ${row.issue.vehicleLabel}`, ` 状态:${row.statusLabel}`, ` 策略:${row.policy ? `${row.policy.name} / ${row.policy.target} / ${formatEscalationMinutes(row.policy.escalationMinutes)}` : '-'}`, ` 已等待:${formatDurationFromMinutes(row.elapsedMinutes)}`, ` 问题:${qualityIssueLabel(row.issue.issueType)} / ${row.issue.protocol}`, ` 建议动作:${row.issue.actionLabel}`, ` 车辆服务:${window.location.origin}${window.location.pathname}${row.issue.vehicleHash}`, ` 原始记录:${window.location.origin}${window.location.pathname}${row.issue.rawHash}` ].join('\n')) : ['暂无待升级告警']) ].join('\n'); } function notificationExecutionRows( issues: QualityPriorityIssue[], rules: QualityAlertRule[], policies: QualityNotificationPolicy[], nowMs = Date.now() ): NotificationExecutionRow[] { const slaByIssue = new Map(slaEscalationRows(issues, policies, nowMs).map((row) => [ `${row.issue.priority}-${row.issue.protocol}-${row.issue.issueType}-${row.issue.vehicleLabel}-${row.issue.lastSeen}`, row ])); return issues.map((issue) => { const key = `${issue.priority}-${issue.protocol}-${issue.issueType}-${issue.vehicleLabel}-${issue.lastSeen}`; const rule = rules.find((item) => item.issueType === issue.issueType); const policy = policyForPriority(issue, policies); const slaRow = slaByIssue.get(key) ?? { issue, policy, status: 'unknown', statusLabel: '待确认时间', statusColor: 'grey' } as SlaEscalationRow; return { key, issue, rule, policy, slaRow, evidenceText: evidenceSummary(issue), evidenceColor: evidenceColor(issue) }; }).sort((left, right) => { const priorityWeight = (value: string) => value === 'P0' ? 2 : value === 'P1' ? 1 : 0; const statusWeight: Record = { overdue: 3, due_soon: 2, tracking: 1, unknown: 0 }; return priorityWeight(right.issue.priority) - priorityWeight(left.issue.priority) || statusWeight[right.slaRow.status] - statusWeight[left.slaRow.status] || left.issue.vehicleLabel.localeCompare(right.issue.vehicleLabel); }); } function notificationExecutionMatrixReport(rows: NotificationExecutionRow[], release?: string) { const p0Count = rows.filter((row) => row.issue.priority === 'P0').length; const incompleteEvidence = rows.filter((row) => countEvidenceLinks(row.issue) < 4).length; const overdueCount = rows.filter((row) => row.slaRow.status === 'overdue').length; return [ '【通知执行矩阵】', `运行版本:${release?.trim() || '-'}`, `执行态势:P0 ${p0Count.toLocaleString()} / 已超时 ${overdueCount.toLocaleString()} / 证据不完整 ${incompleteEvidence.toLocaleString()}`, '', ...(rows.length > 0 ? rows.slice(0, 12).map((row, index) => [ `${index + 1}. [${row.issue.priority}] ${row.issue.vehicleLabel}`, ` 问题:${qualityIssueLabel(row.issue.issueType)} / ${row.issue.protocol}`, ` 规则:${row.rule ? `${ruleTitle(row.rule)} / ${row.rule.owner}` : '-'}`, ` 通知:${row.policy ? `${row.policy.target} / ${row.policy.channel}` : '-'}`, ` 升级:${row.policy ? formatEscalationMinutes(row.policy.escalationMinutes) : '-'}`, ` 状态:${row.slaRow.statusLabel}`, ` 证据:${row.evidenceText}`, ` 动作:${row.issue.actionLabel} - ${row.issue.actionDetail || '-'}`, ` 车辆服务:${window.location.origin}${window.location.pathname}${row.issue.vehicleHash}`, ` 原始记录:${window.location.origin}${window.location.pathname}${row.issue.rawHash}` ].join('\n')) : ['暂无待执行通知']) ].join('\n'); } function policyLevel(policy: QualityNotificationPolicy) { if (policy.name.startsWith('P0')) return 'P0'; if (policy.name.startsWith('P1')) return 'P1'; return '-'; } function coverageOwnerRows(rules: QualityAlertRule[], policies: QualityNotificationPolicy[]): NotificationCoverageOwnerRow[] { const rows = new Map(); for (const rule of rules) { if (Number(rule.count ?? 0) <= 0) continue; const owner = rule.owner?.trim() || '未分配责任方'; rows.set(owner, [...(rows.get(owner) ?? []), rule]); } return Array.from(rows.entries()).map(([owner, ownerRules]) => { const p0Rules = ownerRules.filter((rule) => rule.level === 'P0').length; const primaryPolicy = policies.find((policy) => policyLevel(policy) === (p0Rules > 0 ? 'P0' : 'P1')) ?? policies[0]; return { owner, activeRules: ownerRules.length, p0Rules, hitCount: ownerRules.reduce((total, rule) => total + Number(rule.count ?? 0), 0), primaryPolicy }; }).sort((left, right) => right.hitCount - left.hitCount || right.p0Rules - left.p0Rules || left.owner.localeCompare(right.owner)); } function notificationReachabilityRows( issues: QualityPriorityIssue[], rules: QualityAlertRule[], policies: QualityNotificationPolicy[], slaRows: SlaEscalationRow[] ): NotificationReachabilityRow[] { const slaByIssue = new Map(slaRows.map((row) => [ `${row.issue.priority}-${row.issue.protocol}-${row.issue.issueType}-${row.issue.vehicleLabel}-${row.issue.lastSeen}`, row ])); const ownerMap = new Map(); for (const rule of rules) { if (Number(rule.count ?? 0) <= 0) continue; const owner = rule.owner?.trim() || '未分配责任方'; const current = ownerMap.get(owner) ?? { issues: [], rules: [], overdue: 0, dueSoon: 0 }; current.rules.push(rule); ownerMap.set(owner, current); } for (const issue of issues) { const rule = rules.find((item) => item.issueType === issue.issueType); const owner = rule?.owner?.trim() || policyForPriority(issue, policies)?.target || '未分配责任方'; const current = ownerMap.get(owner) ?? { issues: [], rules: [], overdue: 0, dueSoon: 0 }; current.issues.push(issue); const sla = slaByIssue.get(`${issue.priority}-${issue.protocol}-${issue.issueType}-${issue.vehicleLabel}-${issue.lastSeen}`); if (sla?.status === 'overdue') current.overdue += 1; if (sla?.status === 'due_soon') current.dueSoon += 1; if (!current.policy) current.policy = policyForPriority(issue, policies); ownerMap.set(owner, current); } return Array.from(ownerMap.entries()).map(([owner, value]) => { const p0IssueCount = value.issues.filter((issue) => issue.priority === 'P0').length; const evidenceComplete = value.issues.filter((issue) => countEvidenceLinks(issue) >= 4).length; const p0Rules = value.rules.filter((rule) => rule.level === 'P0').length; const policy = value.policy ?? policies.find((item) => policyLevel(item) === (p0IssueCount > 0 || p0Rules > 0 ? 'P0' : 'P1')) ?? policies[0]; const hasPolicy = Boolean(policy?.target && policy.channel); const hasAcceptance = Boolean(policy?.acceptanceCriteria); const evidenceMissing = value.issues.length > 0 && evidenceComplete < value.issues.length; const blocked = !hasPolicy || value.overdue > 0; const risk = !blocked && (value.dueSoon > 0 || evidenceMissing || !hasAcceptance || p0IssueCount > 0); const status: NotificationReachabilityRow['status'] = blocked ? 'blocked' : risk ? 'risk' : 'ready'; const statusColor: NotificationReachabilityRow['statusColor'] = blocked ? 'red' : risk ? 'orange' : 'green'; return { owner, issueCount: value.issues.length, p0IssueCount, ruleCount: value.rules.length, channel: policy?.channel || '-', target: policy?.target || '-', escalation: policy ? formatEscalationMinutes(policy.escalationMinutes) : '-', acceptance: policy?.acceptanceCriteria || '-', evidenceComplete, status, statusLabel: blocked ? (value.overdue > 0 ? '已超时' : '策略缺失') : risk ? '需跟进' : '可触达', statusColor, nextAction: blocked ? (value.overdue > 0 ? '立即升级责任人并同步业务侧' : '补齐通知对象与渠道') : evidenceMissing ? '补齐车辆服务、实时、轨迹、原始记录证据' : value.dueSoon > 0 ? '临近升级,先确认恢复进度' : p0IssueCount > 0 ? '先发送P0通知并记录回执' : '按策略持续观察并复核验收' }; }).sort((left, right) => { const statusWeight: Record = { blocked: 3, risk: 2, ready: 1 }; return statusWeight[right.status] - statusWeight[left.status] || right.p0IssueCount - left.p0IssueCount || right.issueCount - left.issueCount || left.owner.localeCompare(right.owner); }); } function notificationReachabilityReport(rows: NotificationReachabilityRow[], release?: string) { const blocked = rows.filter((row) => row.status === 'blocked').length; const risk = rows.filter((row) => row.status === 'risk').length; const ready = rows.filter((row) => row.status === 'ready').length; return [ '【通知可达性检查】', `运行版本:${release?.trim() || '-'}`, `可达结论:阻断 ${blocked.toLocaleString()} / 需跟进 ${risk.toLocaleString()} / 可触达 ${ready.toLocaleString()}`, '', ...(rows.length > 0 ? rows.slice(0, 10).map((row, index) => [ `${index + 1}. ${row.owner} / ${row.statusLabel}`, ` 待通知:${row.issueCount.toLocaleString()} 条,P0 ${row.p0IssueCount.toLocaleString()} 条,活跃规则 ${row.ruleCount.toLocaleString()} 类`, ` 通知对象:${row.target}`, ` 渠道:${row.channel}`, ` 升级:${row.escalation}`, ` 证据:${row.evidenceComplete.toLocaleString()}/${row.issueCount.toLocaleString()} 完整`, ` 验收:${row.acceptance}`, ` 下一步:${row.nextAction}` ].join('\n')) : ['暂无待通知责任方']), '', '检查口径:', '1. 必须有责任方、通知对象和渠道', '2. P0 和超时告警优先进入升级队列', '3. 通知前至少带上车辆服务、实时、轨迹和原始记录证据', '4. 恢复后按验收口径复核,确认告警不再命中' ].join('\n'); } function notificationCoverageReport({ plan, release, ownerRows, overdueCount, dueSoonCount, health }: { plan: QualityNotificationPlan | null; release?: string; ownerRows: NotificationCoverageOwnerRow[]; overdueCount: number; dueSoonCount: number; health: OpsHealth | null; }) { const rules = plan?.rules ?? []; const policies = plan?.policies ?? []; const issues = plan?.priorityIssues ?? []; const activeRules = rules.filter((rule) => Number(rule.count ?? 0) > 0); const p0Issues = issues.filter((issue) => issue.priority === 'P0').length; const missingEvidence = issues.filter((issue) => countEvidenceLinks(issue) < 4).length; const unhealthyLinks = (health?.linkHealth ?? []).filter((item) => item.status !== 'ok'); const policyChannels = Array.from(new Set(policies.map((policy) => policy.channel).filter(Boolean))); return [ '【通知覆盖与升级风险】', `运行版本:${release?.trim() || '-'}`, `责任覆盖:${ownerRows.length.toLocaleString()} 个责任方,${activeRules.length.toLocaleString()} 类活跃规则`, `告警压力:待通知 ${issues.length.toLocaleString()} / P0 ${p0Issues.toLocaleString()} / 证据不完整 ${missingEvidence.toLocaleString()}`, `升级风险:已超时 ${overdueCount.toLocaleString()} / 即将升级 ${dueSoonCount.toLocaleString()}`, `通知渠道:${policyChannels.length > 0 ? policyChannels.join(';') : '-'}`, `链路前置:${unhealthyLinks.length > 0 ? unhealthyLinks.map((item) => `${item.name}=${item.status}`).join(';') : '正常'}`, '', '责任方覆盖:', ...(ownerRows.length > 0 ? ownerRows.slice(0, 8).map((row, index) => [ `${index + 1}. ${row.owner}`, ` 活跃规则:${row.activeRules.toLocaleString()},P0规则:${row.p0Rules.toLocaleString()},命中:${row.hitCount.toLocaleString()}`, ` 主策略:${row.primaryPolicy ? `${row.primaryPolicy.name} / ${row.primaryPolicy.target} / ${row.primaryPolicy.channel}` : '-'}`, ` 升级窗口:${row.primaryPolicy ? formatEscalationMinutes(row.primaryPolicy.escalationMinutes) : '-'}`, ` 验收:${row.primaryPolicy?.acceptanceCriteria || '-'}` ].join('\n')) : ['暂无活跃责任方']), '', '管理动作:', '1. P0 命中先确认责任方、渠道和升级窗口是否明确', '2. 证据不完整的车辆先补齐车辆服务、实时、轨迹和原始记录链接', '3. 超时或即将升级的告警必须同步业务责任人', '4. 链路异常时优先恢复采集、MySQL、TDEngine、Redis、Kafka 等前置能力' ].join('\n'); } function notificationRulesRunbook(plan: QualityNotificationPlan | null, release?: string) { const rules = plan?.rules ?? []; const policies = plan?.policies ?? []; const lines = [ '【通知规则Runbook】', `运行版本:${release?.trim() || '-'}`, `活跃规则:${(plan?.activeRuleCount ?? rules.filter((rule) => rule.count > 0).length).toLocaleString()} 类`, `P0规则:${(plan?.p0RuleCount ?? rules.filter((rule) => rule.count > 0 && rule.level === 'P0').length).toLocaleString()} 类`, '', '触发规则:', ...rules.map((rule, index) => [ `${index + 1}. ${ruleTitle(rule)} / ${rule.level} / ${rule.owner}`, ` 触发:${rule.trigger}`, ` 通知:${rule.notify}`, ` SLA:${rule.sla}`, ` 当前命中:${Number(rule.count ?? 0).toLocaleString()}` ].join('\n')), '', '通知策略:', ...policies.map((policy, index) => [ `${index + 1}. ${policy.name} / ${policy.target}`, ` 条件:${policy.condition}`, ` 渠道:${policy.channel}`, ` 升级:${formatEscalationMinutes(policy.escalationMinutes)}`, ` 验收:${policy.acceptanceCriteria || '-'}` ].join('\n')) ]; 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}`, ` 原始记录:${window.location.origin}${window.location.pathname}${issue.rawHash}` ].join('\n')) ].join('\n'); } function alertEscalationChecklist(plan: QualityNotificationPlan | null, release?: string) { const rules = plan?.rules ?? []; const policies = plan?.policies ?? []; const issues = plan?.priorityIssues ?? []; const p0Issues = issues.filter((issue) => issue.priority === 'P0'); const nextIssue = issues[0]; const primaryPolicy = policies.find((policy) => policy.name.startsWith('P0')) ?? policies[0]; const activeRules = rules.filter((rule) => Number(rule.count ?? 0) > 0); return [ '【告警升级值班清单】', `运行版本:${release?.trim() || '-'}`, `值班结论:${p0Issues.length > 0 ? `P0 ${p0Issues.length.toLocaleString()} 条,需要立即确认` : issues.length > 0 ? `待通知 ${issues.length.toLocaleString()} 条,按策略跟进` : '暂无待通知告警'}`, `主通知策略:${primaryPolicy ? `${primaryPolicy.name} / ${primaryPolicy.target} / ${primaryPolicy.channel}` : '-'}`, `升级窗口:${primaryPolicy ? formatEscalationMinutes(primaryPolicy.escalationMinutes) : '-'}`, `验收口径:${primaryPolicy?.acceptanceCriteria || '-'}`, '', '活跃规则:', ...(activeRules.length > 0 ? activeRules.map((rule, index) => `${index + 1}. [${rule.level}] ${ruleTitle(rule)} / ${rule.owner} / 命中 ${Number(rule.count ?? 0).toLocaleString()} / SLA ${rule.sla}`) : ['暂无活跃规则']), '', '下一条处置:', nextIssue ? [ `[${nextIssue.priority}] ${nextIssue.vehicleLabel}`, `问题:${qualityIssueLabel(nextIssue.issueType)} / ${nextIssue.protocol}`, `建议动作:${nextIssue.actionLabel} - ${nextIssue.actionDetail || '-'}`, `SLA:${nextIssue.sla}`, `证据:${evidenceSummary(nextIssue)}`, `车辆服务:${window.location.origin}${window.location.pathname}${nextIssue.vehicleHash}`, `实时证据:${window.location.origin}${window.location.pathname}${nextIssue.realtimeHash}`, `轨迹证据:${window.location.origin}${window.location.pathname}${nextIssue.historyHash}`, `原始记录:${window.location.origin}${window.location.pathname}${nextIssue.rawHash}` ].join('\n') : '暂无待处置告警', '', '值班动作:', '1. 先处理 P0,无来源/VIN 缺失/存储不可写优先于普通字段缺失', '2. 复制单条通知给责任团队,并同步车辆服务、实时、轨迹和原始记录', '3. 超过升级窗口未恢复时,按主通知策略升级到业务责任人', '4. 恢复后检查验收口径,并在告警事件页确认不再命中' ].join('\n'); } function alertNotificationDrillPackage(plan: QualityNotificationPlan | null, health: OpsHealth | null, release?: string) { const rules = plan?.rules ?? []; const policies = plan?.policies ?? []; const issues = plan?.priorityIssues ?? []; const p0Rules = rules.filter((rule) => rule.level === 'P0'); const p1Rules = rules.filter((rule) => rule.level === 'P1'); const p0Issues = issues.filter((issue) => issue.priority === 'P0'); const firstP0Policy = policies.find((policy) => policy.name.startsWith('P0')) ?? policies[0]; const firstP1Policy = policies.find((policy) => policy.name.startsWith('P1')) ?? policies[1] ?? policies[0]; const unhealthyLinks = (health?.linkHealth ?? []).filter((item) => item.status !== 'ok'); const nextIssue = issues[0]; return [ '【告警通知演练包】', `运行版本:${release?.trim() || '-'}`, `链路状态:${unhealthyLinks.length > 0 ? unhealthyLinks.map((item) => `${item.name}=${item.status}`).join(';') : '正常'}`, `规则规模:P0 ${p0Rules.length.toLocaleString()} 类,P1 ${p1Rules.length.toLocaleString()} 类,待通知 ${issues.length.toLocaleString()} 条`, `当前优先级:${p0Issues.length > 0 ? `P0 ${p0Issues.length.toLocaleString()} 条` : issues.length > 0 ? 'P1/普通告警待跟进' : '暂无待通知告警'}`, '', '通知策略核对:', `P0策略:${firstP0Policy ? `${firstP0Policy.name} / ${firstP0Policy.target} / ${firstP0Policy.channel} / ${formatEscalationMinutes(firstP0Policy.escalationMinutes)}` : '-'}`, `P0验收:${firstP0Policy?.acceptanceCriteria || '-'}`, `P1策略:${firstP1Policy ? `${firstP1Policy.name} / ${firstP1Policy.target} / ${firstP1Policy.channel} / ${formatEscalationMinutes(firstP1Policy.escalationMinutes)}` : '-'}`, `P1验收:${firstP1Policy?.acceptanceCriteria || '-'}`, '', '演练步骤:', '1. 在告警事件页确认 P0/P1 命中数量与本页一致', '2. 对第一条待通知告警打开车辆服务、实时、轨迹、原始记录四类证据', '3. 复制单条告警通知给责任团队,记录发送时间', '4. 超过升级窗口仍未恢复时,按策略升级到业务责任人', '5. 恢复后按验收口径复核,并确认告警不再命中', '', '演练样例:', nextIssue ? [ `[${nextIssue.priority}] ${nextIssue.vehicleLabel}`, `问题:${qualityIssueLabel(nextIssue.issueType)} / ${nextIssue.protocol}`, `SLA:${nextIssue.sla}`, `通知文本:${nextIssue.notificationText}`, `车辆服务:${window.location.origin}${window.location.pathname}${nextIssue.vehicleHash}`, `实时证据:${window.location.origin}${window.location.pathname}${nextIssue.realtimeHash}`, `轨迹证据:${window.location.origin}${window.location.pathname}${nextIssue.historyHash}`, `原始记录:${window.location.origin}${window.location.pathname}${nextIssue.rawHash}` ].join('\n') : '暂无待通知样例' ].join('\n'); } function countEvidenceLinks(issue: QualityPriorityIssue) { return [issue.vehicleHash, issue.realtimeHash, issue.historyHash, issue.rawHash].filter(Boolean).length; } function evidenceSummary(issue: QualityPriorityIssue) { const count = countEvidenceLinks(issue); return count >= 4 ? '证据完整' : `缺 ${4 - count} 类证据`; } function evidenceColor(issue: QualityPriorityIssue): 'green' | 'orange' { return countEvidenceLinks(issue) >= 4 ? 'green' : 'orange'; } function notificationExportRows(plan: QualityNotificationPlan | null, release = ''): NotificationExportRow[] { const version = release.trim() || '-'; const ruleRows = (plan?.rules ?? []).map((rule) => ({ category: '触发规则', name: ruleTitle(rule), level: rule.level, ownerOrTarget: rule.owner, triggerOrCondition: rule.trigger, notifyOrChannel: rule.notify, slaOrEscalation: rule.sla, acceptance: '-', count: Number(rule.count ?? 0), release: version })); const policyRows = (plan?.policies ?? []).map((policy) => ({ category: '通知策略', name: policy.name, level: policy.name.startsWith('P0') ? 'P0' : policy.name.startsWith('P1') ? 'P1' : '-', ownerOrTarget: policy.target, triggerOrCondition: policy.condition, notifyOrChannel: policy.channel, slaOrEscalation: formatEscalationMinutes(policy.escalationMinutes), acceptance: policy.acceptanceCriteria || '-', count: '-', release: version })); return [...ruleRows, ...policyRows]; } function navigateHash(hash: string) { if (!hash) return; window.location.hash = hash; } async function copyText(value: string, label: string) { try { await navigator.clipboard.writeText(value); Toast.success(`已复制${label}`); } catch { Toast.error(`复制${label}失败`); } } export function NotificationRules() { const [plan, setPlan] = useState(null); const [health, setHealth] = useState(null); const [loading, setLoading] = useState(true); const load = () => { setLoading(true); Promise.all([ api.alertEventNotificationPlan(new URLSearchParams({ limit: '50' })), api.opsHealth().catch(() => null) ]) .then(([nextPlan, nextHealth]) => { setPlan(nextPlan); setHealth(nextHealth); }) .catch((error: Error) => Toast.error(error.message)) .finally(() => setLoading(false)); }; useEffect(() => { load(); }, []); 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 ?? ''; const p0IssueCount = priorityIssues.filter((issue) => issue.priority === 'P0').length; const evidenceCompleteCount = priorityIssues.filter((issue) => countEvidenceLinks(issue) >= 4).length; const primaryOwner = rules.find((rule) => rule.count > 0)?.owner || policies[0]?.target || '-'; const nextPriorityIssue = priorityIssues[0]; const slaRows = slaEscalationRows(priorityIssues, policies); const overdueCount = slaRows.filter((row) => row.status === 'overdue').length; const dueSoonCount = slaRows.filter((row) => row.status === 'due_soon').length; const nextSlaRow = slaRows[0]; const executionRows = notificationExecutionRows(priorityIssues, rules, policies); const ownerRows = coverageOwnerRows(rules, policies); const reachabilityRows = notificationReachabilityRows(priorityIssues, rules, policies, slaRows); const reachabilityBlockedCount = reachabilityRows.filter((row) => row.status === 'blocked').length; const reachabilityRiskCount = reachabilityRows.filter((row) => row.status === 'risk').length; const totalActiveHits = ownerRows.reduce((total, row) => total + row.hitCount, 0); const unhealthyLinkCount = (health?.linkHealth ?? []).filter((item) => item.status !== 'ok').length; const notificationCustomerSteps = [ { step: '01', title: '发现问题', value: `${priorityIssues.length.toLocaleString()} 待通知`, detail: '断链、无来源、VIN缺失、字段缺失等问题先进入车辆服务告警队列。', action: '告警队列', color: priorityIssues.length > 0 ? 'orange' as const : 'green' as const, onClick: () => navigateHash('#/alert-events') }, { step: '02', title: '补齐证据', value: `${evidenceCompleteCount}/${priorityIssues.length || 0} 完整`, detail: '通知前必须带上车辆服务、实时监控、轨迹回放和原始记录链接。', action: '证据矩阵', color: evidenceCompleteCount === priorityIssues.length ? 'green' as const : 'orange' as const, onClick: () => nextPriorityIssue?.rawHash ? navigateHash(nextPriorityIssue.rawHash) : undefined }, { step: '03', title: '通知责任人', value: `${ownerRows.length.toLocaleString()} 责任方`, detail: '规则命中后按责任团队、目标人群和通知渠道形成可复制通知。', action: '复制通知', color: ownerRows.length > 0 ? 'blue' as const : 'orange' as const, onClick: () => copyText(priorityIssueDigest(plan, release), '待通知告警') }, { step: '04', title: '超时升级', value: `${overdueCount}/${dueSoonCount}`, 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升级报告') }, { step: '05', title: '验收恢复', value: release || '-', detail: '恢复后按验收口径复核,不再命中规则后关闭本次告警闭环。', action: '运行手册', color: 'blue' as const, 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 primaryPolicy = policies.find((policy) => policy.name.startsWith('P0')) ?? policies[0]; const notificationOrchestrationItems = [ { label: '告警触发', value: `${activeRuleCount.toLocaleString()} 类活跃规则`, detail: `${p0RuleCount.toLocaleString()} 类 P0,当前命中 ${totalActiveHits.toLocaleString()} 次。`, color: p0RuleCount > 0 ? 'red' as const : activeRuleCount > 0 ? 'orange' as const : 'green' as const, onClick: () => navigateHash('#/alert-events') }, { label: '受影响车辆', value: `${priorityIssues.length.toLocaleString()} 待通知`, detail: p0IssueCount > 0 ? `P0 ${p0IssueCount.toLocaleString()} 条,优先通知业务责任人。` : `${evidenceCompleteCount}/${priorityIssues.length || 0} 证据完整。`, color: p0IssueCount > 0 ? 'red' as const : priorityIssues.length > 0 ? 'orange' as const : 'green' as const, onClick: () => copyText(priorityIssueDigest(plan, release), '待通知告警') }, { label: '通知对象', value: primaryPolicy?.target || primaryOwner, detail: `渠道:${primaryPolicy?.channel || policies[0]?.channel || '待配置渠道'}。`, color: primaryPolicy?.target ? 'blue' as const : 'orange' as const, onClick: () => copyText(priorityIssueDigest(plan, release), '待通知告警') }, { label: '升级时钟', value: overdueCount > 0 ? `已超时 ${overdueCount.toLocaleString()}` : dueSoonCount > 0 ? `临近 ${dueSoonCount.toLocaleString()}` : '正常跟进', detail: primaryPolicy ? formatEscalationMinutes(primaryPolicy.escalationMinutes) : '缺少升级策略。', color: overdueCount > 0 ? 'red' as const : dueSoonCount > 0 ? 'orange' as const : 'green' as const, onClick: () => copyText(slaEscalationReport(slaRows, release), 'SLA升级报告') }, { label: '恢复验收', value: primaryPolicy?.acceptanceCriteria || '按车辆服务证据验收', detail: '恢复后必须复核实时、轨迹、原始记录和规则命中状态。', color: primaryPolicy?.acceptanceCriteria ? 'green' as const : 'orange' as const, onClick: () => copyText(notificationRulesRunbook(plan, release), '通知规则Runbook') } ]; const notificationOrchestrationActions = [ { label: '查看告警', action: '告警队列', color: priorityIssues.length > 0 ? 'orange' as const : 'green' as const, disabled: false, onClick: () => navigateHash('#/alert-events') }, { label: '复制通知', action: '待通知', color: priorityIssues.length > 0 ? 'blue' as const : 'grey' as const, disabled: priorityIssues.length === 0, onClick: () => copyText(priorityIssueDigest(plan, release), '待通知告警') }, { label: '复制SLA', action: 'SLA报告', color: overdueCount > 0 ? 'red' as const : dueSoonCount > 0 ? 'orange' as const : 'green' as const, disabled: slaRows.length === 0, onClick: () => copyText(slaEscalationReport(slaRows, release), 'SLA升级报告') }, { label: '导出演练', action: '演练包', color: 'blue' as const, disabled: false, onClick: () => copyText(alertNotificationDrillPackage(plan, health, release), '通知演练包') } ]; const exportNotificationRules = () => { const rows = notificationExportRows(plan, release); if (rows.length === 0) { Toast.warning('当前没有可导出的通知规则'); return; } downloadCsv(`notification-rules-${release || 'current'}.csv`, buildCsv(notificationExportColumns, rows)); Toast.success(`已导出 ${rows.length.toLocaleString()} 条通知规则`); }; return (
客户告警闭环 0 ? 'red' : priorityIssues.length > 0 ? 'orange' : 'green'}> {p0IssueCount > 0 ? `P0 ${p0IssueCount.toLocaleString()}` : priorityIssues.length > 0 ? `${priorityIssues.length.toLocaleString()} 待通知` : '暂无待通知'} 0 ? 'red' : dueSoonCount > 0 ? 'orange' : 'green'}>{overdueCount.toLocaleString()} 超时 / {dueSoonCount.toLocaleString()} 临近 从告警触发到通知升级,再到验收恢复 客户需要知道哪些车受影响、证据是否完整、通知给谁、多久升级、恢复后如何验收;规则和策略只是支撑这个闭环的配置。
{notificationCustomerSteps.map((item) => ( ))}
客户通知编排条 0 || overdueCount > 0 ? 'red' : priorityIssues.length > 0 ? 'orange' : 'green'}> {priorityIssues.length > 0 ? `${priorityIssues.length.toLocaleString()} 待通知` : '当前稳定'} {primaryPolicy?.channel || '待配置渠道'} 先把规则命中转成可执行的通知编排:触发条件、受影响车辆、责任方、渠道、SLA 和验收口径在一行里完成确认。 客户需要的是谁受影响、谁处理、多久升级和如何证明恢复;规则表保留为后面的运行证据。
{notificationOrchestrationItems.map((item) => ( ))}
{notificationOrchestrationActions.map((item) => ( ))}
客户口径 0 ? 'orange' : 'green'}> {priorityIssues.length > 0 ? `${priorityIssues.length.toLocaleString()} 待解释` : '当前稳定'} 把通知规则翻译成客户能理解的影响、责任、升级和恢复口径 客户不需要先理解规则配置,先回答受影响车辆、通知对象、升级时限和恢复验收。
{notificationCustomerQuestions.map((item) => ( ))}
{activeRuleCount.toLocaleString()}
活跃规则
{p0RuleCount.toLocaleString()}
P0规则
{policies.length.toLocaleString()}
通知策略
{priorityIssues.length.toLocaleString()}
待通知告警
{release || '-'}
运行版本
通知覆盖与升级风险} style={{ marginTop: 16 }} >
0 || overdueCount > 0 ? 'red' : dueSoonCount > 0 ? 'orange' : 'green'}> {p0IssueCount > 0 || overdueCount > 0 ? '高优先级跟进' : dueSoonCount > 0 ? '临近升级' : '覆盖稳定'} 0 ? 'orange' : 'green'}>{unhealthyLinkCount > 0 ? '链路需关注' : '链路正常'} {ownerRows.length.toLocaleString()} 个责任方 将告警规则、通知策略、证据完整度和 SLA 升级风险按责任团队归并,便于值班人确认是否能真正通知到人。
{[ { label: '责任覆盖', value: ownerRows.length.toLocaleString(), detail: `${activeRuleCount.toLocaleString()} 类活跃规则已映射责任方。`, color: ownerRows.length > 0 ? 'green' as const : 'orange' as const }, { label: '活跃命中', value: totalActiveHits.toLocaleString(), detail: '来自当前告警规则命中数量,用于判断通知压力。', color: totalActiveHits > 0 ? 'orange' as const : 'green' as const }, { label: 'P0压力', value: p0IssueCount.toLocaleString(), detail: `${p0RuleCount.toLocaleString()} 类 P0 规则,优先进入升级队列。`, color: p0IssueCount > 0 ? 'red' as const : 'green' as const }, { label: '升级风险', value: `${overdueCount}/${dueSoonCount}`, detail: '已超时 / 即将升级,直接影响通知闭环时效。', color: overdueCount > 0 ? 'red' as const : dueSoonCount > 0 ? 'orange' as const : 'green' as const }, { label: '链路前置', value: unhealthyLinkCount.toLocaleString(), detail: '采集、存储、缓存等前置链路异常会影响告警判定。', color: unhealthyLinkCount > 0 ? 'orange' as const : 'green' as const } ].map((item) => (
{item.label} {item.value} {item.detail}
))}
{ownerRows.length > 0 ? ownerRows.slice(0, 6).map((row) => (
0 ? 'red' : 'orange'}>{row.owner} 命中 {row.hitCount.toLocaleString()} {row.activeRules.toLocaleString()} 类活跃规则,{row.p0Rules.toLocaleString()} 类 P0。
{row.primaryPolicy?.name || '未匹配策略'} {row.primaryPolicy?.channel || '-'} {row.primaryPolicy ? formatEscalationMinutes(row.primaryPolicy.escalationMinutes) : '-'}
)) : ( 暂无活跃告警责任方 )}
通知可达性检查} style={{ marginTop: 16 }} >
0 ? 'red' : reachabilityRiskCount > 0 ? 'orange' : 'green'}> {reachabilityBlockedCount > 0 ? '存在阻断' : reachabilityRiskCount > 0 ? '需要跟进' : '可触达'} {reachabilityRows.length.toLocaleString()} 个责任方 阻断 {reachabilityBlockedCount.toLocaleString()} / 风险 {reachabilityRiskCount.toLocaleString()} 按责任方检查通知对象、渠道、升级窗口、证据完整度和验收口径,避免告警已经触发但无人收到或无法闭环。
{reachabilityRows.length > 0 ? reachabilityRows.slice(0, 6).map((row) => (
{row.statusLabel} 0 ? 'red' : 'blue'}>{row.owner} {row.issueCount.toLocaleString()} 条待通知 对象:{row.target};渠道:{row.channel};升级:{row.escalation} 证据 {row.evidenceComplete}/{row.issueCount} {row.acceptance === '-' ? '缺验收' : '有验收'} 0 ? 'red' : 'grey'}>P0 {row.p0IssueCount} {row.nextAction}
)) : ( 暂无待检查通知责任方 )}
告警处置队列} style={{ marginTop: 16 }} >
{[ { label: '待通知告警', value: priorityIssues.length.toLocaleString(), color: priorityIssues.length > 0 ? 'orange' as const : 'green' as const }, { label: 'P0优先', value: p0IssueCount.toLocaleString(), color: p0IssueCount > 0 ? 'red' as const : 'green' as const }, { label: '证据完整', value: `${evidenceCompleteCount}/${priorityIssues.length || 0}`, color: evidenceCompleteCount === priorityIssues.length ? 'green' as const : 'orange' as const }, { label: '主责任方', value: primaryOwner, color: 'blue' as const } ].map((item) => (
{item.label}
{item.value}
))}
下一步处置
{nextPriorityIssue ? ( <> {nextPriorityIssue.vehicleLabel} {nextPriorityIssue.actionDetail || nextPriorityIssue.actionLabel} {nextPriorityIssue.priority} {evidenceSummary(nextPriorityIssue)} {nextPriorityIssue.sla} ) : ( 暂无待通知告警 )}
通知执行矩阵} style={{ marginTop: 16 }} > pagination={false} dataSource={executionRows.slice(0, 10)} rowKey={(row?: NotificationExecutionRow) => row?.key ?? ''} columns={[ { title: '优先级', width: 90, render: (_: unknown, row: NotificationExecutionRow) => {row.issue.priority} }, { title: '车辆', width: 220, render: (_: unknown, row: NotificationExecutionRow) => row.issue.vehicleLabel }, { title: '问题', width: 130, render: (_: unknown, row: NotificationExecutionRow) => qualityIssueLabel(row.issue.issueType) }, { title: '触发规则', width: 180, render: (_: unknown, row: NotificationExecutionRow) => row.rule ? `${ruleTitle(row.rule)} / ${row.rule.owner}` : '-' }, { title: '通知对象', width: 210, render: (_: unknown, row: NotificationExecutionRow) => row.policy?.target || '-' }, { title: '渠道', width: 150, render: (_: unknown, row: NotificationExecutionRow) => row.policy?.channel || '-' }, { title: '升级窗口', width: 120, render: (_: unknown, row: NotificationExecutionRow) => row.policy ? formatEscalationMinutes(row.policy.escalationMinutes) : '-' }, { title: 'SLA状态', width: 150, render: (_: unknown, row: NotificationExecutionRow) => {row.slaRow.statusLabel} }, { title: '证据', width: 110, render: (_: unknown, row: NotificationExecutionRow) => {row.evidenceText} }, { title: '操作', render: (_: unknown, row: NotificationExecutionRow) => ( ) } ]} /> SLA升级态势} style={{ marginTop: 16 }} >
{[ { label: '已超时', value: overdueCount.toLocaleString(), color: overdueCount > 0 ? 'red' as const : 'green' as const }, { label: '即将升级', value: dueSoonCount.toLocaleString(), color: dueSoonCount > 0 ? 'orange' as const : 'green' as const }, { label: '待通知', value: slaRows.length.toLocaleString(), color: slaRows.length > 0 ? 'blue' as const : 'green' as const }, { label: '下一优先', value: nextSlaRow?.statusLabel || '-', color: nextSlaRow?.statusColor ?? 'grey' as const } ].map((item) => (
{item.label} {item.value}
))}
pagination={false} dataSource={slaRows.slice(0, 8)} rowKey={(row?: SlaEscalationRow) => `${row?.issue.priority ?? ''}-${row?.issue.vehicleLabel ?? ''}-${row?.issue.lastSeen ?? ''}`} columns={[ { title: '状态', width: 150, render: (_: unknown, row: SlaEscalationRow) => {row.statusLabel} }, { title: '车辆', width: 220, render: (_: unknown, row: SlaEscalationRow) => row.issue.vehicleLabel }, { title: '优先级', width: 90, render: (_: unknown, row: SlaEscalationRow) => {row.issue.priority} }, { title: '策略', width: 210, render: (_: unknown, row: SlaEscalationRow) => row.policy?.name || '-' }, { title: '已等待', width: 120, render: (_: unknown, row: SlaEscalationRow) => formatDurationFromMinutes(row.elapsedMinutes) }, { title: '责任方', width: 190, render: (_: unknown, row: SlaEscalationRow) => row.policy?.target || '-' }, { title: '处置', render: (_: unknown, row: SlaEscalationRow) => ( ) } ]} />
规则运行手册} >
{[ { label: '触发', value: `${activeRuleCount} 类规则`, detail: '规则从车辆服务质量、来源断链、字段缺失和容量风险中生成。' }, { label: '通知', value: `${policies.length} 套策略`, detail: '策略定义目标人群、渠道、升级窗口和验收口径。' }, { label: '闭环', value: `${p0RuleCount} 类 P0`, detail: 'P0 问题必须有证据链接、恢复时间和验收结果。' } ].map((item) => (
{item.label}
{item.value}
{item.detail}
))}
pagination={false} dataSource={rules} rowKey="issueType" columns={[ { title: '规则', width: 180, render: (_: unknown, row: QualityAlertRule) => ruleTitle(row) }, { title: '级别', width: 90, render: (_: unknown, row: QualityAlertRule) => {row.level} }, { title: '责任团队', width: 130, dataIndex: 'owner' }, { title: '当前命中', width: 110, render: (_: unknown, row: QualityAlertRule) => Number(row.count ?? 0).toLocaleString() }, { title: '触发条件', dataIndex: 'trigger' }, { title: '通知动作', dataIndex: 'notify' }, { title: 'SLA', width: 120, dataIndex: 'sla' } ]} /> 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: 120, render: (_: unknown, row: QualityPriorityIssue) => {evidenceSummary(row)} }, { 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) => (
{policy.name} {policy.target}
{policy.condition}
验收:{policy.acceptanceCriteria || '-'}
{formatEscalationMinutes(policy.escalationMinutes)} {policy.channel}
))}
); }