feat(platform): add realtime operations impact board

This commit is contained in:
lingniu
2026-07-04 22:39:10 +08:00
parent 6803d51b78
commit a005dba2d4
3 changed files with 181 additions and 0 deletions

View File

@@ -396,6 +396,46 @@ function realtimeDutyHandoffText({
return lines.join('\n');
}
function realtimeImpactReportText({
filters,
rows,
total,
onlineCount,
locatedCount,
degradedCount,
staleCount,
pageCoverageRate,
amapConfigured,
runtimeRelease
}: {
filters: Record<string, string>;
rows: VehicleRealtimeRow[];
total: number;
onlineCount: number;
locatedCount: number;
degradedCount: number;
staleCount: number;
pageCoverageRate: number;
amapConfigured: boolean;
runtimeRelease?: string;
}) {
const offlineCount = Math.max(0, rows.length - onlineCount);
const impactLevel = degradedCount > 0 || staleCount > 0 || offlineCount > 0 ? '需要处置' : '实时稳定';
return [
'【实时运营影响】',
`当前筛选:${realtimeFilterSummary(filters).join('') || '全部实时车辆'}`,
`运行版本:${runtimeRelease || '未标记'}`,
`影响等级:${impactLevel}`,
`车辆范围:当前页 ${rows.length.toLocaleString()} / 总计 ${total.toLocaleString()},覆盖率 ${formatPercent(pageCoverageRate)}`,
`在线状态:${onlineCount.toLocaleString()} 在线 / ${offlineCount.toLocaleString()} 离线`,
`定位影响:${locatedCount.toLocaleString()} 辆有有效坐标`,
`服务影响:${degradedCount.toLocaleString()} 辆降级 / ${staleCount.toLocaleString()} 辆超时`,
`地图能力:${amapConfigured ? '高德地图可用' : '高德地图未配置,使用坐标预览'}`,
`建议动作:${impactLevel === '实时稳定' ? '保持监控并关注告警队列' : '优先处理离线、超时和来源不完整车辆,并回到轨迹/RAW证据复核'}`,
`实时监控:${appURL(buildAppHash({ page: 'realtime', protocol: filters.protocol, filters }))}`
].join('\n');
}
function buildRealtimeSourceCoverage(rows: VehicleRealtimeRow[]) {
const sourceMap = new Map<string, RealtimeSourceCoverage>();
rows.forEach((row) => {
@@ -624,6 +664,52 @@ export function Realtime({
amapConfigured,
runtimeRelease: runtime?.platformRelease
}), '实时值班交接包');
const realtimeImpactLevel = degradedCount > 0 || staleCount > 0 || rows.length - onlineCount > 0 ? '需要处置' : '实时稳定';
const realtimeImpactColor = realtimeImpactLevel === '实时稳定' ? 'green' as const : degradedCount > 0 || staleCount > 0 ? 'orange' as const : 'blue' as const;
const realtimeImpactItems = [
{
label: '车辆范围',
value: `${rows.length.toLocaleString()} / ${pagination.total.toLocaleString()}`,
detail: `当前页覆盖 ${formatPercent(pageCoverageRate)}`,
color: pageCoverageRate >= 100 ? 'green' as const : 'grey' as const
},
{
label: '在线影响',
value: `${onlineCount.toLocaleString()} 在线`,
detail: `${(rows.length - onlineCount).toLocaleString()} 辆离线`,
color: rows.length - onlineCount > 0 ? 'orange' as const : 'green' as const
},
{
label: '定位影响',
value: `${locatedCount.toLocaleString()}`,
detail: `有效定位率 ${formatPercent(locatedRate)}`,
color: locatedCount > 0 ? 'blue' as const : 'orange' as const
},
{
label: '服务影响',
value: `${degradedCount.toLocaleString()} 降级`,
detail: `${staleCount.toLocaleString()} 辆更新超时`,
color: degradedCount > 0 || staleCount > 0 ? 'orange' as const : 'green' as const
},
{
label: '地图能力',
value: amapConfigured ? '可用' : '坐标预览',
detail: amapConfigured ? '高德地图配置就绪' : '高德未配置,页面降级展示坐标。',
color: amapConfigured ? 'green' as const : 'orange' as const
}
];
const copyRealtimeImpact = () => copyText(realtimeImpactReportText({
filters,
rows,
total: pagination.total,
onlineCount,
locatedCount,
degradedCount,
staleCount,
pageCoverageRate,
amapConfigured,
runtimeRelease: runtime?.platformRelease
}), '实时运营影响');
const selectRealtimeRow = (row: VehicleRealtimeRow) => {
const index = rows.indexOf(row);
if (index < 0) return;
@@ -931,6 +1017,35 @@ export function Realtime({
</div>
</div>
</div>
<Card
bordered
title={<Space><span></span><Button size="small" icon={<IconCopy />} onClick={copyRealtimeImpact}></Button></Space>}
style={{ marginBottom: 16 }}
>
<div className="vp-realtime-impact-board">
<div className="vp-realtime-impact-summary">
<Tag color={realtimeImpactColor}>{realtimeImpactLevel}</Tag>
<div className="vp-monitor-metric-value">{pagination.total.toLocaleString()} </div>
<Typography.Text type="secondary">
{rows.length.toLocaleString()} 线 {onlineCount.toLocaleString()} {locatedCount.toLocaleString()} {degradedCount.toLocaleString()} {staleCount.toLocaleString()}
</Typography.Text>
<Space wrap>
<Tag color={pageCoverageRate >= 100 ? 'green' : 'grey'}> {formatPercent(pageCoverageRate)}</Tag>
<Tag color={amapConfigured ? 'green' : 'orange'}>{amapConfigured ? '高德可用' : '坐标预览'}</Tag>
<Tag color={rows.length - onlineCount > 0 ? 'orange' : 'green'}>{(rows.length - onlineCount).toLocaleString()} 线</Tag>
</Space>
</div>
<div className="vp-realtime-impact-grid">
{realtimeImpactItems.map((item) => (
<div key={item.label} className="vp-realtime-impact-item">
<Tag color={item.color}>{item.label}</Tag>
<strong>{item.value}</strong>
<Typography.Text type="secondary">{item.detail}</Typography.Text>
</div>
))}
</div>
</div>
</Card>
<Card bordered title="实时覆盖判读" style={{ marginBottom: 16 }}>
<div className="vp-stat-insight-grid">
{[

View File

@@ -344,6 +344,48 @@ button.vp-realtime-command-item:focus-visible {
line-height: 28px;
}
.vp-realtime-impact-board {
display: grid;
grid-template-columns: minmax(260px, 0.72fr) minmax(0, 1.28fr);
gap: 16px;
}
.vp-realtime-impact-summary {
min-height: 156px;
padding: 12px;
border: 1px solid rgba(22, 100, 255, 0.28);
border-radius: var(--vp-radius);
background: #f5f9ff;
display: grid;
gap: 10px;
align-content: start;
}
.vp-realtime-impact-grid {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 12px;
}
.vp-realtime-impact-item {
min-height: 156px;
padding: 12px;
border: 1px solid var(--vp-border);
border-radius: var(--vp-radius);
background: #fbfcff;
display: grid;
gap: 10px;
align-content: start;
}
.vp-realtime-impact-item strong {
color: var(--vp-text);
font-size: 20px;
line-height: 26px;
font-weight: 700;
word-break: break-word;
}
.vp-source-coverage-board {
margin-bottom: 16px;
padding: 14px;
@@ -2056,6 +2098,8 @@ button.vp-realtime-command-item:focus-visible {
.vp-current-service-grid,
.vp-realtime-command-board,
.vp-realtime-command-grid,
.vp-realtime-impact-board,
.vp-realtime-impact-grid,
.vp-source-coverage-grid,
.vp-runbook-grid,
.vp-workbench-grid,

View File

@@ -8332,6 +8332,11 @@ test('shows canonical service status in realtime vehicles', async () => {
test('frames realtime page as one vehicle service with source evidence', async () => {
window.history.replaceState(null, '', '/#/realtime');
const writeText = vi.fn(() => Promise.resolve());
Object.defineProperty(navigator, 'clipboard', {
configurable: true,
value: { writeText }
});
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
const path = String(input);
if (path.includes('/api/realtime/vehicles')) {
@@ -8400,6 +8405,23 @@ test('frames realtime page as one vehicle service with source evidence', async (
expect(screen.getByText('JT808 在线')).toBeInTheDocument();
expect(screen.getByText('YUTONG_MQTT 未接入')).toBeInTheDocument();
expect(screen.getAllByText('2/2 来源在线').length).toBeGreaterThan(0);
expect(screen.getByText('实时运营影响')).toBeInTheDocument();
expect(screen.getByRole('button', { name: /复制影响报告/ })).toBeInTheDocument();
expect(screen.getByText('需要处置')).toBeInTheDocument();
expect(screen.getByText('车辆范围')).toBeInTheDocument();
expect(screen.getByText('在线影响')).toBeInTheDocument();
expect(screen.getByText('定位影响')).toBeInTheDocument();
expect(screen.getByText('服务影响')).toBeInTheDocument();
expect(screen.getByText('地图能力')).toBeInTheDocument();
expect(screen.getByText('当前页 1 辆,在线 1 辆,定位有效 1 辆,降级 0 辆,超时 1 辆。')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: /复制影响报告/ }));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【实时运营影响】'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('影响等级:需要处置'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆范围:当前页 1 / 总计 1覆盖率 100%'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('在线状态1 在线 / 0 离线'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('定位影响1 辆有有效坐标'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('服务影响0 辆降级 / 1 辆超时'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('地图能力:高德地图未配置,使用坐标预览'));
});
test('refreshes realtime vehicle data from the monitoring workspace', async () => {