Files
lingniu-vehicle-ingest/vehicle-data-platform/apps/web/src/pages/Realtime.tsx
2026-07-05 18:19:15 +08:00

2510 lines
122 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) {
const title = row.serviceStatus.status === 'degraded' || row.serviceStatus.title === '来源不完整'
? '数据通道不完整'
: row.serviceStatus.title;
return {
label: 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 padDatePart(value: number) {
return String(value).padStart(2, '0');
}
function formatDateTime(value: Date) {
return [
value.getFullYear(),
padDatePart(value.getMonth() + 1),
padDatePart(value.getDate())
].join('-') + ` ${padDatePart(value.getHours())}:${padDatePart(value.getMinutes())}:${padDatePart(value.getSeconds())}`;
}
function defaultTimeWindow() {
const end = new Date();
const start = new Date(end.getTime() - 24 * 60 * 60 * 1000);
return { dateFrom: formatDateTime(start), dateTo: formatDateTime(end) };
}
function timeWindowDurationText(dateFrom: string, dateTo: string) {
const start = new Date(dateFrom.replace(' ', 'T')).getTime();
const end = new Date(dateTo.replace(' ', 'T')).getTime();
if (!Number.isFinite(start) || !Number.isFinite(end) || end <= start) {
return '时间窗待确认';
}
const minutes = Math.round((end - start) / 60000);
if (minutes < 60) return `${minutes} 分钟窗口`;
if (minutes < 1440) return `${Math.round(minutes / 60)} 小时窗口`;
return `${Math.round(minutes / 1440)} 天窗口`;
}
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 [timeWindow, setTimeWindow] = useState(defaultTimeWindow);
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 timeWindowRow = selectedMapRow && canOpenVehicle(selectedMapRow.vin)
? selectedMapRow
: rows.find((row) => canOpenVehicle(row.vin));
const timeWindowKeyword = timeWindowRow?.vin || filters.keyword || '';
const timeWindowProtocol = timeWindowRow ? filters.protocol || timeWindowRow.primaryProtocol || '' : filters.protocol || '';
const timeWindowReady = Boolean(timeWindowKeyword && timeWindow.dateFrom && timeWindow.dateTo);
const timeWindowDuration = timeWindowDurationText(timeWindow.dateFrom, timeWindow.dateTo);
const setTimeWindowPreset = (preset: '15m' | '1h' | 'today' | '24h') => {
const end = new Date();
const start = new Date(end);
if (preset === '15m') start.setMinutes(end.getMinutes() - 15);
if (preset === '1h') start.setHours(end.getHours() - 1);
if (preset === '24h') start.setDate(end.getDate() - 1);
if (preset === 'today') {
start.setHours(0, 0, 0, 0);
}
setTimeWindow({ dateFrom: formatDateTime(start), dateTo: formatDateTime(end) });
};
const timeWindowFilters = (extra: Record<string, string> = {}) => ({
keyword: timeWindowKeyword,
protocol: timeWindowProtocol,
dateFrom: timeWindow.dateFrom,
dateTo: timeWindow.dateTo,
...extra
});
const openTimeWindowHistory = () => {
if (!timeWindowReady) {
Toast.warning('请先选择车辆和时间窗');
return;
}
window.location.hash = buildAppHash({ page: 'history', keyword: timeWindowKeyword, protocol: timeWindowProtocol, filters: timeWindowFilters() });
};
const openTimeWindowRaw = () => {
if (!timeWindowReady) {
Toast.warning('请先选择车辆和时间窗');
return;
}
window.location.hash = buildAppHash({
page: 'history-query',
keyword: timeWindowKeyword,
protocol: timeWindowProtocol,
filters: timeWindowFilters({ tab: 'raw', includeFields: 'true' })
});
};
const openTimeWindowMileage = () => {
if (!timeWindowReady) {
Toast.warning('请先选择车辆和时间窗');
return;
}
window.location.hash = buildAppHash({ page: 'mileage', keyword: timeWindowKeyword, protocol: timeWindowProtocol, filters: timeWindowFilters() });
};
const openTimeWindowQuality = () => {
if (!timeWindowReady || !onOpenQuality) {
Toast.warning('请先选择车辆和时间窗');
return;
}
onOpenQuality(timeWindowFilters());
};
const copyTimeWindowPackage = () => copyText([
'【客户时间窗复盘包】',
`车辆:${timeWindowRow ? [timeWindowRow.plate, timeWindowRow.vin].filter(Boolean).join(' / ') : timeWindowKeyword || '-'}`,
`数据通道:${timeWindowProtocol || '全部数据通道'}`,
`时间窗:${timeWindow.dateFrom}${timeWindow.dateTo}`,
`时间窗判定:${timeWindowDuration}`,
`实时状态:${timeWindowRow ? `${vehicleServiceStatus(timeWindowRow).label} / ${dataFreshness(timeWindowRow).detail}` : '未选车辆'}`,
`轨迹回放:${timeWindowReady ? window.location.origin + window.location.pathname + buildAppHash({ page: 'history', keyword: timeWindowKeyword, protocol: timeWindowProtocol, filters: timeWindowFilters() }) : '-'}`,
`历史数据:${timeWindowReady ? window.location.origin + window.location.pathname + buildAppHash({ page: 'history-query', keyword: timeWindowKeyword, protocol: timeWindowProtocol, filters: timeWindowFilters({ tab: 'raw', includeFields: 'true' }) }) : '-'}`,
`统计查询:${timeWindowReady ? window.location.origin + window.location.pathname + buildAppHash({ page: 'mileage', keyword: timeWindowKeyword, protocol: timeWindowProtocol, filters: timeWindowFilters() }) : '-'}`,
`告警通知:${timeWindowReady ? window.location.origin + window.location.pathname + buildAppHash({ page: 'alert-events', keyword: timeWindowKeyword, protocol: timeWindowProtocol, filters: timeWindowFilters() }) : '-'}`
].join('\n'), '客户时间窗复盘包');
const timeWindowWorkItems = [
{
title: '轨迹回放',
value: timeWindowDuration,
detail: '按时间窗回放位置、速度、里程断点。',
action: '打开轨迹',
color: 'blue' as const,
disabled: !timeWindowReady,
onClick: openTimeWindowHistory
},
{
title: '历史数据',
value: timeWindowProtocol || '全部通道',
detail: '查看该时间窗内的历史明细和字段。',
action: '查询历史',
color: 'blue' as const,
disabled: !timeWindowReady,
onClick: openTimeWindowRaw
},
{
title: '统计查询',
value: timeWindowRow?.totalMileageKm != null ? `${timeWindowRow.totalMileageKm} km` : '待核对',
detail: '核对区间里程、日里程和总里程差值。',
action: '查看统计',
color: 'orange' as const,
disabled: !timeWindowReady,
onClick: openTimeWindowMileage
},
{
title: '告警通知',
value: timeWindowRow ? realtimeIssueLabels(timeWindowRow)[0] : '待选择',
detail: '查看时间窗内断链、离线、定位异常事件。',
action: '查看告警',
color: timeWindowRow && hasSourceIssue(timeWindowRow) ? 'orange' as const : 'green' as const,
disabled: !timeWindowReady || !onOpenQuality,
onClick: openTimeWindowQuality
}
];
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 mapServiceActionItems = [
{
title: '在线车辆',
value: `${onlineCount.toLocaleString()} 在线`,
detail: `在线率 ${formatPercent(onlineRate)},先确认客户当前能看到哪些车。`,
action: '只看在线',
color: onlineCount > 0 ? 'green' as const : 'orange' as const,
disabled: false,
onClick: () => applyFilters({ ...filters, online: 'online' })
},
{
title: '关注车辆',
value: `${mapAttentionRows.length.toLocaleString()} 关注`,
detail: mapAttentionRows[0] ? `${mapAttentionRows[0].plate || mapAttentionRows[0].vin}${realtimeIssueLabels(mapAttentionRows[0]).join('')}` : '当前没有明显离线、超时或坐标异常车辆。',
action: '异常优先',
color: mapAttentionRows.length > 0 ? 'orange' as const : 'green' as const,
disabled: false,
onClick: () => applyFilters({ ...filters, serviceStatus: 'degraded' })
},
{
title: '路线回放',
value: selectedMapRow?.plate || selectedMapRow?.vin || '先选车',
detail: selectedMapRow ? `${dataFreshness(selectedMapRow).detail},回放位置、速度和里程变化。` : '从地图或车辆列表选择一辆车后进入轨迹回放。',
action: '轨迹回放',
color: selectedMapRow ? 'blue' as const : '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 : 'grey' as const,
disabled: !selectedMapRow || !canOpenVehicle(selectedMapRow.vin),
onClick: () => {
if (!selectedMapRow) return;
window.location.hash = buildAppHash({
page: 'mileage',
keyword: selectedMapRow.vin,
protocol: selectedVehicleProtocol
});
}
},
{
title: '证据导出',
value: `${rows.length.toLocaleString()} 当前页`,
detail: '导出当前地图车辆清单,作为客户问询和运营交接证据。',
action: '导出CSV',
color: rows.length > 0 ? 'blue' as const : 'grey' as const,
disabled: rows.length === 0,
onClick: exportRealtime
}
];
const mapFieldCommandItems = [
{
title: '全域定位',
value: `${locatedCount.toLocaleString()} 辆有坐标`,
detail: amapConfigured ? '用高德地图查看车辆分布、在线状态和最近上报位置。' : '高德不可用时先用坐标预览确认车辆分布。',
color: locatedCount > 0 ? 'blue' as const : 'orange' as const,
action: '刷新地图',
disabled: false,
onClick: () => load(filters, pagination.currentPage, pagination.pageSize)
},
{
title: '关注车辆',
value: mapAttentionRows.length > 0 ? `${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: '定位关注车',
disabled: mapAttentionRows.length === 0,
onClick: () => mapAttentionRows[0] && selectRealtimeRow(mapAttentionRows[0])
},
{
title: '轨迹复盘',
value: selectedVehicleLabel,
detail: selectedMapRow ? '打开选中车辆轨迹,核对路径、速度、里程和停留。' : '先选择车辆,再进入轨迹回放和时间窗复盘。',
color: selectedMapRow ? 'blue' as const : 'grey' as const,
action: '打开轨迹',
disabled: !selectedMapRow || !canOpenVehicle(selectedMapRow.vin),
onClick: () => selectedMapRow && onOpenHistory?.(selectedMapRow.vin, selectedVehicleProtocol)
},
{
title: '通知证据',
value: mapAttentionRows.length > 0 ? '需通知' : '可交付',
detail: mapAttentionRows.length > 0 ? '复制地图监控包后进入告警事件,形成通知和处置闭环。' : '地图态势稳定时,可直接复制地图监控包用于交接。',
color: mapAttentionRows.length > 0 ? 'orange' as const : 'green' as const,
action: mapAttentionRows.length > 0 ? '告警事件' : '复制监控包',
disabled: false,
onClick: () => mapAttentionRows.length > 0 && onOpenQuality ? onOpenQuality({ serviceStatus: 'degraded' }) : copyMapCustomerPackage()
}
];
const mapDispatchActionItems = [
{
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,
action: '告警事件',
disabled: mapAttentionRows.length === 0 || !onOpenQuality,
onClick: () => onOpenQuality?.({ serviceStatus: 'degraded' })
},
{
title: '复盘选中',
value: selectedVehicleLabel,
detail: selectedMapRow ? `${dataFreshness(selectedMapRow).detail},回放轨迹和里程变化。` : '先从地图或列表选择车辆。',
color: selectedMapRow ? dataFreshness(selectedMapRow).color : 'grey' as const,
action: '轨迹回放',
disabled: !selectedMapRow || !canOpenVehicle(selectedMapRow.vin),
onClick: () => selectedMapRow && onOpenHistory?.(selectedMapRow.vin, selectedVehicleProtocol)
},
{
title: '车辆档案',
value: selectedVehicleLabel,
detail: selectedMapRow ? `${vehicleServiceStatus(selectedMapRow).label},查看车辆画像和来源证据。` : '选择车辆后进入单车服务。',
color: selectedMapRow ? vehicleServiceStatus(selectedMapRow).color : 'grey' as const,
action: '车辆服务',
disabled: !selectedMapRow || !canOpenVehicle(selectedMapRow.vin),
onClick: () => selectedMapRow && onOpenVehicle(selectedMapRow.vin, selectedVehicleProtocol)
},
{
title: '交接证据',
value: `${rows.length.toLocaleString()}`,
detail: '导出当前地图车辆清单,交接给客户或运营人员继续处理。',
color: rows.length > 0 ? 'blue' as const : 'grey' as const,
action: '导出CSV',
disabled: rows.length === 0,
onClick: exportRealtime
}
];
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
}
];
const realtimeNextActions = [
{
level: degradedCount > 0 || staleCount > 0 ? '优先' : '巡检',
title: degradedCount > 0 || staleCount > 0 ? '先处理异常车辆' : '先看在线车辆',
value: degradedCount > 0 || staleCount > 0 ? `${degradedCount.toLocaleString()} 降级 / ${staleCount.toLocaleString()} 超时` : `${onlineCount.toLocaleString()} 在线`,
detail: degradedCount > 0 || staleCount > 0
? '离线、更新超时、坐标无效或服务降级会直接影响客户看车体验。'
: `当前页 ${locatedCount.toLocaleString()} 辆有有效坐标,可进入地图查看分布。`,
action: degradedCount > 0 || staleCount > 0 ? '关注异常' : '打开地图',
color: degradedCount > 0 || staleCount > 0 ? 'orange' as const : 'green' as const,
onClick: () => degradedCount > 0 || staleCount > 0 ? applyFilters({ ...filters, serviceStatus: 'degraded' }) : (window.location.hash = buildAppHash({ page: 'map', protocol: filters.protocol, filters }))
},
{
level: selectedMapRow ? '单车' : '选车',
title: selectedMapRow ? '复盘选中车辆' : '先选择一辆车',
value: selectedMapRow?.plate || selectedMapRow?.vin || '等待选车',
detail: selectedMapRow ? `${vehicleServiceStatus(selectedMapRow).label}${dataFreshness(selectedMapRow).detail}` : '从地图或实时列表选择车辆后,进入轨迹、统计查询、历史和告警。',
action: selectedMapRow ? '轨迹回放' : '查看车辆',
color: selectedMapRow ? vehicleServiceStatus(selectedMapRow).color : 'blue' as const,
disabled: selectedMapRow ? !canOpenVehicle(selectedMapRow.vin) : false,
onClick: () => selectedMapRow && canOpenVehicle(selectedMapRow.vin) ? onOpenHistory?.(selectedMapRow.vin, selectedVehicleProtocol) : applyFilters({ ...filters, online: 'online' })
},
{
level: '交付',
title: '导出当前实时清单',
value: `${rows.length.toLocaleString()} 辆当前页`,
detail: '客户问询或运营交接时,先导出当前筛选的车辆在线、位置、速度和里程。',
action: '导出 CSV',
color: rows.length > 0 ? 'blue' as const : 'grey' as const,
disabled: rows.length === 0,
onClick: exportRealtime
}
];
const realtimeMapDispatchItems = [
{
title: '查看在线车辆',
value: `${onlineCount.toLocaleString()} 在线`,
detail: `定位有效 ${locatedCount.toLocaleString()} 辆,先确认客户当前能看到哪些车。`,
action: '实时地图',
color: onlineCount > 0 ? 'green' as const : 'orange' as const,
disabled: false,
onClick: () => {
window.location.hash = buildAppHash({ page: 'map', protocol: filters.protocol, filters: { ...filters, online: 'online' } });
}
},
{
title: '复盘选中车辆',
value: selectedMapRow?.plate || selectedMapRow?.vin || '先选车',
detail: selectedMapRow ? `${vehicleServiceStatus(selectedMapRow).label}${dataFreshness(selectedMapRow).detail}` : '从地图或实时列表选择车辆后,回放轨迹和里程变化。',
action: '轨迹回放',
color: selectedMapRow ? vehicleServiceStatus(selectedMapRow).color : 'grey' as const,
disabled: !selectedMapRow || !canOpenVehicle(selectedMapRow.vin),
onClick: () => selectedMapRow && onOpenHistory?.(selectedMapRow.vin, selectedVehicleProtocol)
},
{
title: '处理异常车辆',
value: `${mapAttentionRows.length.toLocaleString()} 关注`,
detail: mapAttentionRows[0] ? `${mapAttentionRows[0].plate || mapAttentionRows[0].vin}${realtimeIssueLabels(mapAttentionRows[0]).join('')}` : '当前筛选下暂无离线、超时或坐标异常车辆。',
action: '告警事件',
color: mapAttentionRows.length > 0 ? 'orange' as const : 'green' as const,
disabled: !onOpenQuality,
onClick: () => onOpenQuality?.({ serviceStatus: 'degraded', protocol: filters.protocol })
},
{
title: '导出当前态势',
value: `${rows.length.toLocaleString()} 辆当前页`,
detail: '导出在线、位置、速度、SOC、里程和数据通道证据用于客户交接。',
action: '导出CSV',
color: rows.length > 0 ? 'blue' as const : 'grey' as const,
disabled: rows.length === 0,
onClick: exportRealtime
}
];
const realtimeCustomerQuestions = [
{
question: '这辆车现在在哪里?',
answer: selectedMapRow ? selectedMapRow.plate || selectedMapRow.vin : `${locatedCount.toLocaleString()} 辆有定位`,
evidence: selectedMapRow && isValidCoordinate(selectedMapRow) ? `${selectedMapRow.longitude}, ${selectedMapRow.latitude}` : `定位有效率 ${formatPercent(locatedRate)}`,
action: '地图定位',
color: locatedCount > 0 ? 'blue' as const : 'orange' as const,
disabled: false,
onClick: () => {
if (selectedMapRow) {
selectRealtimeRow(selectedMapRow);
}
window.location.hash = buildAppHash({ page: 'map', protocol: filters.protocol, filters });
}
},
{
question: '这辆车还在线吗?',
answer: selectedMapRow ? selectedMapRow.online ? '在线' : '离线' : `${onlineCount.toLocaleString()} 辆在线`,
evidence: selectedMapRow ? dataFreshness(selectedMapRow).detail : `在线率 ${formatPercent(onlineRate)}`,
action: '只看在线',
color: selectedMapRow ? selectedMapRow.online ? 'green' as const : 'orange' as const : onlineCount > 0 ? 'green' as const : 'orange' as const,
disabled: false,
onClick: () => applyFilters({ ...filters, online: 'online' })
},
{
question: '为什么状态异常?',
answer: selectedMapRow ? realtimeIssueLabels(selectedMapRow)[0] : `${Math.max(degradedCount, sourceIssueRows.length).toLocaleString()} 辆关注`,
evidence: selectedMapRow ? vehicleServiceStatus(selectedMapRow).label : `${degradedCount.toLocaleString()} 降级 / ${staleCount.toLocaleString()} 超时`,
action: '异常清单',
color: degradedCount > 0 || staleCount > 0 || sourceIssueRows.length > 0 ? 'orange' as const : 'green' as const,
disabled: false,
onClick: () => copyRealtimeIssueChecklist()
},
{
question: '当前状态能交接吗?',
answer: realtimeImpactLevel,
evidence: `当前页 ${rows.length.toLocaleString()} / 总计 ${pagination.total.toLocaleString()}`,
action: '复制交接包',
color: realtimeImpactColor,
disabled: false,
onClick: () => copyRealtimeDutyHandoff()
}
];
const timeWindowMonitorBlock = (
<Card bordered className="vp-time-window-monitor">
<div className="vp-time-window-summary">
<Space wrap>
<Tag color="blue"></Tag>
<Tag color={timeWindowReady ? 'green' : 'orange'}>{timeWindowReady ? '时间窗可用' : '先选车辆'}</Tag>
<Tag color="grey">{timeWindowDuration}</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" disabled={!timeWindowReady} onClick={openTimeWindowHistory}></Button>
<Button size="small" theme="light" type="primary" disabled={!timeWindowReady} onClick={openTimeWindowRaw}></Button>
<Button size="small" theme="light" icon={<IconCopy />} onClick={copyTimeWindowPackage}></Button>
</Space>
</div>
<div className="vp-time-window-main">
<div className="vp-time-window-controls">
<div className="vp-time-window-target">
<span></span>
<strong>{timeWindowRow ? [timeWindowRow.plate, timeWindowRow.vin].filter(Boolean).join(' / ') : timeWindowKeyword || '未选择车辆'}</strong>
<small>{timeWindowProtocol || '全部数据通道'}</small>
</div>
<label>
<span></span>
<input
aria-label="时间窗开始"
value={timeWindow.dateFrom}
onChange={(event) => setTimeWindow((current) => ({ ...current, dateFrom: event.target.value }))}
/>
</label>
<label>
<span></span>
<input
aria-label="时间窗结束"
value={timeWindow.dateTo}
onChange={(event) => setTimeWindow((current) => ({ ...current, dateTo: event.target.value }))}
/>
</label>
<div className="vp-time-window-presets">
<Button size="small" onClick={() => setTimeWindowPreset('15m')}>15</Button>
<Button size="small" onClick={() => setTimeWindowPreset('1h')}>1</Button>
<Button size="small" onClick={() => setTimeWindowPreset('today')}></Button>
<Button size="small" onClick={() => setTimeWindowPreset('24h')}>24</Button>
</div>
</div>
<div className="vp-time-window-actions">
{timeWindowWorkItems.map((item) => (
<button
key={item.title}
type="button"
className="vp-time-window-action"
disabled={item.disabled}
onClick={item.onClick}
aria-label={`时间窗复盘 ${item.title} ${item.action}`}
>
<Tag color={item.color}>{item.title}</Tag>
<strong>{item.value}</strong>
<span>{item.detail}</span>
<em>{item.action}</em>
</button>
))}
</div>
</div>
</Card>
);
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-service-rail">
<div className="vp-map-service-rail-summary">
<Space wrap>
<Tag color="blue"></Tag>
<Tag color={amapConfigured ? 'green' : 'orange'}>{amapConfigured ? 'Live Map 可用' : '坐标预览'}</Tag>
<Tag color={mapAttentionRows.length > 0 ? 'orange' : 'green'}>{mapAttentionRows.length.toLocaleString()} </Tag>
</Space>
<Typography.Title heading={5} style={{ margin: 0 }}> Live Map 线线</Typography.Title>
<Typography.Text type="secondary">
</Typography.Text>
</div>
<div className="vp-map-service-rail-actions">
{mapServiceActionItems.map((item) => (
<button
key={item.title}
type="button"
className="vp-map-service-rail-action"
disabled={item.disabled}
onClick={item.onClick}
aria-label={`地图服务行动条 ${item.title} ${item.action}`}
>
<Tag color={item.color}>{item.title}</Tag>
<strong>{item.value}</strong>
<span>{item.detail}</span>
<em>{item.action}</em>
</button>
))}
</div>
</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-field-command">
<div className="vp-map-field-command-summary">
<Space wrap>
<Tag color="blue"></Tag>
<Tag color={amapConfigured ? 'green' : 'orange'}>{amapConfigured ? '地图可用' : '坐标预览'}</Tag>
<Tag color={mapAttentionRows.length > 0 ? 'orange' : 'green'}>
{mapAttentionRows.length > 0 ? '先处置关注车辆' : '态势稳定'}
</Tag>
</Space>
<Typography.Title heading={5} style={{ margin: 0 }}></Typography.Title>
<Typography.Text type="secondary">
</Typography.Text>
</div>
<div className="vp-map-field-command-grid">
{mapFieldCommandItems.map((item) => (
<button
key={item.title}
type="button"
className="vp-map-field-command-item"
disabled={item.disabled}
onClick={item.onClick}
aria-label={`地图现场指挥 ${item.title} ${item.action}`}
>
<Tag color={item.color}>{item.title}</Tag>
<strong>{item.value}</strong>
<span>{item.detail}</span>
<em>{item.action}</em>
</button>
))}
</div>
</div>
<div className="vp-map-dispatch-actions">
<div className="vp-map-dispatch-actions-summary">
<Space wrap>
<Tag color="blue"></Tag>
<Tag color={mapAttentionRows.length > 0 ? 'orange' : 'green'}>{mapAttentionRows.length.toLocaleString()} </Tag>
<Tag color={selectedMapRow ? 'green' : 'grey'}>{selectedMapRow ? '已选车辆' : '待选车辆'}</Tag>
</Space>
<Typography.Text strong></Typography.Text>
</div>
<div className="vp-map-dispatch-actions-grid">
{mapDispatchActionItems.map((item) => (
<button
key={item.title}
type="button"
className="vp-map-dispatch-action"
disabled={item.disabled}
onClick={item.onClick}
aria-label={`地图调度处置栏 ${item.title} ${item.value} ${item.action}`}
>
<Tag color={item.color}>{item.title}</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-selected-service-strip">
<div className="vp-map-selected-service-summary">
<Space wrap>
<Tag color="blue"></Tag>
{selectedMapRow ? (
<>
<Tag color={vehicleServiceStatus(selectedMapRow).color}>{vehicleServiceStatus(selectedMapRow).label}</Tag>
<Tag color={dataFreshness(selectedMapRow).color}>{dataFreshness(selectedMapRow).detail}</Tag>
</>
) : (
<Tag color="grey"></Tag>
)}
</Space>
<Typography.Title heading={5} style={{ margin: 0 }}>{selectedVehicleLabel}</Typography.Title>
<Typography.Text type="secondary">
</Typography.Text>
</div>
<div className="vp-map-selected-service-actions">
{mapVehicleWorkItems.map((item) => (
<button
key={item.title}
type="button"
className="vp-map-selected-service-action"
disabled={item.disabled}
onClick={item.onClick}
aria-label={`地图选中车辆 ${item.title} ${item.action}`}
>
<Tag color={item.color}>{item.title}</Tag>
<strong>{item.action}</strong>
<span>{item.value}</span>
</button>
))}
<button
type="button"
className="vp-map-selected-service-action"
disabled={!selectedMapRow || !canOpenVehicle(selectedMapRow.vin)}
onClick={openSelectedVehicleRaw}
aria-label="地图选中车辆 数据导出 历史导出"
>
<Tag color="blue"></Tag>
<strong></strong>
<span>{selectedMapRow ? selectedVehicleProtocol || selectedMapRow.primaryProtocol || '全部通道' : '等待选择'}</span>
</button>
</div>
</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>
{timeWindowMonitorBlock}
<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-question-board">
<div className="vp-realtime-question-summary">
<Space wrap>
<Tag color="blue"></Tag>
<Tag color={realtimeImpactColor}>{realtimeImpactLevel}</Tag>
<Tag color={selectedMapRow ? 'green' : 'grey'}>{selectedMapRow ? '已选车辆' : '未选车辆'}</Tag>
</Space>
<Typography.Title heading={5} style={{ margin: 0 }}></Typography.Title>
<Typography.Text type="secondary">
线
</Typography.Text>
</div>
<div className="vp-realtime-question-grid">
{realtimeCustomerQuestions.map((item) => (
<button
key={item.question}
type="button"
className="vp-realtime-question-item"
disabled={item.disabled}
onClick={item.onClick}
aria-label={`实时客户常问 ${item.question} ${item.action}`}
>
<Tag color={item.color}>{item.question}</Tag>
<strong>{item.answer}</strong>
<span>{item.evidence}</span>
<em>{item.action}</em>
</button>
))}
</div>
</div>
<div className="vp-realtime-next-actions">
{realtimeNextActions.map((item) => (
<button
key={item.title}
type="button"
className="vp-realtime-next-action"
disabled={item.disabled}
onClick={item.onClick}
aria-label={`实时运营建议 ${item.title} ${item.action}`}
>
<Tag color={item.color}>{item.level}</Tag>
<strong>{item.title}</strong>
<span>{item.value}</span>
<small>{item.detail}</small>
<em>{item.action}</em>
</button>
))}
</div>
<div className="vp-realtime-map-dispatch">
<div className="vp-realtime-map-dispatch-copy">
<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.Title heading={5} style={{ margin: 0 }}> Live Map 线</Typography.Title>
<Typography.Text type="secondary">
</Typography.Text>
</div>
<div className="vp-realtime-map-dispatch-grid">
{realtimeMapDispatchItems.map((item) => (
<button
key={item.title}
type="button"
className="vp-realtime-map-dispatch-item"
disabled={item.disabled}
onClick={item.onClick}
aria-label={`实时地图调度台 ${item.title} ${item.action}`}
>
<Tag color={item.color}>{item.title}</Tag>
<strong>{item.value}</strong>
<span>{item.detail}</span>
<em>{item.action}</em>
</button>
))}
</div>
</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>
{timeWindowMonitorBlock}
<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>
);
}