feat(platform): add customer alert service desk

This commit is contained in:
lingniu
2026-07-05 20:07:19 +08:00
parent dbdce8f312
commit 2765a3c491
3 changed files with 180 additions and 1 deletions

View File

@@ -1128,10 +1128,89 @@ export function Quality({
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()
}
];
return (
<div className="vp-page">
<PageHeader title="告警事件" description="围绕车辆服务沉淀断链、VIN 缺失、字段缺失和链路健康事件,并形成通知闭环" />
<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-decision-board">
<div className="vp-alert-decision-summary">

View File

@@ -4661,6 +4661,97 @@ button.vp-realtime-command-item:focus-visible {
gap: 12px;
}
.vp-alert-customer-service-desk {
margin-top: 16px;
margin-bottom: 16px;
border: 1px solid rgba(239, 68, 68, 0.18);
border-radius: var(--vp-radius);
background: #fff;
display: grid;
grid-template-columns: minmax(300px, 0.62fr) minmax(0, 1.38fr);
overflow: hidden;
box-shadow: var(--vp-shadow-sm);
}
.vp-alert-customer-service-summary {
padding: 18px;
border-right: 1px solid var(--vp-border);
background: #fff7f7;
display: grid;
gap: 12px;
align-content: start;
}
.vp-alert-customer-service-summary strong {
color: var(--vp-text);
font-size: 18px;
line-height: 26px;
word-break: break-word;
}
.vp-alert-customer-service-summary span {
color: var(--vp-text-muted);
font-size: 13px;
line-height: 20px;
}
.vp-alert-customer-service-grid {
padding: 16px;
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 10px;
}
.vp-alert-customer-service-item {
min-height: 144px;
padding: 12px;
border: 1px solid rgba(239, 68, 68, 0.14);
border-radius: 8px;
background: #fbfcff;
color: inherit;
cursor: pointer;
font: inherit;
text-align: left;
display: grid;
gap: 8px;
align-content: start;
}
.vp-alert-customer-service-item:hover,
.vp-alert-customer-service-item:focus-visible {
border-color: rgba(239, 68, 68, 0.4);
background: #fff7f7;
box-shadow: var(--vp-shadow-sm);
outline: none;
}
.vp-alert-customer-service-item:disabled {
cursor: not-allowed;
opacity: 0.62;
}
.vp-alert-customer-service-item strong {
color: var(--vp-text);
font-size: 18px;
line-height: 24px;
word-break: break-word;
}
.vp-alert-customer-service-item span {
color: var(--vp-text-muted);
font-size: 12px;
line-height: 18px;
word-break: break-word;
}
.vp-alert-customer-service-item em {
color: #dc2626;
font-size: 12px;
font-style: normal;
font-weight: 700;
line-height: 18px;
}
.vp-alert-decision-board {
display: grid;
grid-template-columns: minmax(280px, 0.74fr) minmax(0, 1.26fr);
@@ -9446,6 +9537,8 @@ button.vp-realtime-command-item:focus-visible {
.vp-conclusion-grid,
.vp-monitor-layout,
.vp-alert-flow,
.vp-alert-customer-service-desk,
.vp-alert-customer-service-grid,
.vp-alert-decision-board,
.vp-alert-decision-grid,
.vp-alert-sla-board,

View File

@@ -3887,7 +3887,7 @@ test('shows alert rule and notification policy workspace on quality page', async
expect(screen.getByText('客户关心的是哪些车辆受影响、是否影响实时/轨迹/里程、是否需要通知、何时恢复;协议来源只作为证据辅助判断。')).toBeInTheDocument();
expect(screen.getAllByText('立即处置').length).toBeGreaterThan(0);
expect(screen.getByText('待通知车辆')).toBeInTheDocument();
expect(screen.getByText('证据闭环')).toBeInTheDocument();
expect(screen.getAllByText('证据闭环').length).toBeGreaterThanOrEqual(1);
fireEvent.click(screen.getByRole('button', { name: '复制告警决策' }));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【客户告警决策说明】'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('决策结论:立即处置'));
@@ -11235,6 +11235,13 @@ test('opens realtime status from quality issue row with source evidence', async
render(<App />);
expect(await screen.findByText('13307795425')).toBeInTheDocument();
expect(screen.getByText('客户告警服务台')).toBeInTheDocument();
expect(screen.getByText('客户先看到哪些车受影响、是否要通知、证据是否齐全、恢复后如何验收。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户告警服务台 影响车辆 1 辆 复制影响' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户告警服务台 待通知 P0 1 / P1 0 复制通知' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户告警服务台 焦点车辆 粤AG18312 / 13307795425 车辆服务' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户告警服务台 证据闭环 可复核 复制交接' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户告警服务台 恢复验收 有标准 复制回执' })).toBeInTheDocument();
fireEvent.click(screen.getAllByRole('button', { name: '实时状态' })[0]);
await waitFor(() => {