Files
lingniu-vehicle-ingest/vehicle-data-platform/apps/web/src/pages/Quality.tsx
2026-07-05 23:09:33 +08:00

2113 lines
98 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Button, Card, Col, Form, Row, Select, Space, Table, Tag, Toast } 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, QualitySummary, QualityIssueRow, QualityPriorityIssue } from '../api/types';
import { PageHeader } from '../components/PageHeader';
import { buildAppHash } from '../domain/appRoute';
import { qualityIssueLabel, qualityIssueOptions, qualityProtocolLabel, qualityProtocolOptions } from '../domain/qualityIssue';
import { qualityIssueVehicleLookup } from '../domain/vehicleLookup';
const statusColor: Record<string, 'green' | 'orange' | 'red' | 'grey'> = {
ok: 'green',
warning: 'orange',
error: 'red'
};
function formatLag(value?: number | null) {
return value == null ? '未接入' : value.toLocaleString();
}
function storageReadStatus(health: OpsHealth | null) {
if (!health) return 'pending';
return health.tdengineWritable && health.mysqlWritable ? 'ok' : 'error';
}
function formatRequestTimeout(health: OpsHealth | null) {
const value = health?.runtime?.requestTimeoutMs;
return value == null || value <= 0 ? '未限制' : `${value.toLocaleString()} ms`;
}
const emptySummary: QualitySummary = {
issueVehicleCount: 0,
issueRecordCount: 0,
errorCount: 0,
warningCount: 0,
protocols: [],
issueTypes: []
};
const alertRuleTemplates = [
{
issueType: 'NO_SOURCE',
level: 'P0',
owner: '平台接入',
trigger: '绑定车辆连续无 GB32960、JT808、MQTT 来源证据',
notify: '立即通知接入运维30 分钟未恢复升级给业务责任人',
sla: '30 分钟确认'
},
{
issueType: 'VIN_MISSING',
level: 'P0',
owner: '车辆档案',
trigger: '来源有数据但无法归并到 VIN',
notify: '通知档案维护人补齐车牌、手机号和 VIN 映射',
sla: '2 小时修复'
},
{
issueType: 'LINK_GAP',
level: 'P1',
owner: '链路运维',
trigger: '车辆或平台来源上报间断、Redis 在线状态过期',
notify: '通知平台转发和网关值班人,持续异常进入日报',
sla: '1 小时恢复'
},
{
issueType: 'FIELD_MISSING',
level: 'P1',
owner: '协议解析',
trigger: '核心字段缺失导致定位、里程或统计不可用',
notify: '通知解析负责人核对字段映射和原始记录样本',
sla: '当日闭环'
}
];
const notificationPolicies = [
{
name: 'P0 实时中断',
target: '接入运维 + 业务责任人',
channel: '站内告警 / 邮件 / 企业微信',
condition: '无来源、VIN 缺失、存储不可写',
escalationMinutes: 30,
acceptanceCriteria: '来源恢复并持续 10 分钟,车辆服务可查到实时与历史证据'
},
{
name: 'P1 数据质量',
target: '协议解析 + 数据治理',
channel: '站内告警 / 每日汇总邮件',
condition: '字段缺失、链路间断、容量风险',
escalationMinutes: 120,
acceptanceCriteria: '核心字段恢复解析,影响车辆可通过原始记录与历史证据复核'
},
{
name: 'P2 趋势关注',
target: '数智中心',
channel: '周报 / 趋势看板',
condition: '单源车辆、档案缺项、来源覆盖下降',
escalationMinutes: 1440,
acceptanceCriteria: '趋势原因已归档,责任人和后续治理动作明确'
}
];
type AlertRuleRow = {
issueType: string;
title?: string;
level: string;
owner: string;
trigger: string;
notify: string;
sla: string;
count: number;
};
type PriorityIssueRow = QualityIssueRow & {
priority: 'P0' | 'P1';
actionLabel: string;
actionDetail?: string;
sla: string;
vehicleLabel: string;
realtimeHash?: string;
historyHash?: string;
rawHash?: string;
vehicleHash?: string;
notificationText?: string;
};
type AlertDispatchOwnerRow = {
owner: string;
activeRuleCount: number;
hitCount: number;
p0Count: number;
primaryIssueType: string;
primarySla: string;
primaryHitCount: number;
};
function qualityParams(values: Record<string, string>) {
const params = new URLSearchParams();
if (values?.keyword) params.set('keyword', values.keyword);
if (values?.protocol) params.set('protocol', values.protocol);
if (values?.issueType) params.set('issueType', values.issueType);
return params;
}
function qualityShareURL() {
return `${window.location.origin}${window.location.pathname}${window.location.hash}`;
}
function appURL(hash: string) {
return `${window.location.origin}${window.location.pathname}${hash}`;
}
function issueEvidenceDate(value?: string) {
const match = String(value ?? '').match(/^(\d{4})-(\d{2})-(\d{2})/);
return match ? `${match[1]}-${match[2]}-${match[3]}` : '';
}
function nextDate(value: string) {
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value.trim());
if (!match) return '';
const date = new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]) + 1));
return date.toISOString().slice(0, 10);
}
function qualityActionRecommendation(row: QualityIssueRow) {
if (row.issueType === 'NO_SOURCE') {
return {
label: '确认平台转发',
color: 'orange' as const,
detail: '车辆已绑定但没有任何来源证据,先确认平台转发、端口和订阅。'
};
}
if (row.issueType === 'VIN_MISSING' || !row.vin?.trim()) {
return {
label: '维护身份绑定',
color: 'red' as const,
detail: '数据已有来源但无法归并到 VIN优先用车牌/手机号补齐绑定。'
};
}
if (row.issueType === 'LINK_GAP') {
return {
label: '排查来源链路',
color: 'orange' as const,
detail: '来源存在上报间断,优先检查平台转发、网络和消费延迟。'
};
}
if (row.issueType === 'FIELD_MISSING') {
return {
label: '核对解析字段',
color: 'orange' as const,
detail: '字段缺失会影响统计和展示,优先核对解析映射和原始记录。'
};
}
return {
label: '查看车辆服务',
color: 'blue' as const,
detail: '进入车辆服务详情,结合来源证据继续排查。'
};
}
function issueCount(summary: QualitySummary, issueType: string) {
const issueTypes = Array.isArray(summary.issueTypes) ? summary.issueTypes : [];
return issueTypes.find((item) => item.name === issueType)?.count ?? 0;
}
const issuePriorityWeight: Record<string, number> = {
NO_SOURCE: 10,
VIN_MISSING: 9,
LINK_GAP: 8,
FIELD_MISSING: 7
};
function alertRuleRows(summary: QualitySummary, health: OpsHealth | null): AlertRuleRow[] {
const storageWritable = health == null || (health.tdengineWritable && health.mysqlWritable);
const capacityCount = health?.capacityFindings?.length ?? 0;
const rows = alertRuleTemplates.map((item) => ({
...item,
count: issueCount(summary, item.issueType)
}));
rows.push({
issueType: 'CAPACITY_RISK',
level: storageWritable && capacityCount === 0 ? 'P2' : 'P0',
owner: '基础设施',
trigger: 'Kafka Lag、连接容量、Redis 在线 Key 或存储读写异常',
notify: storageWritable ? '容量风险进入运维日报,超过阈值升级' : '立即通知基础设施值班人处理存储不可写',
sla: storageWritable ? '当日评估' : '15 分钟恢复',
count: capacityCount + (storageWritable ? 0 : 1)
});
return rows;
}
function alertDispatchOwnerRows(rules: AlertRuleRow[]): AlertDispatchOwnerRow[] {
const ownerMap = new Map<string, AlertDispatchOwnerRow>();
rules.forEach((rule) => {
const owner = rule.owner || '未分配';
const current = ownerMap.get(owner) ?? {
owner,
activeRuleCount: 0,
hitCount: 0,
p0Count: 0,
primaryIssueType: rule.issueType,
primarySla: rule.sla,
primaryHitCount: 0
};
current.activeRuleCount += rule.count > 0 ? 1 : 0;
current.hitCount += rule.count;
current.p0Count += rule.count > 0 && rule.level === 'P0' ? 1 : 0;
const shouldReplacePrimary = rule.count > current.primaryHitCount || (rule.count === current.primaryHitCount && rule.level === 'P0');
if (shouldReplacePrimary) {
current.primaryIssueType = rule.issueType;
current.primarySla = rule.sla;
current.primaryHitCount = rule.count;
}
ownerMap.set(owner, current);
});
return [...ownerMap.values()]
.filter((row) => row.hitCount > 0 || row.activeRuleCount > 0)
.sort((left, right) => right.p0Count - left.p0Count || right.hitCount - left.hitCount || left.owner.localeCompare(right.owner));
}
function issueSla(issueType: string) {
return alertRuleTemplates.find((item) => item.issueType === issueType)?.sla ?? '当日闭环';
}
function priorityIssueLevel(row: QualityIssueRow): 'P0' | 'P1' {
if (row.severity === 'error' || row.issueType === 'NO_SOURCE') {
return 'P0';
}
return 'P1';
}
function priorityVehicleLabel(row: QualityIssueRow) {
const identity = row.vin?.trim() || row.phone?.trim() || row.sourceEndpoint?.trim() || '-';
return [row.plate?.trim(), identity].filter(Boolean).join(' / ');
}
function priorityIssueRows(rows: QualityIssueRow[]): PriorityIssueRow[] {
return rows
.map((row) => {
const action = qualityActionRecommendation(row);
return {
...row,
priority: priorityIssueLevel(row),
actionLabel: action.label,
actionDetail: action.detail,
sla: issueSla(row.issueType),
vehicleLabel: priorityVehicleLabel(row)
};
})
.sort((a, b) => {
if (a.priority !== b.priority) return a.priority === 'P0' ? -1 : 1;
const weightDelta = (issuePriorityWeight[b.issueType] ?? 0) - (issuePriorityWeight[a.issueType] ?? 0);
if (weightDelta !== 0) return weightDelta;
return String(b.lastSeen ?? '').localeCompare(String(a.lastSeen ?? ''));
})
.slice(0, 5);
}
function priorityIssueFromRow(row: QualityIssueRow): PriorityIssueRow {
return priorityIssueRows([row])[0];
}
function priorityIssueNotificationText(row: PriorityIssueRow) {
const lookup = qualityIssueVehicleLookup(row);
const dateFrom = issueEvidenceDate(row.lastSeen);
const dateTo = nextDate(dateFrom);
const evidenceFilters = {
...(dateFrom ? { dateFrom } : {}),
...(dateTo ? { dateTo } : {})
};
return [
`${row.priority} 告警通知】${qualityIssueLabel(row.issueType)}`,
`车辆:${row.vehicleLabel}`,
`数据来源:${qualityProtocolLabel(row.protocol)}`,
`问题:${qualityIssueLabel(row.issueType)}`,
`建议动作:${row.actionLabel}`,
`SLA${row.sla}`,
`最后时间:${row.lastSeen || '-'}`,
`详情:${row.detail || '-'}`,
`实时定位:${appURL(buildAppHash({ page: 'realtime', keyword: lookup.key, protocol: row.protocol }))}`,
`轨迹证据:${appURL(buildAppHash({ page: 'history', keyword: lookup.key, protocol: row.protocol, filters: evidenceFilters }))}`,
`原始记录:${appURL(buildAppHash({ page: 'history-query', keyword: lookup.key, protocol: row.protocol, filters: { tab: 'raw', ...evidenceFilters, includeFields: 'true' } }))}`,
`车辆服务:${appURL(buildAppHash({ page: 'detail', keyword: lookup.key, protocol: row.protocol }))}`,
`告警筛选:${qualityShareURL()}`
].join('\n');
}
function alertRuleTemplate(issueType: string) {
return alertRuleTemplates.find((item) => item.issueType === issueType);
}
function acceptanceCriteria(row: PriorityIssueRow) {
if (row.issueType === 'VIN_MISSING') {
return '车辆能解析到 VIN实时、历史和原始记录可通过同一车辆服务查询';
}
if (row.issueType === 'NO_SOURCE') {
return '至少一个生产来源恢复在线,车辆实时状态、历史轨迹和原始记录可查询';
}
if (row.issueType === 'LINK_GAP') {
return '来源恢复连续上报,实时在线窗口和历史轨迹无异常断点';
}
if (row.issueType === 'FIELD_MISSING') {
return '缺失字段恢复解析,统计、定位或里程依赖的核心字段可查询';
}
return '车辆服务状态恢复正常,告警不再命中当前规则';
}
function priorityIssueTicketText(row: PriorityIssueRow) {
const lookup = qualityIssueVehicleLookup(row);
const dateFrom = issueEvidenceDate(row.lastSeen);
const dateTo = nextDate(dateFrom);
const evidenceFilters = {
...(dateFrom ? { dateFrom } : {}),
...(dateTo ? { dateTo } : {})
};
const rule = alertRuleTemplate(row.issueType);
return [
'【告警处置工单】',
`标题:[${row.priority}] ${qualityIssueLabel(row.issueType)} - ${row.vehicleLabel}`,
`优先级:${row.priority}`,
`责任团队:${rule?.owner || '数据治理'}`,
`触发规则:${rule?.trigger || qualityIssueLabel(row.issueType)}`,
`车辆:${row.vehicleLabel}`,
`数据来源:${qualityProtocolLabel(row.protocol)}`,
`来源地址:${row.sourceEndpoint || '-'}`,
`问题:${qualityIssueLabel(row.issueType)}`,
`建议动作:${row.actionLabel}`,
`SLA${row.sla}`,
`最后时间:${row.lastSeen || '-'}`,
`问题说明:${row.detail || '-'}`,
`处置说明:${row.actionDetail || '-'}`,
'处置步骤:',
'1. 打开车辆服务核对当前身份解析结果',
'2. 核对实时定位、轨迹证据和原始记录',
'3. 完成后确认车辆服务状态恢复,问题不再命中当前告警',
`验收标准:${acceptanceCriteria(row)}`,
`实时定位:${appURL(buildAppHash({ page: 'realtime', keyword: lookup.key, protocol: row.protocol }))}`,
`轨迹证据:${appURL(buildAppHash({ page: 'history', keyword: lookup.key, protocol: row.protocol, filters: evidenceFilters }))}`,
`原始记录:${appURL(buildAppHash({ page: 'history-query', keyword: lookup.key, protocol: row.protocol, filters: { tab: 'raw', ...evidenceFilters, includeFields: 'true' } }))}`,
`车辆服务:${appURL(buildAppHash({ page: 'detail', keyword: lookup.key, protocol: row.protocol }))}`,
`告警筛选:${qualityShareURL()}`
].join('\n');
}
function priorityIssueEvidencePackageText(row: PriorityIssueRow) {
const lookup = qualityIssueVehicleLookup(row);
const dateFrom = issueEvidenceDate(row.lastSeen);
const dateTo = nextDate(dateFrom);
const evidenceFilters = {
...(dateFrom ? { dateFrom } : {}),
...(dateTo ? { dateTo } : {})
};
return [
'【告警证据包】',
`优先级:${row.priority}`,
`车辆:${row.vehicleLabel}`,
`数据来源:${qualityProtocolLabel(row.protocol)}`,
`来源地址:${row.sourceEndpoint || '-'}`,
`问题:${qualityIssueLabel(row.issueType)}`,
`建议动作:${row.actionLabel}`,
`SLA${row.sla}`,
`最后时间:${row.lastSeen || '-'}`,
`详情:${row.detail || '-'}`,
`验收标准:${acceptanceCriteria(row)}`,
`实时定位:${appURL(buildAppHash({ page: 'realtime', keyword: lookup.key, protocol: row.protocol }))}`,
`轨迹回放:${appURL(buildAppHash({ page: 'history', keyword: lookup.key, protocol: row.protocol, filters: evidenceFilters }))}`,
`原始记录:${appURL(buildAppHash({ page: 'history-query', keyword: lookup.key, protocol: row.protocol, filters: { tab: 'raw', ...evidenceFilters, includeFields: 'true' } }))}`,
`里程复核:${appURL(buildAppHash({ page: 'mileage', keyword: lookup.key, protocol: row.protocol, filters: evidenceFilters }))}`,
`车辆服务:${appURL(buildAppHash({ page: 'detail', keyword: lookup.key, protocol: row.protocol }))}`,
`告警筛选:${qualityShareURL()}`
].join('\n');
}
function priorityDigestPolicyLines(policies?: QualityNotificationPolicy[]) {
const policy = policies?.find((item) => item.name.startsWith('P0')) ?? policies?.[0];
if (!policy) return [];
const escalation = formatEscalationMinutes(policy.escalationMinutes);
return [
`通知升级:${[policy.name, escalation, policy.target].filter(Boolean).join(' / ')}`,
...(policy.acceptanceCriteria ? [`验收标准:${policy.acceptanceCriteria}`] : [])
];
}
function priorityIssueDigestText(rows: PriorityIssueRow[], summary: QualitySummary, platformRelease?: string, policies?: QualityNotificationPolicy[]) {
const p0Count = rows.filter((row) => row.priority === 'P0').length;
const p1Count = rows.filter((row) => row.priority === 'P1').length;
const release = platformRelease?.trim();
const policyLines = priorityDigestPolicyLines(policies);
const lines = rows.map((row, index) => [
`${index + 1}. [${row.priority}] ${row.vehicleLabel}`,
` 来源:${qualityProtocolLabel(row.protocol)} / 问题:${qualityIssueLabel(row.issueType)}`,
` 建议动作:${row.actionLabel} / SLA${row.sla}`,
` 最后时间:${row.lastSeen || '-'} / 详情:${row.detail || '-'}`
].join('\n'));
return [
'【告警优先队列汇总】',
`问题车辆:${summary.issueVehicleCount.toLocaleString()},问题记录:${summary.issueRecordCount.toLocaleString()}`,
`P0${p0Count.toLocaleString()}P1${p1Count.toLocaleString()}`,
...(release ? [`运行版本:${release}`] : []),
...policyLines,
'',
...lines,
'',
`告警筛选:${qualityShareURL()}`
].join('\n');
}
function slaMinutes(row: PriorityIssueRow) {
if (row.issueType === 'NO_SOURCE') return 30;
if (row.issueType === 'VIN_MISSING') return 120;
if (row.issueType === 'LINK_GAP') return 60;
if (row.issueType === 'FIELD_MISSING') return 24 * 60;
if (row.sla.includes('15 分钟')) return 15;
if (row.sla.includes('30 分钟')) return 30;
if (row.sla.includes('1 小时')) return 60;
if (row.sla.includes('2 小时')) return 120;
return 24 * 60;
}
function formatDuration(minutes: number) {
const value = Math.max(0, Math.round(minutes));
if (value < 60) return `${value} 分钟`;
const hours = Math.floor(value / 60);
const rest = value % 60;
return rest > 0 ? `${hours} 小时 ${rest} 分钟` : `${hours} 小时`;
}
function formatEscalationMinutes(minutes?: number) {
if (!Number.isFinite(minutes) || Number(minutes) <= 0) return '';
return `${formatDuration(Number(minutes))}升级`;
}
function parseIssueTime(value?: string) {
const raw = String(value ?? '').trim();
if (!raw) return Number.NaN;
const normalized = raw.includes('T') ? raw : raw.replace(' ', 'T');
const parsed = Date.parse(normalized);
if (Number.isFinite(parsed)) return parsed;
return Date.parse(`${raw.replace(' ', 'T')}+08:00`);
}
function escalationClock(row: PriorityIssueRow, nowMs = Date.now()) {
const lastSeenMs = parseIssueTime(row.lastSeen);
if (!Number.isFinite(lastSeenMs)) {
return { status: 'SLA 待确认', color: 'grey' as const, detail: '缺少告警时间', deadline: '-' };
}
const minutes = slaMinutes(row);
const deadlineMs = lastSeenMs + minutes * 60 * 1000;
const diffMinutes = (deadlineMs - nowMs) / 60000;
const deadline = new Date(deadlineMs).toLocaleString('zh-CN', { hour12: false });
if (diffMinutes < 0) {
return { status: '超 SLA', color: 'red' as const, detail: `已超 ${formatDuration(Math.abs(diffMinutes))}`, deadline };
}
if (diffMinutes <= Math.max(15, minutes * 0.2)) {
return { status: '即将升级', color: 'orange' as const, detail: `剩余 ${formatDuration(diffMinutes)}`, deadline };
}
return { status: 'SLA 正常', color: 'green' as const, detail: `剩余 ${formatDuration(diffMinutes)}`, deadline };
}
function ruleStatusColor(count: number, level: string): 'green' | 'orange' | 'red' | 'grey' {
if (count <= 0) return 'green';
if (level === 'P0') return 'red';
if (level === 'P1') return 'orange';
return 'grey';
}
function normalizeAlertRules(rows?: QualityAlertRule[]): AlertRuleRow[] | null {
if (!Array.isArray(rows)) return null;
return rows;
}
function normalizeNotificationPolicies(rows?: QualityNotificationPolicy[]): QualityNotificationPolicy[] | null {
if (!Array.isArray(rows)) return null;
return rows;
}
function normalizePriorityIssues(rows?: QualityPriorityIssue[]): PriorityIssueRow[] | null {
if (!Array.isArray(rows)) return null;
return rows;
}
function notificationPolicyRunbookText(policies: QualityNotificationPolicy[], rules: AlertRuleRow[], platformRelease?: string) {
const activeRules = rules.filter((row) => row.count > 0);
const release = platformRelease?.trim();
const lines = policies.map((policy, index) => [
`${index + 1}. ${policy.name} / ${policy.target}`,
` 触发:${policy.condition}`,
` 渠道:${policy.channel}`,
` 升级:${formatEscalationMinutes(policy.escalationMinutes) || '-'}`,
` 验收:${policy.acceptanceCriteria || '-'}`
].join('\n'));
return [
'【告警通知策略Runbook】',
`活跃规则:${activeRules.length.toLocaleString()}`,
`P0规则${activeRules.filter((row) => row.level === 'P0').length.toLocaleString()}`,
...(release ? [`运行版本:${release}`] : []),
'',
...lines,
'',
`告警筛选:${qualityShareURL()}`
].join('\n');
}
function notificationHandoffText({
rows,
summary,
rules,
policies,
filters,
platformRelease
}: {
rows: PriorityIssueRow[];
summary: QualitySummary;
rules: AlertRuleRow[];
policies: QualityNotificationPolicy[];
filters: Record<string, string>;
platformRelease?: string;
}) {
const release = platformRelease?.trim();
const activeRules = rules.filter((row) => row.count > 0);
const p0Rows = rows.filter((row) => row.priority === 'P0');
const p1Rows = rows.filter((row) => row.priority === 'P1');
const primaryPolicy = policies.find((item) => item.name.startsWith('P0')) ?? policies[0];
const filterLines = [
filters.keyword ? `关键词=${filters.keyword}` : '',
filters.protocol ? `数据来源=${qualityProtocolLabel(filters.protocol)}` : '',
filters.issueType ? `问题类型=${qualityIssueLabel(filters.issueType)}` : ''
].filter(Boolean);
const issueLines = rows.length > 0
? rows.map((row, index) => {
const lookup = qualityIssueVehicleLookup(row);
const dateFrom = issueEvidenceDate(row.lastSeen);
const dateTo = nextDate(dateFrom);
const evidenceFilters = {
...(dateFrom ? { dateFrom } : {}),
...(dateTo ? { dateTo } : {})
};
return [
`${index + 1}. [${row.priority}] ${row.vehicleLabel}`,
` 来源:${qualityProtocolLabel(row.protocol)};问题:${qualityIssueLabel(row.issueType)}SLA${row.sla}`,
` 建议动作:${row.actionLabel};最后时间:${row.lastSeen || '-'}`,
` 详情:${row.detail || '-'}`,
` 实时:${appURL(buildAppHash({ page: 'realtime', keyword: lookup.key, protocol: row.protocol }))}`,
` 轨迹:${appURL(buildAppHash({ page: 'history', keyword: lookup.key, protocol: row.protocol, filters: evidenceFilters }))}`,
` 原始记录:${appURL(buildAppHash({ page: 'history-query', keyword: lookup.key, protocol: row.protocol, filters: { tab: 'raw', ...evidenceFilters, includeFields: 'true' } }))}`,
` 车辆服务:${appURL(buildAppHash({ page: 'detail', keyword: lookup.key, protocol: row.protocol }))}`
].join('\n');
})
: ['当前筛选下暂无待通知车辆'];
return [
'【告警通知交接包】',
...(release ? [`运行版本:${release}`] : []),
`当前筛选:${filterLines.length > 0 ? filterLines.join('') : '全部告警'}`,
`问题车辆:${Number(summary.issueVehicleCount ?? 0).toLocaleString()};问题记录:${Number(summary.issueRecordCount ?? 0).toLocaleString()}`,
`优先级P0 ${p0Rows.length.toLocaleString()} 条 / P1 ${p1Rows.length.toLocaleString()}`,
`活跃规则:${activeRules.length.toLocaleString()}${activeRules.length > 0 ? `${activeRules.map((row) => `${row.level} ${row.title || qualityIssueLabel(row.issueType)} ${row.count}`).join('')}` : ''}`,
primaryPolicy ? `主通知策略:${[primaryPolicy.name, primaryPolicy.target, primaryPolicy.channel, formatEscalationMinutes(primaryPolicy.escalationMinutes)].filter(Boolean).join(' / ')}` : '主通知策略:-',
primaryPolicy?.acceptanceCriteria ? `统一验收:${primaryPolicy.acceptanceCriteria}` : '统一验收:车辆服务状态恢复,实时、轨迹和原始记录可查询',
'',
'待通知车辆:',
...issueLines,
'',
`告警事件:${qualityShareURL()}`,
`通知闭环:${appURL(buildAppHash({ page: 'notification-rules' }))}`,
`运维质量:${appURL(buildAppHash({ page: 'ops-quality' }))}`
].join('\n');
}
function alertBusinessImpactText({
summary,
rules,
priorityRows,
health,
filters,
platformRelease
}: {
summary: QualitySummary;
rules: AlertRuleRow[];
priorityRows: PriorityIssueRow[];
health: OpsHealth | null;
filters: Record<string, string>;
platformRelease?: string;
}) {
const release = platformRelease?.trim();
const p0Rows = priorityRows.filter((row) => row.priority === 'P0');
const p1Rows = priorityRows.filter((row) => row.priority === 'P1');
const activeRules = rules.filter((row) => row.count > 0);
const p0Rules = activeRules.filter((row) => row.level === 'P0');
const primaryProtocol = summary.protocols?.[0];
const primaryIssue = summary.issueTypes?.[0];
const storageOk = !health || (health.tdengineWritable && health.mysqlWritable);
const capacityCount = Number(health?.capacityFindings?.length ?? 0);
const severity = !storageOk || p0Rows.length > 0 || p0Rules.length > 0 || Number(summary.errorCount ?? 0) > 0
? '高风险'
: Number(summary.issueVehicleCount ?? 0) > 0 || capacityCount > 0
? '需关注'
: '正常';
const filterLines = [
filters.keyword ? `关键词=${filters.keyword}` : '',
filters.protocol ? `数据来源=${qualityProtocolLabel(filters.protocol)}` : '',
filters.issueType ? `问题类型=${qualityIssueLabel(filters.issueType)}` : ''
].filter(Boolean);
return [
'【告警业务影响报告】',
...(release ? [`运行版本:${release}`] : []),
`影响等级:${severity}`,
`当前筛选:${filterLines.length > 0 ? filterLines.join('') : '全部告警'}`,
`影响车辆:${Number(summary.issueVehicleCount ?? 0).toLocaleString()}`,
`问题记录:${Number(summary.issueRecordCount ?? 0).toLocaleString()}`,
`错误/警告:${Number(summary.errorCount ?? 0).toLocaleString()}/${Number(summary.warningCount ?? 0).toLocaleString()}`,
`优先级P0 ${p0Rows.length.toLocaleString()} 条 / P1 ${p1Rows.length.toLocaleString()}`,
`活跃规则:${activeRules.length.toLocaleString()} 类 / P0规则 ${p0Rules.length.toLocaleString()}`,
`主要来源:${primaryProtocol ? `${qualityProtocolLabel(primaryProtocol.name)} ${primaryProtocol.count.toLocaleString()}` : '-'}`,
`主要问题:${primaryIssue ? `${qualityIssueLabel(primaryIssue.name)} ${primaryIssue.count.toLocaleString()}` : '-'}`,
`容量风险:${storageOk ? '存储可写' : '存储异常'}Kafka Lag ${formatLag(health?.kafkaLag)};容量发现 ${capacityCount.toLocaleString()}`,
`建议动作:${severity === '高风险' ? '立即通知责任团队并按 P0/P1 队列闭环' : severity === '需关注' ? '纳入当日治理,持续观察来源覆盖和字段完整性' : '保持监控'}`,
`告警事件:${qualityShareURL()}`,
`通知闭环:${appURL(buildAppHash({ page: 'notification-rules' }))}`,
`运维质量:${appURL(buildAppHash({ page: 'ops-quality' }))}`
].join('\n');
}
function alertCustomerDecisionText({
summary,
rules,
priorityRows,
health,
filters,
platformRelease
}: {
summary: QualitySummary;
rules: AlertRuleRow[];
priorityRows: PriorityIssueRow[];
health: OpsHealth | null;
filters: Record<string, string>;
platformRelease?: string;
}) {
const release = platformRelease?.trim();
const activeRules = rules.filter((row) => row.count > 0);
const p0Rows = priorityRows.filter((row) => row.priority === 'P0');
const p1Rows = priorityRows.filter((row) => row.priority === 'P1');
const primaryProtocol = summary.protocols?.[0];
const primaryIssue = summary.issueTypes?.[0];
const storageOk = !health || (health.tdengineWritable && health.mysqlWritable);
const capacityCount = Number(health?.capacityFindings?.length ?? 0);
const decision = !storageOk || p0Rows.length > 0 || activeRules.some((row) => row.level === 'P0') || Number(summary.errorCount ?? 0) > 0
? '立即处置'
: Number(summary.issueVehicleCount ?? 0) > 0 || capacityCount > 0
? '当日跟进'
: '持续观察';
const focusIssue = priorityRows[0];
const focusLookup = focusIssue ? qualityIssueVehicleLookup(focusIssue) : undefined;
const filterLines = [
filters.keyword ? `关键词=${filters.keyword}` : '',
filters.protocol ? `数据来源=${qualityProtocolLabel(filters.protocol)}` : '',
filters.issueType ? `问题类型=${qualityIssueLabel(filters.issueType)}` : ''
].filter(Boolean);
return [
'【客户告警决策说明】',
...(release ? [`运行版本:${release}`] : []),
`决策结论:${decision}`,
`筛选范围:${filterLines.length > 0 ? filterLines.join('') : '全部告警'}`,
`影响车辆:${Number(summary.issueVehicleCount ?? 0).toLocaleString()} 辆;问题记录:${Number(summary.issueRecordCount ?? 0).toLocaleString()}`,
`优先队列P0 ${p0Rows.length.toLocaleString()} / P1 ${p1Rows.length.toLocaleString()}`,
`主要来源:${primaryProtocol ? `${qualityProtocolLabel(primaryProtocol.name)} ${primaryProtocol.count.toLocaleString()}` : '-'}`,
`主要问题:${primaryIssue ? `${qualityIssueLabel(primaryIssue.name)} ${primaryIssue.count.toLocaleString()}` : '-'}`,
`链路状态Kafka Lag ${formatLag(health?.kafkaLag)}Redis 在线 ${formatLag(health?.redisOnlineKeys)};存储${storageOk ? '可写' : '异常'};容量发现 ${capacityCount.toLocaleString()}`,
`焦点车辆:${focusIssue ? `${focusIssue.vehicleLabel} / ${qualityProtocolLabel(focusIssue.protocol)} / ${qualityIssueLabel(focusIssue.issueType)}` : '-'}`,
'',
'客户处置路径:',
'1. 先确认影响车辆和 P0/P1 数量。',
'2. P0 立即通知责任团队,超过 SLA 自动升级。',
'3. 每辆焦点车都要打开实时、轨迹、原始记录、里程证据闭环。',
'4. 恢复后确认告警不再命中,并保留交接包。',
'',
`告警事件:${qualityShareURL()}`,
`实时监控:${appURL(buildAppHash({ page: 'realtime', keyword: focusLookup?.key || filters.keyword || '', protocol: focusIssue?.protocol || filters.protocol }))}`,
`轨迹回放:${appURL(buildAppHash({ page: 'history', keyword: focusLookup?.key || filters.keyword || '', protocol: focusIssue?.protocol || filters.protocol }))}`,
`通知闭环:${appURL(buildAppHash({ page: 'notification-rules' }))}`
].join('\n');
}
function alertRecoveryReceiptText({
summary,
impactLabel,
priorityRows,
primaryPolicy,
filters,
platformRelease
}: {
summary: QualitySummary;
impactLabel: string;
priorityRows: PriorityIssueRow[];
primaryPolicy?: QualityNotificationPolicy;
filters: Record<string, string>;
platformRelease?: string;
}) {
const release = platformRelease?.trim();
const filterLines = [
filters.keyword ? `关键词=${filters.keyword}` : '',
filters.protocol ? `数据来源=${qualityProtocolLabel(filters.protocol)}` : '',
filters.issueType ? `问题类型=${qualityIssueLabel(filters.issueType)}` : ''
].filter(Boolean);
const focusRows = priorityRows.slice(0, 5).map((row, index) => {
const lookup = qualityIssueVehicleLookup(row);
return `${index + 1}. ${row.vehicleLabel} / ${qualityProtocolLabel(row.protocol)} / ${lookup.key || '-'} / ${qualityIssueLabel(row.issueType)}`;
});
return [
'【客户告警恢复验收回执】',
...(release ? [`运行版本:${release}`] : []),
`当前筛选:${filterLines.length > 0 ? filterLines.join('') : '全部告警'}`,
`当前影响:${Number(summary.issueVehicleCount ?? 0).toLocaleString()} 辆车 / ${Number(summary.issueRecordCount ?? 0).toLocaleString()} 条告警 / ${impactLabel}`,
'恢复标准:实时可看 / 轨迹可回放 / 里程可核对 / 历史证据可导出',
`通知策略:${primaryPolicy ? [primaryPolicy.name, primaryPolicy.target, primaryPolicy.channel].filter(Boolean).join(' / ') : '-'}`,
`验收口径:${primaryPolicy?.acceptanceCriteria || '车辆服务状态恢复,实时、轨迹、历史和里程证据可查询。'}`,
'',
'待复核车辆:',
...(focusRows.length > 0 ? focusRows : ['当前没有优先车辆']),
'',
`告警事件:${qualityShareURL()}`,
`实时监控:${appURL(buildAppHash({ page: 'realtime', filters }))}`,
`轨迹回放:${appURL(buildAppHash({ page: 'history', filters }))}`,
`里程统计:${appURL(buildAppHash({ page: 'mileage', filters }))}`,
`历史导出:${appURL(buildAppHash({ page: 'history-query', filters: { ...filters, tab: 'raw', includeFields: 'true' } }))}`
].join('\n');
}
async function copyText(value: string, label: string) {
const text = value.trim();
if (!text) {
Toast.warning(`${label}为空`);
return;
}
try {
await navigator.clipboard.writeText(text);
Toast.success(`已复制${label}`);
} catch {
Toast.error(`复制${label}失败`);
}
}
async function copyQualityShareURL() {
await copyText(qualityShareURL(), '筛选链接');
}
export function Quality({
onOpenVehicle,
onOpenRealtime,
onOpenHistory,
onOpenRaw,
onOpenMileage,
onOpenNotificationRules,
onHealthLoaded,
onNotificationPlanLoaded,
onFiltersChange,
initialFilters = {}
}: {
onOpenVehicle: (vin: string, protocol?: string) => void;
onOpenRealtime?: (filters: Record<string, string>) => void;
onOpenHistory?: (filters: Record<string, string>) => void;
onOpenRaw?: (filters: Record<string, string>) => void;
onOpenMileage?: (filters: Record<string, string>) => void;
onOpenNotificationRules?: () => void;
onHealthLoaded?: (health: OpsHealth) => void;
onNotificationPlanLoaded?: (plan: QualityNotificationPlan) => void;
onFiltersChange?: (filters: Record<string, string>) => void;
initialFilters?: Record<string, string>;
}) {
const [issues, setIssues] = useState<QualityIssueRow[]>([]);
const [summary, setSummary] = useState<QualitySummary>(emptySummary);
const [health, setHealth] = useState<OpsHealth | null>(null);
const [notificationPlan, setNotificationPlan] = useState<QualityNotificationPlan | null>(null);
const [loadingIssues, setLoadingIssues] = useState(true);
const [loadingSummary, setLoadingSummary] = useState(true);
const [loadingHealth, setLoadingHealth] = useState(true);
const [filters, setFilters] = useState<Record<string, string>>(initialFilters);
const [pagination, setPagination] = useState({ currentPage: 1, pageSize: 20, total: 0 });
const summaryIssueTypes = Array.isArray(summary.issueTypes) ? summary.issueTypes : [];
const summaryProtocols = Array.isArray(summary.protocols) ? summary.protocols : [];
const issueVehicleCount = Number(summary.issueVehicleCount ?? 0);
const issueRecordCount = Number(summary.issueRecordCount ?? 0);
const errorCount = Number(summary.errorCount ?? 0);
const warningCount = Number(summary.warningCount ?? 0);
const primaryIssueType = summaryIssueTypes[0]?.name;
const rules = normalizeAlertRules(notificationPlan?.rules) ?? alertRuleRows(summary, health);
const policies = normalizeNotificationPolicies(notificationPlan?.policies) ?? notificationPolicies;
const activeRuleCount = notificationPlan?.activeRuleCount ?? rules.filter((item) => item.count > 0).length;
const p0RuleCount = notificationPlan?.p0RuleCount ?? rules.filter((item) => item.count > 0 && item.level === 'P0').length;
const priorityRows = normalizePriorityIssues(notificationPlan?.priorityIssues) ?? priorityIssueRows(issues);
const dispatchOwnerRows = alertDispatchOwnerRows(rules);
const priorityP0Count = priorityRows.filter((row) => row.priority === 'P0').length;
const priorityP1Count = priorityRows.filter((row) => row.priority === 'P1').length;
const focusIssue = priorityRows[0];
const focusLookup = focusIssue ? qualityIssueVehicleLookup(focusIssue) : undefined;
const activeRulesForImpact = rules.filter((row) => row.count > 0);
const storageWritable = !health || (health.tdengineWritable && health.mysqlWritable);
const capacityFindingCount = health?.capacityFindings?.length ?? 0;
const impactSeverity = !storageWritable || priorityP0Count > 0 || p0RuleCount > 0 || errorCount > 0
? { label: '高风险', color: 'red' as const, detail: '需要立即通知责任团队并按 P0/P1 队列闭环。' }
: issueVehicleCount > 0 || capacityFindingCount > 0
? { label: '需关注', color: 'orange' as const, detail: '纳入当日治理,持续观察来源覆盖和字段完整性。' }
: { label: '正常', color: 'green' as const, detail: '当前没有明显业务影响,保持监控。' };
const customerDecision = impactSeverity.label === '高风险'
? { label: '立即处置', color: 'red' as const, detail: '优先确认客户车辆服务是否受影响,并立即触发通知闭环。' }
: impactSeverity.label === '需关注'
? { label: '当日跟进', color: 'orange' as const, detail: '纳入当日数据治理,持续观察来源恢复和车辆证据。' }
: { label: '持续观察', color: 'green' as const, detail: '当前没有客户侧高优先级影响,保持常规监控。' };
const primaryPolicy = policies.find((item) => item.name.startsWith('P0')) ?? policies[0];
const secondaryPolicy = policies.find((item) => item.name.startsWith('P1')) ?? policies[1] ?? primaryPolicy;
const primaryProtocolImpact = summaryProtocols[0];
const primaryIssueImpact = summaryIssueTypes[0];
const impactCards = [
{ label: '影响车辆', value: `${issueVehicleCount.toLocaleString()}`, detail: `${issueRecordCount.toLocaleString()} 条问题记录`, color: issueVehicleCount > 0 ? 'orange' as const : 'green' as const },
{ label: '优先级', value: `P0 ${priorityP0Count} / P1 ${priorityP1Count}`, detail: `${p0RuleCount} 类 P0 规则`, color: priorityP0Count > 0 || p0RuleCount > 0 ? 'red' as const : 'green' as const },
{ label: '主要来源', value: primaryProtocolImpact ? qualityProtocolLabel(primaryProtocolImpact.name) : '-', detail: primaryProtocolImpact ? `${primaryProtocolImpact.count.toLocaleString()}` : '暂无来源问题', color: primaryProtocolImpact ? 'blue' as const : 'green' as const },
{ label: '主要问题', value: primaryIssueImpact ? qualityIssueLabel(primaryIssueImpact.name) : '-', detail: primaryIssueImpact ? `${primaryIssueImpact.count.toLocaleString()}` : '暂无问题类型', color: primaryIssueImpact ? 'orange' as const : 'green' as const },
{ label: '容量与存储', value: storageWritable ? '可写' : '异常', detail: `Kafka Lag ${formatLag(health?.kafkaLag)} / 发现 ${capacityFindingCount}`, color: storageWritable && capacityFindingCount === 0 ? 'green' as const : 'red' as const }
];
const customerNotificationSlaItems = [
{
label: '客户影响范围',
value: `${issueVehicleCount.toLocaleString()} 辆车`,
detail: `${issueRecordCount.toLocaleString()} 条告警记录,优先判断实时、轨迹和里程是否受影响。`,
color: issueVehicleCount > 0 ? 'orange' as const : 'green' as const,
action: '影响摘要',
onClick: () => copyBusinessImpact()
},
{
label: '通知负责人',
value: primaryPolicy?.target || '待配置',
detail: primaryPolicy ? `${primaryPolicy.name} / ${primaryPolicy.channel}` : '缺少通知策略,建议先配置责任团队。',
color: primaryPolicy ? 'blue' as const : 'orange' as const,
action: '通知闭环',
onClick: () => onOpenNotificationRules?.()
},
{
label: '升级时限',
value: formatEscalationMinutes(primaryPolicy?.escalationMinutes) || '未配置',
detail: secondaryPolicy && secondaryPolicy !== primaryPolicy ? `P1 参考:${secondaryPolicy.target} / ${formatEscalationMinutes(secondaryPolicy.escalationMinutes) || '未配置'}` : '高优先级事件需要明确超时升级路径。',
color: priorityP0Count > 0 ? 'red' as const : priorityP1Count > 0 ? 'orange' as const : 'green' as const,
action: '复制策略',
onClick: () => copyPolicyRunbook()
},
{
label: '恢复验收',
value: primaryPolicy?.acceptanceCriteria ? '有标准' : '待补充',
detail: primaryPolicy?.acceptanceCriteria || '车辆服务状态恢复,实时、轨迹、历史和里程证据可查询。',
color: primaryPolicy?.acceptanceCriteria ? 'green' as const : 'orange' as const,
action: '交接包',
onClick: () => copyNotificationHandoff()
}
];
const notificationChannelItems = (primaryPolicy?.channel || '站内告警')
.split('/')
.map((item) => item.trim())
.filter(Boolean)
.map((channel) => ({
channel,
target: primaryPolicy?.target || '待配置责任人',
condition: primaryPolicy?.condition || '告警规则命中',
escalation: formatEscalationMinutes(primaryPolicy?.escalationMinutes) || '未配置升级',
acceptance: primaryPolicy?.acceptanceCriteria || '车辆服务状态恢复,实时、轨迹、历史和里程证据可查询。'
}));
const loadIssues = (values: Record<string, string> = filters, page = pagination.currentPage, pageSize = pagination.pageSize) => {
setLoadingIssues(true);
const params = qualityParams(values);
params.set('limit', String(pageSize));
params.set('offset', String((page - 1) * pageSize));
api.alertEvents(params)
.then((nextPage) => {
setIssues(nextPage.items);
setPagination({ currentPage: page, pageSize, total: nextPage.total });
})
.catch((error: Error) => Toast.error(error.message))
.finally(() => setLoadingIssues(false));
};
const loadSummary = (values: Record<string, string> = filters) => {
setLoadingSummary(true);
api.alertEventSummary(qualityParams(values))
.then(setSummary)
.catch((error: Error) => Toast.error(error.message))
.finally(() => setLoadingSummary(false));
};
const loadHealth = () => {
setLoadingHealth(true);
api.opsHealth()
.then((nextHealth) => {
setHealth(nextHealth);
onHealthLoaded?.(nextHealth);
})
.catch((error: Error) => Toast.error(error.message))
.finally(() => setLoadingHealth(false));
};
const loadNotificationPlan = (values: Record<string, string> = filters, pageSize = pagination.pageSize) => {
const params = qualityParams(values);
params.set('limit', String(pageSize));
api.alertEventNotificationPlan(params)
.then((plan) => {
if (Array.isArray(plan.rules) && Array.isArray(plan.policies) && Array.isArray(plan.priorityIssues)) {
setNotificationPlan(plan);
onNotificationPlanLoaded?.(plan);
}
})
.catch(() => {
setNotificationPlan(null);
});
};
useEffect(() => {
setFilters(initialFilters);
loadSummary(initialFilters);
loadIssues(initialFilters, 1, pagination.pageSize);
loadHealth();
loadNotificationPlan(initialFilters, pagination.pageSize);
}, [JSON.stringify(initialFilters)]);
const applyFilters = (nextFilters: Record<string, string>) => {
setFilters(nextFilters);
onFiltersChange?.(nextFilters);
loadSummary(nextFilters);
loadIssues(nextFilters, 1, pagination.pageSize);
loadNotificationPlan(nextFilters, pagination.pageSize);
};
const drillPrimaryIssue = () => {
if (!primaryIssueType) {
return;
}
applyFilters({ ...filters, issueType: primaryIssueType });
};
const drillProtocol = (protocol: string) => {
applyFilters({ ...filters, protocol });
};
const drillIssueType = (issueType: string) => {
applyFilters({ ...filters, issueType });
};
const filterSummary = [
filters.keyword ? `关键词:${filters.keyword}` : '',
filters.protocol ? `数据来源:${qualityProtocolLabel(filters.protocol)}` : '',
filters.issueType ? `问题类型:${qualityIssueLabel(filters.issueType)}` : ''
].filter(Boolean);
const openIssueHistory = (row: QualityIssueRow) => {
const lookup = qualityIssueVehicleLookup(row);
const dateFrom = issueEvidenceDate(row.lastSeen);
const dateTo = nextDate(dateFrom);
onOpenHistory?.({
keyword: lookup.key,
protocol: row.protocol,
...(dateFrom ? { dateFrom } : {}),
...(dateTo ? { dateTo } : {})
});
};
const openIssueRaw = (row: QualityIssueRow) => {
const lookup = qualityIssueVehicleLookup(row);
const dateFrom = issueEvidenceDate(row.lastSeen);
const dateTo = nextDate(dateFrom);
onOpenRaw?.({
keyword: lookup.key,
protocol: row.protocol,
...(dateFrom ? { dateFrom } : {}),
...(dateTo ? { dateTo } : {}),
includeFields: 'true'
});
};
const openIssueRealtime = (row: QualityIssueRow) => {
const lookup = qualityIssueVehicleLookup(row);
onOpenRealtime?.({
keyword: lookup.key,
protocol: row.protocol
});
};
const openIssueMileage = (row: QualityIssueRow) => {
const lookup = qualityIssueVehicleLookup(row);
const dateFrom = issueEvidenceDate(row.lastSeen);
const dateTo = nextDate(dateFrom);
onOpenMileage?.({
keyword: lookup.key,
protocol: row.protocol,
...(dateFrom ? { dateFrom } : {}),
...(dateTo ? { dateTo } : {})
});
};
const copyPriorityDigest = () => {
if (priorityRows.length === 0) {
Toast.warning('当前没有可复制的优先告警');
return;
}
copyText(priorityIssueDigestText(priorityRows, notificationPlan?.summary ?? summary, health?.runtime?.platformRelease, policies), '优先队列通知汇总');
};
const copyPolicyRunbook = () => {
copyText(notificationPolicyRunbookText(policies, rules, health?.runtime?.platformRelease), '通知策略Runbook');
};
const copyNotificationHandoff = () => {
copyText(notificationHandoffText({
rows: priorityRows,
summary: notificationPlan?.summary ?? summary,
rules,
policies,
filters,
platformRelease: health?.runtime?.platformRelease
}), '告警通知交接包');
};
const copyBusinessImpact = () => {
copyText(alertBusinessImpactText({
summary: notificationPlan?.summary ?? summary,
rules,
priorityRows,
health,
filters,
platformRelease: health?.runtime?.platformRelease
}), '业务影响报告');
};
const copyAlertCustomerDecision = () => {
copyText(alertCustomerDecisionText({
summary: notificationPlan?.summary ?? summary,
rules,
priorityRows,
health,
filters,
platformRelease: health?.runtime?.platformRelease
}), '客户告警决策说明');
};
const copyAlertRecoveryReceipt = () => {
copyText(alertRecoveryReceiptText({
summary: notificationPlan?.summary ?? summary,
impactLabel: impactSeverity.label,
priorityRows,
primaryPolicy,
filters,
platformRelease: health?.runtime?.platformRelease
}), '客户告警恢复验收回执');
};
const alertRecoveryAcceptanceItems = [
{
label: '实时恢复',
value: storageWritable ? '可验证' : '先修复存储',
detail: '确认客户能看到车辆实时状态、在线状态和最新协议字段。',
color: storageWritable ? 'green' as const : 'red' as const,
action: '实时确认',
displayAction: '实时验收',
onClick: () => {
if (onOpenRealtime) {
onOpenRealtime(filters);
} else {
window.location.hash = buildAppHash({ page: 'realtime', filters });
}
}
},
{
label: '轨迹恢复',
value: issueVehicleCount > 0 ? '需复核' : '正常',
detail: '用受影响车辆回放轨迹,确认定位、速度和时间连续可查。',
color: issueVehicleCount > 0 ? 'orange' as const : 'green' as const,
action: '轨迹复核',
displayAction: '轨迹验收',
onClick: () => {
if (onOpenHistory) {
onOpenHistory(filters);
} else {
window.location.hash = buildAppHash({ page: 'history', filters });
}
}
},
{
label: '里程恢复',
value: primaryIssueType ? qualityIssueLabel(primaryIssueType) : '可核对',
detail: '用区间里程和历史证据确认统计结果能解释、能导出、能复盘。',
color: primaryIssueType ? 'orange' as const : 'green' as const,
action: '里程核对',
displayAction: '里程验收',
onClick: () => {
if (onOpenMileage) {
onOpenMileage(filters);
} else {
window.location.hash = buildAppHash({ page: 'mileage', filters });
}
}
},
{
label: '客户回执',
value: primaryPolicy?.target || '责任人待配置',
detail: '复制一份包含影响范围、验收标准、证据入口和待复核车辆的恢复回执。',
color: primaryPolicy ? 'blue' as const : 'orange' as const,
action: '复制回执',
displayAction: '回执复制',
onClick: copyAlertRecoveryReceipt
}
];
const alertActionConclusionItems = [
{
label: '影响范围',
value: `${issueVehicleCount.toLocaleString()}`,
detail: `${issueRecordCount.toLocaleString()} 条告警记录,先判断是否影响客户看车、轨迹和里程。`,
color: issueVehicleCount > 0 ? 'orange' as const : 'green' as const,
action: '复制影响',
onClick: () => copyBusinessImpact()
},
{
label: '通知策略',
value: `P0 ${priorityP0Count.toLocaleString()} / P1 ${priorityP1Count.toLocaleString()}`,
detail: priorityRows.length > 0 ? '已进入责任团队通知队列。' : '当前没有待通知车辆。',
color: priorityP0Count > 0 ? 'red' as const : priorityP1Count > 0 ? 'orange' as const : 'green' as const,
action: '复制通知',
onClick: () => copyPriorityDigest()
},
{
label: 'SLA策略',
value: customerDecision.label,
detail: primaryPolicy ? `${primaryPolicy.name} / ${formatEscalationMinutes(primaryPolicy.escalationMinutes) || '未配置升级'}` : '需要补充通知策略和升级时限。',
color: customerDecision.color,
action: '通知闭环',
onClick: () => onOpenNotificationRules?.()
},
{
label: '证据闭环',
value: storageWritable ? '可复核' : '先修复存储',
detail: '通知必须带上实时、轨迹、里程和历史证据入口。',
color: storageWritable ? 'green' as const : 'red' as const,
action: '复制交接',
onClick: () => copyNotificationHandoff()
},
{
label: '恢复回执',
value: primaryPolicy?.acceptanceCriteria ? '有标准' : '待补充',
detail: primaryPolicy?.acceptanceCriteria || '恢复后确认车辆服务状态、轨迹、历史和里程证据可查询。',
color: primaryPolicy?.acceptanceCriteria ? 'green' as const : 'orange' as const,
action: '复制回执',
onClick: () => copyAlertRecoveryReceipt()
}
];
const alertCustomerServiceItems = [
{
label: '影响车辆',
value: `${issueVehicleCount.toLocaleString()}`,
detail: `${issueRecordCount.toLocaleString()} 条告警记录,先判断是否影响实时、轨迹和里程。`,
color: issueVehicleCount > 0 ? 'orange' as const : 'green' as const,
action: '复制影响',
disabled: false,
onClick: () => copyBusinessImpact()
},
{
label: '待通知',
value: `P0 ${priorityP0Count.toLocaleString()} / P1 ${priorityP1Count.toLocaleString()}`,
detail: priorityRows.length > 0 ? '已按优先级生成责任团队通知。' : '当前没有需要通知的告警车辆。',
color: priorityP0Count > 0 ? 'red' as const : priorityP1Count > 0 ? 'orange' as const : 'green' as const,
action: '复制通知',
disabled: priorityRows.length === 0,
onClick: () => copyPriorityDigest()
},
{
label: '焦点车辆',
value: focusIssue?.vehicleLabel || '暂无',
detail: focusIssue ? `${qualityProtocolLabel(focusIssue.protocol)} / ${qualityIssueLabel(focusIssue.issueType)}` : '没有焦点车辆时保持监控。',
color: focusIssue ? 'blue' as const : 'green' as const,
action: '车辆服务',
disabled: !focusIssue || !focusLookup?.key,
onClick: () => focusIssue && onOpenVehicle(focusLookup?.key ?? '', focusIssue.protocol)
},
{
label: '证据闭环',
value: priorityRows.length > 0 ? '可复核' : '待观察',
detail: '通知必须带上车辆服务、实时、轨迹、原始记录和里程证据。',
color: priorityRows.length > 0 ? 'blue' as const : 'green' as const,
action: '复制交接',
disabled: false,
onClick: () => copyNotificationHandoff()
},
{
label: '恢复验收',
value: primaryPolicy?.acceptanceCriteria ? '有标准' : '待补充',
detail: primaryPolicy?.acceptanceCriteria || '车辆服务状态恢复,实时、轨迹、历史和里程证据可查询。',
color: primaryPolicy?.acceptanceCriteria ? 'green' as const : 'orange' as const,
action: '复制回执',
disabled: false,
onClick: () => copyAlertRecoveryReceipt()
}
];
const alertClosureTimelineItems = [
{
step: '1',
label: '告警触发',
value: `${issueRecordCount.toLocaleString()} 条事件`,
detail: primaryIssueImpact ? `主要问题:${qualityIssueLabel(primaryIssueImpact.name)}` : '等待告警规则命中。',
action: '查看事件',
color: issueRecordCount > 0 ? 'orange' as const : 'green' as const,
onClick: () => loadIssues(filters, pagination.currentPage, pagination.pageSize)
},
{
step: '2',
label: '影响车辆',
value: `${issueVehicleCount.toLocaleString()} 辆车`,
detail: '先判断实时、轨迹、里程和历史导出是否受影响。',
action: '影响摘要',
color: issueVehicleCount > 0 ? 'orange' as const : 'green' as const,
onClick: copyBusinessImpact
},
{
step: '3',
label: '责任通知',
value: primaryPolicy?.target || '责任人待配置',
detail: primaryPolicy ? `${primaryPolicy.name} / ${formatEscalationMinutes(primaryPolicy.escalationMinutes) || '未配置升级'}` : '需要补充通知策略和升级时限。',
action: '通知策略',
color: primaryPolicy ? 'blue' as const : 'orange' as const,
onClick: () => onOpenNotificationRules?.()
},
{
step: '4',
label: '证据复核',
value: '实时/轨迹/里程',
detail: '每次通知都要带上可打开的车辆证据入口和筛选范围。',
action: '证据交接',
color: 'blue' as const,
onClick: copyNotificationHandoff
},
{
step: '5',
label: '恢复验收',
value: primaryPolicy?.acceptanceCriteria ? '有标准' : '待补充',
detail: primaryPolicy?.acceptanceCriteria || '确认车辆服务恢复,实时、轨迹、历史和里程证据可查。',
action: '复制回执',
color: primaryPolicy?.acceptanceCriteria ? 'green' as const : 'orange' as const,
onClick: copyAlertRecoveryReceipt
}
];
return (
<div className="vp-page">
<PageHeader title="告警事件" description="围绕车辆服务沉淀断链、VIN 缺失、字段缺失和链路健康事件,并形成通知闭环" />
<section className="vp-alert-action-conclusion" aria-label="告警处置结论栏">
<div className="vp-alert-action-conclusion-summary">
<Space wrap>
<Tag color={customerDecision.color}></Tag>
<Tag color={priorityP0Count > 0 ? 'red' : priorityP1Count > 0 ? 'orange' : 'green'}>
P0 {priorityP0Count.toLocaleString()} / P1 {priorityP1Count.toLocaleString()}
</Tag>
</Space>
<strong>SLA</strong>
</div>
<div className="vp-alert-action-conclusion-grid">
{alertActionConclusionItems.map((item) => (
<button
key={item.label}
type="button"
className="vp-alert-action-conclusion-item"
onClick={item.onClick}
aria-label={`告警处置结论栏 ${item.label} ${item.value} ${item.action}`}
>
<Tag color={item.color}>{item.label}</Tag>
<strong>{item.value}</strong>
<span>{item.detail}</span>
<em>{item.action}</em>
</button>
))}
</div>
</section>
<section className="vp-alert-customer-service-desk" aria-label="客户告警服务台">
<div className="vp-alert-customer-service-summary">
<Space wrap>
<Tag color={customerDecision.color}></Tag>
<Tag color={priorityP0Count > 0 ? 'red' : priorityP1Count > 0 ? 'orange' : 'green'}>
P0 {priorityP0Count.toLocaleString()} / P1 {priorityP1Count.toLocaleString()}
</Tag>
<Tag color={storageWritable ? 'green' : 'red'}>{storageWritable ? '证据可查' : '存储异常'}</Tag>
</Space>
<strong></strong>
<span>
</span>
</div>
<div className="vp-alert-customer-service-grid">
{alertCustomerServiceItems.map((item) => (
<button
key={item.label}
type="button"
className="vp-alert-customer-service-item"
disabled={item.disabled}
onClick={item.onClick}
aria-label={`客户告警服务台 ${item.label} ${item.value} ${item.action}`}
>
<Tag color={item.color}>{item.label}</Tag>
<strong>{item.value}</strong>
<span>{item.detail}</span>
<em>{item.action}</em>
</button>
))}
</div>
</section>
<Card bordered title="客户告警闭环时间线">
<div className="vp-alert-closure-timeline">
<div className="vp-alert-closure-summary">
<Space wrap>
<Tag color={customerDecision.color}>{customerDecision.label}</Tag>
<Tag color={priorityP0Count > 0 ? 'red' : priorityP1Count > 0 ? 'orange' : 'green'}>
P0 {priorityP0Count.toLocaleString()} / P1 {priorityP1Count.toLocaleString()}
</Tag>
<Tag color={primaryPolicy ? 'blue' : 'orange'}>{primaryPolicy?.name || '策略待配置'}</Tag>
</Space>
<strong></strong>
<span>
线便
</span>
</div>
<div className="vp-alert-closure-steps">
{alertClosureTimelineItems.map((item) => (
<button
key={item.label}
type="button"
className="vp-alert-closure-step"
onClick={item.onClick}
aria-label={`客户告警闭环时间线 ${item.step} ${item.label} ${item.value} ${item.action}`}
>
<span>{item.step}</span>
<div>
<Tag color={item.color}>{item.label}</Tag>
<strong>{item.value}</strong>
<small>{item.detail}</small>
<em>{item.action}</em>
</div>
</button>
))}
</div>
</div>
</Card>
<Card bordered title="客户告警决策台">
<div className="vp-alert-decision-board">
<div className="vp-alert-decision-summary">
<Space wrap>
<Tag color={customerDecision.color}>{customerDecision.label}</Tag>
<Tag color={priorityP0Count > 0 ? 'red' : 'green'}>P0 {priorityP0Count.toLocaleString()}</Tag>
<Tag color={storageWritable ? 'green' : 'red'}>{storageWritable ? '存储可写' : '存储异常'}</Tag>
</Space>
<strong></strong>
<span>
//
</span>
<Space wrap>
<Button size="small" theme="solid" type="primary" onClick={copyAlertCustomerDecision}></Button>
<Button size="small" disabled={priorityRows.length === 0} onClick={copyPriorityDigest}></Button>
{onOpenNotificationRules ? <Button size="small" onClick={onOpenNotificationRules}></Button> : null}
</Space>
</div>
<div className="vp-alert-decision-grid">
{[
{
label: '影响结论',
value: customerDecision.label,
detail: customerDecision.detail,
color: customerDecision.color,
action: '复制决策',
disabled: false,
onClick: copyAlertCustomerDecision
},
{
label: '待通知车辆',
value: `P0 ${priorityP0Count.toLocaleString()} / P1 ${priorityP1Count.toLocaleString()}`,
detail: priorityRows.length > 0 ? '已按车辆生成通知、证据和交接包。' : '当前没有进入优先队列的车辆。',
color: priorityP0Count > 0 ? 'red' as const : priorityP1Count > 0 ? 'orange' as const : 'green' as const,
action: '通知汇总',
disabled: priorityRows.length === 0,
onClick: copyPriorityDigest
},
{
label: '焦点车辆',
value: focusIssue?.vehicleLabel || '暂无',
detail: focusIssue ? `${qualityProtocolLabel(focusIssue.protocol)} / ${qualityIssueLabel(focusIssue.issueType)} / ${focusIssue.sla}` : '没有待处置车辆时保持监控。',
color: focusIssue ? 'blue' as const : 'green' as const,
action: '车辆服务',
disabled: !focusIssue || !focusLookup?.key,
onClick: () => focusIssue && onOpenVehicle(focusLookup?.key ?? '', focusIssue.protocol)
},
{
label: '主要问题',
value: primaryIssueImpact ? qualityIssueLabel(primaryIssueImpact.name) : '-',
detail: primaryIssueImpact ? `${primaryIssueImpact.count.toLocaleString()} 条命中,点击筛选该问题。` : '暂无客户侧问题类型。',
color: primaryIssueImpact ? 'orange' as const : 'green' as const,
action: '筛选问题',
disabled: !primaryIssueImpact,
onClick: () => primaryIssueImpact && drillIssueType(primaryIssueImpact.name)
},
{
label: '证据闭环',
value: '实时/轨迹/原始/里程',
detail: '所有通知都必须回到车辆证据,不只停留在来源或链路指标。',
color: 'blue' as const,
action: '交接包',
disabled: false,
onClick: copyNotificationHandoff
}
].map((item) => (
<button
key={item.label}
className="vp-alert-decision-item"
type="button"
disabled={item.disabled}
onClick={item.onClick}
>
<div>
<Tag color={item.color}>{item.label}</Tag>
<Tag color="grey">{item.action}</Tag>
</div>
<strong>{item.value}</strong>
<span>{item.detail}</span>
</button>
))}
</div>
</div>
</Card>
<Card bordered title="客户通知 SLA" style={{ marginTop: 16 }}>
<div className="vp-alert-sla-board">
<div className="vp-alert-sla-summary">
<Space wrap>
<Tag color={customerDecision.color}>{customerDecision.label}</Tag>
<Tag color={priorityP0Count > 0 ? 'red' : 'green'}>P0 {priorityP0Count.toLocaleString()}</Tag>
<Tag color={primaryPolicy ? 'blue' : 'orange'}>{primaryPolicy?.name || '策略待配置'}</Tag>
</Space>
<strong></strong>
<span>
</span>
<Space wrap>
<Button size="small" theme="solid" type="primary" onClick={copyNotificationHandoff}></Button>
<Button size="small" onClick={copyPolicyRunbook}> SLA </Button>
{onOpenNotificationRules ? <Button size="small" onClick={onOpenNotificationRules}></Button> : null}
</Space>
</div>
<div className="vp-alert-sla-grid">
{customerNotificationSlaItems.map((item) => (
<button
key={item.label}
type="button"
className="vp-alert-sla-item"
onClick={item.onClick}
aria-label={`客户通知SLA ${item.label} ${item.action}`}
>
<div>
<Tag color={item.color}>{item.label}</Tag>
<Tag color="grey">{item.action}</Tag>
</div>
<strong>{item.value}</strong>
<span>{item.detail}</span>
</button>
))}
</div>
</div>
</Card>
<Card bordered title="客户通知渠道闭环" style={{ marginTop: 16 }}>
<div className="vp-alert-channel-board">
<div className="vp-alert-channel-summary">
<Space wrap>
<Tag color={primaryPolicy ? 'blue' : 'orange'}>{primaryPolicy?.name || '策略待配置'}</Tag>
<Tag color={priorityP0Count > 0 ? 'red' : priorityP1Count > 0 ? 'orange' : 'green'}>
P0 {priorityP0Count.toLocaleString()} / P1 {priorityP1Count.toLocaleString()}
</Tag>
<Tag color={customerDecision.color}>{customerDecision.label}</Tag>
</Space>
<strong></strong>
<span>
</span>
<Space wrap>
<Button size="small" theme="solid" type="primary" onClick={copyNotificationHandoff}></Button>
<Button size="small" onClick={copyPolicyRunbook}></Button>
{onOpenNotificationRules ? <Button size="small" onClick={onOpenNotificationRules}></Button> : null}
</Space>
</div>
<div className="vp-alert-channel-grid">
{notificationChannelItems.map((item) => (
<button
key={item.channel}
type="button"
className="vp-alert-channel-item"
aria-label={`通知渠道闭环 ${item.channel} 复制交接包`}
onClick={copyNotificationHandoff}
>
<div>
<Tag color="blue">{item.channel}</Tag>
<Tag color={priorityP0Count > 0 ? 'red' : 'orange'}>{item.escalation}</Tag>
</div>
<strong>{item.target}</strong>
<span>{item.condition}</span>
<em>{item.acceptance}</em>
</button>
))}
</div>
</div>
</Card>
<Card bordered title="客户恢复验收台" style={{ marginTop: 16 }}>
<div className="vp-alert-recovery-board">
<div className="vp-alert-recovery-summary">
<Space wrap>
<Tag color={impactSeverity.color}>{impactSeverity.label}</Tag>
<Tag color={storageWritable ? 'green' : 'red'}>{storageWritable ? '存储可写' : '存储异常'}</Tag>
<Tag color={primaryPolicy ? 'blue' : 'orange'}>{primaryPolicy?.target || '责任人待配置'}</Tag>
</Space>
<strong>绿</strong>
<span>
</span>
<Space wrap>
<Button size="small" theme="solid" type="primary" onClick={copyAlertRecoveryReceipt}></Button>
<Button size="small" onClick={copyNotificationHandoff}></Button>
</Space>
</div>
<div className="vp-alert-recovery-grid">
{alertRecoveryAcceptanceItems.map((item) => (
<button
key={item.label}
type="button"
className="vp-alert-recovery-item"
aria-label={`客户恢复验收台 ${item.label} ${item.action}`}
onClick={item.onClick}
>
<div>
<Tag color={item.color}>{item.label}</Tag>
<Tag color="grey">{item.displayAction}</Tag>
</div>
<strong>{item.value}</strong>
<span>{item.detail}</span>
</button>
))}
</div>
</div>
</Card>
<Card bordered title="告警事件与通知中心">
<div className="vp-alert-center-board">
<div className="vp-alert-center-summary">
<Space wrap>
<Tag color={impactSeverity.color}>{impactSeverity.label}</Tag>
<Tag color={priorityP0Count > 0 ? 'red' : 'green'}>P0 {priorityP0Count.toLocaleString()}</Tag>
<Tag color={activeRuleCount > 0 ? 'orange' : 'green'}> {activeRuleCount.toLocaleString()}</Tag>
</Space>
<strong>{issueVehicleCount.toLocaleString()} </strong>
<span>
</span>
<Space wrap>
<Button size="small" theme="solid" type="primary" disabled={priorityRows.length === 0} onClick={copyNotificationHandoff}></Button>
<Button size="small" onClick={copyBusinessImpact}></Button>
{onOpenNotificationRules ? <Button size="small" onClick={onOpenNotificationRules}></Button> : null}
</Space>
</div>
<div className="vp-alert-center-grid">
{[
{
label: '影响车辆',
value: `${issueVehicleCount.toLocaleString()}`,
detail: `${issueRecordCount.toLocaleString()} 条告警记录,优先确认车辆服务是否仍可用。`,
color: issueVehicleCount > 0 ? 'orange' as const : 'green' as const,
onClick: () => undefined,
disabled: true
},
{
label: '优先队列',
value: `P0 ${priorityP0Count.toLocaleString()} / P1 ${priorityP1Count.toLocaleString()}`,
detail: priorityRows.length > 0 ? '已按优先级生成可复制通知和处置工单。' : '当前没有待通知车辆。',
color: priorityP0Count > 0 ? 'red' as const : priorityP1Count > 0 ? 'orange' as const : 'green' as const,
onClick: copyPriorityDigest,
disabled: priorityRows.length === 0
},
{
label: '主要来源',
value: primaryProtocolImpact ? qualityProtocolLabel(primaryProtocolImpact.name) : '-',
detail: primaryProtocolImpact ? `${primaryProtocolImpact.count.toLocaleString()} 条问题,点击筛选该来源。` : '暂无来源问题。',
color: primaryProtocolImpact ? 'blue' as const : 'green' as const,
onClick: () => primaryProtocolImpact && drillProtocol(primaryProtocolImpact.name),
disabled: !primaryProtocolImpact
},
{
label: '主要问题',
value: primaryIssueImpact ? qualityIssueLabel(primaryIssueImpact.name) : '-',
detail: primaryIssueImpact ? `${primaryIssueImpact.count.toLocaleString()} 条命中,点击筛选该问题。` : '暂无问题类型。',
color: primaryIssueImpact ? 'orange' as const : 'green' as const,
onClick: () => primaryIssueImpact && drillIssueType(primaryIssueImpact.name),
disabled: !primaryIssueImpact
},
{
label: '链路状态',
value: storageWritable ? '可写' : '异常',
detail: `Kafka Lag ${formatLag(health?.kafkaLag)},容量发现 ${capacityFindingCount.toLocaleString()} 项。`,
color: storageWritable && capacityFindingCount === 0 ? 'green' as const : 'red' as const,
onClick: loadHealth,
disabled: false
}
].map((item) => (
<button
key={item.label}
className="vp-alert-center-item"
type="button"
disabled={item.disabled}
onClick={item.onClick}
>
<Tag color={item.color}>{item.label}</Tag>
<strong>{item.value}</strong>
<span>{item.detail}</span>
</button>
))}
</div>
</div>
</Card>
<Card bordered title="车辆风险处置任务板" style={{ marginTop: 16 }}>
<div className="vp-alert-vehicle-task-board">
<div className="vp-alert-vehicle-focus">
<Space wrap>
<Tag color={focusIssue?.priority === 'P0' ? 'red' : focusIssue ? 'orange' : 'green'}>
{focusIssue ? focusIssue.priority : '无待处置'}
</Tag>
<Tag color={focusIssue ? 'blue' : 'green'}>
{focusIssue ? qualityIssueLabel(focusIssue.issueType) : '车辆服务正常'}
</Tag>
</Space>
<strong>{focusIssue?.vehicleLabel || '暂无优先车辆'}</strong>
<span>
{focusIssue
? `${focusIssue.actionLabel}${focusIssue.actionDetail || focusIssue.detail || '请结合实时、轨迹、原始记录和里程证据闭环。'}`
: '当前没有进入优先队列的车辆,保持实时监控并定期复核告警规则。'}
</span>
<Space wrap>
<Button size="small" theme="solid" type="primary" disabled={!focusLookup?.key} onClick={() => focusIssue && onOpenVehicle(focusLookup?.key ?? '', focusIssue.protocol)}>
</Button>
<Button size="small" disabled={!focusIssue} onClick={() => focusIssue && copyText(priorityIssueNotificationText(focusIssue), '焦点车辆告警通知')}>
</Button>
</Space>
</div>
<div className="vp-alert-vehicle-task-grid">
{[
{
label: '实时确认',
value: focusIssue ? '看在线' : '待命',
detail: '先确认车辆是否仍在线、最后上报来源和实时字段是否恢复。',
action: '实时状态',
color: focusIssue ? 'blue' as const : 'grey' as const,
disabled: !focusIssue || !focusLookup?.key || !onOpenRealtime,
onClick: () => focusIssue && openIssueRealtime(focusIssue)
},
{
label: '轨迹证据',
value: focusIssue ? '看前后' : '待命',
detail: '按告警发生日期打开轨迹,判断是否影响车辆定位和回放。',
action: '轨迹回放',
color: focusIssue ? 'green' as const : 'grey' as const,
disabled: !focusIssue || !focusLookup?.key || !onOpenHistory,
onClick: () => focusIssue && openIssueHistory(focusIssue)
},
{
label: '里程复核',
value: focusIssue ? '核统计' : '待命',
detail: '对同一天里程统计与轨迹证据做交叉校验,避免影响 BI。',
action: '里程统计',
color: focusIssue ? 'orange' as const : 'grey' as const,
disabled: !focusIssue || !focusLookup?.key || !onOpenMileage,
onClick: () => focusIssue && openIssueMileage(focusIssue)
},
{
label: '原始证据',
value: focusIssue ? '查原始' : '待命',
detail: '保留原始帧和解析字段,给平台、客户和研发确认责任边界。',
action: '原始记录',
color: focusIssue ? 'blue' as const : 'grey' as const,
disabled: !focusIssue || !focusLookup?.key || !onOpenRaw,
onClick: () => focusIssue && openIssueRaw(focusIssue)
}
].map((item) => (
<div key={item.label} className="vp-alert-vehicle-task-item">
<Tag color={item.color}>{item.label}</Tag>
<strong>{item.value}</strong>
<span>{item.detail}</span>
<Button size="small" disabled={item.disabled} onClick={item.onClick}>{item.action}</Button>
</div>
))}
</div>
</div>
</Card>
<div className="vp-kpi-grid">
{[
{ label: '问题车辆', value: issueVehicleCount.toLocaleString() },
{ label: '问题记录', value: issueRecordCount.toLocaleString() },
{ label: '错误 / 警告', value: `${errorCount}/${warningCount}` },
{
label: '主要问题',
value: primaryIssueType ? `${qualityIssueLabel(primaryIssueType)} ${summaryIssueTypes[0].count}` : '-',
onClick: primaryIssueType ? drillPrimaryIssue : undefined
}
].map((item) => (
<Card key={item.label} bordered loading={loadingSummary}>
{item.onClick ? (
<button
className="vp-result-summary-button"
type="button"
aria-label={`${item.label} ${item.value}`}
onClick={item.onClick}
>
<div className="vp-kpi-value">{item.value}</div>
<div className="vp-kpi-label">{item.label}</div>
</button>
) : (
<>
<div className="vp-kpi-value">{item.value}</div>
<div className="vp-kpi-label">{item.label}</div>
</>
)}
</Card>
))}
</div>
<Row gutter={16}>
<Col span={5}><Card bordered title="Kafka Lag">{formatLag(health?.kafkaLag)}</Card></Col>
<Col span={5}><Card bordered title="活跃连接">{formatLag(health?.activeConnections)}</Card></Col>
<Col span={5}><Card bordered title="Redis 在线 Key">{formatLag(health?.redisOnlineKeys)}</Card></Col>
<Col span={5}><Card bordered title="运行版本">{health?.runtime?.platformRelease || '未标记'}</Card></Col>
<Col span={4}>
<Card bordered title="存储读取">
<Tag color={statusColor[storageReadStatus(health)] ?? 'grey'}>
{storageReadStatus(health) === 'pending' ? '检测中' : storageReadStatus(health) === 'ok' ? '正常' : '异常'}
</Tag>
</Card>
</Col>
</Row>
<Card
bordered
title={<Space><span></span><Button size="small" onClick={copyBusinessImpact}></Button></Space>}
style={{ marginTop: 16 }}
>
<div className="vp-alert-impact-board">
<div className="vp-alert-impact-summary">
<Tag color={impactSeverity.color}>{impactSeverity.label}</Tag>
<div className="vp-alert-flow-value">{issueVehicleCount.toLocaleString()} </div>
<div>{impactSeverity.detail}</div>
<Space wrap>
<Tag color={storageWritable ? 'green' : 'red'}>{storageWritable ? '存储可写' : '存储异常'}</Tag>
<Tag color={activeRulesForImpact.length > 0 ? 'orange' : 'green'}>{activeRulesForImpact.length} </Tag>
<Tag color={capacityFindingCount > 0 ? 'orange' : 'green'}>{capacityFindingCount} </Tag>
</Space>
</div>
<div className="vp-alert-impact-grid">
{impactCards.map((item) => (
<div key={item.label} className="vp-alert-impact-item">
<Tag color={item.color}>{item.label}</Tag>
<strong>{item.value}</strong>
<div>{item.detail}</div>
</div>
))}
</div>
</div>
</Card>
<Card
bordered
title={<Space><span></span><Button size="small" onClick={copyNotificationHandoff}></Button></Space>}
style={{ marginTop: 16 }}
>
<div className="vp-alert-flow">
{[
{ label: '事件触发', value: `${activeRuleCount} 类活跃`, detail: '断链、无来源、VIN 缺失、字段缺失和容量异常进入告警池。' },
{ label: '分级通知', value: `${p0RuleCount} 类 P0`, detail: '按错误、警告、关注分层推送给平台、运维和业务责任人。' },
{ label: '处置回执', value: `${issueVehicleCount.toLocaleString()} 辆车`, detail: '告警需要关联车辆服务、来源证据和处理结论,形成可追踪闭环。' }
].map((item) => (
<div key={item.label} className="vp-alert-flow-item">
<Tag color="blue">{item.label}</Tag>
<div className="vp-alert-flow-value">{item.value}</div>
<div>{item.detail}</div>
</div>
))}
</div>
</Card>
<Card bordered title="告警分派看板" style={{ marginTop: 16 }}>
<div className="vp-alert-dispatch-grid">
{dispatchOwnerRows.length === 0 ? (
<Tag color="green"></Tag>
) : dispatchOwnerRows.map((item) => (
<button
key={item.owner}
type="button"
className="vp-alert-dispatch-item"
aria-label={`分派团队 ${item.owner}`}
onClick={() => drillIssueType(item.primaryIssueType)}
>
<div className="vp-alert-dispatch-head">
<Tag color={item.p0Count > 0 ? 'red' : 'orange'}>{item.owner}</Tag>
<strong>{item.hitCount.toLocaleString()} </strong>
</div>
<div className="vp-alert-policy-detail">
{item.activeRuleCount.toLocaleString()} / P0 {item.p0Count.toLocaleString()}
</div>
<div className="vp-alert-policy-detail">
{item.primaryIssueType === 'CAPACITY_RISK' ? '容量与存储风险' : qualityIssueLabel(item.primaryIssueType)}
</div>
<div className="vp-alert-policy-detail">SLA{item.primarySla}</div>
</button>
))}
</div>
</Card>
<Card
bordered
title={(
<Space>
<span></span>
<Button size="small" disabled={priorityRows.length === 0} onClick={copyPriorityDigest}></Button>
</Space>
)}
style={{ marginTop: 16 }}
>
<Table<PriorityIssueRow>
loading={loadingIssues}
pagination={false}
rowKey={(row?: PriorityIssueRow) => `${row?.priority ?? ''}-${row?.protocol ?? ''}-${row?.issueType ?? ''}-${row?.vehicleLabel ?? ''}-${row?.lastSeen ?? ''}`}
dataSource={priorityRows}
columns={[
{ title: '优先级', width: 90, render: (_: unknown, row: PriorityIssueRow) => <Tag color={row.priority === 'P0' ? 'red' : 'orange'}>{row.priority}</Tag> },
{ title: '车辆', width: 210, dataIndex: 'vehicleLabel' },
{ title: '问题', width: 140, render: (_: unknown, row: PriorityIssueRow) => qualityIssueLabel(row.issueType) },
{ title: '建议动作', width: 150, dataIndex: 'actionLabel' },
{ title: 'SLA', width: 130, dataIndex: 'sla' },
{ title: '最后时间', width: 170, dataIndex: 'lastSeen' },
{ title: '说明', dataIndex: 'detail' },
{
title: '操作',
width: 360,
render: (_: unknown, row: PriorityIssueRow) => {
const lookup = qualityIssueVehicleLookup(row);
return (
<Space spacing={4} wrap>
<Button size="small" disabled={!lookup.key || !onOpenRealtime} onClick={() => openIssueRealtime(row)}></Button>
<Button size="small" disabled={!lookup.key || !onOpenHistory} onClick={() => openIssueHistory(row)}></Button>
<Button size="small" disabled={!lookup.key || !onOpenRaw} onClick={() => openIssueRaw(row)}></Button>
<Button size="small" onClick={() => copyText(row.notificationText || priorityIssueNotificationText(row), '告警通知')}></Button>
<Button size="small" onClick={() => copyText(priorityIssueEvidencePackageText(row), '告警证据包')}></Button>
<Button size="small" onClick={() => copyText(priorityIssueTicketText(row), '处置工单')}></Button>
<Button size="small" disabled={!lookup.key} onClick={() => onOpenVehicle(lookup.key, row.protocol)}></Button>
</Space>
);
}
}
]}
/>
</Card>
<Card bordered title="告警升级时钟" style={{ marginTop: 16 }}>
<Table<PriorityIssueRow>
loading={loadingIssues}
pagination={false}
rowKey={(row?: PriorityIssueRow) => `clock-${row?.priority ?? ''}-${row?.protocol ?? ''}-${row?.issueType ?? ''}-${row?.vehicleLabel ?? ''}-${row?.lastSeen ?? ''}`}
dataSource={priorityRows}
columns={[
{
title: '升级状态',
width: 120,
render: (_: unknown, row: PriorityIssueRow) => {
const clock = escalationClock(row);
return <Tag color={clock.color}>{clock.status}</Tag>;
}
},
{ title: '车辆', width: 210, dataIndex: 'vehicleLabel' },
{ title: '优先级', width: 90, render: (_: unknown, row: PriorityIssueRow) => <Tag color={row.priority === 'P0' ? 'red' : 'orange'}>{row.priority}</Tag> },
{ title: '问题', width: 140, render: (_: unknown, row: PriorityIssueRow) => qualityIssueLabel(row.issueType) },
{ title: 'SLA', width: 130, dataIndex: 'sla' },
{
title: '剩余/超时',
width: 150,
render: (_: unknown, row: PriorityIssueRow) => escalationClock(row).detail
},
{
title: '升级截止',
width: 210,
render: (_: unknown, row: PriorityIssueRow) => escalationClock(row).deadline
},
{ title: '建议动作', dataIndex: 'actionLabel' }
]}
/>
</Card>
<div className="vp-alert-ops-grid">
<Card bordered title={<Space><span></span>{onOpenNotificationRules ? <Button size="small" onClick={onOpenNotificationRules}></Button> : null}</Space>}>
<Table<AlertRuleRow>
pagination={false}
dataSource={rules}
rowKey="issueType"
columns={[
{ title: '规则', render: (_: unknown, row: AlertRuleRow) => row.title || (row.issueType === 'CAPACITY_RISK' ? '容量与存储风险' : qualityIssueLabel(row.issueType)) },
{ title: '级别', width: 90, render: (_: unknown, row: AlertRuleRow) => <Tag color={ruleStatusColor(row.count, row.level)}>{row.level}</Tag> },
{ title: '当前命中', width: 110, render: (_: unknown, row: AlertRuleRow) => row.count.toLocaleString() },
{ title: '触发条件', dataIndex: 'trigger' },
{ title: 'SLA', dataIndex: 'sla', width: 110 }
]}
/>
</Card>
<Card
bordered
title={<Space><span></span><Button size="small" onClick={copyPolicyRunbook}>Runbook</Button></Space>}
>
<div className="vp-notification-policy-list">
{policies.map((item) => (
<div key={item.name} className="vp-notification-policy">
<div>
<Space>
<Tag color={item.name.startsWith('P0') ? 'red' : item.name.startsWith('P1') ? 'orange' : 'grey'}>{item.name}</Tag>
<strong>{item.target}</strong>
</Space>
<div className="vp-alert-policy-detail">{item.condition}</div>
{item.acceptanceCriteria ? <div className="vp-alert-policy-detail">{item.acceptanceCriteria}</div> : null}
</div>
<Space wrap>
{formatEscalationMinutes(item.escalationMinutes) ? <Tag color="red">{formatEscalationMinutes(item.escalationMinutes)}</Tag> : null}
<Tag color="blue">{item.channel}</Tag>
</Space>
</div>
))}
</div>
</Card>
</div>
{health?.capacityFindings?.length ? (
<Card bordered title="容量检查发现" style={{ marginTop: 16 }}>
<Space wrap>
{health.capacityFindings.map((item) => (
<Tag key={item} color="orange">{item}</Tag>
))}
</Space>
</Card>
) : null}
{filterSummary.length > 0 ? (
<Card bordered title="当前筛选" style={{ marginTop: 16 }}>
<Space wrap>
{filterSummary.map((item) => (
<Tag key={item} color="blue">{item}</Tag>
))}
<Button size="small" onClick={() => applyFilters({})}></Button>
</Space>
</Card>
) : null}
<Card bordered title="问题来源分布" loading={loadingSummary} style={{ marginTop: 16 }}>
<Table
pagination={false}
dataSource={summaryProtocols}
rowKey="name"
columns={[
{ title: '数据来源', render: (_: unknown, row: { name: string }) => qualityProtocolLabel(row.name) },
{ title: '问题数', dataIndex: 'count', width: 120 },
{
title: '操作',
width: 140,
render: (_: unknown, row: { name: string }) => (
<Button size="small" onClick={() => drillProtocol(row.name)}> {qualityProtocolLabel(row.name)} </Button>
)
}
]}
/>
</Card>
<Card bordered title="问题类型分布" loading={loadingSummary} style={{ marginTop: 16 }}>
<Table
pagination={false}
dataSource={summaryIssueTypes}
rowKey="name"
columns={[
{ title: '问题类型', render: (_: unknown, row: { name: string }) => qualityIssueLabel(row.name) },
{ title: '问题数', dataIndex: 'count', width: 120 },
{
title: '操作',
width: 160,
render: (_: unknown, row: { name: string }) => (
<Button size="small" onClick={() => drillIssueType(row.name)}> {qualityIssueLabel(row.name)}</Button>
)
}
]}
/>
</Card>
<Card bordered title="质量问题" style={{ marginTop: 16 }}>
<Form key={JSON.stringify(filters)} initValues={filters} layout="horizontal" onSubmit={(values) => {
const nextFilters = values as Record<string, string>;
applyFilters(nextFilters);
}} style={{ marginBottom: 12 }}>
<Form.Input field="keyword" label="关键词" placeholder="VIN / 车牌 / 手机号 / 来源地址" style={{ width: 260 }} />
<Form.Select field="protocol" label="数据来源" placeholder="全部来源" style={{ width: 160 }}>
{qualityProtocolOptions.map((item) => (
<Select.Option key={item.value} value={item.value}>{item.label}</Select.Option>
))}
</Form.Select>
<Form.Select field="issueType" label="问题类型" placeholder="全部问题" style={{ width: 160 }}>
{qualityIssueOptions.map((item) => (
<Select.Option key={item.value} value={item.value}>{item.label}</Select.Option>
))}
</Form.Select>
<Space>
<Button htmlType="submit" theme="solid" type="primary"></Button>
<Button icon={<IconCopy />} onClick={copyQualityShareURL}></Button>
<Button onClick={() => {
applyFilters({});
}}></Button>
</Space>
</Form>
<Table
loading={loadingIssues}
rowKey={(row?: QualityIssueRow) => `${row?.protocol ?? ''}-${row?.issueType ?? ''}-${row?.lastSeen ?? ''}-${row?.detail ?? ''}`}
dataSource={issues}
pagination={{
currentPage: pagination.currentPage,
pageSize: pagination.pageSize,
total: pagination.total,
showSizeChanger: true,
onPageChange: (page) => loadIssues(filters, page, pagination.pageSize),
onPageSizeChange: (pageSize) => loadIssues(filters, 1, pageSize)
}}
columns={[
{ title: 'VIN', dataIndex: 'vin' },
{ title: '车牌', dataIndex: 'plate' },
{ title: '手机号', dataIndex: 'phone' },
{ title: '来源地址', dataIndex: 'sourceEndpoint' },
{ title: '数据来源', render: (_: unknown, row: QualityIssueRow) => qualityProtocolLabel(row.protocol) },
{ title: '问题', render: (_: unknown, row: QualityIssueRow) => qualityIssueLabel(row.issueType) },
{ title: '级别', render: (_: unknown, row: QualityIssueRow) => <Tag color={row.severity === 'error' ? 'red' : 'orange'}>{row.severity}</Tag> },
{ title: '最后时间', dataIndex: 'lastSeen' },
{ title: '说明', dataIndex: 'detail' },
{
title: '处置建议',
width: 260,
render: (_: unknown, row: QualityIssueRow) => {
const action = qualityActionRecommendation(row);
return (
<div>
<Tag color={action.color}>{action.label}</Tag>
<div style={{ marginTop: 6 }}>{action.detail}</div>
</div>
);
}
},
{
title: '操作',
width: 480,
render: (_: unknown, row: QualityIssueRow) => {
const lookup = qualityIssueVehicleLookup(row);
return (
<Space spacing={4} wrap>
<Button size="small" icon={<IconCopy />} onClick={() => copyText(row.phone, '手机号')}></Button>
<Button size="small" icon={<IconCopy />} onClick={() => copyText(row.sourceEndpoint, '来源')}></Button>
<Button size="small" disabled={!lookup.key || !onOpenRealtime} onClick={() => openIssueRealtime(row)}></Button>
<Button size="small" disabled={!lookup.key || !onOpenHistory} onClick={() => openIssueHistory(row)}></Button>
<Button size="small" disabled={!lookup.key || !onOpenRaw} onClick={() => openIssueRaw(row)}></Button>
<Button size="small" disabled={!lookup.key || !onOpenMileage} onClick={() => openIssueMileage(row)}></Button>
<Button size="small" onClick={() => copyText(priorityIssueNotificationText(priorityIssueFromRow(row)), '告警通知')}></Button>
<Button size="small" onClick={() => copyText(priorityIssueEvidencePackageText(priorityIssueFromRow(row)), '告警证据包')}></Button>
<Button size="small" disabled={!lookup.key} onClick={() => onOpenVehicle(lookup.key, row.protocol)}>
{lookup.label}
</Button>
</Space>
);
}
}
]}
/>
</Card>
<Card
bordered
title={<Space><span></span><Button size="small" loading={loadingHealth} onClick={loadHealth}></Button></Space>}
style={{ marginTop: 16 }}
>
<Table
loading={loadingHealth}
dataSource={health?.linkHealth ?? []}
pagination={false}
columns={[
{ title: '链路', dataIndex: 'name' },
{
title: '状态',
render: (_: unknown, row: { status: string }) => (
<Tag color={statusColor[row.status] ?? 'grey'}>{row.status}</Tag>
)
},
{ title: '说明', dataIndex: 'detail' }
]}
/>
</Card>
</div>
);
}