diff --git a/vehicle-data-platform/apps/web/src/pages/VehicleDetail.tsx b/vehicle-data-platform/apps/web/src/pages/VehicleDetail.tsx index 93a066b6..047d2172 100644 --- a/vehicle-data-platform/apps/web/src/pages/VehicleDetail.tsx +++ b/vehicle-data-platform/apps/web/src/pages/VehicleDetail.tsx @@ -69,6 +69,13 @@ type VehicleServiceAction = { onClick: () => void; }; +type VehicleServiceConclusion = { + status: string; + color: 'green' | 'orange' | 'red'; + risk: string; + nextStep: string; +}; + async function copyVehicleServiceURL() { try { await navigator.clipboard.writeText(`${window.location.origin}${window.location.pathname}${window.location.hash}`); @@ -230,6 +237,62 @@ export function VehicleDetail({ onQueryChange?.(nextQuery.keyword, nextQuery.protocol); load(nextQuery); }; + const serviceConclusion = useMemo(() => { + if (!hasResolvedVIN) { + return { + status: '不可服务', + color: 'red', + risk: '身份未归并到 VIN', + nextStep: '先维护车辆身份绑定,再使用实时、历史和里程服务。' + }; + } + if (sourceCount <= 0) { + return { + status: '不可服务', + color: 'red', + risk: '没有任何来源证据', + nextStep: '优先确认平台转发、端口、订阅和绑定配置。' + }; + } + if (onlineSourceCount <= 0) { + return { + status: '不可服务', + color: 'red', + risk: '全部来源离线', + nextStep: '先恢复至少一个实时来源,再判断车辆状态。' + }; + } + if (qualityCount > 0) { + return { + status: '降级可服务', + color: 'orange', + risk: `${qualityCount} 项质量问题影响可信度`, + nextStep: '先处理质量问题,再将该车用于 BI、告警或对外查询。' + }; + } + if (missingProtocols.length > 0) { + return { + status: '降级可服务', + color: 'orange', + risk: `缺少 ${missingProtocols.join('、')} 来源`, + nextStep: '车辆仍可查询,但需要补齐缺失来源后再做跨来源校验。' + }; + } + if (isFiniteNumber(consistencyMileageDelta) && consistencyMileageDelta > 1) { + return { + status: '降级可服务', + color: 'orange', + risk: `跨来源里程差异 ${formatCompactNumber(consistencyMileageDelta, ' km')}`, + nextStep: '核对各来源总里程口径,确认统计使用的主来源。' + }; + } + return { + status: '可服务', + color: 'green', + risk: '身份、来源和核心实时证据可用', + nextStep: '可进入实时、历史、RAW 和里程查询继续分析。' + }; + }, [consistencyMileageDelta, hasResolvedVIN, missingProtocols, onlineSourceCount, qualityCount, sourceCount]); const qualityFiltersForCurrentVehicle = () => ({ keyword: resolvedVIN, ...(activeProtocol ? { protocol: activeProtocol } : {}) @@ -379,6 +442,21 @@ export function VehicleDetail({ ) : null} + +
+ {[ + { label: '服务判断', value: {serviceConclusion.status} }, + { label: '主要风险', value: serviceConclusion.risk }, + { label: '下一步', value: serviceConclusion.nextStep } + ].map((item) => ( +
+
{item.label}
+
{item.value}
+
+ ))} +
+
+ {overview || consistency ? (
diff --git a/vehicle-data-platform/apps/web/src/styles/global.css b/vehicle-data-platform/apps/web/src/styles/global.css index 62b10d19..6ef71409 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -295,6 +295,20 @@ body { margin-bottom: 12px; } +.vp-conclusion-grid { + display: grid; + grid-template-columns: 160px minmax(180px, 1fr) minmax(260px, 1.4fr); + gap: 12px; +} + +.vp-conclusion-item { + min-height: 78px; + padding: 12px; + border: 1px solid var(--vp-border); + border-radius: var(--vp-radius); + background: #fbfcff; +} + .vp-evidence-item { min-height: 70px; padding: 12px; @@ -377,3 +391,11 @@ body { .semi-navigation-item-text { font-size: 14px; } + +@media (max-width: 900px) { + .vp-evidence-grid, + .vp-action-grid, + .vp-conclusion-grid { + grid-template-columns: 1fr; + } +} diff --git a/vehicle-data-platform/apps/web/src/test/App.test.tsx b/vehicle-data-platform/apps/web/src/test/App.test.tsx index 82026fee..f1de18fb 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -5534,6 +5534,10 @@ test('shows actionable vehicle service recommendations on detail', async () => { render(); + expect(await screen.findByText('车辆服务结论')).toBeInTheDocument(); + expect(screen.getByText('降级可服务')).toBeInTheDocument(); + 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: '处理质量问题 3 项' })).toBeInTheDocument();