feat(platform): add realtime operations impact board
This commit is contained in:
@@ -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">
|
||||
{[
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user