4564 lines
218 KiB
TypeScript
4564 lines
218 KiB
TypeScript
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 offsetDateString(value: string, offsetDays: number) {
|
||
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]) + offsetDays);
|
||
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 weekStartDateString(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]));
|
||
const day = date.getDay();
|
||
const mondayOffset = day === 0 ? -6 : 1 - day;
|
||
return offsetDateString(value, mondayOffset);
|
||
}
|
||
|
||
function dayRangeText(dateFrom?: string, dateTo?: string) {
|
||
if (!dateFrom && !dateTo) return '未限定时间';
|
||
if (!dateFrom || !dateTo) return '单边时间窗';
|
||
const start = new Date(`${dateFrom}T00:00:00+08:00`).getTime();
|
||
const end = new Date(`${dateTo}T00:00:00+08:00`).getTime();
|
||
if (!Number.isFinite(start) || !Number.isFinite(end)) return '时间格式待确认';
|
||
const days = Math.max(0, Math.round((end - start) / 86400000));
|
||
return days <= 0 ? '同日或倒置时间窗' : `${days} 天窗口`;
|
||
}
|
||
|
||
function naturalDayRangeText(dateFrom?: string, dateTo?: string) {
|
||
if (!dateFrom && !dateTo) return '未限定时间';
|
||
if (!dateFrom || !dateTo) return '单边时间窗';
|
||
const start = new Date(`${dateFrom}T00:00:00+08:00`).getTime();
|
||
const end = new Date(`${dateTo}T00:00:00+08:00`).getTime();
|
||
if (!Number.isFinite(start) || !Number.isFinite(end)) return '时间格式待确认';
|
||
const days = Math.max(0, Math.round((end - start) / 86400000)) + 1;
|
||
return days <= 0 ? '同日或倒置时间窗' : `${days} 天窗口`;
|
||
}
|
||
|
||
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>;
|
||
}
|
||
|
||
function defaultTimeMonitorFilters() {
|
||
const today = todayDateString();
|
||
return {
|
||
dateFrom: previousDateString(today),
|
||
dateTo: today
|
||
};
|
||
}
|
||
|
||
export function Dashboard({
|
||
onOpenVehicle,
|
||
onOpenQuality,
|
||
onOpenMap,
|
||
onOpenRealtime,
|
||
onOpenVehicles,
|
||
onOpenHistory,
|
||
onOpenMileage,
|
||
customerTimeWindow,
|
||
onCopyCustomerScope,
|
||
onSaveCustomerView,
|
||
focusMode = 'overview',
|
||
initialTimeMonitorFilters
|
||
}: {
|
||
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;
|
||
customerTimeWindow?: Record<string, string>;
|
||
onCopyCustomerScope?: () => void;
|
||
onSaveCustomerView?: () => void;
|
||
focusMode?: 'overview' | 'time-window';
|
||
initialTimeMonitorFilters?: Record<string, string>;
|
||
}) {
|
||
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>>({
|
||
...defaultTimeMonitorFilters(),
|
||
...(initialTimeMonitorFilters ?? {})
|
||
});
|
||
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));
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
const dateFrom = customerTimeWindow?.dateFrom?.trim();
|
||
const dateTo = customerTimeWindow?.dateTo?.trim();
|
||
if (!dateFrom && !dateTo) {
|
||
return;
|
||
}
|
||
setTimeMonitorFilters((previous) => {
|
||
const next = {
|
||
...previous,
|
||
dateFrom: dateFrom ?? '',
|
||
dateTo: dateTo ?? ''
|
||
};
|
||
if (previous.dateFrom === next.dateFrom && previous.dateTo === next.dateTo) {
|
||
return previous;
|
||
}
|
||
return next;
|
||
});
|
||
}, [customerTimeWindow?.dateFrom, customerTimeWindow?.dateTo]);
|
||
|
||
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 customerJourneySteps = [
|
||
{
|
||
step: '01',
|
||
title: '定位车辆',
|
||
detail: '先在地图上确认车辆是否在线、坐标是否可信。',
|
||
action: '车辆地图',
|
||
onClick: () => onOpenMap({ online: 'online' })
|
||
},
|
||
{
|
||
step: '02',
|
||
title: '查看状态',
|
||
detail: '进入实时监控查看速度、SOC、里程和最新上报。',
|
||
action: '实时监控',
|
||
onClick: () => onOpenRealtime({ online: 'online' })
|
||
},
|
||
{
|
||
step: '03',
|
||
title: '复盘轨迹',
|
||
detail: '按自定义时间窗回放位置、速度和里程断点。',
|
||
action: '轨迹回放',
|
||
onClick: () => onOpenHistory()
|
||
},
|
||
{
|
||
step: '04',
|
||
title: '统计查询',
|
||
detail: '用同一时间窗核对区间里程和每日统计。',
|
||
action: '统计查询',
|
||
onClick: () => onOpenMileage({})
|
||
},
|
||
{
|
||
step: '05',
|
||
title: '查询导出',
|
||
detail: '导出历史位置、原始记录、字段明细和告警证据。',
|
||
action: '历史数据',
|
||
onClick: () => openTimeMonitorRaw()
|
||
}
|
||
];
|
||
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: '同一时间窗贯穿轨迹、统计、原始记录和告警,不需要跨页面重复输入。',
|
||
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 timeMonitorScopeValue = timeMonitorScope();
|
||
const customerServiceScopeLabel = `${timeMonitorScopeValue.keyword || '全部车辆'} / ${timeMonitorScopeValue.protocol || '全部数据通道'} / ${timeMonitorScopeValue.dateFrom || '-'} 至 ${timeMonitorScopeValue.dateTo || '-'}`;
|
||
const usesCustomerTimeWindow = Boolean(
|
||
(customerTimeWindow?.dateFrom || customerTimeWindow?.dateTo)
|
||
&& customerTimeWindow?.dateFrom === timeMonitorScopeValue.dateFrom
|
||
&& customerTimeWindow?.dateTo === timeMonitorScopeValue.dateTo
|
||
);
|
||
const timeMonitorWindowLabel = usesCustomerTimeWindow
|
||
? naturalDayRangeText(timeMonitorScopeValue.dateFrom, timeMonitorScopeValue.dateTo)
|
||
: dayRangeText(timeMonitorScopeValue.dateFrom, timeMonitorScopeValue.dateTo);
|
||
const timeMonitorChecklist = [
|
||
{
|
||
label: '范围可解释',
|
||
value: timeMonitorScopeValue.keyword ? '单车聚焦' : '全域巡检',
|
||
detail: `${timeMonitorScopeValue.protocol || '全部数据通道'} / ${timeMonitorWindowLabel}`,
|
||
color: timeMonitorScopeValue.dateFrom && timeMonitorScopeValue.dateTo ? 'green' as const : 'orange' as const
|
||
},
|
||
{
|
||
label: '轨迹可复盘',
|
||
value: `${formatCount(summary?.activeToday)} 活跃车辆`,
|
||
detail: '可进入轨迹页核对位置、速度、里程断点和停驶片段。',
|
||
color: (summary?.activeToday ?? 0) > 0 ? 'green' as const : 'orange' as const
|
||
},
|
||
{
|
||
label: '数据可导出',
|
||
value: `${formatCount(summary?.frameToday)} 今日数据`,
|
||
detail: '历史查询默认带原始记录与解析字段,支持按字段缩小导出体积。',
|
||
color: (summary?.frameToday ?? 0) > 0 ? 'blue' as const : 'orange' as const
|
||
},
|
||
{
|
||
label: '异常可闭环',
|
||
value: `${formatCount(summary?.issueVehicles)} 告警车辆`,
|
||
detail: highPriorityIssue ? `${qualityIssueLabel(highPriorityIssue.issueType)} / ${priorityIssueVehicleLabel(highPriorityIssue)}` : '当前没有高优先级告警。',
|
||
color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const
|
||
}
|
||
];
|
||
const timeMonitorHasRange = Boolean(timeMonitorScopeValue.dateFrom && timeMonitorScopeValue.dateTo);
|
||
const timeMonitorHasTrajectory = (summary?.activeToday ?? 0) > 0;
|
||
const timeMonitorHasHistory = (summary?.frameToday ?? 0) > 0;
|
||
const timeMonitorHasAlerts = (summary?.issueVehicles ?? 0) > 0;
|
||
const timeMonitorDeliveryStatus = !timeMonitorHasRange
|
||
? { label: '待补时间', color: 'orange' as const, detail: '开始和结束时间不完整,交付前先锁定复盘窗口。' }
|
||
: !timeMonitorHasHistory
|
||
? { label: '证据不足', color: 'red' as const, detail: '当前没有历史证据,无法支撑客户复盘或导出。' }
|
||
: timeMonitorHasAlerts
|
||
? { label: '带风险交付', color: 'orange' as const, detail: '存在告警车辆,交付时需要附带告警闭环和原始证据。' }
|
||
: { label: '可交付', color: 'green' as const, detail: '时间窗、轨迹、历史证据和统计入口已准备好。' };
|
||
const timeMonitorDeliveryActions = [
|
||
{
|
||
label: '客户结论',
|
||
value: timeMonitorDeliveryStatus.label,
|
||
detail: timeMonitorDeliveryStatus.detail,
|
||
action: '复制复盘包',
|
||
color: timeMonitorDeliveryStatus.color,
|
||
onClick: () => copyTimeMonitorReviewPackage()
|
||
},
|
||
{
|
||
label: '证据准备',
|
||
value: timeMonitorHasTrajectory && timeMonitorHasHistory ? '证据可用' : '需要补证',
|
||
detail: `轨迹 ${formatCount(summary?.activeToday)} 活跃 / 历史 ${formatCount(summary?.frameToday)} 帧。`,
|
||
action: timeMonitorHasHistory ? '历史导出' : '查历史',
|
||
color: timeMonitorHasHistory ? 'blue' as const : 'orange' as const,
|
||
onClick: openTimeMonitorRaw
|
||
},
|
||
{
|
||
label: '异常说明',
|
||
value: timeMonitorHasAlerts ? `${formatCount(summary?.issueVehicles)} 告警` : '无高风险',
|
||
detail: highPriorityIssue ? `${qualityIssueLabel(highPriorityIssue.issueType)} / ${priorityIssueVehicleLabel(highPriorityIssue)}` : '当前没有需要额外说明的高优先级告警。',
|
||
action: '告警复盘',
|
||
color: timeMonitorHasAlerts ? 'orange' as const : 'green' as const,
|
||
onClick: openTimeMonitorAlerts
|
||
}
|
||
];
|
||
const copyTimeMonitorReviewPackage = () => {
|
||
const scope = timeMonitorScope();
|
||
const rawFilters = { ...scope, tab: 'raw', includeFields: 'true' };
|
||
const lines = [
|
||
'【客户时间窗复盘包】',
|
||
`范围:${timeMonitorSummary}`,
|
||
`交付结论:${timeMonitorDeliveryStatus.label};${timeMonitorDeliveryStatus.detail}`,
|
||
`时间窗判定:${dayRangeText(scope.dateFrom, scope.dateTo)}`,
|
||
'平台能力:实时地图 / 轨迹回放 / 统计查询 / 历史数据查询 / 告警通知',
|
||
`车辆范围:${scope.keyword || '全部车辆'}`,
|
||
`数据通道:${scope.protocol || '全部数据通道'}`,
|
||
`时间范围:${scope.dateFrom || '-'} 至 ${scope.dateTo || '-'}`,
|
||
`轨迹证据:今日活跃 ${formatCount(summary?.activeToday)},用于回放位置、速度、里程断点`,
|
||
`统计证据:车辆口径 ${formatCount(serviceSummary?.totalVehicles)},用于区间里程和日统计闭合`,
|
||
`历史证据:今日数据 ${formatCount(summary?.frameToday)},用于原始记录与解析字段复核`,
|
||
`告警事件:${formatCount(summary?.issueVehicles)} 辆车存在告警`,
|
||
'交付核对:',
|
||
...timeMonitorChecklist.map((item, index) => `${index + 1}. ${item.label}:${item.value};${item.detail}`),
|
||
'下一步动作:',
|
||
...timeMonitorDeliveryActions.map((item, index) => `${index + 1}. ${item.label}:${item.value};${item.detail}`),
|
||
`轨迹回放:${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: '按车辆、时间和字段裁剪导出原始记录与解析字段。',
|
||
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 timeWindowServicePathItems = [
|
||
{
|
||
step: '1',
|
||
label: '锁定范围',
|
||
value: timeMonitorWindowLabel,
|
||
detail: timeMonitorScopeValue.keyword ? `单车 ${timeMonitorScopeValue.keyword}` : '先确认车辆或车辆池,再应用同一时间窗。',
|
||
action: '应用时间窗',
|
||
color: timeMonitorHasRange ? 'green' as const : 'orange' as const,
|
||
onClick: () => setTimeMonitorFilters(timeMonitorScopeValue)
|
||
},
|
||
{
|
||
step: '2',
|
||
label: '回放轨迹',
|
||
value: `${formatCount(summary?.activeToday)} 活跃`,
|
||
detail: '查看时间窗内位置、速度、停驶和里程断点。',
|
||
action: '轨迹回放',
|
||
color: timeMonitorHasTrajectory ? 'blue' as const : 'orange' as const,
|
||
onClick: openTimeMonitorHistory
|
||
},
|
||
{
|
||
step: '3',
|
||
label: '核对里程',
|
||
value: `${formatCount(serviceSummary?.totalVehicles)} 车辆`,
|
||
detail: '用同一范围核对区间里程、日统计和 BI 口径。',
|
||
action: '统计查询',
|
||
color: 'green' as const,
|
||
onClick: openTimeMonitorMileage
|
||
},
|
||
{
|
||
step: '4',
|
||
label: '导出证据',
|
||
value: `${formatCount(summary?.frameToday)} 帧`,
|
||
detail: '导出轨迹、原始记录和解析字段,形成客户证据包。',
|
||
action: '数据导出',
|
||
color: timeMonitorHasHistory ? 'blue' as const : 'orange' as const,
|
||
onClick: openTimeMonitorRaw
|
||
},
|
||
{
|
||
step: '5',
|
||
label: '告警说明',
|
||
value: `${formatCount(summary?.issueVehicles)} 告警`,
|
||
detail: highPriorityIssue ? `${qualityIssueLabel(highPriorityIssue.issueType)} / ${priorityIssueVehicleLabel(highPriorityIssue)}` : '没有高优先级告警时,交付时说明当前无异常。',
|
||
action: '告警通知',
|
||
color: timeMonitorHasAlerts ? 'orange' as const : 'green' as const,
|
||
onClick: openTimeMonitorAlerts
|
||
}
|
||
];
|
||
const timeWindowNextPrimary = !timeMonitorHasRange
|
||
? '锁定范围'
|
||
: timeMonitorHasAlerts
|
||
? '告警复盘'
|
||
: timeMonitorHasHistory
|
||
? '历史导出'
|
||
: timeMonitorHasTrajectory
|
||
? '轨迹回放'
|
||
: '应用时间窗';
|
||
const runTimeWindowPrimaryAction = () => {
|
||
if (!timeMonitorHasRange) {
|
||
setTimeMonitorFilters(timeMonitorScopeValue);
|
||
return;
|
||
}
|
||
if (timeMonitorHasAlerts) {
|
||
openTimeMonitorAlerts();
|
||
return;
|
||
}
|
||
if (timeMonitorHasHistory) {
|
||
openTimeMonitorRaw();
|
||
return;
|
||
}
|
||
if (timeMonitorHasTrajectory) {
|
||
openTimeMonitorHistory();
|
||
return;
|
||
}
|
||
setTimeMonitorFilters(timeMonitorScopeValue);
|
||
};
|
||
const timeWindowNextItems = [
|
||
{
|
||
label: '首要动作',
|
||
value: timeWindowNextPrimary,
|
||
detail: timeMonitorHasAlerts ? '存在告警车辆,先给客户解释风险与处置闭环。' : timeMonitorHasHistory ? '历史证据可用,优先导出原始记录与解析字段。' : '先锁定车辆和时间范围。',
|
||
color: timeMonitorHasAlerts ? 'orange' as const : timeMonitorHasHistory ? 'blue' as const : 'orange' as const,
|
||
onClick: runTimeWindowPrimaryAction
|
||
},
|
||
{
|
||
label: '交付状态',
|
||
value: timeMonitorDeliveryStatus.label,
|
||
detail: timeMonitorDeliveryStatus.detail,
|
||
color: timeMonitorDeliveryStatus.color,
|
||
onClick: () => copyTimeMonitorReviewPackage()
|
||
},
|
||
{
|
||
label: '证据状态',
|
||
value: `${formatCount(summary?.frameToday)} 帧`,
|
||
detail: `${formatCount(summary?.activeToday)} 活跃车辆,可核对轨迹与历史明细。`,
|
||
color: timeMonitorHasHistory ? 'blue' as const : 'orange' as const,
|
||
onClick: openTimeMonitorRaw
|
||
},
|
||
{
|
||
label: '服务范围',
|
||
value: timeMonitorScopeValue.keyword ? '单车聚焦' : '全域巡检',
|
||
detail: `${timeMonitorScopeValue.keyword || '全部车辆'} / ${timeMonitorScopeValue.protocol || '全部数据通道'} / ${timeMonitorWindowLabel}`,
|
||
color: timeMonitorScopeValue.keyword ? 'green' as const : 'blue' as const,
|
||
onClick: () => setTimeMonitorFilters(timeMonitorScopeValue)
|
||
}
|
||
];
|
||
const timeWindowNextActions = [
|
||
{
|
||
label: '处理告警',
|
||
action: '告警复盘',
|
||
color: timeMonitorHasAlerts ? 'orange' as const : 'green' as const,
|
||
disabled: false,
|
||
onClick: openTimeMonitorAlerts
|
||
},
|
||
{
|
||
label: '导出证据',
|
||
action: '历史导出',
|
||
color: timeMonitorHasHistory ? 'blue' as const : 'orange' as const,
|
||
disabled: false,
|
||
onClick: openTimeMonitorRaw
|
||
},
|
||
{
|
||
label: '核对里程',
|
||
action: '统计查询',
|
||
color: 'green' as const,
|
||
disabled: false,
|
||
onClick: openTimeMonitorMileage
|
||
},
|
||
{
|
||
label: '复制复盘',
|
||
action: '交付包',
|
||
color: timeMonitorDeliveryStatus.color,
|
||
disabled: false,
|
||
onClick: () => copyTimeMonitorReviewPackage()
|
||
}
|
||
];
|
||
const dedicatedTimeMonitorActions = [
|
||
{
|
||
label: '轨迹回放',
|
||
value: timeMonitorWindowLabel,
|
||
detail: '按同一辆车和时间窗回放位置、速度、停留和里程断点。',
|
||
color: timeMonitorHasTrajectory ? 'blue' as const : 'orange' as const,
|
||
onClick: openTimeMonitorHistory
|
||
},
|
||
{
|
||
label: '统计查询',
|
||
value: timeMonitorWindowLabel,
|
||
detail: '用同一时间窗核对区间里程、日报闭合和异常点。',
|
||
color: 'green' as const,
|
||
onClick: openTimeMonitorMileage
|
||
},
|
||
{
|
||
label: '历史导出',
|
||
value: `${formatCount(summary?.frameToday)} 帧`,
|
||
detail: '导出历史位置、原始记录和字段证据,形成客户可复核数据包。',
|
||
color: timeMonitorHasHistory ? 'blue' as const : 'orange' as const,
|
||
onClick: openTimeMonitorRaw
|
||
},
|
||
{
|
||
label: '告警复盘',
|
||
value: `${formatCount(summary?.issueVehicles)} 告警`,
|
||
detail: '用同一时间窗解释异常车辆、通知策略和恢复验收。',
|
||
color: timeMonitorHasAlerts ? '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)} 活跃,原始记录 ${formatCount(summary?.frameToday)} 帧`,
|
||
detail: '位置、原始记录和解析字段可分页查询,并能回跳轨迹、车辆服务和告警。',
|
||
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()
|
||
}
|
||
];
|
||
const customerServicePathItems = [
|
||
{
|
||
title: '找车',
|
||
value: formatCount(serviceSummary?.totalVehicles),
|
||
detail: '按 VIN、车牌、手机号或 OEM 进入车辆服务,协议来源只作为证据。',
|
||
action: '车辆中心',
|
||
color: 'blue' as const,
|
||
onClick: () => onOpenVehicles({})
|
||
},
|
||
{
|
||
title: '看位置',
|
||
value: `${commandLocatedCount.toLocaleString()} 有定位`,
|
||
detail: '优先打开实时地图,判断车辆是否在线、坐标是否有效、是否存在断链。',
|
||
action: '实时地图',
|
||
color: commandLocatedCount > 0 ? 'green' as const : 'orange' as const,
|
||
onClick: () => onOpenMap({ online: 'online' })
|
||
},
|
||
{
|
||
title: '复盘时间',
|
||
value: dayRangeText(timeMonitorScopeValue.dateFrom, timeMonitorScopeValue.dateTo),
|
||
detail: '同一个时间窗贯穿轨迹回放、统计查询、原始记录和告警复盘。',
|
||
action: '轨迹回放',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorHistory
|
||
},
|
||
{
|
||
title: '交付数据',
|
||
value: `${formatCount(summary?.frameToday)} 今日数据`,
|
||
detail: '按车辆、时间、字段裁剪导出位置历史、原始记录和字段明细。',
|
||
action: '查询导出',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorRaw
|
||
},
|
||
{
|
||
title: '闭环异常',
|
||
value: `${formatCount(summary?.issueVehicles)} 告警`,
|
||
detail: highPriorityIssue ? `${qualityIssueLabel(highPriorityIssue.issueType)} / ${priorityIssueVehicleLabel(highPriorityIssue)}` : '暂无高优先级告警,保持日常巡检。',
|
||
action: '告警事件',
|
||
color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const,
|
||
onClick: () => onOpenQuality()
|
||
}
|
||
];
|
||
const customerServiceJourneySteps = [
|
||
{
|
||
step: '1',
|
||
title: '选车',
|
||
value: formatCount(serviceSummary?.totalVehicles),
|
||
detail: '先用 VIN、车牌、手机号或 OEM 锁定服务对象。',
|
||
action: '车辆中心',
|
||
color: 'blue' as const,
|
||
onClick: () => onOpenVehicles({})
|
||
},
|
||
{
|
||
step: '2',
|
||
title: '实时看车',
|
||
value: `${formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles)} 在线`,
|
||
detail: '打开地图看在线、坐标、最后上报和区域态势。',
|
||
action: '实时地图',
|
||
color: 'green' as const,
|
||
onClick: () => onOpenMap({ online: 'online' })
|
||
},
|
||
{
|
||
step: '3',
|
||
title: '锁定时间窗',
|
||
value: dayRangeText(timeMonitorScopeValue.dateFrom, timeMonitorScopeValue.dateTo),
|
||
detail: '同一范围同步带入轨迹、统计、历史导出和告警。',
|
||
action: '自定义时间',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorHistory
|
||
},
|
||
{
|
||
step: '4',
|
||
title: '复盘统计',
|
||
value: `${formatCount(summary?.activeToday)} 活跃`,
|
||
detail: '用轨迹、位置历史和里程统计回答这段时间发生了什么。',
|
||
action: '轨迹统计',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorMileage
|
||
},
|
||
{
|
||
step: '5',
|
||
title: '交付闭环',
|
||
value: `${formatCount(summary?.issueVehicles)} 告警`,
|
||
detail: '导出证据数据,并把异常车辆推到告警通知闭环。',
|
||
action: '导出通知',
|
||
color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const,
|
||
onClick: openTimeMonitorRaw
|
||
}
|
||
];
|
||
const copyCustomerServiceGuide = () => {
|
||
const scope = timeMonitorScope();
|
||
const rawFilters = { ...scope, tab: 'raw', includeFields: 'true' };
|
||
const lines = [
|
||
'【车辆客户服务说明】',
|
||
'服务对象:车辆,不是接入来源。',
|
||
`车辆规模:${formatCount(serviceSummary?.totalVehicles)},在线车辆:${formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles)},有效定位:${commandLocatedCount.toLocaleString()}`,
|
||
`当前时间窗:${timeMonitorSummary}(${dayRangeText(scope.dateFrom, scope.dateTo)})`,
|
||
`告警车辆:${formatCount(summary?.issueVehicles)},最高优先级:${highPriorityIssue ? `${qualityIssueLabel(highPriorityIssue.issueType)} / ${priorityIssueVehicleLabel(highPriorityIssue)}` : '暂无'}`,
|
||
'',
|
||
'客户服务路径:',
|
||
'1. 找车:先按 VIN、车牌、手机号或 OEM 锁定车辆。',
|
||
'2. 看位置:打开实时地图确认车辆在线、坐标和最新上报。',
|
||
'3. 复盘时间:同一时间窗查看轨迹、统计和告警。',
|
||
'4. 交付数据:按车辆、时间、字段导出位置历史、原始记录和字段明细。',
|
||
'5. 闭环异常:告警事件进入通知和处置流程。',
|
||
`车辆中心:${appURL(buildAppHash({ page: 'vehicles' }))}`,
|
||
`实时地图:${appURL(buildAppHash({ page: 'map', filters: { online: 'online' } }))}`,
|
||
`轨迹回放:${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 }))}`
|
||
];
|
||
copyText(lines.join('\n'), '客户服务说明');
|
||
};
|
||
const customerCockpitMetrics = [
|
||
{
|
||
label: '车辆总数',
|
||
value: formatCount(serviceSummary?.totalVehicles),
|
||
detail: '以 VIN 聚合车牌、手机号、OEM 和来源证据',
|
||
color: 'blue' as const,
|
||
onClick: () => onOpenVehicles({})
|
||
},
|
||
{
|
||
label: '在线车辆',
|
||
value: formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles),
|
||
detail: vehicleServiceOnlineText(serviceSummary, summary),
|
||
color: 'green' as const,
|
||
onClick: () => onOpenMap({ online: 'online' })
|
||
},
|
||
{
|
||
label: '有效定位',
|
||
value: commandLocatedCount.toLocaleString(),
|
||
detail: '用于实时地图、轨迹回放和客户位置问询',
|
||
color: commandLocatedCount > 0 ? 'green' as const : 'orange' as const,
|
||
onClick: () => onOpenRealtime({ 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()
|
||
}
|
||
];
|
||
const customerServiceCockpitItems = [
|
||
{
|
||
label: '车辆总览',
|
||
value: `${formatCount(serviceSummary?.totalVehicles)} 辆`,
|
||
detail: '按 VIN、车牌、手机号或 OEM 进入车辆服务,不从协议列表开始。',
|
||
action: '车辆中心',
|
||
color: 'blue' as const,
|
||
onClick: () => onOpenVehicles({})
|
||
},
|
||
{
|
||
label: '实时可见',
|
||
value: `${formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles)} 在线`,
|
||
detail: `${commandLocatedCount.toLocaleString()} 辆有有效坐标,先回答客户车辆在哪里。`,
|
||
action: '地图看车',
|
||
color: commandLocatedCount > 0 ? 'green' as const : 'orange' as const,
|
||
onClick: () => onOpenMap({ online: 'online' })
|
||
},
|
||
{
|
||
label: '轨迹复盘',
|
||
value: `${formatCount(summary?.activeToday)} 今日活跃`,
|
||
detail: '同一辆车、同一时间窗回放路线、速度、停驶和里程断点。',
|
||
action: '回放轨迹',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorHistory
|
||
},
|
||
{
|
||
label: '里程统计',
|
||
value: dayRangeText(timeMonitorScopeValue.dateFrom, timeMonitorScopeValue.dateTo),
|
||
detail: '核对区间里程、每日闭合、异常记录和 BI 统计口径。',
|
||
action: '统计查询',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorMileage
|
||
},
|
||
{
|
||
label: '数据交付',
|
||
value: `${formatCount(summary?.frameToday)} 今日数据`,
|
||
detail: '按车辆、时间和字段裁剪历史位置、原始记录和解析证据。',
|
||
action: '导出证据',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorRaw
|
||
},
|
||
{
|
||
label: '告警通知',
|
||
value: `${formatCount(summary?.issueVehicles)} 告警`,
|
||
detail: highPriorityIssue ? `${qualityIssueLabel(highPriorityIssue.issueType)} / ${priorityIssueVehicleLabel(highPriorityIssue)}` : '暂无高优先级告警。',
|
||
action: '闭环处理',
|
||
color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const,
|
||
onClick: () => onOpenQuality(highPriorityIssue?.issueType ? { issueType: highPriorityIssue.issueType } : {})
|
||
}
|
||
];
|
||
const customerCockpitWorkflows = [
|
||
{
|
||
title: '实时看车',
|
||
description: '打开地图确认车辆在哪里、是否在线、最后上报是否新鲜。',
|
||
action: '实时地图',
|
||
evidence: `${commandLocatedCount.toLocaleString()} 辆有定位`,
|
||
onClick: () => onOpenMap({ online: 'online' })
|
||
},
|
||
{
|
||
title: '轨迹复盘',
|
||
description: '用同一辆车和同一时间窗回放位置、速度、里程断点。',
|
||
action: '轨迹回放',
|
||
evidence: `${formatCount(summary?.activeToday)} 今日活跃`,
|
||
onClick: openTimeMonitorHistory
|
||
},
|
||
{
|
||
title: '统计查询',
|
||
description: '按车辆口径核对区间里程、日统计和异常来源。',
|
||
action: '统计查询',
|
||
evidence: `${formatCount(serviceSummary?.totalVehicles)} 辆可统计`,
|
||
onClick: openTimeMonitorMileage
|
||
},
|
||
{
|
||
title: '数据交付',
|
||
description: '按车辆、时间和字段导出位置历史、原始记录和解析字段。',
|
||
action: '历史导出',
|
||
evidence: `${formatCount(summary?.frameToday)} 今日数据`,
|
||
onClick: openTimeMonitorRaw
|
||
},
|
||
{
|
||
title: '告警通知',
|
||
description: '将断链、离线、定位异常和字段缺失变成可通知事件。',
|
||
action: '告警事件',
|
||
evidence: `${formatCount(summary?.issueVehicles)} 告警车辆`,
|
||
onClick: () => onOpenQuality()
|
||
}
|
||
];
|
||
const modernFleetOperations = [
|
||
{
|
||
label: 'Live Map',
|
||
value: `${formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles)} 在线`,
|
||
detail: `有效定位 ${commandLocatedCount.toLocaleString()} 辆,先确认车辆在哪里。`,
|
||
action: '打开地图',
|
||
color: 'green' as const,
|
||
onClick: () => onOpenMap({ online: 'online' })
|
||
},
|
||
{
|
||
label: 'Route Replay',
|
||
value: `${formatCount(summary?.activeToday)} 活跃`,
|
||
detail: `时间窗 ${dayRangeText(timeMonitorScopeValue.dateFrom, timeMonitorScopeValue.dateTo)},用于客户复盘。`,
|
||
action: '回放轨迹',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorHistory
|
||
},
|
||
{
|
||
label: 'Mileage',
|
||
value: `${formatCount(serviceSummary?.totalVehicles)} 车辆`,
|
||
detail: '按车辆口径查询区间里程、日统计和异常断点。',
|
||
action: '查统计',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorMileage
|
||
},
|
||
{
|
||
label: 'Export',
|
||
value: `${formatCount(summary?.frameToday)} 今日数据`,
|
||
detail: '位置、原始记录和字段证据按车辆与时间裁剪交付。',
|
||
action: '导出数据',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorRaw
|
||
},
|
||
{
|
||
label: 'Alerts',
|
||
value: `${formatCount(summary?.issueVehicles)} 告警`,
|
||
detail: highPriorityIssue ? `${qualityIssueLabel(highPriorityIssue.issueType)} / ${priorityIssueVehicleLabel(highPriorityIssue)}` : '暂无高优先级告警。',
|
||
action: '通知闭环',
|
||
color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const,
|
||
onClick: () => onOpenQuality(highPriorityIssue?.issueType ? { issueType: highPriorityIssue.issueType } : {})
|
||
}
|
||
];
|
||
const customerPlatformBlueprintItems = [
|
||
{
|
||
label: '实时地图',
|
||
value: `${formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles)} 在线`,
|
||
detail: `有效定位 ${commandLocatedCount.toLocaleString()} 辆,支撑客户看车、调度和在线巡检。`,
|
||
action: '打开地图',
|
||
color: commandLocatedCount > 0 ? 'green' as const : 'orange' as const,
|
||
onClick: () => onOpenMap({ online: 'online' })
|
||
},
|
||
{
|
||
label: '轨迹回放',
|
||
value: `${formatCount(summary?.activeToday)} 活跃`,
|
||
detail: '按车辆和时间窗回放路线、速度、停留与里程断点。',
|
||
action: '回放轨迹',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorHistory
|
||
},
|
||
{
|
||
label: '围栏告警',
|
||
value: `${formatCount(summary?.issueVehicles)} 告警`,
|
||
detail: highPriorityIssue ? `${qualityIssueLabel(highPriorityIssue.issueType)} / ${priorityIssueVehicleLabel(highPriorityIssue)}` : '断链、定位异常、围栏事件进入告警通知闭环。',
|
||
action: '告警通知',
|
||
color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const,
|
||
onClick: () => onOpenQuality(highPriorityIssue?.issueType ? { issueType: highPriorityIssue.issueType } : {})
|
||
},
|
||
{
|
||
label: '报表导出',
|
||
value: `${formatCount(summary?.frameToday)} 今日数据`,
|
||
detail: '按车辆、时间、字段裁剪轨迹、历史明细、统计和告警说明。',
|
||
action: '历史导出',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorRaw
|
||
},
|
||
{
|
||
label: '车辆健康',
|
||
value: `${formatCount(serviceSummary?.totalVehicles)} 车辆`,
|
||
detail: '围绕车辆档案、在线状态、能耗字段和异常维护建立服务视图。',
|
||
action: '车辆中心',
|
||
color: 'blue' as const,
|
||
onClick: () => onOpenVehicles({})
|
||
}
|
||
];
|
||
const customerSavedViewItems = [
|
||
{
|
||
label: '在线车队视图',
|
||
value: `${formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles)} 在线`,
|
||
detail: '默认进入车辆地图,按在线车辆、最新位置和定位有效性巡检。',
|
||
action: '打开地图',
|
||
color: 'green' as const,
|
||
onClick: () => onOpenMap({ online: 'online' })
|
||
},
|
||
{
|
||
label: '轨迹日报模板',
|
||
value: `${formatCount(summary?.activeToday)} 活跃`,
|
||
detail: '按当前时间窗复用轨迹回放和历史位置查询,处理每日客户问询。',
|
||
action: '历史导出',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorHistory
|
||
},
|
||
{
|
||
label: '里程周报模板',
|
||
value: `${formatCount(serviceSummary?.totalVehicles)} 车辆`,
|
||
detail: '按车辆口径复用里程统计,支撑周报、对账和 BI 复核。',
|
||
action: '统计查询',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorMileage
|
||
},
|
||
{
|
||
label: '告警订阅视图',
|
||
value: `${formatCount(summary?.issueVehicles)} 告警`,
|
||
detail: '围绕断链、离线、定位异常和字段缺失形成通知闭环。',
|
||
action: '通知闭环',
|
||
color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const,
|
||
onClick: () => onOpenQuality(highPriorityIssue?.issueType ? { issueType: highPriorityIssue.issueType } : {})
|
||
}
|
||
];
|
||
const tenSecondTriageItems = [
|
||
{
|
||
label: '告警车辆',
|
||
value: `${formatCount(summary?.issueVehicles)} 辆`,
|
||
detail: highPriorityIssue ? `${qualityIssueLabel(highPriorityIssue.issueType)} / ${priorityIssueVehicleLabel(highPriorityIssue)}` : '暂无高优先级告警,保持日常巡检。',
|
||
action: '处理告警',
|
||
color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const,
|
||
onClick: () => onOpenQuality(highPriorityIssue?.issueType ? { issueType: highPriorityIssue.issueType } : {})
|
||
},
|
||
{
|
||
label: '无数据车辆',
|
||
value: `${formatCount(serviceSummary?.noDataVehicles)} 辆`,
|
||
detail: '优先确认平台转发、车辆是否在线以及是否有任何来源证据。',
|
||
action: '查车辆',
|
||
color: (serviceSummary?.noDataVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const,
|
||
onClick: () => onOpenVehicles({ serviceStatus: 'no_data' })
|
||
},
|
||
{
|
||
label: '身份未绑定',
|
||
value: `${formatCount(serviceSummary?.identityRequiredVehicles)} 辆`,
|
||
detail: '已有上报但无法稳定归并到 VIN,会影响地图、轨迹和统计聚合。',
|
||
action: '维护绑定',
|
||
color: (serviceSummary?.identityRequiredVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const,
|
||
onClick: () => onOpenVehicles({ serviceStatus: 'identity_required' })
|
||
}
|
||
];
|
||
const amapVehicleServiceItems = [
|
||
{
|
||
label: '实时看车',
|
||
value: `${commandLocatedCount.toLocaleString()} 有定位`,
|
||
detail: '高德底图承接车辆在线、最新位置、速度和最后上报时间。',
|
||
action: '打开地图',
|
||
color: commandLocatedCount > 0 ? 'green' as const : 'orange' as const,
|
||
onClick: () => onOpenMap({ online: 'online' })
|
||
},
|
||
{
|
||
label: '路线回放',
|
||
value: dayRangeText(timeMonitorScopeValue.dateFrom, timeMonitorScopeValue.dateTo),
|
||
detail: '同一车辆和时间窗回放路径,核对速度、停留、里程和断点。',
|
||
action: '轨迹回放',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorHistory
|
||
},
|
||
{
|
||
label: '区域告警',
|
||
value: `${formatCount(summary?.issueVehicles)} 告警`,
|
||
detail: highPriorityIssue ? `${qualityIssueLabel(highPriorityIssue.issueType)} / ${priorityIssueVehicleLabel(highPriorityIssue)}` : '围栏、断链、定位异常后续统一进入告警通知。',
|
||
action: '告警通知',
|
||
color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const,
|
||
onClick: () => onOpenQuality(highPriorityIssue?.issueType ? { issueType: highPriorityIssue.issueType } : {})
|
||
},
|
||
{
|
||
label: '时间窗导出',
|
||
value: `${formatCount(summary?.frameToday)} 今日数据`,
|
||
detail: '按车辆、时间和字段导出位置、原始记录、解析字段和统计证据。',
|
||
action: '历史导出',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorRaw
|
||
}
|
||
];
|
||
const amapCapabilityItems = [
|
||
{
|
||
label: '实时底图',
|
||
value: amapConfigured ? '可用' : '降级',
|
||
detail: amapConfigured ? '车辆在线分布、最后位置和地图选车可直接使用高德底图。' : '未配置 Web JS Key 时保留坐标预览。',
|
||
color: amapConfigured ? 'green' as const : 'orange' as const
|
||
},
|
||
{
|
||
label: '轨迹路径',
|
||
value: amapConfigured ? '可回放' : '坐标线',
|
||
detail: amapConfigured ? '轨迹回放可叠加道路底图,辅助判断停留、绕行和断点。' : '缺少底图时只展示历史坐标证据。',
|
||
color: amapConfigured ? 'green' as const : 'orange' as const
|
||
},
|
||
{
|
||
label: '区域围栏',
|
||
value: amapApiConfigured ? '可扩展' : '待接入',
|
||
detail: amapApiConfigured ? '服务端 API 可承接围栏、区域、路线和位置服务扩展。' : '围栏和区域能力需要服务端地图 API。',
|
||
color: amapApiConfigured ? 'blue' as const : 'orange' as const
|
||
},
|
||
{
|
||
label: '地址解析',
|
||
value: amapSecurityProxyEnabled ? '代理可用' : '待代理',
|
||
detail: amapSecurityProxyEnabled ? '逆地理和路线请求可通过安全代理收敛密钥。' : '生产环境建议通过 /_AMapService 代理服务端能力。',
|
||
color: amapSecurityProxyEnabled ? 'green' as const : 'orange' as const
|
||
}
|
||
];
|
||
const customerQuestionActions = [
|
||
{
|
||
question: '这辆车现在在哪里?',
|
||
answer: `${commandLocatedCount.toLocaleString()} 辆有可用坐标`,
|
||
action: '打开地图',
|
||
color: commandLocatedCount > 0 ? 'green' as const : 'orange' as const,
|
||
onClick: () => onOpenMap({ online: 'online' })
|
||
},
|
||
{
|
||
question: '这段时间跑了多少公里?',
|
||
answer: dayRangeText(timeMonitorScopeValue.dateFrom, timeMonitorScopeValue.dateTo),
|
||
action: '查里程',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorMileage
|
||
},
|
||
{
|
||
question: '能不能回放轨迹?',
|
||
answer: `${formatCount(summary?.activeToday)} 辆今日活跃`,
|
||
action: '轨迹回放',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorHistory
|
||
},
|
||
{
|
||
question: '数据能导出给客户吗?',
|
||
answer: `${formatCount(summary?.frameToday)} 条今日证据`,
|
||
action: '查询导出',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorRaw
|
||
},
|
||
{
|
||
question: '哪些车需要马上处理?',
|
||
answer: highPriorityIssue ? `${qualityIssueLabel(highPriorityIssue.issueType)} / ${priorityIssueVehicleLabel(highPriorityIssue)}` : '暂无高优先级告警',
|
||
action: '告警事件',
|
||
color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const,
|
||
onClick: () => onOpenQuality(highPriorityIssue?.issueType ? { issueType: highPriorityIssue.issueType } : {})
|
||
}
|
||
];
|
||
const customerProblemDesk = [
|
||
{
|
||
question: '现在车辆在哪里',
|
||
answer: `${commandLocatedCount.toLocaleString()} 辆有可用坐标,先进入实时地图确认分布和最新上报。`,
|
||
metric: `${formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles)} 在线`,
|
||
action: '打开实时地图',
|
||
color: 'green' as const,
|
||
onClick: () => onOpenMap({ online: 'online' })
|
||
},
|
||
{
|
||
question: '这段时间跑了多少',
|
||
answer: `${dayRangeText(timeMonitorScopeValue.dateFrom, timeMonitorScopeValue.dateTo)},用同一时间窗核对区间里程和每日统计。`,
|
||
metric: `${formatCount(serviceSummary?.totalVehicles)} 车辆`,
|
||
action: '查统计查询',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorMileage
|
||
},
|
||
{
|
||
question: '轨迹能不能回放',
|
||
answer: `今日活跃 ${formatCount(summary?.activeToday)},可回放位置、速度、停驶和里程断点。`,
|
||
metric: `${formatCount(summary?.activeToday)} 活跃`,
|
||
action: '打开轨迹回放',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorHistory
|
||
},
|
||
{
|
||
question: '需要导出哪些证据',
|
||
answer: `今日数据 ${formatCount(summary?.frameToday)},按车辆、时间、字段裁剪导出位置和原始证据。`,
|
||
metric: `${formatCount(summary?.frameToday)} 数据`,
|
||
action: '查询历史导出',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorRaw
|
||
},
|
||
{
|
||
question: '哪些车需要通知处理',
|
||
answer: highPriorityIssue ? `${qualityIssueLabel(highPriorityIssue.issueType)} / ${priorityIssueVehicleLabel(highPriorityIssue)}` : '暂无高优先级告警,保持日常巡检。',
|
||
metric: `${formatCount(summary?.issueVehicles)} 告警`,
|
||
action: '查看告警事件',
|
||
color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const,
|
||
onClick: () => onOpenQuality(highPriorityIssue?.issueType ? { issueType: highPriorityIssue.issueType } : {})
|
||
}
|
||
];
|
||
const onlineVehicleCount = serviceSummary?.onlineVehicles ?? summary?.onlineVehicles ?? 0;
|
||
const missingLocationCount = Math.max(onlineVehicleCount - commandLocatedCount, 0);
|
||
const vehiclePriorityQueue = [
|
||
{
|
||
title: '告警待处理',
|
||
value: `${formatCount(summary?.issueVehicles)} 辆`,
|
||
detail: highPriorityIssue ? `${qualityIssueLabel(highPriorityIssue.issueType)} / ${priorityIssueVehicleLabel(highPriorityIssue)}` : '暂无高优先级告警,保持日常巡检。',
|
||
action: '告警事件',
|
||
color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const,
|
||
onClick: () => onOpenQuality(highPriorityIssue?.issueType ? { issueType: highPriorityIssue.issueType } : {})
|
||
},
|
||
{
|
||
title: '无有效定位',
|
||
value: `${missingLocationCount.toLocaleString()} 辆`,
|
||
detail: '在线车辆缺少有效坐标时,客户无法完成地图看车和轨迹复盘。',
|
||
action: '打开地图',
|
||
color: missingLocationCount > 0 ? 'orange' as const : 'green' as const,
|
||
onClick: () => onOpenMap({ online: 'online' })
|
||
},
|
||
{
|
||
title: '身份未绑定',
|
||
value: `${formatCount(serviceSummary?.identityRequiredVehicles)} 辆`,
|
||
detail: '已有数据但无法归并到 VIN,会影响车辆服务、统计和导出。',
|
||
action: '维护绑定',
|
||
color: (serviceSummary?.identityRequiredVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const,
|
||
onClick: () => onOpenVehicles({ serviceStatus: 'identity_required' })
|
||
},
|
||
{
|
||
title: '暂无数据',
|
||
value: `${formatCount(serviceSummary?.noDataVehicles)} 辆`,
|
||
detail: '车辆没有形成实时、轨迹或历史证据,优先确认平台转发。',
|
||
action: '车辆列表',
|
||
color: (serviceSummary?.noDataVehicles ?? 0) > 0 ? 'red' as const : 'green' as const,
|
||
onClick: () => onOpenVehicles({ serviceStatus: 'no_data' })
|
||
}
|
||
];
|
||
const customerNextActions = [
|
||
{
|
||
level: (summary?.issueVehicles ?? 0) > 0 ? '优先' : '巡检',
|
||
title: (summary?.issueVehicles ?? 0) > 0 ? '先处理告警车辆' : '先查看在线车辆地图',
|
||
detail: (summary?.issueVehicles ?? 0) > 0
|
||
? (highPriorityIssue ? `${qualityIssueLabel(highPriorityIssue.issueType)} / ${priorityIssueVehicleLabel(highPriorityIssue)}` : '存在告警车辆,建议先进入告警事件。')
|
||
: `${formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles)} 辆在线,${commandLocatedCount.toLocaleString()} 辆有定位。`,
|
||
action: (summary?.issueVehicles ?? 0) > 0 ? '进入告警' : '打开地图',
|
||
color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const,
|
||
onClick: () => (summary?.issueVehicles ?? 0) > 0 ? onOpenQuality(highPriorityIssue?.issueType ? { issueType: highPriorityIssue.issueType } : {}) : onOpenMap({ online: 'online' })
|
||
},
|
||
{
|
||
level: focusVehicle ? '单车' : '范围',
|
||
title: focusVehicle ? '复盘重点车辆' : '选择一辆车复盘',
|
||
detail: focusVehicle ? `${focusVehicle.label},${focusVehicle.reason}` : '从车辆中心或地图选择车辆后,进入轨迹、统计和历史证据。',
|
||
action: focusVehicle ? '车辆服务' : '车辆中心',
|
||
color: focusVehicle?.statusColor ?? 'blue' as const,
|
||
onClick: () => focusVehicle?.lookupKey ? onOpenVehicle(focusVehicle.lookupKey, focusVehicle.protocol) : onOpenVehicles({})
|
||
},
|
||
{
|
||
level: '时间窗',
|
||
title: '按同一范围交付数据',
|
||
detail: `${dayRangeText(timeMonitorScopeValue.dateFrom, timeMonitorScopeValue.dateTo)},同步轨迹、统计、历史和告警。`,
|
||
action: '复制复盘包',
|
||
color: 'blue' as const,
|
||
onClick: copyTimeMonitorReviewPackage
|
||
}
|
||
];
|
||
const customerTrustSignals = [
|
||
{
|
||
label: '数据来源',
|
||
value: `${formatCount(serviceSummary?.multiSourceVehicles)} 多源 / ${formatCount(serviceSummary?.singleSourceVehicles)} 单源`,
|
||
detail: '来源只作为可信度证据,不作为客户主导航。'
|
||
},
|
||
{
|
||
label: '地图能力',
|
||
value: amapConfigured ? '高德地图可用' : '坐标预览',
|
||
detail: amapConfigured ? '支持实时看车和轨迹底图。' : '地图配置缺失时仍可查看坐标证据。'
|
||
},
|
||
{
|
||
label: '时间窗',
|
||
value: dayRangeText(timeMonitorScopeValue.dateFrom, timeMonitorScopeValue.dateTo),
|
||
detail: '同一时间范围贯穿轨迹、统计、历史和告警。'
|
||
}
|
||
];
|
||
const customerTaskNavigation = [
|
||
{
|
||
title: '看车在哪',
|
||
value: `${formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles)} 在线`,
|
||
detail: `${commandLocatedCount.toLocaleString()} 辆有有效定位`,
|
||
action: '实时地图',
|
||
color: 'green' as const,
|
||
onClick: () => onOpenMap({ online: 'online' })
|
||
},
|
||
{
|
||
title: '查这段时间',
|
||
value: dayRangeText(timeMonitorScopeValue.dateFrom, timeMonitorScopeValue.dateTo),
|
||
detail: '同窗查看轨迹和里程统计',
|
||
action: '轨迹统计',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorHistory
|
||
},
|
||
{
|
||
title: '导出数据',
|
||
value: `${formatCount(summary?.frameToday)} 今日数据`,
|
||
detail: '位置、原始记录和字段证据',
|
||
action: '历史导出',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorRaw
|
||
},
|
||
{
|
||
title: '处理告警',
|
||
value: `${formatCount(summary?.issueVehicles)} 告警车辆`,
|
||
detail: highPriorityIssue ? qualityIssueLabel(highPriorityIssue.issueType) : '当前无高优先级',
|
||
action: '告警事件',
|
||
color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const,
|
||
onClick: () => onOpenQuality(highPriorityIssue?.issueType ? { issueType: highPriorityIssue.issueType } : {})
|
||
}
|
||
];
|
||
const customerDeliveryItems = [
|
||
{
|
||
title: '在线车辆交付',
|
||
value: `${formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles)} 在线`,
|
||
detail: `地图有效定位 ${commandLocatedCount.toLocaleString()} 辆,先交付客户最关心的车辆当前位置和在线态势。`,
|
||
action: '实时地图',
|
||
color: 'green' as const,
|
||
onClick: () => onOpenMap({ online: 'online' })
|
||
},
|
||
{
|
||
title: '时间窗复盘',
|
||
value: dayRangeText(timeMonitorScopeValue.dateFrom, timeMonitorScopeValue.dateTo),
|
||
detail: '同一时间范围同步到轨迹、统计、历史证据和告警复盘,避免客户反复给条件。',
|
||
action: '轨迹回放',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorHistory
|
||
},
|
||
{
|
||
title: '数据证据交付',
|
||
value: `${formatCount(summary?.frameToday)} 今日数据`,
|
||
detail: '按车辆、时间和字段裁剪导出位置、原始记录、解析字段和统计依据。',
|
||
action: '历史导出',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorRaw
|
||
},
|
||
{
|
||
title: '告警通知交付',
|
||
value: `${formatCount(summary?.issueVehicles)} 告警车辆`,
|
||
detail: highPriorityIssue ? `${qualityIssueLabel(highPriorityIssue.issueType)} / ${priorityIssueVehicleLabel(highPriorityIssue)}` : '当前暂无高优先级告警,保持日常巡检和断链预警。',
|
||
action: '告警事件',
|
||
color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const,
|
||
onClick: () => onOpenQuality(highPriorityIssue?.issueType ? { issueType: highPriorityIssue.issueType } : {})
|
||
}
|
||
];
|
||
const customerVehicleMonitorItems = [
|
||
{
|
||
label: '车辆在哪里',
|
||
value: `${commandLocatedCount.toLocaleString()} 辆有定位`,
|
||
detail: '打开地图查看在线车辆、最后上报、速度和坐标可信度。',
|
||
action: '打开地图',
|
||
color: commandLocatedCount > 0 ? 'green' as const : 'orange' as const,
|
||
onClick: () => onOpenMap({ online: 'online' })
|
||
},
|
||
{
|
||
label: '回放这段时间',
|
||
value: `${formatCount(summary?.activeToday)} 今日活跃`,
|
||
detail: '用同一时间窗回放轨迹、速度、停驶和里程断点。',
|
||
action: '轨迹回放',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorHistory
|
||
},
|
||
{
|
||
label: '里程统计',
|
||
value: `${formatCount(serviceSummary?.totalVehicles)} 辆可统计`,
|
||
detail: '按车辆口径核对区间里程、日里程和异常记录。',
|
||
action: '查里程',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorMileage
|
||
},
|
||
{
|
||
label: '导出客户数据',
|
||
value: `${formatCount(summary?.frameToday)} 今日数据`,
|
||
detail: '按车辆、时间和字段裁剪导出历史位置与原始证据。',
|
||
action: '导出数据',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorRaw
|
||
},
|
||
{
|
||
label: '告警通知',
|
||
value: `${formatCount(summary?.issueVehicles)} 告警车辆`,
|
||
detail: highPriorityIssue ? `${qualityIssueLabel(highPriorityIssue.issueType)} / ${priorityIssueVehicleLabel(highPriorityIssue)}` : '暂无高优先级告警。',
|
||
action: '通知闭环',
|
||
color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const,
|
||
onClick: () => onOpenQuality(highPriorityIssue?.issueType ? { issueType: highPriorityIssue.issueType } : {})
|
||
}
|
||
];
|
||
const customerServiceCommandItems = [
|
||
{
|
||
label: '实时地图',
|
||
value: `${formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles)} 在线`,
|
||
detail: `${commandLocatedCount.toLocaleString()} 辆有有效定位,优先回答车辆在哪里。`,
|
||
action: '进入地图',
|
||
color: 'green' as const,
|
||
onClick: () => onOpenMap({ online: 'online' })
|
||
},
|
||
{
|
||
label: '轨迹回放',
|
||
value: `${formatCount(summary?.activeToday)} 活跃`,
|
||
detail: `${dayRangeText(timeMonitorScopeValue.dateFrom, timeMonitorScopeValue.dateTo)},按车辆复盘路线、速度和里程断点。`,
|
||
action: '回放轨迹',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorHistory
|
||
},
|
||
{
|
||
label: '统计查询',
|
||
value: `${formatCount(serviceSummary?.totalVehicles)} 车辆`,
|
||
detail: '按车辆口径核对区间里程、日统计和异常来源。',
|
||
action: '查统计',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorMileage
|
||
},
|
||
{
|
||
label: '时间窗监控',
|
||
value: dayRangeText(timeMonitorScopeValue.dateFrom, timeMonitorScopeValue.dateTo),
|
||
detail: '同一时间窗贯穿轨迹、统计、历史导出和告警复盘。',
|
||
action: '自定义时间',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorHistory
|
||
},
|
||
{
|
||
label: '数据导出',
|
||
value: `${formatCount(summary?.frameToday)} 今日数据`,
|
||
detail: '按车辆、时间和字段裁剪位置、原始记录和解析字段。',
|
||
action: '查询导出',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorRaw
|
||
},
|
||
{
|
||
label: '告警通知',
|
||
value: `${formatCount(summary?.issueVehicles)} 告警`,
|
||
detail: highPriorityIssue ? `${qualityIssueLabel(highPriorityIssue.issueType)} / ${priorityIssueVehicleLabel(highPriorityIssue)}` : '暂无高优先级告警。',
|
||
action: '闭环告警',
|
||
color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const,
|
||
onClick: () => onOpenQuality(highPriorityIssue?.issueType ? { issueType: highPriorityIssue.issueType } : {})
|
||
}
|
||
];
|
||
const customerFleetGroupItems = [
|
||
{
|
||
label: '在线运营组',
|
||
value: `${formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles)} 在线`,
|
||
detail: '给调度和客服复用:默认进入在线车辆地图,先回答客户车辆在哪里。',
|
||
action: '打开地图',
|
||
color: 'green' as const,
|
||
onClick: () => onOpenMap({ online: 'online' })
|
||
},
|
||
{
|
||
label: '告警处置组',
|
||
value: `${formatCount(summary?.issueVehicles)} 告警`,
|
||
detail: '给运维和业务负责人复用:只看需要通知、升级和验收的车辆。',
|
||
action: '告警通知',
|
||
color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const,
|
||
onClick: () => onOpenQuality(highPriorityIssue?.issueType ? { issueType: highPriorityIssue.issueType } : {})
|
||
},
|
||
{
|
||
label: '身份维护组',
|
||
value: `${formatCount(serviceSummary?.identityRequiredVehicles)} 待绑定`,
|
||
detail: '给数据维护人员复用:补齐 VIN、车牌、手机号和客户可查身份。',
|
||
action: '维护车辆',
|
||
color: (serviceSummary?.identityRequiredVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const,
|
||
onClick: () => onOpenVehicles({ serviceStatus: 'identity_required' })
|
||
},
|
||
{
|
||
label: '报表交付组',
|
||
value: `${formatCount(summary?.frameToday)} 今日数据`,
|
||
detail: '给客户交付人员复用:按同一车辆范围导出历史、轨迹、字段和统计证据。',
|
||
action: '历史导出',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorRaw
|
||
}
|
||
];
|
||
const customerSlaItems = [
|
||
{
|
||
label: '断链发现',
|
||
value: '1 分钟内',
|
||
detail: 'Redis 在线状态按 1 分钟 TTL 形成准实时断链判断,优先确认车辆是否仍在上报。',
|
||
action: '在线状态',
|
||
color: 'green' as const,
|
||
onClick: () => onOpenRealtime({ online: 'offline' })
|
||
},
|
||
{
|
||
label: '异常通知',
|
||
value: `${formatCount(summary?.issueVehicles)} 告警`,
|
||
detail: highPriorityIssue ? `${qualityIssueLabel(highPriorityIssue.issueType)} 进入通知和升级闭环。` : '暂无高优先级告警,保持例行巡检。',
|
||
action: '通知闭环',
|
||
color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const,
|
||
onClick: () => onOpenQuality(highPriorityIssue?.issueType ? { issueType: highPriorityIssue.issueType } : {})
|
||
},
|
||
{
|
||
label: '恢复验收',
|
||
value: '轨迹/统计可查',
|
||
detail: '恢复不是只看链路 active,还要确认实时、轨迹、里程统计和历史证据都能打开。',
|
||
action: '验收证据',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorHistory
|
||
},
|
||
{
|
||
label: '数据交付',
|
||
value: `${formatCount(summary?.frameToday)} 今日数据`,
|
||
detail: '按客户车辆和时间窗导出历史位置、原始记录、解析字段和统计证据。',
|
||
action: '导出证据',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorRaw
|
||
}
|
||
];
|
||
const vehicleHealthEnergyItems = [
|
||
{
|
||
label: '实时工况',
|
||
value: `${formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles)} 在线`,
|
||
detail: '查看车辆最新速度、SOC、总里程、在线状态和多来源实时字段。',
|
||
action: '实时监控',
|
||
color: 'green' as const,
|
||
onClick: () => onOpenRealtime({ online: 'online' })
|
||
},
|
||
{
|
||
label: 'SOC/氢能字段',
|
||
value: `${formatCount(summary?.frameToday)} 今日数据`,
|
||
detail: '从历史字段中回查 SOC、燃料电池、氢耗和关键工况字段。',
|
||
action: '字段证据',
|
||
color: 'blue' as const,
|
||
onClick: () => onOpenHistory({ tab: 'fields', includeFields: 'true' })
|
||
},
|
||
{
|
||
label: '异常维护',
|
||
value: `${formatCount(summary?.issueVehicles)} 告警`,
|
||
detail: highPriorityIssue ? `${qualityIssueLabel(highPriorityIssue.issueType)} 需要进入告警闭环。` : '暂无高优先级异常,继续观察车辆健康状态。',
|
||
action: '告警事件',
|
||
color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const,
|
||
onClick: () => onOpenQuality(highPriorityIssue?.issueType ? { issueType: highPriorityIssue.issueType } : {})
|
||
},
|
||
{
|
||
label: '里程能耗',
|
||
value: dayRangeText(timeMonitorScopeValue.dateFrom, timeMonitorScopeValue.dateTo),
|
||
detail: '用同一时间窗核对里程、运行状态和能耗相关统计口径。',
|
||
action: '统计查询',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorMileage
|
||
}
|
||
];
|
||
const dispatchOperationsHighlights = [
|
||
{
|
||
label: '实时地图',
|
||
value: `${commandLocatedCount.toLocaleString()} 有定位`,
|
||
detail: '先看车辆空间分布、在线态势和最新位置。',
|
||
action: '打开地图',
|
||
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)}` : '没有高优先级告警,保持巡检。',
|
||
action: '处理告警',
|
||
color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const,
|
||
onClick: () => onOpenQuality(highPriorityIssue?.issueType ? { issueType: highPriorityIssue.issueType } : {})
|
||
}
|
||
];
|
||
const dispatchOperationsTasks = [
|
||
{
|
||
title: '看全域位置',
|
||
value: `${formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles)} 在线`,
|
||
detail: '打开实时地图,确认在线车辆、有效坐标和重点区域。',
|
||
action: '实时地图',
|
||
color: 'green' as const,
|
||
onClick: () => onOpenMap({ online: 'online' })
|
||
},
|
||
{
|
||
title: '回放重点轨迹',
|
||
value: `${formatCount(summary?.activeToday)} 活跃`,
|
||
detail: '按车辆和时间窗回放路线,核对停驶、偏航和里程断点。',
|
||
action: '轨迹回放',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorHistory
|
||
},
|
||
{
|
||
title: '核对时间窗里程',
|
||
value: `${formatCount(serviceSummary?.totalVehicles)} 车辆`,
|
||
detail: '按同一时间范围核对区间里程、每日统计和 BI 口径。',
|
||
action: '统计查询',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorMileage
|
||
},
|
||
{
|
||
title: '导出客户证据',
|
||
value: `${formatCount(summary?.frameToday)} 帧`,
|
||
detail: '导出历史位置、原始记录、解析字段和告警说明。',
|
||
action: '历史导出',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorRaw
|
||
}
|
||
];
|
||
const deliveryToday = timeMonitorScopeValue.dateTo || todayDateString();
|
||
const customerTimeRangePresets = [
|
||
{
|
||
label: '最近一天',
|
||
range: `${previousDateString(deliveryToday)} 至 ${deliveryToday}`,
|
||
filters: { ...timeMonitorScopeValue, dateFrom: previousDateString(deliveryToday), dateTo: deliveryToday }
|
||
},
|
||
{
|
||
label: '最近三天',
|
||
range: `${offsetDateString(deliveryToday, -3)} 至 ${deliveryToday}`,
|
||
filters: { ...timeMonitorScopeValue, dateFrom: offsetDateString(deliveryToday, -3), dateTo: deliveryToday }
|
||
},
|
||
{
|
||
label: '本周',
|
||
range: `${weekStartDateString(deliveryToday)} 至 ${deliveryToday}`,
|
||
filters: { ...timeMonitorScopeValue, dateFrom: weekStartDateString(deliveryToday), dateTo: deliveryToday }
|
||
},
|
||
{
|
||
label: '自定义复盘',
|
||
range: '当前范围',
|
||
filters: timeMonitorScopeValue
|
||
}
|
||
];
|
||
const deliveryScopeText = `${timeMonitorScopeValue.keyword || '全部车辆'} / ${timeMonitorScopeValue.protocol || '全部数据通道'} / ${timeMonitorScopeValue.dateFrom || '-'} 至 ${timeMonitorScopeValue.dateTo || '-'}`;
|
||
const customerDeliveryWorkbenchItems = [
|
||
{
|
||
label: '轨迹回放',
|
||
value: `${formatCount(summary?.activeToday)} 活跃`,
|
||
detail: '回放同一时间窗内的路线、速度、停驶和里程断点。',
|
||
action: '回放轨迹',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorHistory
|
||
},
|
||
{
|
||
label: '里程统计',
|
||
value: `${formatCount(serviceSummary?.totalVehicles)} 车辆`,
|
||
detail: '核对区间里程、每日统计和 BI 展示口径。',
|
||
action: '统计查询',
|
||
color: 'green' as const,
|
||
onClick: openTimeMonitorMileage
|
||
},
|
||
{
|
||
label: '历史明细',
|
||
value: `${formatCount(summary?.frameToday)} 帧`,
|
||
detail: '查询位置历史、原始记录和解析字段,作为客户证据。',
|
||
action: '历史导出',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorRaw
|
||
},
|
||
{
|
||
label: '告警说明',
|
||
value: `${formatCount(summary?.issueVehicles)} 告警`,
|
||
detail: highPriorityIssue ? `${qualityIssueLabel(highPriorityIssue.issueType)} / ${priorityIssueVehicleLabel(highPriorityIssue)}` : '当前没有高优先级告警,可作为无异常说明。',
|
||
action: '告警通知',
|
||
color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const,
|
||
onClick: openTimeMonitorAlerts
|
||
}
|
||
];
|
||
const customerPrimaryFlowItems = [
|
||
{
|
||
label: '实时地图',
|
||
value: `${formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles)} 在线`,
|
||
detail: '先确认车辆在哪里、是否在线、是否有有效坐标。',
|
||
action: '打开地图',
|
||
color: 'green' as const,
|
||
onClick: () => onOpenMap({ online: 'online' })
|
||
},
|
||
{
|
||
label: '时间窗复盘',
|
||
value: dayRangeText(timeMonitorScopeValue.dateFrom, timeMonitorScopeValue.dateTo),
|
||
detail: '用同一时间范围串起轨迹、位置、里程、历史明细和导出。',
|
||
action: '查询导出',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorHistory
|
||
},
|
||
{
|
||
label: '里程统计',
|
||
value: `${formatCount(serviceSummary?.totalVehicles)} 车辆`,
|
||
detail: '以车辆为对象核对区间里程、每日统计和 BI 口径。',
|
||
action: '统计查询',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorMileage
|
||
},
|
||
{
|
||
label: '告警通知',
|
||
value: `${formatCount(summary?.issueVehicles)} 告警`,
|
||
detail: highPriorityIssue ? `${qualityIssueLabel(highPriorityIssue.issueType)} / ${priorityIssueVehicleLabel(highPriorityIssue)}` : '无高优先级异常时可交付无异常说明。',
|
||
action: '通知处理',
|
||
color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const,
|
||
onClick: openTimeMonitorAlerts
|
||
}
|
||
];
|
||
const customerVehicleServiceHubItems = [
|
||
{
|
||
label: '实时地图',
|
||
value: `${formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles)} 在线`,
|
||
detail: '先确认车辆在哪里、是否在线、坐标是否可信。',
|
||
action: '看车在哪',
|
||
color: 'green' as const,
|
||
onClick: () => onOpenMap({ online: 'online' })
|
||
},
|
||
{
|
||
label: '轨迹回放',
|
||
value: `${formatCount(summary?.activeToday)} 活跃`,
|
||
detail: '按车辆和时间窗回放路线、停驶、速度和里程断点。',
|
||
action: '回放路线',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorHistory
|
||
},
|
||
{
|
||
label: '里程统计',
|
||
value: dayRangeText(timeMonitorScopeValue.dateFrom, timeMonitorScopeValue.dateTo),
|
||
detail: '核对区间里程、每日里程和客户 BI 统计口径。',
|
||
action: '核对里程',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorMileage
|
||
},
|
||
{
|
||
label: '数据导出',
|
||
value: `${formatCount(summary?.frameToday)} 今日数据`,
|
||
detail: '导出位置、原始记录、解析字段和告警说明。',
|
||
action: '交付证据',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorRaw
|
||
},
|
||
{
|
||
label: '告警通知',
|
||
value: `${formatCount(summary?.issueVehicles)} 告警`,
|
||
detail: highPriorityIssue ? `${qualityIssueLabel(highPriorityIssue.issueType)} / ${priorityIssueVehicleLabel(highPriorityIssue)}` : '没有高优先级异常时可直接说明无异常。',
|
||
action: '闭环通知',
|
||
color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const,
|
||
onClick: () => onOpenQuality(highPriorityIssue?.issueType ? { issueType: highPriorityIssue.issueType } : {})
|
||
}
|
||
];
|
||
const operationsEvidenceItems = [
|
||
{
|
||
label: '链路健康',
|
||
value: unhealthyLinkCount > 0 ? `${formatCount(unhealthyLinkCount)} 异常` : '正常',
|
||
detail: '解释车辆实时、轨迹、统计或导出不可用时的链路证据。',
|
||
action: '查看告警',
|
||
color: unhealthyLinkCount > 0 ? 'orange' as const : 'green' as const,
|
||
onClick: () => onOpenQuality(highPriorityIssue?.issueType ? { issueType: highPriorityIssue.issueType } : {})
|
||
},
|
||
{
|
||
label: '消息积压',
|
||
value: formatLag(summary?.kafkaLag),
|
||
detail: '判断接收、解析和历史落地是否存在排队。',
|
||
action: '查看运维',
|
||
color: (summary?.kafkaLag ?? 0) > 0 ? 'orange' as const : 'green' as const,
|
||
onClick: () => onOpenQuality()
|
||
},
|
||
{
|
||
label: '缓存在线',
|
||
value: formatCount(opsHealth?.redisOnlineKeys),
|
||
detail: '辅助解释在线车辆数和实时状态是否可信。',
|
||
action: '缓存证据',
|
||
color: (opsHealth?.redisOnlineKeys ?? 0) > 0 ? 'blue' as const : 'orange' as const,
|
||
onClick: () => onOpenRealtime({ online: 'online' })
|
||
}
|
||
];
|
||
const customerVehicleServiceDashboardItems = [
|
||
{
|
||
question: '车辆在哪里',
|
||
value: `${commandLocatedCount.toLocaleString()} 有定位`,
|
||
detail: '进入实时地图查看车辆分布、在线状态、最后上报和当前位置。',
|
||
action: '打开地图',
|
||
color: commandLocatedCount > 0 ? 'green' as const : 'orange' as const,
|
||
onClick: () => onOpenMap({ online: 'online' })
|
||
},
|
||
{
|
||
question: '轨迹能否回放',
|
||
value: `${formatCount(summary?.activeToday)} 活跃`,
|
||
detail: '按车辆和自定义时间窗回放路线、速度、停驶和里程断点。',
|
||
action: '轨迹回放',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorHistory
|
||
},
|
||
{
|
||
question: '里程怎么算',
|
||
value: `${formatCount(serviceSummary?.totalVehicles)} 车辆`,
|
||
detail: '查询区间里程、每日里程和统计闭合情况,支撑 BI 口径复核。',
|
||
action: '统计查询',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorMileage
|
||
},
|
||
{
|
||
question: '数据怎么导出',
|
||
value: `${formatCount(summary?.frameToday)} 今日数据`,
|
||
detail: '按车辆、时间和字段裁剪导出位置历史、原始记录和解析证据。',
|
||
action: '历史导出',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorRaw
|
||
},
|
||
{
|
||
question: '异常谁处理',
|
||
value: `${formatCount(summary?.issueVehicles)} 告警`,
|
||
detail: highPriorityIssue ? `${qualityIssueLabel(highPriorityIssue.issueType)} / ${priorityIssueVehicleLabel(highPriorityIssue)}` : '暂无高优先级告警,可保持日常监控。',
|
||
action: '告警通知',
|
||
color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const,
|
||
onClick: () => onOpenQuality(highPriorityIssue?.issueType ? { issueType: highPriorityIssue.issueType } : {})
|
||
}
|
||
];
|
||
const customerVehicleServicePathItems = [
|
||
{
|
||
label: '地图监控',
|
||
question: '在线在哪里',
|
||
value: `${formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles)} 在线`,
|
||
detail: `${commandLocatedCount.toLocaleString()} 辆有有效坐标,先让客户看到车辆位置和在线状态。`,
|
||
action: '打开地图',
|
||
color: 'green' as const,
|
||
onClick: () => onOpenMap({ online: 'online' })
|
||
},
|
||
{
|
||
label: '时间窗',
|
||
question: '这段时间怎么跑',
|
||
value: dayRangeText(timeMonitorScopeValue.dateFrom, timeMonitorScopeValue.dateTo),
|
||
detail: '用同一时间窗串起轨迹、位置、里程、告警和导出证据。',
|
||
action: '开始复盘',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorHistory
|
||
},
|
||
{
|
||
label: '里程统计',
|
||
question: '里程是否可信',
|
||
value: `${formatCount(serviceSummary?.totalVehicles)} 车辆`,
|
||
detail: '面向客户核对区间里程、每日汇总和 BI 统计口径。',
|
||
action: '核对统计',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorMileage
|
||
},
|
||
{
|
||
label: '告警通知',
|
||
question: '异常是否通知',
|
||
value: `${formatCount(summary?.issueVehicles)} 告警`,
|
||
detail: highPriorityIssue ? `${qualityIssueLabel(highPriorityIssue.issueType)} / ${priorityIssueVehicleLabel(highPriorityIssue)}` : '暂无高优先级告警,保持日常巡检。',
|
||
action: '查看告警',
|
||
color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const,
|
||
onClick: () => onOpenQuality(highPriorityIssue?.issueType ? { issueType: highPriorityIssue.issueType } : {})
|
||
},
|
||
{
|
||
label: '数据导出',
|
||
question: '数据能否交付',
|
||
value: `${formatCount(summary?.frameToday)} 今日数据`,
|
||
detail: '按车辆、时间窗和字段裁剪导出位置、原始记录和解析证据。',
|
||
action: '导出证据',
|
||
color: 'blue' as const,
|
||
onClick: openTimeMonitorRaw
|
||
}
|
||
];
|
||
|
||
return (
|
||
<div className="vp-page">
|
||
<PageHeader
|
||
title={focusMode === 'time-window' ? '自定义时间监控' : '车辆服务工作台'}
|
||
description={focusMode === 'time-window'
|
||
? '把客户选择的一辆车和一个时间窗固定下来,同时进入实时地图、轨迹回放、里程统计、历史导出和告警复盘。'
|
||
: '面向客户的车辆地图、实时监控、轨迹回放、统计查询、自定义时间窗复盘、历史查询、告警通知和数据导出'}
|
||
/>
|
||
<Spin spinning={loading}>
|
||
{focusMode === 'overview' ? (
|
||
<section className="vp-customer-service-path" aria-label="客户车辆服务路径">
|
||
<div className="vp-customer-service-path-copy">
|
||
<Space wrap>
|
||
<Tag color="blue">客户车辆服务路径</Tag>
|
||
<Tag color="green">{vehicleServiceOnlineText(serviceSummary, summary)}</Tag>
|
||
<Tag color={(summary?.issueVehicles ?? 0) > 0 ? 'orange' : 'green'}>{formatCount(summary?.issueVehicles)} 告警</Tag>
|
||
</Space>
|
||
<Typography.Title heading={3} style={{ margin: 0 }}>
|
||
客户车辆服务路径
|
||
</Typography.Title>
|
||
<Typography.Text type="secondary">
|
||
客户入口只回答车辆问题:在线在哪里、这段时间怎么跑、里程是否可信、异常是否通知、数据能否交付。
|
||
</Typography.Text>
|
||
<Typography.Text type="secondary" className="vp-customer-service-path-evidence">
|
||
32960 / 808 / MQTT 只是证据通道
|
||
</Typography.Text>
|
||
<Typography.Text type="secondary">
|
||
默认沉到车辆可信度、历史明细和运维解释里。
|
||
</Typography.Text>
|
||
</div>
|
||
<div className="vp-customer-service-path-grid">
|
||
{customerVehicleServicePathItems.map((item) => (
|
||
<button
|
||
key={item.label}
|
||
type="button"
|
||
className="vp-customer-service-path-item"
|
||
onClick={item.onClick}
|
||
aria-label={`客户车辆服务路径 ${item.label} ${item.question} ${item.action}`}
|
||
>
|
||
<Tag color={item.color}>{item.label}</Tag>
|
||
<strong>{item.question}</strong>
|
||
<span>{item.value}</span>
|
||
<p>{item.detail}</p>
|
||
<em>{item.action}</em>
|
||
</button>
|
||
))}
|
||
</div>
|
||
<div className="vp-customer-service-overview" aria-label="客户服务总览">
|
||
<div className="vp-customer-service-overview-copy">
|
||
<Typography.Text strong>客户服务总览</Typography.Text>
|
||
<Typography.Text type="secondary">
|
||
地图、轨迹、统计、导出是客户能直接使用的车辆服务;协议来源只进入证据和追溯。
|
||
</Typography.Text>
|
||
</div>
|
||
<div className="vp-customer-service-overview-grid">
|
||
{customerHeroActions.map((item) => (
|
||
<button
|
||
key={item.title}
|
||
type="button"
|
||
className="vp-customer-service-overview-item"
|
||
onClick={item.onClick}
|
||
aria-label={`客户服务总览 ${item.title} ${item.action}`}
|
||
>
|
||
<strong>{item.title}</strong>
|
||
<span>{item.value}</span>
|
||
<p>{item.detail}</p>
|
||
<em>{item.action}</em>
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
<div className="vp-customer-service-next" aria-label="建议下一步">
|
||
<div className="vp-customer-service-next-copy">
|
||
<Typography.Text strong>建议下一步</Typography.Text>
|
||
<Typography.Text type="secondary">
|
||
根据当前告警、车辆在线和时间窗状态,优先给客户一个明确行动。
|
||
</Typography.Text>
|
||
</div>
|
||
<div className="vp-customer-service-next-grid">
|
||
{customerNextActions.map((item) => (
|
||
<button
|
||
key={`${item.level}-${item.title}`}
|
||
type="button"
|
||
className="vp-customer-service-next-item"
|
||
onClick={item.onClick}
|
||
aria-label={`建议下一步 ${item.level} ${item.title} ${item.action}`}
|
||
>
|
||
<Tag color={item.color}>{item.level}</Tag>
|
||
<strong>{item.title}</strong>
|
||
<span>{item.detail}</span>
|
||
<em>{item.action}</em>
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
<div className="vp-customer-service-scope" aria-label="客户服务范围">
|
||
<div className="vp-customer-service-scope-copy">
|
||
<Typography.Text strong>当前服务范围</Typography.Text>
|
||
<Typography.Text type="secondary">
|
||
{customerServiceScopeLabel}
|
||
</Typography.Text>
|
||
</div>
|
||
<div className="vp-customer-service-scope-actions">
|
||
<Button size="small" theme="solid" type="primary" aria-label="客户服务范围 复制范围" onClick={onCopyCustomerScope}>
|
||
复制范围
|
||
</Button>
|
||
<Button size="small" theme="light" type="primary" aria-label="客户服务范围 保存视图" onClick={onSaveCustomerView}>
|
||
保存视图
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
) : null}
|
||
{focusMode === 'time-window' ? (
|
||
<section className="vp-time-monitor-dedicated" aria-label="时间窗监控">
|
||
<div className="vp-time-monitor-dedicated-copy">
|
||
<Space wrap>
|
||
<Tag color="blue">时间窗监控</Tag>
|
||
<Tag color={timeMonitorHasRange ? 'green' : 'orange'}>{timeMonitorWindowLabel}</Tag>
|
||
<Tag color={timeMonitorDeliveryStatus.color}>{timeMonitorDeliveryStatus.label}</Tag>
|
||
</Space>
|
||
<Typography.Title heading={4} style={{ margin: 0 }}>
|
||
当前监控范围:{timeMonitorScopeValue.keyword || '全部车辆'} / {timeMonitorScopeValue.protocol || '全部数据通道'} / {timeMonitorScopeValue.dateFrom || '-'} 至 {timeMonitorScopeValue.dateTo || '-'}
|
||
</Typography.Title>
|
||
<Typography.Text type="secondary">
|
||
用同一组筛选条件贯穿地图定位、轨迹复盘、里程对账、历史证据导出和告警说明,避免客户跨页面重复输入。
|
||
</Typography.Text>
|
||
</div>
|
||
<div className="vp-time-monitor-dedicated-grid">
|
||
{dedicatedTimeMonitorActions.map((item) => (
|
||
<button
|
||
key={item.label}
|
||
type="button"
|
||
className="vp-time-monitor-dedicated-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>
|
||
) : null}
|
||
<details className="vp-dashboard-secondary-details vp-dashboard-customer-advanced">
|
||
<summary>
|
||
<span>客户进阶工作台</span>
|
||
<small>地图大屏、指挥台、车辆服务驾驶舱和态势分析默认收起;客户首页先回答车辆在哪里、时间窗怎么查、里程怎么核、数据怎么交付。</small>
|
||
</summary>
|
||
<div className="vp-dashboard-secondary-body">
|
||
<section className="vp-vehicle-command-center" aria-label="车辆服务指挥台">
|
||
<div className="vp-vehicle-command-map">
|
||
<VehicleMap
|
||
points={commandMapPoints}
|
||
maxFallbackPoints={90}
|
||
heightClassName="vp-vehicle-command-map-canvas"
|
||
fallbackLabel="显示车辆服务指挥台坐标预览"
|
||
/>
|
||
<div className="vp-vehicle-command-map-status">
|
||
<Tag color={amapConfigured ? 'green' : 'orange'}>{amapConfigured ? '高德地图可用' : '坐标预览'}</Tag>
|
||
<Tag color={(summary?.issueVehicles ?? 0) > 0 ? 'orange' : 'green'}>{formatCount(summary?.issueVehicles)} 告警</Tag>
|
||
</div>
|
||
</div>
|
||
<div className="vp-vehicle-command-main">
|
||
<Space wrap>
|
||
<Tag color="blue">车辆服务指挥台</Tag>
|
||
<Tag color="green">客户首屏</Tag>
|
||
</Space>
|
||
<Typography.Title heading={3} style={{ margin: 0 }}>
|
||
10 秒内回答客户最关心的事:车辆在哪里、哪辆车异常、这段时间怎么跑、数据能不能导出。
|
||
</Typography.Title>
|
||
<div className="vp-vehicle-command-actions">
|
||
{customerTaskNavigation.map((item) => (
|
||
<button
|
||
key={item.title}
|
||
type="button"
|
||
className="vp-vehicle-command-action"
|
||
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-vehicle-command-queue">
|
||
<div className="vp-vehicle-command-queue-head">
|
||
<Typography.Text strong>优先处理车辆</Typography.Text>
|
||
<Typography.Text type="secondary">按影响客户看车、轨迹、统计和导出的程度排序。</Typography.Text>
|
||
</div>
|
||
<div className="vp-vehicle-command-queue-grid">
|
||
{vehiclePriorityQueue.map((item) => (
|
||
<button
|
||
key={item.title}
|
||
type="button"
|
||
className="vp-vehicle-command-queue-item"
|
||
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>
|
||
<section className="vp-customer-vehicle-service-hub" aria-label="客户车辆服务中枢">
|
||
<div className="vp-customer-vehicle-service-summary">
|
||
<Space wrap>
|
||
<Tag color="blue">客户车辆服务中枢</Tag>
|
||
<Tag color={amapConfigured ? 'green' : 'orange'}>{amapConfigured ? '地图可用' : '坐标预览'}</Tag>
|
||
<Tag color={(summary?.issueVehicles ?? 0) > 0 ? 'orange' : 'green'}>{formatCount(summary?.issueVehicles)} 告警</Tag>
|
||
</Space>
|
||
<Typography.Title heading={4} style={{ margin: 0 }}>
|
||
客户只关心车怎么服务:在哪里、是否在线、这段时间怎么跑、里程是否可信、异常是否已通知、数据能否交付。
|
||
</Typography.Title>
|
||
<Typography.Text type="secondary">
|
||
运维接入、队列、存储、协议解析降级为证据层,默认不打断客户主流程。
|
||
</Typography.Text>
|
||
</div>
|
||
<div className="vp-customer-vehicle-service-grid">
|
||
{customerVehicleServiceHubItems.map((item) => (
|
||
<button
|
||
key={item.label}
|
||
type="button"
|
||
className="vp-customer-vehicle-service-item"
|
||
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-customer-map-situation" aria-label="车辆地图态势">
|
||
<div className="vp-customer-map-situation-map">
|
||
<VehicleMap
|
||
points={commandMapPoints}
|
||
maxFallbackPoints={120}
|
||
heightClassName="vp-customer-map-situation-canvas"
|
||
fallbackLabel="显示车辆实时坐标预览"
|
||
/>
|
||
</div>
|
||
<div className="vp-customer-map-situation-panel">
|
||
<Space wrap>
|
||
<Tag color="blue">车辆地图态势</Tag>
|
||
<Tag color={amapConfigured ? 'green' : 'orange'}>{amapConfigured ? '高德地图可用' : '坐标预览'}</Tag>
|
||
<Tag color={(summary?.issueVehicles ?? 0) > 0 ? 'orange' : 'green'}>{formatCount(summary?.issueVehicles)} 告警</Tag>
|
||
</Space>
|
||
<Typography.Title heading={5} style={{ margin: 0 }}>先看在线车辆在哪里,再决定是否回放轨迹、核对里程、导出数据或通知处理。</Typography.Title>
|
||
<div className="vp-customer-map-situation-metrics">
|
||
{customerCockpitMetrics.slice(1, 4).map((item) => (
|
||
<button key={item.label} type="button" onClick={item.onClick} aria-label={`车辆地图态势 ${item.label} ${item.value}`}>
|
||
<span>{item.label}</span>
|
||
<strong>{item.value}</strong>
|
||
</button>
|
||
))}
|
||
</div>
|
||
<Space wrap>
|
||
<Button size="small" theme="solid" type="primary" aria-label="车辆地图态势 打开实时地图" onClick={() => onOpenMap({ online: 'online' })}>打开实时地图</Button>
|
||
<Button size="small" theme="light" type="primary" aria-label="车辆地图态势 回放轨迹" onClick={openTimeMonitorHistory}>回放轨迹</Button>
|
||
<Button size="small" theme="light" type="primary" onClick={openTimeMonitorMileage}>统计查询</Button>
|
||
<Button size="small" theme="light" type="warning" onClick={() => onOpenQuality()}>告警通知</Button>
|
||
</Space>
|
||
</div>
|
||
</section>
|
||
<section className="vp-customer-vehicle-service-dashboard" aria-label="客户车辆服务驾驶舱">
|
||
<div className="vp-customer-vehicle-service-dashboard-copy">
|
||
<Space wrap>
|
||
<Tag color="blue">客户车辆服务驾驶舱</Tag>
|
||
<Tag color={amapConfigured ? 'green' : 'orange'}>{amapConfigured ? '高德地图可用' : '坐标预览'}</Tag>
|
||
<Tag color={(summary?.issueVehicles ?? 0) > 0 ? 'orange' : 'green'}>{formatCount(summary?.issueVehicles)} 告警</Tag>
|
||
</Space>
|
||
<Typography.Title heading={3} style={{ margin: 0 }}>
|
||
客户进来先看车辆,不先看协议。
|
||
</Typography.Title>
|
||
<Typography.Text type="secondary">
|
||
把实时地图、轨迹回放、里程统计、历史导出和告警通知收敛成一个可交付的车辆服务入口。
|
||
</Typography.Text>
|
||
<div className="vp-customer-vehicle-service-dashboard-status" aria-label="客户车辆服务驾驶舱状态">
|
||
<span>
|
||
<strong>{formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles)}</strong>
|
||
在线车辆
|
||
</span>
|
||
<span>
|
||
<strong>{dayRangeText(timeMonitorScopeValue.dateFrom, timeMonitorScopeValue.dateTo)}</strong>
|
||
服务时间窗
|
||
</span>
|
||
<span>
|
||
<strong>{focusVehicle?.label ?? '暂无重点车辆'}</strong>
|
||
今日重点
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div className="vp-customer-vehicle-service-dashboard-grid">
|
||
{customerVehicleServiceDashboardItems.map((item) => (
|
||
<button
|
||
key={item.question}
|
||
type="button"
|
||
className="vp-customer-vehicle-service-dashboard-item"
|
||
onClick={item.onClick}
|
||
aria-label={`客户车辆服务驾驶舱 ${item.question} ${item.value} ${item.action}`}
|
||
>
|
||
<Tag color={item.color}>{item.question}</Tag>
|
||
<strong>{item.value}</strong>
|
||
<span>{item.detail}</span>
|
||
<em>{item.action}</em>
|
||
</button>
|
||
))}
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</details>
|
||
<section className="vp-vehicle-service-cockpit" aria-label="车辆服务驾驶舱">
|
||
<div className="vp-vehicle-service-cockpit-copy">
|
||
<Space wrap>
|
||
<Tag color="blue">车辆服务驾驶舱</Tag>
|
||
<Tag color="green">客户主入口</Tag>
|
||
<Tag color={(summary?.issueVehicles ?? 0) > 0 ? 'orange' : 'green'}>{formatCount(summary?.issueVehicles)} 告警</Tag>
|
||
</Space>
|
||
<Typography.Title heading={4} style={{ margin: 0 }}>
|
||
先看车辆,不先看协议;客户进来先知道哪些车在线、在哪里、今天跑了多少、哪些异常要通知。
|
||
</Typography.Title>
|
||
<Typography.Text type="secondary">
|
||
32960、808 和宇通 MQTT 都沉到证据层;首页只围绕车辆地图、轨迹回放、里程统计、数据交付和告警通知组织动作。
|
||
</Typography.Text>
|
||
</div>
|
||
<div className="vp-vehicle-service-cockpit-grid">
|
||
{customerServiceCockpitItems.map((item) => (
|
||
<button
|
||
key={item.label}
|
||
type="button"
|
||
className="vp-vehicle-service-cockpit-item"
|
||
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>
|
||
<details className="vp-dashboard-secondary-details vp-dashboard-customer-service-playbook">
|
||
<summary>
|
||
<span>客户服务配置和说明</span>
|
||
<small>把蓝图、常用视图、处置说明和服务闭环收起,默认首页只保留地图、车辆、时间窗、导出和告警。</small>
|
||
</summary>
|
||
<div className="vp-dashboard-secondary-body">
|
||
<section className="vp-customer-platform-blueprint" aria-label="客户车辆服务中台蓝图">
|
||
<div className="vp-customer-platform-blueprint-copy">
|
||
<Space wrap>
|
||
<Tag color="blue">客户车辆服务中台蓝图</Tag>
|
||
<Tag color={amapConfigured ? 'green' : 'orange'}>{amapConfigured ? '高德地图可用' : '坐标预览'}</Tag>
|
||
<Tag color={(summary?.issueVehicles ?? 0) > 0 ? 'orange' : 'green'}>{formatCount(summary?.issueVehicles)} 告警</Tag>
|
||
</Space>
|
||
<Typography.Title heading={5} style={{ margin: 0 }}>
|
||
参考主流车联网平台的信息架构,把 Live Map、Route Replay、Geofence Alerts、Reports Export 和 Vehicle Health 收敛成客户可直接使用的车辆服务。
|
||
</Typography.Title>
|
||
<Typography.Text type="secondary">
|
||
三个接入来源继续作为证据层,客户首页只围绕车辆监控、时间窗复盘、告警通知和报表交付组织路径。
|
||
</Typography.Text>
|
||
</div>
|
||
<div className="vp-customer-platform-blueprint-grid">
|
||
{customerPlatformBlueprintItems.map((item) => (
|
||
<button
|
||
key={item.label}
|
||
type="button"
|
||
className="vp-customer-platform-blueprint-item"
|
||
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-customer-saved-view-library" aria-label="客户常用视图库">
|
||
<div className="vp-customer-saved-view-library-copy">
|
||
<Space wrap>
|
||
<Tag color="blue">客户常用视图库</Tag>
|
||
<Tag color="green">一个车辆服务入口</Tag>
|
||
</Space>
|
||
<Typography.Title heading={5} style={{ margin: 0 }}>
|
||
一个车辆服务入口,而不是三个数据源入口。
|
||
</Typography.Title>
|
||
<Typography.Text type="secondary">
|
||
把车队范围、时间窗、地图态势、轨迹回放、统计查询、导出模板和告警订阅保存成客户可复用的车辆服务入口。
|
||
</Typography.Text>
|
||
</div>
|
||
<div className="vp-customer-saved-view-library-grid">
|
||
{customerSavedViewItems.map((item) => (
|
||
<button
|
||
key={item.label}
|
||
type="button"
|
||
className="vp-customer-saved-view-library-item"
|
||
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-ten-second-triage" aria-label="十秒车辆处置台">
|
||
<div className="vp-ten-second-triage-copy">
|
||
<Space wrap>
|
||
<Tag color="blue">十秒车辆处置台</Tag>
|
||
<Tag color={(summary?.issueVehicles ?? 0) > 0 || (serviceSummary?.noDataVehicles ?? 0) > 0 ? 'orange' : 'green'}>先处理异常车辆</Tag>
|
||
</Space>
|
||
<Typography.Title heading={5} style={{ margin: 0 }}>
|
||
登录后先知道哪三类车辆最需要处理,再进入地图、车辆中心或告警通知。
|
||
</Typography.Title>
|
||
<Typography.Text type="secondary">
|
||
首页只保留能直接行动的优先级:告警车辆、无数据车辆、身份未绑定车辆;协议来源留在证据层解释原因。
|
||
</Typography.Text>
|
||
</div>
|
||
<div className="vp-ten-second-triage-grid">
|
||
{tenSecondTriageItems.map((item) => (
|
||
<button
|
||
key={item.label}
|
||
type="button"
|
||
className="vp-ten-second-triage-item"
|
||
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-customer-service-journey" aria-label="车辆服务闭环">
|
||
<div className="vp-customer-service-journey-copy">
|
||
<Tag color="blue">车辆服务闭环</Tag>
|
||
<Typography.Title heading={5} style={{ margin: 0 }}>客户从首页开始,只需要按这五步完成找车、看车、复盘、统计、导出和通知。</Typography.Title>
|
||
</div>
|
||
<div className="vp-customer-service-journey-steps">
|
||
{customerServiceJourneySteps.map((item) => (
|
||
<button
|
||
key={item.step}
|
||
type="button"
|
||
className="vp-customer-service-journey-step"
|
||
onClick={item.onClick}
|
||
aria-label={`车辆服务闭环 ${item.step} ${item.title} ${item.value} ${item.action}`}
|
||
>
|
||
<span>{item.step}</span>
|
||
<div>
|
||
<strong>{item.title}</strong>
|
||
<small>{item.value}</small>
|
||
<em>{item.detail}</em>
|
||
</div>
|
||
<Tag color={item.color}>{item.action}</Tag>
|
||
</button>
|
||
))}
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</details>
|
||
<section className="vp-dispatch-operations-home" aria-label="客户调度运营首页">
|
||
<div className="vp-dispatch-operations-map">
|
||
<div className="vp-dispatch-operations-copy">
|
||
<Space wrap>
|
||
<Tag color="blue">客户调度运营首页</Tag>
|
||
<Tag color={amapConfigured ? 'green' : 'orange'}>{amapConfigured ? '地图可用' : '坐标预览'}</Tag>
|
||
<Tag color={(summary?.issueVehicles ?? 0) > 0 ? 'orange' : 'green'}>{formatCount(summary?.issueVehicles)} 告警</Tag>
|
||
</Space>
|
||
<Typography.Title heading={4} style={{ margin: 0 }}>值班人员进来先看地图态势和异常队列,再进入轨迹回放、时间窗统计、历史导出和告警通知。</Typography.Title>
|
||
<Typography.Text type="secondary">
|
||
首页把车辆监控组织成一个调度工作台:地图负责看车,任务队列负责处理今天要交付和要解释的问题。
|
||
</Typography.Text>
|
||
</div>
|
||
<VehicleMap
|
||
points={commandMapPoints}
|
||
maxFallbackPoints={120}
|
||
heightClassName="vp-dispatch-operations-map-canvas"
|
||
fallbackLabel="显示车辆调度坐标预览"
|
||
/>
|
||
<div className="vp-dispatch-operations-highlights">
|
||
{dispatchOperationsHighlights.map((item) => (
|
||
<button
|
||
key={item.label}
|
||
type="button"
|
||
className="vp-dispatch-operations-highlight"
|
||
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-dispatch-operations-panel">
|
||
<div className="vp-dispatch-operations-panel-head">
|
||
<Typography.Text strong>今日车辆任务</Typography.Text>
|
||
<Typography.Text type="secondary">按客户最常用动作排序,不按协议来源排序。</Typography.Text>
|
||
</div>
|
||
<div className="vp-dispatch-operations-task-grid">
|
||
{dispatchOperationsTasks.map((item) => (
|
||
<button
|
||
key={item.title}
|
||
type="button"
|
||
className="vp-dispatch-operations-task"
|
||
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>
|
||
<section className="vp-customer-delivery-workbench" aria-label="客户查询导出交付台">
|
||
<div className="vp-customer-delivery-workbench-copy">
|
||
<Space wrap>
|
||
<Tag color="blue">客户查询导出交付台</Tag>
|
||
<Tag color={timeMonitorHasRange ? 'green' : 'orange'}>{dayRangeText(timeMonitorScopeValue.dateFrom, timeMonitorScopeValue.dateTo)}</Tag>
|
||
<Tag color={timeMonitorDeliveryStatus.color}>{timeMonitorDeliveryStatus.label}</Tag>
|
||
</Space>
|
||
<Typography.Title heading={5} style={{ margin: 0 }}>
|
||
先锁定车辆和时间窗,再选择轨迹、里程、历史明细或告警说明,最终交付一份客户能看懂的数据包。
|
||
</Typography.Title>
|
||
<div className="vp-customer-delivery-scope">
|
||
<span>当前交付范围</span>
|
||
<strong>{deliveryScopeText}</strong>
|
||
</div>
|
||
<div className="vp-customer-time-presets">
|
||
<span>常用时间范围</span>
|
||
<div>
|
||
{customerTimeRangePresets.map((item) => (
|
||
<button
|
||
key={item.label}
|
||
type="button"
|
||
onClick={() => setTimeMonitorFilters(item.filters)}
|
||
aria-label={`客户常用时间范围 ${item.label} ${item.range} 应用`}
|
||
>
|
||
<strong>{item.label}</strong>
|
||
<small>{item.range}</small>
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="vp-customer-delivery-workbench-grid">
|
||
{customerDeliveryWorkbenchItems.map((item) => (
|
||
<button
|
||
key={item.label}
|
||
type="button"
|
||
className="vp-customer-delivery-workbench-item"
|
||
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-customer-service-separation" aria-label="客户主流程与运维证据层">
|
||
<div className="vp-customer-primary-flow">
|
||
<div className="vp-customer-primary-flow-copy">
|
||
<Space wrap>
|
||
<Tag color="blue">客户主流程</Tag>
|
||
<Tag color="green">一个车辆服务</Tag>
|
||
</Space>
|
||
<Typography.Title heading={5} style={{ margin: 0 }}>默认展示车辆地图、时间窗、里程、导出和告警;三类数据源只作为二级证据。</Typography.Title>
|
||
</div>
|
||
<div className="vp-customer-primary-flow-grid">
|
||
{customerPrimaryFlowItems.map((item) => (
|
||
<button
|
||
key={item.label}
|
||
type="button"
|
||
className="vp-customer-primary-flow-item"
|
||
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-operations-evidence-layer">
|
||
<div className="vp-operations-evidence-layer-copy">
|
||
<Space wrap>
|
||
<Tag color="grey">运维证据层</Tag>
|
||
<Tag color={unhealthyLinkCount > 0 ? 'orange' : 'green'}>{unhealthyLinkCount > 0 ? `${formatCount(unhealthyLinkCount)} 异常` : '链路正常'}</Tag>
|
||
</Space>
|
||
<Typography.Text type="secondary">
|
||
用于解释车辆服务不可用的链路、队列、缓存、存储和来源证据,不作为客户默认入口。
|
||
</Typography.Text>
|
||
</div>
|
||
<div className="vp-operations-evidence-layer-grid">
|
||
{operationsEvidenceItems.map((item) => (
|
||
<button
|
||
key={item.label}
|
||
type="button"
|
||
className="vp-operations-evidence-layer-item"
|
||
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>
|
||
</section>
|
||
<section className="vp-customer-service-command" aria-label="客户车辆服务总控台">
|
||
<div className="vp-customer-service-command-main">
|
||
<div className="vp-customer-service-command-copy">
|
||
<Space wrap>
|
||
<Tag color="blue">客户车辆服务总控台</Tag>
|
||
<Tag color="grey">来源证据收敛层</Tag>
|
||
<Tag color={amapConfigured ? 'green' : 'orange'}>{amapConfigured ? '地图可用' : '坐标预览'}</Tag>
|
||
<Tag color={(summary?.issueVehicles ?? 0) > 0 ? 'orange' : 'green'}>{formatCount(summary?.issueVehicles)} 告警</Tag>
|
||
</Space>
|
||
<Typography.Title heading={4} style={{ margin: 0 }}>把接入来源收敛成一套车辆服务。</Typography.Title>
|
||
<Typography.Text type="secondary">
|
||
协议来源只用于证明车辆服务可信度,客户主流程仍然从车辆、地图、时间窗、统计、导出和告警进入。
|
||
</Typography.Text>
|
||
<Typography.Text type="secondary">
|
||
三个接入来源统一沉到证据层,首页只回答客户怎么监控车辆、回放轨迹、核对里程、导出数据和处理告警。
|
||
</Typography.Text>
|
||
</div>
|
||
<div className="vp-customer-service-command-evidence">
|
||
<span>来源证据:GB32960 / JT808 / YUTONG_MQTT</span>
|
||
<strong>{vehicleServiceOnlineText(serviceSummary, summary)}</strong>
|
||
<small>{dayRangeText(timeMonitorScopeValue.dateFrom, timeMonitorScopeValue.dateTo)}</small>
|
||
</div>
|
||
</div>
|
||
<div className="vp-customer-service-command-grid">
|
||
{customerServiceCommandItems.map((item) => (
|
||
<button
|
||
key={item.label}
|
||
type="button"
|
||
className="vp-customer-service-command-item"
|
||
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-customer-fleet-groups" aria-label="客户车队分组工作台">
|
||
<div className="vp-customer-fleet-groups-copy">
|
||
<Space wrap>
|
||
<Tag color="blue">客户车队分组工作台</Tag>
|
||
<Tag color="green">{formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles)} 在线</Tag>
|
||
<Tag color={(summary?.issueVehicles ?? 0) > 0 ? 'orange' : 'green'}>{formatCount(summary?.issueVehicles)} 告警</Tag>
|
||
</Space>
|
||
<Typography.Title heading={5} style={{ margin: 0 }}>按客户责任范围组织车辆池:在线运营、告警处置、身份维护和报表交付都能保存成复用视图。</Typography.Title>
|
||
<Typography.Text type="secondary">
|
||
客户不是每次都从全量车辆开始筛选;常用车队视图应该直接带到地图、通知、车辆中心和历史导出。
|
||
</Typography.Text>
|
||
</div>
|
||
<div className="vp-customer-fleet-groups-grid">
|
||
{customerFleetGroupItems.map((item) => (
|
||
<button
|
||
key={item.label}
|
||
type="button"
|
||
className="vp-customer-fleet-group-item"
|
||
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-customer-service-sla" aria-label="客户服务 SLA 承诺">
|
||
<div className="vp-customer-service-sla-copy">
|
||
<Space wrap>
|
||
<Tag color="blue">客户服务 SLA 承诺</Tag>
|
||
<Tag color={(summary?.issueVehicles ?? 0) > 0 ? 'orange' : 'green'}>{formatCount(summary?.issueVehicles)} 告警</Tag>
|
||
<Tag color={unhealthyLinkCount > 0 ? 'orange' : 'green'}>{unhealthyLinkCount.toLocaleString()} 链路异常</Tag>
|
||
</Space>
|
||
<Typography.Title heading={5} style={{ margin: 0 }}>把断链发现、异常通知、恢复验收和数据交付变成客户能看懂的服务承诺。</Typography.Title>
|
||
<Typography.Text type="secondary">
|
||
运维证据仍然独立展示;客户首页只看到哪些承诺正在被满足、哪些异常需要通知和验收。
|
||
</Typography.Text>
|
||
</div>
|
||
<div className="vp-customer-service-sla-grid">
|
||
{customerSlaItems.map((item) => (
|
||
<button
|
||
key={item.label}
|
||
type="button"
|
||
className="vp-customer-service-sla-item"
|
||
onClick={item.onClick}
|
||
aria-label={`客户服务 SLA 承诺 ${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-vehicle-health-energy" aria-label="车辆健康与能耗工作台">
|
||
<div className="vp-vehicle-health-energy-copy">
|
||
<Space wrap>
|
||
<Tag color="green">车辆健康与能耗工作台</Tag>
|
||
<Tag color="blue">{formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles)} 在线</Tag>
|
||
<Tag color={(summary?.issueVehicles ?? 0) > 0 ? 'orange' : 'green'}>{formatCount(summary?.issueVehicles)} 告警</Tag>
|
||
</Space>
|
||
<Typography.Title heading={5} style={{ margin: 0 }}>把实时工况、SOC/氢能字段、异常维护和里程能耗放到同一辆车的服务视角里。</Typography.Title>
|
||
<Typography.Text type="secondary">
|
||
客户不需要先理解字段来自 32960、808 还是 MQTT;先按车辆查看健康、能耗、异常和统计证据。
|
||
</Typography.Text>
|
||
</div>
|
||
<div className="vp-vehicle-health-energy-grid">
|
||
{vehicleHealthEnergyItems.map((item) => (
|
||
<button
|
||
key={item.label}
|
||
type="button"
|
||
className="vp-vehicle-health-energy-item"
|
||
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-customer-cockpit" aria-label="现代车队运营台">
|
||
<div className="vp-customer-cockpit-summary">
|
||
<Space wrap>
|
||
<Tag color="blue">现代车队运营台</Tag>
|
||
<Tag color={amapConfigured ? 'green' : 'orange'}>{amapConfigured ? '地图可用' : '坐标预览'}</Tag>
|
||
<Tag color={(summary?.issueVehicles ?? 0) > 0 ? 'orange' : 'green'}>{formatCount(summary?.issueVehicles)} 告警</Tag>
|
||
</Space>
|
||
<Typography.Title heading={4} style={{ margin: 0 }}>一屏完成车辆地图、轨迹回放、里程统计、数据导出和告警闭环。</Typography.Title>
|
||
</div>
|
||
<div className="vp-customer-cockpit-actions">
|
||
{modernFleetOperations.map((item) => (
|
||
<button
|
||
key={item.label}
|
||
type="button"
|
||
className="vp-customer-cockpit-action"
|
||
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-amap-service-foundation" aria-label="高德地图车辆服务底座">
|
||
<div className="vp-amap-service-foundation-copy">
|
||
<Space wrap>
|
||
<Tag color="blue">高德地图车辆服务底座</Tag>
|
||
<Tag color={amapConfigured ? 'green' : 'orange'}>{amapConfigured ? 'Web JS 已配置' : '坐标预览'}</Tag>
|
||
<Tag color={amapApiConfigured ? 'green' : 'orange'}>{amapApiConfigured ? '服务端 API 已配置' : '服务端 API 待配置'}</Tag>
|
||
<Tag color={amapSecurityProxyEnabled ? 'green' : 'orange'}>{amapSecurityProxyEnabled ? '安全代理已启用' : '安全代理未启用'}</Tag>
|
||
</Space>
|
||
<Typography.Title heading={5} style={{ margin: 0 }}>高德能力不单独成为页面目标,而是支撑同一辆车的实时看车、路线回放、区域告警和时间窗数据交付。</Typography.Title>
|
||
<Typography.Text type="secondary">
|
||
客户看到的是车辆服务动作;32960、808、MQTT 只作为位置、轨迹、告警和统计可信度的来源证据。
|
||
</Typography.Text>
|
||
<div className="vp-amap-capability-list" aria-label="地图服务能力清单">
|
||
<div className="vp-amap-capability-head">
|
||
<strong>地图服务能力清单</strong>
|
||
<span>把高德 Web JS 和服务端 API 翻译成客户能使用的车辆服务能力。</span>
|
||
</div>
|
||
<div className="vp-amap-capability-grid">
|
||
{amapCapabilityItems.map((item) => (
|
||
<div key={item.label} className="vp-amap-capability-item">
|
||
<Tag color={item.color}>{item.label}</Tag>
|
||
<strong>{item.value}</strong>
|
||
<span>{item.detail}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="vp-amap-service-foundation-grid">
|
||
{amapVehicleServiceItems.map((item) => (
|
||
<button
|
||
key={item.label}
|
||
type="button"
|
||
className="vp-amap-service-foundation-item"
|
||
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-customer-vehicle-monitor" aria-label="客户车辆监控台">
|
||
<div className="vp-customer-vehicle-monitor-copy">
|
||
<Space wrap>
|
||
<Tag color="blue">客户车辆监控台</Tag>
|
||
<Tag color={amapConfigured ? 'green' : 'orange'}>{amapConfigured ? '地图可用' : '坐标预览'}</Tag>
|
||
<Tag color={(summary?.issueVehicles ?? 0) > 0 ? 'orange' : 'green'}>{formatCount(summary?.issueVehicles)} 告警</Tag>
|
||
</Space>
|
||
<strong>先回答客户最常问的车辆问题:车在哪里、这段时间怎么跑、里程怎么算、数据怎么导出、异常谁处理。</strong>
|
||
<span>32960、808、MQTT 只在证据层出现;首页默认按车辆、时间窗和业务动作组织。</span>
|
||
</div>
|
||
<div className="vp-customer-vehicle-monitor-grid">
|
||
{customerVehicleMonitorItems.map((item) => (
|
||
<button
|
||
key={item.label}
|
||
type="button"
|
||
className="vp-customer-vehicle-monitor-item"
|
||
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>
|
||
<Card bordered className="vp-fleet-monitor" bodyStyle={{ padding: 0 }}>
|
||
<div className="vp-fleet-monitor-map">
|
||
<div className="vp-fleet-monitor-copy">
|
||
<Space wrap>
|
||
<Tag color="blue">车辆监控中心</Tag>
|
||
<Tag color={amapConfigured ? 'green' : 'orange'}>{amapConfigured ? '高德地图可用' : '坐标预览'}</Tag>
|
||
<Tag color={(summary?.issueVehicles ?? 0) > 0 ? 'orange' : 'green'}>{formatCount(summary?.issueVehicles)} 告警车辆</Tag>
|
||
</Space>
|
||
<Typography.Title heading={3} style={{ margin: 0 }}>先看车在哪里,再处理轨迹、统计、告警和导出</Typography.Title>
|
||
<Typography.Text type="secondary">
|
||
客户主流程围绕车辆本身:地图看车、实时状态、时间窗复盘、统计查询、历史数据导出和告警通知。接入来源只作为追溯证据。
|
||
</Typography.Text>
|
||
</div>
|
||
<VehicleMap
|
||
points={commandMapPoints}
|
||
maxFallbackPoints={120}
|
||
heightClassName="vp-fleet-monitor-map-canvas"
|
||
fallbackLabel="显示车辆实时坐标预览"
|
||
/>
|
||
<div className="vp-fleet-monitor-stats">
|
||
{customerCockpitMetrics.map((item) => (
|
||
<button key={item.label} type="button" className="vp-fleet-monitor-stat" 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>
|
||
<div className="vp-fleet-monitor-panel">
|
||
<div className="vp-fleet-task-router">
|
||
<div className="vp-fleet-task-router-head">
|
||
<Typography.Text strong>客户任务导航</Typography.Text>
|
||
<Typography.Text type="secondary">先按客户问题进入业务页面</Typography.Text>
|
||
</div>
|
||
<div className="vp-fleet-task-grid">
|
||
{customerTaskNavigation.map((item) => (
|
||
<button
|
||
key={item.title}
|
||
type="button"
|
||
className="vp-fleet-task-item"
|
||
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-customer-problem-desk">
|
||
<div className="vp-customer-problem-head">
|
||
<div>
|
||
<Typography.Text strong>客户问题处理台</Typography.Text>
|
||
<Typography.Text type="secondary">先回答客户问题,再下钻到车辆、轨迹、统计和证据。</Typography.Text>
|
||
</div>
|
||
<Tag color="blue">车辆服务优先</Tag>
|
||
</div>
|
||
<Typography.Text type="secondary" className="vp-customer-problem-principle">
|
||
客户首先关心的是车辆、位置、时间、里程、告警和导出;GB32960、JT808、MQTT 只放在证据层。
|
||
</Typography.Text>
|
||
<div className="vp-customer-problem-grid">
|
||
{customerProblemDesk.map((item) => (
|
||
<button
|
||
key={item.question}
|
||
type="button"
|
||
className="vp-customer-problem-item"
|
||
onClick={item.onClick}
|
||
aria-label={`客户问题处理台 ${item.question} ${item.action}`}
|
||
>
|
||
<div>
|
||
<strong>{item.question}</strong>
|
||
<span>{item.answer}</span>
|
||
</div>
|
||
<Tag color={item.color}>{item.metric}</Tag>
|
||
<em>{item.action}</em>
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
<div className="vp-vehicle-priority-queue">
|
||
<div className="vp-vehicle-priority-queue-head">
|
||
<div>
|
||
<Typography.Text strong>车辆服务优先级队列</Typography.Text>
|
||
<Typography.Text type="secondary">先处理影响客户看车、查轨迹、算里程和收通知的问题车辆。</Typography.Text>
|
||
</div>
|
||
<Tag color="blue">按车辆问题排序</Tag>
|
||
</div>
|
||
<div className="vp-vehicle-priority-queue-grid">
|
||
{vehiclePriorityQueue.map((item) => (
|
||
<button
|
||
key={item.title}
|
||
type="button"
|
||
className="vp-vehicle-priority-queue-item"
|
||
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-fleet-monitor-panel-head">
|
||
<Typography.Text strong>今日先处理</Typography.Text>
|
||
<Button size="small" theme="light" type="primary" onClick={copyCustomerServiceGuide}>复制说明</Button>
|
||
</div>
|
||
<div className="vp-fleet-next-actions">
|
||
{customerNextActions.map((item) => (
|
||
<button
|
||
key={item.title}
|
||
type="button"
|
||
className="vp-fleet-next-action"
|
||
onClick={item.onClick}
|
||
aria-label={`车辆服务建议 ${item.title} ${item.action}`}
|
||
>
|
||
<Tag color={item.color}>{item.level}</Tag>
|
||
<strong>{item.title}</strong>
|
||
<span>{item.detail}</span>
|
||
<em>{item.action}</em>
|
||
</button>
|
||
))}
|
||
</div>
|
||
<Typography.Text strong>车辆服务工作流</Typography.Text>
|
||
<div className="vp-fleet-monitor-actions">
|
||
{customerCockpitWorkflows.map((item) => (
|
||
<button key={item.title} type="button" className="vp-fleet-monitor-action" onClick={item.onClick} aria-label={`客户车辆工作流 ${item.title} ${item.action}`}>
|
||
<span>{item.title}</span>
|
||
<strong>{item.action}</strong>
|
||
<small>{item.description}</small>
|
||
<em>{item.evidence}</em>
|
||
</button>
|
||
))}
|
||
</div>
|
||
<Typography.Text strong>客户常问</Typography.Text>
|
||
<div className="vp-fleet-question-grid">
|
||
{customerQuestionActions.map((item) => (
|
||
<button
|
||
key={item.question}
|
||
type="button"
|
||
className="vp-fleet-question-item"
|
||
onClick={item.onClick}
|
||
aria-label={`客户常问 ${item.question} ${item.action}`}
|
||
>
|
||
<strong>{item.question}</strong>
|
||
<span>{item.answer}</span>
|
||
<Tag color={item.color}>{item.action}</Tag>
|
||
</button>
|
||
))}
|
||
</div>
|
||
<div className="vp-fleet-monitor-time">
|
||
<Space wrap>
|
||
<Tag color="blue">自定义时间窗</Tag>
|
||
<Tag color={timeMonitorScope().keyword ? 'green' : 'grey'}>{timeMonitorScope().keyword ? '单车' : '全域'}</Tag>
|
||
</Space>
|
||
<strong>{timeMonitorSummary}</strong>
|
||
<span>同一时间窗会同步用于轨迹回放、统计查询、历史查询和告警复盘。</span>
|
||
<Space wrap>
|
||
<Button size="small" theme="solid" 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>
|
||
</Space>
|
||
</div>
|
||
<div className="vp-fleet-monitor-trust">
|
||
{customerTrustSignals.map((item) => (
|
||
<div key={item.label} className="vp-fleet-monitor-trust-item">
|
||
<span>{item.label}</span>
|
||
<strong>{item.value}</strong>
|
||
<small>{item.detail}</small>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
<Card bordered className="vp-customer-delivery-board" bodyStyle={{ padding: 0 }}>
|
||
<div className="vp-customer-delivery-summary">
|
||
<Space wrap>
|
||
<Tag color="blue">客户交付驾驶舱</Tag>
|
||
<Tag color={(summary?.issueVehicles ?? 0) > 0 ? 'orange' : 'green'}>{formatCount(summary?.issueVehicles)} 告警车辆</Tag>
|
||
<Tag color={amapConfigured ? 'green' : 'orange'}>{amapConfigured ? '地图可交付' : '坐标可交付'}</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={copyCustomerServiceGuide}>复制客户服务说明</Button>
|
||
<Button size="small" theme="light" type="primary" aria-label="客户交付驾驶舱 导出驾驶舱 CSV" onClick={exportDashboardSnapshot}>导出驾驶舱 CSV</Button>
|
||
</Space>
|
||
</div>
|
||
<div className="vp-customer-delivery-grid">
|
||
{customerDeliveryItems.map((item) => (
|
||
<button
|
||
key={item.title}
|
||
type="button"
|
||
className="vp-customer-delivery-item"
|
||
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>
|
||
</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-service-path">
|
||
<div className="vp-time-service-path-copy">
|
||
<Space wrap>
|
||
<Tag color="blue">时间窗服务路径</Tag>
|
||
<Tag color={timeMonitorDeliveryStatus.color}>{timeMonitorDeliveryStatus.label}</Tag>
|
||
</Space>
|
||
<Typography.Text strong>锁定同一辆车和同一时间范围后,按轨迹、里程、历史证据和告警说明形成客户可复盘的交付路径。</Typography.Text>
|
||
<Typography.Text type="secondary">
|
||
这条路径把自定义时间窗从查询条件变成客户交付流程:先确定范围,再按同一范围打开轨迹、统计、导出和告警。
|
||
</Typography.Text>
|
||
</div>
|
||
<div className="vp-time-service-path-grid">
|
||
{timeWindowServicePathItems.map((item) => (
|
||
<button
|
||
key={item.label}
|
||
type="button"
|
||
className="vp-time-service-path-item"
|
||
onClick={item.onClick}
|
||
aria-label={`时间窗服务路径 ${item.step} ${item.label} ${item.value} ${item.action}`}
|
||
>
|
||
<span>{item.step}</span>
|
||
<div>
|
||
<Tag color={item.color}>{item.label}</Tag>
|
||
<strong>{item.value}</strong>
|
||
<small>{item.detail}</small>
|
||
<em>{item.action}</em>
|
||
</div>
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
<div className="vp-time-next-action-strip" aria-label="时间窗下一步行动条">
|
||
<div className="vp-time-next-action-copy">
|
||
<Space wrap>
|
||
<Tag color="blue">时间窗下一步行动条</Tag>
|
||
<Tag color={timeMonitorDeliveryStatus.color}>{timeMonitorDeliveryStatus.label}</Tag>
|
||
<Tag color={timeMonitorHasAlerts ? 'orange' : 'green'}>{timeMonitorHasAlerts ? `${formatCount(summary?.issueVehicles)} 告警` : '无高风险'}</Tag>
|
||
</Space>
|
||
<Typography.Title heading={5} style={{ margin: 0 }}>
|
||
把客户选择的车辆和时间范围转成下一步动作:先处理风险,再核对轨迹、里程、历史证据和交付说明。
|
||
</Typography.Title>
|
||
<Typography.Text type="secondary">
|
||
当前范围:{timeMonitorSummary};时间窗:{timeMonitorWindowLabel}。
|
||
</Typography.Text>
|
||
</div>
|
||
<div className="vp-time-next-action-grid">
|
||
{timeWindowNextItems.map((item) => (
|
||
<button
|
||
key={item.label}
|
||
type="button"
|
||
className="vp-time-next-action-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 className="vp-time-next-action-actions">
|
||
{timeWindowNextActions.map((item) => (
|
||
<button
|
||
key={item.label}
|
||
type="button"
|
||
className="vp-time-next-action-button"
|
||
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>
|
||
</div>
|
||
<div className="vp-time-delivery-strip">
|
||
<div className="vp-time-delivery-summary">
|
||
<Space wrap>
|
||
<Tag color={timeMonitorDeliveryStatus.color}>{timeMonitorDeliveryStatus.label}</Tag>
|
||
<Tag color={timeMonitorScopeValue.keyword ? 'green' : 'blue'}>{timeMonitorScopeValue.keyword ? '单车时间窗' : '车辆池时间窗'}</Tag>
|
||
</Space>
|
||
<Typography.Text strong>时间窗交付决策</Typography.Text>
|
||
<Typography.Text type="secondary">{timeMonitorDeliveryStatus.detail}</Typography.Text>
|
||
</div>
|
||
<div className="vp-time-delivery-grid">
|
||
{timeMonitorDeliveryActions.map((item) => (
|
||
<button
|
||
key={item.label}
|
||
type="button"
|
||
className="vp-time-delivery-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-audit-strip">
|
||
<div className="vp-time-audit-head">
|
||
<Typography.Text strong>时间窗服务核对</Typography.Text>
|
||
<Typography.Text type="secondary">
|
||
交付给客户前,先确认范围、轨迹、导出和异常闭环四件事都能讲清楚。
|
||
</Typography.Text>
|
||
</div>
|
||
<div className="vp-time-audit-grid">
|
||
{timeMonitorChecklist.map((item) => (
|
||
<div key={item.label} className="vp-time-audit-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>
|
||
<details className="vp-dashboard-secondary-details">
|
||
<summary>
|
||
<span>更多车辆服务入口</span>
|
||
<small>常用服务、今日任务、KPI 和车辆入口默认收起,主页面优先保留地图、时间窗监控、告警和导出。</small>
|
||
</summary>
|
||
<div className="vp-dashboard-secondary-body">
|
||
<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 }}>
|
||
<div className="vp-service-entry-board">
|
||
<div className="vp-service-entry-summary">
|
||
<Space wrap>
|
||
<Tag color="blue">客户服务路径</Tag>
|
||
<Tag color="green">{vehicleServiceOnlineText(serviceSummary, summary)}</Tag>
|
||
<Tag color={(summary?.issueVehicles ?? 0) > 0 ? 'orange' : 'green'}>{formatCount(summary?.issueVehicles)} 告警事件</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={() => onOpenVehicles({ online: 'online' })}>查看在线车辆</Button>
|
||
<Button size="small" theme="light" type="primary" onClick={copyCustomerServiceGuide}>复制客户服务说明</Button>
|
||
<Button size="small" theme="light" type="primary" onClick={exportDashboardSnapshot}>导出驾驶舱 CSV</Button>
|
||
<Button size="small" theme="light" type="tertiary" onClick={copyOperationsHandoff}>复制客户交付摘要</Button>
|
||
</Space>
|
||
</div>
|
||
<div className="vp-service-entry-grid">
|
||
{customerServicePathItems.map((item) => (
|
||
<button
|
||
key={item.title}
|
||
type="button"
|
||
className="vp-service-entry-item"
|
||
onClick={item.onClick}
|
||
aria-label={`车辆服务入口 ${item.title} ${item.action}`}
|
||
>
|
||
<Tag color={item.color}>{item.title}</Tag>
|
||
<strong>{item.value}</strong>
|
||
<span>{item.detail}</span>
|
||
<em>{item.action}</em>
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
</div>
|
||
</details>
|
||
<details className="vp-dashboard-evidence-details">
|
||
<summary>
|
||
<span>运维和证据层</span>
|
||
<small>来源覆盖、地图配置、告警预览和实时证据默认收起,客户主流程先看车辆监控、轨迹、统计、告警和导出。</small>
|
||
</summary>
|
||
<div className="vp-dashboard-evidence-body">
|
||
<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>
|
||
</div>
|
||
</details>
|
||
</Spin>
|
||
</div>
|
||
);
|
||
}
|