feat(platform): add online statistics operations board

This commit is contained in:
lingniu
2026-07-04 22:59:48 +08:00
parent c0f5ec186e
commit f65b035be7
3 changed files with 195 additions and 1 deletions

View File

@@ -399,6 +399,55 @@ function onlineStatusHandoffText({
].join('\n');
}
function onlineOperationsReportText({
filters,
summary,
onlineRows,
total,
statisticsURL,
vehicleURL
}: {
filters: Record<string, string>;
summary: OnlineStatisticsSummary;
onlineRows: OnlineVehicleStatusRow[];
total: number;
statisticsURL: string;
vehicleURL?: string;
}) {
const keyword = filters.keyword?.trim() || '全部车辆';
const protocol = filters.protocol?.trim() || '全部来源';
const protocolStats = Array.isArray(summary.protocolStats) ? summary.protocolStats : [];
const offlineRows = onlineRows.filter((row) => !row.online);
const longestOffline = offlineRows.reduce<OnlineVehicleStatusRow | undefined>((current, row) => {
if (!current) return row;
return Number(row.offlineDurationMinutes ?? 0) > Number(current.offlineDurationMinutes ?? 0) ? row : current;
}, undefined);
return [
'【在线统计运营态势】',
`车辆范围:${keyword}`,
`数据来源:${protocol}`,
`在线概览:${summary.onlineVehicleCount.toLocaleString()} 在线 / ${summary.offlineVehicleCount.toLocaleString()} 离线 / 在线率 ${formatPercent(summary.onlineRatePercent)}`,
`Redis 在线 Key${summary.redisOnlineKeys == null ? '-' : summary.redisOnlineKeys.toLocaleString()}`,
`统计证据:${summary.evidence || '-'}`,
`当前页车辆:${onlineRows.length.toLocaleString()} / 当前筛选 ${total.toLocaleString()}`,
`当前页离线:${offlineRows.length.toLocaleString()}`,
`最长离线:${longestOffline ? `${longestOffline.plate || longestOffline.vin} / ${formatOfflineDuration(longestOffline.offlineDurationMinutes)} / ${longestOffline.lastSeen || '-'}` : '-'}`,
'',
'协议在线:',
...(protocolStats.length > 0 ? protocolStats.map((item, index) => {
const rate = item.total > 0 ? (item.online / item.total) * 100 : 0;
return `${index + 1}. ${item.protocol}${item.online.toLocaleString()}/${item.total.toLocaleString()},在线率 ${formatPercent(rate)}`;
}) : ['暂无协议在线统计']),
'',
'处置建议:',
'1. 在线率下降时先检查 Redis online key、协议接入和最后上报时间',
'2. 离线车辆优先打开车辆服务核对实时、轨迹、RAW 和告警证据',
'3. 统计发布前确认离线车辆是否影响当日里程和区间闭合',
`统计查询:${statisticsURL}`,
...(vehicleURL ? [`车辆服务:${vehicleURL}`] : [])
].join('\n');
}
async function copyText(value: string, label: string) {
const text = value.trim();
if (!text) {
@@ -474,6 +523,11 @@ export function Mileage({
redisOnlineKeyText,
onlineSummary.evidence
].filter(Boolean).join('');
const onlineProtocolStats = Array.isArray(onlineSummary.protocolStats) ? onlineSummary.protocolStats : [];
const topOfflineRow = onlineRows.filter((row) => !row.online).reduce<OnlineVehicleStatusRow | undefined>((current, row) => {
if (!current) return row;
return Number(row.offlineDurationMinutes ?? 0) > Number(current.offlineDurationMinutes ?? 0) ? row : current;
}, undefined);
const publishAuditItems: MileagePublishAuditItem[] = [
{
key: 'closure',
@@ -723,6 +777,22 @@ export function Mileage({
'在线状态交接'
);
};
const copyOnlineOperationsReport = () => {
const vehicleURL = currentVehicleKeyword
? appURL(buildAppHash({ page: 'detail', keyword: currentVehicleKeyword, protocol: currentProtocol }))
: undefined;
copyText(
onlineOperationsReportText({
filters,
summary: onlineSummary,
onlineRows,
total: onlinePagination.total,
statisticsURL: appURL(window.location.hash || buildAppHash({ page: 'mileage', keyword: currentVehicleKeyword, protocol: currentProtocol })),
vehicleURL
}),
'在线统计运营态势'
);
};
useEffect(() => {
const nextFilters = mergeInitialFilters(initialVin, initialProtocol, initialFilters);
@@ -865,6 +935,69 @@ export function Mileage({
</Card>
))}
</div>
<Card
bordered
loading={onlineLoading}
title={<Space><span>线</span><Button size="small" onClick={copyOnlineOperationsReport}>线</Button></Space>}
style={{ marginTop: 16 }}
>
<div className="vp-online-ops-board">
<div className="vp-online-ops-summary">
<Space wrap>
<Tag color={onlineRatePercent >= 90 ? 'green' : onlineRatePercent >= 60 ? 'orange' : 'red'}>
线 {formatPercent(onlineRatePercent)}
</Tag>
<Tag color={onlineSummary.redisOnlineKeys == null ? 'grey' : 'blue'}>
Redis {onlineSummary.redisOnlineKeys == null ? '-' : onlineSummary.redisOnlineKeys.toLocaleString()}
</Tag>
</Space>
<div className="vp-monitor-metric-value">{onlineVehicleCount.toLocaleString()} 线</div>
<Typography.Text type="secondary">
线线
</Typography.Text>
</div>
<div className="vp-online-ops-grid">
{[
{
label: '离线车辆',
value: offlineVehicleCount.toLocaleString(),
detail: '离线车辆会影响实时统计、位置查询和当日里程完整性。',
color: offlineVehicleCount > 0 ? 'orange' as const : 'green' as const
},
{
label: '协议覆盖',
value: onlineProtocolStats.length.toLocaleString(),
detail: onlineProtocolStats.length > 0 ? onlineProtocolStats.map((item) => `${item.protocol} ${item.online}/${item.total}`).join('') : '暂无协议在线统计。',
color: onlineProtocolStats.length > 0 ? 'blue' as const : 'grey' as const
},
{
label: '当前页离线',
value: onlineRows.filter((row) => !row.online).length.toLocaleString(),
detail: `${onlineRows.length.toLocaleString()} 条在线状态明细参与当前页复核。`,
color: onlineRows.some((row) => !row.online) ? 'orange' as const : 'green' as const
},
{
label: '最长离线',
value: topOfflineRow ? formatOfflineDuration(topOfflineRow.offlineDurationMinutes) : '-',
detail: topOfflineRow ? `${topOfflineRow.plate || topOfflineRow.vin} / ${topOfflineRow.lastSeen || '-'}` : '当前页暂无离线车辆。',
color: topOfflineRow ? 'orange' as const : 'green' as const
},
{
label: '统计证据',
value: onlineSummary.evidence || '-',
detail: '后端在线统计证据,可与 Redis key 和车辆快照互相校验。',
color: onlineSummary.evidence ? 'blue' as const : 'grey' as const
}
].map((item) => (
<div key={item.label} className="vp-online-ops-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="统计闭合工作台" loading={summaryLoading || loading} style={{ marginTop: 16 }}>
<div className="vp-stat-closure-board">
{[