feat(platform): add realtime operations impact board
This commit is contained in:
@@ -396,6 +396,46 @@ function realtimeDutyHandoffText({
|
|||||||
return lines.join('\n');
|
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[]) {
|
function buildRealtimeSourceCoverage(rows: VehicleRealtimeRow[]) {
|
||||||
const sourceMap = new Map<string, RealtimeSourceCoverage>();
|
const sourceMap = new Map<string, RealtimeSourceCoverage>();
|
||||||
rows.forEach((row) => {
|
rows.forEach((row) => {
|
||||||
@@ -624,6 +664,52 @@ export function Realtime({
|
|||||||
amapConfigured,
|
amapConfigured,
|
||||||
runtimeRelease: runtime?.platformRelease
|
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 selectRealtimeRow = (row: VehicleRealtimeRow) => {
|
||||||
const index = rows.indexOf(row);
|
const index = rows.indexOf(row);
|
||||||
if (index < 0) return;
|
if (index < 0) return;
|
||||||
@@ -931,6 +1017,35 @@ export function Realtime({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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 }}>
|
<Card bordered title="实时覆盖判读" style={{ marginBottom: 16 }}>
|
||||||
<div className="vp-stat-insight-grid">
|
<div className="vp-stat-insight-grid">
|
||||||
{[
|
{[
|
||||||
|
|||||||
@@ -344,6 +344,48 @@ button.vp-realtime-command-item:focus-visible {
|
|||||||
line-height: 28px;
|
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 {
|
.vp-source-coverage-board {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
padding: 14px;
|
padding: 14px;
|
||||||
@@ -2056,6 +2098,8 @@ button.vp-realtime-command-item:focus-visible {
|
|||||||
.vp-current-service-grid,
|
.vp-current-service-grid,
|
||||||
.vp-realtime-command-board,
|
.vp-realtime-command-board,
|
||||||
.vp-realtime-command-grid,
|
.vp-realtime-command-grid,
|
||||||
|
.vp-realtime-impact-board,
|
||||||
|
.vp-realtime-impact-grid,
|
||||||
.vp-source-coverage-grid,
|
.vp-source-coverage-grid,
|
||||||
.vp-runbook-grid,
|
.vp-runbook-grid,
|
||||||
.vp-workbench-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 () => {
|
test('frames realtime page as one vehicle service with source evidence', async () => {
|
||||||
window.history.replaceState(null, '', '/#/realtime');
|
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) => {
|
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
|
||||||
const path = String(input);
|
const path = String(input);
|
||||||
if (path.includes('/api/realtime/vehicles')) {
|
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('JT808 在线')).toBeInTheDocument();
|
||||||
expect(screen.getByText('YUTONG_MQTT 未接入')).toBeInTheDocument();
|
expect(screen.getByText('YUTONG_MQTT 未接入')).toBeInTheDocument();
|
||||||
expect(screen.getAllByText('2/2 来源在线').length).toBeGreaterThan(0);
|
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 () => {
|
test('refreshes realtime vehicle data from the monitoring workspace', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user