feat(platform): refine vehicle detail evidence wording
This commit is contained in:
@@ -58,7 +58,7 @@ function formatSeconds(value?: number) {
|
||||
}
|
||||
|
||||
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.lastSeen && !source.hasRealtime && !source.hasHistory && !source.hasRaw && !source.hasMileage) {
|
||||
return { label: '暂无来源', color: 'grey' as const };
|
||||
@@ -70,7 +70,7 @@ function sourceFusionRow(source: VehicleSourceStatus, realtime: RealtimeLocation
|
||||
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.protocol === primaryProtocol ? '主实时证据' : '',
|
||||
source.online ? '在线心跳' : '',
|
||||
source.hasRealtime || realtime ? '实时字段' : '',
|
||||
hasLocation ? '定位证据' : '',
|
||||
@@ -284,9 +284,10 @@ export function VehicleDetail({
|
||||
(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 sourceCount = Math.max(detail?.sourceStatus?.length ?? 0, realtimeSources.size, summary?.sourceCount ?? 0, protocols.length);
|
||||
const onlineSourceCount = detail?.sourceStatus?.length
|
||||
? detail.sourceStatus.filter((source) => source.online).length
|
||||
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);
|
||||
@@ -410,8 +411,8 @@ export function VehicleDetail({
|
||||
`服务判断:${serviceConclusion.status}`,
|
||||
`主要风险:${serviceConclusion.risk}`,
|
||||
`下一步:${serviceConclusion.nextStep}`,
|
||||
`来源证据:${evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount} 来源在线` : '-'}`,
|
||||
`定位证据:${evidenceLocatedSourceCount > 0 ? `${evidenceLocatedSourceCount} 个来源有位置` : '暂无位置'}`,
|
||||
`证据来源:${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}`,
|
||||
@@ -427,16 +428,16 @@ export function VehicleDetail({
|
||||
};
|
||||
const copySourceFusionReport = () => {
|
||||
if (!detail || sourceFusionRows.length === 0) {
|
||||
Toast.warning('当前没有可复制的多源归并矩阵');
|
||||
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} 在线` : '-'}`,
|
||||
`主证据来源:${overview?.primaryProtocol || summary?.primaryProtocol || '-'}`,
|
||||
`证据覆盖:${evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount} 在线` : '-'}`,
|
||||
`一致性:${consistencyTitle}`,
|
||||
'',
|
||||
...sourceFusionRows.map((row, index) => [
|
||||
@@ -455,7 +456,7 @@ export function VehicleDetail({
|
||||
`原始记录:${vehicleServiceURL(vehicleServiceHash('history-query', { tab: 'raw', includeFields: 'true' }))}`,
|
||||
`里程统计:${vehicleServiceURL(vehicleServiceHash('mileage'))}`
|
||||
];
|
||||
copyText(lines.join('\n'), '多源归并矩阵');
|
||||
copyText(lines.join('\n'), '多证据归并矩阵');
|
||||
};
|
||||
const qualityIssueAction = (count: number) => {
|
||||
if (count <= 0) {
|
||||
@@ -478,7 +479,7 @@ export function VehicleDetail({
|
||||
actions.push({
|
||||
key: 'identity',
|
||||
label: '维护身份绑定',
|
||||
description: `关键词 ${displayLookupKey} 暂未解析到 VIN,先补齐绑定后再看跨来源服务。`,
|
||||
description: `关键词 ${displayLookupKey} 暂未解析到 VIN,先补齐绑定后再看跨证据服务。`,
|
||||
color: 'orange',
|
||||
onClick: () => onOpenVehicles?.({ bindingStatus: 'unbound' })
|
||||
});
|
||||
@@ -486,8 +487,8 @@ export function VehicleDetail({
|
||||
for (const item of missingProtocols) {
|
||||
actions.push({
|
||||
key: `missing-${item}`,
|
||||
label: `补齐 ${item} 来源`,
|
||||
description: `${item} 当前没有形成有效来源证据,会影响这辆车的统一服务可信度。`,
|
||||
label: `补齐 ${item} 证据`,
|
||||
description: `${item} 当前没有形成有效证据来源,会影响这辆车的统一服务可信度。`,
|
||||
color: 'orange',
|
||||
onClick: () => onOpenVehicles?.({ serviceStatus: 'degraded', missingProtocol: item })
|
||||
});
|
||||
@@ -526,13 +527,13 @@ export function VehicleDetail({
|
||||
{
|
||||
label: '实时',
|
||||
value: formatCompactNumber(overview?.realtimeCount ?? detail?.realtime?.length ?? 0),
|
||||
detail: evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount} 来源在线` : '暂无来源',
|
||||
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} 个位置来源` : '暂无位置',
|
||||
detail: evidenceLocatedSourceCount > 0 ? `${evidenceLocatedSourceCount} 个位置证据` : '暂无位置',
|
||||
color: (overview?.historyCount ?? detail?.history?.total ?? 0) > 0 ? 'blue' as const : 'grey' as const
|
||||
},
|
||||
{
|
||||
@@ -568,9 +569,9 @@ export function VehicleDetail({
|
||||
color: serviceConclusion.color
|
||||
},
|
||||
{
|
||||
label: '实时主来源',
|
||||
label: '实时主证据来源',
|
||||
value: overview?.primaryProtocol || summary?.primaryProtocol || '-',
|
||||
detail: evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount} 来源在线` : '暂无来源',
|
||||
detail: evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount} 在线` : '暂无来源',
|
||||
color: evidenceOnlineSourceCount > 0 ? 'green' as const : 'orange' as const
|
||||
},
|
||||
{
|
||||
@@ -593,8 +594,8 @@ export function VehicleDetail({
|
||||
`车辆身份:${hasResolvedVIN ? displayVIN : '待绑定'}`,
|
||||
`服务状态:${serviceStatus?.title ?? serviceConclusion.status}`,
|
||||
`服务说明:${serviceStatus?.detail ?? serviceConclusion.risk}`,
|
||||
`实时主来源:${overview?.primaryProtocol || summary?.primaryProtocol || '-'}`,
|
||||
`来源在线:${evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount}` : '-'}`,
|
||||
`实时主证据来源:${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}`,
|
||||
@@ -618,8 +619,8 @@ export function VehicleDetail({
|
||||
`查询关键词:${displayLookupKey}`,
|
||||
`当前范围:${scopeText}`,
|
||||
`档案完整度:${archiveCompleteness}${archiveMissingLabels.length > 0 ? `(${archiveMissingLabels.join('、')})` : ''}`,
|
||||
`主来源:${overview?.primaryProtocol || summary?.primaryProtocol || '-'}`,
|
||||
`来源在线:${evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount}` : '-'}`,
|
||||
`主证据来源:${overview?.primaryProtocol || summary?.primaryProtocol || '-'}`,
|
||||
`证据在线:${evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount}` : '-'}`,
|
||||
`服务状态:${serviceStatus?.title ?? serviceConclusion.status}`,
|
||||
`服务说明:${serviceStatus?.detail ?? serviceConclusion.risk}`,
|
||||
`资产概览:${serviceAssetItems.map((item) => `${item.label} ${item.value}`).join(' / ')}`,
|
||||
@@ -647,8 +648,8 @@ export function VehicleDetail({
|
||||
`服务状态:${serviceStatus?.title ?? serviceConclusion.status}`,
|
||||
`业务影响:${serviceConclusion.risk}`,
|
||||
`建议动作:${serviceConclusion.nextStep}`,
|
||||
`在线来源:${evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount}` : '-'}`,
|
||||
`定位来源:${evidenceLocatedSourceCount > 0 ? `${evidenceLocatedSourceCount}` : '0'}`,
|
||||
`在线证据:${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 ? '' : '暂无明确处置项,持续观察实时状态。'}`,
|
||||
@@ -665,9 +666,9 @@ export function VehicleDetail({
|
||||
{
|
||||
title: '实时定位',
|
||||
value: lastSeen,
|
||||
meta: evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount} 来源在线` : '暂无来源',
|
||||
meta: evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount} 在线` : '暂无来源',
|
||||
color: online ? 'green' as const : 'orange' as const,
|
||||
detail: '查看该车最新位置、速度、SOC、总里程和多来源实时字段。',
|
||||
detail: '查看该车最新位置、速度、SOC、总里程和多证据实时字段。',
|
||||
disabled: !hasResolvedVIN,
|
||||
actions: [
|
||||
{ label: '查看实时', onClick: () => onOpenRealtime(resolvedVIN, activeProtocol) }
|
||||
@@ -676,7 +677,7 @@ export function VehicleDetail({
|
||||
{
|
||||
title: '轨迹回放',
|
||||
value: `${formatCompactNumber(overview?.historyCount ?? detail?.history?.total ?? 0)} 条历史`,
|
||||
meta: evidenceLocatedSourceCount > 0 ? `${evidenceLocatedSourceCount} 个位置来源` : '暂无位置',
|
||||
meta: evidenceLocatedSourceCount > 0 ? `${evidenceLocatedSourceCount} 个位置证据` : '暂无位置',
|
||||
color: evidenceLocatedSourceCount > 0 ? 'blue' as const : 'grey' as const,
|
||||
detail: '回放历史轨迹,复核定位断点、速度变化和里程连续性。',
|
||||
disabled: !hasResolvedVIN,
|
||||
@@ -711,7 +712,7 @@ export function VehicleDetail({
|
||||
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: '核对统计口径,确认区间总值、每日里程与来源证据是否闭合。',
|
||||
detail: '核对统计口径,确认区间总值、每日里程与证据来源是否闭合。',
|
||||
disabled: !hasResolvedVIN,
|
||||
actions: [
|
||||
{ label: '查看里程', onClick: () => onOpenMileage(resolvedVIN, activeProtocol) }
|
||||
@@ -721,7 +722,7 @@ export function VehicleDetail({
|
||||
const serviceRunbookSteps = [
|
||||
{
|
||||
title: '确认实时状态',
|
||||
evidence: evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount} 来源在线,最后上报 ${lastSeen}` : '暂无来源在线证据',
|
||||
evidence: evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount} 在线,最后上报 ${lastSeen}` : '暂无在线证据',
|
||||
acceptance: '车辆在线状态、最新时间、有效坐标均可解释。',
|
||||
action: '打开实时',
|
||||
disabled: !hasResolvedVIN,
|
||||
@@ -729,7 +730,7 @@ export function VehicleDetail({
|
||||
},
|
||||
{
|
||||
title: '回放轨迹断点',
|
||||
evidence: evidenceLocatedSourceCount > 0 ? `${evidenceLocatedSourceCount} 个来源有位置,时间差 ${formatSeconds(evidenceTimeDeltaSeconds)}` : '暂无可回放位置',
|
||||
evidence: evidenceLocatedSourceCount > 0 ? `${evidenceLocatedSourceCount} 个证据有位置,时间差 ${formatSeconds(evidenceTimeDeltaSeconds)}` : '暂无可回放位置',
|
||||
acceptance: '轨迹点、速度和总里程变化连续,异常点可定位到时间段。',
|
||||
action: '轨迹回放',
|
||||
disabled: !hasResolvedVIN,
|
||||
@@ -753,7 +754,7 @@ export function VehicleDetail({
|
||||
},
|
||||
{
|
||||
title: '复核统计口径',
|
||||
evidence: `${formatCompactNumber(overview?.mileageCount ?? detail?.mileage?.total ?? 0)} 条统计,来源里程差 ${formatCompactNumber(evidenceMileageDelta, ' km')}`,
|
||||
evidence: `${formatCompactNumber(overview?.mileageCount ?? detail?.mileage?.total ?? 0)} 条统计,证据里程差 ${formatCompactNumber(evidenceMileageDelta, ' km')}`,
|
||||
acceptance: '区间里程、每日里程和轨迹总里程证据可以互相解释。',
|
||||
action: '里程统计',
|
||||
disabled: !hasResolvedVIN,
|
||||
@@ -790,7 +791,7 @@ export function VehicleDetail({
|
||||
{
|
||||
title: '看实时位置',
|
||||
value: lastSeen,
|
||||
detail: evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount} 来源在线,先确认车辆当前是否仍在上报。` : '暂无来源在线证据,优先确认数据接入。',
|
||||
detail: evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount} 在线,先确认车辆当前是否仍在上报。` : '暂无在线证据,优先确认数据接入。',
|
||||
color: online ? 'green' as const : 'orange' as const,
|
||||
primaryAction: '实时监控',
|
||||
secondaryAction: '复制摘要',
|
||||
@@ -801,7 +802,7 @@ export function VehicleDetail({
|
||||
{
|
||||
title: '回放轨迹证据',
|
||||
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,
|
||||
primaryAction: '轨迹回放',
|
||||
secondaryAction: '处理清单',
|
||||
@@ -823,7 +824,7 @@ export function VehicleDetail({
|
||||
{
|
||||
title: '复核里程统计',
|
||||
value: `${formatCompactNumber(overview?.mileageCount ?? detail?.mileage?.total ?? 0)} 条统计`,
|
||||
detail: `来源里程差 ${formatCompactNumber(evidenceMileageDelta, ' km')},确认区间和日统计能闭合。`,
|
||||
detail: `证据里程差 ${formatCompactNumber(evidenceMileageDelta, ' km')},确认区间和日统计能闭合。`,
|
||||
color: isFiniteNumber(evidenceMileageDelta) && Math.abs(evidenceMileageDelta) > 1 ? 'orange' as const : 'green' as const,
|
||||
primaryAction: '里程统计',
|
||||
secondaryAction: '诊断摘要',
|
||||
@@ -856,7 +857,7 @@ export function VehicleDetail({
|
||||
{
|
||||
title: '轨迹回放',
|
||||
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,
|
||||
action: '回放轨迹',
|
||||
disabled: !hasResolvedVIN,
|
||||
@@ -903,11 +904,11 @@ export function VehicleDetail({
|
||||
`平台能力:实时监控 / 轨迹回放 / 历史数据查询 / 告警通知 / 统计查询`,
|
||||
`服务状态:${serviceStatus?.title ?? serviceConclusion.status}`,
|
||||
`服务说明:${serviceStatus?.detail ?? serviceConclusion.risk}`,
|
||||
`在线来源:${evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount}` : '-'}`,
|
||||
`在线证据:${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')}`,
|
||||
`里程统计:${overview?.mileageCount ?? detail.mileage.total ?? 0} 条,证据差异 ${formatCompactNumber(evidenceMileageDelta, ' km')}`,
|
||||
`告警事件:${qualityCount.toLocaleString()} 项`,
|
||||
`车辆服务:${vehicleServiceURL(vehicleServiceHash('detail'))}`,
|
||||
`实时监控:${vehicleServiceURL(vehicleServiceHash('realtime'))}`,
|
||||
@@ -949,8 +950,8 @@ export function VehicleDetail({
|
||||
{ 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: evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount} 在线` : '-' },
|
||||
{ section: '证据链', item: '定位证据', value: evidenceLocatedSourceCount > 0 ? `${evidenceLocatedSourceCount} 个证据有位置` : '暂无位置' },
|
||||
{ section: '证据链', item: '里程差异', value: formatCompactNumber(evidenceMileageDelta, ' km') },
|
||||
{ section: '证据链', item: '时间差异', value: formatSeconds(evidenceTimeDeltaSeconds) },
|
||||
{ section: '证据链', item: '一致性结论', value: consistencyTitle },
|
||||
@@ -962,9 +963,9 @@ export function VehicleDetail({
|
||||
];
|
||||
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('|') || '-' }
|
||||
{ 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));
|
||||
@@ -982,7 +983,7 @@ export function VehicleDetail({
|
||||
load(nextQuery);
|
||||
}}>
|
||||
<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="JT808">JT808</Select.Option>
|
||||
<Select.Option value="YUTONG_MQTT">YUTONG_MQTT</Select.Option>
|
||||
@@ -1006,7 +1007,7 @@ export function VehicleDetail({
|
||||
<span className="vp-scope-label">当前查看范围</span>
|
||||
<Tag color={activeProtocol ? 'blue' : 'green'}>{scopeText}</Tag>
|
||||
<Typography.Text type="tertiary">
|
||||
{activeProtocol ? '下方实时、轨迹、原始记录和里程仅展示该数据通道返回的数据。' : '下方数据按车辆聚合展示全部可用数据通道。'}
|
||||
{activeProtocol ? '下方实时、轨迹、原始记录和里程仅展示该证据来源返回的数据。' : '下方数据按车辆聚合展示全部可用证据来源。'}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
@@ -1075,7 +1076,7 @@ export function VehicleDetail({
|
||||
{[
|
||||
{ label: '手机号', value: archivePhone || '-' },
|
||||
{ label: 'OEM', value: archiveOEM || '-' },
|
||||
{ label: '主来源', value: overview?.primaryProtocol || summary?.primaryProtocol || '-' },
|
||||
{ label: '主证据来源', value: overview?.primaryProtocol || summary?.primaryProtocol || '-' },
|
||||
{ label: '最后上报', value: lastSeen },
|
||||
{ label: '档案完整度', value: archiveCompleteness },
|
||||
{ label: '下一步', value: serviceActions[0]?.label ?? serviceConclusion.nextStep }
|
||||
@@ -1158,9 +1159,9 @@ export function VehicleDetail({
|
||||
row
|
||||
data={[
|
||||
{ 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: overview.primaryProtocol || '-' },
|
||||
{ key: '主证据来源', value: overview.primaryProtocol || '-' },
|
||||
{ key: '最后上报', value: overview.lastSeen || '-' },
|
||||
{ key: '服务数据', value: `轨迹 ${overview.historyCount} / 原始记录 ${overview.rawCount} / 里程 ${overview.mileageCount}` },
|
||||
{ key: '实时来源', value: overview.realtimeCount },
|
||||
@@ -1284,11 +1285,11 @@ export function VehicleDetail({
|
||||
<Card bordered title="车辆服务证据链" style={{ marginTop: 16 }}>
|
||||
<div className="vp-evidence-grid">
|
||||
{[
|
||||
{ label: '来源证据', value: evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount} 来源在线` : '-' },
|
||||
{ label: '定位证据', value: evidenceLocatedSourceCount > 0 ? `${evidenceLocatedSourceCount} 个来源有位置` : '暂无位置' },
|
||||
{ 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: overview?.primaryProtocol || summary?.primaryProtocol || '-' },
|
||||
{ label: '服务结论', value: consistencyTitle }
|
||||
].map((item) => (
|
||||
<div key={item.label} className="vp-evidence-item">
|
||||
@@ -1304,7 +1305,7 @@ export function VehicleDetail({
|
||||
{hasResolvedVIN && sourceFusionRows.length > 0 ? (
|
||||
<Card
|
||||
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 }}
|
||||
>
|
||||
<div className="vp-source-fusion-grid">
|
||||
@@ -1483,12 +1484,12 @@ export function VehicleDetail({
|
||||
{ key: '归并来源数', value: archiveSourceCount },
|
||||
{ key: '在线', value: <StatusTag status={online ? 'ok' : 'offline'} /> },
|
||||
{
|
||||
key: '数据来源',
|
||||
key: '证据来源',
|
||||
value: protocols.length > 0 ? (
|
||||
<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: latestRaw?.serverTime ?? '-' },
|
||||
{ key: '质量问题', value: <Tag color={qualityCount > 0 ? 'orange' : 'green'}>{qualityCount > 0 ? `${qualityCount} 项` : '无'}</Tag> }
|
||||
@@ -1500,7 +1501,7 @@ export function VehicleDetail({
|
||||
type="warning"
|
||||
bordered
|
||||
title="身份绑定待处理"
|
||||
description={`车辆关键词 ${displayLookupKey} 暂未解析到 VIN,请维护车辆身份绑定后再查看历史、里程和跨来源数据。`}
|
||||
description={`车辆关键词 ${displayLookupKey} 暂未解析到 VIN,请维护车辆身份绑定后再查看历史、里程和跨证据数据。`}
|
||||
style={{ marginTop: 12 }}
|
||||
/>
|
||||
) : null}
|
||||
@@ -1508,14 +1509,14 @@ export function VehicleDetail({
|
||||
|
||||
{hasResolvedVIN ? (
|
||||
<>
|
||||
<Card bordered title="数据来源覆盖" style={{ marginTop: 16 }}>
|
||||
<Card bordered title="证据来源覆盖" style={{ marginTop: 16 }}>
|
||||
{detail?.sourceStatus?.length ? (
|
||||
<>
|
||||
<div className="vp-source-toolbar">
|
||||
<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} />
|
||||
<Button size="small" disabled={!activeProtocol} onClick={() => switchSource('')}>全部来源</Button>
|
||||
<Button size="small" disabled={!activeProtocol} onClick={() => switchSource('')}>全部证据来源</Button>
|
||||
</Space>
|
||||
</div>
|
||||
<Table
|
||||
@@ -1525,7 +1526,7 @@ export function VehicleDetail({
|
||||
dataSource={detail.sourceStatus}
|
||||
columns={[
|
||||
{
|
||||
title: '来源',
|
||||
title: '证据来源',
|
||||
dataIndex: 'protocol',
|
||||
width: 140,
|
||||
render: (_: unknown, row: VehicleSourceStatus) => (
|
||||
@@ -1615,21 +1616,21 @@ export function VehicleDetail({
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Typography.Text type="secondary">暂未查询到该车辆的数据来源覆盖。</Typography.Text>
|
||||
<Typography.Text type="secondary">暂未查询到该车辆的证据来源覆盖。</Typography.Text>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card bordered title="跨来源一致性" style={{ marginTop: 16 }}>
|
||||
<Card bordered title="跨证据一致性" style={{ marginTop: 16 }}>
|
||||
<Descriptions
|
||||
row
|
||||
data={[
|
||||
{ key: '一致性结论', value: consistencyTitle },
|
||||
{ key: '来源覆盖', value: consistencySourceCount > 0 ? `${consistencySourceCount} 个来源` : '-' },
|
||||
{ key: '在线来源', value: consistencySourceCount > 0 ? `${consistencyOnlineSourceCount}/${consistencySourceCount} 在线` : '-' },
|
||||
{ key: '缺失来源', value: missingProtocolText },
|
||||
{ key: '位置覆盖', value: consistencyLocatedSourceCount > 0 ? `${consistencyLocatedSourceCount} 个来源有位置` : '暂无位置' },
|
||||
{ key: '证据覆盖', value: consistencySourceCount > 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: formatSeconds(consistencyTimeDeltaSeconds) },
|
||||
{ key: '诊断说明', value: consistencyDetail },
|
||||
{ key: '车辆服务视角', value: activeProtocol ? `当前只看 ${activeProtocol}` : '三类来源合并为同一车辆服务' }
|
||||
]}
|
||||
|
||||
@@ -11016,18 +11016,18 @@ test('switches vehicle detail to a source from the source matrix', async () => {
|
||||
expect(screen.getAllByText('GB32960 在线').length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getAllByText('JT808 在线').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.getByText('无实时字段')).toBeInTheDocument();
|
||||
expect(screen.getByText('无原始记录')).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: '复制归并矩阵' }));
|
||||
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【单车多源归并矩阵】'));
|
||||
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('1. GB32960 / 主来源'));
|
||||
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('贡献:主实时来源、在线心跳、实时字段、定位证据、轨迹回放、原始记录、统计口径'));
|
||||
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【单车多证据归并矩阵】'));
|
||||
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('1. GB32960 / 主证据'));
|
||||
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('贡献:主实时证据、在线心跳、实时字段、定位证据、轨迹回放、原始记录、统计口径'));
|
||||
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();
|
||||
@@ -11382,8 +11382,8 @@ test('shows vehicle service evidence chain before source details', async () => {
|
||||
render(<App />);
|
||||
|
||||
expect(await screen.findByText('车辆服务证据链')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('2/3 来源在线').length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText('2 个来源有位置').length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText('2/3 在线').length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText('2 个证据有位置').length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText('1.4 km').length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText('125 秒').length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText('MQTT 离线,32960 与 808 仍可支撑车辆服务').length).toBeGreaterThan(0);
|
||||
@@ -11635,7 +11635,7 @@ test('copies vehicle service diagnostic summary', async () => {
|
||||
expect(await screen.findByText('车辆运营总览')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /复制归一摘要/ })).toBeInTheDocument();
|
||||
expect(screen.getByText('车辆身份')).toBeInTheDocument();
|
||||
expect(screen.getByText('实时主来源')).toBeInTheDocument();
|
||||
expect(screen.getByText('实时主证据来源')).toBeInTheDocument();
|
||||
expect(screen.getByText('业务可用性')).toBeInTheDocument();
|
||||
expect(await screen.findByText('车辆服务结论')).toBeInTheDocument();
|
||||
expect(screen.getByText('车辆服务档案总览')).toBeInTheDocument();
|
||||
@@ -11656,8 +11656,8 @@ test('copies vehicle service diagnostic summary', async () => {
|
||||
expect(unifiedCopied).toContain('当前范围:单一来源:JT808');
|
||||
expect(unifiedCopied).toContain('车辆身份:VIN-SUMMARY-001');
|
||||
expect(unifiedCopied).toContain('服务状态:来源不完整');
|
||||
expect(unifiedCopied).toContain('实时主来源:JT808');
|
||||
expect(unifiedCopied).toContain('来源在线:1/2');
|
||||
expect(unifiedCopied).toContain('实时主证据来源:JT808');
|
||||
expect(unifiedCopied).toContain('证据在线:1/2');
|
||||
expect(unifiedCopied).toContain('业务可用性:在线可用');
|
||||
expect(unifiedCopied).toContain('服务数据:实时 2 / 轨迹 12 / 原始记录 34 / 里程 3 / 告警 1');
|
||||
expect(unifiedCopied).toContain('车辆服务:http://localhost:3000/#/detail?keyword=VIN-SUMMARY-001&protocol=JT808');
|
||||
@@ -11669,8 +11669,8 @@ test('copies vehicle service diagnostic summary', async () => {
|
||||
expect(dossierCopied).toContain('【车辆服务档案】');
|
||||
expect(dossierCopied).toContain('车辆:粤A诊断1 / VIN-SUMMARY-001');
|
||||
expect(dossierCopied).toContain('当前范围:单一来源:JT808');
|
||||
expect(dossierCopied).toContain('主来源:JT808');
|
||||
expect(dossierCopied).toContain('来源在线:1/2');
|
||||
expect(dossierCopied).toContain('主证据来源:JT808');
|
||||
expect(dossierCopied).toContain('证据在线:1/2');
|
||||
expect(dossierCopied).toContain('资产概览:实时 2 / 轨迹 12 / 原始记录 34 / 里程 3 / 告警 1');
|
||||
expect(dossierCopied).toContain('车辆服务:http://localhost:3000/#/detail?keyword=VIN-SUMMARY-001&protocol=JT808');
|
||||
fireEvent.click(screen.getByRole('button', { name: /复制交付包/ }));
|
||||
@@ -11697,8 +11697,8 @@ test('copies vehicle service diagnostic summary', async () => {
|
||||
expect(copied).toContain('车辆:粤A诊断1 / VIN-SUMMARY-001');
|
||||
expect(copied).toContain('当前范围:单一来源:JT808');
|
||||
expect(copied).toContain('服务状态:来源不完整');
|
||||
expect(copied).toContain('来源证据:1/2 来源在线');
|
||||
expect(copied).toContain('定位证据:2 个来源有位置');
|
||||
expect(copied).toContain('证据来源:1/2 在线');
|
||||
expect(copied).toContain('定位证据:2 个证据有位置');
|
||||
expect(copied).toContain('里程差异:12.6 km');
|
||||
expect(copied).toContain('时间差异:90 秒');
|
||||
expect(copied).toContain('服务数据:实时 2 / 轨迹 12 / 原始记录 34 / 里程 3 / 告警 1');
|
||||
@@ -11719,11 +11719,11 @@ test('copies vehicle service diagnostic summary', async () => {
|
||||
expect(operationsCopied).toContain('车辆:粤A诊断1 / VIN-SUMMARY-001');
|
||||
expect(operationsCopied).toContain('当前范围:单一来源:JT808');
|
||||
expect(operationsCopied).toContain('服务状态:来源不完整');
|
||||
expect(operationsCopied).toContain('在线来源:1/2');
|
||||
expect(operationsCopied).toContain('定位来源:2');
|
||||
expect(operationsCopied).toContain('在线证据:1/2');
|
||||
expect(operationsCopied).toContain('定位证据:2');
|
||||
expect(operationsCopied).toContain('质量问题:1 项');
|
||||
expect(operationsCopied).toContain('下一步清单:');
|
||||
expect(operationsCopied).toContain('补齐 YUTONG_MQTT 来源');
|
||||
expect(operationsCopied).toContain('补齐 YUTONG_MQTT 证据');
|
||||
expect(operationsCopied).toContain('排查 GB32960 离线');
|
||||
expect(operationsCopied).toContain('处理质量问题 1 项');
|
||||
expect(operationsCopied).toContain('车辆服务:http://localhost:3000/#/detail?keyword=VIN-SUMMARY-001&protocol=JT808');
|
||||
@@ -12442,13 +12442,13 @@ test('shows cross-source consistency for one vehicle service', async () => {
|
||||
|
||||
render(<App />);
|
||||
|
||||
expect(await screen.findByText('跨来源一致性')).toBeInTheDocument();
|
||||
expect(await screen.findByText('跨证据一致性')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('来源不完整').length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getAllByText('2/3 个来源在线,车辆服务可用但缺少 YUTONG_MQTT 来源。').length).toBeGreaterThan(0);
|
||||
expect(screen.getByText('3 个来源')).toBeInTheDocument();
|
||||
expect(screen.getByText('2/3 在线')).toBeInTheDocument();
|
||||
expect(screen.getByText('缺失来源')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('3 个来源有位置').length).toBeGreaterThan(0);
|
||||
expect(screen.getByText('3 个证据')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('2/3 在线').length).toBeGreaterThan(0);
|
||||
expect(screen.getByText('缺失证据')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('3 个证据有位置').length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText('0.4 km').length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText('35 秒').length).toBeGreaterThan(0);
|
||||
fireEvent.click(screen.getByRole('button', { name: '查看缺 YUTONG_MQTT 车辆' }));
|
||||
@@ -12553,11 +12553,11 @@ test('shows actionable vehicle service recommendations on detail', async () => {
|
||||
expect(screen.getByText('3 项质量问题影响可信度')).toBeInTheDocument();
|
||||
expect(screen.getByText('先处理质量问题,再将该车用于 BI、告警或对外查询。')).toBeInTheDocument();
|
||||
expect(await screen.findByText('服务处置建议')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: '补齐 YUTONG_MQTT 来源' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: '补齐 YUTONG_MQTT 证据' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: '处理质量问题 3 项' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: '排查 YUTONG_MQTT 离线' })).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '补齐 YUTONG_MQTT 来源' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '补齐 YUTONG_MQTT 证据' }));
|
||||
expect(window.location.hash).toBe('#/vehicles?serviceStatus=degraded&missingProtocol=YUTONG_MQTT');
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user