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

3913 lines
191 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 mapExecutiveControlPackageText({
filters,
rows,
total,
onlineCount,
locatedCount,
attentionCount,
selectedRow,
selectedProtocol,
amapConfigured
}: {
filters: Record<string, string>;
rows: VehicleRealtimeRow[];
total: number;
onlineCount: number;
locatedCount: number;
attentionCount: number;
selectedRow?: VehicleRealtimeRow;
selectedProtocol?: string;
amapConfigured: boolean;
}) {
const selectedVIN = selectedRow?.vin || '';
return [
'【客户车辆地图总控包】',
`当前筛选:${realtimeFilterSummary(filters).join('') || '全部车辆'}`,
`地图总控:当前页 ${rows.length.toLocaleString()} / 总计 ${total.toLocaleString()}`,
`服务链路:实时找车 -> 轨迹回放 -> 围栏告警 -> 报表导出`,
`在线定位:${onlineCount.toLocaleString()} 在线;${locatedCount.toLocaleString()} 辆有坐标;${attentionCount.toLocaleString()} 辆关注`,
`地图能力:${amapConfigured ? '高德地图可用' : '坐标预览'}`,
`选中车辆:${selectedRow ? `${selectedRow.plate || '-'} / ${selectedVIN} / ${selectedProtocol || selectedRow.primaryProtocol || '-'}` : '未选择'}`,
selectedRow ? `选中车辆状态:${vehicleServiceStatus(selectedRow).label}${dataFreshness(selectedRow).detail}` : '',
`实时地图:${appURL(buildAppHash({ page: 'map', protocol: filters.protocol, filters: { ...filters, online: filters.online || 'online' } }))}`,
selectedVIN ? `轨迹回放:${appURL(buildAppHash({ page: 'history', keyword: selectedVIN, protocol: selectedProtocol }))}` : '',
`围栏告警:${appURL(buildAppHash({ page: 'alert-events', filters: { serviceStatus: 'degraded', ...(filters.protocol ? { protocol: filters.protocol } : {}) } }))}`,
selectedVIN ? `里程统计:${appURL(buildAppHash({ page: 'mileage', keyword: selectedVIN, protocol: selectedProtocol }))}` : '',
selectedVIN ? `历史导出:${appURL(buildAppHash({ page: 'history-query', keyword: selectedVIN, protocol: selectedProtocol, filters: { tab: 'raw', includeFields: 'true' } }))}` : ''
].filter(Boolean).join('\n');
}
function selectedMapVehicleHandoffText(row: VehicleRealtimeRow, protocol: string) {
const vin = row.vin || '';
const vehicle = `${row.plate || '-'} / ${vin || '-'} / ${protocol || row.primaryProtocol || '-'}`;
const position = isValidCoordinate(row) ? `${row.longitude},${row.latitude}` : '无有效坐标';
return [
'【地图选中车辆交接卡】',
`车辆:${vehicle}`,
`服务状态:${vehicleServiceStatus(row).label}`,
`在线状态:${row.online ? '在线' : '离线'}`,
`最后上报:${row.lastSeen || '-'}`,
`位置:${position}`,
`速度:${row.speedKmh ?? '-'} km/hSOC${row.socPercent ?? '-'}%;总里程:${row.totalMileageKm ?? '-'} km`,
`问题:${realtimeIssueLabels(row).join('')}`,
`建议动作:${realtimeIssueAction(row)}`,
`车辆服务:${appURL(buildAppHash({ page: 'detail', keyword: vin, protocol }))}`,
`实时地图:${appURL(buildAppHash({ page: 'map', keyword: vin, protocol }))}`,
`轨迹回放:${appURL(buildAppHash({ page: 'history', keyword: vin, protocol }))}`,
`里程统计:${appURL(buildAppHash({ page: 'mileage', keyword: vin, protocol }))}`,
`历史数据:${appURL(buildAppHash({ page: 'history-query', keyword: vin, protocol, filters: { tab: 'raw', includeFields: 'true' } }))}`,
`告警通知:${appURL(buildAppHash({ page: 'alert-events', keyword: vin, protocol }))}`
].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 mapAreaMonitorText({
filters,
rows,
locatedCount,
attentionCount,
selectedRow,
selectedProtocol,
amapConfigured
}: {
filters: Record<string, string>;
rows: VehicleRealtimeRow[];
locatedCount: number;
attentionCount: number;
selectedRow?: VehicleRealtimeRow;
selectedProtocol?: string;
amapConfigured: boolean;
}) {
const selectedVIN = selectedRow?.vin || '';
return [
'【区域围栏监控说明】',
`当前筛选:${realtimeFilterSummary(filters).join('') || '全部车辆'}`,
`区域态势:${locatedCount.toLocaleString()} 辆可定位 / ${attentionCount.toLocaleString()} 辆关注 / ${amapConfigured ? '高德地图可用' : '坐标预览'}`,
`车辆范围:当前页 ${rows.length.toLocaleString()}`,
`围栏建议:先用有效定位车辆作为区域覆盖基础,再对离线、超时、越界和长时间停留车辆触发告警通知。`,
selectedRow ? `选中车辆:${selectedRow.plate || '-'} / ${selectedVIN} / ${selectedProtocol || selectedRow.primaryProtocol || '-'}` : '选中车辆:未选择',
selectedRow ? `选中车辆位置:${isValidCoordinate(selectedRow) ? `${selectedRow.longitude},${selectedRow.latitude}` : '无有效坐标'}` : '',
`实时地图:${appURL(buildAppHash({ page: 'map', protocol: filters.protocol, filters }))}`,
`关注车辆:${appURL(buildAppHash({ page: 'map', protocol: filters.protocol, filters: { ...filters, serviceStatus: 'degraded' } }))}`,
selectedVIN ? `轨迹复盘:${appURL(buildAppHash({ page: 'history', keyword: selectedVIN, protocol: selectedProtocol }))}` : '',
`告警通知:${appURL(buildAppHash({ page: 'alert-events', filters: { serviceStatus: 'degraded' } }))}`
].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 timeWindowTaskItems = [
{
step: '1',
title: '锁定车辆',
value: timeWindowRow?.plate || timeWindowKeyword || '未选择车辆',
detail: timeWindowReady ? `${timeWindow.dateFrom}${timeWindow.dateTo}` : '先从实时列表或地图选择车辆,再确认时间窗。',
action: timeWindowReady ? '已锁定' : '先选车辆',
color: timeWindowReady ? 'green' as const : 'orange' as const,
disabled: false,
onClick: () => {
if (!timeWindowReady) {
Toast.warning('请先选择车辆和时间窗');
}
}
},
{
step: '2',
title: '轨迹复盘',
value: timeWindowDuration,
detail: '回放位置、速度、里程断点和停留变化。',
action: '轨迹回放',
color: 'blue' as const,
disabled: !timeWindowReady,
onClick: openTimeWindowHistory
},
{
step: '3',
title: '里程核对',
value: timeWindowRow?.totalMileageKm != null ? `${timeWindowRow.totalMileageKm} km` : '待核对',
detail: '用同一时间窗进入里程统计,核对区间差值和日统计闭合。',
action: '里程统计',
color: timeWindowRow?.totalMileageKm != null ? 'blue' as const : 'orange' as const,
disabled: !timeWindowReady,
onClick: openTimeWindowMileage
},
{
step: '4',
title: '历史导出',
value: timeWindowProtocol || '全部通道',
detail: '导出位置、原始帧和字段证据,支撑客户问询。',
action: '导出证据',
color: 'blue' as const,
disabled: !timeWindowReady,
onClick: openTimeWindowRaw
},
{
step: '5',
title: '告警闭环',
value: timeWindowRow ? vehicleServiceStatus(timeWindowRow).label : '待选择',
detail: '复盘断链、离线、定位异常和通知处理记录。',
action: '告警复盘',
color: timeWindowRow && hasSourceIssue(timeWindowRow) ? 'orange' as const : timeWindowReady ? 'green' as const : 'grey' 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 realtimePriorityRows = mapAttentionRows.slice(0, 4);
const realtimePrioritySummary = realtimePriorityRows.length > 0
? `${realtimePriorityRows.length.toLocaleString()} 辆优先处置`
: '暂无阻断';
const missingCoordinateCount = Math.max(0, rows.length - locatedCount);
const noCoordinateRows = rows.filter((row) => !isValidCoordinate(row));
const selectedVehicleProtocol = selectedMapRow ? filters.protocol || selectedMapRow.primaryProtocol || '' : '';
const selectedVehicleLabel = selectedMapRow?.plate || selectedMapRow?.vin || '未选择车辆';
const mapStatusFilterItems = [
{
label: '在线车辆',
value: `${onlineCount.toLocaleString()} 在线`,
detail: `在线率 ${formatPercent(onlineRate)},客户当前可实时看车。`,
color: onlineCount > 0 ? 'green' as const : 'orange' as const,
onClick: () => applyFilters({ ...filters, online: 'online' })
},
{
label: '离线车辆',
value: `${Math.max(0, rows.length - onlineCount).toLocaleString()} 离线`,
detail: '优先判断平台是否仍在转发,或车辆是否停止上报。',
color: rows.length - onlineCount > 0 ? 'orange' as const : 'green' as const,
onClick: () => applyFilters({ ...filters, online: 'offline' })
},
{
label: '无坐标车辆',
value: `${missingCoordinateCount.toLocaleString()} 无坐标`,
detail: '没有有效经纬度时,地图、轨迹和围栏都需要先复核。',
color: missingCoordinateCount > 0 ? 'orange' as const : 'green' as const,
onClick: () => {
const firstNoCoordinate = noCoordinateRows.find((row) => canOpenVehicle(row.vin));
if (firstNoCoordinate) {
selectRealtimeRow(firstNoCoordinate);
return;
}
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,
onClick: () => applyFilters({ ...filters, serviceStatus: 'degraded' })
}
];
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 copySelectedMapVehicleHandoff = () => {
if (!selectedMapRow || !canOpenVehicle(selectedMapRow.vin)) {
Toast.warning('请先选择可交接的车辆');
return;
}
copyText(selectedMapVehicleHandoffText(selectedMapRow, selectedVehicleProtocol), '选中车辆交接卡');
};
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 mapShiftTimeWindowLabel = timeWindowDuration === '1 天窗口' ? '24 小时窗口' : timeWindowDuration;
const mapCommandTaskItems = [
{
label: '实时监控',
value: `${onlineCount.toLocaleString()} 在线`,
detail: `${locatedCount.toLocaleString()} 辆有定位,${staleCount.toLocaleString()} 辆更新超时。`,
color: onlineCount > 0 ? 'green' as const : 'orange' as const,
disabled: false,
onClick: () => applyFilters({ ...filters, online: 'online' })
},
{
label: '轨迹回放',
value: selectedMapRow?.plate || selectedMapRow?.vin || '先选车',
detail: selectedMapRow ? `${dataFreshness(selectedMapRow).detail},回放位置、速度和里程。` : '从地图或车辆列表选择一辆车。',
color: selectedMapRow ? 'blue' as const : 'grey' as const,
disabled: !selectedMapRow || !canOpenVehicle(selectedMapRow.vin),
onClick: () => selectedMapRow && onOpenHistory?.(selectedMapRow.vin, selectedVehicleProtocol)
},
{
label: '历史查询',
value: selectedVehicleProtocol || '全部来源',
detail: '查看位置、原始帧和解析字段,支撑客户复核。',
color: 'blue' as const,
disabled: !selectedMapRow || !canOpenVehicle(selectedMapRow.vin),
onClick: openSelectedVehicleRaw
},
{
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,
disabled: !onOpenQuality,
onClick: () => onOpenQuality?.(selectedMapRow && canOpenVehicle(selectedMapRow.vin)
? { keyword: selectedMapRow.vin, protocol: selectedVehicleProtocol }
: { serviceStatus: 'degraded' })
},
{
label: '统计导出',
value: `${rows.length.toLocaleString()}`,
detail: '进入里程统计,或导出当前地图车辆清单。',
color: 'blue' as const,
disabled: rows.length === 0,
onClick: exportRealtime
}
];
const mapSelectedServiceItems = [
{
label: '车辆档案',
action: '查看车辆',
detail: selectedVehicleLabel,
color: selectedMapRow ? vehicleServiceStatus(selectedMapRow).color : 'grey' as const,
disabled: !selectedMapRow || !canOpenVehicle(selectedMapRow.vin),
onClick: () => selectedMapRow && onOpenVehicle(selectedMapRow.vin, selectedVehicleProtocol)
},
{
label: '轨迹回放',
action: '回放轨迹',
detail: selectedMapRow ? dataFreshness(selectedMapRow).detail : '等待选择',
color: selectedMapRow ? 'blue' as const : 'grey' as const,
disabled: !selectedMapRow || !canOpenVehicle(selectedMapRow.vin),
onClick: () => selectedMapRow && onOpenHistory?.(selectedMapRow.vin, selectedVehicleProtocol)
},
{
label: '历史数据',
action: '查询历史',
detail: selectedVehicleProtocol || '全部来源',
color: 'blue' as const,
disabled: !selectedMapRow || !canOpenVehicle(selectedMapRow.vin),
onClick: openSelectedVehicleRaw
},
{
label: '告警通知',
action: '查看告警',
detail: selectedMapRow ? realtimeIssueLabels(selectedMapRow)[0] : '等待选择',
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 })
},
{
label: '交接卡',
action: '复制交接',
detail: selectedMapRow ? realtimeIssueLabels(selectedMapRow)[0] : '等待选择',
color: 'blue' as const,
disabled: !selectedMapRow || !canOpenVehicle(selectedMapRow.vin),
onClick: copySelectedMapVehicleHandoff
}
];
const mapShiftConsoleItems = [
{
label: '今日态势',
value: `${onlineCount.toLocaleString()} 在线 / ${mapAttentionRows.length.toLocaleString()} 关注`,
detail: `当前页 ${rows.length.toLocaleString()} 辆,定位有效率 ${formatPercent(locatedRate)}`,
action: '只看在线',
color: mapAttentionRows.length > 0 ? 'orange' as const : 'green' as const,
disabled: false,
onClick: () => applyFilters({ ...filters, online: 'online' })
},
{
label: '优先车辆',
value: mapAttentionRows[0]?.plate || mapAttentionRows[0]?.vin || selectedVehicleLabel,
detail: mapAttentionRows[0] ? realtimeIssueLabels(mapAttentionRows[0]).join('') : '当前没有必须优先处理的车辆。',
action: '处理关注',
color: mapAttentionRows.length > 0 ? 'orange' as const : selectedMapRow ? vehicleServiceStatus(selectedMapRow).color : 'grey' as const,
disabled: !mapAttentionRows[0] && !selectedMapRow,
onClick: () => {
if (mapAttentionRows[0]) {
selectRealtimeRow(mapAttentionRows[0]);
return;
}
if (selectedMapRow) {
selectRealtimeRow(selectedMapRow);
}
}
},
{
label: '时间窗复盘',
value: mapShiftTimeWindowLabel,
detail: timeWindowRow ? `${timeWindowRow.plate || timeWindowRow.vin} / ${timeWindowProtocol || '全部来源证据'}` : '先选择一辆车再复盘。',
action: '打开轨迹',
color: timeWindowReady ? 'blue' as const : 'orange' as const,
disabled: !timeWindowReady,
onClick: openTimeWindowHistory
},
{
label: '交接说明',
value: '可复制',
detail: mapAttentionRows.length > 0 ? '交接时先说明关注车辆和处理入口。' : '地图态势稳定,可直接复制交接口径。',
action: '复制交接',
color: 'blue' as const,
disabled: false,
onClick: copyRealtimeDutyHandoff
}
];
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 copyMapAreaMonitor = () => copyText(mapAreaMonitorText({
filters,
rows,
locatedCount,
attentionCount: mapAttentionRows.length,
selectedRow: selectedMapRow,
selectedProtocol: selectedVehicleProtocol,
amapConfigured
}), '区域围栏监控说明');
const mapAreaMonitorItems = [
{
title: '电子围栏',
value: `${locatedCount.toLocaleString()} 辆可定位`,
detail: '先把有效定位车辆作为围栏覆盖基础,后续按区域配置越界和进出围栏规则。',
color: locatedCount > 0 ? 'blue' as const : 'orange' as const,
action: '关注车辆',
disabled: false,
onClick: () => applyFilters({ ...filters, serviceStatus: 'degraded' })
},
{
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 ? `${mapAttentionRows.length.toLocaleString()} 辆关注` : '暂无越界',
detail: mapAttentionRows.length > 0 ? '将离线、超时、坐标异常车辆交给告警事件形成通知闭环。' : '当前区域态势稳定,可继续巡检。',
color: mapAttentionRows.length > 0 ? 'orange' as const : 'green' as const,
action: '告警事件',
disabled: !onOpenQuality,
onClick: () => onOpenQuality?.({ serviceStatus: 'degraded' })
},
{
title: '区域报表',
value: `${rows.length.toLocaleString()} 辆当前页`,
detail: '复制区域监控说明,交付当前围栏态势、关注车辆和后续通知链接。',
color: 'blue' as const,
action: '复制说明',
disabled: false,
onClick: copyMapAreaMonitor
}
];
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 mapViewModeItems = [
{
label: '车辆总览',
value: `${rows.length.toLocaleString()}`,
detail: `在线 ${onlineCount.toLocaleString()},有效定位 ${locatedCount.toLocaleString()},先确认客户能看到的车辆范围。`,
action: '当前页',
color: 'blue' as const,
disabled: false,
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('')}` : '当前筛选下暂无离线、定位或通道异常车辆。',
action: '关注车辆',
color: mapAttentionRows.length > 0 ? 'orange' as const : 'green' as const,
disabled: false,
onClick: () => applyFilters({ ...filters, serviceStatus: 'degraded' })
},
{
label: '轨迹复盘',
value: selectedVehicleLabel,
detail: selectedMapRow ? '围绕选中车辆进入轨迹、统计和历史查询。' : '从地图或车辆列表选择一辆车后复盘。',
action: '选中车辆',
color: selectedMapRow ? 'blue' as const : 'grey' as const,
disabled: !selectedMapRow || !canOpenVehicle(selectedMapRow.vin),
onClick: () => selectedMapRow && onOpenHistory?.(selectedMapRow.vin, selectedVehicleProtocol)
},
{
label: '交付导出',
value: `${rows.length.toLocaleString()}`,
detail: '导出当前地图车辆清单、在线状态和客户交付证据。',
action: '导出证据',
color: rows.length > 0 ? 'blue' as const : 'grey' as const,
disabled: rows.length === 0,
onClick: exportRealtime
}
];
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 liveMapDecisionItems = [
{
label: '在线覆盖',
value: `${onlineCount.toLocaleString()} / ${rows.length.toLocaleString()}`,
detail: `${(rows.length - onlineCount).toLocaleString()} 辆离线,在线率 ${formatPercent(onlineRate)}`,
color: onlineCount > 0 ? 'green' as const : 'orange' as const,
onClick: () => applyFilters({ ...filters, online: 'online' })
},
{
label: '定位覆盖',
value: `${locatedCount.toLocaleString()} / ${rows.length.toLocaleString()}`,
detail: `${missingCoordinateCount.toLocaleString()} 辆无坐标,定位率 ${formatPercent(locatedRate)}`,
color: locatedCount > 0 ? 'blue' as const : 'orange' as const,
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,
onClick: () => {
if (mapAttentionRows[0]) {
selectRealtimeRow(mapAttentionRows[0]);
return;
}
applyFilters({ ...filters, serviceStatus: 'degraded' });
}
},
{
label: '当前选车',
value: selectedVehicleLabel,
detail: selectedMapRow ? `${vehicleServiceStatus(selectedMapRow).label} / ${dataFreshness(selectedMapRow).detail}` : '从地图或车辆列表选择车辆。',
color: selectedMapRow ? vehicleServiceStatus(selectedMapRow).color : 'grey' as const,
disabled: !selectedMapRow || !canOpenVehicle(selectedMapRow.vin),
onClick: () => selectedMapRow && onOpenVehicle(selectedMapRow.vin, selectedVehicleProtocol)
}
];
const liveMapDecisionActions = [
{
label: '只看在线',
action: '在线',
color: 'green' as const,
disabled: false,
onClick: () => applyFilters({ ...filters, online: 'online' })
},
{
label: '定位关注',
action: '关注车',
color: mapAttentionRows.length > 0 ? 'orange' as const : 'green' as const,
disabled: mapAttentionRows.length === 0,
onClick: () => mapAttentionRows[0] && selectRealtimeRow(mapAttentionRows[0])
},
{
label: '轨迹复盘',
action: '轨迹',
color: selectedMapRow ? 'blue' as const : 'grey' as const,
disabled: !selectedMapRow || !canOpenVehicle(selectedMapRow.vin),
onClick: () => selectedMapRow && onOpenHistory?.(selectedMapRow.vin, selectedVehicleProtocol)
},
{
label: '导出地图',
action: 'CSV',
color: rows.length > 0 ? 'blue' as const : 'grey' as const,
disabled: rows.length === 0,
onClick: exportRealtime
}
];
const mapLayerControlItems = [
{
label: '在线车辆',
value: `${onlineCount.toLocaleString()} 在线`,
detail: '只看当前还能实时服务的车辆,适合客户查看在线态势。',
color: onlineCount > 0 ? 'green' as const : 'orange' as const,
onClick: () => applyFilters({ ...filters, online: 'online' })
},
{
label: '关注车辆',
value: `${mapAttentionRows.length.toLocaleString()} 关注`,
detail: mapAttentionRows[0] ? `优先车辆 ${mapAttentionRows[0].plate || mapAttentionRows[0].vin}` : '当前没有离线、超时或通道异常车辆。',
color: mapAttentionRows.length > 0 ? 'orange' as const : 'green' as const,
onClick: () => mapAttentionRows[0] ? selectRealtimeRow(mapAttentionRows[0]) : applyFilters({ ...filters, serviceStatus: 'degraded' })
},
{
label: '定位车辆',
value: `${locatedCount.toLocaleString()} 定位`,
detail: `有效定位率 ${formatPercent(locatedRate)},决定地图、轨迹和围栏是否可用。`,
color: locatedCount > 0 ? 'blue' as const : 'orange' as const,
onClick: () => load(filters, pagination.currentPage, pagination.pageSize)
},
{
label: '选中车辆',
value: selectedVehicleLabel,
detail: selectedMapRow ? '进入单车服务、轨迹、统计、历史和告警。' : '从地图或车辆列表选择一辆车。',
color: selectedMapRow ? vehicleServiceStatus(selectedMapRow).color : 'grey' as const,
disabled: !selectedMapRow || !canOpenVehicle(selectedMapRow.vin),
onClick: () => selectedMapRow && onOpenVehicle(selectedMapRow.vin, selectedVehicleProtocol)
}
];
const mapLayerControlActions = [
{
label: '只看在线',
action: '在线层',
color: 'green' as const,
disabled: false,
onClick: () => applyFilters({ ...filters, online: 'online' })
},
{
label: '优先关注',
action: '关注层',
color: mapAttentionRows.length > 0 ? 'orange' as const : 'green' as const,
disabled: mapAttentionRows.length === 0,
onClick: () => mapAttentionRows[0] && selectRealtimeRow(mapAttentionRows[0])
},
{
label: '选车服务',
action: '服务层',
color: selectedMapRow ? 'blue' as const : 'grey' as const,
disabled: !selectedMapRow || !canOpenVehicle(selectedMapRow.vin),
onClick: () => selectedMapRow && onOpenVehicle(selectedMapRow.vin, selectedVehicleProtocol)
},
{
label: '导出态势',
action: '交付层',
color: rows.length > 0 ? 'blue' as const : 'grey' as const,
disabled: rows.length === 0,
onClick: exportRealtime
}
];
const copyMapCustomerPackage = () => copyText(mapCustomerPackageText({
filters,
rows,
total: pagination.total,
onlineCount,
locatedCount,
attentionCount: mapAttentionRows.length,
staleCount,
selectedRow: selectedMapRow,
selectedProtocol: selectedVehicleProtocol,
amapConfigured
}), '客户地图监控包');
const copyMapExecutiveControlPackage = () => copyText(mapExecutiveControlPackageText({
filters,
rows,
total: pagination.total,
onlineCount,
locatedCount,
attentionCount: mapAttentionRows.length,
selectedRow: selectedMapRow,
selectedProtocol: selectedVehicleProtocol,
amapConfigured
}), '客户车辆地图总控包');
const copyMapFleetJourneyPackage = () => copyText([
'【车队服务旅程交付包】',
`当前筛选:${realtimeFilterSummary(filters).join('') || '全部车辆'}`,
`实时找车:${onlineCount.toLocaleString()} 在线 / ${rows.length.toLocaleString()} 当前页 / ${pagination.total.toLocaleString()} 总计`,
`轨迹复盘:${selectedMapRow ? `${selectedMapRow.plate || '-'} / ${selectedMapRow.vin || '-'} / ${selectedVehicleProtocol || selectedMapRow.primaryProtocol || '-'}` : '未选择车辆'}`,
`里程核对:${selectedMapRow?.totalMileageKm != null ? `${selectedMapRow.totalMileageKm} km` : '待核对'}`,
`导出通知:${mapAttentionRows.length.toLocaleString()} 关注 / ${staleCount.toLocaleString()} 更新超时`,
`实时地图:${appURL(buildAppHash({ page: 'map', protocol: filters.protocol, filters }))}`,
selectedMapRow && canOpenVehicle(selectedMapRow.vin) ? `车辆服务:${appURL(buildAppHash({ page: 'detail', keyword: selectedMapRow.vin, protocol: selectedVehicleProtocol }))}` : '',
selectedMapRow && canOpenVehicle(selectedMapRow.vin) ? `轨迹回放:${appURL(buildAppHash({ page: 'history', keyword: selectedMapRow.vin, protocol: selectedVehicleProtocol }))}` : '',
selectedMapRow && canOpenVehicle(selectedMapRow.vin) ? `里程统计:${appURL(buildAppHash({ page: 'mileage', keyword: selectedMapRow.vin, protocol: selectedVehicleProtocol }))}` : '',
selectedMapRow && canOpenVehicle(selectedMapRow.vin) ? `历史导出:${appURL(buildAppHash({ page: 'history-query', keyword: selectedMapRow.vin, protocol: selectedVehicleProtocol, filters: { tab: 'raw', includeFields: 'true' } }))}` : '',
`告警通知:${appURL(buildAppHash({ page: 'alert-events', filters: { serviceStatus: 'degraded', ...(filters.protocol ? { protocol: filters.protocol } : {}) } }))}`
].filter(Boolean).join('\n'), '车队服务旅程交付包');
const mapFleetJourneyItems = [
{
step: '1',
title: '实时找车',
value: `${onlineCount.toLocaleString()} 在线`,
detail: `${locatedCount.toLocaleString()} 辆有定位,先让客户知道当前能看到哪些车。`,
color: onlineCount > 0 ? 'green' as const : 'orange' as const,
disabled: false,
onClick: () => applyFilters({ ...filters, online: 'online' })
},
{
step: '2',
title: '轨迹复盘',
value: selectedVehicleLabel,
detail: selectedMapRow ? `${dataFreshness(selectedMapRow).detail},进入轨迹复盘位置、速度和里程断点。` : '先从地图或车辆列表选择一辆车。',
color: selectedMapRow ? 'blue' as const : 'grey' as const,
disabled: !selectedMapRow || !canOpenVehicle(selectedMapRow.vin),
onClick: () => selectedMapRow && onOpenHistory?.(selectedMapRow.vin, selectedVehicleProtocol)
},
{
step: '3',
title: '里程核对',
value: selectedMapRow?.totalMileageKm != null ? `${selectedMapRow.totalMileageKm} km` : '待核对',
detail: '进入同一车辆的里程统计,核对区间差值、日统计和总里程。',
color: selectedMapRow?.totalMileageKm != null ? 'blue' as const : 'orange' as const,
disabled: !selectedMapRow || !canOpenVehicle(selectedMapRow.vin),
onClick: () => selectedMapRow && (window.location.hash = buildAppHash({
page: 'mileage',
keyword: selectedMapRow.vin,
protocol: selectedVehicleProtocol
}))
},
{
step: '4',
title: '导出通知',
value: `${mapAttentionRows.length.toLocaleString()} 关注`,
detail: `${staleCount.toLocaleString()} 辆更新超时,交付前进入告警通知或复制交付包。`,
color: mapAttentionRows.length > 0 ? 'orange' as const : 'green' as const,
disabled: false,
onClick: () => mapAttentionRows.length > 0 && onOpenQuality ? onOpenQuality({ serviceStatus: 'degraded', protocol: filters.protocol }) : copyMapFleetJourneyPackage()
}
];
const copyMapCustomerDecision = () => copyText(mapCustomerDecisionText({
filters,
rows,
total: pagination.total,
onlineCount,
locatedCount,
attentionRows: mapAttentionRows,
selectedRow: selectedMapRow,
selectedProtocol: selectedVehicleProtocol,
amapConfigured
}), '地图客户决策说明');
const mapExecutiveControlItems = [
{
title: '实时找车',
value: `${onlineCount.toLocaleString()} 在线`,
detail: `${locatedCount.toLocaleString()} 辆有坐标,先确认客户当前能看见哪些车。`,
action: '打开地图',
color: onlineCount > 0 ? 'green' as const : 'orange' as const,
disabled: false,
onClick: () => applyFilters({ ...filters, online: 'online' })
},
{
title: '轨迹回放',
value: selectedMapRow?.plate || selectedMapRow?.vin || '先选车',
detail: selectedMapRow ? '围绕选中车辆回放位置、速度、里程和断点。' : '从地图或列表选择一辆车后进入轨迹回放。',
action: '回放轨迹',
color: selectedMapRow ? 'blue' as const : '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: '导出当前地图车辆范围、在线状态、位置和服务状态。',
action: '导出报表',
color: rows.length > 0 ? 'blue' as const : 'grey' as const,
disabled: rows.length === 0,
onClick: exportRealtime
}
];
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 realtimeSourceEvidenceText = (() => {
const protocols: string[] = [];
rows.forEach((row) => {
const rowProtocols = row.protocols?.length ? row.protocols : row.primaryProtocol ? [row.primaryProtocol] : [];
rowProtocols.forEach((protocol) => {
if (protocol && !protocols.includes(protocol)) {
protocols.push(protocol);
}
});
});
return protocols.length > 0 ? protocols.join(' / ') : '暂无来源证据';
})();
const realtimeServiceOverviewItems = [
{
label: '在线车辆',
value: `${onlineCount.toLocaleString()} 在线`,
detail: `${(rows.length - onlineCount).toLocaleString()} 辆离线,在线率 ${formatPercent(onlineRate)}`,
color: onlineCount > 0 ? 'green' as const : 'orange' as const,
onClick: () => applyFilters({ ...filters, online: 'online' })
},
{
label: '有效定位',
value: `${locatedCount.toLocaleString()}`,
detail: `${missingCoordinateCount.toLocaleString()} 辆无坐标,定位率 ${formatPercent(locatedRate)}`,
color: locatedCount > 0 ? 'blue' as const : 'orange' as const,
onClick: () => {
window.location.hash = buildAppHash({ page: 'map', protocol: filters.protocol, filters });
}
},
{
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,
onClick: () => applyFilters({ ...filters, serviceStatus: 'degraded' })
},
{
label: '地图能力',
displayLabel: '高德能力',
value: amapConfigured ? '高德可用' : '坐标预览',
detail: amapConfigured ? '高德 Web JS 与安全代理就绪' : '地图降级为坐标预览',
color: amapConfigured ? 'green' as const : 'orange' as const,
onClick: () => load(filters, pagination.currentPage, pagination.pageSize)
}
];
const realtimeServiceActions = [
{
label: '打开地图',
action: '地图态势',
color: 'blue' as const,
disabled: false,
onClick: () => {
window.location.hash = buildAppHash({ page: 'map', protocol: filters.protocol, filters });
}
},
{
label: '轨迹复盘',
action: '轨迹回放',
color: selectedMapRow ? 'blue' as const : 'grey' as const,
disabled: !selectedMapRow || !canOpenVehicle(selectedMapRow.vin),
onClick: () => selectedMapRow && onOpenHistory?.(selectedMapRow.vin, selectedVehicleProtocol)
},
{
label: '导出清单',
action: '导出CSV',
color: rows.length > 0 ? 'blue' as const : 'grey' as const,
disabled: rows.length === 0,
onClick: exportRealtime
},
{
label: '告警通知',
action: '关注车辆',
color: mapAttentionRows.length > 0 ? 'orange' as const : 'green' as const,
disabled: !onOpenQuality,
onClick: () => onOpenQuality?.({ serviceStatus: 'degraded' })
}
];
const realtimeCustomerCommandItems = [
{
label: '实时地图',
value: `${locatedCount.toLocaleString()} 有定位`,
detail: '先回答客户车辆在哪里,定位无效车辆进入异常队列。',
action: '打开地图',
color: locatedCount > 0 ? 'blue' as const : 'orange' as const,
disabled: false,
onClick: () => {
window.location.hash = buildAppHash({ page: 'map', protocol: filters.protocol, filters });
}
},
{
label: '在线车辆',
value: `${onlineCount.toLocaleString()} 在线`,
detail: `${(rows.length - onlineCount).toLocaleString()} 辆离线,优先确认当前可服务车辆。`,
action: '只看在线',
color: onlineCount > 0 ? 'green' as const : 'orange' as const,
disabled: false,
onClick: () => applyFilters({ ...filters, online: 'online' })
},
{
label: '异常车辆',
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' })
},
{
label: '轨迹复盘',
value: selectedMapRow?.plate || selectedMapRow?.vin || '未选车',
detail: selectedMapRow ? '围绕选中车辆回放轨迹、核对里程和异常时间窗。' : '先从地图或表格选择一辆车。',
action: '轨迹回放',
color: selectedMapRow ? 'blue' as const : 'grey' as const,
disabled: !selectedMapRow || !canOpenVehicle(selectedMapRow.vin),
onClick: () => selectedMapRow && onOpenHistory?.(selectedMapRow.vin, selectedVehicleProtocol)
},
{
label: '导出交付',
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 realtimeSingleVehicleItems = [
{
label: '车辆服务',
value: selectedMapRow?.plate || selectedMapRow?.vin || '未选车辆',
detail: selectedMapRow ? `${vehicleServiceStatus(selectedMapRow).label}${sourceEvidenceText(selectedMapRow)}` : '先从地图或列表选择车辆。',
color: selectedMapRow ? vehicleServiceStatus(selectedMapRow).color : 'grey' as const,
action: '车辆档案',
disabled: !selectedMapRow || !canOpenVehicle(selectedMapRow.vin),
onClick: () => selectedMapRow && onOpenVehicle(selectedMapRow.vin, selectedVehicleProtocol)
},
{
label: '轨迹回放',
value: selectedMapRow ? dataFreshness(selectedMapRow).label : '待选车',
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)
},
{
label: '里程统计',
value: selectedMapRow?.totalMileageKm != null ? `${selectedMapRow.totalMileageKm} km` : '无里程',
detail: '核对单车总里程、日里程和区间闭合。',
color: selectedMapRow?.totalMileageKm != null ? 'blue' as const : 'grey' as const,
action: '查里程',
disabled: !selectedMapRow || !canOpenVehicle(selectedMapRow.vin),
onClick: () => {
if (!selectedMapRow) return;
window.location.hash = buildAppHash({ page: 'mileage', keyword: selectedMapRow.vin, protocol: selectedVehicleProtocol });
}
},
{
label: '历史导出',
value: selectedVehicleProtocol || selectedMapRow?.primaryProtocol || '全部通道',
detail: '进入历史查询导出同一车辆的 RAW、位置和字段证据。',
color: selectedMapRow ? 'blue' as const : 'grey' as const,
action: '导出证据',
disabled: !selectedMapRow || !canOpenVehicle(selectedMapRow.vin),
onClick: openSelectedVehicleRaw
},
{
label: '告警通知',
value: selectedMapRow ? vehicleServiceStatus(selectedMapRow).label : '待选车',
detail: selectedMapRow ? realtimeIssueLabels(selectedMapRow).join('') : '选择车辆后查看断链、离线和定位异常。',
color: selectedMapRow && hasSourceIssue(selectedMapRow) ? 'orange' as const : selectedMapRow ? 'green' as const : 'grey' as const,
action: '查看告警',
disabled: !selectedMapRow || !canOpenVehicle(selectedMapRow.vin) || !onOpenQuality,
onClick: () => selectedMapRow && onOpenQuality?.({ keyword: selectedMapRow.vin, protocol: selectedVehicleProtocol })
}
];
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>
<div className="vp-time-window-task-board">
<div className="vp-time-window-task-summary">
<Space wrap>
<Tag color="blue"></Tag>
<Tag color={timeWindowReady ? 'green' : 'orange'}>{timeWindowReady ? '可交付' : '待锁定'}</Tag>
</Space>
<Typography.Text strong>穿</Typography.Text>
</div>
<div className="vp-time-window-task-grid">
{timeWindowTaskItems.map((item) => (
<button
key={item.step}
type="button"
className="vp-time-window-task-item"
disabled={item.disabled}
onClick={item.onClick}
aria-label={`时间窗监控任务台 ${item.step} ${item.title} ${item.value} ${item.action}`}
>
<Tag color={item.color}>{item.step}</Tag>
<strong>{item.title}</strong>
<span>{item.value}</span>
<small>{item.detail}</small>
<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>
<section className="vp-map-command-center" aria-label="车辆服务地图中枢">
<div className="vp-map-command-main">
<div className="vp-map-command-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={4} style={{ margin: 0 }}>
32960808MQTT 线
</Typography.Title>
<Typography.Text type="secondary">
</Typography.Text>
</div>
<div className="vp-map-command-tasks">
{mapCommandTaskItems.map((item) => (
<button
key={item.label}
type="button"
className="vp-map-command-task"
disabled={item.disabled}
onClick={item.onClick}
aria-label={`车辆服务地图任务 ${item.label} ${item.value}`}
>
<Tag color={item.color}>{item.label}</Tag>
<strong>{item.value}</strong>
<span>{item.detail}</span>
</button>
))}
</div>
</div>
<aside className="vp-map-command-side">
<div className="vp-map-command-side-card">
<Typography.Text strong></Typography.Text>
<div className="vp-map-foundation-facts">
<span>Web JS</span><strong>{amapConfigured ? '已配置' : '未配置'}</strong>
<span></span><strong>{amapSecurityProxyEnabled ? '已启用' : '未启用'}</strong>
<span> Key</span><strong>{runtime?.amapApiConfigured ? '已配置' : '未配置'}</strong>
</div>
</div>
<div className="vp-map-command-side-card">
<Typography.Text strong></Typography.Text>
<Typography.Text type="secondary">
</Typography.Text>
</div>
</aside>
</section>
<section className="vp-map-fleet-journey" aria-label="车队服务旅程条">
<div className="vp-map-fleet-journey-copy">
<Space wrap>
<Tag color="blue"></Tag>
<Tag color={mapAttentionRows.length > 0 ? 'orange' : 'green'}>
{mapAttentionRows.length > 0 ? `${mapAttentionRows.length.toLocaleString()} 关注` : '态势稳定'}
</Tag>
<Tag color={selectedMapRow ? vehicleServiceStatus(selectedMapRow).color : 'grey'}>
{selectedMapRow ? selectedVehicleLabel : '未选车'}
</Tag>
</Space>
<Typography.Title heading={5} style={{ margin: 0 }}>
</Typography.Title>
<Typography.Text type="secondary">
32960808MQTT 沿
</Typography.Text>
<Button size="small" theme="solid" type="primary" icon={<IconCopy />} onClick={copyMapFleetJourneyPackage}>
</Button>
</div>
<div className="vp-map-fleet-journey-steps">
{mapFleetJourneyItems.map((item) => (
<button
key={item.step}
type="button"
className="vp-map-fleet-journey-step"
disabled={item.disabled}
onClick={item.onClick}
aria-label={`车队服务旅程条 ${item.step} ${item.title} ${item.value}`}
>
<Tag color={item.color}>{item.step}</Tag>
<strong>{item.title}</strong>
<span>{item.value}</span>
<small>{item.detail}</small>
</button>
))}
</div>
</section>
<div className="vp-map-service-rail" aria-label="客户车辆地图总控">
<div className="vp-map-service-rail-summary">
<Space wrap>
<Tag color="blue"></Tag>
<Tag color={amapConfigured ? 'green' : 'orange'}>{amapConfigured ? '高德地图可用' : '坐标预览'}</Tag>
<Tag color={mapAttentionRows.length > 0 ? 'orange' : 'green'}>
{mapAttentionRows.length > 0 ? `${mapAttentionRows.length.toLocaleString()} 辆关注` : '态势稳定'}
</Tag>
</Space>
<Typography.Title heading={5} style={{ margin: 0 }}>使</Typography.Title>
<Typography.Text type="secondary">
Live Map 使线
</Typography.Text>
<Space wrap>
<Button size="small" theme="solid" type="primary" icon={<IconCopy />} onClick={copyMapExecutiveControlPackage}></Button>
<Button size="small" disabled={!selectedMapRow || !canOpenVehicle(selectedMapRow.vin)} onClick={() => selectedMapRow && onOpenHistory?.(selectedMapRow.vin, selectedVehicleProtocol)}></Button>
<Button size="small" disabled={!onOpenQuality} onClick={() => onOpenQuality?.({ serviceStatus: 'degraded', protocol: filters.protocol })}></Button>
</Space>
</div>
<div className="vp-map-service-rail-actions">
{mapExecutiveControlItems.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.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>
<section className="vp-live-map-decision-strip" aria-label="Live Map 决策条">
<div className="vp-live-map-decision-copy">
<Space wrap>
<Tag color="blue">Live Map </Tag>
<Tag color={amapConfigured ? 'green' : 'orange'}>{amapConfigured ? '高德可用' : '坐标预览'}</Tag>
<Tag color={mapAttentionRows.length > 0 ? 'orange' : 'green'}>
{mapAttentionRows.length > 0 ? `${mapAttentionRows.length.toLocaleString()} 关注` : '态势稳定'}
</Tag>
</Space>
<Typography.Title heading={5} style={{ margin: 0 }}>
线
</Typography.Title>
<Typography.Text type="secondary">
线
</Typography.Text>
</div>
<div className="vp-live-map-decision-grid">
{liveMapDecisionItems.map((item) => (
<button
key={item.label}
type="button"
className="vp-live-map-decision-item"
disabled={item.disabled}
onClick={item.onClick}
aria-label={`Live Map 决策条 ${item.label} ${item.value}`}
>
<Tag color={item.color}>{item.label}</Tag>
<strong>{item.value}</strong>
<span>{item.detail}</span>
</button>
))}
</div>
<div className="vp-live-map-decision-actions">
{liveMapDecisionActions.map((item) => (
<button
key={item.label}
type="button"
className="vp-live-map-decision-action"
disabled={item.disabled}
onClick={item.onClick}
aria-label={`Live Map 决策动作 ${item.label} ${item.action}`}
>
<Tag color={item.color}>{item.label}</Tag>
<span>{item.action}</span>
</button>
))}
</div>
</section>
<section className="vp-map-status-filter-strip" aria-label="地图状态筛选条">
<div className="vp-map-status-filter-copy">
<Space wrap>
<Tag color="blue"></Tag>
<Tag color={mapAttentionRows.length > 0 ? 'orange' : 'green'}>
{mapAttentionRows.length > 0 ? `${mapAttentionRows.length.toLocaleString()} 关注` : '状态稳定'}
</Tag>
</Space>
<Typography.Text type="secondary">
线线
</Typography.Text>
</div>
<div className="vp-map-status-filter-grid">
{mapStatusFilterItems.map((item) => (
<button
key={item.label}
type="button"
className="vp-map-status-filter-item"
onClick={item.onClick}
aria-label={`地图状态筛选条 ${item.label} ${item.value}`}
>
<Tag color={item.color}>{item.label}</Tag>
<strong>{item.value}</strong>
<span>{item.detail}</span>
</button>
))}
</div>
</section>
<section className="vp-map-layer-control" aria-label="地图图层控制台">
<div className="vp-map-layer-control-copy">
<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-layer-control-grid">
{mapLayerControlItems.map((item) => (
<button
key={item.label}
type="button"
className="vp-map-layer-control-item"
disabled={item.disabled}
onClick={item.onClick}
aria-label={`地图图层控制台 ${item.label} ${item.value}`}
>
<Tag color={item.color}>{item.label}</Tag>
<strong>{item.value}</strong>
<span>{item.detail}</span>
</button>
))}
</div>
<div className="vp-map-layer-control-actions">
{mapLayerControlActions.map((item) => (
<button
key={item.label}
type="button"
className="vp-map-layer-control-action"
disabled={item.disabled}
onClick={item.onClick}
aria-label={`地图图层动作 ${item.label} ${item.action}`}
>
<Tag color={item.color}>{item.label}</Tag>
<span>{item.action}</span>
</button>
))}
</div>
</section>
<section className="vp-map-single-service" aria-label="单车服务台">
<div className="vp-map-single-service-copy">
<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>
</div>
<div className="vp-map-single-service-actions">
{mapSelectedServiceItems.map((item) => (
<button
key={item.label}
type="button"
className="vp-map-single-service-action"
disabled={item.disabled}
onClick={item.onClick}
aria-label={`单车服务台 ${item.label} ${item.action}`}
>
<Tag color={item.color}>{item.label}</Tag>
<strong>{item.action}</strong>
<span>{item.detail}</span>
</button>
))}
</div>
</section>
<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-shift-console">
<div className="vp-map-shift-console-summary">
<Space wrap>
<Tag color="blue"></Tag>
<Tag color={mapAttentionRows.length > 0 ? 'orange' : 'green'}>
{mapAttentionRows.length > 0 ? '有关注车辆' : '态势稳定'}
</Tag>
<Tag color={selectedMapRow ? 'green' : 'grey'}>{selectedMapRow ? '已选车辆' : '待选车辆'}</Tag>
</Space>
<Typography.Text strong></Typography.Text>
<Typography.Text type="secondary">
</Typography.Text>
</div>
<div className="vp-map-shift-console-grid">
{mapShiftConsoleItems.map((item) => (
<button
key={item.label}
type="button"
className="vp-map-shift-console-item"
disabled={item.disabled}
onClick={item.onClick}
aria-label={`客户地图值班台 ${item.label} ${item.value} ${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-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-view-mode">
<div className="vp-map-view-mode-summary">
<Space wrap>
<Tag color="blue"></Tag>
<Tag color={mapAttentionRows.length > 0 ? 'orange' : 'green'}>
{mapAttentionRows.length > 0 ? '异常优先' : '运营稳定'}
</Tag>
<Tag color={selectedMapRow ? 'blue' : 'grey'}>{selectedVehicleLabel}</Tag>
</Space>
<Typography.Text strong>使</Typography.Text>
<Typography.Text type="secondary">
</Typography.Text>
</div>
<div className="vp-map-view-mode-grid">
{mapViewModeItems.map((item) => (
<button
key={item.label}
type="button"
className="vp-map-view-mode-item"
disabled={item.disabled}
onClick={item.onClick}
aria-label={`地图视图模式 ${item.label} ${item.value} ${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-area-monitor">
<div className="vp-map-area-monitor-summary">
<Space wrap>
<Tag color="blue"></Tag>
<Tag color={locatedCount > 0 ? 'green' : 'orange'}>{locatedCount.toLocaleString()} </Tag>
<Tag color={mapAttentionRows.length > 0 ? 'orange' : 'green'}>{mapAttentionRows.length.toLocaleString()} </Tag>
</Space>
<Typography.Title heading={5} style={{ margin: 0 }}></Typography.Title>
<Typography.Text type="secondary">
</Typography.Text>
</div>
<div className="vp-map-area-monitor-grid">
{mapAreaMonitorItems.map((item) => (
<button
key={item.title}
type="button"
className="vp-map-area-monitor-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>
<button
type="button"
className="vp-map-selected-service-action"
disabled={!selectedMapRow || !canOpenVehicle(selectedMapRow.vin)}
onClick={copySelectedMapVehicleHandoff}
aria-label="地图选中车辆 交接卡 复制交接"
>
<Tag color="blue"></Tag>
<strong></strong>
<span>{selectedMapRow ? realtimeIssueLabels(selectedMapRow)[0] : '等待选择'}</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} />
<section className="vp-realtime-service-overview" aria-label="实时车辆服务总览">
<div className="vp-realtime-service-overview-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 }}>
线
</Typography.Title>
<Typography.Text type="secondary">
{filterSummary.length > 0 ? filterSummary.join(' / ') : '全部车辆'}{realtimeSourceEvidenceText}
</Typography.Text>
</div>
<div className="vp-realtime-service-overview-metrics">
{realtimeServiceOverviewItems.map((item) => (
<button
key={item.label}
type="button"
className="vp-realtime-service-overview-metric"
onClick={item.onClick}
aria-label={`实时车辆服务总览 ${item.label} ${item.value}`}
>
<Tag color={item.color}>{item.displayLabel ?? item.label}</Tag>
<strong>{item.value}</strong>
<span>{item.detail}</span>
</button>
))}
</div>
<div className="vp-realtime-service-overview-actions">
{realtimeServiceActions.map((item) => (
<button
key={item.label}
type="button"
className="vp-realtime-service-overview-action"
disabled={item.disabled}
onClick={item.onClick}
aria-label={`实时服务动作 ${item.label} ${item.action}`}
>
<Tag color={item.color}>{item.label}</Tag>
<span>{item.action}</span>
</button>
))}
</div>
</section>
<section className="vp-realtime-command-center" aria-label="客户实时监控总控台">
<div className="vp-realtime-command-center-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 }}></Typography.Title>
<Typography.Text type="secondary">
{realtimeSourceEvidenceText}
</Typography.Text>
</div>
<div className="vp-realtime-command-center-grid">
{realtimeCustomerCommandItems.map((item) => (
<button
key={item.label}
type="button"
className="vp-realtime-command-center-item"
disabled={item.disabled}
onClick={item.onClick}
aria-label={`客户实时监控总控台 ${item.label} ${item.value} ${item.action}`}
>
<Tag color={item.color}>{item.label}</Tag>
<strong>{item.value}</strong>
<span>{item.detail}</span>
<em>{item.action}</em>
</button>
))}
</div>
</section>
<section className="vp-realtime-priority-board" aria-label="实时处置优先级">
<div className="vp-realtime-priority-summary">
<Space wrap>
<Tag color="blue"></Tag>
<Tag color={realtimePriorityRows.length > 0 ? 'orange' : 'green'}>{realtimePrioritySummary}</Tag>
<Tag color="grey"> {rows.length.toLocaleString()} </Tag>
</Space>
<Typography.Title heading={5} style={{ margin: 0 }}>
</Typography.Title>
<Typography.Text type="secondary">
</Typography.Text>
</div>
<div className="vp-realtime-priority-grid">
{realtimePriorityRows.length === 0 ? (
<div className="vp-realtime-priority-empty">
<Tag color="green"></Tag>
<strong></strong>
<span></span>
</div>
) : realtimePriorityRows.map((row, index) => {
const label = row.plate || row.vin || '未知车辆';
const protocol = filters.protocol || row.primaryProtocol || '';
const issueText = realtimeIssueLabels(row).join('');
return (
<article key={`${row.vin}-${row.primaryProtocol}-realtime-priority`} className="vp-realtime-priority-item">
<div className="vp-realtime-priority-rank">{index + 1}</div>
<div className="vp-realtime-priority-main">
<Space wrap>
<Tag color={vehicleServiceStatus(row).color}>{vehicleServiceStatus(row).label}</Tag>
<Tag color={dataFreshness(row).color}>{dataFreshness(row).label}</Tag>
<Tag color={isValidCoordinate(row) ? 'green' : 'orange'}>{isValidCoordinate(row) ? '有定位' : '无定位'}</Tag>
</Space>
<strong>{label}</strong>
<span>{issueText}</span>
<small>{row.vin} / {protocol || '全部来源证据'} / {row.lastSeen || '-'}</small>
</div>
<div className="vp-realtime-priority-actions">
<button type="button" onClick={() => onOpenQuality?.({ keyword: row.vin, protocol })} disabled={!onOpenQuality || !canOpenVehicle(row.vin)} aria-label={`实时处置优先级 ${index + 1} ${label} 查看告警`}></button>
<button type="button" onClick={() => onOpenHistory?.(row.vin, protocol)} disabled={!canOpenVehicle(row.vin)} aria-label={`实时处置优先级 ${index + 1} ${label} 轨迹回放`}></button>
<button type="button" onClick={() => onOpenVehicle(row.vin, protocol)} disabled={!canOpenVehicle(row.vin)} aria-label={`实时处置优先级 ${index + 1} ${label} 车辆档案`}></button>
</div>
</article>
);
})}
</div>
</section>
<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>
<section className="vp-realtime-single-snapshot" aria-label="实时单车服务快照">
<div className="vp-realtime-single-summary">
<Space wrap>
<Tag color="blue"></Tag>
<Tag color={selectedMapRow ? vehicleServiceStatus(selectedMapRow).color : 'grey'}>{selectedMapRow ? vehicleServiceStatus(selectedMapRow).label : '未选车辆'}</Tag>
<Tag color={selectedMapRow ? dataFreshness(selectedMapRow).color : 'grey'}>{selectedMapRow ? dataFreshness(selectedMapRow).label : '等待数据'}</Tag>
</Space>
<Typography.Title heading={5} style={{ margin: 0 }}>线</Typography.Title>
<Typography.Text type="secondary">
{selectedMapRow ? `${selectedMapRow.plate || selectedMapRow.vin} / ${selectedVehicleProtocol || selectedMapRow.primaryProtocol || '全部通道'} / ${selectedMapRow.lastSeen || '无最后时间'}` : '从地图或实时列表选择一辆车后,快照会自动聚焦到单车服务。'}
</Typography.Text>
</div>
<div className="vp-realtime-single-grid">
{realtimeSingleVehicleItems.map((item) => (
<button
key={item.label}
type="button"
className="vp-realtime-single-item"
disabled={item.disabled}
onClick={item.onClick}
aria-label={`实时单车服务快照 ${item.label} ${item.value} ${item.action}`}
>
<Tag color={item.color}>{item.label}</Tag>
<strong>{item.value}</strong>
<span>{item.detail}</span>
<em>{item.action}</em>
</button>
))}
</div>
</section>
{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>
);
}