Files
lingniu-vehicle-ingest/vehicle-data-platform/apps/web/src/pages/Realtime.tsx

1830 lines
89 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Button, Card, Form, Select, Space, Table, Tabs, Tag, Toast, Typography } from '@douyinfe/semi-ui';
import { IconCopy } from '@douyinfe/semi-icons';
import { useEffect, useState } from 'react';
import { api } from '../api/client';
import type { OpsHealth, VehicleRealtimeRow } from '../api/types';
import { DataEmpty } from '../components/DataEmpty';
import { PageHeader } from '../components/PageHeader';
import { SourceStatusTags } from '../components/SourceStatusTags';
import { StatusTag } from '../components/StatusTag';
import { VehicleMap, type VehicleMapPoint } from '../components/VehicleMap';
import { getAMapConfig, isAMapConfigured } from '../config/appConfig';
import { buildAppHash } from '../domain/appRoute';
import { buildCsv, downloadCsv, type CsvColumn } from '../domain/csvExport';
function canOpenVehicle(vin?: string) {
const value = vin?.trim();
return Boolean(value && value !== 'unknown');
}
function vehicleServiceStatus(row: VehicleRealtimeRow) {
if (row.serviceStatus) {
return {
label: row.serviceStatus.title,
color: row.serviceStatus.severity === 'ok' ? 'green' as const : row.serviceStatus.severity === 'error' ? 'red' as const : 'orange' as const
};
}
if (row.onlineSourceCount <= 0) {
return { label: '车辆离线', color: 'red' as const };
}
if (row.onlineSourceCount < row.sourceCount) {
return { label: '来源不完整', color: 'orange' as const };
}
return { label: '服务正常', color: 'green' as const };
}
function sourceEvidenceText(row: VehicleRealtimeRow) {
return `${row.onlineSourceCount}/${row.sourceCount} 通道在线`;
}
function formatPercent(value: number) {
return Number.isFinite(value) ? `${value.toLocaleString(undefined, { maximumFractionDigits: 1 })}%` : '0%';
}
function serviceStatusWeight(row: VehicleRealtimeRow) {
const severity = row.serviceStatus?.severity;
if (severity === 'error') return 4;
if (severity === 'warning') return 3;
if (row.onlineSourceCount <= 0) return 4;
if (row.onlineSourceCount < row.sourceCount) return 3;
return 1;
}
function sourceIssueTags(row: VehicleRealtimeRow) {
const tags = (row.sourceStatus ?? [])
.filter((source) => !source.online || !source.hasRealtime)
.map((source) => `${source.protocol} ${source.hasRealtime ? '离线' : '未接入'}`) ?? [];
if (tags.length > 0) return tags;
if (row.onlineSourceCount < row.sourceCount) return ['来源缺失'];
return [];
}
function hasSourceIssue(row: VehicleRealtimeRow) {
const severity = row.serviceStatus?.severity;
return severity === 'warning' || severity === 'error' || row.onlineSourceCount < row.sourceCount || sourceIssueTags(row).length > 0;
}
function isValidCoordinate(row: VehicleRealtimeRow) {
return Number.isFinite(row.longitude) && Number.isFinite(row.latitude) && row.longitude !== 0 && row.latitude !== 0;
}
function realtimeMapPointId(row: VehicleRealtimeRow, index = 0) {
return row.vin?.trim() || `${row.primaryProtocol || 'source'}-${index}`;
}
function formatRefreshTime(date: Date) {
return date.toLocaleTimeString('zh-CN', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
}
function parseVehicleTime(value?: string) {
const normalized = value?.trim();
if (!normalized) {
return Number.NaN;
}
return new Date(normalized.replace(' ', 'T')).getTime();
}
function dataFreshness(row: VehicleRealtimeRow) {
const timestamp = parseVehicleTime(row.lastSeen);
if (!Number.isFinite(timestamp)) {
return {
label: '无时间',
color: 'grey' as const,
detail: '未上报最后时间',
stale: true
};
}
const ageSeconds = Math.floor((Date.now() - timestamp) / 1000);
if (ageSeconds <= 300) {
return {
label: '数据新鲜',
color: 'green' as const,
detail: ageSeconds <= 0 ? '刚刚更新' : `${Math.max(1, Math.ceil(ageSeconds / 60))} 分钟内更新`,
stale: false
};
}
if (ageSeconds < 3600) {
return {
label: '更新超时',
color: 'orange' as const,
detail: `${Math.ceil(ageSeconds / 60)} 分钟未更新`,
stale: true
};
}
if (ageSeconds < 86400) {
return {
label: '更新超时',
color: 'red' as const,
detail: `${Math.ceil(ageSeconds / 3600)} 小时未更新`,
stale: true
};
}
return {
label: '更新超时',
color: 'red' as const,
detail: `${Math.ceil(ageSeconds / 86400)} 天未更新`,
stale: true
};
}
function amapMarkerURL(row: VehicleRealtimeRow) {
const name = encodeURIComponent(row.plate || row.vin || '车辆位置');
return `https://uri.amap.com/marker?position=${row.longitude},${row.latitude}&name=${name}&src=lingniu-vehicle-platform`;
}
const onlineLabel: Record<string, string> = {
online: '在线',
offline: '离线'
};
const serviceStatusLabel: Record<string, string> = {
healthy: '服务正常',
degraded: '来源不完整',
offline: '车辆离线',
identity_required: '身份未绑定'
};
const realtimeExportColumns: CsvColumn<VehicleRealtimeRow>[] = [
{ title: 'VIN', value: (row) => row.vin },
{ title: '车牌', value: (row) => row.plate },
{ title: '手机号', value: (row) => row.phone },
{ title: 'OEM', value: (row) => row.oem },
{ title: '主要数据通道', value: (row) => row.primaryProtocol },
{ title: '数据通道', value: (row) => row.protocols?.join('|') },
{ title: '在线', value: (row) => row.online ? '在线' : '离线' },
{ title: '车辆服务状态', value: (row) => vehicleServiceStatus(row).label },
{ title: '通道在线', value: (row) => sourceEvidenceText(row) },
{ title: '经度', value: (row) => row.longitude },
{ title: '纬度', value: (row) => row.latitude },
{ title: '速度km/h', value: (row) => row.speedKmh },
{ title: 'SOC%', value: (row) => row.socPercent },
{ title: '总里程km', value: (row) => row.totalMileageKm },
{ title: '最后时间', value: (row) => row.lastSeen },
{ title: '绑定状态', value: (row) => row.bindingStatus }
];
type RealtimeSourceCoverage = {
protocol: string;
total: number;
online: number;
located: number;
degraded: number;
stale: number;
};
function realtimeExportFileName(filters: Record<string, string>) {
const keyword = filters.keyword?.trim() || 'all';
const protocol = filters.protocol?.trim() || 'all-source';
const online = filters.online?.trim() || 'all-online';
return `realtime-vehicles-${keyword}-${protocol}-${online}.csv`;
}
function realtimeFilterSummary(filters: Record<string, string>) {
return [
filters.keyword ? `关键词:${filters.keyword}` : '',
filters.protocol ? `数据通道:${filters.protocol}` : '',
filters.online ? `在线:${onlineLabel[filters.online] ?? filters.online}` : '',
filters.serviceStatus ? `服务状态:${serviceStatusLabel[filters.serviceStatus] ?? filters.serviceStatus}` : ''
].filter(Boolean);
}
function realtimeOperationsSummaryText({
filters,
rows,
total,
onlineCount,
locatedCount,
degradedCount,
sourceTypeCount,
amapConfigured,
sourceIssueRows
}: {
filters: Record<string, string>;
rows: VehicleRealtimeRow[];
total: number;
onlineCount: number;
locatedCount: number;
degradedCount: number;
sourceTypeCount: number;
amapConfigured: boolean;
sourceIssueRows: VehicleRealtimeRow[];
}) {
const issueLines = sourceIssueRows.length > 0
? sourceIssueRows.map((row, index) => {
const status = vehicleServiceStatus(row);
return `${index + 1}. ${row.plate || row.vin} / ${row.primaryProtocol || '-'} / ${status.label} / ${row.serviceStatus?.detail || sourceEvidenceText(row)}`;
}).join('\n')
: '暂无重点车辆';
return [
'【实时监控摘要】',
`当前筛选:${realtimeFilterSummary(filters).join('') || '全部实时车辆'}`,
`车辆总数:${total.toLocaleString()},当前页:${rows.length.toLocaleString()}`,
`在线车辆:${onlineCount.toLocaleString()},定位有效:${locatedCount.toLocaleString()}`,
`需要关注:${degradedCount.toLocaleString()},数据通道:${sourceTypeCount.toLocaleString()}`,
`地图配置:${amapConfigured ? '已配置' : '未配置'}`,
'重点车辆:',
issueLines,
`实时页面:${window.location.origin}${window.location.pathname}${window.location.hash}`
].join('\n');
}
function appURL(hash: string) {
return `${window.location.origin}${window.location.pathname}${hash}`;
}
function rawFrameAPIURL(row: VehicleRealtimeRow, protocol: string) {
const params = new URLSearchParams({
protocol,
vin: row.vin || '',
limit: '20',
includeFields: 'true'
});
return `${window.location.origin}/api/history/raw-frames?${params.toString()}`;
}
function realtimeIssueLabels(row: VehicleRealtimeRow) {
const labels: string[] = [];
const freshness = dataFreshness(row);
if (freshness.stale) {
labels.push(freshness.detail);
}
if (!isValidCoordinate(row)) {
labels.push('坐标无效');
}
if (hasSourceIssue(row)) {
labels.push(row.serviceStatus?.detail || sourceIssueTags(row).join('、') || sourceEvidenceText(row));
}
return labels.length > 0 ? labels : ['暂无异常'];
}
function realtimeIssueAction(row: VehicleRealtimeRow) {
const issues = realtimeIssueLabels(row).join('');
if (!isValidCoordinate(row)) {
return `核对${row.primaryProtocol || '实时数据'}定位字段解析和平台转发`;
}
if (dataFreshness(row).stale) {
return `确认${row.primaryProtocol || '实时数据'}是否持续上报,必要时查看告警事件`;
}
if (hasSourceIssue(row)) {
return '处理离线数据通道,确认车辆实时状态一致';
}
return issues === '暂无异常' ? '持续观察' : '结合车辆服务详情继续排查';
}
function realtimeIssueChecklistText({
filters,
rows,
total
}: {
filters: Record<string, string>;
rows: VehicleRealtimeRow[];
total: number;
}) {
const issueRows = rows
.filter((row) => canOpenVehicle(row.vin) && (dataFreshness(row).stale || !isValidCoordinate(row) || hasSourceIssue(row)))
.sort((a, b) => {
const statusDelta = serviceStatusWeight(b) - serviceStatusWeight(a);
if (statusDelta !== 0) return statusDelta;
return Number(dataFreshness(b).stale) - Number(dataFreshness(a).stale);
});
const lines = [
'【实时异常处置清单】',
`当前筛选:${realtimeFilterSummary(filters).join('') || '全部实时车辆'}`,
`异常车辆:${issueRows.length.toLocaleString()} / 当前页 ${rows.length.toLocaleString()} / 总计 ${total.toLocaleString()}`,
''
];
if (issueRows.length === 0) {
lines.push('当前页暂无实时异常车辆。');
}
issueRows.forEach((row, index) => {
const protocol = filters.protocol || row.primaryProtocol || '';
const status = vehicleServiceStatus(row);
const issues = realtimeIssueLabels(row).join('');
lines.push(
`${index + 1}. ${row.plate || '-'} / ${row.vin} / ${protocol || '-'}`,
` 状态:${status.label};在线:${row.online ? '在线' : '离线'};问题:${issues}`,
` 位置:${isValidCoordinate(row) ? `${row.longitude},${row.latitude}` : '无有效坐标'};速度:${row.speedKmh ?? '-'} km/hSOC${row.socPercent ?? '-'}%`,
` 最后时间:${row.lastSeen || '-'};建议动作:${realtimeIssueAction(row)}`,
` 车辆服务:${appURL(buildAppHash({ page: 'detail', keyword: row.vin, protocol }))}`,
` 实时监控:${appURL(buildAppHash({ page: 'realtime', keyword: row.vin, protocol }))}`,
` 轨迹回放:${appURL(buildAppHash({ page: 'history', keyword: row.vin, protocol }))}`,
` 告警事件:${appURL(buildAppHash({ page: 'alert-events', keyword: row.vin, protocol }))}`
);
});
return lines.join('\n');
}
function realtimeDutyHandoffText({
filters,
rows,
total,
onlineCount,
locatedCount,
degradedCount,
staleCount,
sourceTypeCount,
amapConfigured,
runtimeRelease
}: {
filters: Record<string, string>;
rows: VehicleRealtimeRow[];
total: number;
onlineCount: number;
locatedCount: number;
degradedCount: number;
staleCount: number;
sourceTypeCount: number;
amapConfigured: boolean;
runtimeRelease?: string;
}) {
const sourceCoverage = new Map<string, { total: number; online: number; realtime: number }>();
rows.forEach((row) => {
row.sourceStatus?.forEach((source) => {
const current = sourceCoverage.get(source.protocol) ?? { total: 0, online: 0, realtime: 0 };
current.total += 1;
current.online += source.online ? 1 : 0;
current.realtime += source.hasRealtime ? 1 : 0;
sourceCoverage.set(source.protocol, current);
});
});
const sourceLines = [...sourceCoverage.entries()]
.sort(([left], [right]) => left.localeCompare(right))
.map(([protocol, item]) => `${protocol}:在线 ${item.online}/${item.total},实时 ${item.realtime}/${item.total}`);
const sampleRows = rows
.filter((row) => canOpenVehicle(row.vin))
.sort((a, b) => {
const statusDelta = serviceStatusWeight(b) - serviceStatusWeight(a);
if (statusDelta !== 0) return statusDelta;
return Number(dataFreshness(b).stale) - Number(dataFreshness(a).stale);
})
.slice(0, 8);
const lines = [
'【实时运营交接包】',
`当前筛选:${realtimeFilterSummary(filters).join('') || '全部实时车辆'}`,
`页面车辆:${rows.length.toLocaleString()} / 总计 ${total.toLocaleString()};版本:${runtimeRelease || '未标记'}`,
`在线:${onlineCount.toLocaleString()};离线:${(rows.length - onlineCount).toLocaleString()};定位有效:${locatedCount.toLocaleString()};需要关注:${degradedCount.toLocaleString()};超时:${staleCount.toLocaleString()};数据通道:${sourceTypeCount.toLocaleString()}`,
`地图:${amapConfigured ? '高德已配置' : '高德未配置'}`,
`车辆覆盖:${sourceLines.length > 0 ? sourceLines.join('') : '当前页暂无来源明细'}`,
`实时入口:${appURL(buildAppHash({ page: 'realtime', protocol: filters.protocol, filters }))}`,
''
];
if (sampleRows.length === 0) {
lines.push('当前页暂无可交接 VIN 样本。');
return lines.join('\n');
}
lines.push('车辆样本:');
sampleRows.forEach((row, index) => {
const protocol = filters.protocol || row.primaryProtocol || '';
const status = vehicleServiceStatus(row);
const freshness = dataFreshness(row);
lines.push(
`${index + 1}. ${row.plate || '-'} / ${row.vin} / ${protocol || '-'} / ${status.label}`,
` 最后上报:${row.lastSeen || '-'};新鲜度:${freshness.label};位置:${isValidCoordinate(row) ? `${row.longitude},${row.latitude}` : '无有效坐标'};速度:${row.speedKmh ?? '-'} km/hSOC${row.socPercent ?? '-'}%`,
` 数据覆盖:${sourceEvidenceText(row)};问题:${realtimeIssueLabels(row).join('')}`,
` 车辆服务:${appURL(buildAppHash({ page: 'detail', keyword: row.vin, protocol }))}`,
` 实时监控:${appURL(buildAppHash({ page: 'realtime', keyword: row.vin, protocol }))}`,
` 轨迹回放:${appURL(buildAppHash({ page: 'history', keyword: row.vin, protocol }))}`,
` 里程统计:${appURL(buildAppHash({ page: 'mileage', keyword: row.vin, protocol }))}`,
` 原始记录:${protocol ? rawFrameAPIURL(row, protocol) : '-'}`
);
});
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 === '实时稳定' ? '保持监控并关注告警队列' : '优先处理离线、超时和来源不完整车辆,并回到轨迹和原始记录复核'}`,
`实时监控:${appURL(buildAppHash({ page: 'realtime', protocol: filters.protocol, filters }))}`
].join('\n');
}
function mapCustomerPackageText({
filters,
rows,
total,
onlineCount,
locatedCount,
attentionCount,
staleCount,
selectedRow,
selectedProtocol,
amapConfigured
}: {
filters: Record<string, string>;
rows: VehicleRealtimeRow[];
total: number;
onlineCount: number;
locatedCount: number;
attentionCount: number;
staleCount: number;
selectedRow?: VehicleRealtimeRow;
selectedProtocol?: string;
amapConfigured: boolean;
}) {
const selectedVIN = selectedRow?.vin || '';
return [
'【客户地图监控包】',
`当前筛选:${realtimeFilterSummary(filters).join('') || '全部车辆'}`,
`车辆范围:当前页 ${rows.length.toLocaleString()} / 总计 ${total.toLocaleString()}`,
`在线态势:${onlineCount.toLocaleString()} 在线 / ${(rows.length - onlineCount).toLocaleString()} 离线`,
`定位态势:${locatedCount.toLocaleString()} 辆有有效坐标`,
`关注车辆:${attentionCount.toLocaleString()} 辆;更新超时 ${staleCount.toLocaleString()}`,
`地图能力:${amapConfigured ? '高德地图可用' : '坐标预览'}`,
`选中车辆:${selectedRow ? `${selectedRow.plate || '-'} / ${selectedVIN} / ${selectedProtocol || selectedRow.primaryProtocol || '-'}` : '未选择'}`,
selectedRow ? `选中车辆状态:${vehicleServiceStatus(selectedRow).label}${dataFreshness(selectedRow).detail}${realtimeIssueLabels(selectedRow).join('')}` : '',
selectedRow ? `选中车辆位置:${isValidCoordinate(selectedRow) ? `${selectedRow.longitude},${selectedRow.latitude}` : '无有效坐标'}` : '',
`实时地图:${appURL(buildAppHash({ page: 'map', protocol: filters.protocol, filters }))}`,
selectedVIN ? `车辆服务:${appURL(buildAppHash({ page: 'detail', keyword: selectedVIN, protocol: selectedProtocol }))}` : '',
selectedVIN ? `轨迹回放:${appURL(buildAppHash({ page: 'history', keyword: selectedVIN, protocol: selectedProtocol }))}` : '',
selectedVIN ? `里程统计:${appURL(buildAppHash({ page: 'mileage', keyword: selectedVIN, protocol: selectedProtocol }))}` : '',
selectedVIN ? `历史数据:${appURL(buildAppHash({ page: 'history-query', keyword: selectedVIN, protocol: selectedProtocol, filters: { tab: 'raw', includeFields: 'true' } }))}` : '',
selectedVIN ? `告警通知:${appURL(buildAppHash({ page: 'alert-events', keyword: selectedVIN, protocol: selectedProtocol }))}` : ''
].filter(Boolean).join('\n');
}
function mapCustomerDecisionText({
filters,
rows,
total,
onlineCount,
locatedCount,
attentionRows,
selectedRow,
selectedProtocol,
amapConfigured
}: {
filters: Record<string, string>;
rows: VehicleRealtimeRow[];
total: number;
onlineCount: number;
locatedCount: number;
attentionRows: VehicleRealtimeRow[];
selectedRow?: VehicleRealtimeRow;
selectedProtocol?: string;
amapConfigured: boolean;
}) {
const selectedVIN = selectedRow?.vin || '';
const topAttention = attentionRows[0];
return [
'【地图客户决策说明】',
`当前筛选:${realtimeFilterSummary(filters).join('') || '全部车辆'}`,
`车辆范围:当前页 ${rows.length.toLocaleString()} / 总计 ${total.toLocaleString()}`,
`在线判断:${onlineCount.toLocaleString()} 在线 / ${(rows.length - onlineCount).toLocaleString()} 离线`,
`定位判断:${locatedCount.toLocaleString()} 辆有有效坐标 / ${Math.max(0, rows.length - locatedCount).toLocaleString()} 辆无坐标`,
`关注判断:${attentionRows.length.toLocaleString()} 辆需要关注${topAttention ? `;优先车辆 ${topAttention.plate || topAttention.vin}${realtimeIssueLabels(topAttention).join('')}` : ''}`,
`地图能力:${amapConfigured ? '高德地图可用' : '坐标预览'}`,
`选中车辆:${selectedRow ? `${selectedRow.plate || '-'} / ${selectedVIN} / ${selectedProtocol || selectedRow.primaryProtocol || '-'}` : '未选择'}`,
selectedRow ? `选中车辆下一步:${vehicleServiceStatus(selectedRow).label}${dataFreshness(selectedRow).detail}${realtimeIssueLabels(selectedRow).join('')}` : '选中车辆下一步:先从地图或车辆列表选择一辆车',
`实时地图:${appURL(buildAppHash({ page: 'map', protocol: filters.protocol, filters }))}`,
`在线车辆:${appURL(buildAppHash({ page: 'map', protocol: filters.protocol, filters: { ...filters, online: 'online' } }))}`,
selectedVIN ? `车辆服务:${appURL(buildAppHash({ page: 'detail', keyword: selectedVIN, protocol: selectedProtocol }))}` : '',
selectedVIN ? `轨迹回放:${appURL(buildAppHash({ page: 'history', keyword: selectedVIN, protocol: selectedProtocol }))}` : '',
selectedVIN ? `数据导出:${appURL(buildAppHash({ page: 'history-query', keyword: selectedVIN, protocol: selectedProtocol, filters: { tab: 'raw', includeFields: 'true' } }))}` : ''
].filter(Boolean).join('\n');
}
function buildRealtimeSourceCoverage(rows: VehicleRealtimeRow[]) {
const sourceMap = new Map<string, RealtimeSourceCoverage>();
rows.forEach((row) => {
const protocols = row.sourceStatus?.length
? row.sourceStatus.map((source) => source.protocol)
: row.primaryProtocol ? [row.primaryProtocol] : [];
protocols.forEach((protocol) => {
const source = row.sourceStatus?.find((item) => item.protocol === protocol);
const current = sourceMap.get(protocol) ?? { protocol, total: 0, online: 0, located: 0, degraded: 0, stale: 0 };
current.total += 1;
current.online += (source ? source.online : row.online) ? 1 : 0;
current.located += isValidCoordinate(row) ? 1 : 0;
current.degraded += (!source || !source.online || !source.hasRealtime || row.onlineSourceCount < row.sourceCount) ? 1 : 0;
current.stale += dataFreshness(row).stale ? 1 : 0;
sourceMap.set(protocol, current);
});
});
return [...sourceMap.values()].sort((left, right) => right.total - left.total || left.protocol.localeCompare(right.protocol));
}
async function copyText(value: string, label: string) {
const text = value.trim();
if (!text) {
Toast.warning(`${label}为空`);
return;
}
try {
await navigator.clipboard.writeText(text);
Toast.success(`已复制${label}`);
} catch {
Toast.error(`复制${label}失败`);
}
}
export function Realtime({
mode = 'realtime',
title = '实时监控',
description = '以车辆为主对象查看最新位置、在线来源、核心实时数据和地图作业状态',
onOpenVehicle,
onOpenHistory,
onOpenQuality,
onFiltersChange,
initialFilters = {}
}: {
mode?: 'realtime' | 'map';
title?: string;
description?: string;
onOpenVehicle: (vin: string, protocol?: string) => void;
onOpenHistory?: (vin: string, protocol?: string) => void;
onOpenQuality?: (filters: Record<string, string>) => void;
onFiltersChange?: (filters: Record<string, string>) => void;
initialFilters?: Record<string, string>;
}) {
const [rows, setRows] = useState<VehicleRealtimeRow[]>([]);
const [loading, setLoading] = useState(true);
const [filters, setFilters] = useState<Record<string, string>>(initialFilters);
const [pagination, setPagination] = useState({ currentPage: 1, pageSize: 50, total: 0 });
const [opsHealth, setOpsHealth] = useState<OpsHealth | null>(null);
const [selectedMapPointId, setSelectedMapPointId] = useState('');
const [lastRefreshAt, setLastRefreshAt] = useState('');
const [autoRefresh, setAutoRefresh] = useState(true);
const refreshIntervalSeconds = 15;
const amapConfig = getAMapConfig();
const runtime = opsHealth?.runtime;
const amapConfigured = runtime?.amapWebJsConfigured ?? isAMapConfigured(amapConfig);
const amapSecurityServiceHost = runtime?.amapSecurityServiceHost || amapConfig.securityServiceHost;
const amapSecurityProxyEnabled = runtime?.amapSecurityProxyEnabled ?? Boolean(amapSecurityServiceHost);
const amapSecurityCodeExposed = runtime?.amapSecurityCodeExposed ?? Boolean(amapConfig.securityJsCode && !amapSecurityServiceHost);
const load = (values: Record<string, string> = filters, page = pagination.currentPage, pageSize = pagination.pageSize) => {
setLoading(true);
const params = new URLSearchParams({ limit: String(pageSize), offset: String((page - 1) * pageSize) });
if (values?.keyword) params.set('keyword', values.keyword);
if (values?.protocol) params.set('protocol', values.protocol);
if (values?.online) params.set('online', values.online);
if (values?.serviceStatus) params.set('serviceStatus', values.serviceStatus);
return api.vehicleRealtime(params)
.then((nextPage) => {
setRows(nextPage.items);
setPagination({ currentPage: page, pageSize, total: nextPage.total });
setLastRefreshAt(formatRefreshTime(new Date()));
})
.catch((error: Error) => Toast.error(error.message))
.finally(() => setLoading(false));
};
useEffect(() => {
setFilters(initialFilters);
load(initialFilters, 1, pagination.pageSize);
api.opsHealth()
.then(setOpsHealth)
.catch(() => setOpsHealth(null));
}, [JSON.stringify(initialFilters)]);
useEffect(() => {
if (!autoRefresh) {
return undefined;
}
const timer = window.setInterval(() => {
load(filters, pagination.currentPage, pagination.pageSize);
}, refreshIntervalSeconds * 1000);
return () => window.clearInterval(timer);
}, [autoRefresh, JSON.stringify(filters), pagination.currentPage, pagination.pageSize]);
const applyFilters = (nextFilters: Record<string, string>) => {
setFilters(nextFilters);
onFiltersChange?.(nextFilters);
load(nextFilters, 1, pagination.pageSize);
};
const openQualityEvidence = (row: VehicleRealtimeRow) => {
if (!canOpenVehicle(row.vin)) return;
onOpenQuality?.({
keyword: row.vin,
protocol: filters.protocol || row.primaryProtocol || ''
});
};
const exportRealtime = () => {
if (rows.length === 0) {
Toast.warning('当前没有可导出的实时车辆');
return;
}
downloadCsv(realtimeExportFileName(filters), buildCsv(realtimeExportColumns, rows));
Toast.success(`已导出 ${rows.length} 条实时车辆`);
};
const filterSummary = realtimeFilterSummary(filters);
const onlineCount = rows.filter((row) => row.online).length;
const locatedCount = rows.filter(isValidCoordinate).length;
const degradedCount = rows.filter((row) => row.onlineSourceCount < row.sourceCount).length;
const staleCount = rows.filter((row) => dataFreshness(row).stale).length;
const onlineRate = rows.length > 0 ? (onlineCount / rows.length) * 100 : 0;
const locatedRate = rows.length > 0 ? (locatedCount / rows.length) * 100 : 0;
const degradedRate = rows.length > 0 ? (degradedCount / rows.length) * 100 : 0;
const pageCoverageRate = pagination.total > 0 ? (rows.length / pagination.total) * 100 : 0;
const primaryProtocols = new Set(rows.map((row) => row.primaryProtocol).filter(Boolean));
const sourceCoverageRows = buildRealtimeSourceCoverage(rows);
const sourceIssueRows = rows
.filter((row) => canOpenVehicle(row.vin) && hasSourceIssue(row))
.sort((a, b) => {
const statusDelta = serviceStatusWeight(b) - serviceStatusWeight(a);
if (statusDelta !== 0) return statusDelta;
return String(b.lastSeen ?? '').localeCompare(String(a.lastSeen ?? ''));
})
.slice(0, 4);
const mapServiceRows = rows
.filter((row) => isValidCoordinate(row) && canOpenVehicle(row.vin))
.sort((a, b) => {
const statusDelta = serviceStatusWeight(b) - serviceStatusWeight(a);
if (statusDelta !== 0) return statusDelta;
return String(b.lastSeen ?? '').localeCompare(String(a.lastSeen ?? ''));
})
.slice(0, 5);
const defaultMapRow = mapServiceRows[0] ?? rows.find((row) => isValidCoordinate(row) && canOpenVehicle(row.vin));
const selectedMapRow = rows.find((row, index) => realtimeMapPointId(row, index) === selectedMapPointId) ?? defaultMapRow;
const selectedMapPointKey = selectedMapRow ? realtimeMapPointId(selectedMapRow, rows.indexOf(selectedMapRow)) : selectedMapPointId;
const mapPoints: VehicleMapPoint[] = rows.map((row, index) => ({
id: realtimeMapPointId(row, index),
label: row.plate || row.vin || 'unknown',
longitude: row.longitude,
latitude: row.latitude,
online: row.online,
title: `${row.plate || row.vin || '-'} ${vehicleServiceStatus(row).label} ${row.primaryProtocol || ''} ${row.lastSeen || ''}`
}));
const mapIntegrationRows = [
{
label: 'Web JS Key',
value: amapConfigured ? '已配置' : '未配置',
color: amapConfigured ? 'green' as const : 'orange' as const,
detail: amapConfigured ? '前端可加载高德 Web JS API。' : '缺少公开 Key地图将使用坐标预览。'
},
{
label: '服务端 API Key',
value: runtime?.amapApiConfigured ? '已配置' : '未配置',
color: runtime?.amapApiConfigured ? 'green' as const : 'orange' as const,
detail: runtime?.amapApiConfigured ? '后端可支持地理编码、路线、围栏等服务端地图能力。' : '服务端地图能力未配置,后续地理服务会降级。'
},
{
label: '安全代理',
value: amapSecurityServiceHost || '未启用',
color: amapSecurityProxyEnabled ? 'green' as const : 'grey' as const,
detail: amapSecurityProxyEnabled ? '安全密钥由服务端追加,不下发到浏览器。' : '未配置代理时会退回前端安全码模式。'
},
{
label: '安全码',
value: amapSecurityCodeExposed ? '已暴露' : '未暴露',
color: amapSecurityCodeExposed ? 'red' as const : 'green' as const,
detail: amapSecurityCodeExposed ? '当前安全码会下发到浏览器,只建议调试使用。' : '生产安全码保持在服务端环境变量中。'
},
{
label: '当前版本',
value: runtime?.platformRelease || '未标记',
color: runtime?.platformRelease ? 'blue' as const : 'grey' as const,
detail: runtime?.platformRelease ? '当前 ECS 正在运行的车辆中台 release。' : '未注入 PLATFORM_RELEASE部署追踪会降级。'
},
{
label: '定位覆盖',
value: `${locatedCount.toLocaleString()} / ${rows.length.toLocaleString()}`,
color: locatedCount > 0 ? 'blue' as const : 'orange' as const,
detail: '当前页有效经纬度车辆数。'
},
{
label: '高德 URI',
value: '坐标跳转',
color: 'blue' as const,
detail: '车辆队列可直接打开高德坐标,轨迹页可打开线路。'
}
];
const copyRealtimeSummary = () => copyText(realtimeOperationsSummaryText({
filters,
rows,
total: pagination.total,
onlineCount,
locatedCount,
degradedCount,
sourceTypeCount: primaryProtocols.size,
amapConfigured,
sourceIssueRows
}), '实时摘要');
const copyRealtimeIssueChecklist = () => copyText(realtimeIssueChecklistText({ filters, rows, total: pagination.total }), '实时异常处置清单');
const copyRealtimeDutyHandoff = () => copyText(realtimeDutyHandoffText({
filters,
rows,
total: pagination.total,
onlineCount,
locatedCount,
degradedCount,
staleCount,
sourceTypeCount: primaryProtocols.size,
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;
setSelectedMapPointId(realtimeMapPointId(row, index));
};
const selectRealtimeMapPoint = (point: VehicleMapPoint) => {
setSelectedMapPointId(point.id);
};
const mapAttentionRows = rows
.filter((row) => canOpenVehicle(row.vin) && (dataFreshness(row).stale || !isValidCoordinate(row) || hasSourceIssue(row)))
.sort((a, b) => serviceStatusWeight(b) - serviceStatusWeight(a) || String(b.lastSeen ?? '').localeCompare(String(a.lastSeen ?? '')))
.slice(0, 8);
const mapFleetKpis = [
{ label: '车辆总数', value: pagination.total.toLocaleString(), color: 'blue' as const, helper: `当前页 ${rows.length.toLocaleString()}` },
{ label: '在线车辆', value: onlineCount.toLocaleString(), color: onlineCount > 0 ? 'green' as const : 'orange' as const, helper: `在线率 ${formatPercent(onlineRate)}` },
{ label: '有效定位', value: locatedCount.toLocaleString(), color: locatedCount > 0 ? 'green' as const : 'orange' as const, helper: `定位率 ${formatPercent(locatedRate)}` },
{ label: '需关注', value: mapAttentionRows.length.toLocaleString(), color: mapAttentionRows.length > 0 ? 'orange' as const : 'green' as const, helper: `${staleCount.toLocaleString()} 辆更新超时` }
];
const missingCoordinateCount = Math.max(0, rows.length - locatedCount);
const selectedVehicleProtocol = selectedMapRow ? filters.protocol || selectedMapRow.primaryProtocol || '' : '';
const selectedVehicleLabel = selectedMapRow?.plate || selectedMapRow?.vin || '未选择车辆';
const openSelectedVehicleRaw = () => {
if (!selectedMapRow || !canOpenVehicle(selectedMapRow.vin)) {
Toast.warning('请先选择可查询的车辆');
return;
}
window.location.hash = buildAppHash({
page: 'history-query',
keyword: selectedMapRow.vin,
protocol: selectedVehicleProtocol,
filters: { tab: 'raw', includeFields: 'true' }
});
};
const mapVehicleWorkItems = [
{
title: '车辆服务',
value: selectedVehicleLabel,
detail: selectedMapRow ? '查看车辆画像、来源状态和最新实时字段。' : '先从地图或车辆列表选择一辆车。',
action: '车辆档案',
color: selectedMapRow ? vehicleServiceStatus(selectedMapRow).color : 'grey' as const,
disabled: !selectedMapRow || !canOpenVehicle(selectedMapRow.vin),
onClick: () => selectedMapRow && onOpenVehicle(selectedMapRow.vin, selectedVehicleProtocol)
},
{
title: '轨迹复盘',
value: selectedMapRow ? dataFreshness(selectedMapRow).detail : '等待选择',
detail: '复核位置、速度、里程和断点,支撑客户问询。',
action: '打开轨迹',
color: selectedMapRow ? dataFreshness(selectedMapRow).color : 'grey' as const,
disabled: !selectedMapRow || !canOpenVehicle(selectedMapRow.vin),
onClick: () => selectedMapRow && onOpenHistory?.(selectedMapRow.vin, selectedVehicleProtocol)
},
{
title: '里程核对',
value: selectedMapRow?.totalMileageKm != null ? `${selectedMapRow.totalMileageKm} km` : '无里程',
detail: '进入单车里程统计,核对日里程和区间差值。',
action: '查看里程',
color: selectedMapRow?.totalMileageKm != null ? 'blue' as const : 'orange' as const,
disabled: !selectedMapRow || !canOpenVehicle(selectedMapRow.vin),
onClick: () => {
if (!selectedMapRow) return;
window.location.hash = buildAppHash({
page: 'mileage',
keyword: selectedMapRow.vin,
protocol: selectedVehicleProtocol
});
}
},
{
title: '告警处置',
value: selectedMapRow ? realtimeIssueLabels(selectedMapRow)[0] : '等待选择',
detail: '围绕选中车辆查看断链、离线、定位异常等事件。',
action: '查看告警',
color: selectedMapRow && hasSourceIssue(selectedMapRow) ? 'orange' as const : selectedMapRow ? 'green' as const : 'grey' as const,
disabled: !selectedMapRow || !canOpenVehicle(selectedMapRow.vin) || !onOpenQuality,
onClick: () => selectedMapRow && onOpenQuality?.({
keyword: selectedMapRow.vin,
protocol: selectedVehicleProtocol
})
}
];
const mapCustomerTasks = [
{
title: '实时盯车',
value: `${onlineCount.toLocaleString()} 在线`,
detail: '先看在线车辆和有效定位,确认客户当前能看到车在哪里。',
color: 'green' as const,
primaryAction: '只看在线',
secondaryAction: '刷新地图',
onPrimary: () => applyFilters({ ...filters, online: 'online' }),
onSecondary: () => load(filters, pagination.currentPage, pagination.pageSize),
disabled: false,
secondaryDisabled: false
},
{
title: '异常优先',
value: `${mapAttentionRows.length.toLocaleString()} 辆关注`,
detail: mapAttentionRows[0] ? `${mapAttentionRows[0].plate || mapAttentionRows[0].vin}${realtimeIssueLabels(mapAttentionRows[0]).join('')}` : '当前筛选下暂无需要关注的车辆。',
color: mapAttentionRows.length > 0 ? 'orange' as const : 'green' as const,
primaryAction: '关注车辆',
secondaryAction: '告警事件',
onPrimary: () => applyFilters({ ...filters, serviceStatus: 'degraded' }),
onSecondary: () => onOpenQuality?.({ serviceStatus: 'degraded' }),
disabled: !onOpenQuality && mapAttentionRows.length === 0,
secondaryDisabled: !onOpenQuality
},
{
title: '轨迹复盘',
value: selectedMapRow?.plate || selectedMapRow?.vin || '未选车辆',
detail: selectedMapRow ? `围绕 ${selectedMapRow.plate || selectedMapRow.vin} 回放位置、速度和里程变化。` : '从地图或车辆队列选择一辆车后回放轨迹。',
color: selectedMapRow ? 'blue' as const : 'grey' as const,
primaryAction: '轨迹回放',
secondaryAction: '车辆档案',
disabled: !selectedMapRow || !canOpenVehicle(selectedMapRow.vin),
onPrimary: () => selectedMapRow && onOpenHistory?.(selectedMapRow.vin, selectedVehicleProtocol),
onSecondary: () => selectedMapRow && onOpenVehicle(selectedMapRow.vin, selectedVehicleProtocol),
secondaryDisabled: !selectedMapRow || !canOpenVehicle(selectedMapRow.vin)
},
{
title: '查询导出',
value: `${rows.length.toLocaleString()} 辆当前页`,
detail: '导出当前车辆清单,或进入选中车辆的历史记录。',
color: 'blue' as const,
primaryAction: '导出当前页',
secondaryAction: '历史记录',
onPrimary: exportRealtime,
onSecondary: openSelectedVehicleRaw,
disabled: false,
secondaryDisabled: !selectedMapRow || !canOpenVehicle(selectedMapRow.vin)
}
];
const mapCustomerDecisionItems = [
{
label: '先看在线',
value: `${onlineCount.toLocaleString()} 在线`,
detail: `${(rows.length - onlineCount).toLocaleString()} 辆离线,先确认客户能看到哪些车还在上报。`,
color: onlineCount > 0 ? 'green' as const : 'orange' as const,
action: '只看在线',
onClick: () => applyFilters({ ...filters, online: 'online' })
},
{
label: '再看定位',
value: `${locatedCount.toLocaleString()} 有坐标`,
detail: `${missingCoordinateCount.toLocaleString()} 辆无有效坐标,地图可视范围决定客户能否看车。`,
color: locatedCount > 0 ? 'blue' as const : 'orange' as const,
action: '地图态势',
onClick: () => load(filters, pagination.currentPage, pagination.pageSize)
},
{
label: '优先异常',
value: `${mapAttentionRows.length.toLocaleString()} 关注`,
detail: mapAttentionRows[0] ? `${mapAttentionRows[0].plate || mapAttentionRows[0].vin}${realtimeIssueLabels(mapAttentionRows[0]).join('')}` : '当前没有明显离线、超时或坐标异常车辆。',
color: mapAttentionRows.length > 0 ? 'orange' as const : 'green' as const,
action: '告警事件',
onClick: () => onOpenQuality?.({ serviceStatus: 'degraded' })
},
{
label: '选车复盘',
value: selectedVehicleLabel,
detail: selectedMapRow ? `${vehicleServiceStatus(selectedMapRow).label}${dataFreshness(selectedMapRow).detail}` : '从地图或车辆列表选择一辆车后进入轨迹、里程和历史查询。',
color: selectedMapRow ? vehicleServiceStatus(selectedMapRow).color : 'grey' as const,
action: '轨迹回放',
disabled: !selectedMapRow || !canOpenVehicle(selectedMapRow.vin),
onClick: () => selectedMapRow && onOpenHistory?.(selectedMapRow.vin, selectedVehicleProtocol)
}
];
const mapCustomerPackageItems = [
{
label: '车辆范围',
value: `${rows.length.toLocaleString()} / ${pagination.total.toLocaleString()}`,
detail: `当前筛选覆盖 ${formatPercent(pageCoverageRate)}`,
color: pageCoverageRate >= 100 ? 'green' as const : 'blue' 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 ? 'green' as const : 'orange' as const
},
{
label: '关注车辆',
value: `${mapAttentionRows.length.toLocaleString()}`,
detail: `${staleCount.toLocaleString()} 辆更新超时`,
color: mapAttentionRows.length > 0 ? 'orange' as const : 'green' as const
},
{
label: '选中车辆',
value: selectedVehicleLabel,
detail: selectedMapRow ? `${vehicleServiceStatus(selectedMapRow).label} / ${dataFreshness(selectedMapRow).detail}` : '从地图或车辆列表选择',
color: selectedMapRow ? vehicleServiceStatus(selectedMapRow).color : 'grey' as const
}
];
const copyMapCustomerPackage = () => copyText(mapCustomerPackageText({
filters,
rows,
total: pagination.total,
onlineCount,
locatedCount,
attentionCount: mapAttentionRows.length,
staleCount,
selectedRow: selectedMapRow,
selectedProtocol: selectedVehicleProtocol,
amapConfigured
}), '客户地图监控包');
const copyMapCustomerDecision = () => copyText(mapCustomerDecisionText({
filters,
rows,
total: pagination.total,
onlineCount,
locatedCount,
attentionRows: mapAttentionRows,
selectedRow: selectedMapRow,
selectedProtocol: selectedVehicleProtocol,
amapConfigured
}), '地图客户决策说明');
const realtimeCustomerJourneyItems = [
{
step: '01',
title: '只看在线',
value: `${onlineCount.toLocaleString()} 在线`,
detail: '先确认客户当前能看到哪些车辆仍在上报。',
action: '在线车辆',
color: onlineCount > 0 ? 'green' as const : 'orange' as const,
onClick: () => applyFilters({ ...filters, online: 'online' }),
disabled: false
},
{
step: '02',
title: '地图态势',
value: `${locatedCount.toLocaleString()} 有定位`,
detail: '在地图上确认车辆位置、定位有效性和分布情况。',
action: '查看地图',
color: locatedCount > 0 ? 'blue' as const : 'orange' as const,
onClick: () => {
window.location.hash = buildAppHash({ page: 'map', protocol: filters.protocol, filters });
},
disabled: false
},
{
step: '03',
title: '异常优先',
value: `${Math.max(degradedCount, sourceIssueRows.length).toLocaleString()} 关注`,
detail: '优先处理离线、更新超时、坐标无效或服务降级车辆。',
action: '关注车辆',
color: degradedCount > 0 || sourceIssueRows.length > 0 ? 'orange' as const : 'green' as const,
onClick: () => applyFilters({ ...filters, serviceStatus: 'degraded' }),
disabled: false
},
{
step: '04',
title: '单车复盘',
value: selectedMapRow?.plate || selectedMapRow?.vin || '未选车',
detail: selectedMapRow ? '围绕选中车辆回放轨迹、核对里程和告警。' : '先在地图或表格中选择一辆可查询车辆。',
action: '轨迹回放',
color: selectedMapRow ? 'blue' as const : 'grey' as const,
onClick: () => selectedMapRow && onOpenHistory?.(selectedMapRow.vin, selectedVehicleProtocol),
disabled: !selectedMapRow || !canOpenVehicle(selectedMapRow.vin)
},
{
step: '05',
title: '查询导出',
value: `${rows.length.toLocaleString()} 当前页`,
detail: '导出当前实时车辆清单,用于客户问询和运营交接。',
action: '导出 CSV',
color: rows.length > 0 ? 'blue' as const : 'grey' as const,
onClick: exportRealtime,
disabled: rows.length === 0
}
];
if (mode === 'map') {
return (
<div className="vp-page vp-map-page">
<PageHeader title={title} description={description} />
<div className="vp-map-commandbar">
<Form key={JSON.stringify(filters)} initValues={filters} layout="horizontal" onSubmit={(values) => {
applyFilters(values as Record<string, string>);
}}>
<Form.Input field="keyword" label="车辆" placeholder="VIN / 车牌 / 手机号 / OEM" style={{ width: 260 }} />
<Form.Select field="protocol" label="数据通道" placeholder="全部通道" style={{ width: 160 }}>
<Select.Option value="GB32960">GB32960</Select.Option>
<Select.Option value="JT808">JT808</Select.Option>
<Select.Option value="YUTONG_MQTT">YUTONG_MQTT</Select.Option>
</Form.Select>
<Form.Select field="online" label="状态" placeholder="全部车辆" style={{ width: 130 }}>
<Select.Option value="online">线</Select.Option>
<Select.Option value="offline">线</Select.Option>
</Form.Select>
<Space>
<Button htmlType="submit" theme="solid" type="primary"></Button>
<Button onClick={() => applyFilters({})}></Button>
</Space>
</Form>
<Space wrap className="vp-map-refresh-actions">
<Tag color={autoRefresh ? 'green' : 'grey'}>{autoRefresh ? `自动刷新 ${refreshIntervalSeconds}` : '自动刷新已暂停'}</Tag>
<Typography.Text type="secondary">{lastRefreshAt || '等待首次刷新'}</Typography.Text>
<Button size="small" theme="light" loading={loading} onClick={() => load(filters, pagination.currentPage, pagination.pageSize)}></Button>
<Button size="small" theme="light" onClick={() => setAutoRefresh((value) => !value)}>{autoRefresh ? '暂停刷新' : '开启刷新'}</Button>
</Space>
</div>
<div className="vp-map-decision-board">
<div className="vp-map-decision-summary">
<Space wrap>
<Tag color="blue"></Tag>
<Tag color={amapConfigured ? 'green' : 'orange'}>{amapConfigured ? '高德地图可用' : '坐标预览'}</Tag>
<Tag color={mapAttentionRows.length > 0 ? 'orange' : 'green'}>{mapAttentionRows.length.toLocaleString()} </Tag>
</Space>
<Typography.Text strong>线</Typography.Text>
<Typography.Text type="secondary">
线
</Typography.Text>
<Space wrap>
<Button size="small" theme="solid" type="primary" onClick={() => applyFilters({ ...filters, online: 'online' })}>线</Button>
<Button size="small" theme="light" type="primary" icon={<IconCopy />} onClick={copyMapCustomerDecision}></Button>
</Space>
</div>
<div className="vp-map-decision-grid">
{mapCustomerDecisionItems.map((item) => (
<button
key={item.label}
type="button"
className="vp-map-decision-item"
disabled={item.disabled}
onClick={item.onClick}
aria-label={`地图客户决策 ${item.label} ${item.action}`}
>
<Tag color={item.color}>{item.label}</Tag>
<strong>{item.value}</strong>
<span>{item.detail}</span>
<em>{item.action}</em>
</button>
))}
</div>
</div>
<div className="vp-map-kpi-strip">
{mapFleetKpis.map((item) => (
<button
key={item.label}
type="button"
className="vp-map-kpi-item"
onClick={() => {
if (item.label === '在线车辆') applyFilters({ ...filters, online: 'online' });
if (item.label === '需关注') applyFilters({ ...filters, serviceStatus: 'degraded' });
}}
>
<Tag color={item.color}>{item.label}</Tag>
<strong>{item.value}</strong>
<span>{item.helper}</span>
</button>
))}
</div>
<div className="vp-map-task-strip">
{mapCustomerTasks.map((item) => (
<div key={item.title} className="vp-map-task-item">
<div className="vp-map-task-head">
<Tag color={item.color}>{item.title}</Tag>
<strong>{item.value}</strong>
</div>
<Typography.Text type="secondary">{item.detail}</Typography.Text>
<Space wrap>
<Button
size="small"
theme="solid"
type="primary"
disabled={item.disabled}
aria-label={`地图监控任务 ${item.title} ${item.primaryAction}`}
onClick={item.onPrimary}
>
{item.primaryAction}
</Button>
<Button
size="small"
theme="light"
type="primary"
disabled={item.disabled || item.secondaryDisabled}
aria-label={`地图监控任务 ${item.title} ${item.secondaryAction}`}
onClick={item.onSecondary}
>
{item.secondaryAction}
</Button>
</Space>
</div>
))}
</div>
<div className="vp-map-customer-package">
<div className="vp-map-customer-package-summary">
<Typography.Text strong></Typography.Text>
<Space wrap>
<Tag color={amapConfigured ? 'green' : 'orange'}>{amapConfigured ? '高德地图可用' : '坐标预览'}</Tag>
<Tag color={mapAttentionRows.length > 0 ? 'orange' : 'green'}>
{mapAttentionRows.length > 0 ? '存在关注车辆' : '地图态势稳定'}
</Tag>
</Space>
<Typography.Text strong>{selectedVehicleLabel}</Typography.Text>
<Typography.Text type="secondary">
线
</Typography.Text>
<Space wrap>
<Button size="small" theme="solid" type="primary" icon={<IconCopy />} onClick={copyMapCustomerPackage}></Button>
<Button size="small" disabled={!selectedMapRow || !canOpenVehicle(selectedMapRow.vin)} onClick={() => selectedMapRow && onOpenHistory?.(selectedMapRow.vin, selectedVehicleProtocol)}></Button>
<Button size="small" disabled={!selectedMapRow || !canOpenVehicle(selectedMapRow.vin)} onClick={() => selectedMapRow && onOpenVehicle(selectedMapRow.vin, selectedVehicleProtocol)}></Button>
<Button size="small" disabled={!selectedMapRow || !canOpenVehicle(selectedMapRow.vin)} onClick={openSelectedVehicleRaw}></Button>
</Space>
</div>
<div className="vp-map-customer-package-grid">
{mapCustomerPackageItems.map((item) => (
<div key={item.label} className="vp-map-customer-package-item">
<Tag color={item.color}>{item.label}</Tag>
<strong>{item.value}</strong>
<span>{item.detail}</span>
</div>
))}
</div>
</div>
<div className="vp-map-vehicle-workbench">
<div className="vp-map-vehicle-workbench-head">
<div>
<Typography.Text strong></Typography.Text>
<Typography.Text type="tertiary" size="small">
{selectedVehicleLabel}
</Typography.Text>
</div>
<Space wrap>
{selectedMapRow ? (
<>
<Tag color={vehicleServiceStatus(selectedMapRow).color}>{vehicleServiceStatus(selectedMapRow).label}</Tag>
<Tag color={dataFreshness(selectedMapRow).color}>{dataFreshness(selectedMapRow).detail}</Tag>
<Tag color="blue">{selectedVehicleProtocol || '未知来源'}</Tag>
</>
) : (
<Tag color="grey"></Tag>
)}
</Space>
</div>
<div className="vp-map-vehicle-work-grid">
{mapVehicleWorkItems.map((item) => (
<button
key={item.title}
type="button"
className="vp-map-vehicle-work-item"
disabled={item.disabled}
aria-label={`地图车辆作业 ${item.title} ${item.action}`}
onClick={item.onClick}
>
<span className="vp-map-vehicle-work-title">
<Tag color={item.color}>{item.title}</Tag>
<strong>{item.value}</strong>
</span>
<span>{item.detail}</span>
<em>{item.action}</em>
</button>
))}
</div>
</div>
<div className="vp-map-workspace">
<section className="vp-map-canvas-panel">
<div className="vp-map-canvas-toolbar">
<Space wrap>
<Tag color={amapConfigured ? 'green' : 'orange'}>{amapConfigured ? '高德地图可用' : '坐标预览'}</Tag>
<Tag color="blue">{locatedCount.toLocaleString()} </Tag>
{filterSummary.map((item) => <Tag key={item} color="grey">{item}</Tag>)}
</Space>
<Space wrap>
<Button size="small" theme="light" onClick={exportRealtime}></Button>
<Button size="small" theme="light" disabled={!onOpenQuality} onClick={() => onOpenQuality?.({ serviceStatus: 'degraded' })}></Button>
</Space>
</div>
<VehicleMap
points={mapPoints}
maxFallbackPoints={160}
selectedId={selectedMapPointKey}
onPointSelect={selectRealtimeMapPoint}
heightClassName="vp-map-customer-canvas"
fallbackLabel="地图加载中,先显示车辆坐标态势"
/>
</section>
<aside className="vp-map-customer-side">
{selectedMapRow ? (
<div className="vp-map-customer-card vp-map-selected-card">
<div className="vp-map-card-title"></div>
<Space wrap>
<Tag color={vehicleServiceStatus(selectedMapRow).color}>{vehicleServiceStatus(selectedMapRow).label}</Tag>
<Tag color={dataFreshness(selectedMapRow).color}>{dataFreshness(selectedMapRow).detail}</Tag>
<Tag color="blue">{selectedMapRow.primaryProtocol || '未知来源'}</Tag>
</Space>
<Typography.Title heading={5} style={{ margin: '8px 0 0' }}>
{selectedMapRow.plate || selectedMapRow.vin}
</Typography.Title>
<Typography.Text type="tertiary">{selectedMapRow.vin}</Typography.Text>
<div className="vp-map-selected-metrics">
<span></span><strong>{selectedMapRow.speedKmh ?? '-'} km/h</strong>
<span>SOC</span><strong>{selectedMapRow.socPercent ?? '-'}%</strong>
<span></span><strong>{selectedMapRow.totalMileageKm ?? '-'} km</strong>
<span></span><strong>{selectedMapRow.lastSeen || '-'}</strong>
</div>
<Space wrap>
<Button size="small" theme="solid" type="primary" onClick={() => onOpenVehicle(selectedMapRow.vin, filters.protocol || selectedMapRow.primaryProtocol)}></Button>
<Button size="small" onClick={() => onOpenHistory?.(selectedMapRow.vin, filters.protocol || selectedMapRow.primaryProtocol)}></Button>
<Button size="small" disabled={!isValidCoordinate(selectedMapRow)} onClick={() => window.open(amapMarkerURL(selectedMapRow), '_blank', 'noopener,noreferrer')}></Button>
</Space>
</div>
) : null}
<div className="vp-map-customer-card">
<div className="vp-map-card-title"></div>
{mapAttentionRows.length === 0 ? (
<Typography.Text type="tertiary"></Typography.Text>
) : mapAttentionRows.map((row) => {
const status = vehicleServiceStatus(row);
return (
<button key={`${row.vin}-${row.primaryProtocol}-map-attention`} type="button" className="vp-map-attention-row" onClick={() => selectRealtimeRow(row)}>
<div>
<Typography.Text strong>{row.plate || row.vin}</Typography.Text>
<Typography.Text type="tertiary" size="small">{row.vin}</Typography.Text>
</div>
<Tag color={status.color}>{status.label}</Tag>
<Typography.Text type="secondary" size="small">{realtimeIssueLabels(row).join('')}</Typography.Text>
</button>
);
})}
</div>
<div className="vp-map-customer-card">
<div className="vp-map-card-title"></div>
{mapServiceRows.length === 0 ? (
<Typography.Text type="tertiary"></Typography.Text>
) : mapServiceRows.map((row) => (
<button
key={`${row.vin}-${row.primaryProtocol}-map-list`}
type="button"
className={`vp-map-vehicle-row ${selectedMapRow?.vin === row.vin ? 'vp-map-vehicle-row-selected' : ''}`}
onClick={() => selectRealtimeRow(row)}
>
<span>{row.plate || row.vin}</span>
<Tag color={dataFreshness(row).color}>{dataFreshness(row).label}</Tag>
</button>
))}
</div>
</aside>
</div>
</div>
);
}
return (
<div className="vp-page">
<PageHeader title={title} description={description} />
<Card bordered className="vp-realtime-customer-board" bodyStyle={{ padding: 0 }}>
<div className="vp-realtime-customer-summary">
<Space wrap>
<Tag color="blue"></Tag>
<Tag color={amapConfigured ? 'green' : 'orange'}>{amapConfigured ? '高德地图可用' : '坐标预览'}</Tag>
<Tag color={degradedCount > 0 || staleCount > 0 ? 'orange' : 'green'}>{degradedCount.toLocaleString()} / {staleCount.toLocaleString()} </Tag>
</Space>
<Typography.Title heading={5} style={{ margin: 0 }}></Typography.Title>
<Typography.Text type="secondary">
线
</Typography.Text>
<Space wrap>
<Button size="small" theme="solid" type="primary" onClick={() => applyFilters({ ...filters, online: 'online' })}>线</Button>
<Button size="small" theme="light" type="primary" onClick={() => { window.location.hash = buildAppHash({ page: 'map', protocol: filters.protocol, filters }); }}></Button>
<Button size="small" theme="light" type="warning" onClick={() => applyFilters({ ...filters, serviceStatus: 'degraded' })}></Button>
<Button size="small" theme="light" onClick={copyRealtimeSummary}></Button>
</Space>
</div>
<div className="vp-realtime-customer-steps">
{realtimeCustomerJourneyItems.map((item) => (
<button
key={item.step}
type="button"
className="vp-realtime-customer-step"
disabled={item.disabled}
onClick={item.onClick}
aria-label={`实时监控路径 ${item.title} ${item.action}`}
>
<span>{item.step}</span>
<Tag color={item.color}>{item.title}</Tag>
<strong>{item.value}</strong>
<small>{item.detail}</small>
<em>{item.action}</em>
</button>
))}
</div>
</Card>
<Card bordered>
<Form key={JSON.stringify(filters)} initValues={filters} layout="horizontal" onSubmit={(values) => {
const nextFilters = values as Record<string, string>;
applyFilters(nextFilters);
}} style={{ marginBottom: 12 }}>
<Form.Input field="keyword" label="车辆关键词" placeholder="VIN / 车牌 / 手机号 / OEM" style={{ width: 260 }} />
<Form.Select field="protocol" label="数据通道" placeholder="全部数据通道" style={{ width: 180 }}>
<Select.Option value="GB32960">GB32960</Select.Option>
<Select.Option value="JT808">JT808</Select.Option>
<Select.Option value="YUTONG_MQTT">YUTONG_MQTT</Select.Option>
</Form.Select>
<Form.Select field="online" label="在线" placeholder="全部" style={{ width: 130 }}>
<Select.Option value="online">线</Select.Option>
<Select.Option value="offline">线</Select.Option>
</Form.Select>
<Form.Select field="serviceStatus" label="服务状态" placeholder="全部" style={{ width: 150 }}>
<Select.Option value="healthy"></Select.Option>
<Select.Option value="degraded"></Select.Option>
<Select.Option value="offline">线</Select.Option>
<Select.Option value="identity_required"></Select.Option>
</Form.Select>
<Space>
<Button htmlType="submit" theme="solid" type="primary"></Button>
<Button onClick={() => {
applyFilters({});
}}></Button>
</Space>
</Form>
<div className="vp-table-toolbar">
<Space wrap>
<Tag color={autoRefresh ? 'green' : 'grey'}> {refreshIntervalSeconds}</Tag>
<Typography.Text type="secondary">{lastRefreshAt || '等待首次刷新'}</Typography.Text>
<Button size="small" theme="light" aria-label="刷新实时数据" loading={loading} onClick={() => load(filters, pagination.currentPage, pagination.pageSize)}>
</Button>
<Button size="small" theme="light" aria-label={autoRefresh ? '暂停自动刷新' : '开启自动刷新'} onClick={() => setAutoRefresh((value) => !value)}>
{autoRefresh ? '暂停自动刷新' : '开启自动刷新'}
</Button>
</Space>
</div>
{filterSummary.length > 0 ? (
<Card bordered title="当前实时筛选" style={{ marginBottom: 12 }}>
<Space wrap>
{filterSummary.map((item) => (
<Tag key={item} color="blue">{item}</Tag>
))}
<Button size="small" onClick={() => applyFilters({})}></Button>
</Space>
</Card>
) : null}
<div className="vp-realtime-command-board">
<div>
<div className="vp-current-service-title"></div>
<div className="vp-realtime-command-grid">
{[
{ label: '在线车辆', value: onlineCount, color: 'green' as const, action: () => applyFilters({ ...filters, online: 'online' }) },
{ label: '离线车辆', value: rows.length - onlineCount, color: rows.length - onlineCount > 0 ? 'orange' as const : 'green' as const, action: () => applyFilters({ ...filters, online: 'offline' }) },
{ label: '定位有效', value: locatedCount, color: locatedCount > 0 ? 'blue' as const : 'orange' as const },
{ label: '需要关注', value: degradedCount, color: degradedCount > 0 ? 'orange' as const : 'green' as const, action: () => applyFilters({ ...filters, serviceStatus: 'degraded' }) },
{ label: '更新超时', value: staleCount, color: staleCount > 0 ? 'red' as const : 'green' as const },
{ label: '数据通道', value: primaryProtocols.size, color: 'blue' as const }
].map((item) => (
item.action ? (
<button
key={item.label}
className="vp-realtime-command-item"
type="button"
aria-label={`实时筛选 ${item.label} ${item.value.toLocaleString()}`}
onClick={item.action}
>
<Tag color={item.color}>{item.label}</Tag>
<span>{item.value.toLocaleString()}</span>
</button>
) : (
<div key={item.label} className="vp-realtime-command-item">
<Tag color={item.color}>{item.label}</Tag>
<span>{item.value.toLocaleString()}</span>
</div>
)
))}
</div>
</div>
<div>
<div className="vp-current-service-title"></div>
<div className="vp-current-action-list">
{degradedCount > 0 ? (
<Button size="small" theme="light" type="warning" onClick={() => applyFilters({ ...filters, serviceStatus: 'degraded' })}>
{degradedCount.toLocaleString()}
</Button>
) : <Tag color="green"></Tag>}
{staleCount > 0 ? <Tag color="red"> {staleCount.toLocaleString()}</Tag> : <Tag color="green"></Tag>}
{sourceIssueRows.length > 0 && onOpenQuality ? (
<Button size="small" theme="light" type="warning" onClick={() => onOpenQuality({ serviceStatus: 'degraded' })}>
{sourceIssueRows.length.toLocaleString()}
</Button>
) : null}
</div>
</div>
</div>
<div className="vp-source-coverage-board">
<div className="vp-current-service-title"></div>
<div className="vp-source-coverage-grid">
{sourceCoverageRows.length === 0 ? (
<Typography.Text type="tertiary"></Typography.Text>
) : sourceCoverageRows.map((item) => (
<button
key={item.protocol}
type="button"
className="vp-source-coverage-item"
aria-label={`筛选数据通道 ${item.protocol}`}
onClick={() => applyFilters({ ...filters, protocol: item.protocol })}
>
<div className="vp-source-coverage-item-head">
<Tag color={item.degraded > 0 ? 'orange' : 'green'}>{item.protocol}</Tag>
<span>{item.total.toLocaleString()} </span>
</div>
<div className="vp-source-coverage-bars">
<span>线 {item.online.toLocaleString()}</span>
<span> {item.located.toLocaleString()}</span>
<span> {item.degraded.toLocaleString()}</span>
<span> {item.stale.toLocaleString()}</span>
</div>
</button>
))}
</div>
</div>
<div className="vp-monitor-layout">
<div className="vp-monitor-map">
<div className="vp-monitor-map-header">
<Space wrap>
<Tag color={amapConfigured ? 'green' : 'orange'}>
{amapConfigured ? '高德地图配置就绪' : '高德地图待配置'}
</Tag>
<Tag color="blue">{locatedCount.toLocaleString()} </Tag>
<Tag color="green">{onlineCount.toLocaleString()} 线</Tag>
<Button size="small" theme="light" icon={<IconCopy />} aria-label="复制实时摘要" onClick={copyRealtimeSummary}></Button>
<Button size="small" theme="light" icon={<IconCopy />} aria-label="复制实时异常处置清单" onClick={copyRealtimeIssueChecklist}></Button>
<Button size="small" theme="light" icon={<IconCopy />} aria-label="复制实时运营交接包" onClick={copyRealtimeDutyHandoff}></Button>
</Space>
</div>
<VehicleMap
points={mapPoints}
maxFallbackPoints={80}
selectedId={selectedMapPointKey}
onPointSelect={selectRealtimeMapPoint}
fallbackLabel="高德地图未配置,显示实时坐标预览"
/>
</div>
<div className="vp-monitor-side">
{[
{ label: '当前车辆', value: pagination.total.toLocaleString(), color: 'blue' as const },
{ label: '在线车辆', value: onlineCount.toLocaleString(), color: 'green' as const },
{ label: '定位有效', value: locatedCount.toLocaleString(), color: 'green' as const },
{ label: '超时车辆', value: staleCount.toLocaleString(), color: staleCount > 0 ? 'orange' as const : 'green' as const },
{ label: '需要关注', value: degradedCount.toLocaleString(), color: degradedCount > 0 ? 'orange' as const : 'green' as const },
{ label: '数据通道', value: primaryProtocols.size.toLocaleString(), color: 'blue' as const }
].map((item) => (
<div key={item.label} className="vp-monitor-metric">
<Tag color={item.color}>{item.label}</Tag>
<div className="vp-monitor-metric-value">{item.value}</div>
</div>
))}
<Typography.Text type="secondary">
线
</Typography.Text>
{selectedMapRow ? (
<div className="vp-selected-vehicle-panel">
<div className="vp-map-service-queue-title"></div>
<Space wrap>
<Tag color={vehicleServiceStatus(selectedMapRow).color}>{vehicleServiceStatus(selectedMapRow).label}</Tag>
<Tag color={dataFreshness(selectedMapRow).color}>{dataFreshness(selectedMapRow).label}</Tag>
<Tag color={selectedMapRow.online ? 'green' : 'orange'}>{selectedMapRow.online ? '在线' : '离线'}</Tag>
<Tag color="blue">{selectedMapRow.primaryProtocol || '未知通道'}</Tag>
</Space>
<Typography.Title heading={6} style={{ margin: '8px 0 0' }}>
{selectedMapRow.plate || selectedMapRow.vin}
</Typography.Title>
<Typography.Text type="tertiary">{selectedMapRow.vin}</Typography.Text>
<div className="vp-selected-vehicle-grid">
<span></span><strong>{selectedMapRow.speedKmh ?? '-'} km/h</strong>
<span>SOC</span><strong>{selectedMapRow.socPercent ?? '-'}%</strong>
<span></span><strong>{selectedMapRow.totalMileageKm ?? '-'} km</strong>
<span></span><strong>{dataFreshness(selectedMapRow).detail}</strong>
<span></span><strong>{selectedMapRow.lastSeen || '-'}</strong>
</div>
<Space spacing={6} wrap>
<Button size="small" onClick={() => onOpenVehicle(selectedMapRow.vin, filters.protocol || selectedMapRow.primaryProtocol)}></Button>
<Button size="small" onClick={() => onOpenHistory?.(selectedMapRow.vin, filters.protocol || selectedMapRow.primaryProtocol)}></Button>
<Button size="small" disabled={!isValidCoordinate(selectedMapRow)} onClick={() => window.open(amapMarkerURL(selectedMapRow), '_blank', 'noopener,noreferrer')}></Button>
</Space>
</div>
) : null}
<div className="vp-map-integration-panel">
<div className="vp-map-service-queue-title"></div>
{mapIntegrationRows.map((item) => (
<div key={item.label} className="vp-map-integration-row">
<div>
<Typography.Text strong>{item.label}</Typography.Text>
<Typography.Text type="tertiary" size="small">{item.detail}</Typography.Text>
</div>
<Tag color={item.color}>{item.value}</Tag>
</div>
))}
</div>
<div className="vp-source-consistency-panel">
<div className="vp-map-service-queue-title"></div>
{sourceIssueRows.length === 0 ? (
<Typography.Text type="tertiary"></Typography.Text>
) : (
sourceIssueRows.map((row) => {
const status = vehicleServiceStatus(row);
return (
<div key={`${row.vin}-${row.primaryProtocol}-consistency`} className="vp-map-service-item">
<div className="vp-map-service-item-main">
<div>
<Typography.Text strong>{row.plate || row.vin}</Typography.Text>
<Typography.Text type="tertiary" size="small">{row.vin}</Typography.Text>
</div>
<Tag color={status.color}>{status.label}</Tag>
</div>
<Space spacing={4} wrap>
<Tag color="blue">{sourceEvidenceText(row)}</Tag>
<Tag color={dataFreshness(row).color}>{dataFreshness(row).label}</Tag>
{sourceIssueTags(row).map((item) => (
<Tag key={item} color="orange">{item}</Tag>
))}
</Space>
<Typography.Text type="secondary" size="small">
{row.serviceStatus?.detail || sourceEvidenceText(row)}
</Typography.Text>
<div className="vp-map-service-item-footer">
<Typography.Text type="tertiary" size="small">{row.lastSeen || '-'}</Typography.Text>
<Space spacing={4} wrap>
<Button size="small" disabled={!onOpenQuality} onClick={() => openQualityEvidence(row)}></Button>
<Button size="small" onClick={() => onOpenVehicle(row.vin, filters.protocol || row.primaryProtocol)}></Button>
</Space>
</div>
</div>
);
})
)}
</div>
<div className="vp-map-service-queue">
<div className="vp-map-service-queue-title"></div>
{mapServiceRows.length === 0 ? (
<Typography.Text type="tertiary"></Typography.Text>
) : (
mapServiceRows.map((row) => {
const status = vehicleServiceStatus(row);
return (
<div
key={`${row.vin}-${row.primaryProtocol}`}
role="button"
tabIndex={0}
className={`vp-map-service-item vp-map-service-item-button ${selectedMapRow?.vin === row.vin ? 'vp-map-service-item-selected' : ''}`}
onClick={() => selectRealtimeRow(row)}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
selectRealtimeRow(row);
}
}}
>
<div className="vp-map-service-item-main">
<div>
<Typography.Text strong>{row.plate || row.vin}</Typography.Text>
<Typography.Text type="tertiary" size="small">{row.vin}</Typography.Text>
</div>
<Space spacing={4} wrap>
<Tag color={dataFreshness(row).color}>{dataFreshness(row).label}</Tag>
<Tag color={status.color}>{status.label}</Tag>
</Space>
</div>
<Typography.Text type="secondary" size="small">
{row.serviceStatus?.detail || sourceEvidenceText(row)}
</Typography.Text>
<Typography.Text type="tertiary" size="small">
{dataFreshness(row).detail}
</Typography.Text>
<div className="vp-map-service-item-footer">
<Typography.Text type="tertiary" size="small">{row.lastSeen || '-'}</Typography.Text>
<Space spacing={4} wrap>
<Button size="small" onClick={() => window.open(amapMarkerURL(row), '_blank', 'noopener,noreferrer')}></Button>
<Button size="small" onClick={() => onOpenHistory?.(row.vin, filters.protocol || row.primaryProtocol)}></Button>
<Button size="small" disabled={!onOpenQuality} onClick={() => openQualityEvidence(row)}></Button>
<Button size="small" onClick={() => onOpenVehicle(row.vin, filters.protocol || row.primaryProtocol)}></Button>
</Space>
</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 }}>
<div className="vp-stat-insight-grid">
{[
{ label: '在线率', value: formatPercent(onlineRate), color: onlineRate >= 80 ? 'green' as const : 'orange' as const, detail: `${onlineCount.toLocaleString()} / ${rows.length.toLocaleString()} 辆在线。` },
{ label: '定位有效率', value: formatPercent(locatedRate), color: locatedRate >= 80 ? 'green' as const : 'orange' as const, detail: `${locatedCount.toLocaleString()} / ${rows.length.toLocaleString()} 辆坐标有效。` },
{ label: '新鲜数据', value: `${(rows.length - staleCount).toLocaleString()}`, color: staleCount > 0 ? 'orange' as const : 'green' as const, detail: `${staleCount.toLocaleString()} 辆超过 5 分钟未更新。` },
{ label: '降级率', value: formatPercent(degradedRate), color: degradedCount > 0 ? 'orange' as const : 'green' as const, detail: `${degradedCount.toLocaleString()} 辆存在来源不完整或离线。` },
{ label: '分页覆盖率', value: formatPercent(pageCoverageRate), color: pageCoverageRate >= 100 ? 'green' as const : 'grey' as const, detail: '当前页车辆数 / 当前筛选总车辆数。' }
].map((item) => (
<Card key={item.label} bordered>
<Tag color={item.color}>{item.label}</Tag>
<div className="vp-monitor-metric-value">{item.value}</div>
<Typography.Text type="secondary">{item.detail}</Typography.Text>
</Card>
))}
</div>
</Card>
<Tabs type="line">
<Tabs.TabPane tab="表格视图" itemKey="table">
<div className="vp-table-toolbar">
<Space wrap>
<Tag color="blue"> {rows.length.toLocaleString()} </Tag>
<Button size="small" onClick={exportRealtime}> CSV</Button>
</Space>
</div>
{rows.length === 0 && !loading ? (
<DataEmpty />
) : (
<Table
loading={loading}
rowKey="vin"
dataSource={rows}
pagination={{
currentPage: pagination.currentPage,
pageSize: pagination.pageSize,
total: pagination.total,
showSizeChanger: true,
onPageChange: (page) => load(filters, page, pagination.pageSize),
onPageSizeChange: (pageSize) => load(filters, 1, pageSize)
}}
columns={[
{ title: '车牌', dataIndex: 'plate', width: 120 },
{ title: 'VIN', dataIndex: 'vin', width: 190 },
{
title: '车辆服务状态',
width: 130,
render: (_: unknown, row: VehicleRealtimeRow) => {
const status = vehicleServiceStatus(row);
return <Tag color={status.color}>{status.label}</Tag>;
}
},
{
title: '数据新鲜度',
width: 130,
render: (_: unknown, row: VehicleRealtimeRow) => {
const freshness = dataFreshness(row);
return <Tag color={freshness.color}>{freshness.label}</Tag>;
}
},
{
title: '车辆核心数据',
width: 230,
render: (_: unknown, row: VehicleRealtimeRow) => (
<Space spacing={4} wrap>
<Tag color="blue">{row.speedKmh ?? '-'} km/h</Tag>
<Tag color="green">SOC {row.socPercent ?? '-'}%</Tag>
<Typography.Text type="tertiary">{row.totalMileageKm ?? '-'} km</Typography.Text>
</Space>
)
},
{
title: '实时通道',
width: 260,
render: (_: unknown, row: VehicleRealtimeRow) => (
<SourceStatusTags sourceStatus={row.sourceStatus} protocols={row.protocols} lastSeen={row.lastSeen} />
)
},
{ title: '数据覆盖', width: 120, render: (_: unknown, row: VehicleRealtimeRow) => sourceEvidenceText(row) },
{ title: '在线', width: 90, render: (_: unknown, row: VehicleRealtimeRow) => <StatusTag status={row.online ? 'ok' : 'offline'} /> },
{ title: '最后时间', dataIndex: 'lastSeen', width: 170 },
{
title: '操作',
width: 190,
render: (_: unknown, row: VehicleRealtimeRow) => (
<Space spacing={4} wrap>
<Button disabled={!isValidCoordinate(row)} onClick={() => selectRealtimeRow(row)}></Button>
<Button disabled={!canOpenVehicle(row.vin)} onClick={() => onOpenVehicle(row.vin, filters.protocol || row.primaryProtocol)}></Button>
<Button disabled={!canOpenVehicle(row.vin) || !onOpenQuality} onClick={() => openQualityEvidence(row)}></Button>
</Space>
)
}
]}
/>
)}
</Tabs.TabPane>
<Tabs.TabPane tab="地图视图" itemKey="map">
<VehicleMap
points={mapPoints}
heightClassName=""
selectedId={selectedMapPointKey}
onPointSelect={selectRealtimeMapPoint}
fallbackLabel="高德地图未配置,显示实时坐标预览"
/>
</Tabs.TabPane>
</Tabs>
</Card>
</div>
);
}