import { Banner, Button, Card, Descriptions, Form, Select, Space, Table, Tabs, Tag, Toast, Typography } from '@douyinfe/semi-ui'; import { IconCopy, IconDownload, IconRefresh, IconSearch } from '@douyinfe/semi-icons'; import { useEffect, useMemo, useRef, useState } from 'react'; import { api } from '../api/client'; import type { QualityIssueRow, RealtimeLocationRow, VehicleDetail as VehicleDetailData, VehicleSourceStatus } 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 { buildAppHash } from '../domain/appRoute'; import { buildCsv, downloadCsv, type CsvColumn } from '../domain/csvExport'; import { qualityIssueLabel } from '../domain/qualityIssue'; import { isLikelyVIN } from '../domain/vehicleLookup'; import { summarizeVehicleService } from '../domain/vehicleService'; type VehicleQuery = { keyword: string; protocol?: string; }; const sourceCapabilities: Array<{ key: keyof Pick; label: string }> = [ { key: 'hasRealtime', label: '实时' }, { key: 'hasHistory', label: '轨迹' }, { key: 'hasRaw', label: '历史数据' }, { key: 'hasMileage', label: '里程' } ]; const coverageStatusText: Record = { online: '全部在线', partial: '部分在线', offline: '全部离线', no_data: '暂无来源' }; function isFiniteNumber(value: unknown): value is number { return typeof value === 'number' && Number.isFinite(value); } function formatCompactNumber(value?: number, suffix = '') { if (!isFiniteNumber(value)) return '-'; return `${value.toLocaleString(undefined, { maximumFractionDigits: 1 })}${suffix}`; } function formatLocation(row?: RealtimeLocationRow) { if (!row || !isFiniteNumber(row.longitude) || !isFiniteNumber(row.latitude)) return '-'; return `${row.longitude}, ${row.latitude}`; } function parseTimeMs(value?: string) { if (!value) return undefined; const ms = Date.parse(value); return Number.isFinite(ms) ? ms : undefined; } function formatSeconds(value?: number) { if (!isFiniteNumber(value)) return '-'; return `${Math.round(value)} 秒`; } function customerStatusTitle(title?: string) { if (!title) return ''; return title === '来源不完整' ? '数据通道不完整' : title; } function customerScopeText(protocol?: string) { return protocol ? `单一数据通道:${protocol}` : '全部数据通道聚合'; } function sourceServiceRole(source: VehicleSourceStatus, primaryProtocol?: string) { if (source.protocol === primaryProtocol) return { label: '主数据通道', color: 'blue' as const }; if (source.online) return { label: '在线数据通道', color: 'green' as const }; if (!source.lastSeen && !source.hasRealtime && !source.hasHistory && !source.hasRaw && !source.hasMileage) { return { label: '暂无来源', color: 'grey' as const }; } return { label: '离线证据', color: 'grey' as const }; } function sourceFusionRow(source: VehicleSourceStatus, realtime: RealtimeLocationRow | undefined, primaryProtocol?: string): SourceFusionRow { const hasLocation = !!realtime && isFiniteNumber(realtime.longitude) && isFiniteNumber(realtime.latitude) && realtime.longitude !== 0 && realtime.latitude !== 0; const hasMileage = isFiniteNumber(realtime?.totalMileageKm) || source.hasMileage; const contribution = [ source.protocol === primaryProtocol ? '主实时通道' : '', source.online ? '在线心跳' : '', source.hasRealtime || realtime ? '实时字段' : '', hasLocation ? '定位数据' : '', source.hasHistory ? '轨迹回放' : '', source.hasRaw ? '历史数据' : '', hasMileage ? '统计口径' : '' ].filter(Boolean); return { source, realtime, role: sourceServiceRole(source, primaryProtocol), freshness: source.online ? `在线 / ${source.lastSeen || realtime?.lastSeen || '-'}` : source.lastSeen ? `离线 / ${source.lastSeen}` : '未接入', realtimeText: source.hasRealtime || realtime ? `速度 ${formatCompactNumber(realtime?.speedKmh, ' km/h')} / SOC ${formatCompactNumber(realtime?.socPercent, '%')}` : '无实时字段', locationText: hasLocation ? `${realtime?.longitude}, ${realtime?.latitude}` : '无有效坐标', rawText: source.hasRaw ? '可查历史数据' : '无历史数据', mileageText: hasMileage ? formatCompactNumber(realtime?.totalMileageKm, ' km') : '无里程数据', contribution: contribution.length > 0 ? contribution : ['待补来源'] }; } type VehicleServiceAction = { key: string; label: string; description: string; color: 'green' | 'orange' | 'red' | 'blue'; onClick: () => void; }; type SourceFusionRow = { source: VehicleSourceStatus; realtime?: RealtimeLocationRow; role: { label: string; color: 'green' | 'orange' | 'red' | 'blue' | 'grey' }; freshness: string; realtimeText: string; locationText: string; rawText: string; mileageText: string; contribution: string[]; }; type VehicleReportRow = { section: string; item: string; value: string; }; const vehicleReportColumns: CsvColumn[] = [ { title: '模块', value: (row) => row.section }, { title: '项目', value: (row) => row.item }, { title: '值', value: (row) => row.value } ]; function reportText(value: unknown) { if (value == null || value === '') return '-'; if (Array.isArray(value)) return value.length > 0 ? value.join('|') : '-'; return String(value); } function vehicleReportFileName(vin: string, protocol?: string) { return `vehicle-service-${vin || 'unknown'}-${protocol || 'all-source'}.csv`; } function qualityIssuePriority(row: QualityIssueRow) { if (row.severity === 'error') return 0; if (row.severity === 'warning') return 1; return 2; } function issueEvidenceDate(value?: string) { const match = value?.match(/\d{4}-\d{2}-\d{2}/); return match?.[0]; } function nextDate(value: string) { const [year, month, day] = value.split('-').map(Number); const date = new Date(Date.UTC(year, month - 1, day + 1)); return date.toISOString().slice(0, 10); } function qualityIssueVehicleLabel(row: QualityIssueRow, fallbackVIN: string) { const vin = row.vin?.trim() || fallbackVIN; return [row.plate?.trim(), vin].filter(Boolean).join(' / ') || row.phone || row.sourceEndpoint || '-'; } async function copyText(value: string, label: string) { try { await navigator.clipboard.writeText(value); Toast.success(`已复制${label}`); } catch { Toast.error(`复制${label}失败`); } } export function VehicleDetail({ vin, protocol, platformRelease, onOpenRealtime, onOpenHistory, onOpenRaw, onOpenMileage, onOpenVehicles, onOpenQuality, onOpenHistoryEvidence, onOpenRawEvidence, onQueryChange }: { vin: string; protocol?: string; platformRelease?: string; onOpenRealtime: (vin: string, protocol?: string) => void; onOpenHistory: (vin: string, protocol?: string) => void; onOpenRaw: (vin: string, protocol?: string) => void; onOpenMileage: (vin: string, protocol?: string) => void; onOpenVehicles?: (filters?: Record) => void; onOpenQuality?: (filters?: Record) => void; onOpenHistoryEvidence?: (filters?: Record) => void; onOpenRawEvidence?: (filters?: Record) => void; onQueryChange?: (keyword: string, protocol?: string) => void; }) { const [query, setQuery] = useState({ keyword: vin, protocol }); const [detail, setDetail] = useState(null); const [loading, setLoading] = useState(false); const requestSeq = useRef(0); const load = (nextQuery = query) => { const keyword = nextQuery.keyword.trim(); if (!keyword) { Toast.warning('请输入 VIN / 车牌 / 手机号'); return; } setLoading(true); const params = new URLSearchParams({ keyword }); if (nextQuery.protocol?.trim()) { params.set('protocol', nextQuery.protocol.trim()); } const seq = requestSeq.current + 1; requestSeq.current = seq; api.vehicleDetail(params) .then((nextDetail) => { if (requestSeq.current === seq) { setDetail(nextDetail); } }) .catch((error: Error) => { if (requestSeq.current === seq) { Toast.error(error.message); } }) .finally(() => { if (requestSeq.current === seq) { setLoading(false); } }); }; useEffect(() => { const nextQuery = { keyword: vin, protocol }; setQuery(nextQuery); load(nextQuery); }, [vin, protocol]); const identity = detail?.identity; const summary = detail?.realtimeSummary; const latest = detail?.realtime?.[0]; const resolution = detail?.resolution; const overview = detail?.serviceOverview; const resolvedVIN = resolution?.vin || detail?.vin || summary?.vin || identity?.vin || latest?.vin || query.keyword; const hasResolvedVIN = resolution?.resolved ?? detail?.lookupResolved ?? isLikelyVIN(resolvedVIN); const displayVIN = hasResolvedVIN ? resolvedVIN : '-'; const displayLookupKey = resolution?.lookupKey || detail?.lookupKey || query.keyword; const protocols = useMemo(() => resolution?.protocols?.length ? resolution.protocols : detail?.sources ?? [], [detail?.sources, resolution?.protocols]); const latestRaw = detail?.raw?.items?.[0]; const qualityCount = detail?.quality?.total ?? 0; const priorityQualityIssue = useMemo(() => { const items = detail?.quality?.items ?? []; return [...items].sort((a, b) => { const severityDelta = qualityIssuePriority(a) - qualityIssuePriority(b); if (severityDelta !== 0) return severityDelta; return (Date.parse(b.lastSeen || '') || 0) - (Date.parse(a.lastSeen || '') || 0); })[0]; }, [detail?.quality?.items]); const online = resolution?.online || summary?.online || identity?.online || false; const lastSeen = resolution?.lastSeen || summary?.lastSeen || latest?.lastSeen || '-'; const serviceStatus = detail?.serviceStatus; const realtimeRows = detail?.realtime ?? []; const locatedRealtimeRows = useMemo(() => realtimeRows.filter((row) => ( isFiniteNumber(row.longitude) && isFiniteNumber(row.latitude) && row.longitude !== 0 && row.latitude !== 0 )), [realtimeRows]); const sourceOnlineByProtocol = useMemo(() => new Map((detail?.sourceStatus ?? []).map((source) => [source.protocol, source.online])), [detail?.sourceStatus]); const vehicleMapPoints = useMemo(() => locatedRealtimeRows.map((row) => ({ id: `${row.protocol || 'source'}-${row.lastSeen || row.vin}`, label: row.protocol || row.vin, longitude: row.longitude, latitude: row.latitude, online: sourceOnlineByProtocol.get(row.protocol) ?? true, title: `${row.protocol || '来源'} / ${formatCompactNumber(row.speedKmh, ' km/h')}` })), [locatedRealtimeRows, sourceOnlineByProtocol]); const latestRealtimeByProtocol = useMemo(() => { const next = new Map(); for (const row of realtimeRows) { if (!row.protocol || next.has(row.protocol)) continue; next.set(row.protocol, row); } return next; }, [realtimeRows]); const sourceFusionRows = useMemo(() => ( (detail?.sourceStatus ?? []).map((source) => sourceFusionRow(source, latestRealtimeByProtocol.get(source.protocol), summary?.primaryProtocol)) ), [detail?.sourceStatus, latestRealtimeByProtocol, summary?.primaryProtocol]); const realtimeSources = new Set(realtimeRows.map((row) => row.protocol).filter(Boolean)); const sourceStatusRows = detail?.sourceStatus ?? []; const sourceCount = Math.max(sourceStatusRows.length, realtimeSources.size, summary?.sourceCount ?? 0, protocols.length); const onlineSourceCount = sourceStatusRows.length ? sourceStatusRows.filter((source) => source.online).length : summary?.onlineSourceCount ?? 0; const locatedSourceCount = realtimeRows.filter((row) => isFiniteNumber(row.longitude) && isFiniteNumber(row.latitude)).length; const mileageValues = realtimeRows.map((row) => row.totalMileageKm).filter(isFiniteNumber); const mileageDelta = mileageValues.length > 1 ? Math.max(...mileageValues) - Math.min(...mileageValues) : undefined; const timeValues = [ ...realtimeRows.map((row) => parseTimeMs(row.lastSeen)), ...(detail?.sourceStatus ?? []).map((source) => parseTimeMs(source.lastSeen)) ].filter(isFiniteNumber); const sourceTimeDeltaSeconds = timeValues.length > 1 ? (Math.max(...timeValues) - Math.min(...timeValues)) / 1000 : undefined; const consistency = detail?.sourceConsistency; const consistencySourceCount = consistency?.sourceCount ?? sourceCount; const consistencyOnlineSourceCount = consistency?.onlineSourceCount ?? onlineSourceCount; const consistencyLocatedSourceCount = consistency?.locatedSourceCount ?? locatedSourceCount; const consistencyMileageDelta = consistency?.mileageDeltaKm ?? mileageDelta; const consistencyTimeDeltaSeconds = consistency?.sourceTimeDeltaSeconds ?? sourceTimeDeltaSeconds; const consistencyTitle = consistency?.title ?? '-'; const consistencyDetail = consistency?.detail ?? '-'; const missingProtocols = consistency?.missingProtocols ?? []; const missingProtocolText = missingProtocols.length > 0 ? ( {missingProtocols.map((item) => ( ))} ) : '-'; const evidenceSourceCount = overview?.sourceCount ?? consistencySourceCount; const evidenceOnlineSourceCount = overview?.onlineSourceCount ?? consistencyOnlineSourceCount; const evidenceLocatedSourceCount = consistencyLocatedSourceCount; const evidenceMileageDelta = consistencyMileageDelta; const evidenceTimeDeltaSeconds = consistencyTimeDeltaSeconds; const activeProtocol = query.protocol?.trim() ?? ''; const scopeText = customerScopeText(activeProtocol); const archivePlate = resolution?.plate || summary?.plate || identity?.plate || latest?.plate || ''; const archivePhone = resolution?.phone || summary?.phone || identity?.phone || ''; const archiveOEM = resolution?.oem || summary?.oem || identity?.oem || ''; const archiveFields = [hasResolvedVIN && displayVIN !== '-', archivePlate, archivePhone, archiveOEM]; const archiveCompleteness = `${archiveFields.filter(Boolean).length}/${archiveFields.length}`; const archiveMissingLabels = [ { value: hasResolvedVIN && displayVIN !== '-' ? displayVIN : '', label: '缺VIN' }, { value: archivePlate, label: '缺车牌' }, { value: archivePhone, label: '缺手机号' }, { value: archiveOEM, label: '缺OEM' } ] .filter((item) => !String(item.value ?? '').trim()) .map((item) => item.label); const archiveSourceCount = Math.max(protocols.length, sourceCount); const formKey = `${query.keyword}-${query.protocol ?? ''}`; const switchSource = (nextProtocol = '') => { const nextQuery = { keyword: resolvedVIN || query.keyword, protocol: nextProtocol }; setQuery(nextQuery); onQueryChange?.(nextQuery.keyword, nextQuery.protocol); load(nextQuery); }; const serviceConclusion = useMemo(() => summarizeVehicleService({ resolved: hasResolvedVIN, sourceCount, onlineSourceCount, qualityIssueCount: qualityCount, missingProtocols, mileageDeltaKm: consistencyMileageDelta }), [consistencyMileageDelta, hasResolvedVIN, missingProtocols, onlineSourceCount, qualityCount, sourceCount]); const qualityFiltersForCurrentVehicle = () => ({ keyword: resolvedVIN, ...(activeProtocol ? { protocol: activeProtocol } : {}) }); const qualityIssueEvidenceFilters = (row: QualityIssueRow, tab?: 'raw') => { const dateFrom = issueEvidenceDate(row.lastSeen); return { keyword: row.vin?.trim() || resolvedVIN, protocol: row.protocol || activeProtocol, ...(dateFrom ? { dateFrom, dateTo: nextDate(dateFrom) } : {}), ...(tab === 'raw' ? { tab: 'raw', includeFields: 'true' } : {}) }; }; const openQualityIssueHistoryEvidence = (row: QualityIssueRow) => { const filters = qualityIssueEvidenceFilters(row); if (onOpenHistoryEvidence) { onOpenHistoryEvidence(filters); return; } onOpenHistory(filters.keyword, filters.protocol); }; const openQualityIssueRawEvidence = (row: QualityIssueRow) => { const filters = qualityIssueEvidenceFilters(row, 'raw'); if (onOpenRawEvidence) { onOpenRawEvidence(filters); return; } onOpenRaw(filters.keyword, filters.protocol); }; const vehicleServiceURL = (hash: string) => `${window.location.origin}${window.location.pathname}${hash}`; const vehicleServiceHash = (page: 'detail' | 'realtime' | 'history' | 'history-query' | 'mileage', filters?: Record) => buildAppHash({ page, keyword: resolvedVIN, protocol: activeProtocol, filters }); const copyVehicleServiceURL = () => copyText(vehicleServiceURL(window.location.hash), '服务链接'); const copyVehicleDiagnosticSummary = () => { if (!detail) { Toast.warning('当前没有可复制的诊断摘要'); return; } const vehicleName = [archivePlate, displayVIN].filter((item) => item && item !== '-').join(' / ') || displayLookupKey; const lines = [ '【车辆服务诊断摘要】', ...(platformRelease?.trim() ? [`运行版本:${platformRelease.trim()}`] : []), `车辆:${vehicleName}`, `查询关键词:${reportText(displayLookupKey)}`, `当前范围:${scopeText}`, `服务状态:${customerStatusTitle(serviceStatus?.title) || serviceConclusion.status}`, `服务说明:${serviceStatus?.detail ?? '-'}`, `服务判断:${serviceConclusion.status}`, `主要风险:${serviceConclusion.risk}`, `下一步:${serviceConclusion.nextStep}`, `数据通道:${evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount} 在线` : '-'}`, `定位数据:${evidenceLocatedSourceCount > 0 ? `${evidenceLocatedSourceCount} 个通道有位置` : '暂无位置'}`, `里程差异:${formatCompactNumber(evidenceMileageDelta, ' km')}`, `时间差异:${formatSeconds(evidenceTimeDeltaSeconds)}`, `服务数据:实时 ${overview?.realtimeCount ?? detail.realtime.length} / 轨迹 ${overview?.historyCount ?? detail.history.total ?? 0} / 历史数据 ${overview?.rawCount ?? detail.raw.total ?? 0} / 里程 ${overview?.mileageCount ?? detail.mileage.total ?? 0} / 告警 ${overview?.qualityIssueCount ?? qualityCount}`, `一致性:${customerStatusTitle(consistencyTitle) || consistencyTitle}`, `一致性说明:${consistencyDetail}`, `车辆服务:${vehicleServiceURL(vehicleServiceHash('detail'))}`, `实时:${vehicleServiceURL(vehicleServiceHash('realtime'))}`, `轨迹:${vehicleServiceURL(vehicleServiceHash('history'))}`, `历史数据:${vehicleServiceURL(vehicleServiceHash('history-query', { tab: 'raw', includeFields: 'true' }))}`, `统计查询:${vehicleServiceURL(vehicleServiceHash('mileage'))}` ]; copyText(lines.join('\n'), '诊断摘要'); }; const copySourceFusionReport = () => { if (!detail || sourceFusionRows.length === 0) { Toast.warning('当前没有可复制的数据通道归并矩阵'); return; } const vehicleName = [archivePlate, displayVIN].filter((item) => item && item !== '-').join(' / ') || displayLookupKey; const lines = [ '【单车数据通道归并矩阵】', `车辆:${vehicleName}`, `当前范围:${scopeText}`, `主数据通道:${overview?.primaryProtocol || summary?.primaryProtocol || '-'}`, `通道覆盖:${evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount} 在线` : '-'}`, `一致性:${customerStatusTitle(consistencyTitle) || consistencyTitle}`, '', ...sourceFusionRows.map((row, index) => [ `${index + 1}. ${row.source.protocol} / ${row.role.label}`, ` 新鲜度:${row.freshness}`, ` 贡献:${row.contribution.join('、')}`, ` 实时:${row.realtimeText}`, ` 定位:${row.locationText}`, ` 历史数据:${row.rawText}`, ` 里程:${row.mileageText}` ].join('\n')), '', `车辆服务:${vehicleServiceURL(vehicleServiceHash('detail'))}`, `实时监控:${vehicleServiceURL(vehicleServiceHash('realtime'))}`, `轨迹回放:${vehicleServiceURL(vehicleServiceHash('history'))}`, `历史数据:${vehicleServiceURL(vehicleServiceHash('history-query', { tab: 'raw', includeFields: 'true' }))}`, `统计查询:${vehicleServiceURL(vehicleServiceHash('mileage'))}` ]; copyText(lines.join('\n'), '数据通道归并矩阵'); }; const qualityIssueAction = (count: number) => { if (count <= 0) { return ; } return ( ); }; const serviceActions = useMemo(() => { const actions: VehicleServiceAction[] = []; if (!hasResolvedVIN) { actions.push({ key: 'identity', label: '维护身份绑定', description: `关键词 ${displayLookupKey} 暂未解析到 VIN,先补齐绑定后再看跨证据服务。`, color: 'orange', onClick: () => onOpenVehicles?.({ bindingStatus: 'unbound' }) }); } for (const item of missingProtocols) { actions.push({ key: `missing-${item}`, label: `补齐 ${item} 证据`, description: `${item} 当前没有形成有效数据通道,会影响这辆车的统一服务可信度。`, color: 'orange', onClick: () => onOpenVehicles?.({ serviceStatus: 'degraded', missingProtocol: item }) }); } for (const source of detail?.sourceStatus ?? []) { if (source.online) continue; actions.push({ key: `offline-${source.protocol}`, label: `排查 ${source.protocol} 离线`, description: `${source.protocol} 最后上报时间:${source.lastSeen || '无上报'}。`, color: 'orange', onClick: () => switchSource(source.protocol) }); } if (qualityCount > 0) { actions.push({ key: 'quality', label: `处理质量问题 ${qualityCount} 项`, description: '质量问题会影响车辆服务的定位、里程和实时状态可信度。', color: 'orange', onClick: () => onOpenQuality?.(qualityFiltersForCurrentVehicle()) }); } if (actions.length === 0 && hasResolvedVIN) { actions.push({ key: 'healthy', label: '查看实时服务', description: '当前没有明确处置项,可直接查看该车统一实时状态。', color: 'green', onClick: () => onOpenRealtime(resolvedVIN, activeProtocol) }); } return actions; }, [activeProtocol, detail?.sourceStatus, displayLookupKey, hasResolvedVIN, missingProtocols, onOpenQuality, onOpenRealtime, onOpenVehicles, qualityCount, resolvedVIN]); const serviceAssetItems = [ { label: '实时', value: formatCompactNumber(overview?.realtimeCount ?? detail?.realtime?.length ?? 0), detail: evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount} 在线` : '暂无来源', color: evidenceOnlineSourceCount > 0 ? 'green' as const : 'orange' as const }, { label: '轨迹', value: formatCompactNumber(overview?.historyCount ?? detail?.history?.total ?? 0), detail: evidenceLocatedSourceCount > 0 ? `${evidenceLocatedSourceCount} 个位置证据` : '暂无位置', color: (overview?.historyCount ?? detail?.history?.total ?? 0) > 0 ? 'blue' as const : 'grey' as const }, { label: '历史数据', value: formatCompactNumber(overview?.rawCount ?? detail?.raw?.total ?? 0), detail: latestRaw?.frameType || '解析字段', color: (overview?.rawCount ?? detail?.raw?.total ?? 0) > 0 ? 'blue' as const : 'grey' as const }, { label: '里程', value: formatCompactNumber(overview?.mileageCount ?? detail?.mileage?.total ?? 0), detail: formatCompactNumber(evidenceMileageDelta, ' km 差异'), color: isFiniteNumber(evidenceMileageDelta) && Math.abs(evidenceMileageDelta) > 1 ? 'orange' as const : 'green' as const }, { label: '告警', value: qualityCount.toLocaleString(), detail: priorityQualityIssue ? qualityIssueLabel(priorityQualityIssue.issueType) : '暂无告警', color: qualityCount > 0 ? 'orange' as const : 'green' as const } ]; const unifiedVehicleServiceItems = [ { label: '车辆身份', value: hasResolvedVIN ? displayVIN : displayLookupKey, detail: archivePlate || archivePhone || archiveOEM || '待补身份', color: hasResolvedVIN ? 'green' as const : 'orange' as const }, { label: '服务状态', value: customerStatusTitle(serviceStatus?.title) || serviceConclusion.status, detail: serviceStatus?.detail ?? serviceConclusion.risk, color: serviceConclusion.color }, { label: '实时主数据通道', value: overview?.primaryProtocol || summary?.primaryProtocol || '-', detail: evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount} 在线` : '暂无来源', color: evidenceOnlineSourceCount > 0 ? 'green' as const : 'orange' as const }, { label: '业务可用性', value: online ? '在线可用' : hasResolvedVIN ? '离线待查' : '待绑定', detail: lastSeen === '-' ? '无最后上报' : `最后上报 ${lastSeen}`, color: online ? 'green' as const : 'orange' as const } ]; const copyUnifiedVehicleService = () => { if (!detail) { Toast.warning('当前没有可复制的统一车辆服务摘要'); return; } const vehicleName = [archivePlate, displayVIN].filter((item) => item && item !== '-').join(' / ') || displayLookupKey; const lines = [ '【统一车辆服务摘要】', `车辆:${vehicleName}`, `当前范围:${scopeText}`, `车辆身份:${hasResolvedVIN ? displayVIN : '待绑定'}`, `服务状态:${customerStatusTitle(serviceStatus?.title) || serviceConclusion.status}`, `服务说明:${serviceStatus?.detail ?? serviceConclusion.risk}`, `实时主数据通道:${overview?.primaryProtocol || summary?.primaryProtocol || '-'}`, `通道在线:${evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount}` : '-'}`, `业务可用性:${online ? '在线可用' : hasResolvedVIN ? '离线待查' : '待绑定'}`, `服务数据:实时 ${overview?.realtimeCount ?? detail.realtime.length} / 轨迹 ${overview?.historyCount ?? detail.history.total ?? 0} / 历史数据 ${overview?.rawCount ?? detail.raw.total ?? 0} / 里程 ${overview?.mileageCount ?? detail.mileage.total ?? 0} / 告警 ${overview?.qualityIssueCount ?? qualityCount}`, `下一步:${serviceActions[0]?.label ?? serviceConclusion.nextStep}`, `车辆服务:${vehicleServiceURL(vehicleServiceHash('detail'))}`, `实时监控:${vehicleServiceURL(vehicleServiceHash('realtime'))}`, `轨迹回放:${vehicleServiceURL(vehicleServiceHash('history'))}`, `历史数据:${vehicleServiceURL(vehicleServiceHash('history-query', { tab: 'raw', includeFields: 'true' }))}`, `统计查询:${vehicleServiceURL(vehicleServiceHash('mileage'))}` ]; copyText(lines.join('\n'), '统一车辆服务摘要'); }; const copyVehicleServiceDossier = () => { if (!detail) { Toast.warning('当前没有可复制的车辆服务档案'); return; } const vehicleName = [archivePlate, displayVIN].filter((item) => item && item !== '-').join(' / ') || displayLookupKey; const lines = [ '【车辆服务档案】', `车辆:${vehicleName}`, `查询关键词:${displayLookupKey}`, `当前范围:${scopeText}`, `档案完整度:${archiveCompleteness}${archiveMissingLabels.length > 0 ? `(${archiveMissingLabels.join('、')})` : ''}`, `主数据通道:${overview?.primaryProtocol || summary?.primaryProtocol || '-'}`, `通道在线:${evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount}` : '-'}`, `服务状态:${customerStatusTitle(serviceStatus?.title) || serviceConclusion.status}`, `服务说明:${serviceStatus?.detail ?? serviceConclusion.risk}`, `资产概览:${serviceAssetItems.map((item) => `${item.label} ${item.value}`).join(' / ')}`, `一致性:${customerStatusTitle(consistencyTitle) || consistencyTitle}`, `下一步:${serviceActions[0]?.label ?? serviceConclusion.nextStep}`, `车辆服务:${vehicleServiceURL(vehicleServiceHash('detail'))}`, `实时监控:${vehicleServiceURL(vehicleServiceHash('realtime'))}`, `轨迹回放:${vehicleServiceURL(vehicleServiceHash('history'))}`, `历史数据:${vehicleServiceURL(vehicleServiceHash('history-query', { tab: 'raw', includeFields: 'true' }))}`, `统计查询:${vehicleServiceURL(vehicleServiceHash('mileage'))}` ]; copyText(lines.join('\n'), '车辆服务档案'); }; const copyVehicleOperationsSummary = () => { if (!detail) { Toast.warning('当前没有可复制的运营摘要'); return; } const vehicleName = [archivePlate, displayVIN].filter((item) => item && item !== '-').join(' / ') || displayLookupKey; const actionLines = serviceActions.map((item, index) => `${index + 1}. ${item.label}:${item.description}`); const lines = [ '【车辆服务运营摘要】', `车辆:${vehicleName}`, `当前范围:${scopeText}`, `服务状态:${customerStatusTitle(serviceStatus?.title) || serviceConclusion.status}`, `业务影响:${serviceConclusion.risk}`, `建议动作:${serviceConclusion.nextStep}`, `在线数据通道:${evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount}` : '-'}`, `定位数据:${evidenceLocatedSourceCount > 0 ? `${evidenceLocatedSourceCount}` : '0'}`, `质量问题:${qualityCount.toLocaleString()} 项`, `服务数据:轨迹 ${overview?.historyCount ?? detail.history.total ?? 0} / 历史数据 ${overview?.rawCount ?? detail.raw.total ?? 0} / 里程 ${overview?.mileageCount ?? detail.mileage.total ?? 0}`, `下一步清单:${actionLines.length > 0 ? '' : '暂无明确处置项,持续观察实时状态。'}`, ...actionLines, `车辆服务:${vehicleServiceURL(vehicleServiceHash('detail'))}`, `实时监控:${vehicleServiceURL(vehicleServiceHash('realtime'))}`, `轨迹回放:${vehicleServiceURL(vehicleServiceHash('history'))}`, `历史数据:${vehicleServiceURL(vehicleServiceHash('history-query', { tab: 'raw', includeFields: 'true' }))}`, `统计查询:${vehicleServiceURL(vehicleServiceHash('mileage'))}` ]; copyText(lines.join('\n'), '运营摘要'); }; const detailWorkbench = [ { title: '实时定位', value: lastSeen, meta: evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount} 在线` : '暂无来源', color: online ? 'green' as const : 'orange' as const, detail: '查看该车最新位置、速度、SOC、总里程和多通道实时字段。', disabled: !hasResolvedVIN, actions: [ { label: '查看实时', onClick: () => onOpenRealtime(resolvedVIN, activeProtocol) } ] }, { title: '轨迹回放', value: `${formatCompactNumber(overview?.historyCount ?? detail?.history?.total ?? 0)} 条历史`, meta: evidenceLocatedSourceCount > 0 ? `${evidenceLocatedSourceCount} 个位置证据` : '暂无位置', color: evidenceLocatedSourceCount > 0 ? 'blue' as const : 'grey' as const, detail: '回放历史轨迹,复核定位断点、速度变化和里程连续性。', disabled: !hasResolvedVIN, actions: [ { label: '打开轨迹', onClick: () => onOpenHistory(resolvedVIN, activeProtocol) } ] }, { title: '历史数据', value: `${formatCompactNumber(overview?.rawCount ?? detail?.raw?.total ?? 0)} 帧`, meta: latestRaw?.frameType || '字段明细', color: (overview?.rawCount ?? detail?.raw?.total ?? 0) > 0 ? 'blue' as const : 'grey' as const, detail: '查看历史明细和字段明细,用于复核定位、里程和告警依据。', disabled: !hasResolvedVIN, actions: [ { label: '查看历史数据', onClick: () => onOpenRaw(resolvedVIN, activeProtocol) } ] }, { title: '告警处置', value: `${qualityCount.toLocaleString()} 项`, meta: priorityQualityIssue ? qualityIssueLabel(priorityQualityIssue.issueType) : '暂无告警', color: qualityCount > 0 ? 'orange' as const : 'green' as const, detail: '进入告警队列,处理断链、VIN 缺失、字段缺失和来源不一致。', disabled: !hasResolvedVIN && qualityCount <= 0, actions: [ { label: '查看告警', onClick: () => onOpenQuality?.(qualityFiltersForCurrentVehicle()) } ] }, { title: '统计复核', value: `${formatCompactNumber(overview?.mileageCount ?? detail?.mileage?.total ?? 0)} 条统计`, meta: formatCompactNumber(evidenceMileageDelta, ' km 差异'), color: isFiniteNumber(evidenceMileageDelta) && Math.abs(evidenceMileageDelta) > 1 ? 'orange' as const : 'green' as const, detail: '核对统计口径,确认区间总值、每日里程与数据通道是否闭合。', disabled: !hasResolvedVIN, actions: [ { label: '查看统计', onClick: () => onOpenMileage(resolvedVIN, activeProtocol) } ] } ]; const serviceRunbookSteps = [ { title: '确认实时状态', evidence: evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount} 在线,最后上报 ${lastSeen}` : '暂无在线数据', acceptance: '车辆在线状态、最新时间、有效坐标均可解释。', action: '打开实时', disabled: !hasResolvedVIN, onClick: () => onOpenRealtime(resolvedVIN, activeProtocol) }, { title: '回放轨迹断点', evidence: evidenceLocatedSourceCount > 0 ? `${evidenceLocatedSourceCount} 个通道有位置,时间差 ${formatSeconds(evidenceTimeDeltaSeconds)}` : '暂无可回放位置', acceptance: '轨迹点、速度和总里程变化连续,异常点可定位到时间段。', action: '轨迹回放', disabled: !hasResolvedVIN, onClick: () => onOpenHistory(resolvedVIN, activeProtocol) }, { title: '查询历史数据', evidence: `${formatCompactNumber(overview?.rawCount ?? detail?.raw?.total ?? 0)} 条历史数据,最新类型 ${latestRaw?.frameType || '-'}`, acceptance: '关键字段来自解析字段而不是页面二次推断。', action: '历史数据', disabled: !hasResolvedVIN, onClick: () => onOpenRaw(resolvedVIN, activeProtocol) }, { title: '处理告警闭环', evidence: priorityQualityIssue ? `${qualityIssueLabel(priorityQualityIssue.issueType)} / ${priorityQualityIssue.lastSeen || '-'}` : '暂无质量告警', acceptance: '断链、VIN 缺失、字段缺失等问题进入告警队列并有处置结论。', action: '告警队列', disabled: !onOpenQuality || (!hasResolvedVIN && qualityCount <= 0), onClick: () => onOpenQuality?.(qualityFiltersForCurrentVehicle()) }, { title: '复核统计口径', evidence: `${formatCompactNumber(overview?.mileageCount ?? detail?.mileage?.total ?? 0)} 条统计,证据里程差 ${formatCompactNumber(evidenceMileageDelta, ' km')}`, acceptance: '区间里程、每日里程和轨迹总里程可以互相解释。', action: '统计查询', disabled: !hasResolvedVIN, onClick: () => onOpenMileage(resolvedVIN, activeProtocol) } ]; const copyVehicleRunbook = () => { if (!detail) { Toast.warning('当前没有可复制的单车处理清单'); return; } const vehicleName = [archivePlate, displayVIN].filter((item) => item && item !== '-').join(' / ') || displayLookupKey; const lines = [ '【单车服务处理清单】', `车辆:${vehicleName}`, `当前范围:${scopeText}`, `服务状态:${customerStatusTitle(serviceStatus?.title) || serviceConclusion.status}`, `一致性:${customerStatusTitle(consistencyTitle) || consistencyTitle}`, ...serviceRunbookSteps.map((item, index) => [ `${index + 1}. ${item.title}`, ` 证据:${item.evidence}`, ` 验收:${item.acceptance}` ].join('\n')), `实时监控:${vehicleServiceURL(vehicleServiceHash('realtime'))}`, `轨迹回放:${vehicleServiceURL(vehicleServiceHash('history'))}`, `历史数据:${vehicleServiceURL(vehicleServiceHash('history-query', { tab: 'raw', includeFields: 'true' }))}`, `告警事件:${vehicleServiceURL(buildAppHash({ page: 'alert-events', keyword: resolvedVIN, protocol: activeProtocol }))}`, `统计查询:${vehicleServiceURL(vehicleServiceHash('mileage'))}`, `车辆服务:${vehicleServiceURL(vehicleServiceHash('detail'))}` ]; copyText(lines.join('\n'), '单车处理清单'); }; const detailTaskBoard = [ { title: '看实时位置', value: lastSeen, detail: evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount} 在线,先确认车辆当前是否仍在上报。` : '暂无在线数据,优先确认数据接入。', color: online ? 'green' as const : 'orange' as const, primaryAction: '实时监控', secondaryAction: '复制摘要', disabled: !hasResolvedVIN, onPrimary: () => onOpenRealtime(resolvedVIN, activeProtocol), onSecondary: () => copyUnifiedVehicleService() }, { title: '回放轨迹证据', value: `${formatCompactNumber(overview?.historyCount ?? detail?.history?.total ?? 0)} 条历史`, detail: evidenceLocatedSourceCount > 0 ? `${evidenceLocatedSourceCount} 个位置证据,可回放定位、速度和里程断点。` : '暂无有效位置证据,先检查实时坐标。', color: evidenceLocatedSourceCount > 0 ? 'blue' as const : 'grey' as const, primaryAction: '轨迹回放', secondaryAction: '处理清单', disabled: !hasResolvedVIN, onPrimary: () => onOpenHistory(resolvedVIN, activeProtocol), onSecondary: () => copyVehicleRunbook() }, { title: '查询历史数据', value: `${formatCompactNumber(overview?.rawCount ?? detail?.raw?.total ?? 0)} 帧`, detail: latestRaw?.frameType ? `最新历史数据类型 ${latestRaw.frameType},用于核对解析字段。` : '按车辆和数据通道查询历史明细与解析字段。', color: (overview?.rawCount ?? detail?.raw?.total ?? 0) > 0 ? 'blue' as const : 'grey' as const, primaryAction: '历史数据', secondaryAction: '导出档案', disabled: !hasResolvedVIN, onPrimary: () => onOpenRaw(resolvedVIN, activeProtocol), onSecondary: () => exportVehicleReport() }, { title: '复核统计查询', value: `${formatCompactNumber(overview?.mileageCount ?? detail?.mileage?.total ?? 0)} 条统计`, detail: `证据里程差 ${formatCompactNumber(evidenceMileageDelta, ' km')},确认区间和日统计能闭合。`, color: isFiniteNumber(evidenceMileageDelta) && Math.abs(evidenceMileageDelta) > 1 ? 'orange' as const : 'green' as const, primaryAction: '统计查询', secondaryAction: '诊断摘要', disabled: !hasResolvedVIN, onPrimary: () => onOpenMileage(resolvedVIN, activeProtocol), onSecondary: () => copyVehicleDiagnosticSummary() }, { title: '闭环告警事件', value: `${qualityCount.toLocaleString()} 项`, detail: priorityQualityIssue ? `${qualityIssueLabel(priorityQualityIssue.issueType)} / ${priorityQualityIssue.lastSeen || '-'}` : '暂无高优先级告警,保持观察。', color: qualityCount > 0 ? 'orange' as const : 'green' as const, primaryAction: '告警事件', secondaryAction: '运营摘要', disabled: !onOpenQuality || (!hasResolvedVIN && qualityCount <= 0), onPrimary: () => onOpenQuality?.(qualityFiltersForCurrentVehicle()), onSecondary: () => copyVehicleOperationsSummary() } ]; const customerServicePackageItems = [ { title: '实时监控', value: online ? '在线' : hasResolvedVIN ? '离线' : '待绑定', detail: lastSeen === '-' ? '暂无最后上报时间' : `最后上报 ${lastSeen}`, color: online ? 'green' as const : 'orange' as const, action: '打开监控', disabled: !hasResolvedVIN, onClick: () => onOpenRealtime(resolvedVIN, activeProtocol) }, { title: '轨迹回放', value: `${formatCompactNumber(overview?.historyCount ?? detail?.history?.total ?? 0)} 条`, detail: evidenceLocatedSourceCount > 0 ? `${evidenceLocatedSourceCount} 个通道可定位` : '暂无可用定位数据', color: evidenceLocatedSourceCount > 0 ? 'blue' as const : 'grey' as const, action: '回放轨迹', disabled: !hasResolvedVIN, onClick: () => onOpenHistory(resolvedVIN, activeProtocol) }, { title: '历史数据查询', value: `${formatCompactNumber(overview?.rawCount ?? detail?.raw?.total ?? 0)} 帧`, detail: latestRaw?.frameType ? `最新 ${latestRaw.frameType}` : 'RAW 与解析字段证据', color: (overview?.rawCount ?? detail?.raw?.total ?? 0) > 0 ? 'blue' as const : 'grey' as const, action: '查询历史', disabled: !hasResolvedVIN, onClick: () => onOpenRaw(resolvedVIN, activeProtocol) }, { title: '告警通知', value: `${qualityCount.toLocaleString()} 项`, detail: priorityQualityIssue ? `${qualityIssueLabel(priorityQualityIssue.issueType)} / ${priorityQualityIssue.lastSeen || '-'}` : '暂无待通知告警', color: qualityCount > 0 ? 'orange' as const : 'green' as const, action: '告警闭环', disabled: !onOpenQuality || (!hasResolvedVIN && qualityCount <= 0), onClick: () => onOpenQuality?.(qualityFiltersForCurrentVehicle()) }, { title: '统计查询', value: `${formatCompactNumber(overview?.mileageCount ?? detail?.mileage?.total ?? 0)} 条`, detail: `里程差 ${formatCompactNumber(evidenceMileageDelta, ' km')}`, color: isFiniteNumber(evidenceMileageDelta) && Math.abs(evidenceMileageDelta) > 1 ? 'orange' as const : 'green' as const, action: '统计复核', disabled: !hasResolvedVIN, onClick: () => onOpenMileage(resolvedVIN, activeProtocol) } ]; const liveViewActions = [ { title: '实时位置', value: formatLocation(latest), detail: lastSeen === '-' ? '暂无最后上报时间' : `最后上报 ${lastSeen}`, action: '查看实时', color: online ? 'green' as const : 'orange' as const, disabled: !hasResolvedVIN, onClick: () => onOpenRealtime(resolvedVIN, activeProtocol) }, { title: '轨迹历史', value: `${formatCompactNumber(overview?.historyCount ?? detail?.history?.total ?? 0)} 条`, detail: evidenceLocatedSourceCount > 0 ? `${evidenceLocatedSourceCount} 个定位证据` : '暂无有效定位', action: '轨迹回放', color: evidenceLocatedSourceCount > 0 ? 'blue' as const : 'grey' as const, disabled: !hasResolvedVIN, onClick: () => onOpenHistory(resolvedVIN, activeProtocol) }, { title: '里程与能源', value: `${formatCompactNumber(latest?.totalMileageKm, ' km')}`, detail: `SOC ${formatCompactNumber(latest?.socPercent, '%')} / 速度 ${formatCompactNumber(latest?.speedKmh, ' km/h')}`, action: '统计查询', color: isFiniteNumber(latest?.totalMileageKm) ? 'green' as const : 'orange' as const, disabled: !hasResolvedVIN, onClick: () => onOpenMileage(resolvedVIN, activeProtocol) }, { title: '数据导出', value: `${formatCompactNumber(overview?.rawCount ?? detail?.raw?.total ?? 0)} 帧`, detail: latestRaw?.frameType ? `最新 ${latestRaw.frameType}` : '历史明细与字段', action: '历史导出', color: (overview?.rawCount ?? detail?.raw?.total ?? 0) > 0 ? 'blue' as const : 'grey' as const, disabled: !hasResolvedVIN, onClick: () => onOpenRaw(resolvedVIN, activeProtocol) }, { title: '告警通知', value: `${qualityCount.toLocaleString()} 项`, detail: priorityQualityIssue ? `${qualityIssueLabel(priorityQualityIssue.issueType)} / ${priorityQualityIssue.lastSeen || '-'}` : '暂无待通知告警', action: '告警闭环', color: qualityCount > 0 ? 'orange' as const : 'green' as const, disabled: !onOpenQuality || (!hasResolvedVIN && qualityCount <= 0), onClick: () => onOpenQuality?.(qualityFiltersForCurrentVehicle()) } ]; const customerQuestionActions = [ { question: '这辆车现在在哪里?', answer: formatLocation(latest), action: '实时监控', color: online ? 'green' as const : 'orange' as const, disabled: !hasResolvedVIN, onClick: () => onOpenRealtime(resolvedVIN, activeProtocol) }, { question: '最近是否还在线?', answer: lastSeen === '-' ? '暂无最后上报' : `最后上报 ${lastSeen}`, action: '查看实时', color: online ? 'green' as const : 'orange' as const, disabled: !hasResolvedVIN, onClick: () => onOpenRealtime(resolvedVIN, activeProtocol) }, { question: '能不能回放轨迹?', answer: `${formatCompactNumber(overview?.historyCount ?? detail?.history?.total ?? 0)} 条轨迹证据`, action: '轨迹回放', color: evidenceLocatedSourceCount > 0 ? 'blue' as const : 'grey' as const, disabled: !hasResolvedVIN, onClick: () => onOpenHistory(resolvedVIN, activeProtocol) }, { question: '这辆车跑了多少?', answer: `${formatCompactNumber(latest?.totalMileageKm, ' km')} / ${formatCompactNumber(overview?.mileageCount ?? detail?.mileage?.total ?? 0)} 条统计`, action: '统计查询', color: isFiniteNumber(latest?.totalMileageKm) ? 'green' as const : 'orange' as const, disabled: !hasResolvedVIN, onClick: () => onOpenMileage(resolvedVIN, activeProtocol) }, { question: '数据能导出吗?', answer: `${formatCompactNumber(overview?.rawCount ?? detail?.raw?.total ?? 0)} 条历史数据`, action: '历史导出', color: (overview?.rawCount ?? detail?.raw?.total ?? 0) > 0 ? 'blue' as const : 'grey' as const, disabled: !hasResolvedVIN, onClick: () => onOpenRaw(resolvedVIN, activeProtocol) }, { question: '是否需要通知处理?', answer: priorityQualityIssue ? `${qualityIssueLabel(priorityQualityIssue.issueType)} / ${priorityQualityIssue.lastSeen || '-'}` : '暂无待通知告警', action: '告警通知', color: qualityCount > 0 ? 'orange' as const : 'green' as const, disabled: !onOpenQuality || (!hasResolvedVIN && qualityCount <= 0), onClick: () => onOpenQuality?.(qualityFiltersForCurrentVehicle()) } ]; const copyCustomerServicePackage = () => { if (!detail) { Toast.warning('当前没有可复制的客户服务交付包'); return; } const vehicleName = [archivePlate, displayVIN].filter((item) => item && item !== '-').join(' / ') || displayLookupKey; const lines = [ '【单车客户服务交付包】', `车辆:${vehicleName}`, `当前范围:${scopeText}`, `平台能力:实时监控 / 轨迹回放 / 历史数据查询 / 告警通知 / 统计查询`, `服务状态:${customerStatusTitle(serviceStatus?.title) || serviceConclusion.status}`, `服务说明:${serviceStatus?.detail ?? serviceConclusion.risk}`, `在线数据通道:${evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount}` : '-'}`, `最新上报:${lastSeen}`, `轨迹证据:${overview?.historyCount ?? detail.history.total ?? 0} 条`, `历史数据:${overview?.rawCount ?? detail.raw.total ?? 0} 条`, `统计查询:${overview?.mileageCount ?? detail.mileage.total ?? 0} 条,证据差异 ${formatCompactNumber(evidenceMileageDelta, ' km')}`, `告警事件:${qualityCount.toLocaleString()} 项`, `车辆服务:${vehicleServiceURL(vehicleServiceHash('detail'))}`, `实时监控:${vehicleServiceURL(vehicleServiceHash('realtime'))}`, `轨迹回放:${vehicleServiceURL(vehicleServiceHash('history'))}`, `历史数据查询:${vehicleServiceURL(vehicleServiceHash('history-query', { tab: 'raw', includeFields: 'true' }))}`, `告警通知:${vehicleServiceURL(buildAppHash({ page: 'alert-events', keyword: resolvedVIN, protocol: activeProtocol }))}`, `统计查询:${vehicleServiceURL(vehicleServiceHash('mileage'))}` ]; copyText(lines.join('\n'), '客户服务交付包'); }; const qualityTable = ( `${row?.protocol ?? ''}-${row?.issueType ?? ''}-${row?.lastSeen ?? ''}`} dataSource={detail?.quality?.items ?? []} columns={[ { title: '来源', dataIndex: 'protocol', width: 130 }, { title: '问题', dataIndex: 'issueType', width: 150 }, { title: '级别', width: 110, render: (_: unknown, row: QualityIssueRow) => {row.severity} }, { title: '最后时间', dataIndex: 'lastSeen', width: 190 }, { title: '说明', dataIndex: 'detail' } ]} /> ); const exportVehicleReport = () => { if (!detail) { Toast.warning('当前没有可导出的车辆档案'); return; } const rows: VehicleReportRow[] = [ { section: '身份档案', item: '车辆主键', value: reportText(displayVIN) }, { section: '身份档案', item: '查询关键词', value: reportText(displayLookupKey) }, { section: '身份档案', item: '车牌', value: reportText(archivePlate) }, { section: '身份档案', item: '手机号', value: reportText(archivePhone) }, { section: '身份档案', item: 'OEM', value: reportText(archiveOEM) }, { section: '身份档案', item: '档案完整度', value: archiveCompleteness }, { section: '身份档案', item: '档案缺项', value: reportText(archiveMissingLabels) }, { section: '服务结论', item: '服务判断', value: serviceConclusion.status }, { section: '服务结论', item: '主要风险', value: serviceConclusion.risk }, { section: '服务结论', item: '下一步', value: serviceConclusion.nextStep }, { section: '车辆服务', item: '数据通道', value: evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount} 在线` : '-' }, { section: '车辆服务', item: '定位数据', value: evidenceLocatedSourceCount > 0 ? `${evidenceLocatedSourceCount} 个通道有位置` : '暂无位置' }, { section: '车辆服务', item: '里程差异', value: formatCompactNumber(evidenceMileageDelta, ' km') }, { section: '车辆服务', item: '时间差异', value: formatSeconds(evidenceTimeDeltaSeconds) }, { section: '车辆服务', item: '一致性结论', value: customerStatusTitle(consistencyTitle) || consistencyTitle }, { section: '车辆服务', item: '一致性说明', value: consistencyDetail }, { section: '数据规模', item: '历史记录', value: String(overview?.historyCount ?? detail.history.total ?? 0) }, { section: '数据规模', item: '历史数据', value: String(overview?.rawCount ?? detail.raw.total ?? 0) }, { section: '数据规模', item: '里程记录', value: String(overview?.mileageCount ?? detail.mileage.total ?? 0) }, { section: '数据规模', item: '质量问题', value: String(qualityCount) } ]; for (const source of detail.sourceStatus ?? []) { rows.push( { section: `数据通道 ${source.protocol}`, item: '在线', value: source.online ? '在线' : '离线' }, { section: `数据通道 ${source.protocol}`, item: '最后时间', value: reportText(source.lastSeen) }, { section: `数据通道 ${source.protocol}`, item: '能力', value: sourceCapabilities.filter((item) => source[item.key]).map((item) => item.label).join('|') || '-' } ); } downloadCsv(vehicleReportFileName(displayVIN, activeProtocol), buildCsv(vehicleReportColumns, rows)); Toast.success('已导出车辆服务档案'); }; return (
{ const nextQuery = { keyword: String(values.keyword ?? ''), protocol: String(values.protocol ?? '') }; setQuery(nextQuery); onQueryChange?.(nextQuery.keyword, nextQuery.protocol); load(nextQuery); }}> GB32960 JT808 YUTONG_MQTT
当前查看范围 {scopeText} {activeProtocol ? '下方实时、轨迹、历史数据和里程仅展示该数据通道返回的数据。' : '下方数据按车辆聚合展示全部可用数据通道。'}
{serviceStatus ? (
车辆服务状态 {customerStatusTitle(serviceStatus.title)} {serviceStatus.detail}
) : null}
单车 Live View {online ? '在线' : hasResolvedVIN ? '离线' : '待绑定'} {customerStatusTitle(serviceStatus?.title) || serviceConclusion.status} {[archivePlate, displayVIN].filter((item) => item && item !== '-').join(' / ') || displayLookupKey} 客户打开一辆车时,先看当前位置、最后上报、速度、SOC、总里程和告警,再进入轨迹回放、统计查询、数据导出和告警闭环。
{[ { label: '当前位置', value: formatLocation(latest) }, { label: '最后上报', value: lastSeen }, { label: '速度', value: formatCompactNumber(latest?.speedKmh, ' km/h') }, { label: 'SOC', value: formatCompactNumber(latest?.socPercent, '%') }, { label: '总里程', value: formatCompactNumber(latest?.totalMileageKm, ' km') } ].map((item) => (
{item.label} {item.value}
))}
{liveViewActions.map((item) => ( ))}
{customerQuestionActions.map((item) => ( ))}
车辆运营总览} style={{ marginTop: 16 }} >
{scopeText} {customerStatusTitle(serviceStatus?.title) || serviceConclusion.status} {[archivePlate, displayVIN].filter((item) => item && item !== '-').join(' / ') || displayLookupKey} {serviceStatus?.detail ?? serviceConclusion.risk}
{unifiedVehicleServiceItems.map((item) => (
{item.label} {item.value} {item.detail}
))}
车辆服务档案总览} style={{ marginTop: 16 }} >
{hasResolvedVIN ? '已解析 VIN' : '待维护身份'} {scopeText} {customerStatusTitle(serviceStatus?.title) || serviceConclusion.status} {[archivePlate, displayVIN].filter((item) => item && item !== '-').join(' / ') || displayLookupKey}
{[ { label: '手机号', value: archivePhone || '-' }, { label: 'OEM', value: archiveOEM || '-' }, { label: '主数据通道', value: overview?.primaryProtocol || summary?.primaryProtocol || '-' }, { label: '最后上报', value: lastSeen }, { label: '档案完整度', value: archiveCompleteness }, { label: '下一步', value: serviceActions[0]?.label ?? serviceConclusion.nextStep } ].map((item) => (
{item.label} {item.value}
))}
{serviceAssetItems.map((item) => (
{item.label} {item.value} {item.detail}
))}
{archiveMissingLabels.length > 0 ? (
{archiveMissingLabels.map((label) => ( {label} ))}
) : null}
单车客户服务交付包} style={{ marginTop: 16 }} >
{customerStatusTitle(serviceStatus?.title) || serviceConclusion.status} {scopeText} {hasResolvedVIN ? 'VIN 已解析' : '身份待绑定'} {[archivePlate, displayVIN].filter((item) => item && item !== '-').join(' / ') || displayLookupKey} 面向客户交付时,以车辆为主对象提供实时监控、轨迹回放、历史数据查询、告警通知和统计查询,协议来源只作为可追溯证据。
{customerServicePackageItems.map((item) => ( ))}
{overview ? ( {coverageStatusText[overview.coverageStatus] ?? overview.coverageStatus} }, { key: '主数据通道', value: overview.primaryProtocol || '-' }, { key: '最后上报', value: overview.lastSeen || '-' }, { key: '服务数据', value: `轨迹 ${overview.historyCount} / 历史数据 ${overview.rawCount} / 里程 ${overview.mileageCount}` }, { key: '实时来源', value: overview.realtimeCount }, { key: '质量问题', value: qualityIssueAction(overview.qualityIssueCount) } ]} /> ) : null}
{[ { label: '服务判断', value: {serviceConclusion.status} }, { label: '主要风险', value: serviceConclusion.risk }, { label: '下一步', value: serviceConclusion.nextStep } ].map((item) => (
{item.label}
{item.value}
))}
{detailTaskBoard.map((item) => (
{item.title} {item.value}
{item.detail}
))}
{detailWorkbench.map((item) => (
{item.title} {item.meta}
{item.value}
{item.detail} {item.actions.map((action) => ( ))}
))}
单车服务处理清单} style={{ marginTop: 16 }} >
{serviceRunbookSteps.map((item, index) => (
{index + 1}
{item.title} {item.evidence}
{item.acceptance}
))}
{overview || consistency ? (
{[ { label: '数据通道', value: evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount} 在线` : '-' }, { label: '定位数据', value: evidenceLocatedSourceCount > 0 ? `${evidenceLocatedSourceCount} 个通道有位置` : '暂无位置' }, { label: '里程差异', value: formatCompactNumber(evidenceMileageDelta, ' km') }, { label: '时间差异', value: formatSeconds(evidenceTimeDeltaSeconds) }, { label: '主数据通道', value: overview?.primaryProtocol || summary?.primaryProtocol || '-' }, { label: '服务结论', value: customerStatusTitle(consistencyTitle) || consistencyTitle } ].map((item) => (
{item.label}
{item.value}
))}
{consistencyDetail}
) : null} {hasResolvedVIN && sourceFusionRows.length > 0 ? ( 数据通道归并矩阵} style={{ marginTop: 16 }} >
{sourceFusionRows.map((row) => (
{row.source.protocol} {row.freshness} {row.role.label}
{row.contribution.map((item) => ( {item} ))}
{[ { label: '实时', value: row.realtimeText }, { label: '定位', value: row.locationText }, { label: '历史数据', value: row.rawText }, { label: '里程', value: row.mileageText } ].map((item) => (
{item.label} {item.value}
))}
))}
) : null} {hasResolvedVIN && vehicleMapPoints.length > 0 ? (
{locatedRealtimeRows.length.toLocaleString()} 个来源有最新位置 {scopeText} {locatedRealtimeRows.map((row) => (
{row.protocol} {row.protocol} / {formatCompactNumber(row.speedKmh, ' km/h')}
{row.longitude}, {row.latitude} {formatCompactNumber(row.totalMileageKm, ' km')} / {row.lastSeen || '-'}
))}
) : null}
{serviceActions.map((item) => (
{item.label} {item.description}
))}
{priorityQualityIssue ? (
{qualityIssueLabel(priorityQualityIssue.issueType)} {priorityQualityIssue.protocol || activeProtocol || '全部来源'} {priorityQualityIssue.lastSeen || '无时间'} {qualityIssueVehicleLabel(priorityQualityIssue, resolvedVIN)} {priorityQualityIssue.detail || '该车存在告警事件,需要结合轨迹和历史数据确认影响范围。'}
) : null}
{hasResolvedVIN ? '已解析 VIN' : '未解析到 VIN'} }, { key: '车牌', value: archivePlate || '-' }, { key: '手机号', value: archivePhone || '-' }, { key: 'OEM', value: archiveOEM || '-' }, { key: '档案完整度', value: ( {archiveCompleteness} {archiveMissingLabels.map((label) => ( {label} ))} ) }, { key: '归并来源数', value: archiveSourceCount }, { key: '在线', value: }, { key: '数据通道', value: protocols.length > 0 ? ( ) : '-' }, { key: '在线数据通道', value: summary ? `${summary.onlineSourceCount}/${summary.sourceCount}` : '-' }, { key: '最后位置时间', value: lastSeen }, { key: '最新历史数据时间', value: latestRaw?.serverTime ?? '-' }, { key: '质量问题', value: 0 ? 'orange' : 'green'}>{qualityCount > 0 ? `${qualityCount} 项` : '无'} } ]} />
{!hasResolvedVIN ? ( ) : null}
{hasResolvedVIN ? ( <> {detail?.sourceStatus?.length ? ( <>
{activeProtocol ? `当前 ${activeProtocol}` : '当前 全部数据通道'}
( {row.protocol} ) }, { title: '车辆服务角色', width: 130, render: (_: unknown, row: VehicleSourceStatus) => { const role = sourceServiceRole(row, summary?.primaryProtocol); return {role.label}; } }, { title: '在线', width: 100, render: (_: unknown, row: VehicleSourceStatus) => }, { title: '最后时间', dataIndex: 'lastSeen', width: 190, render: (value?: string) => value || '无上报' }, { title: '最新位置', width: 170, render: (_: unknown, row: VehicleSourceStatus) => formatLocation(latestRealtimeByProtocol.get(row.protocol)) }, { title: '里程', width: 110, render: (_: unknown, row: VehicleSourceStatus) => formatCompactNumber(latestRealtimeByProtocol.get(row.protocol)?.totalMileageKm, ' km') }, { title: '速度', width: 110, render: (_: unknown, row: VehicleSourceStatus) => formatCompactNumber(latestRealtimeByProtocol.get(row.protocol)?.speedKmh, ' km/h') }, { title: 'SOC', width: 90, render: (_: unknown, row: VehicleSourceStatus) => formatCompactNumber(latestRealtimeByProtocol.get(row.protocol)?.socPercent, '%') }, ...sourceCapabilities.map((item) => ({ title: item.label, width: 90, render: (_: unknown, row: VehicleSourceStatus) => {row[item.key] ? '有' : '无'} })), { title: '操作', width: 330, render: (_: unknown, row: VehicleSourceStatus) => ( ) } ]} /> ) : ( 暂未查询到该车辆的数据通道覆盖。 )} 0 ? `${consistencySourceCount} 个通道` : '-' }, { key: '在线数据通道', value: consistencySourceCount > 0 ? `${consistencyOnlineSourceCount}/${consistencySourceCount} 在线` : '-' }, { key: '缺失通道', value: missingProtocolText }, { key: '位置覆盖', value: consistencyLocatedSourceCount > 0 ? `${consistencyLocatedSourceCount} 个通道有位置` : '暂无位置' }, { key: '里程差异', value: formatCompactNumber(consistencyMileageDelta, ' km') }, { key: '通道时间差', value: formatSeconds(consistencyTimeDeltaSeconds) }, { key: '诊断说明', value: consistencyDetail }, { key: '车辆服务视角', value: activeProtocol ? `当前只看 ${activeProtocol}` : '三类来源合并为同一车辆服务' } ]} /> ) : null} {!hasResolvedVIN ? ( {qualityTable} ) : (
{[ { label: '最新来源', value: summary?.primaryProtocol || '-' }, { label: '速度 km/h', value: summary ? summary.speedKmh : '-' }, { label: 'SOC %', value: summary ? summary.socPercent : '-' }, { label: '总里程 km', value: summary ? summary.totalMileageKm : '-' }, { label: '经纬度', value: summary ? `${summary.longitude}, ${summary.latitude}` : '-' }, { label: '最后时间', value: lastSeen } ].map((item) => (
{item.label}
{item.value}
))}
最新历史数据字段明细
{JSON.stringify(latestRaw?.parsedFields ?? {}, null, 2)}
{qualityTable} )} ); }