From 5dea611dd939a4c5b7405057b758c08f75d3a9e1 Mon Sep 17 00:00:00 2001 From: lingniu Date: Sat, 4 Jul 2026 22:28:08 +0800 Subject: [PATCH] feat(platform): add unified vehicle service view --- .../apps/web/src/pages/VehicleDetail.tsx | 88 +++++++++++++++++++ .../apps/web/src/styles/global.css | 58 ++++++++++++ .../apps/web/src/test/App.test.tsx | 20 +++++ 3 files changed, 166 insertions(+) diff --git a/vehicle-data-platform/apps/web/src/pages/VehicleDetail.tsx b/vehicle-data-platform/apps/web/src/pages/VehicleDetail.tsx index 92d1f488..8a7b9150 100644 --- a/vehicle-data-platform/apps/web/src/pages/VehicleDetail.tsx +++ b/vehicle-data-platform/apps/web/src/pages/VehicleDetail.tsx @@ -554,6 +554,58 @@ export function VehicleDetail({ color: qualityCount > 0 ? 'orange' as const : 'green' as const } ]; + const unifiedVehicleServiceItems = [ + { + label: '车辆身份', + value: hasResolvedVIN ? displayVIN : displayLookupKey, + detail: archivePlate || archivePhone || archiveOEM || '待补身份', + color: hasResolvedVIN ? 'green' as const : 'orange' as const + }, + { + label: '服务状态', + value: serviceStatus?.title ?? serviceConclusion.status, + detail: serviceStatus?.detail ?? serviceConclusion.risk, + color: serviceConclusion.color + }, + { + label: '实时主来源', + value: overview?.primaryProtocol || summary?.primaryProtocol || '-', + detail: evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount} 来源在线` : '暂无来源', + color: evidenceOnlineSourceCount > 0 ? 'green' as const : 'orange' as const + }, + { + label: '业务可用性', + value: online ? '在线可用' : hasResolvedVIN ? '离线待查' : '待绑定', + detail: lastSeen === '-' ? '无最后上报' : `最后上报 ${lastSeen}`, + color: online ? 'green' as const : 'orange' as const + } + ]; + const copyUnifiedVehicleService = () => { + if (!detail) { + Toast.warning('当前没有可复制的统一车辆服务摘要'); + return; + } + const vehicleName = [archivePlate, displayVIN].filter((item) => item && item !== '-').join(' / ') || displayLookupKey; + const lines = [ + '【统一车辆服务摘要】', + `车辆:${vehicleName}`, + `当前范围:${scopeText}`, + `车辆身份:${hasResolvedVIN ? displayVIN : '待绑定'}`, + `服务状态:${serviceStatus?.title ?? serviceConclusion.status}`, + `服务说明:${serviceStatus?.detail ?? serviceConclusion.risk}`, + `实时主来源:${overview?.primaryProtocol || summary?.primaryProtocol || '-'}`, + `来源在线:${evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount}` : '-'}`, + `业务可用性:${online ? '在线可用' : hasResolvedVIN ? '离线待查' : '待绑定'}`, + `数据资产:实时 ${overview?.realtimeCount ?? detail.realtime.length} / 轨迹 ${overview?.historyCount ?? detail.history.total ?? 0} / RAW ${overview?.rawCount ?? detail.raw.total ?? 0} / 里程 ${overview?.mileageCount ?? detail.mileage.total ?? 0} / 告警 ${overview?.qualityIssueCount ?? qualityCount}`, + `下一步:${serviceActions[0]?.label ?? serviceConclusion.nextStep}`, + `车辆服务:${vehicleServiceURL(vehicleServiceHash('detail'))}`, + `实时监控:${vehicleServiceURL(vehicleServiceHash('realtime'))}`, + `轨迹回放:${vehicleServiceURL(vehicleServiceHash('history'))}`, + `历史RAW:${vehicleServiceURL(vehicleServiceHash('history-query', { tab: 'raw', includeFields: 'true' }))}`, + `统计查询:${vehicleServiceURL(vehicleServiceHash('mileage'))}` + ]; + copyText(lines.join('\n'), '统一车辆服务摘要'); + }; const copyVehicleServiceDossier = () => { if (!detail) { Toast.warning('当前没有可复制的车辆服务档案'); @@ -836,6 +888,42 @@ export function VehicleDetail({ ) : null} + 统一车辆服务视图} + style={{ marginTop: 16 }} + > +
+
+ + {scopeText} + {serviceStatus?.title ?? serviceConclusion.status} + + + {[archivePlate, displayVIN].filter((item) => item && item !== '-').join(' / ') || displayLookupKey} + + + {serviceStatus?.detail ?? serviceConclusion.risk} + +
+ + + + +
+
+
+ {unifiedVehicleServiceItems.map((item) => ( +
+ {item.label} + {item.value} + {item.detail} +
+ ))} +
+
+
+ 车辆服务档案总览} diff --git a/vehicle-data-platform/apps/web/src/styles/global.css b/vehicle-data-platform/apps/web/src/styles/global.css index a976e2fc..91b12648 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -1330,6 +1330,62 @@ button.vp-realtime-command-item:focus-visible { gap: 16px; } +.vp-unified-service-board { + display: grid; + grid-template-columns: minmax(320px, 0.82fr) minmax(0, 1.18fr); + gap: 16px; +} + +.vp-unified-service-main { + min-height: 178px; + padding: 14px; + border: 1px solid rgba(22, 100, 255, 0.28); + border-radius: var(--vp-radius); + background: #f5f9ff; + display: grid; + gap: 12px; + align-content: start; +} + +.vp-unified-service-main .semi-typography { + line-height: 21px; +} + +.vp-unified-service-actions { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.vp-unified-service-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; +} + +.vp-unified-service-item { + min-height: 178px; + padding: 14px; + border: 1px solid var(--vp-border); + border-radius: var(--vp-radius); + background: #fbfcff; + display: grid; + gap: 10px; + align-content: start; +} + +.vp-unified-service-item strong { + color: var(--vp-text); + font-size: 21px; + line-height: 27px; + font-weight: 700; + word-break: break-word; +} + +.vp-unified-service-item .semi-typography { + line-height: 20px; +} + .vp-service-dossier-main { min-height: 210px; padding: 14px; @@ -1947,6 +2003,8 @@ button.vp-realtime-command-item:focus-visible { @media (max-width: 900px) { .vp-evidence-grid, .vp-action-grid, + .vp-unified-service-board, + .vp-unified-service-grid, .vp-service-dossier, .vp-service-dossier-assets, .vp-map-ops-board, 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 ce080cce..460e3489 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -10905,8 +10905,28 @@ test('copies vehicle service diagnostic summary', async () => { render(); + expect(await screen.findByText('统一车辆服务视图')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /复制归一摘要/ })).toBeInTheDocument(); + expect(screen.getByText('车辆身份')).toBeInTheDocument(); + expect(screen.getByText('实时主来源')).toBeInTheDocument(); + expect(screen.getByText('业务可用性')).toBeInTheDocument(); expect(await screen.findByText('车辆服务结论')).toBeInTheDocument(); expect(screen.getByText('车辆服务档案总览')).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: /复制归一摘要/ })); + await waitFor(() => { + expect(writeText.mock.calls.some((call) => String(call[0]).includes('【统一车辆服务摘要】'))).toBe(true); + }); + const unifiedCopied = String(writeText.mock.lastCall?.[0] ?? ''); + expect(unifiedCopied).toContain('【统一车辆服务摘要】'); + expect(unifiedCopied).toContain('车辆:粤A诊断1 / VIN-SUMMARY-001'); + 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('业务可用性:在线可用'); + expect(unifiedCopied).toContain('数据资产:实时 2 / 轨迹 12 / RAW 34 / 里程 3 / 告警 1'); + expect(unifiedCopied).toContain('车辆服务:http://localhost:3000/#/detail?keyword=VIN-SUMMARY-001&protocol=JT808'); fireEvent.click(screen.getByRole('button', { name: /复制档案/ })); await waitFor(() => { expect(writeText.mock.calls.some((call) => String(call[0]).includes('【车辆服务档案】'))).toBe(true);