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

1865 lines
92 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, Col, Form, Row, Select, Space, Spin, Table, Tag, Toast, Typography } from '@douyinfe/semi-ui';
import { useEffect, useMemo, useState } from 'react';
import { api } from '../api/client';
import type { DashboardSummary, OpsHealth, QualityIssueRow, VehicleCoverageRow, VehicleRealtimeRow, VehicleServiceSummary } from '../api/types';
import { PageHeader } from '../components/PageHeader';
import { SourceStatusTags } from '../components/SourceStatusTags';
import { StatusTag } from '../components/StatusTag';
import { VehicleMap, type VehicleMapPoint } from '../components/VehicleMap';
import { isAMapConfigured } from '../config/appConfig';
import { buildAppHash } from '../domain/appRoute';
import { buildCsv, downloadCsv, type CsvColumn } from '../domain/csvExport';
import { qualityIssueLabel, qualityProtocolLabel } from '../domain/qualityIssue';
import { qualityIssueVehicleLookup } from '../domain/vehicleLookup';
const serviceStatusTitle: Record<string, string> = {
healthy: '服务正常',
degraded: '来源不完整',
offline: '车辆离线',
no_data: '暂无数据来源',
identity_required: '身份未绑定'
};
const missingProtocolTitle: Record<string, string> = {
GB32960: '缺 GB32960',
JT808: '缺 JT808',
YUTONG_MQTT: '缺 YUTONG_MQTT'
};
type DashboardSnapshotRow = {
section: string;
item: string;
value: string;
detail: string;
};
type FocusVehicleService = {
label: string;
lookupKey: string;
protocol?: string;
reason: string;
statusLabel: string;
statusColor: 'green' | 'orange' | 'red' | 'grey' | 'blue';
realtimeEvidence: string;
historyEvidence: string;
alertEvidence: string;
statisticEvidence: string;
issueType?: string;
};
const dashboardSnapshotColumns: CsvColumn<DashboardSnapshotRow>[] = [
{ title: '模块', value: (row) => row.section },
{ title: '指标', value: (row) => row.item },
{ title: '值', value: (row) => row.value },
{ title: '说明', value: (row) => row.detail }
];
function formatLag(value?: number | null) {
return value == null ? '未接入' : value.toLocaleString();
}
function formatCount(value?: number | null) {
return value == null ? '0' : value.toLocaleString();
}
function vehicleServiceOnlineText(serviceSummary: VehicleServiceSummary | null, summary: DashboardSummary | null) {
const onlineVehicles = serviceSummary?.onlineVehicles ?? summary?.onlineVehicles;
const totalVehicles = serviceSummary?.totalVehicles;
if (onlineVehicles == null || totalVehicles == null) {
return '未接入';
}
return `${onlineVehicles.toLocaleString()} / ${totalVehicles.toLocaleString()} 在线`;
}
function rowServiceStatus(row: { serviceStatus?: { title: string; severity: string }; onlineSourceCount: number; sourceCount: number }) {
if (row.serviceStatus) {
return {
label: row.serviceStatus.title,
color: row.serviceStatus.severity === 'ok' ? 'green' as const : row.serviceStatus.severity === 'error' ? 'red' as const : 'orange' as const
};
}
if (row.onlineSourceCount <= 0) {
return { label: '车辆离线', color: 'red' as const };
}
if (row.onlineSourceCount < row.sourceCount) {
return { label: '来源不完整', color: 'orange' as const };
}
return { label: '服务正常', color: 'green' as const };
}
function sourceEvidenceText(row: { onlineSourceCount: number; sourceCount: number }) {
return `${row.onlineSourceCount}/${row.sourceCount} 来源在线`;
}
function hasValidCoordinate(row: VehicleRealtimeRow) {
return Number.isFinite(row.longitude) && Number.isFinite(row.latitude) && row.longitude !== 0 && row.latitude !== 0;
}
function issueEvidenceDate(value?: string) {
const match = String(value ?? '').match(/^(\d{4})-(\d{2})-(\d{2})/);
return match ? `${match[1]}-${match[2]}-${match[3]}` : '';
}
function nextDate(value: string) {
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value.trim());
if (!match) return '';
const date = new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]) + 1));
return date.toISOString().slice(0, 10);
}
function todayDateString() {
const date = new Date();
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
function previousDateString(value: string) {
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value.trim());
if (!match) return value;
const date = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]) - 1);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
function priorityIssueVehicleLabel(row: QualityIssueRow) {
const identity = row.vin?.trim() && row.vin !== 'unknown' ? row.vin.trim() : row.phone?.trim();
return [row.plate?.trim(), identity].filter(Boolean).join(' / ') || row.sourceEndpoint || '-';
}
function priorityIssueEvidenceFilters(row: QualityIssueRow) {
const lookup = qualityIssueVehicleLookup(row);
const dateFrom = issueEvidenceDate(row.lastSeen);
return {
keyword: lookup.key,
protocol: row.protocol,
...(dateFrom ? { dateFrom, dateTo: nextDate(dateFrom) } : {})
};
}
function appURL(hash: string) {
return `${window.location.origin}${window.location.pathname}${hash}`;
}
async function copyText(value: string, label: string) {
try {
await navigator.clipboard.writeText(value);
Toast.success(`已复制${label}`);
} catch {
Toast.error(`复制${label}失败`);
}
}
function priorityIssueNotificationText(row: QualityIssueRow) {
const lookup = qualityIssueVehicleLookup(row);
const filters = priorityIssueEvidenceFilters(row);
const rawFilters = { ...filters, tab: 'raw', includeFields: 'true' };
const priority = row.severity === 'error' ? 'P0' : 'P1';
return [
`${priority} 告警通知】${qualityIssueLabel(row.issueType)}`,
`车辆:${priorityIssueVehicleLabel(row)}`,
`数据来源:${qualityProtocolLabel(row.protocol)}`,
`问题:${qualityIssueLabel(row.issueType)}`,
`最后时间:${row.lastSeen || '-'}`,
`详情:${row.detail || '-'}`,
`实时定位:${appURL(buildAppHash({ page: 'realtime', keyword: lookup.key, protocol: row.protocol }))}`,
`轨迹证据:${appURL(buildAppHash({ page: 'history', keyword: filters.keyword, protocol: filters.protocol, filters }))}`,
`原始记录:${appURL(buildAppHash({ page: 'history-query', keyword: rawFilters.keyword, protocol: rawFilters.protocol, filters: rawFilters }))}`,
`车辆服务:${appURL(buildAppHash({ page: 'detail', keyword: lookup.key, protocol: row.protocol }))}`,
`告警筛选:${appURL(buildAppHash({ page: 'alert-events', protocol: row.protocol, filters: { issueType: row.issueType } }))}`
].join('\n');
}
function focusVehicleFromIssue(row: QualityIssueRow): FocusVehicleService {
const lookup = qualityIssueVehicleLookup(row);
const filters = priorityIssueEvidenceFilters(row);
const label = priorityIssueVehicleLabel(row);
return {
label,
lookupKey: lookup.key,
protocol: row.protocol,
reason: `${row.severity === 'error' ? 'P0' : 'P1'} ${qualityIssueLabel(row.issueType)}`,
statusLabel: row.severity === 'error' ? '优先处置' : '持续跟踪',
statusColor: row.severity === 'error' ? 'red' : 'orange',
realtimeEvidence: `来源 ${qualityProtocolLabel(row.protocol)} / 最后 ${row.lastSeen || '-'}`,
historyEvidence: filters.dateFrom ? `${filters.dateFrom}${filters.dateTo}` : '待确认时间窗',
alertEvidence: row.detail || qualityIssueLabel(row.issueType),
statisticEvidence: '里程统计需回溯车辆口径',
issueType: row.issueType
};
}
function focusVehicleFromRealtime(row: VehicleRealtimeRow): FocusVehicleService {
const status = rowServiceStatus(row);
const label = [row.plate?.trim(), row.vin?.trim()].filter(Boolean).join(' / ') || row.phone || '-';
const location = hasValidCoordinate(row) ? `${row.longitude.toFixed(6)}, ${row.latitude.toFixed(6)}` : '无有效坐标';
return {
label,
lookupKey: row.vin || row.phone || row.plate,
protocol: row.primaryProtocol,
reason: row.online ? '最新在线车辆' : '最新车辆',
statusLabel: status.label,
statusColor: status.color,
realtimeEvidence: `${row.onlineSourceCount}/${row.sourceCount} 来源在线 / ${row.lastSeen || '-'}`,
historyEvidence: location,
alertEvidence: row.onlineSourceCount < row.sourceCount ? '来源不完整,建议检查缺失协议' : '暂无高优先级告警',
statisticEvidence: `速度 ${formatCount(row.speedKmh)} km/h / 里程 ${formatCount(row.totalMileageKm)} km`
};
}
function sourceConsistencyAction(row: VehicleCoverageRow, onFilter: (filters: Record<string, string>) => void) {
const consistency = row.sourceConsistency;
if (!consistency) {
return <Tag color="grey"></Tag>;
}
const color = consistency.severity === 'ok' ? 'green' as const : consistency.severity === 'error' ? 'red' as const : 'orange' as const;
const label = consistency.title || consistency.status;
if ((consistency.missingProtocols ?? []).length > 0) {
return (
<Button
size="small"
theme="light"
type={color === 'red' ? 'danger' : color === 'orange' ? 'warning' : 'primary'}
onClick={() => onFilter({ serviceStatus: 'degraded', missingProtocol: consistency.missingProtocols[0] })}
>
{label}
</Button>
);
}
return <Tag color={color}>{label}</Tag>;
}
export function Dashboard({
onOpenVehicle,
onOpenQuality,
onOpenMap,
onOpenRealtime,
onOpenVehicles,
onOpenHistory,
onOpenMileage
}: {
onOpenVehicle: (vin: string, protocol?: string) => void;
onOpenQuality: (filters?: Record<string, string>) => void;
onOpenMap: (filters?: Record<string, string>) => void;
onOpenRealtime: (filters?: Record<string, string>) => void;
onOpenVehicles: (filters?: Record<string, string>) => void;
onOpenHistory: (filters?: Record<string, string>) => void;
onOpenMileage: (filters?: Record<string, string>) => void;
}) {
const [summary, setSummary] = useState<DashboardSummary | null>(null);
const [serviceSummary, setServiceSummary] = useState<VehicleServiceSummary | null>(null);
const [coverage, setCoverage] = useState<VehicleCoverageRow[]>([]);
const [locations, setLocations] = useState<VehicleRealtimeRow[]>([]);
const [qualityIssues, setQualityIssues] = useState<QualityIssueRow[]>([]);
const [opsHealth, setOpsHealth] = useState<OpsHealth | null>(null);
const [timeMonitorFilters, setTimeMonitorFilters] = useState<Record<string, string>>(() => {
const today = todayDateString();
return {
dateFrom: previousDateString(today),
dateTo: today
};
});
const [loading, setLoading] = useState(true);
const [coverageLoading, setCoverageLoading] = useState(false);
const [coverageServiceStatusTitle, setCoverageServiceStatusTitle] = useState('');
const [coverageFilters, setCoverageFilters] = useState<Record<string, string>>({});
const localAMapConfigured = isAMapConfigured();
const amapConfigured = opsHealth?.runtime?.amapWebJsConfigured ?? localAMapConfigured;
const amapApiConfigured = opsHealth?.runtime?.amapApiConfigured ?? false;
const amapSecurityProxyEnabled = opsHealth?.runtime?.amapSecurityProxyEnabled ?? false;
const amapSecurityCodeExposed = opsHealth?.runtime?.amapSecurityCodeExposed ?? false;
const loadCoverage = (values?: Record<string, string>) => {
const nextValues = values ?? {};
setCoverageLoading(true);
setCoverageFilters(nextValues);
const scopeTitle = [
nextValues.serviceStatus ? serviceStatusTitle[nextValues.serviceStatus] ?? nextValues.serviceStatus : '',
nextValues.missingProtocol ? missingProtocolTitle[nextValues.missingProtocol] ?? `${nextValues.missingProtocol}` : ''
].filter(Boolean).join(' / ');
setCoverageServiceStatusTitle(scopeTitle);
const params = new URLSearchParams({ limit: '8' });
if (nextValues.keyword) params.set('keyword', nextValues.keyword);
if (nextValues.coverage) params.set('coverage', nextValues.coverage);
if (nextValues.missingProtocol) params.set('missingProtocol', nextValues.missingProtocol);
if (nextValues.online) params.set('online', nextValues.online);
if (nextValues.bindingStatus) params.set('bindingStatus', nextValues.bindingStatus);
if (nextValues.serviceStatus) params.set('serviceStatus', nextValues.serviceStatus);
api.vehicleCoverage(params)
.then((page) => setCoverage(page.items))
.catch((error: Error) => Toast.error(error.message))
.finally(() => setCoverageLoading(false));
};
useEffect(() => {
const tasks = [
api.dashboardSummary().then(setSummary),
api.vehicleServiceSummary().then(setServiceSummary),
api.vehicleCoverage(new URLSearchParams({ limit: '8' })).then((page) => setCoverage(page.items)),
api.vehicleRealtime(new URLSearchParams({ limit: '8' })).then((page) => setLocations(page.items)),
api.alertEvents(new URLSearchParams({ limit: '5' })).then((page) => setQualityIssues(page.items)),
api.opsHealth().then(setOpsHealth)
];
Promise.allSettled(tasks)
.then((results) => {
const failed = results.find((result) => result.status === 'rejected');
if (failed?.status === 'rejected') {
const reason = failed.reason;
Toast.error(reason instanceof Error ? reason.message : '总览数据加载失败');
}
})
.finally(() => setLoading(false));
}, []);
const serviceActionQueue = useMemo<Array<{ label: string; count: number; filters: Record<string, string>; detail: string }>>(() => {
const items: Array<{ label: string; count: number; filters: Record<string, string>; detail: string }> = [];
if ((serviceSummary?.noDataVehicles ?? 0) > 0) {
items.push({
label: '确认平台转发',
count: serviceSummary?.noDataVehicles ?? 0,
filters: { serviceStatus: 'no_data' },
detail: '车辆没有形成任何来源证据,优先确认上游平台是否持续转发。'
});
}
if ((serviceSummary?.identityRequiredVehicles ?? 0) > 0) {
items.push({
label: '维护身份绑定',
count: serviceSummary?.identityRequiredVehicles ?? 0,
filters: { serviceStatus: 'identity_required' },
detail: '已有数据但无法稳定归并到 VIN会影响车辆服务聚合。'
});
}
if ((serviceSummary?.archiveIncompleteVehicles ?? 0) > 0) {
items.push({
label: '完善车辆档案',
count: serviceSummary?.archiveIncompleteVehicles ?? 0,
filters: { archiveStatus: 'incomplete' },
detail: '车辆缺少车牌、手机号或 OEM 等基础档案,影响后续运营查询和治理。'
});
}
for (const field of serviceSummary?.archiveMissingFields ?? []) {
if (field.count <= 0) continue;
items.push({
label: `补齐${field.title}`,
count: field.count,
filters: { archiveMissing: field.field },
detail: `${field.title}会影响车辆档案检索、绑定确认和运营侧筛选。`
});
}
for (const source of serviceSummary?.missingSources ?? []) {
if (source.count <= 0) continue;
items.push({
label: `补齐 ${source.protocol} 来源`,
count: source.count,
filters: { serviceStatus: 'degraded', missingProtocol: source.protocol },
detail: `${source.protocol} 来源缺失会降低跨来源定位、里程和实时判断可信度。`
});
}
return items;
}, [serviceSummary]);
const commandOnlineCount = locations.filter((row) => row.online).length;
const commandLocatedCount = locations.filter(hasValidCoordinate).length;
const commandDegradedCount = locations.filter((row) => row.onlineSourceCount <= 0 || row.onlineSourceCount < row.sourceCount).length;
const kpis: Array<{ label: string; value: string; filters: Record<string, string> }> = [
{ label: '总车辆', value: formatCount(serviceSummary?.totalVehicles), filters: {} },
{ label: '在线车辆', value: formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles), filters: { online: 'online' } },
{ label: '今日活跃', value: formatCount(summary?.activeToday), filters: { online: 'online' } },
{ label: '有效定位', value: commandLocatedCount.toLocaleString(), filters: { online: 'online' } },
{ label: '告警车辆', value: formatCount(summary?.issueVehicles), filters: { serviceStatus: 'degraded' } },
{ label: '今日数据', value: formatCount(summary?.frameToday), filters: {} },
{ label: '档案不完整', value: formatCount(serviceSummary?.archiveIncompleteVehicles), filters: { archiveStatus: 'incomplete' } }
];
const highPriorityIssue = qualityIssues.find((item) => item.severity === 'error') ?? qualityIssues[0];
const highPriorityLookup = highPriorityIssue ? qualityIssueVehicleLookup(highPriorityIssue) : undefined;
const highPriorityEvidenceFilters = highPriorityIssue ? priorityIssueEvidenceFilters(highPriorityIssue) : undefined;
const focusVehicle = highPriorityIssue ? focusVehicleFromIssue(highPriorityIssue) : locations[0] ? focusVehicleFromRealtime(locations[0]) : undefined;
const unhealthyLinkCount = (summary?.linkHealth ?? []).filter((item) => item.status !== 'ok').length;
const commandMapPoints: VehicleMapPoint[] = locations.map((row, index) => ({
id: row.vin || `${row.primaryProtocol || 'source'}-${index}`,
label: row.plate || row.vin || 'unknown',
longitude: row.longitude,
latitude: row.latitude,
online: row.online,
title: `${row.plate || row.vin || '-'} ${row.primaryProtocol || ''} ${row.lastSeen || ''}`
}));
const customerHeroActions = [
{
title: '实时车辆地图',
value: `${commandLocatedCount.toLocaleString()} 辆有定位`,
detail: '按车查看实时位置、在线状态、速度和最新上报时间。',
action: '进入地图',
onClick: () => onOpenMap({ online: 'online' })
},
{
title: '轨迹回放',
value: `${formatCount(summary?.activeToday)} 今日活跃`,
detail: '选择车辆和时间范围,回放历史路线并核对断点。',
action: '回放轨迹',
onClick: () => onOpenHistory()
},
{
title: '里程统计',
value: `${formatCount(serviceSummary?.totalVehicles)} 辆可统计`,
detail: '按自定义时间查询区间里程、日里程和异常记录。',
action: '查看统计',
onClick: () => onOpenMileage({})
},
{
title: '历史数据导出',
value: `${formatCount(summary?.frameToday)} 今日数据`,
detail: '按车辆、时间和字段裁剪查询历史位置与原始记录。',
action: '查询导出',
onClick: () => onOpenHistory({ tab: 'raw', includeFields: 'true' })
}
];
const customerHealthItems = [
{
label: '在线车辆',
value: formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles),
detail: vehicleServiceOnlineText(serviceSummary, summary),
color: 'green' as const,
onClick: () => onOpenVehicles({ online: 'online' })
},
{
label: '有效定位',
value: commandLocatedCount.toLocaleString(),
detail: `当前抽样 ${locations.length.toLocaleString()} 辆,定位可用于地图和回放。`,
color: commandLocatedCount > 0 ? 'blue' as const : 'orange' as const,
onClick: () => onOpenMap({ online: 'online' })
},
{
label: '需要关注',
value: formatCount(summary?.issueVehicles),
detail: highPriorityIssue ? `${qualityIssueLabel(highPriorityIssue.issueType)} / ${priorityIssueVehicleLabel(highPriorityIssue)}` : '暂无高优先级告警。',
color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const,
onClick: () => onOpenQuality()
},
{
label: '时间范围监控',
value: formatCount(summary?.activeToday),
detail: '支持按任意时间窗查询轨迹、历史数据、里程和导出。',
color: 'blue' as const,
onClick: () => onOpenHistory()
}
];
const taskTimeMonitorSummary = (() => {
const vehicle = String(timeMonitorFilters.keyword ?? '').trim() || '全部车辆';
const source = String(timeMonitorFilters.protocol ?? '').trim() || '全部数据通道';
const dateFrom = String(timeMonitorFilters.dateFrom ?? '').trim();
const dateTo = String(timeMonitorFilters.dateTo ?? '').trim();
const range = dateFrom || dateTo ? `${dateFrom || '-'}${dateTo || '-'}` : '全部时间';
return `${vehicle} / ${source} / ${range}`;
})();
const customerTaskBoard = [
{
title: '先看全域在线',
value: `${formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles)} 在线`,
detail: `地图有效定位 ${commandLocatedCount.toLocaleString()} 辆,优先确认车辆是否仍在上报。`,
color: 'green' as const,
primaryAction: '打开实时地图',
secondaryAction: '查看在线车辆',
onPrimary: () => onOpenMap({ online: 'online' }),
onSecondary: () => onOpenRealtime({ online: 'online' })
},
{
title: '再看重点车辆',
value: focusVehicle?.label ?? '暂无重点车辆',
detail: focusVehicle ? `${focusVehicle.reason}${focusVehicle.realtimeEvidence}` : '当前没有高优先级车辆,保持日常巡检。',
color: focusVehicle?.statusColor ?? 'green' as const,
primaryAction: '车辆服务',
secondaryAction: '复制处置卡',
onPrimary: () => focusVehicle?.lookupKey && onOpenVehicle(focusVehicle.lookupKey, focusVehicle.protocol),
onSecondary: () => copyFocusVehicleService(),
disabled: !focusVehicle?.lookupKey
},
{
title: '按时间窗复盘',
value: taskTimeMonitorSummary,
detail: '同一时间窗贯穿轨迹、里程、RAW 和告警,不需要跨页面重复输入。',
color: 'blue' as const,
primaryAction: '轨迹回放',
secondaryAction: '里程统计',
onPrimary: () => openTimeMonitorHistory(),
onSecondary: () => openTimeMonitorMileage()
},
{
title: '导出证据数据',
value: `${formatCount(summary?.frameToday)} 今日数据`,
detail: '面向 BI、客户复盘和异常定位按车辆、时间、字段裁剪导出。',
color: 'blue' as const,
primaryAction: '历史导出',
secondaryAction: '导出概览',
onPrimary: () => openTimeMonitorRaw(),
onSecondary: () => exportDashboardSnapshot()
},
{
title: '闭环告警通知',
value: `${formatCount(summary?.issueVehicles)} 告警车辆`,
detail: highPriorityIssue ? `${qualityIssueLabel(highPriorityIssue.issueType)} / ${priorityIssueVehicleLabel(highPriorityIssue)}` : '暂无高优先级告警。',
color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const,
primaryAction: '告警事件',
secondaryAction: '复制交接',
onPrimary: () => onOpenQuality(highPriorityIssue?.issueType ? { issueType: highPriorityIssue.issueType } : {}),
onSecondary: () => copyOperationsHandoff()
}
];
const capabilities = [
{
title: '实时地图',
status: `在线态势 ${formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles)}`,
color: 'green' as const,
description: '以高德地图为主视角查看车辆在线分布、定位有效性、来源一致性和地图接入状态。',
action: '打开地图',
onClick: () => onOpenMap({ online: 'online' })
},
{
title: '实时监控',
status: `在线 ${formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles)}`,
color: 'green' as const,
description: '按车辆聚合最新位置、在线状态和多来源实时字段,支持实时地图查看。',
action: '查看在线车辆',
onClick: () => onOpenRealtime({ online: 'online' })
},
{
title: '轨迹回放',
status: `有效定位 ${formatCount(commandLocatedCount)}`,
color: commandLocatedCount > 0 ? 'blue' as const : 'grey' as const,
description: '围绕车辆回放历史位置轨迹,结合高德线路和原始记录定位异常。',
action: '打开轨迹',
onClick: () => onOpenHistory()
},
{
title: '历史数据查询',
status: `今日帧 ${formatCount(summary?.frameToday)}`,
color: 'blue' as const,
description: '查询位置历史和原始记录字段,为车辆问题复盘提供依据。',
action: '查询历史',
onClick: () => onOpenHistory({ tab: 'raw' })
},
{
title: '告警事件触发与通知',
status: `告警 ${formatCount(summary?.issueVehicles)}`,
color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const,
description: '围绕断链、无来源、VIN 缺失、字段缺失形成告警和通知闭环。',
action: '查看告警',
onClick: () => onOpenQuality()
},
{
title: '里程统计',
status: `统计车辆 ${formatCount(serviceSummary?.totalVehicles)}`,
color: 'green' as const,
description: '提供里程等运营统计,保证区间总值与每日统计口径闭合。',
action: '查看统计',
onClick: () => onOpenMileage()
}
];
const vehicleServicePrinciples = [
{
title: '一车一档',
value: formatCount(serviceSummary?.totalVehicles),
detail: '以 VIN 为主对象沉淀车牌、手机号、OEM、绑定状态和来源覆盖。'
},
{
title: '一车一实时',
value: formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles),
detail: '实时状态合并位置、在线、速度、SOC、里程和来源新鲜度。'
},
{
title: '一车一轨迹',
value: formatCount(commandLocatedCount),
detail: '轨迹回放围绕车辆展开,协议来源只作为证据过滤条件。'
},
{
title: '一车一统计',
value: formatCount(serviceSummary?.totalVehicles),
detail: '里程等运营指标按车辆口径闭合,再回溯到来源证据。'
}
];
const workflowSteps = [
{
title: '接入巡检',
value: unhealthyLinkCount > 0 ? `${unhealthyLinkCount.toLocaleString()} 项关注` : '链路正常',
color: unhealthyLinkCount > 0 ? 'orange' as const : 'green' as const,
detail: '检查平台转发、消息队列、缓存和存储等关键链路。',
action: '查看告警',
onClick: () => onOpenQuality()
},
{
title: '实时态势',
value: `${formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles)} 在线`,
color: 'green' as const,
detail: '确认车辆是否在线、是否有有效坐标以及来源是否完整。',
action: '实时监控',
onClick: () => onOpenRealtime({ online: 'online' })
},
{
title: '轨迹复盘',
value: `${formatCount(summary?.activeToday)} 活跃`,
color: 'blue' as const,
detail: '按车辆进入历史轨迹,复盘位置、速度、里程和断点。',
action: '轨迹回放',
onClick: () => onOpenHistory()
},
{
title: '历史证据',
value: `${formatCount(summary?.frameToday)}`,
color: 'blue' as const,
detail: '查询原始记录和字段明细,形成可追溯的数据依据。',
action: '历史查询',
onClick: () => onOpenHistory({ tab: 'raw' })
},
{
title: '告警事件',
value: `${formatCount(summary?.issueVehicles)} 车辆`,
color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const,
detail: '把断链、缺 VIN、字段缺失等问题进入通知和处置队列。',
action: '告警事件',
onClick: () => onOpenQuality()
},
{
title: '统计复核',
value: `${formatCount(serviceSummary?.totalVehicles)} 车辆`,
color: 'green' as const,
detail: '核对里程统计口径,保证区间统计和每日统计闭合。',
action: '里程统计',
onClick: () => onOpenMileage()
}
];
const mapReadiness = [
{
title: '高德 Web JS',
value: amapConfigured ? '已配置' : '待配置',
color: amapConfigured ? 'green' as const : 'orange' as const,
detail: amapConfigured ? '实时监控和轨迹回放可加载高德底图。' : '缺少 Web JS Key 时降级为坐标预览。'
},
{
title: '安全代理',
value: amapSecurityProxyEnabled ? '已启用' : '未启用',
color: amapSecurityProxyEnabled ? 'green' as const : 'orange' as const,
detail: amapSecurityProxyEnabled ? '安全码由服务端代理追加,不下发到浏览器。' : '生产环境建议启用 /_AMapService。'
},
{
title: '服务端 API',
value: amapApiConfigured ? '已配置' : '待配置',
color: amapApiConfigured ? 'green' as const : 'orange' as const,
detail: amapApiConfigured ? '逆地理、路线、围栏等服务端地图能力可扩展。' : '后端地图服务会降级。'
},
{
title: '安全码暴露',
value: amapSecurityCodeExposed ? '需处理' : '未暴露',
color: amapSecurityCodeExposed ? 'red' as const : 'green' as const,
detail: amapSecurityCodeExposed ? '浏览器端可见安全码,需要切换到安全代理。' : '符合生产密钥收敛要求。'
}
];
const mapWorkItems = [
{
title: '实时监控',
value: `${commandLocatedCount.toLocaleString()} 个有效坐标`,
detail: '在线车辆进入地图态势,点击车辆后继续查看车辆服务和实时字段。',
action: '实时地图',
onClick: () => onOpenMap({ online: 'online' })
},
{
title: '轨迹回放',
value: `${formatCount(summary?.activeToday)} 今日活跃`,
detail: '按车辆和时间窗回放历史位置,支持打开高德线路和原始记录。',
action: '轨迹回放',
onClick: () => onOpenHistory()
},
{
title: '历史证据',
value: `${formatCount(summary?.frameToday)} 今日帧`,
detail: '地图点位异常时直接回查位置历史、解析字段和原始帧。',
action: '历史查询',
onClick: () => onOpenHistory({ tab: 'raw', includeFields: 'true' })
},
{
title: '告警通知',
value: `${formatCount(summary?.issueVehicles)} 告警车辆`,
detail: '断链、无来源、坐标异常进入告警队列并触发通知闭环。',
action: '告警事件',
onClick: () => onOpenQuality()
}
];
const operationWorkbench = [
{
title: '实时监控',
value: `${formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles)} 在线`,
meta: `有效定位 ${commandLocatedCount.toLocaleString()}`,
color: 'green' as const,
detail: '面向值班人员确认车辆是否在线、坐标是否有效、来源是否完整。',
actions: [
{ label: '打开实时地图', onClick: () => onOpenMap({ online: 'online' }) },
{ label: '查看在线车辆', onClick: () => onOpenRealtime({ online: 'online' }) }
]
},
{
title: '轨迹回放',
value: `${formatCount(summary?.activeToday)} 今日活跃`,
meta: '高德轨迹证据',
color: 'blue' as const,
detail: '按车辆回看位置、速度、里程和断点,用于定位异常复盘。',
actions: [
{ label: '打开轨迹回放', onClick: () => onOpenHistory() },
{ label: '查原始记录', onClick: () => onOpenHistory({ tab: 'raw', includeFields: 'true' }) }
]
},
{
title: '历史数据查询',
value: `${formatCount(summary?.frameToday)} 今日帧`,
meta: '字段裁剪与导出',
color: 'blue' as const,
detail: '查询位置历史、原始记录和字段明细,为车辆服务提供可追溯依据。',
actions: [
{ label: '查询历史数据', onClick: () => onOpenHistory({ tab: 'location' }) },
{ label: '查询解析字段', onClick: () => onOpenHistory({ tab: 'raw', includeFields: 'true' }) }
]
},
{
title: '告警事件与通知',
value: `${formatCount(summary?.issueVehicles)} 告警车辆`,
meta: highPriorityIssue ? `${highPriorityIssue.severity === 'error' ? 'P0' : 'P1'} ${qualityIssueLabel(highPriorityIssue.issueType)}` : '暂无高优先级',
color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const,
detail: '把断链、无来源、VIN 缺失、字段缺失转成可通知、可闭环的事件。',
actions: [
{ label: '查看告警事件', onClick: () => onOpenQuality() },
{ label: '处理最高优先级', onClick: () => onOpenQuality(highPriorityIssue?.issueType ? { issueType: highPriorityIssue.issueType } : {}) }
]
},
{
title: '里程统计',
value: `${formatCount(serviceSummary?.totalVehicles)} 车辆口径`,
meta: '区间闭合复核',
color: 'green' as const,
detail: '里程等指标先按车辆口径闭合,再回溯轨迹和原始记录。',
actions: [
{ label: '打开里程统计', onClick: () => onOpenMileage() },
{ label: '查看车辆中心', onClick: () => onOpenVehicles({}) }
]
}
];
const timeMonitorScope = (values = timeMonitorFilters) => {
const normalized: Record<string, string> = {};
for (const key of ['keyword', 'protocol', 'dateFrom', 'dateTo'] as const) {
const value = String(values[key] ?? '').trim();
if (value) {
normalized[key] = value;
}
}
return normalized;
};
const openTimeMonitorHistory = () => {
onOpenHistory(timeMonitorScope());
};
const openTimeMonitorMileage = () => {
onOpenMileage(timeMonitorScope());
};
const openTimeMonitorRaw = () => {
onOpenHistory({ ...timeMonitorScope(), tab: 'raw', includeFields: 'true' });
};
const openTimeMonitorAlerts = () => {
const scope = timeMonitorScope();
onOpenQuality({
...(scope.keyword ? { keyword: scope.keyword } : {}),
...(scope.protocol ? { protocol: scope.protocol } : {})
});
};
const timeMonitorSummary = (() => {
const scope = timeMonitorScope();
const vehicle = scope.keyword || '全部车辆';
const source = scope.protocol || '全部数据通道';
const range = scope.dateFrom || scope.dateTo ? `${scope.dateFrom || '-'}${scope.dateTo || '-'}` : '全部时间';
return `${vehicle} / ${source} / ${range}`;
})();
const timeMonitorReviewItems = [
{
label: '车辆范围',
value: timeMonitorScope().keyword || '全部车辆',
detail: timeMonitorScope().protocol || '全部数据通道',
color: timeMonitorScope().keyword ? 'green' as const : 'blue' as const
},
{
label: '复盘时间',
value: timeMonitorScope().dateFrom || timeMonitorScope().dateTo ? `${timeMonitorScope().dateFrom || '-'}${timeMonitorScope().dateTo || '-'}` : '全部时间',
detail: '同步带入轨迹、里程、历史证据和告警',
color: 'blue' as const
},
{
label: '轨迹证据',
value: `${formatCount(summary?.activeToday)} 活跃`,
detail: '位置、速度、里程断点同窗复核',
color: 'blue' as const
},
{
label: '统计证据',
value: `${formatCount(serviceSummary?.totalVehicles)} 车辆`,
detail: '区间里程、日统计和异常点闭合',
color: 'green' as const
},
{
label: '风险事件',
value: `${formatCount(summary?.issueVehicles)} 告警`,
detail: highPriorityIssue ? qualityIssueLabel(highPriorityIssue.issueType) : '暂无高优先级',
color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const
}
];
const copyTimeMonitorReviewPackage = () => {
const scope = timeMonitorScope();
const rawFilters = { ...scope, tab: 'raw', includeFields: 'true' };
const lines = [
'【客户时间窗复盘包】',
`范围:${timeMonitorSummary}`,
'平台能力:实时地图 / 轨迹回放 / 里程统计 / 历史数据查询 / 告警通知',
`车辆范围:${scope.keyword || '全部车辆'}`,
`数据通道:${scope.protocol || '全部数据通道'}`,
`时间范围:${scope.dateFrom || '-'}${scope.dateTo || '-'}`,
`轨迹证据:今日活跃 ${formatCount(summary?.activeToday)},用于回放位置、速度、里程断点`,
`统计证据:车辆口径 ${formatCount(serviceSummary?.totalVehicles)},用于区间里程和日统计闭合`,
`历史证据:今日数据 ${formatCount(summary?.frameToday)},用于 RAW 与解析字段复核`,
`告警事件:${formatCount(summary?.issueVehicles)} 辆车存在告警`,
`轨迹回放:${appURL(buildAppHash({ page: 'history', keyword: scope.keyword, protocol: scope.protocol, filters: scope }))}`,
`里程统计:${appURL(buildAppHash({ page: 'mileage', keyword: scope.keyword, protocol: scope.protocol, filters: scope }))}`,
`历史数据查询:${appURL(buildAppHash({ page: 'history-query', keyword: scope.keyword, protocol: scope.protocol, filters: rawFilters }))}`,
`告警通知:${appURL(buildAppHash({ page: 'alert-events', keyword: scope.keyword, protocol: scope.protocol, filters: scope }))}`,
`实时地图:${appURL(buildAppHash({ page: 'map', keyword: scope.keyword, protocol: scope.protocol, filters: scope }))}`
];
copyText(lines.join('\n'), '时间窗复盘包');
};
const timeMonitorWorkbench = [
{
label: '轨迹回放',
value: `${formatCount(summary?.activeToday)} 活跃`,
detail: '查看时间窗内车辆位置、速度、里程和断点。',
action: '打开轨迹',
color: 'blue' as const,
onClick: openTimeMonitorHistory
},
{
label: '里程核对',
value: `${formatCount(serviceSummary?.totalVehicles)} 车辆`,
detail: '用同一范围核对区间里程和日统计闭合。',
action: '核对里程',
color: 'green' as const,
onClick: openTimeMonitorMileage
},
{
label: '历史证据',
value: `${formatCount(summary?.frameToday)}`,
detail: '按车辆、时间和字段裁剪导出 RAW 与解析字段。',
action: '导出证据',
color: 'blue' as const,
onClick: openTimeMonitorRaw
},
{
label: '告警复盘',
value: `${formatCount(summary?.issueVehicles)} 告警`,
detail: '查看该车辆或来源在当前范围内的风险事件。',
action: '查看告警',
color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const,
onClick: openTimeMonitorAlerts
}
];
const dataFlowStages = [
{
stage: '01',
title: '协议接入',
owner: '网关接收',
evidence: `32960 / 808 / MQTT连接 ${formatCount(opsHealth?.activeConnections)}`,
detail: '只接收真实实时数据帧和必要鉴权注册帧,协议不是最终产品。',
action: '运维质量',
onClick: () => onOpenQuality()
},
{
stage: '02',
title: '统一解析',
owner: '字段映射',
evidence: `今日帧 ${formatCount(summary?.frameToday)}Kafka Lag ${formatLag(summary?.kafkaLag)}`,
detail: '一次解析生成扁平字段、原始证据和车辆身份关联,避免三处重复解析。',
action: '历史字段',
onClick: () => onOpenHistory({ tab: 'raw', includeFields: 'true' })
},
{
stage: '03',
title: '实时投影',
owner: 'Redis KV',
evidence: `在线 Key ${formatCount(opsHealth?.redisOnlineKeys)},在线车辆 ${formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles)}`,
detail: '快速写入车辆最新状态、在线 TTL 和全量协议字段,支撑 0-1 秒监控。',
action: '实时监控',
onClick: () => onOpenRealtime({ online: 'online' })
},
{
stage: '04',
title: '车辆归并',
owner: 'Vehicle Service',
evidence: `多源 ${formatCount(serviceSummary?.multiSourceVehicles)},单源 ${formatCount(serviceSummary?.singleSourceVehicles)}`,
detail: '以 VIN 合并来源证据,车牌、手机号和 OEM 只是车辆服务的检索键。',
action: '车辆中心',
onClick: () => onOpenVehicles({})
},
{
stage: '05',
title: '历史证据',
owner: 'TDengine / MySQL',
evidence: `轨迹 ${formatCount(summary?.activeToday)} 活跃RAW ${formatCount(summary?.frameToday)}`,
detail: '位置、RAW 和解析字段可分页查询,并能回跳轨迹、车辆服务和告警。',
action: '轨迹回放',
onClick: () => onOpenHistory()
},
{
stage: '06',
title: '运营闭环',
owner: '告警 / 统计',
evidence: `告警 ${formatCount(summary?.issueVehicles)},统计车辆 ${formatCount(serviceSummary?.totalVehicles)}`,
detail: '断链、缺字段、里程异常和离线时长进入通知、统计和复核工作流。',
action: '里程统计',
onClick: () => onOpenMileage()
}
];
const scenarioNavigation = [
{
title: '实时监控',
objective: '确认车辆是否在线、位置是否可信、三类来源是否齐全。',
evidence: `Redis 在线态 / 最新定位 / 有效坐标 ${commandLocatedCount.toLocaleString()}`,
sla: '目标 0-1 秒内进入实时视图',
primaryAction: '实时监控',
secondaryAction: '地图态势',
onPrimary: () => onOpenRealtime({ online: 'online' }),
onSecondary: () => onOpenMap({ online: 'online' })
},
{
title: '轨迹回放',
objective: '按车辆复盘位置、速度、里程和断点,定位平台转发或车辆异常。',
evidence: `高德轨迹底座 / 今日活跃 ${formatCount(summary?.activeToday)}`,
sla: '支持按车辆和时间窗快速回放',
primaryAction: '轨迹回放',
secondaryAction: '原始记录',
onPrimary: () => onOpenHistory(),
onSecondary: () => onOpenHistory({ tab: 'raw', includeFields: 'true' })
},
{
title: '历史数据查询',
objective: '围绕车辆查询位置、原始记录和字段明细,给 BI、运维和业务复盘提供依据。',
evidence: `今日数据 ${formatCount(summary?.frameToday)} / 支持字段裁剪与导出`,
sla: '优先返回必要字段,避免大 JSON 拖慢查询',
primaryAction: '历史查询',
secondaryAction: '字段证据',
onPrimary: () => onOpenHistory({ tab: 'location' }),
onSecondary: () => onOpenHistory({ tab: 'raw', includeFields: 'true' })
},
{
title: '告警事件触发和通知',
objective: '把断链、无来源、VIN 缺失、字段缺失转换为可通知、可闭环事件。',
evidence: `告警车辆 ${formatCount(summary?.issueVehicles)} / 最高优先级 ${highPriorityIssue ? qualityIssueLabel(highPriorityIssue.issueType) : '暂无'}`,
sla: 'P0 进入通知队列,超时升级',
primaryAction: '告警事件',
secondaryAction: '通知规则',
onPrimary: () => onOpenQuality(),
onSecondary: () => onOpenQuality(highPriorityIssue?.issueType ? { issueType: highPriorityIssue.issueType } : {})
},
{
title: '里程统计',
objective: '按车辆口径查询里程等指标,保证区间统计和日统计可闭合。',
evidence: `车辆口径 ${formatCount(serviceSummary?.totalVehicles)} / 区间统计可追溯轨迹证据`,
sla: '统计值必须能追溯轨迹和原始记录',
primaryAction: '里程统计',
secondaryAction: '车辆中心',
onPrimary: () => onOpenMileage(),
onSecondary: () => onOpenVehicles({})
}
];
const copyOperationsHandoff = () => {
const unhealthyLinks = (summary?.linkHealth ?? []).filter((item) => item.status !== 'ok');
const priorityAction = serviceActionQueue[0];
const highPriorityLookupKey = highPriorityLookup?.key;
const highPriorityFilters = highPriorityEvidenceFilters;
const lines = [
'【车辆数据中台运营交接摘要】',
`在线车辆:${formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles)} / ${formatCount(serviceSummary?.totalVehicles)}`,
`今日活跃:${formatCount(summary?.activeToday)}`,
`今日帧量:${formatCount(summary?.frameToday)}`,
`实时地图有效定位:${commandLocatedCount.toLocaleString()}`,
`告警车辆:${formatCount(summary?.issueVehicles)}`,
`高德地图Web JS ${amapConfigured ? '已配置' : '待配置'} / 服务端 API ${amapApiConfigured ? '已配置' : '待配置'} / 安全代理 ${amapSecurityProxyEnabled ? '已启用' : '未启用'}`,
`优先动作:${priorityAction ? `${priorityAction.label} ${priorityAction.count.toLocaleString()}辆 - ${priorityAction.detail}` : '暂无待办'}`,
highPriorityIssue ? `最高告警:${highPriorityIssue.severity === 'error' ? 'P0' : 'P1'} ${qualityIssueLabel(highPriorityIssue.issueType)} / ${priorityIssueVehicleLabel(highPriorityIssue)} / ${highPriorityIssue.lastSeen || '-'}` : '最高告警:暂无',
highPriorityIssue ? `告警详情:${highPriorityIssue.detail || '-'}` : '',
`实时监控:${appURL(buildAppHash({ page: 'realtime', filters: { online: 'online' } }))}`,
`实时地图:${appURL(buildAppHash({ page: 'map', filters: { online: 'online' } }))}`,
`轨迹回放:${appURL(buildAppHash({ page: 'history', keyword: highPriorityFilters?.keyword, protocol: highPriorityFilters?.protocol, filters: highPriorityFilters }))}`,
`原始记录:${appURL(buildAppHash({ page: 'history-query', keyword: highPriorityFilters?.keyword, protocol: highPriorityFilters?.protocol, filters: { ...highPriorityFilters, tab: 'raw', includeFields: 'true' } }))}`,
`告警事件:${appURL(buildAppHash({ page: 'alert-events', filters: highPriorityIssue?.issueType ? { issueType: highPriorityIssue.issueType } : {} }))}`,
`里程统计:${appURL(buildAppHash({ page: 'mileage', keyword: highPriorityLookupKey, protocol: highPriorityIssue?.protocol }))}`,
`车辆服务:${appURL(buildAppHash({ page: 'detail', keyword: highPriorityLookupKey, protocol: highPriorityIssue?.protocol }))}`
].filter(Boolean);
copyText(lines.join('\n'), '运营交接摘要');
};
const copyScenarioBlueprint = () => {
const lines = [
'【车辆数据中台功能蓝图】',
'定位32960 / 808 / 宇通 MQTT 都只是车辆服务证据源,最终围绕一辆车提供实时、轨迹、历史、告警、统计能力。',
`高德地图Web JS ${amapConfigured ? '已配置' : '待配置'};服务端 API ${amapApiConfigured ? '已配置' : '待配置'};安全代理 ${amapSecurityProxyEnabled ? '已启用' : '未启用'};安全码${amapSecurityCodeExposed ? '已暴露' : '未暴露'}`,
`车辆规模:${formatCount(serviceSummary?.totalVehicles)};在线:${formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles)};告警:${formatCount(summary?.issueVehicles)}`,
'',
...scenarioNavigation.map((item, index) => [
`${index + 1}. ${item.title}`,
` 目标:${item.objective}`,
` 证据:${item.evidence}`,
` SLA${item.sla}`
].join('\n')),
'',
`入口:${appURL(buildAppHash({ page: 'dashboard' }))}`
];
copyText(lines.join('\n'), '功能蓝图');
};
const copyDataFlowBlueprint = () => {
const lines = [
'【车辆数据中台数据流转图】',
'原则32960 / 808 / 宇通 MQTT 是接入证据,最终都归并为同一辆车的车辆服务。',
`运行态:连接 ${formatCount(opsHealth?.activeConnections)};在线 Key ${formatCount(opsHealth?.redisOnlineKeys)}Kafka Lag ${formatLag(summary?.kafkaLag)};今日帧 ${formatCount(summary?.frameToday)}`,
'',
...dataFlowStages.map((item) => [
`${item.stage}. ${item.title} / ${item.owner}`,
` 证据:${item.evidence}`,
` 说明:${item.detail}`
].join('\n')),
'',
`实时监控:${appURL(buildAppHash({ page: 'realtime', filters: { online: 'online' } }))}`,
`车辆中心:${appURL(buildAppHash({ page: 'vehicles' }))}`,
`轨迹回放:${appURL(buildAppHash({ page: 'history' }))}`,
`历史查询:${appURL(buildAppHash({ page: 'history-query', filters: { tab: 'raw', includeFields: 'true' } }))}`,
`告警事件:${appURL(buildAppHash({ page: 'alert-events' }))}`,
`里程统计:${appURL(buildAppHash({ page: 'mileage' }))}`,
`运维质量:${appURL(buildAppHash({ page: 'ops-quality' }))}`
];
copyText(lines.join('\n'), '数据流转图');
};
const copyFocusVehicleService = () => {
if (!focusVehicle) {
Toast.warning('当前没有重点车辆服务可复制');
return;
}
const commonFilters = { keyword: focusVehicle.lookupKey, protocol: focusVehicle.protocol ?? '' };
const lines = [
'【重点车辆服务处置卡】',
`车辆:${focusVehicle.label}`,
`原因:${focusVehicle.reason}`,
`服务状态:${focusVehicle.statusLabel}`,
`实时证据:${focusVehicle.realtimeEvidence}`,
`轨迹证据:${focusVehicle.historyEvidence}`,
`告警证据:${focusVehicle.alertEvidence}`,
`统计证据:${focusVehicle.statisticEvidence}`,
`车辆服务:${appURL(buildAppHash({ page: 'detail', keyword: focusVehicle.lookupKey, protocol: focusVehicle.protocol }))}`,
`实时监控:${appURL(buildAppHash({ page: 'realtime', keyword: focusVehicle.lookupKey, protocol: focusVehicle.protocol }))}`,
`轨迹回放:${appURL(buildAppHash({ page: 'history', keyword: focusVehicle.lookupKey, protocol: focusVehicle.protocol }))}`,
`原始记录:${appURL(buildAppHash({ page: 'history-query', keyword: focusVehicle.lookupKey, protocol: focusVehicle.protocol, filters: { ...commonFilters, tab: 'raw', includeFields: 'true' } }))}`,
`里程统计:${appURL(buildAppHash({ page: 'mileage', keyword: focusVehicle.lookupKey, protocol: focusVehicle.protocol }))}`,
`告警事件:${appURL(buildAppHash({ page: 'alert-events', keyword: focusVehicle.lookupKey, protocol: focusVehicle.protocol, filters: focusVehicle.issueType ? { issueType: focusVehicle.issueType } : {} }))}`
];
copyText(lines.join('\n'), '重点车辆服务处置卡');
};
const buildDashboardSnapshotRows = () => {
const unhealthyLinks = (summary?.linkHealth ?? []).filter((item) => item.status !== 'ok');
const priorityAction = serviceActionQueue[0];
const rows: DashboardSnapshotRow[] = [
{
section: '车辆服务',
item: '在线车辆',
value: `${formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles)} / ${formatCount(serviceSummary?.totalVehicles)}`,
detail: '统一车辆服务在线态势'
},
{
section: '车辆服务',
item: '今日活跃',
value: formatCount(summary?.activeToday),
detail: '当天有有效上报或历史记录的车辆服务对象'
},
{
section: '实时监控',
item: '有效定位',
value: commandLocatedCount.toLocaleString(),
detail: `当前预览 ${locations.length.toLocaleString()} 条车辆实时数据`
},
{
section: '实时监控',
item: '来源异常',
value: commandDegradedCount.toLocaleString(),
detail: '当前预览中离线或来源不完整车辆'
},
{
section: '历史数据',
item: '今日帧量',
value: formatCount(summary?.frameToday),
detail: '历史查询和导出可按车辆、时间、字段裁剪'
},
{
section: '告警事件',
item: '告警车辆',
value: formatCount(summary?.issueVehicles),
detail: highPriorityIssue ? `${qualityIssueLabel(highPriorityIssue.issueType)} / ${priorityIssueVehicleLabel(highPriorityIssue)}` : '暂无最高优先级告警'
},
{
section: '地图能力',
item: '高德地图',
value: amapConfigured ? '已配置' : '待配置',
detail: '用于实时监控和轨迹回放地图底座'
},
{
section: '优先动作',
item: priorityAction?.label ?? '暂无待办',
value: priorityAction ? priorityAction.count.toLocaleString() : '0',
detail: priorityAction?.detail ?? '当前没有必须立即处理的车辆服务事项'
}
];
(serviceSummary?.serviceStatuses ?? summary?.serviceStatuses ?? []).forEach((item) => {
rows.push({
section: '服务状态',
item: item.title || serviceStatusTitle[item.status] || item.status,
value: item.count.toLocaleString(),
detail: serviceStatusTitle[item.status] || item.status
});
});
return rows;
};
const exportDashboardSnapshot = () => {
const rows = buildDashboardSnapshotRows();
if (rows.length === 0) {
Toast.warning('当前没有可导出的驾驶舱摘要');
return;
}
downloadCsv('dashboard-snapshot.csv', buildCsv(dashboardSnapshotColumns, rows));
Toast.success(`已导出 ${rows.length.toLocaleString()} 条驾驶舱摘要`);
};
const commandCenterItems = [
{
title: '全域车辆监控',
value: `${formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles)} 在线`,
detail: `有效定位 ${commandLocatedCount.toLocaleString()} 辆,先从地图确认车辆分布、在线状态和最新位置。`,
color: 'green' as const,
primaryAction: '打开车辆地图',
secondaryAction: '在线车辆',
onPrimary: () => onOpenMap({ online: 'online' }),
onSecondary: () => onOpenVehicles({ online: 'online' })
},
{
title: '异常车辆闭环',
value: `${formatCount(summary?.issueVehicles)} 告警`,
detail: highPriorityIssue ? `最高优先级:${qualityIssueLabel(highPriorityIssue.issueType)} / ${priorityIssueVehicleLabel(highPriorityIssue)}` : '当前没有高优先级告警,保持实时巡检。',
color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const,
primaryAction: '告警事件',
secondaryAction: '通知规则',
onPrimary: () => onOpenQuality(highPriorityIssue?.issueType ? { issueType: highPriorityIssue.issueType } : {}),
onSecondary: () => onOpenQuality(highPriorityIssue?.issueType ? { issueType: highPriorityIssue.issueType } : {})
},
{
title: '自定义时间复盘',
value: taskTimeMonitorSummary,
detail: '一个时间窗贯穿轨迹回放、里程统计、历史查询和告警复盘,适合客户问询与 BI 核对。',
color: 'blue' as const,
primaryAction: '轨迹复盘',
secondaryAction: '里程统计',
onPrimary: () => openTimeMonitorHistory(),
onSecondary: () => openTimeMonitorMileage()
},
{
title: '数据交付与导出',
value: `${formatCount(summary?.frameToday)} 今日数据`,
detail: '按车辆、时间、字段裁剪导出位置、原始记录和字段证据,交付给业务或客户复盘。',
color: 'blue' as const,
primaryAction: '历史导出',
secondaryAction: '导出概览',
onPrimary: () => openTimeMonitorRaw(),
onSecondary: () => exportDashboardSnapshot()
}
];
return (
<div className="vp-page">
<PageHeader title="车辆运营监控台" description="面向客户的车辆地图、实时状态、轨迹回放、里程统计、历史查询和数据导出入口" />
<Spin spinning={loading}>
<Card bordered className="vp-customer-hero" bodyStyle={{ padding: 0 }}>
<div className="vp-customer-hero-map">
<div className="vp-customer-hero-copy">
<Typography.Title heading={3} style={{ margin: 0 }}></Typography.Title>
<Typography.Text type="secondary">
32960808 MQTT 线
</Typography.Text>
<Space wrap>
<Button theme="solid" type="primary" onClick={() => onOpenMap({ online: 'online' })}></Button>
<Button theme="light" type="primary" onClick={() => onOpenHistory()}></Button>
<Button theme="light" type="primary" onClick={() => onOpenMileage({})}></Button>
<Button theme="light" onClick={exportDashboardSnapshot}></Button>
</Space>
</div>
<VehicleMap
points={commandMapPoints}
maxFallbackPoints={80}
heightClassName="vp-customer-hero-map-canvas"
fallbackLabel="显示车辆实时坐标预览"
/>
</div>
<div className="vp-customer-hero-side">
<div className="vp-customer-hero-side-head">
<Tag color={amapConfigured ? 'green' : 'orange'}>{amapConfigured ? '地图可用' : '地图待配置'}</Tag>
<Tag color={(summary?.issueVehicles ?? 0) > 0 ? 'orange' : 'green'}>{formatCount(summary?.issueVehicles)} </Tag>
</div>
<div className="vp-customer-health-grid">
{customerHealthItems.map((item) => (
<button key={item.label} type="button" className="vp-customer-health-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>
</div>
</Card>
<Card bordered title="车辆监控指挥台" style={{ marginBottom: 16 }}>
<div className="vp-command-center-grid">
{commandCenterItems.map((item) => (
<div key={item.title} className="vp-command-center-item">
<div className="vp-command-center-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"
aria-label={`车辆监控指挥台 ${item.title} ${item.primaryAction}`}
onClick={item.onPrimary}
>
{item.primaryAction}
</Button>
<Button
size="small"
theme="light"
type="primary"
aria-label={`车辆监控指挥台 ${item.title} ${item.secondaryAction}`}
onClick={item.onSecondary}
>
{item.secondaryAction}
</Button>
</Space>
</div>
))}
</div>
</Card>
<Card bordered title="客户常用工作流" style={{ marginBottom: 16 }}>
<div className="vp-customer-workflow-grid">
{customerHeroActions.map((item) => (
<button key={item.title} type="button" className="vp-customer-workflow-item" onClick={item.onClick} aria-label={`客户工作流 ${item.title}`}>
<div>
<Typography.Title heading={6} style={{ margin: 0 }}>{item.title}</Typography.Title>
<Typography.Text type="secondary">{item.detail}</Typography.Text>
</div>
<strong>{item.value}</strong>
<Tag color="blue">{item.action}</Tag>
</button>
))}
</div>
</Card>
<Card bordered title="今日车辆服务任务板" style={{ marginBottom: 16 }}>
<div className="vp-customer-task-grid">
{customerTaskBoard.map((item) => (
<div key={item.title} className="vp-customer-task-item">
<div className="vp-customer-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.secondaryAction !== '复制处置卡'}
aria-label={`今日任务 ${item.title} ${item.secondaryAction}`}
onClick={item.onSecondary}
>
{item.secondaryAction}
</Button>
</Space>
</div>
))}
</div>
</Card>
<div className="vp-kpi-grid">
{kpis.map((item) => (
<Card key={item.label} bordered className="vp-kpi-card" bodyStyle={{ padding: 0 }}>
<button className="vp-kpi-button" type="button" onClick={() => onOpenVehicles(item.filters)} aria-label={`${item.label} ${item.value}`}>
<div className="vp-kpi-value">{item.value}</div>
<div className="vp-kpi-label">{item.label}</div>
</button>
</Card>
))}
</div>
<Card bordered title="车辆服务入口" style={{ marginBottom: 16 }}>
<Space wrap>
<Tag color="blue">{vehicleServiceOnlineText(serviceSummary, summary)}</Tag>
<Button size="small" theme="light" type="primary" onClick={() => onOpenVehicles({ online: 'online' })}>线</Button>
<Tag color="blue"> {commandLocatedCount.toLocaleString()}</Tag>
<Button size="small" theme="light" type="primary" onClick={() => onOpenMap({ online: 'online' })}></Button>
<Tag color="blue"> {formatCount(summary?.activeToday)}</Tag>
<Button size="small" theme="light" type="primary" onClick={() => onOpenHistory()}></Button>
<Tag color="blue"> {formatCount(summary?.frameToday)}</Tag>
<Button size="small" theme="light" type="primary" onClick={() => onOpenHistory({ tab: 'raw', includeFields: 'true' })}></Button>
<Tag color={(summary?.issueVehicles ?? 0) > 0 ? 'orange' : 'green'}>
{formatCount(summary?.issueVehicles)}
</Tag>
<Button size="small" theme="light" type={(summary?.issueVehicles ?? 0) > 0 ? 'warning' : 'tertiary'} onClick={() => onOpenQuality()}></Button>
<Button size="small" theme="solid" type="primary" onClick={copyOperationsHandoff}></Button>
<Button size="small" theme="light" type="primary" onClick={exportDashboardSnapshot}> CSV</Button>
</Space>
</Card>
<Card
bordered
title={<Space><span></span><Tag color="blue">{timeMonitorSummary}</Tag></Space>}
style={{ marginBottom: 16 }}
>
<Form
initValues={timeMonitorFilters}
layout="horizontal"
onSubmit={(values) => setTimeMonitorFilters(timeMonitorScope(values as Record<string, string>))}
style={{ marginBottom: 12 }}
>
<Form.Input field="keyword" label="车辆" placeholder="VIN / 车牌 / 手机号" style={{ width: 240 }} />
<Form.Select field="protocol" label="数据通道" placeholder="全部通道" style={{ width: 150 }}>
<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.Input field="dateFrom" label="开始日期" placeholder="2026-07-01" style={{ width: 150 }} />
<Form.Input field="dateTo" label="结束日期" placeholder="2026-07-05" style={{ width: 150 }} />
<Space>
<Button htmlType="submit" theme="solid" type="primary"></Button>
<Button onClick={() => {
const today = todayDateString();
setTimeMonitorFilters({ dateFrom: previousDateString(today), dateTo: today });
}}></Button>
</Space>
</Form>
<div className="vp-time-monitor-actions">
<Button theme="solid" type="primary" onClick={openTimeMonitorHistory}></Button>
<Button theme="light" type="primary" onClick={openTimeMonitorMileage}></Button>
<Button theme="light" type="primary" onClick={openTimeMonitorRaw}></Button>
<Button theme="light" type="warning" onClick={openTimeMonitorAlerts}></Button>
</div>
<Typography.Text type="secondary">
</Typography.Text>
<div className="vp-time-review-package">
<div className="vp-time-review-summary">
<Space wrap>
<Tag color="blue"></Tag>
<Tag color={timeMonitorScope().keyword ? 'green' : 'grey'}>{timeMonitorScope().keyword ? '单车复盘' : '全域复盘'}</Tag>
</Space>
<Typography.Title heading={5} style={{ margin: 0 }}>{timeMonitorSummary}</Typography.Title>
<Typography.Text type="secondary">
</Typography.Text>
<Space wrap>
<Button size="small" theme="solid" type="primary" onClick={copyTimeMonitorReviewPackage}></Button>
<Button size="small" theme="light" type="primary" onClick={openTimeMonitorHistory}></Button>
<Button size="small" theme="light" type="primary" onClick={openTimeMonitorMileage}></Button>
<Button size="small" theme="light" type="primary" onClick={openTimeMonitorRaw}></Button>
<Button size="small" theme="light" type="warning" onClick={openTimeMonitorAlerts}></Button>
</Space>
</div>
<div className="vp-time-review-grid">
{timeMonitorReviewItems.map((item) => (
<div key={item.label} className="vp-time-review-item">
<Tag color={item.color}>{item.label}</Tag>
<strong>{item.value}</strong>
<span>{item.detail}</span>
</div>
))}
</div>
</div>
<div className="vp-time-monitor-workbench">
<div className="vp-time-monitor-scope">
<Space wrap>
<Tag color="blue"></Tag>
<Tag color={timeMonitorScope().keyword ? 'green' : 'grey'}>{timeMonitorScope().keyword ? '单车' : '全域'}</Tag>
</Space>
<strong>{timeMonitorSummary}</strong>
<Typography.Text type="secondary">
BI
</Typography.Text>
</div>
<div className="vp-time-monitor-work-grid">
{timeMonitorWorkbench.map((item) => (
<button
key={item.label}
type="button"
className="vp-time-monitor-work-item"
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-time-monitor-grid">
{[
{ title: '今天车辆轨迹', detail: '查看当天活跃车辆的轨迹、速度和里程断点。', action: '查今天', onClick: openTimeMonitorHistory },
{ title: '历史时间窗', detail: '按任意起止时间查询位置历史、原始记录和字段明细。', action: '自定义查询', onClick: openTimeMonitorRaw },
{ title: '区间里程', detail: '按车辆和时间范围核对区间里程、日统计和异常点。', action: '里程统计', onClick: openTimeMonitorMileage },
{ title: '告警复盘', detail: '按车辆或事件回放告警发生前后的轨迹和数据证据。', action: '告警事件', onClick: openTimeMonitorAlerts }
].map((item) => (
<button key={item.title} type="button" className="vp-time-monitor-item" onClick={item.onClick} aria-label={`时间监控 ${item.title}`}>
<div>
<Typography.Title heading={6} style={{ margin: 0 }}>{item.title}</Typography.Title>
<Typography.Text type="secondary">{item.detail}</Typography.Text>
</div>
<Tag color="blue">{item.action}</Tag>
</button>
))}
</div>
</Card>
<Card bordered title="地图运营能力" style={{ marginBottom: 16 }}>
<div className="vp-map-ops-board">
<div className="vp-map-ops-readiness">
{mapReadiness.map((item) => (
<div key={item.title} className="vp-map-ops-readiness-item">
<Tag color={item.color}>{item.title}</Tag>
<div className="vp-map-ops-value">{item.value}</div>
<Typography.Text type="secondary">{item.detail}</Typography.Text>
</div>
))}
</div>
<div className="vp-map-ops-work">
{mapWorkItems.map((item) => (
<button key={item.title} className="vp-map-ops-work-item" type="button" onClick={item.onClick} aria-label={`地图运营 ${item.title}`}>
<div className="vp-map-ops-work-head">
<Typography.Title heading={6} style={{ margin: 0 }}>{item.title}</Typography.Title>
<Tag color="blue">{item.action}</Tag>
</div>
<div className="vp-map-ops-work-value">{item.value}</div>
<Typography.Text type="secondary">{item.detail}</Typography.Text>
</button>
))}
</div>
</div>
</Card>
{focusVehicle ? (
<Card
bordered
title={<Space><span></span><Tag color={focusVehicle.statusColor}>{focusVehicle.statusLabel}</Tag><Button size="small" onClick={copyFocusVehicleService}></Button></Space>}
style={{ marginBottom: 16 }}
>
<div className="vp-focus-service">
<div className="vp-focus-service-main">
<Tag color="blue">{focusVehicle.reason}</Tag>
<Typography.Title heading={5} style={{ margin: '8px 0 4px' }}>{focusVehicle.label}</Typography.Title>
<Typography.Text type="secondary"></Typography.Text>
</div>
<div className="vp-focus-service-evidence">
{[
{ label: '实时证据', value: focusVehicle.realtimeEvidence },
{ label: '轨迹证据', value: focusVehicle.historyEvidence },
{ label: '告警证据', value: focusVehicle.alertEvidence },
{ label: '统计证据', value: focusVehicle.statisticEvidence }
].map((item) => (
<div key={item.label} className="vp-focus-service-evidence-item">
<span>{item.label}</span>
<strong>{item.value}</strong>
</div>
))}
</div>
<Space wrap>
<Button size="small" theme="solid" type="primary" aria-label="重点车辆 车辆服务" disabled={!focusVehicle.lookupKey} onClick={() => focusVehicle.lookupKey && onOpenVehicle(focusVehicle.lookupKey, focusVehicle.protocol)}></Button>
<Button size="small" theme="light" aria-label="重点车辆 实时监控" disabled={!focusVehicle.lookupKey} onClick={() => onOpenRealtime({ keyword: focusVehicle.lookupKey, protocol: focusVehicle.protocol ?? '' })}></Button>
<Button size="small" theme="light" aria-label="重点车辆 轨迹回放" disabled={!focusVehicle.lookupKey} onClick={() => onOpenHistory({ keyword: focusVehicle.lookupKey, protocol: focusVehicle.protocol ?? '' })}></Button>
<Button size="small" theme="light" aria-label="重点车辆 原始记录" disabled={!focusVehicle.lookupKey} onClick={() => onOpenHistory({ keyword: focusVehicle.lookupKey, protocol: focusVehicle.protocol ?? '', tab: 'raw', includeFields: 'true' })}></Button>
<Button size="small" theme="light" aria-label="重点车辆 里程统计" disabled={!focusVehicle.lookupKey} onClick={() => onOpenMileage({ keyword: focusVehicle.lookupKey, protocol: focusVehicle.protocol ?? '' })}></Button>
<Button size="small" theme="light" type="warning" aria-label="重点车辆 告警事件" onClick={() => onOpenQuality({ keyword: focusVehicle.lookupKey, protocol: focusVehicle.protocol ?? '', ...(focusVehicle.issueType ? { issueType: focusVehicle.issueType } : {}) })}></Button>
</Space>
</div>
</Card>
) : null}
<Card bordered title="车辆运营工作台" style={{ marginBottom: 16 }}>
<div className="vp-workbench-grid">
{operationWorkbench.map((item) => (
<div key={item.title} className="vp-workbench-item">
<div className="vp-workbench-head">
<Tag color={item.color}>{item.title}</Tag>
<Typography.Text type="secondary">{item.meta}</Typography.Text>
</div>
<div className="vp-workbench-value">{item.value}</div>
<Typography.Text type="secondary">{item.detail}</Typography.Text>
<Space wrap>
{item.actions.map((action) => (
<Button
key={action.label}
size="small"
theme="light"
type="primary"
aria-label={`作业台 ${item.title} ${action.label}`}
onClick={action.onClick}
>
{action.label}
</Button>
))}
</Space>
</div>
))}
</div>
</Card>
<Card bordered title="一车一服务模型" style={{ marginBottom: 16 }}>
<div className="vp-service-model-grid">
{vehicleServicePrinciples.map((item) => (
<div key={item.title} className="vp-service-model-item">
<Tag color="blue">{item.title}</Tag>
<div className="vp-service-model-value">{item.value}</div>
<Typography.Text type="secondary">{item.detail}</Typography.Text>
</div>
))}
</div>
</Card>
<Card
bordered
title={<Space><span></span><Button size="small" onClick={copyScenarioBlueprint}></Button></Space>}
style={{ marginBottom: 16 }}
>
<div className="vp-scenario-grid">
{scenarioNavigation.map((item) => (
<div key={item.title} className="vp-scenario-item">
<div className="vp-scenario-main">
<Typography.Title heading={6} style={{ margin: 0 }}>{item.title}</Typography.Title>
<Typography.Text type="secondary">{item.objective}</Typography.Text>
</div>
<div className="vp-scenario-evidence">{item.evidence}</div>
<Tag color="blue">{item.sla}</Tag>
<Space wrap>
<Button size="small" theme="solid" type="primary" aria-label={`场景导航 ${item.title} ${item.primaryAction}`} onClick={item.onPrimary}>
{item.primaryAction}
</Button>
<Button size="small" theme="light" aria-label={`场景导航 ${item.title} ${item.secondaryAction}`} onClick={item.onSecondary}>
{item.secondaryAction}
</Button>
</Space>
</div>
))}
</div>
</Card>
{highPriorityIssue ? (
<Card bordered title="最高优先级告警" style={{ marginBottom: 16 }}>
<div className="vp-priority-alert">
<div className="vp-priority-alert-main">
<Space wrap>
<Tag color={highPriorityIssue.severity === 'error' ? 'red' : 'orange'}>
{highPriorityIssue.severity === 'error' ? 'P0' : 'P1'}
</Tag>
<Tag color="orange">{qualityIssueLabel(highPriorityIssue.issueType)}</Tag>
<Tag color="blue">{qualityProtocolLabel(highPriorityIssue.protocol)}</Tag>
<Tag color="grey">{highPriorityIssue.lastSeen || '-'}</Tag>
</Space>
<Typography.Title heading={6} style={{ margin: '10px 0 4px' }}>
{priorityIssueVehicleLabel(highPriorityIssue)}
</Typography.Title>
<Typography.Text type="secondary">{highPriorityIssue.detail || '暂无告警详情'}</Typography.Text>
</div>
<Space wrap>
<Button
size="small"
theme="solid"
type="primary"
disabled={!highPriorityLookup?.key}
onClick={() => highPriorityLookup?.key && onOpenVehicle(highPriorityLookup.key, highPriorityIssue.protocol)}
>
</Button>
<Button
size="small"
theme="light"
aria-label="首页告警轨迹证据"
disabled={!highPriorityEvidenceFilters?.keyword}
onClick={() => highPriorityEvidenceFilters && onOpenHistory(highPriorityEvidenceFilters)}
>
</Button>
<Button
size="small"
theme="light"
disabled={!highPriorityEvidenceFilters?.keyword}
onClick={() => highPriorityEvidenceFilters && onOpenHistory({ ...highPriorityEvidenceFilters, tab: 'raw', includeFields: 'true' })}
>
</Button>
<Button
size="small"
theme="light"
aria-label="首页复制告警通知"
onClick={() => copyText(priorityIssueNotificationText(highPriorityIssue), '告警通知')}
>
</Button>
<Button size="small" theme="light" type="warning" onClick={() => onOpenQuality({ issueType: highPriorityIssue.issueType })}>
</Button>
</Space>
</div>
</Card>
) : null}
<Card bordered title="车辆运营能力矩阵" style={{ marginBottom: 16 }}>
<div className="vp-capability-grid">
{capabilities.map((item) => (
<div key={item.title} className="vp-capability-item">
<div>
<Typography.Title heading={6} style={{ margin: 0 }}>{item.title}</Typography.Title>
<Typography.Text type="secondary">{item.description}</Typography.Text>
</div>
<Tag className="vp-capability-status" color={item.color}>{item.status}</Tag>
<Button size="small" theme="light" type="primary" aria-label={`能力入口 ${item.title}`} onClick={item.onClick}>
{item.action}
</Button>
</div>
))}
</div>
</Card>
<Card
bordered
title="实时态势地图"
style={{ marginBottom: 16 }}
>
<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="green">线 {commandOnlineCount.toLocaleString()} / {locations.length.toLocaleString()}</Tag>
<Tag color="blue"> {commandLocatedCount.toLocaleString()}</Tag>
<Tag color={commandDegradedCount > 0 ? 'orange' : 'green'}>/线 {commandDegradedCount.toLocaleString()}</Tag>
</Space>
</div>
<VehicleMap
points={commandMapPoints}
maxFallbackPoints={80}
fallbackLabel="高德地图未配置,显示车辆态势坐标预览"
/>
</div>
<div className="vp-monitor-side">
<div className="vp-monitor-metric">
<Tag color="green"></Tag>
<div className="vp-monitor-metric-value">{commandOnlineCount.toLocaleString()}</div>
<Typography.Text type="secondary">线</Typography.Text>
</div>
<div className="vp-monitor-metric">
<Tag color={commandDegradedCount > 0 ? 'orange' : 'green'}></Tag>
<div className="vp-monitor-metric-value">{commandDegradedCount.toLocaleString()}</div>
<Typography.Text type="secondary">线</Typography.Text>
</div>
<Space vertical align="start">
<Button theme="solid" type="primary" onClick={() => onOpenMap({ online: 'online' })}></Button>
<Button
disabled={!highPriorityIssue?.issueType}
theme="light"
type="warning"
onClick={() => onOpenQuality(highPriorityIssue?.issueType ? { issueType: highPriorityIssue.issueType } : {})}
>
</Button>
<Button theme="light" onClick={() => onOpenVehicles({ serviceStatus: 'degraded' })}></Button>
</Space>
</div>
</div>
</Card>
{serviceActionQueue.length > 0 ? (
<Card bordered title="车辆服务处置队列" style={{ marginBottom: 16 }}>
<div className="vp-action-grid">
{serviceActionQueue.map((item) => (
<div key={`${item.label}-${item.count}`} className="vp-action-item">
<div>
<Tag color="orange">{item.label} {item.count.toLocaleString()}</Tag>
<Typography.Text type="secondary" style={{ display: 'block', marginTop: 8 }}>{item.detail}</Typography.Text>
</div>
<Button size="small" theme="light" type="warning" onClick={() => onOpenVehicles(item.filters)}>
{item.label} {item.count.toLocaleString()}
</Button>
</div>
))}
</div>
</Card>
) : null}
<Card
title={<Space><span></span><Button size="small" onClick={() => onOpenQuality()}></Button></Space>}
bordered
style={{ marginTop: 16 }}
>
<Table
pagination={false}
rowKey={(row?: QualityIssueRow) => `${row?.protocol ?? ''}-${row?.issueType ?? ''}-${row?.lastSeen ?? ''}-${row?.detail ?? ''}`}
dataSource={qualityIssues}
columns={[
{ title: 'VIN', dataIndex: 'vin', width: 160 },
{ title: '车牌', dataIndex: 'plate', width: 110 },
{ title: '手机号', dataIndex: 'phone', width: 130 },
{ title: '来源地址', dataIndex: 'sourceEndpoint', width: 180 },
{ title: '来源', dataIndex: 'protocol', width: 110 },
{ title: '问题', dataIndex: 'issueType', width: 140 },
{ title: '级别', width: 90, render: (_: unknown, row: QualityIssueRow) => <Tag color={row.severity === 'error' ? 'red' : 'orange'}>{row.severity}</Tag> },
{ title: '最后时间', dataIndex: 'lastSeen', width: 170 },
{
title: '操作',
width: 130,
render: (_: unknown, row: QualityIssueRow) => {
const lookup = qualityIssueVehicleLookup(row);
return <Button disabled={!lookup.key} onClick={() => onOpenVehicle(lookup.key, row.protocol)}>{lookup.label}</Button>;
}
}
]}
/>
</Card>
<Card
title={<Space><span></span><Button size="small" onClick={() => onOpenVehicles(coverageFilters)}></Button></Space>}
bordered
style={{ marginTop: 16 }}
>
{coverageServiceStatusTitle ? (
<div className="vp-scope-bar" style={{ marginBottom: 12 }}>
<Tag color="blue">{coverageServiceStatusTitle}</Tag>
</div>
) : null}
<Form layout="horizontal" onSubmit={(values) => loadCoverage(values as Record<string, string>)} style={{ marginBottom: 12 }}>
<Form.Input field="keyword" label="关键词" placeholder="VIN / 车牌 / 手机号 / OEM" style={{ width: 240 }} />
<Form.Select field="coverage" label="来源覆盖" placeholder="全部" style={{ width: 130 }}>
<Select.Option value="single"></Select.Option>
<Select.Option value="multi"></Select.Option>
</Form.Select>
<Form.Select field="missingProtocol" label="缺失来源" placeholder="全部" style={{ width: 170 }} data-testid="dashboard-missing-protocol-filter">
<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="bindingStatus" label="绑定" placeholder="全部" style={{ width: 130 }}>
<Select.Option value="bound"></Select.Option>
<Select.Option value="unbound"></Select.Option>
</Form.Select>
<Form.Select field="serviceStatus" label="服务状态" placeholder="全部" style={{ width: 150 }} data-testid="dashboard-service-status-filter">
<Select.Option value="healthy"></Select.Option>
<Select.Option value="degraded"></Select.Option>
<Select.Option value="offline">线</Select.Option>
<Select.Option value="no_data"></Select.Option>
<Select.Option value="identity_required"></Select.Option>
</Form.Select>
<Space>
<Button htmlType="submit" theme="solid" type="primary"></Button>
<Button onClick={() => loadCoverage({})}></Button>
</Space>
</Form>
<Table
loading={coverageLoading}
pagination={false}
rowKey="vin"
dataSource={coverage}
columns={[
{ title: '车牌', dataIndex: 'plate', width: 110 },
{ title: 'VIN', dataIndex: 'vin', width: 190 },
{
title: '来源证据',
width: 300,
render: (_: unknown, row: VehicleCoverageRow) => (
<SourceStatusTags sourceStatus={row.sourceStatus} protocols={row.protocols} lastSeen={row.lastSeen} />
)
},
{
title: '证据覆盖',
width: 130,
render: (_: unknown, row: VehicleCoverageRow) => sourceEvidenceText(row)
},
{
title: '服务状态',
width: 130,
render: (_: unknown, row: VehicleCoverageRow) => {
const status = rowServiceStatus(row);
return <Tag color={status.color}>{status.label}</Tag>;
}
},
{
title: '来源一致性',
width: 140,
render: (_: unknown, row: VehicleCoverageRow) => sourceConsistencyAction(row, loadCoverage)
},
{ title: '在线', width: 90, render: (_: unknown, row: VehicleCoverageRow) => <StatusTag status={row.online ? 'ok' : 'offline'} /> },
{ title: '绑定', width: 90, render: (_: unknown, row: VehicleCoverageRow) => <Tag color={row.bindingStatus === 'bound' ? 'green' : 'orange'}>{row.bindingStatus === 'bound' ? '已绑定' : '未绑定'}</Tag> },
{ title: '最后时间', dataIndex: 'lastSeen', width: 170 },
{ title: '操作', width: 110, render: (_: unknown, row: VehicleCoverageRow) => <Button onClick={() => onOpenVehicle(row.vin)}></Button> }
]}
/>
</Card>
<Row gutter={16} style={{ marginTop: 16 }}>
<Col span={12}>
<Card title="实时位置预览" bordered>
<div className="vp-map" style={{ height: 260 }}>
{locations.map((row, index) => (
<span
key={row.vin}
className="vp-map-dot"
title={`${row.plate} ${row.primaryProtocol}`}
style={{ left: `${18 + index * 13}%`, top: `${24 + (index % 4) * 15}%` }}
/>
))}
</div>
</Card>
</Col>
<Col span={12}>
<Card title="最新车辆" bordered>
<Table
pagination={false}
rowKey="vin"
dataSource={locations}
columns={[
{ title: '车牌', dataIndex: 'plate' },
{ title: 'VIN', dataIndex: 'vin' },
{
title: '来源证据',
render: (_: unknown, row: VehicleRealtimeRow) => (
<Space spacing={4} wrap>
{row.protocols.map((protocol) => <Tag key={protocol} color={protocol === row.primaryProtocol ? 'blue' : 'grey'}>{protocol}</Tag>)}
</Space>
)
},
{
title: '服务状态',
render: (_: unknown, row: VehicleRealtimeRow) => {
const status = rowServiceStatus(row);
return <Tag color={status.color}>{status.label}</Tag>;
}
},
{ title: '最后时间', dataIndex: 'lastSeen' },
{ title: '操作', width: 110, render: (_: unknown, row: VehicleRealtimeRow) => <Button onClick={() => onOpenVehicle(row.vin, row.primaryProtocol)}></Button> }
]}
/>
</Card>
</Col>
</Row>
</Spin>
</div>
);
}