feat(platform): add quality escalation clock

This commit is contained in:
lingniu
2026-07-04 14:45:39 +08:00
parent b7dd65e435
commit cb49798b6d
2 changed files with 171 additions and 4 deletions

View File

@@ -355,6 +355,53 @@ function priorityIssueDigestText(rows: PriorityIssueRow[], summary: QualitySumma
].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 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';
@@ -662,6 +709,39 @@ export function Quality({
]}
/>
</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="告警触发规则">
<Table<AlertRuleRow>