feat(platform): refine vehicle service workspace

This commit is contained in:
lingniu
2026-07-05 13:01:34 +08:00
parent 9f5a091b87
commit 3cdded42c5
4 changed files with 196 additions and 175 deletions

View File

@@ -84,7 +84,7 @@ export default function App() {
api.alertEventNotificationPlan(new URLSearchParams({ limit: '5' })).catch(() => null) api.alertEventNotificationPlan(new URLSearchParams({ limit: '5' })).catch(() => null)
]) ])
.then(([health, notificationPlan]) => { .then(([health, notificationPlan]) => {
setLinkIssueCount(health.linkHealth.filter((item) => item.status !== 'ok').length); setLinkIssueCount((health.linkHealth ?? []).filter((item) => item.status !== 'ok').length);
setPlatformRelease(health.runtime?.platformRelease ?? ''); setPlatformRelease(health.runtime?.platformRelease ?? '');
applyNotificationPlanSummary(notificationPlan); applyNotificationPlanSummary(notificationPlan);
return health; return health;
@@ -458,11 +458,11 @@ export default function App() {
'history-query': <History mode="query" initialVin={analysisVin} initialProtocol={activeProtocol} initialTab={historyTab} initialFilters={historyFilters} onFiltersChange={updateHistoryFilters} onOpenVehicle={openVehicle} onOpenMileage={openMileageWithFilters} onOpenRaw={openRawWithFilters} />, 'history-query': <History mode="query" initialVin={analysisVin} initialProtocol={activeProtocol} initialTab={historyTab} initialFilters={historyFilters} onFiltersChange={updateHistoryFilters} onOpenVehicle={openVehicle} onOpenMileage={openMileageWithFilters} onOpenRaw={openRawWithFilters} />,
mileage: <Mileage initialVin={analysisVin} initialProtocol={activeProtocol} initialFilters={mileageFilters} onFiltersChange={updateMileageFilters} onOpenVehicle={openVehicle} onOpenHistory={openHistoryWithFilters} onOpenRaw={openRawWithFilters} />, mileage: <Mileage initialVin={analysisVin} initialProtocol={activeProtocol} initialFilters={mileageFilters} onFiltersChange={updateMileageFilters} onOpenVehicle={openVehicle} onOpenHistory={openHistoryWithFilters} onOpenRaw={openRawWithFilters} />,
'alert-events': <Quality onOpenVehicle={openVehicle} onOpenRealtime={openRealtime} onOpenHistory={openHistoryWithFilters} onOpenRaw={openRawWithFilters} onOpenMileage={openMileageWithFilters} onOpenNotificationRules={() => navigatePage('notification-rules')} onHealthLoaded={(health) => { 'alert-events': <Quality onOpenVehicle={openVehicle} onOpenRealtime={openRealtime} onOpenHistory={openHistoryWithFilters} onOpenRaw={openRawWithFilters} onOpenMileage={openMileageWithFilters} onOpenNotificationRules={() => navigatePage('notification-rules')} onHealthLoaded={(health) => {
setLinkIssueCount(health.linkHealth.filter((item) => item.status !== 'ok').length); setLinkIssueCount((health.linkHealth ?? []).filter((item) => item.status !== 'ok').length);
setPlatformRelease(health.runtime?.platformRelease ?? ''); setPlatformRelease(health.runtime?.platformRelease ?? '');
}} onNotificationPlanLoaded={applyNotificationPlanSummary} onFiltersChange={updateQualityFilters} initialFilters={qualityFilters} />, }} onNotificationPlanLoaded={applyNotificationPlanSummary} onFiltersChange={updateQualityFilters} initialFilters={qualityFilters} />,
quality: <Quality onOpenVehicle={openVehicle} onOpenRealtime={openRealtime} onOpenHistory={openHistoryWithFilters} onOpenRaw={openRawWithFilters} onOpenMileage={openMileageWithFilters} onOpenNotificationRules={() => navigatePage('notification-rules')} onHealthLoaded={(health) => { quality: <Quality onOpenVehicle={openVehicle} onOpenRealtime={openRealtime} onOpenHistory={openHistoryWithFilters} onOpenRaw={openRawWithFilters} onOpenMileage={openMileageWithFilters} onOpenNotificationRules={() => navigatePage('notification-rules')} onHealthLoaded={(health) => {
setLinkIssueCount(health.linkHealth.filter((item) => item.status !== 'ok').length); setLinkIssueCount((health.linkHealth ?? []).filter((item) => item.status !== 'ok').length);
setPlatformRelease(health.runtime?.platformRelease ?? ''); setPlatformRelease(health.runtime?.platformRelease ?? '');
}} onNotificationPlanLoaded={applyNotificationPlanSummary} onFiltersChange={updateQualityFilters} initialFilters={qualityFilters} />, }} onNotificationPlanLoaded={applyNotificationPlanSummary} onFiltersChange={updateQualityFilters} initialFilters={qualityFilters} />,
'notification-rules': <NotificationRules />, 'notification-rules': <NotificationRules />,

View File

@@ -20,8 +20,8 @@ type VehicleQuery = {
const sourceCapabilities: Array<{ key: keyof Pick<VehicleSourceStatus, 'hasRealtime' | 'hasHistory' | 'hasRaw' | 'hasMileage'>; label: string }> = [ const sourceCapabilities: Array<{ key: keyof Pick<VehicleSourceStatus, 'hasRealtime' | 'hasHistory' | 'hasRaw' | 'hasMileage'>; label: string }> = [
{ key: 'hasRealtime', label: '实时' }, { key: 'hasRealtime', label: '实时' },
{ key: 'hasHistory', label: '历史' }, { key: 'hasHistory', label: '轨迹' },
{ key: 'hasRaw', label: '原始记录' }, { key: 'hasRaw', label: '历史数据' },
{ key: 'hasMileage', label: '里程' } { key: 'hasMileage', label: '里程' }
]; ];
@@ -57,9 +57,18 @@ function formatSeconds(value?: number) {
return `${Math.round(value)}`; 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) { function sourceServiceRole(source: VehicleSourceStatus, primaryProtocol?: string) {
if (source.protocol === primaryProtocol) return { label: '主证据', color: 'blue' as const }; if (source.protocol === primaryProtocol) return { label: '主数据通道', color: 'blue' as const };
if (source.online) return { label: '在线证据', color: 'green' as const }; if (source.online) return { label: '在线数据通道', color: 'green' as const };
if (!source.lastSeen && !source.hasRealtime && !source.hasHistory && !source.hasRaw && !source.hasMileage) { if (!source.lastSeen && !source.hasRealtime && !source.hasHistory && !source.hasRaw && !source.hasMileage) {
return { label: '暂无来源', color: 'grey' as const }; return { label: '暂无来源', color: 'grey' as const };
} }
@@ -70,12 +79,12 @@ function sourceFusionRow(source: VehicleSourceStatus, realtime: RealtimeLocation
const hasLocation = !!realtime && isFiniteNumber(realtime.longitude) && isFiniteNumber(realtime.latitude) && realtime.longitude !== 0 && realtime.latitude !== 0; const hasLocation = !!realtime && isFiniteNumber(realtime.longitude) && isFiniteNumber(realtime.latitude) && realtime.longitude !== 0 && realtime.latitude !== 0;
const hasMileage = isFiniteNumber(realtime?.totalMileageKm) || source.hasMileage; const hasMileage = isFiniteNumber(realtime?.totalMileageKm) || source.hasMileage;
const contribution = [ const contribution = [
source.protocol === primaryProtocol ? '主实时证据' : '', source.protocol === primaryProtocol ? '主实时通道' : '',
source.online ? '在线心跳' : '', source.online ? '在线心跳' : '',
source.hasRealtime || realtime ? '实时字段' : '', source.hasRealtime || realtime ? '实时字段' : '',
hasLocation ? '定位据' : '', hasLocation ? '定位据' : '',
source.hasHistory ? '轨迹回放' : '', source.hasHistory ? '轨迹回放' : '',
source.hasRaw ? '原始记录' : '', source.hasRaw ? '历史数据' : '',
hasMileage ? '统计口径' : '' hasMileage ? '统计口径' : ''
].filter(Boolean); ].filter(Boolean);
return { return {
@@ -85,8 +94,8 @@ function sourceFusionRow(source: VehicleSourceStatus, realtime: RealtimeLocation
freshness: source.online ? `在线 / ${source.lastSeen || realtime?.lastSeen || '-'}` : source.lastSeen ? `离线 / ${source.lastSeen}` : '未接入', freshness: source.online ? `在线 / ${source.lastSeen || realtime?.lastSeen || '-'}` : source.lastSeen ? `离线 / ${source.lastSeen}` : '未接入',
realtimeText: source.hasRealtime || realtime ? `速度 ${formatCompactNumber(realtime?.speedKmh, ' km/h')} / SOC ${formatCompactNumber(realtime?.socPercent, '%')}` : '无实时字段', realtimeText: source.hasRealtime || realtime ? `速度 ${formatCompactNumber(realtime?.speedKmh, ' km/h')} / SOC ${formatCompactNumber(realtime?.socPercent, '%')}` : '无实时字段',
locationText: hasLocation ? `${realtime?.longitude}, ${realtime?.latitude}` : '无有效坐标', locationText: hasLocation ? `${realtime?.longitude}, ${realtime?.latitude}` : '无有效坐标',
rawText: source.hasRaw ? '可查原始记录' : '无原始记录', rawText: source.hasRaw ? '可查历史数据' : '无历史数据',
mileageText: hasMileage ? formatCompactNumber(realtime?.totalMileageKm, ' km') : '无里程据', mileageText: hasMileage ? formatCompactNumber(realtime?.totalMileageKm, ' km') : '无里程据',
contribution: contribution.length > 0 ? contribution : ['待补来源'] contribution: contribution.length > 0 ? contribution : ['待补来源']
}; };
} }
@@ -327,7 +336,7 @@ export function VehicleDetail({
const evidenceMileageDelta = consistencyMileageDelta; const evidenceMileageDelta = consistencyMileageDelta;
const evidenceTimeDeltaSeconds = consistencyTimeDeltaSeconds; const evidenceTimeDeltaSeconds = consistencyTimeDeltaSeconds;
const activeProtocol = query.protocol?.trim() ?? ''; const activeProtocol = query.protocol?.trim() ?? '';
const scopeText = activeProtocol ? `单一来源:${activeProtocol}` : '全部来源聚合'; const scopeText = customerScopeText(activeProtocol);
const archivePlate = resolution?.plate || summary?.plate || identity?.plate || latest?.plate || ''; const archivePlate = resolution?.plate || summary?.plate || identity?.plate || latest?.plate || '';
const archivePhone = resolution?.phone || summary?.phone || identity?.phone || ''; const archivePhone = resolution?.phone || summary?.phone || identity?.phone || '';
const archiveOEM = resolution?.oem || summary?.oem || identity?.oem || ''; const archiveOEM = resolution?.oem || summary?.oem || identity?.oem || '';
@@ -406,39 +415,39 @@ export function VehicleDetail({
`车辆:${vehicleName}`, `车辆:${vehicleName}`,
`查询关键词:${reportText(displayLookupKey)}`, `查询关键词:${reportText(displayLookupKey)}`,
`当前范围:${scopeText}`, `当前范围:${scopeText}`,
`服务状态:${serviceStatus?.title ?? serviceConclusion.status}`, `服务状态:${customerStatusTitle(serviceStatus?.title) || serviceConclusion.status}`,
`服务说明:${serviceStatus?.detail ?? '-'}`, `服务说明:${serviceStatus?.detail ?? '-'}`,
`服务判断:${serviceConclusion.status}`, `服务判断:${serviceConclusion.status}`,
`主要风险:${serviceConclusion.risk}`, `主要风险:${serviceConclusion.risk}`,
`下一步:${serviceConclusion.nextStep}`, `下一步:${serviceConclusion.nextStep}`,
`证据来源${evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount} 在线` : '-'}`, `数据通道${evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount} 在线` : '-'}`,
`定位据:${evidenceLocatedSourceCount > 0 ? `${evidenceLocatedSourceCount}证据有位置` : '暂无位置'}`, `定位据:${evidenceLocatedSourceCount > 0 ? `${evidenceLocatedSourceCount}通道有位置` : '暂无位置'}`,
`里程差异:${formatCompactNumber(evidenceMileageDelta, ' km')}`, `里程差异:${formatCompactNumber(evidenceMileageDelta, ' km')}`,
`时间差异:${formatSeconds(evidenceTimeDeltaSeconds)}`, `时间差异:${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}`, `服务数据:实时 ${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}`,
`一致性:${consistencyTitle}`, `一致性:${customerStatusTitle(consistencyTitle) || consistencyTitle}`,
`一致性说明:${consistencyDetail}`, `一致性说明:${consistencyDetail}`,
`车辆服务:${vehicleServiceURL(vehicleServiceHash('detail'))}`, `车辆服务:${vehicleServiceURL(vehicleServiceHash('detail'))}`,
`实时:${vehicleServiceURL(vehicleServiceHash('realtime'))}`, `实时:${vehicleServiceURL(vehicleServiceHash('realtime'))}`,
`轨迹:${vehicleServiceURL(vehicleServiceHash('history'))}`, `轨迹:${vehicleServiceURL(vehicleServiceHash('history'))}`,
`原始记录${vehicleServiceURL(vehicleServiceHash('history-query', { tab: 'raw', includeFields: 'true' }))}`, `历史数据${vehicleServiceURL(vehicleServiceHash('history-query', { tab: 'raw', includeFields: 'true' }))}`,
`里程:${vehicleServiceURL(vehicleServiceHash('mileage'))}` `里程:${vehicleServiceURL(vehicleServiceHash('mileage'))}`
]; ];
copyText(lines.join('\n'), '诊断摘要'); copyText(lines.join('\n'), '诊断摘要');
}; };
const copySourceFusionReport = () => { const copySourceFusionReport = () => {
if (!detail || sourceFusionRows.length === 0) { if (!detail || sourceFusionRows.length === 0) {
Toast.warning('当前没有可复制的多证据归并矩阵'); Toast.warning('当前没有可复制的数据通道归并矩阵');
return; return;
} }
const vehicleName = [archivePlate, displayVIN].filter((item) => item && item !== '-').join(' / ') || displayLookupKey; const vehicleName = [archivePlate, displayVIN].filter((item) => item && item !== '-').join(' / ') || displayLookupKey;
const lines = [ const lines = [
'【单车多证据归并矩阵】', '【单车数据通道归并矩阵】',
`车辆:${vehicleName}`, `车辆:${vehicleName}`,
`当前范围:${scopeText}`, `当前范围:${scopeText}`,
`证据来源${overview?.primaryProtocol || summary?.primaryProtocol || '-'}`, `数据通道${overview?.primaryProtocol || summary?.primaryProtocol || '-'}`,
`证据覆盖:${evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount} 在线` : '-'}`, `通道覆盖:${evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount} 在线` : '-'}`,
`一致性:${consistencyTitle}`, `一致性:${customerStatusTitle(consistencyTitle) || consistencyTitle}`,
'', '',
...sourceFusionRows.map((row, index) => [ ...sourceFusionRows.map((row, index) => [
`${index + 1}. ${row.source.protocol} / ${row.role.label}`, `${index + 1}. ${row.source.protocol} / ${row.role.label}`,
@@ -446,17 +455,17 @@ export function VehicleDetail({
` 贡献:${row.contribution.join('、')}`, ` 贡献:${row.contribution.join('、')}`,
` 实时:${row.realtimeText}`, ` 实时:${row.realtimeText}`,
` 定位:${row.locationText}`, ` 定位:${row.locationText}`,
` 原始记录${row.rawText}`, ` 历史数据${row.rawText}`,
` 里程:${row.mileageText}` ` 里程:${row.mileageText}`
].join('\n')), ].join('\n')),
'', '',
`车辆服务:${vehicleServiceURL(vehicleServiceHash('detail'))}`, `车辆服务:${vehicleServiceURL(vehicleServiceHash('detail'))}`,
`实时监控:${vehicleServiceURL(vehicleServiceHash('realtime'))}`, `实时监控:${vehicleServiceURL(vehicleServiceHash('realtime'))}`,
`轨迹回放:${vehicleServiceURL(vehicleServiceHash('history'))}`, `轨迹回放:${vehicleServiceURL(vehicleServiceHash('history'))}`,
`原始记录${vehicleServiceURL(vehicleServiceHash('history-query', { tab: 'raw', includeFields: 'true' }))}`, `历史数据${vehicleServiceURL(vehicleServiceHash('history-query', { tab: 'raw', includeFields: 'true' }))}`,
`里程统计:${vehicleServiceURL(vehicleServiceHash('mileage'))}` `里程统计:${vehicleServiceURL(vehicleServiceHash('mileage'))}`
]; ];
copyText(lines.join('\n'), '多证据归并矩阵'); copyText(lines.join('\n'), '数据通道归并矩阵');
}; };
const qualityIssueAction = (count: number) => { const qualityIssueAction = (count: number) => {
if (count <= 0) { if (count <= 0) {
@@ -488,7 +497,7 @@ export function VehicleDetail({
actions.push({ actions.push({
key: `missing-${item}`, key: `missing-${item}`,
label: `补齐 ${item} 证据`, label: `补齐 ${item} 证据`,
description: `${item} 当前没有形成有效证据来源,会影响这辆车的统一服务可信度。`, description: `${item} 当前没有形成有效数据通道,会影响这辆车的统一服务可信度。`,
color: 'orange', color: 'orange',
onClick: () => onOpenVehicles?.({ serviceStatus: 'degraded', missingProtocol: item }) onClick: () => onOpenVehicles?.({ serviceStatus: 'degraded', missingProtocol: item })
}); });
@@ -537,7 +546,7 @@ export function VehicleDetail({
color: (overview?.historyCount ?? detail?.history?.total ?? 0) > 0 ? 'blue' as const : 'grey' as const color: (overview?.historyCount ?? detail?.history?.total ?? 0) > 0 ? 'blue' as const : 'grey' as const
}, },
{ {
label: '原始记录', label: '历史数据',
value: formatCompactNumber(overview?.rawCount ?? detail?.raw?.total ?? 0), value: formatCompactNumber(overview?.rawCount ?? detail?.raw?.total ?? 0),
detail: latestRaw?.frameType || '解析字段', detail: latestRaw?.frameType || '解析字段',
color: (overview?.rawCount ?? detail?.raw?.total ?? 0) > 0 ? 'blue' as const : 'grey' as const color: (overview?.rawCount ?? detail?.raw?.total ?? 0) > 0 ? 'blue' as const : 'grey' as const
@@ -564,12 +573,12 @@ export function VehicleDetail({
}, },
{ {
label: '服务状态', label: '服务状态',
value: serviceStatus?.title ?? serviceConclusion.status, value: customerStatusTitle(serviceStatus?.title) || serviceConclusion.status,
detail: serviceStatus?.detail ?? serviceConclusion.risk, detail: serviceStatus?.detail ?? serviceConclusion.risk,
color: serviceConclusion.color color: serviceConclusion.color
}, },
{ {
label: '实时主证据来源', label: '实时主数据通道',
value: overview?.primaryProtocol || summary?.primaryProtocol || '-', value: overview?.primaryProtocol || summary?.primaryProtocol || '-',
detail: evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount} 在线` : '暂无来源', detail: evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount} 在线` : '暂无来源',
color: evidenceOnlineSourceCount > 0 ? 'green' as const : 'orange' as const color: evidenceOnlineSourceCount > 0 ? 'green' as const : 'orange' as const
@@ -592,17 +601,17 @@ export function VehicleDetail({
`车辆:${vehicleName}`, `车辆:${vehicleName}`,
`当前范围:${scopeText}`, `当前范围:${scopeText}`,
`车辆身份:${hasResolvedVIN ? displayVIN : '待绑定'}`, `车辆身份:${hasResolvedVIN ? displayVIN : '待绑定'}`,
`服务状态:${serviceStatus?.title ?? serviceConclusion.status}`, `服务状态:${customerStatusTitle(serviceStatus?.title) || serviceConclusion.status}`,
`服务说明:${serviceStatus?.detail ?? serviceConclusion.risk}`, `服务说明:${serviceStatus?.detail ?? serviceConclusion.risk}`,
`实时主证据来源${overview?.primaryProtocol || summary?.primaryProtocol || '-'}`, `实时主数据通道${overview?.primaryProtocol || summary?.primaryProtocol || '-'}`,
`证据在线:${evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount}` : '-'}`, `通道在线:${evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount}` : '-'}`,
`业务可用性:${online ? '在线可用' : hasResolvedVIN ? '离线待查' : '待绑定'}`, `业务可用性:${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}`, `服务数据:实时 ${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}`, `下一步:${serviceActions[0]?.label ?? serviceConclusion.nextStep}`,
`车辆服务:${vehicleServiceURL(vehicleServiceHash('detail'))}`, `车辆服务:${vehicleServiceURL(vehicleServiceHash('detail'))}`,
`实时监控:${vehicleServiceURL(vehicleServiceHash('realtime'))}`, `实时监控:${vehicleServiceURL(vehicleServiceHash('realtime'))}`,
`轨迹回放:${vehicleServiceURL(vehicleServiceHash('history'))}`, `轨迹回放:${vehicleServiceURL(vehicleServiceHash('history'))}`,
`数据导出${vehicleServiceURL(vehicleServiceHash('history-query', { tab: 'raw', includeFields: 'true' }))}`, `历史数据:${vehicleServiceURL(vehicleServiceHash('history-query', { tab: 'raw', includeFields: 'true' }))}`,
`里程统计:${vehicleServiceURL(vehicleServiceHash('mileage'))}` `里程统计:${vehicleServiceURL(vehicleServiceHash('mileage'))}`
]; ];
copyText(lines.join('\n'), '统一车辆服务摘要'); copyText(lines.join('\n'), '统一车辆服务摘要');
@@ -619,17 +628,17 @@ export function VehicleDetail({
`查询关键词:${displayLookupKey}`, `查询关键词:${displayLookupKey}`,
`当前范围:${scopeText}`, `当前范围:${scopeText}`,
`档案完整度:${archiveCompleteness}${archiveMissingLabels.length > 0 ? `${archiveMissingLabels.join('、')}` : ''}`, `档案完整度:${archiveCompleteness}${archiveMissingLabels.length > 0 ? `${archiveMissingLabels.join('、')}` : ''}`,
`证据来源${overview?.primaryProtocol || summary?.primaryProtocol || '-'}`, `数据通道${overview?.primaryProtocol || summary?.primaryProtocol || '-'}`,
`证据在线:${evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount}` : '-'}`, `通道在线:${evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount}` : '-'}`,
`服务状态:${serviceStatus?.title ?? serviceConclusion.status}`, `服务状态:${customerStatusTitle(serviceStatus?.title) || serviceConclusion.status}`,
`服务说明:${serviceStatus?.detail ?? serviceConclusion.risk}`, `服务说明:${serviceStatus?.detail ?? serviceConclusion.risk}`,
`资产概览:${serviceAssetItems.map((item) => `${item.label} ${item.value}`).join(' / ')}`, `资产概览:${serviceAssetItems.map((item) => `${item.label} ${item.value}`).join(' / ')}`,
`一致性:${consistencyTitle}`, `一致性:${customerStatusTitle(consistencyTitle) || consistencyTitle}`,
`下一步:${serviceActions[0]?.label ?? serviceConclusion.nextStep}`, `下一步:${serviceActions[0]?.label ?? serviceConclusion.nextStep}`,
`车辆服务:${vehicleServiceURL(vehicleServiceHash('detail'))}`, `车辆服务:${vehicleServiceURL(vehicleServiceHash('detail'))}`,
`实时监控:${vehicleServiceURL(vehicleServiceHash('realtime'))}`, `实时监控:${vehicleServiceURL(vehicleServiceHash('realtime'))}`,
`轨迹回放:${vehicleServiceURL(vehicleServiceHash('history'))}`, `轨迹回放:${vehicleServiceURL(vehicleServiceHash('history'))}`,
`原始记录${vehicleServiceURL(vehicleServiceHash('history-query', { tab: 'raw', includeFields: 'true' }))}`, `历史数据${vehicleServiceURL(vehicleServiceHash('history-query', { tab: 'raw', includeFields: 'true' }))}`,
`里程统计:${vehicleServiceURL(vehicleServiceHash('mileage'))}` `里程统计:${vehicleServiceURL(vehicleServiceHash('mileage'))}`
]; ];
copyText(lines.join('\n'), '车辆服务档案'); copyText(lines.join('\n'), '车辆服务档案');
@@ -645,19 +654,19 @@ export function VehicleDetail({
'【车辆服务运营摘要】', '【车辆服务运营摘要】',
`车辆:${vehicleName}`, `车辆:${vehicleName}`,
`当前范围:${scopeText}`, `当前范围:${scopeText}`,
`服务状态:${serviceStatus?.title ?? serviceConclusion.status}`, `服务状态:${customerStatusTitle(serviceStatus?.title) || serviceConclusion.status}`,
`业务影响:${serviceConclusion.risk}`, `业务影响:${serviceConclusion.risk}`,
`建议动作:${serviceConclusion.nextStep}`, `建议动作:${serviceConclusion.nextStep}`,
`在线证据${evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount}` : '-'}`, `在线数据通道${evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount}` : '-'}`,
`定位据:${evidenceLocatedSourceCount > 0 ? `${evidenceLocatedSourceCount}` : '0'}`, `定位据:${evidenceLocatedSourceCount > 0 ? `${evidenceLocatedSourceCount}` : '0'}`,
`质量问题:${qualityCount.toLocaleString()}`, `质量问题:${qualityCount.toLocaleString()}`,
`服务数据:轨迹 ${overview?.historyCount ?? detail.history.total ?? 0} / 原始记录 ${overview?.rawCount ?? detail.raw.total ?? 0} / 里程 ${overview?.mileageCount ?? detail.mileage.total ?? 0}`, `服务数据:轨迹 ${overview?.historyCount ?? detail.history.total ?? 0} / 历史数据 ${overview?.rawCount ?? detail.raw.total ?? 0} / 里程 ${overview?.mileageCount ?? detail.mileage.total ?? 0}`,
`下一步清单:${actionLines.length > 0 ? '' : '暂无明确处置项,持续观察实时状态。'}`, `下一步清单:${actionLines.length > 0 ? '' : '暂无明确处置项,持续观察实时状态。'}`,
...actionLines, ...actionLines,
`车辆服务:${vehicleServiceURL(vehicleServiceHash('detail'))}`, `车辆服务:${vehicleServiceURL(vehicleServiceHash('detail'))}`,
`实时监控:${vehicleServiceURL(vehicleServiceHash('realtime'))}`, `实时监控:${vehicleServiceURL(vehicleServiceHash('realtime'))}`,
`轨迹回放:${vehicleServiceURL(vehicleServiceHash('history'))}`, `轨迹回放:${vehicleServiceURL(vehicleServiceHash('history'))}`,
`原始记录${vehicleServiceURL(vehicleServiceHash('history-query', { tab: 'raw', includeFields: 'true' }))}`, `历史数据${vehicleServiceURL(vehicleServiceHash('history-query', { tab: 'raw', includeFields: 'true' }))}`,
`里程统计:${vehicleServiceURL(vehicleServiceHash('mileage'))}` `里程统计:${vehicleServiceURL(vehicleServiceHash('mileage'))}`
]; ];
copyText(lines.join('\n'), '运营摘要'); copyText(lines.join('\n'), '运营摘要');
@@ -668,7 +677,7 @@ export function VehicleDetail({
value: lastSeen, value: lastSeen,
meta: evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount} 在线` : '暂无来源', meta: evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount} 在线` : '暂无来源',
color: online ? 'green' as const : 'orange' as const, color: online ? 'green' as const : 'orange' as const,
detail: '查看该车最新位置、速度、SOC、总里程和多证据实时字段。', detail: '查看该车最新位置、速度、SOC、总里程和多通道实时字段。',
disabled: !hasResolvedVIN, disabled: !hasResolvedVIN,
actions: [ actions: [
{ label: '查看实时', onClick: () => onOpenRealtime(resolvedVIN, activeProtocol) } { label: '查看实时', onClick: () => onOpenRealtime(resolvedVIN, activeProtocol) }
@@ -686,14 +695,14 @@ export function VehicleDetail({
] ]
}, },
{ {
title: '原始记录', title: '历史数据',
value: `${formatCompactNumber(overview?.rawCount ?? detail?.raw?.total ?? 0)}`, value: `${formatCompactNumber(overview?.rawCount ?? detail?.raw?.total ?? 0)}`,
meta: latestRaw?.frameType || '字段明细', meta: latestRaw?.frameType || '字段明细',
color: (overview?.rawCount ?? detail?.raw?.total ?? 0) > 0 ? 'blue' as const : 'grey' as const, color: (overview?.rawCount ?? detail?.raw?.total ?? 0) > 0 ? 'blue' as const : 'grey' as const,
detail: '查看原始记录和字段明细,用于复核定位、里程和告警依据。', detail: '查看历史明细和字段明细,用于复核定位、里程和告警依据。',
disabled: !hasResolvedVIN, disabled: !hasResolvedVIN,
actions: [ actions: [
{ label: '查看原始记录', onClick: () => onOpenRaw(resolvedVIN, activeProtocol) } { label: '查看历史数据', onClick: () => onOpenRaw(resolvedVIN, activeProtocol) }
] ]
}, },
{ {
@@ -712,7 +721,7 @@ export function VehicleDetail({
value: `${formatCompactNumber(overview?.mileageCount ?? detail?.mileage?.total ?? 0)} 条统计`, value: `${formatCompactNumber(overview?.mileageCount ?? detail?.mileage?.total ?? 0)} 条统计`,
meta: formatCompactNumber(evidenceMileageDelta, ' km 差异'), meta: formatCompactNumber(evidenceMileageDelta, ' km 差异'),
color: isFiniteNumber(evidenceMileageDelta) && Math.abs(evidenceMileageDelta) > 1 ? 'orange' as const : 'green' as const, color: isFiniteNumber(evidenceMileageDelta) && Math.abs(evidenceMileageDelta) > 1 ? 'orange' as const : 'green' as const,
detail: '核对统计口径,确认区间总值、每日里程与证据来源是否闭合。', detail: '核对统计口径,确认区间总值、每日里程与数据通道是否闭合。',
disabled: !hasResolvedVIN, disabled: !hasResolvedVIN,
actions: [ actions: [
{ label: '查看里程', onClick: () => onOpenMileage(resolvedVIN, activeProtocol) } { label: '查看里程', onClick: () => onOpenMileage(resolvedVIN, activeProtocol) }
@@ -722,7 +731,7 @@ export function VehicleDetail({
const serviceRunbookSteps = [ const serviceRunbookSteps = [
{ {
title: '确认实时状态', title: '确认实时状态',
evidence: evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount} 在线,最后上报 ${lastSeen}` : '暂无在线据', evidence: evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount} 在线,最后上报 ${lastSeen}` : '暂无在线据',
acceptance: '车辆在线状态、最新时间、有效坐标均可解释。', acceptance: '车辆在线状态、最新时间、有效坐标均可解释。',
action: '打开实时', action: '打开实时',
disabled: !hasResolvedVIN, disabled: !hasResolvedVIN,
@@ -730,17 +739,17 @@ export function VehicleDetail({
}, },
{ {
title: '回放轨迹断点', title: '回放轨迹断点',
evidence: evidenceLocatedSourceCount > 0 ? `${evidenceLocatedSourceCount}证据有位置,时间差 ${formatSeconds(evidenceTimeDeltaSeconds)}` : '暂无可回放位置', evidence: evidenceLocatedSourceCount > 0 ? `${evidenceLocatedSourceCount}通道有位置,时间差 ${formatSeconds(evidenceTimeDeltaSeconds)}` : '暂无可回放位置',
acceptance: '轨迹点、速度和总里程变化连续,异常点可定位到时间段。', acceptance: '轨迹点、速度和总里程变化连续,异常点可定位到时间段。',
action: '轨迹回放', action: '轨迹回放',
disabled: !hasResolvedVIN, disabled: !hasResolvedVIN,
onClick: () => onOpenHistory(resolvedVIN, activeProtocol) onClick: () => onOpenHistory(resolvedVIN, activeProtocol)
}, },
{ {
title: '核对原始记录', title: '查询历史数据',
evidence: `${formatCompactNumber(overview?.rawCount ?? detail?.raw?.total ?? 0)} ,最新类型 ${latestRaw?.frameType || '-'}`, evidence: `${formatCompactNumber(overview?.rawCount ?? detail?.raw?.total ?? 0)} 条历史数据,最新类型 ${latestRaw?.frameType || '-'}`,
acceptance: '关键字段来自解析字段而不是页面二次推断。', acceptance: '关键字段来自解析字段而不是页面二次推断。',
action: '原始记录', action: '历史数据',
disabled: !hasResolvedVIN, disabled: !hasResolvedVIN,
onClick: () => onOpenRaw(resolvedVIN, activeProtocol) onClick: () => onOpenRaw(resolvedVIN, activeProtocol)
}, },
@@ -755,7 +764,7 @@ export function VehicleDetail({
{ {
title: '复核统计口径', title: '复核统计口径',
evidence: `${formatCompactNumber(overview?.mileageCount ?? detail?.mileage?.total ?? 0)} 条统计,证据里程差 ${formatCompactNumber(evidenceMileageDelta, ' km')}`, evidence: `${formatCompactNumber(overview?.mileageCount ?? detail?.mileage?.total ?? 0)} 条统计,证据里程差 ${formatCompactNumber(evidenceMileageDelta, ' km')}`,
acceptance: '区间里程、每日里程和轨迹总里程证据可以互相解释。', acceptance: '区间里程、每日里程和轨迹总里程可以互相解释。',
action: '里程统计', action: '里程统计',
disabled: !hasResolvedVIN, disabled: !hasResolvedVIN,
onClick: () => onOpenMileage(resolvedVIN, activeProtocol) onClick: () => onOpenMileage(resolvedVIN, activeProtocol)
@@ -771,8 +780,8 @@ export function VehicleDetail({
'【单车服务处理清单】', '【单车服务处理清单】',
`车辆:${vehicleName}`, `车辆:${vehicleName}`,
`当前范围:${scopeText}`, `当前范围:${scopeText}`,
`服务状态:${serviceStatus?.title ?? serviceConclusion.status}`, `服务状态:${customerStatusTitle(serviceStatus?.title) || serviceConclusion.status}`,
`一致性:${consistencyTitle}`, `一致性:${customerStatusTitle(consistencyTitle) || consistencyTitle}`,
...serviceRunbookSteps.map((item, index) => [ ...serviceRunbookSteps.map((item, index) => [
`${index + 1}. ${item.title}`, `${index + 1}. ${item.title}`,
` 证据:${item.evidence}`, ` 证据:${item.evidence}`,
@@ -780,7 +789,7 @@ export function VehicleDetail({
].join('\n')), ].join('\n')),
`实时监控:${vehicleServiceURL(vehicleServiceHash('realtime'))}`, `实时监控:${vehicleServiceURL(vehicleServiceHash('realtime'))}`,
`轨迹回放:${vehicleServiceURL(vehicleServiceHash('history'))}`, `轨迹回放:${vehicleServiceURL(vehicleServiceHash('history'))}`,
`原始记录${vehicleServiceURL(vehicleServiceHash('history-query', { tab: 'raw', includeFields: 'true' }))}`, `历史数据${vehicleServiceURL(vehicleServiceHash('history-query', { tab: 'raw', includeFields: 'true' }))}`,
`告警事件:${vehicleServiceURL(buildAppHash({ page: 'alert-events', keyword: resolvedVIN, protocol: activeProtocol }))}`, `告警事件:${vehicleServiceURL(buildAppHash({ page: 'alert-events', keyword: resolvedVIN, protocol: activeProtocol }))}`,
`里程统计:${vehicleServiceURL(vehicleServiceHash('mileage'))}`, `里程统计:${vehicleServiceURL(vehicleServiceHash('mileage'))}`,
`车辆服务:${vehicleServiceURL(vehicleServiceHash('detail'))}` `车辆服务:${vehicleServiceURL(vehicleServiceHash('detail'))}`
@@ -791,7 +800,7 @@ export function VehicleDetail({
{ {
title: '看实时位置', title: '看实时位置',
value: lastSeen, value: lastSeen,
detail: evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount} 在线,先确认车辆当前是否仍在上报。` : '暂无在线据,优先确认数据接入。', detail: evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount} 在线,先确认车辆当前是否仍在上报。` : '暂无在线据,优先确认数据接入。',
color: online ? 'green' as const : 'orange' as const, color: online ? 'green' as const : 'orange' as const,
primaryAction: '实时监控', primaryAction: '实时监控',
secondaryAction: '复制摘要', secondaryAction: '复制摘要',
@@ -811,11 +820,11 @@ export function VehicleDetail({
onSecondary: () => copyVehicleRunbook() onSecondary: () => copyVehicleRunbook()
}, },
{ {
title: '导出原始证据', title: '查询历史数据',
value: `${formatCompactNumber(overview?.rawCount ?? detail?.raw?.total ?? 0)}`, value: `${formatCompactNumber(overview?.rawCount ?? detail?.raw?.total ?? 0)}`,
detail: latestRaw?.frameType ? `最新原始记录类型 ${latestRaw.frameType},用于核对解析字段。` : '按车辆和来源导出 RAW 与解析字段。', detail: latestRaw?.frameType ? `最新历史数据类型 ${latestRaw.frameType},用于核对解析字段。` : '按车辆和数据通道查询历史明细与解析字段。',
color: (overview?.rawCount ?? detail?.raw?.total ?? 0) > 0 ? 'blue' as const : 'grey' as const, color: (overview?.rawCount ?? detail?.raw?.total ?? 0) > 0 ? 'blue' as const : 'grey' as const,
primaryAction: '数据导出', primaryAction: '历史数据',
secondaryAction: '导出档案', secondaryAction: '导出档案',
disabled: !hasResolvedVIN, disabled: !hasResolvedVIN,
onPrimary: () => onOpenRaw(resolvedVIN, activeProtocol), onPrimary: () => onOpenRaw(resolvedVIN, activeProtocol),
@@ -857,7 +866,7 @@ export function VehicleDetail({
{ {
title: '轨迹回放', title: '轨迹回放',
value: `${formatCompactNumber(overview?.historyCount ?? detail?.history?.total ?? 0)}`, value: `${formatCompactNumber(overview?.historyCount ?? detail?.history?.total ?? 0)}`,
detail: evidenceLocatedSourceCount > 0 ? `${evidenceLocatedSourceCount}证据可定位` : '暂无可用定位据', detail: evidenceLocatedSourceCount > 0 ? `${evidenceLocatedSourceCount}通道可定位` : '暂无可用定位据',
color: evidenceLocatedSourceCount > 0 ? 'blue' as const : 'grey' as const, color: evidenceLocatedSourceCount > 0 ? 'blue' as const : 'grey' as const,
action: '回放轨迹', action: '回放轨迹',
disabled: !hasResolvedVIN, disabled: !hasResolvedVIN,
@@ -902,12 +911,12 @@ export function VehicleDetail({
`车辆:${vehicleName}`, `车辆:${vehicleName}`,
`当前范围:${scopeText}`, `当前范围:${scopeText}`,
`平台能力:实时监控 / 轨迹回放 / 历史数据查询 / 告警通知 / 统计查询`, `平台能力:实时监控 / 轨迹回放 / 历史数据查询 / 告警通知 / 统计查询`,
`服务状态:${serviceStatus?.title ?? serviceConclusion.status}`, `服务状态:${customerStatusTitle(serviceStatus?.title) || serviceConclusion.status}`,
`服务说明:${serviceStatus?.detail ?? serviceConclusion.risk}`, `服务说明:${serviceStatus?.detail ?? serviceConclusion.risk}`,
`在线证据${evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount}` : '-'}`, `在线数据通道${evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount}` : '-'}`,
`最新上报:${lastSeen}`, `最新上报:${lastSeen}`,
`轨迹证据:${overview?.historyCount ?? detail.history.total ?? 0}`, `轨迹证据:${overview?.historyCount ?? detail.history.total ?? 0}`,
`原始证据:${overview?.rawCount ?? detail.raw.total ?? 0} `, `历史数据:${overview?.rawCount ?? detail.raw.total ?? 0} `,
`里程统计:${overview?.mileageCount ?? detail.mileage.total ?? 0} 条,证据差异 ${formatCompactNumber(evidenceMileageDelta, ' km')}`, `里程统计:${overview?.mileageCount ?? detail.mileage.total ?? 0} 条,证据差异 ${formatCompactNumber(evidenceMileageDelta, ' km')}`,
`告警事件:${qualityCount.toLocaleString()}`, `告警事件:${qualityCount.toLocaleString()}`,
`车辆服务:${vehicleServiceURL(vehicleServiceHash('detail'))}`, `车辆服务:${vehicleServiceURL(vehicleServiceHash('detail'))}`,
@@ -950,22 +959,22 @@ export function VehicleDetail({
{ section: '服务结论', item: '服务判断', value: serviceConclusion.status }, { section: '服务结论', item: '服务判断', value: serviceConclusion.status },
{ section: '服务结论', item: '主要风险', value: serviceConclusion.risk }, { section: '服务结论', item: '主要风险', value: serviceConclusion.risk },
{ section: '服务结论', item: '下一步', value: serviceConclusion.nextStep }, { section: '服务结论', item: '下一步', value: serviceConclusion.nextStep },
{ section: '证据链', item: '证据来源', value: evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount} 在线` : '-' }, { section: '车辆服务', item: '数据通道', value: evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount} 在线` : '-' },
{ section: '证据链', item: '定位据', value: evidenceLocatedSourceCount > 0 ? `${evidenceLocatedSourceCount}证据有位置` : '暂无位置' }, { section: '车辆服务', item: '定位据', value: evidenceLocatedSourceCount > 0 ? `${evidenceLocatedSourceCount}通道有位置` : '暂无位置' },
{ section: '证据链', item: '里程差异', value: formatCompactNumber(evidenceMileageDelta, ' km') }, { section: '车辆服务', item: '里程差异', value: formatCompactNumber(evidenceMileageDelta, ' km') },
{ section: '证据链', item: '时间差异', value: formatSeconds(evidenceTimeDeltaSeconds) }, { section: '车辆服务', item: '时间差异', value: formatSeconds(evidenceTimeDeltaSeconds) },
{ section: '证据链', item: '一致性结论', value: consistencyTitle }, { section: '车辆服务', item: '一致性结论', value: customerStatusTitle(consistencyTitle) || consistencyTitle },
{ section: '证据链', item: '一致性说明', value: consistencyDetail }, { section: '车辆服务', item: '一致性说明', value: consistencyDetail },
{ section: '数据规模', item: '历史记录', value: String(overview?.historyCount ?? detail.history.total ?? 0) }, { 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?.rawCount ?? detail.raw.total ?? 0) },
{ section: '数据规模', item: '里程记录', value: String(overview?.mileageCount ?? detail.mileage.total ?? 0) }, { section: '数据规模', item: '里程记录', value: String(overview?.mileageCount ?? detail.mileage.total ?? 0) },
{ section: '数据规模', item: '质量问题', value: String(qualityCount) } { section: '数据规模', item: '质量问题', value: String(qualityCount) }
]; ];
for (const source of detail.sourceStatus ?? []) { for (const source of detail.sourceStatus ?? []) {
rows.push( rows.push(
{ section: `证据来源 ${source.protocol}`, item: '在线', value: source.online ? '在线' : '离线' }, { section: `数据通道 ${source.protocol}`, item: '在线', value: source.online ? '在线' : '离线' },
{ section: `证据来源 ${source.protocol}`, item: '最后时间', value: reportText(source.lastSeen) }, { section: `数据通道 ${source.protocol}`, item: '最后时间', value: reportText(source.lastSeen) },
{ section: `证据来源 ${source.protocol}`, item: '能力', value: sourceCapabilities.filter((item) => source[item.key]).map((item) => item.label).join('|') || '-' } { section: `数据通道 ${source.protocol}`, item: '能力', value: sourceCapabilities.filter((item) => source[item.key]).map((item) => item.label).join('|') || '-' }
); );
} }
downloadCsv(vehicleReportFileName(displayVIN, activeProtocol), buildCsv(vehicleReportColumns, rows)); downloadCsv(vehicleReportFileName(displayVIN, activeProtocol), buildCsv(vehicleReportColumns, rows));
@@ -974,7 +983,7 @@ export function VehicleDetail({
return ( return (
<div className="vp-page"> <div className="vp-page">
<PageHeader title="车辆服务" description="以车辆为主对象查看实时位置、轨迹回放、里程统计、告警和可导出的原始记录" /> <PageHeader title="车辆服务" description="以车辆为主对象查看实时位置、轨迹回放、里程统计、告警通知和历史数据" />
<Card bordered> <Card bordered>
<Form key={formKey} initValues={query} layout="horizontal" onSubmit={(values) => { <Form key={formKey} initValues={query} layout="horizontal" onSubmit={(values) => {
const nextQuery = { keyword: String(values.keyword ?? ''), protocol: String(values.protocol ?? '') }; const nextQuery = { keyword: String(values.keyword ?? ''), protocol: String(values.protocol ?? '') };
@@ -983,7 +992,7 @@ export function VehicleDetail({
load(nextQuery); load(nextQuery);
}}> }}>
<Form.Input field="keyword" label="车辆关键词" placeholder="VIN / 车牌 / 手机号" style={{ width: 280 }} /> <Form.Input field="keyword" label="车辆关键词" placeholder="VIN / 车牌 / 手机号" style={{ width: 280 }} />
<Form.Select field="protocol" label="证据来源" placeholder="全部来源" style={{ width: 180 }}> <Form.Select field="protocol" label="数据通道" placeholder="全部数据通道" style={{ width: 180 }}>
<Select.Option value="GB32960">GB32960</Select.Option> <Select.Option value="GB32960">GB32960</Select.Option>
<Select.Option value="JT808">JT808</Select.Option> <Select.Option value="JT808">JT808</Select.Option>
<Select.Option value="YUTONG_MQTT">YUTONG_MQTT</Select.Option> <Select.Option value="YUTONG_MQTT">YUTONG_MQTT</Select.Option>
@@ -997,7 +1006,7 @@ export function VehicleDetail({
<Button disabled={!detail} icon={<IconDownload />} onClick={exportVehicleReport}> CSV</Button> <Button disabled={!detail} icon={<IconDownload />} onClick={exportVehicleReport}> CSV</Button>
<Button disabled={!hasResolvedVIN} onClick={() => onOpenRealtime(resolvedVIN, activeProtocol)}></Button> <Button disabled={!hasResolvedVIN} onClick={() => onOpenRealtime(resolvedVIN, activeProtocol)}></Button>
<Button disabled={!hasResolvedVIN} onClick={() => onOpenHistory(resolvedVIN, activeProtocol)}></Button> <Button disabled={!hasResolvedVIN} onClick={() => onOpenHistory(resolvedVIN, activeProtocol)}></Button>
<Button disabled={!hasResolvedVIN} onClick={() => onOpenRaw(resolvedVIN, activeProtocol)}></Button> <Button disabled={!hasResolvedVIN} onClick={() => onOpenRaw(resolvedVIN, activeProtocol)}></Button>
<Button disabled={!hasResolvedVIN} onClick={() => onOpenMileage(resolvedVIN, activeProtocol)}></Button> <Button disabled={!hasResolvedVIN} onClick={() => onOpenMileage(resolvedVIN, activeProtocol)}></Button>
</Space> </Space>
</Form> </Form>
@@ -1007,7 +1016,7 @@ export function VehicleDetail({
<span className="vp-scope-label"></span> <span className="vp-scope-label"></span>
<Tag color={activeProtocol ? 'blue' : 'green'}>{scopeText}</Tag> <Tag color={activeProtocol ? 'blue' : 'green'}>{scopeText}</Tag>
<Typography.Text type="tertiary"> <Typography.Text type="tertiary">
{activeProtocol ? '下方实时、轨迹、原始记录和里程仅展示该证据来源返回的数据。' : '下方数据按车辆聚合展示全部可用证据来源。'} {activeProtocol ? '下方实时、轨迹、历史数据和里程仅展示该数据通道返回的数据。' : '下方数据按车辆聚合展示全部可用数据通道。'}
</Typography.Text> </Typography.Text>
</div> </div>
@@ -1015,7 +1024,7 @@ export function VehicleDetail({
<div className={`vp-service-status vp-service-status-${serviceStatus.severity}`}> <div className={`vp-service-status vp-service-status-${serviceStatus.severity}`}>
<span className="vp-scope-label"></span> <span className="vp-scope-label"></span>
<Tag color={serviceStatus.severity === 'ok' ? 'green' : serviceStatus.severity === 'error' ? 'red' : 'orange'}> <Tag color={serviceStatus.severity === 'ok' ? 'green' : serviceStatus.severity === 'error' ? 'red' : 'orange'}>
{serviceStatus.title} {customerStatusTitle(serviceStatus.title)}
</Tag> </Tag>
<Typography.Text type="tertiary">{serviceStatus.detail}</Typography.Text> <Typography.Text type="tertiary">{serviceStatus.detail}</Typography.Text>
</div> </div>
@@ -1030,7 +1039,7 @@ export function VehicleDetail({
<div className="vp-unified-service-main"> <div className="vp-unified-service-main">
<Space wrap> <Space wrap>
<Tag color={activeProtocol ? 'blue' : 'green'}>{scopeText}</Tag> <Tag color={activeProtocol ? 'blue' : 'green'}>{scopeText}</Tag>
<Tag color={serviceConclusion.color}>{serviceStatus?.title ?? serviceConclusion.status}</Tag> <Tag color={serviceConclusion.color}>{customerStatusTitle(serviceStatus?.title) || serviceConclusion.status}</Tag>
</Space> </Space>
<Typography.Title heading={4} style={{ margin: 0 }}> <Typography.Title heading={4} style={{ margin: 0 }}>
{[archivePlate, displayVIN].filter((item) => item && item !== '-').join(' / ') || displayLookupKey} {[archivePlate, displayVIN].filter((item) => item && item !== '-').join(' / ') || displayLookupKey}
@@ -1041,7 +1050,7 @@ export function VehicleDetail({
<div className="vp-unified-service-actions"> <div className="vp-unified-service-actions">
<Button size="small" theme="solid" type="primary" disabled={!hasResolvedVIN} onClick={() => onOpenRealtime(resolvedVIN, activeProtocol)}></Button> <Button size="small" theme="solid" type="primary" disabled={!hasResolvedVIN} onClick={() => onOpenRealtime(resolvedVIN, activeProtocol)}></Button>
<Button size="small" theme="light" disabled={!hasResolvedVIN} onClick={() => onOpenHistory(resolvedVIN, activeProtocol)}></Button> <Button size="small" theme="light" disabled={!hasResolvedVIN} onClick={() => onOpenHistory(resolvedVIN, activeProtocol)}></Button>
<Button size="small" theme="light" disabled={!hasResolvedVIN} onClick={() => onOpenRaw(resolvedVIN, activeProtocol)}></Button> <Button size="small" theme="light" disabled={!hasResolvedVIN} onClick={() => onOpenRaw(resolvedVIN, activeProtocol)}></Button>
<Button size="small" theme="light" disabled={!hasResolvedVIN} onClick={() => onOpenMileage(resolvedVIN, activeProtocol)}></Button> <Button size="small" theme="light" disabled={!hasResolvedVIN} onClick={() => onOpenMileage(resolvedVIN, activeProtocol)}></Button>
</div> </div>
</div> </div>
@@ -1067,7 +1076,7 @@ export function VehicleDetail({
<Space wrap> <Space wrap>
<Tag color={hasResolvedVIN ? 'green' : 'orange'}>{hasResolvedVIN ? '已解析 VIN' : '待维护身份'}</Tag> <Tag color={hasResolvedVIN ? 'green' : 'orange'}>{hasResolvedVIN ? '已解析 VIN' : '待维护身份'}</Tag>
<Tag color={activeProtocol ? 'blue' : 'green'}>{scopeText}</Tag> <Tag color={activeProtocol ? 'blue' : 'green'}>{scopeText}</Tag>
<Tag color={serviceConclusion.color}>{serviceStatus?.title ?? serviceConclusion.status}</Tag> <Tag color={serviceConclusion.color}>{customerStatusTitle(serviceStatus?.title) || serviceConclusion.status}</Tag>
</Space> </Space>
<Typography.Title heading={4} style={{ margin: 0 }}> <Typography.Title heading={4} style={{ margin: 0 }}>
{[archivePlate, displayVIN].filter((item) => item && item !== '-').join(' / ') || displayLookupKey} {[archivePlate, displayVIN].filter((item) => item && item !== '-').join(' / ') || displayLookupKey}
@@ -1076,7 +1085,7 @@ export function VehicleDetail({
{[ {[
{ label: '手机号', value: archivePhone || '-' }, { label: '手机号', value: archivePhone || '-' },
{ label: 'OEM', value: archiveOEM || '-' }, { label: 'OEM', value: archiveOEM || '-' },
{ label: '主证据来源', value: overview?.primaryProtocol || summary?.primaryProtocol || '-' }, { label: '主数据通道', value: overview?.primaryProtocol || summary?.primaryProtocol || '-' },
{ label: '最后上报', value: lastSeen }, { label: '最后上报', value: lastSeen },
{ label: '档案完整度', value: archiveCompleteness }, { label: '档案完整度', value: archiveCompleteness },
{ label: '下一步', value: serviceActions[0]?.label ?? serviceConclusion.nextStep } { label: '下一步', value: serviceActions[0]?.label ?? serviceConclusion.nextStep }
@@ -1115,7 +1124,7 @@ export function VehicleDetail({
<div className="vp-customer-service-package"> <div className="vp-customer-service-package">
<div className="vp-customer-service-summary"> <div className="vp-customer-service-summary">
<Space wrap> <Space wrap>
<Tag color={serviceConclusion.color}>{serviceStatus?.title ?? serviceConclusion.status}</Tag> <Tag color={serviceConclusion.color}>{customerStatusTitle(serviceStatus?.title) || serviceConclusion.status}</Tag>
<Tag color={activeProtocol ? 'blue' : 'green'}>{scopeText}</Tag> <Tag color={activeProtocol ? 'blue' : 'green'}>{scopeText}</Tag>
<Tag color={hasResolvedVIN ? 'green' : 'orange'}>{hasResolvedVIN ? 'VIN 已解析' : '身份待绑定'}</Tag> <Tag color={hasResolvedVIN ? 'green' : 'orange'}>{hasResolvedVIN ? 'VIN 已解析' : '身份待绑定'}</Tag>
</Space> </Space>
@@ -1159,11 +1168,11 @@ export function VehicleDetail({
row row
data={[ data={[
{ key: '服务车辆', value: [overview.plate, overview.vin].filter(Boolean).join(' / ') || '-' }, { key: '服务车辆', value: [overview.plate, overview.vin].filter(Boolean).join(' / ') || '-' },
{ key: '在线证据', value: `${overview.onlineSourceCount}/${overview.sourceCount}` }, { key: '在线数据通道', value: `${overview.onlineSourceCount}/${overview.sourceCount}` },
{ key: '覆盖状态', value: <Tag color={overview.coverageStatus === 'online' ? 'green' : overview.coverageStatus === 'partial' ? 'orange' : 'grey'}>{coverageStatusText[overview.coverageStatus] ?? overview.coverageStatus}</Tag> }, { key: '覆盖状态', value: <Tag color={overview.coverageStatus === 'online' ? 'green' : overview.coverageStatus === 'partial' ? 'orange' : 'grey'}>{coverageStatusText[overview.coverageStatus] ?? overview.coverageStatus}</Tag> },
{ key: '主证据来源', value: overview.primaryProtocol || '-' }, { key: '主数据通道', value: overview.primaryProtocol || '-' },
{ key: '最后上报', value: overview.lastSeen || '-' }, { key: '最后上报', value: overview.lastSeen || '-' },
{ key: '服务数据', value: `轨迹 ${overview.historyCount} / 原始记录 ${overview.rawCount} / 里程 ${overview.mileageCount}` }, { key: '服务数据', value: `轨迹 ${overview.historyCount} / 历史数据 ${overview.rawCount} / 里程 ${overview.mileageCount}` },
{ key: '实时来源', value: overview.realtimeCount }, { key: '实时来源', value: overview.realtimeCount },
{ key: '质量问题', value: qualityIssueAction(overview.qualityIssueCount) } { key: '质量问题', value: qualityIssueAction(overview.qualityIssueCount) }
]} ]}
@@ -1282,15 +1291,15 @@ export function VehicleDetail({
</Card> </Card>
{overview || consistency ? ( {overview || consistency ? (
<Card bordered title="车辆服务据链" style={{ marginTop: 16 }}> <Card bordered title="车辆服务据链" style={{ marginTop: 16 }}>
<div className="vp-evidence-grid"> <div className="vp-evidence-grid">
{[ {[
{ label: '证据来源', value: evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount} 在线` : '-' }, { label: '数据通道', value: evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount} 在线` : '-' },
{ label: '定位据', value: evidenceLocatedSourceCount > 0 ? `${evidenceLocatedSourceCount}证据有位置` : '暂无位置' }, { label: '定位据', value: evidenceLocatedSourceCount > 0 ? `${evidenceLocatedSourceCount}通道有位置` : '暂无位置' },
{ label: '里程差异', value: formatCompactNumber(evidenceMileageDelta, ' km') }, { label: '里程差异', value: formatCompactNumber(evidenceMileageDelta, ' km') },
{ label: '时间差异', value: formatSeconds(evidenceTimeDeltaSeconds) }, { label: '时间差异', value: formatSeconds(evidenceTimeDeltaSeconds) },
{ label: '主证据来源', value: overview?.primaryProtocol || summary?.primaryProtocol || '-' }, { label: '主数据通道', value: overview?.primaryProtocol || summary?.primaryProtocol || '-' },
{ label: '服务结论', value: consistencyTitle } { label: '服务结论', value: customerStatusTitle(consistencyTitle) || consistencyTitle }
].map((item) => ( ].map((item) => (
<div key={item.label} className="vp-evidence-item"> <div key={item.label} className="vp-evidence-item">
<div className="vp-evidence-label">{item.label}</div> <div className="vp-evidence-label">{item.label}</div>
@@ -1305,7 +1314,7 @@ export function VehicleDetail({
{hasResolvedVIN && sourceFusionRows.length > 0 ? ( {hasResolvedVIN && sourceFusionRows.length > 0 ? (
<Card <Card
bordered bordered
title={<Space><span></span><Button size="small" aria-label="复制归并矩阵" icon={<IconCopy />} onClick={copySourceFusionReport}></Button></Space>} title={<Space><span></span><Button size="small" aria-label="复制归并矩阵" icon={<IconCopy />} onClick={copySourceFusionReport}></Button></Space>}
style={{ marginTop: 16 }} style={{ marginTop: 16 }}
> >
<div className="vp-source-fusion-grid"> <div className="vp-source-fusion-grid">
@@ -1327,7 +1336,7 @@ export function VehicleDetail({
{[ {[
{ label: '实时', value: row.realtimeText }, { label: '实时', value: row.realtimeText },
{ label: '定位', value: row.locationText }, { label: '定位', value: row.locationText },
{ label: '原始记录', value: row.rawText }, { label: '历史数据', value: row.rawText },
{ label: '里程', value: row.mileageText } { label: '里程', value: row.mileageText }
].map((item) => ( ].map((item) => (
<div key={item.label} className="vp-source-fusion-fact"> <div key={item.label} className="vp-source-fusion-fact">
@@ -1349,7 +1358,7 @@ export function VehicleDetail({
</Button> </Button>
<Button size="small" theme="light" disabled={!row.source.hasRealtime} onClick={() => onOpenRealtime(resolvedVIN, row.source.protocol)}></Button> <Button size="small" theme="light" disabled={!row.source.hasRealtime} onClick={() => onOpenRealtime(resolvedVIN, row.source.protocol)}></Button>
<Button size="small" theme="light" disabled={!row.source.hasHistory} onClick={() => onOpenHistory(resolvedVIN, row.source.protocol)}></Button> <Button size="small" theme="light" disabled={!row.source.hasHistory} onClick={() => onOpenHistory(resolvedVIN, row.source.protocol)}></Button>
<Button size="small" theme="light" disabled={!row.source.hasRaw} onClick={() => onOpenRaw(resolvedVIN, row.source.protocol)}></Button> <Button size="small" theme="light" disabled={!row.source.hasRaw} onClick={() => onOpenRaw(resolvedVIN, row.source.protocol)}></Button>
<Button size="small" theme="light" disabled={!row.source.hasMileage} onClick={() => onOpenMileage(resolvedVIN, row.source.protocol)}></Button> <Button size="small" theme="light" disabled={!row.source.hasMileage} onClick={() => onOpenMileage(resolvedVIN, row.source.protocol)}></Button>
</Space> </Space>
</div> </div>
@@ -1424,7 +1433,7 @@ export function VehicleDetail({
{qualityIssueVehicleLabel(priorityQualityIssue, resolvedVIN)} {qualityIssueVehicleLabel(priorityQualityIssue, resolvedVIN)}
</Typography.Text> </Typography.Text>
<Typography.Text type="secondary" style={{ display: 'block', marginTop: 6 }}> <Typography.Text type="secondary" style={{ display: 'block', marginTop: 6 }}>
{priorityQualityIssue.detail || '该车存在告警事件,需要结合轨迹和原始记录确认影响范围。'} {priorityQualityIssue.detail || '该车存在告警事件,需要结合轨迹和历史数据确认影响范围。'}
</Typography.Text> </Typography.Text>
</div> </div>
<Space wrap> <Space wrap>
@@ -1445,13 +1454,13 @@ export function VehicleDetail({
</Button> </Button>
<Button <Button
aria-label="单车告警原始记录" aria-label="单车告警历史数据"
size="small" size="small"
theme="light" theme="light"
type="tertiary" type="tertiary"
onClick={() => openQualityIssueRawEvidence(priorityQualityIssue)} onClick={() => openQualityIssueRawEvidence(priorityQualityIssue)}
> >
</Button> </Button>
</Space> </Space>
</div> </div>
@@ -1484,14 +1493,14 @@ export function VehicleDetail({
{ key: '归并来源数', value: archiveSourceCount }, { key: '归并来源数', value: archiveSourceCount },
{ key: '在线', value: <StatusTag status={online ? 'ok' : 'offline'} /> }, { key: '在线', value: <StatusTag status={online ? 'ok' : 'offline'} /> },
{ {
key: '证据来源', key: '数据通道',
value: protocols.length > 0 ? ( value: protocols.length > 0 ? (
<SourceStatusTags sourceStatus={detail?.sourceStatus} protocols={protocols} lastSeen={lastSeen === '-' ? '' : lastSeen} /> <SourceStatusTags sourceStatus={detail?.sourceStatus} protocols={protocols} lastSeen={lastSeen === '-' ? '' : lastSeen} />
) : '-' ) : '-'
}, },
{ key: '在线证据', value: summary ? `${summary.onlineSourceCount}/${summary.sourceCount}` : '-' }, { key: '在线数据通道', value: summary ? `${summary.onlineSourceCount}/${summary.sourceCount}` : '-' },
{ key: '最后位置时间', value: lastSeen }, { key: '最后位置时间', value: lastSeen },
{ key: '最新原始记录时间', value: latestRaw?.serverTime ?? '-' }, { key: '最新历史数据时间', value: latestRaw?.serverTime ?? '-' },
{ key: '质量问题', value: <Tag color={qualityCount > 0 ? 'orange' : 'green'}>{qualityCount > 0 ? `${qualityCount}` : '无'}</Tag> } { key: '质量问题', value: <Tag color={qualityCount > 0 ? 'orange' : 'green'}>{qualityCount > 0 ? `${qualityCount}` : '无'}</Tag> }
]} ]}
/> />
@@ -1509,14 +1518,14 @@ export function VehicleDetail({
{hasResolvedVIN ? ( {hasResolvedVIN ? (
<> <>
<Card bordered title="证据来源覆盖" style={{ marginTop: 16 }}> <Card bordered title="数据通道覆盖" style={{ marginTop: 16 }}>
{detail?.sourceStatus?.length ? ( {detail?.sourceStatus?.length ? (
<> <>
<div className="vp-source-toolbar"> <div className="vp-source-toolbar">
<Space> <Space>
<Tag color={activeProtocol ? 'grey' : 'blue'}>{activeProtocol ? `当前 ${activeProtocol}` : '当前 全部证据来源'}</Tag> <Tag color={activeProtocol ? 'grey' : 'blue'}>{activeProtocol ? `当前 ${activeProtocol}` : '当前 全部数据通道'}</Tag>
<SourceStatusTags sourceStatus={detail.sourceStatus} protocols={protocols} lastSeen={lastSeen === '-' ? '' : lastSeen} /> <SourceStatusTags sourceStatus={detail.sourceStatus} protocols={protocols} lastSeen={lastSeen === '-' ? '' : lastSeen} />
<Button size="small" disabled={!activeProtocol} onClick={() => switchSource('')}></Button> <Button size="small" disabled={!activeProtocol} onClick={() => switchSource('')}></Button>
</Space> </Space>
</div> </div>
<Table <Table
@@ -1526,7 +1535,7 @@ export function VehicleDetail({
dataSource={detail.sourceStatus} dataSource={detail.sourceStatus}
columns={[ columns={[
{ {
title: '证据来源', title: '数据通道',
dataIndex: 'protocol', dataIndex: 'protocol',
width: 140, width: 140,
render: (_: unknown, row: VehicleSourceStatus) => ( render: (_: unknown, row: VehicleSourceStatus) => (
@@ -1598,7 +1607,7 @@ export function VehicleDetail({
disabled={!row.hasRaw} disabled={!row.hasRaw}
onClick={() => onOpenRaw(resolvedVIN, row.protocol)} onClick={() => onOpenRaw(resolvedVIN, row.protocol)}
> >
{row.protocol} {row.protocol}
</Button> </Button>
<Button <Button
size="small" size="small"
@@ -1616,7 +1625,7 @@ export function VehicleDetail({
/> />
</> </>
) : ( ) : (
<Typography.Text type="secondary"></Typography.Text> <Typography.Text type="secondary"></Typography.Text>
)} )}
</Card> </Card>
@@ -1624,13 +1633,13 @@ export function VehicleDetail({
<Descriptions <Descriptions
row row
data={[ data={[
{ key: '一致性结论', value: consistencyTitle }, { key: '一致性结论', value: customerStatusTitle(consistencyTitle) || consistencyTitle },
{ key: '证据覆盖', value: consistencySourceCount > 0 ? `${consistencySourceCount}证据` : '-' }, { key: '通道覆盖', value: consistencySourceCount > 0 ? `${consistencySourceCount}通道` : '-' },
{ key: '在线证据', value: consistencySourceCount > 0 ? `${consistencyOnlineSourceCount}/${consistencySourceCount} 在线` : '-' }, { key: '在线数据通道', value: consistencySourceCount > 0 ? `${consistencyOnlineSourceCount}/${consistencySourceCount} 在线` : '-' },
{ key: '缺失证据', value: missingProtocolText }, { key: '缺失通道', value: missingProtocolText },
{ key: '位置覆盖', value: consistencyLocatedSourceCount > 0 ? `${consistencyLocatedSourceCount}证据有位置` : '暂无位置' }, { key: '位置覆盖', value: consistencyLocatedSourceCount > 0 ? `${consistencyLocatedSourceCount}通道有位置` : '暂无位置' },
{ key: '里程差异', value: formatCompactNumber(consistencyMileageDelta, ' km') }, { key: '里程差异', value: formatCompactNumber(consistencyMileageDelta, ' km') },
{ key: '证据时间差', value: formatSeconds(consistencyTimeDeltaSeconds) }, { key: '通道时间差', value: formatSeconds(consistencyTimeDeltaSeconds) },
{ key: '诊断说明', value: consistencyDetail }, { key: '诊断说明', value: consistencyDetail },
{ key: '车辆服务视角', value: activeProtocol ? `当前只看 ${activeProtocol}` : '三类来源合并为同一车辆服务' } { key: '车辆服务视角', value: activeProtocol ? `当前只看 ${activeProtocol}` : '三类来源合并为同一车辆服务' }
]} ]}
@@ -1713,7 +1722,7 @@ export function VehicleDetail({
{ title: '入库时间', dataIndex: 'serverTime', width: 190 } { title: '入库时间', dataIndex: 'serverTime', width: 190 }
]} ]}
/> />
<Typography.Text type="tertiary"></Typography.Text> <Typography.Text type="tertiary"></Typography.Text>
<pre className="vp-json">{JSON.stringify(latestRaw?.parsedFields ?? {}, null, 2)}</pre> <pre className="vp-json">{JSON.stringify(latestRaw?.parsedFields ?? {}, null, 2)}</pre>
</Tabs.TabPane> </Tabs.TabPane>
<Tabs.TabPane tab="里程" itemKey="mileage"> <Tabs.TabPane tab="里程" itemKey="mileage">

View File

@@ -149,7 +149,7 @@ type VehicleActionQueueItem = {
function vehicleFilterSummary(filters: Record<string, string>) { function vehicleFilterSummary(filters: Record<string, string>) {
return [ return [
filters.keyword ? `关键词:${filters.keyword}` : '', filters.keyword ? `关键词:${filters.keyword}` : '',
filters.protocol ? `证据来源${filters.protocol}` : '', filters.protocol ? `数据通道${filters.protocol}` : '',
filters.coverage ? `车辆覆盖:${coverageLabel[filters.coverage] ?? filters.coverage}` : '', filters.coverage ? `车辆覆盖:${coverageLabel[filters.coverage] ?? filters.coverage}` : '',
filters.missingProtocol ? `缺失来源:${filters.missingProtocol}` : '', filters.missingProtocol ? `缺失来源:${filters.missingProtocol}` : '',
filters.serviceStatus ? `服务状态:${serviceStatusLabel[filters.serviceStatus] ?? filters.serviceStatus}` : '', filters.serviceStatus ? `服务状态:${serviceStatusLabel[filters.serviceStatus] ?? filters.serviceStatus}` : '',

View File

@@ -6583,12 +6583,19 @@ test('prevents unscoped raw parsed-field query from history form', async () => {
await waitFor(() => { await waitFor(() => {
expect(keywordInput).toHaveValue(''); expect(keywordInput).toHaveValue('');
}); });
fireEvent.change(screen.getByPlaceholderText('2026-07-03 00:00:00'), { target: { value: '' } });
fireEvent.change(screen.getByPlaceholderText('2026-07-03 23:59:59'), { target: { value: '' } });
fireEvent.click(screen.getByRole('checkbox', { name: '返回字段明细' })); fireEvent.click(screen.getByRole('checkbox', { name: '返回字段明细' }));
fireEvent.click(screen.getByRole('button', { name: 'search 查询' })); fireEvent.click(screen.getByRole('button', { name: 'search 查询' }));
expect((await screen.findAllByText('字段明细查询需要车辆、时间范围或字段裁剪')).length).toBeGreaterThanOrEqual(1); expect((await screen.findAllByText('字段明细查询需要车辆、时间范围或字段裁剪')).length).toBeGreaterThanOrEqual(1);
expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/history/locations?'), undefined); expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/history/locations?'), undefined);
expect(fetchMock).not.toHaveBeenCalledWith('/api/history/raw-frames/query', expect.anything()); const unscopedRawQueries = fetchMock.mock.calls.filter(([url, init]) => {
if (url !== '/api/history/raw-frames/query') return false;
const body = JSON.parse(String((init as RequestInit | undefined)?.body ?? '{}')) as Record<string, unknown>;
return !body.keyword && !body.dateFrom && !body.dateTo && !body.fields;
});
expect(unscopedRawQueries).toHaveLength(0);
}); });
test('shows trajectory playback workspace from history locations', async () => { test('shows trajectory playback workspace from history locations', async () => {
@@ -7545,7 +7552,8 @@ test('opens vehicle service from mileage row with row source evidence', async ()
render(<App />); render(<App />);
expect(await screen.findByText('VIN-MILEAGE-ROW')).toBeInTheDocument(); expect(await screen.findByText('VIN-MILEAGE-ROW')).toBeInTheDocument();
fireEvent.click(screen.getAllByRole('button', { name: '车辆服务' })[0]); const serviceButtons = screen.getAllByRole('button', { name: '车辆服务' });
fireEvent.click(serviceButtons[serviceButtons.length - 1]);
await waitFor(() => { await waitFor(() => {
expect(window.location.hash).toBe('#/detail?keyword=VIN-MILEAGE-ROW&protocol=JT808'); expect(window.location.hash).toBe('#/detail?keyword=VIN-MILEAGE-ROW&protocol=JT808');
@@ -8329,7 +8337,9 @@ test('opens vehicle service from history results with row source evidence', asyn
render(<App />); render(<App />);
expect(await screen.findByText('VIN-JT808-001')).toBeInTheDocument(); expect(await screen.findByText('VIN-JT808-001')).toBeInTheDocument();
fireEvent.click(screen.getAllByRole('button', { name: '车辆服务' })[0]); const locationRow = screen.getByText('VIN-JT808-001').closest('tr');
expect(locationRow).not.toBeNull();
fireEvent.click(within(locationRow!).getByRole('button', { name: '车辆服务' }));
await waitFor(() => { await waitFor(() => {
expect(window.location.hash).toBe('#/detail?keyword=VIN-JT808-001&protocol=JT808'); expect(window.location.hash).toBe('#/detail?keyword=VIN-JT808-001&protocol=JT808');
@@ -8609,7 +8619,9 @@ test('opens vehicle service from raw history rows with row source evidence', asy
fireEvent.click(await screen.findByRole('tab', { name: '历史明细' })); fireEvent.click(await screen.findByRole('tab', { name: '历史明细' }));
expect(await screen.findByText('raw-gb32960-001')).toBeInTheDocument(); expect(await screen.findByText('raw-gb32960-001')).toBeInTheDocument();
fireEvent.click(screen.getAllByRole('button', { name: '车辆服务' })[0]); const rawRow = screen.getByText('raw-gb32960-001').closest('tr');
expect(rawRow).not.toBeNull();
fireEvent.click(within(rawRow!).getByRole('button', { name: '车辆服务' }));
await waitFor(() => { await waitFor(() => {
expect(window.location.hash).toBe('#/detail?keyword=VIN-GB32960-001&protocol=GB32960'); expect(window.location.hash).toBe('#/detail?keyword=VIN-GB32960-001&protocol=GB32960');
@@ -11063,19 +11075,19 @@ test('switches vehicle detail to a source from the source matrix', async () => {
expect(screen.getAllByText('GB32960 在线').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('GB32960 在线').length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText('JT808 在线').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('JT808 在线').length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText('YUTONG_MQTT 未接入').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('YUTONG_MQTT 未接入').length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('多证据归并矩阵')).toBeInTheDocument(); expect(screen.getByText('数据通道归并矩阵')).toBeInTheDocument();
expect(screen.getByText('主实时证据')).toBeInTheDocument(); expect(screen.getByText('主实时通道')).toBeInTheDocument();
expect(screen.getAllByText('定位据').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('定位据').length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText('统计口径').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('统计口径').length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('无实时字段')).toBeInTheDocument(); expect(screen.getByText('无实时字段')).toBeInTheDocument();
expect(screen.getByText('无原始记录')).toBeInTheDocument(); expect(screen.getByText('无历史数据')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '复制归并矩阵' })); fireEvent.click(screen.getByRole('button', { name: '复制归并矩阵' }));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【单车多证据归并矩阵】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【单车数据通道归并矩阵】'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('1. GB32960 / 主证据')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('1. GB32960 / 主数据通道'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('贡献:主实时证据、在线心跳、实时字段、定位据、轨迹回放、原始记录、统计口径')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('贡献:主实时通道、在线心跳、实时字段、定位据、轨迹回放、历史数据、统计口径'));
expect(screen.getByText('车辆服务角色')).toBeInTheDocument(); expect(screen.getByText('车辆服务角色')).toBeInTheDocument();
expect(screen.getAllByText('主证据').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('主数据通道').length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText('在线证据').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('在线数据通道').length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText('暂无来源').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('暂无来源').length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('最新位置')).toBeInTheDocument(); expect(screen.getByText('最新位置')).toBeInTheDocument();
expect(screen.getAllByText('113.2, 23.1').length).toBeGreaterThan(0); expect(screen.getAllByText('113.2, 23.1').length).toBeGreaterThan(0);
@@ -11161,7 +11173,7 @@ test('shows unified service overview on vehicle detail', async () => {
expect(screen.getByText('今日单车服务任务板')).toBeInTheDocument(); expect(screen.getByText('今日单车服务任务板')).toBeInTheDocument();
expect(screen.getByText('看实时位置')).toBeInTheDocument(); expect(screen.getByText('看实时位置')).toBeInTheDocument();
expect(screen.getByText('回放轨迹证据')).toBeInTheDocument(); expect(screen.getByText('回放轨迹证据')).toBeInTheDocument();
expect(screen.getByText('导出原始证据')).toBeInTheDocument(); expect(screen.getAllByText('查询历史数据').length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('复核里程统计')).toBeInTheDocument(); expect(screen.getByText('复核里程统计')).toBeInTheDocument();
expect(screen.getByText('闭环告警事件')).toBeInTheDocument(); expect(screen.getByText('闭环告警事件')).toBeInTheDocument();
expect(screen.getByText('单车运营工作台')).toBeInTheDocument(); expect(screen.getByText('单车运营工作台')).toBeInTheDocument();
@@ -11169,17 +11181,17 @@ test('shows unified service overview on vehicle detail', async () => {
expect(screen.getAllByText('档案完整度').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('档案完整度').length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('实时')).toBeInTheDocument(); expect(screen.getByText('实时')).toBeInTheDocument();
expect(screen.getAllByText('轨迹').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('轨迹').length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText('原始记录').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('历史数据').length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText('里程').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('里程').length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText('告警').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('告警').length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('实时定位')).toBeInTheDocument(); expect(screen.getByText('实时定位')).toBeInTheDocument();
expect(screen.getAllByText('轨迹回放').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('轨迹回放').length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText('原始记录').length).toBeGreaterThan(0); expect(screen.getAllByText('历史数据').length).toBeGreaterThan(0);
expect(screen.getByText('告警处置')).toBeInTheDocument(); expect(screen.getByText('告警处置')).toBeInTheDocument();
expect(screen.getByText('里程复核')).toBeInTheDocument(); expect(screen.getByText('里程复核')).toBeInTheDocument();
expect(screen.getByText('确认实时状态')).toBeInTheDocument(); expect(screen.getByText('确认实时状态')).toBeInTheDocument();
expect(screen.getByText('回放轨迹断点')).toBeInTheDocument(); expect(screen.getByText('回放轨迹断点')).toBeInTheDocument();
expect(screen.getByText('核对原始记录')).toBeInTheDocument(); expect(screen.getAllByText('查询历史数据').length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('处理告警闭环')).toBeInTheDocument(); expect(screen.getByText('处理告警闭环')).toBeInTheDocument();
expect(screen.getByText('复核统计口径')).toBeInTheDocument(); expect(screen.getByText('复核统计口径')).toBeInTheDocument();
expect(screen.getByText('车辆在线状态、最新时间、有效坐标均可解释。')).toBeInTheDocument(); expect(screen.getByText('车辆在线状态、最新时间、有效坐标均可解释。')).toBeInTheDocument();
@@ -11189,13 +11201,13 @@ test('shows unified service overview on vehicle detail', async () => {
expect(screen.getAllByText('1 项').length).toBeGreaterThan(0); expect(screen.getAllByText('1 项').length).toBeGreaterThan(0);
expect(screen.getAllByText('7 条统计').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('7 条统计').length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('2/3')).toBeInTheDocument(); expect(screen.getByText('2/3')).toBeInTheDocument();
expect(screen.getByText('轨迹 12 / 原始记录 34 / 里程 7')).toBeInTheDocument(); expect(screen.getByText('轨迹 12 / 历史数据 34 / 里程 7')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '单车任务 看实时位置 实时监控' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '单车任务 看实时位置 实时监控' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '单车任务 回放轨迹证据 轨迹回放' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '单车任务 回放轨迹证据 轨迹回放' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '单车任务 导出原始证据 数据导出' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '单车任务 查询历史数据 历史数据' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '单车任务 复核里程统计 里程统计' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '单车任务 复核里程统计 里程统计' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '单车任务 闭环告警事件 告警事件' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '单车任务 闭环告警事件 告警事件' })).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '单车运营工作台 原始记录 查看原始记录' })); fireEvent.click(screen.getByRole('button', { name: '单车运营工作台 历史数据 查看历史数据' }));
expect(window.location.hash).toBe('#/history-query?keyword=VIN001&tab=raw'); expect(window.location.hash).toBe('#/history-query?keyword=VIN001&tab=raw');
}); });
@@ -11348,7 +11360,7 @@ test('quality health shows structured capacity findings', async () => {
expect(screen.getByText('kafka lag 42')).toBeInTheDocument(); expect(screen.getByText('kafka lag 42')).toBeInTheDocument();
}); });
test('shows vehicle service evidence chain before source details', async () => { test('shows vehicle service data chain before source details', async () => {
window.history.replaceState(null, '', '/#/detail?keyword=VIN001'); window.history.replaceState(null, '', '/#/detail?keyword=VIN001');
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
const path = String(input); const path = String(input);
@@ -11428,9 +11440,9 @@ test('shows vehicle service evidence chain before source details', async () => {
render(<App />); render(<App />);
expect(await screen.findByText('车辆服务据链')).toBeInTheDocument(); expect(await screen.findByText('车辆服务据链')).toBeInTheDocument();
expect(screen.getAllByText('2/3 在线').length).toBeGreaterThan(0); expect(screen.getAllByText('2/3 在线').length).toBeGreaterThan(0);
expect(screen.getAllByText('2 个证据有位置').length).toBeGreaterThan(0); expect(screen.getAllByText('2 个通道有位置').length).toBeGreaterThan(0);
expect(screen.getAllByText('1.4 km').length).toBeGreaterThan(0); expect(screen.getAllByText('1.4 km').length).toBeGreaterThan(0);
expect(screen.getAllByText('125 秒').length).toBeGreaterThan(0); expect(screen.getAllByText('125 秒').length).toBeGreaterThan(0);
expect(screen.getAllByText('MQTT 离线32960 与 808 仍可支撑车辆服务').length).toBeGreaterThan(0); expect(screen.getAllByText('MQTT 离线32960 与 808 仍可支撑车辆服务').length).toBeGreaterThan(0);
@@ -11682,7 +11694,7 @@ test('copies vehicle service diagnostic summary', async () => {
expect(await screen.findByText('车辆运营总览')).toBeInTheDocument(); expect(await screen.findByText('车辆运营总览')).toBeInTheDocument();
expect(screen.getByRole('button', { name: /复制归一摘要/ })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /复制归一摘要/ })).toBeInTheDocument();
expect(screen.getByText('车辆身份')).toBeInTheDocument(); expect(screen.getByText('车辆身份')).toBeInTheDocument();
expect(screen.getByText('实时主证据来源')).toBeInTheDocument(); expect(screen.getByText('实时主数据通道')).toBeInTheDocument();
expect(screen.getByText('业务可用性')).toBeInTheDocument(); expect(screen.getByText('业务可用性')).toBeInTheDocument();
expect(await screen.findByText('车辆服务结论')).toBeInTheDocument(); expect(await screen.findByText('车辆服务结论')).toBeInTheDocument();
expect(screen.getByText('车辆服务档案总览')).toBeInTheDocument(); expect(screen.getByText('车辆服务档案总览')).toBeInTheDocument();
@@ -11700,13 +11712,13 @@ test('copies vehicle service diagnostic summary', async () => {
const unifiedCopied = String(writeText.mock.lastCall?.[0] ?? ''); const unifiedCopied = String(writeText.mock.lastCall?.[0] ?? '');
expect(unifiedCopied).toContain('【统一车辆服务摘要】'); expect(unifiedCopied).toContain('【统一车辆服务摘要】');
expect(unifiedCopied).toContain('车辆粤A诊断1 / VIN-SUMMARY-001'); expect(unifiedCopied).toContain('车辆粤A诊断1 / VIN-SUMMARY-001');
expect(unifiedCopied).toContain('当前范围:单一来源JT808'); expect(unifiedCopied).toContain('当前范围:单一数据通道JT808');
expect(unifiedCopied).toContain('车辆身份VIN-SUMMARY-001'); expect(unifiedCopied).toContain('车辆身份VIN-SUMMARY-001');
expect(unifiedCopied).toContain('服务状态:来源不完整'); expect(unifiedCopied).toContain('服务状态:数据通道不完整');
expect(unifiedCopied).toContain('实时主证据来源JT808'); expect(unifiedCopied).toContain('实时主数据通道JT808');
expect(unifiedCopied).toContain('证据在线1/2'); expect(unifiedCopied).toContain('通道在线1/2');
expect(unifiedCopied).toContain('业务可用性:在线可用'); expect(unifiedCopied).toContain('业务可用性:在线可用');
expect(unifiedCopied).toContain('服务数据:实时 2 / 轨迹 12 / 原始记录 34 / 里程 3 / 告警 1'); expect(unifiedCopied).toContain('服务数据:实时 2 / 轨迹 12 / 历史数据 34 / 里程 3 / 告警 1');
expect(unifiedCopied).toContain('车辆服务http://localhost:3000/#/detail?keyword=VIN-SUMMARY-001&protocol=JT808'); expect(unifiedCopied).toContain('车辆服务http://localhost:3000/#/detail?keyword=VIN-SUMMARY-001&protocol=JT808');
fireEvent.click(screen.getByRole('button', { name: /复制档案/ })); fireEvent.click(screen.getByRole('button', { name: /复制档案/ }));
await waitFor(() => { await waitFor(() => {
@@ -11715,10 +11727,10 @@ test('copies vehicle service diagnostic summary', async () => {
const dossierCopied = String(writeText.mock.lastCall?.[0] ?? ''); const dossierCopied = String(writeText.mock.lastCall?.[0] ?? '');
expect(dossierCopied).toContain('【车辆服务档案】'); expect(dossierCopied).toContain('【车辆服务档案】');
expect(dossierCopied).toContain('车辆粤A诊断1 / VIN-SUMMARY-001'); expect(dossierCopied).toContain('车辆粤A诊断1 / VIN-SUMMARY-001');
expect(dossierCopied).toContain('当前范围:单一来源JT808'); expect(dossierCopied).toContain('当前范围:单一数据通道JT808');
expect(dossierCopied).toContain('主证据来源JT808'); expect(dossierCopied).toContain('主数据通道JT808');
expect(dossierCopied).toContain('证据在线1/2'); expect(dossierCopied).toContain('通道在线1/2');
expect(dossierCopied).toContain('资产概览:实时 2 / 轨迹 12 / 原始记录 34 / 里程 3 / 告警 1'); expect(dossierCopied).toContain('资产概览:实时 2 / 轨迹 12 / 历史数据 34 / 里程 3 / 告警 1');
expect(dossierCopied).toContain('车辆服务http://localhost:3000/#/detail?keyword=VIN-SUMMARY-001&protocol=JT808'); expect(dossierCopied).toContain('车辆服务http://localhost:3000/#/detail?keyword=VIN-SUMMARY-001&protocol=JT808');
fireEvent.click(screen.getByRole('button', { name: /复制交付包/ })); fireEvent.click(screen.getByRole('button', { name: /复制交付包/ }));
await waitFor(() => { await waitFor(() => {
@@ -11742,19 +11754,19 @@ test('copies vehicle service diagnostic summary', async () => {
expect(copied).toContain('【车辆服务诊断摘要】'); expect(copied).toContain('【车辆服务诊断摘要】');
expect(copied).toContain('运行版本platform-20260704155844'); expect(copied).toContain('运行版本platform-20260704155844');
expect(copied).toContain('车辆粤A诊断1 / VIN-SUMMARY-001'); expect(copied).toContain('车辆粤A诊断1 / VIN-SUMMARY-001');
expect(copied).toContain('当前范围:单一来源JT808'); expect(copied).toContain('当前范围:单一数据通道JT808');
expect(copied).toContain('服务状态:来源不完整'); expect(copied).toContain('服务状态:数据通道不完整');
expect(copied).toContain('证据来源1/2 在线'); expect(copied).toContain('数据通道1/2 在线');
expect(copied).toContain('定位2 个证据有位置'); expect(copied).toContain('定位2 个通道有位置');
expect(copied).toContain('里程差异12.6 km'); expect(copied).toContain('里程差异12.6 km');
expect(copied).toContain('时间差异90 秒'); expect(copied).toContain('时间差异90 秒');
expect(copied).toContain('服务数据:实时 2 / 轨迹 12 / 原始记录 34 / 里程 3 / 告警 1'); expect(copied).toContain('服务数据:实时 2 / 轨迹 12 / 历史数据 34 / 里程 3 / 告警 1');
expect(copied).toContain('一致性:来源不完整'); expect(copied).toContain('一致性:数据通道不完整');
expect(copied).toContain('下一步:'); expect(copied).toContain('下一步:');
expect(copied).toContain('车辆服务http://localhost:3000/#/detail?keyword=VIN-SUMMARY-001&protocol=JT808'); expect(copied).toContain('车辆服务http://localhost:3000/#/detail?keyword=VIN-SUMMARY-001&protocol=JT808');
expect(copied).toContain('实时http://localhost:3000/#/realtime?keyword=VIN-SUMMARY-001&protocol=JT808'); expect(copied).toContain('实时http://localhost:3000/#/realtime?keyword=VIN-SUMMARY-001&protocol=JT808');
expect(copied).toContain('轨迹http://localhost:3000/#/history?keyword=VIN-SUMMARY-001&protocol=JT808'); expect(copied).toContain('轨迹http://localhost:3000/#/history?keyword=VIN-SUMMARY-001&protocol=JT808');
expect(copied).toContain('原始记录http://localhost:3000/#/history-query?keyword=VIN-SUMMARY-001&protocol=JT808&tab=raw&includeFields=true'); expect(copied).toContain('历史数据http://localhost:3000/#/history-query?keyword=VIN-SUMMARY-001&protocol=JT808&tab=raw&includeFields=true');
expect(copied).toContain('里程http://localhost:3000/#/mileage?keyword=VIN-SUMMARY-001&protocol=JT808'); expect(copied).toContain('里程http://localhost:3000/#/mileage?keyword=VIN-SUMMARY-001&protocol=JT808');
fireEvent.click(screen.getByRole('button', { name: /复制运营摘要/ })); fireEvent.click(screen.getByRole('button', { name: /复制运营摘要/ }));
@@ -11764,10 +11776,10 @@ test('copies vehicle service diagnostic summary', async () => {
const operationsCopied = String(writeText.mock.lastCall?.[0] ?? ''); const operationsCopied = String(writeText.mock.lastCall?.[0] ?? '');
expect(operationsCopied).toContain('【车辆服务运营摘要】'); expect(operationsCopied).toContain('【车辆服务运营摘要】');
expect(operationsCopied).toContain('车辆粤A诊断1 / VIN-SUMMARY-001'); expect(operationsCopied).toContain('车辆粤A诊断1 / VIN-SUMMARY-001');
expect(operationsCopied).toContain('当前范围:单一来源JT808'); expect(operationsCopied).toContain('当前范围:单一数据通道JT808');
expect(operationsCopied).toContain('服务状态:来源不完整'); expect(operationsCopied).toContain('服务状态:数据通道不完整');
expect(operationsCopied).toContain('在线证据1/2'); expect(operationsCopied).toContain('在线数据通道1/2');
expect(operationsCopied).toContain('定位2'); expect(operationsCopied).toContain('定位2');
expect(operationsCopied).toContain('质量问题1 项'); expect(operationsCopied).toContain('质量问题1 项');
expect(operationsCopied).toContain('下一步清单:'); expect(operationsCopied).toContain('下一步清单:');
expect(operationsCopied).toContain('补齐 YUTONG_MQTT 证据'); expect(operationsCopied).toContain('补齐 YUTONG_MQTT 证据');
@@ -11776,7 +11788,7 @@ test('copies vehicle service diagnostic summary', async () => {
expect(operationsCopied).toContain('车辆服务http://localhost:3000/#/detail?keyword=VIN-SUMMARY-001&protocol=JT808'); expect(operationsCopied).toContain('车辆服务http://localhost:3000/#/detail?keyword=VIN-SUMMARY-001&protocol=JT808');
expect(operationsCopied).toContain('实时监控http://localhost:3000/#/realtime?keyword=VIN-SUMMARY-001&protocol=JT808'); expect(operationsCopied).toContain('实时监控http://localhost:3000/#/realtime?keyword=VIN-SUMMARY-001&protocol=JT808');
expect(operationsCopied).toContain('轨迹回放http://localhost:3000/#/history?keyword=VIN-SUMMARY-001&protocol=JT808'); expect(operationsCopied).toContain('轨迹回放http://localhost:3000/#/history?keyword=VIN-SUMMARY-001&protocol=JT808');
expect(operationsCopied).toContain('原始记录http://localhost:3000/#/history-query?keyword=VIN-SUMMARY-001&protocol=JT808&tab=raw&includeFields=true'); expect(operationsCopied).toContain('历史数据http://localhost:3000/#/history-query?keyword=VIN-SUMMARY-001&protocol=JT808&tab=raw&includeFields=true');
expect(operationsCopied).toContain('里程统计http://localhost:3000/#/mileage?keyword=VIN-SUMMARY-001&protocol=JT808'); expect(operationsCopied).toContain('里程统计http://localhost:3000/#/mileage?keyword=VIN-SUMMARY-001&protocol=JT808');
fireEvent.click(screen.getByRole('button', { name: '复制处理清单' })); fireEvent.click(screen.getByRole('button', { name: '复制处理清单' }));
@@ -11786,10 +11798,10 @@ test('copies vehicle service diagnostic summary', async () => {
const runbookCopied = String(writeText.mock.lastCall?.[0] ?? ''); const runbookCopied = String(writeText.mock.lastCall?.[0] ?? '');
expect(runbookCopied).toContain('【单车服务处理清单】'); expect(runbookCopied).toContain('【单车服务处理清单】');
expect(runbookCopied).toContain('车辆粤A诊断1 / VIN-SUMMARY-001'); expect(runbookCopied).toContain('车辆粤A诊断1 / VIN-SUMMARY-001');
expect(runbookCopied).toContain('当前范围:单一来源JT808'); expect(runbookCopied).toContain('当前范围:单一数据通道JT808');
expect(runbookCopied).toContain('1. 确认实时状态'); expect(runbookCopied).toContain('1. 确认实时状态');
expect(runbookCopied).toContain('2. 回放轨迹断点'); expect(runbookCopied).toContain('2. 回放轨迹断点');
expect(runbookCopied).toContain('3. 核对原始记录'); expect(runbookCopied).toContain('3. 查询历史数据');
expect(runbookCopied).toContain('4. 处理告警闭环'); expect(runbookCopied).toContain('4. 处理告警闭环');
expect(runbookCopied).toContain('5. 复核统计口径'); expect(runbookCopied).toContain('5. 复核统计口径');
expect(runbookCopied).toContain('验收:车辆在线状态、最新时间、有效坐标均可解释。'); expect(runbookCopied).toContain('验收:车辆在线状态、最新时间、有效坐标均可解释。');
@@ -12037,7 +12049,7 @@ test('opens raw history with the currently selected vehicle detail source', asyn
render(<App />); render(<App />);
fireEvent.click(await screen.findByRole('button', { name: '仅看 JT808' })); fireEvent.click(await screen.findByRole('button', { name: '仅看 JT808' }));
fireEvent.click(screen.getAllByRole('button', { name: '数据导出' })[0]); fireEvent.click(screen.getAllByRole('button', { name: '历史数据' })[0]);
await waitFor(() => { await waitFor(() => {
expect(window.location.hash).toBe('#/history-query?keyword=VIN001&protocol=JT808&tab=raw'); expect(window.location.hash).toBe('#/history-query?keyword=VIN001&protocol=JT808&tab=raw');
@@ -12195,7 +12207,7 @@ test('opens source-specific history directly from vehicle detail source card', a
}); });
}); });
test('opens source-specific raw history directly from vehicle detail source evidence', async () => { test('opens source-specific raw history directly from vehicle detail source data channel', async () => {
window.history.replaceState(null, '', '/#/detail?keyword=VIN001'); window.history.replaceState(null, '', '/#/detail?keyword=VIN001');
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
const path = String(input); const path = String(input);
@@ -12263,7 +12275,7 @@ test('opens source-specific raw history directly from vehicle detail source evid
render(<App />); render(<App />);
fireEvent.click(await screen.findByRole('button', { name: '查看 JT808 原始记录' })); fireEvent.click(await screen.findByRole('button', { name: '查看 JT808 历史数据' }));
await waitFor(() => { await waitFor(() => {
expect(window.location.hash).toBe('#/history-query?keyword=VIN001&protocol=JT808&tab=raw'); expect(window.location.hash).toBe('#/history-query?keyword=VIN001&protocol=JT808&tab=raw');
@@ -12394,9 +12406,9 @@ test('shows selected source scope on vehicle detail', async () => {
render(<App />); render(<App />);
expect(await screen.findByText('当前查看范围')).toBeInTheDocument(); expect(await screen.findByText('当前查看范围')).toBeInTheDocument();
expect(screen.getAllByText('单一来源JT808').length).toBeGreaterThan(0); expect(screen.getAllByText('单一数据通道JT808').length).toBeGreaterThan(0);
expect(screen.getByText('车辆服务状态')).toBeInTheDocument(); expect(screen.getByText('车辆服务状态')).toBeInTheDocument();
expect(screen.getAllByText('来源不完整').length).toBeGreaterThan(0); expect(screen.getAllByText('数据通道不完整').length).toBeGreaterThan(0);
expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/vehicle-service?keyword=VIN001&protocol=JT808'), undefined); expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/vehicle-service?keyword=VIN001&protocol=JT808'), undefined);
}); });
@@ -12490,12 +12502,12 @@ test('shows cross-source consistency for one vehicle service', async () => {
render(<App />); render(<App />);
expect(await screen.findByText('跨证据一致性')).toBeInTheDocument(); expect(await screen.findByText('跨证据一致性')).toBeInTheDocument();
expect(screen.getAllByText('来源不完整').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('数据通道不完整').length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText('2/3 个来源在线,车辆服务可用但缺少 YUTONG_MQTT 来源。').length).toBeGreaterThan(0); expect(screen.getAllByText('2/3 个来源在线,车辆服务可用但缺少 YUTONG_MQTT 来源。').length).toBeGreaterThan(0);
expect(screen.getByText('3 个证据')).toBeInTheDocument(); expect(screen.getByText('3 个通道')).toBeInTheDocument();
expect(screen.getAllByText('2/3 在线').length).toBeGreaterThan(0); expect(screen.getAllByText('2/3 在线').length).toBeGreaterThan(0);
expect(screen.getByText('缺失证据')).toBeInTheDocument(); expect(screen.getByText('缺失通道')).toBeInTheDocument();
expect(screen.getAllByText('3 个证据有位置').length).toBeGreaterThan(0); expect(screen.getAllByText('3 个通道有位置').length).toBeGreaterThan(0);
expect(screen.getAllByText('0.4 km').length).toBeGreaterThan(0); expect(screen.getAllByText('0.4 km').length).toBeGreaterThan(0);
expect(screen.getAllByText('35 秒').length).toBeGreaterThan(0); expect(screen.getAllByText('35 秒').length).toBeGreaterThan(0);
fireEvent.click(screen.getByRole('button', { name: '查看缺 YUTONG_MQTT 车辆' })); fireEvent.click(screen.getByRole('button', { name: '查看缺 YUTONG_MQTT 车辆' }));