diff --git a/vehicle-data-platform/apps/web/src/pages/VehicleDetail.tsx b/vehicle-data-platform/apps/web/src/pages/VehicleDetail.tsx index dfd8add6..17acfb52 100644 --- a/vehicle-data-platform/apps/web/src/pages/VehicleDetail.tsx +++ b/vehicle-data-platform/apps/web/src/pages/VehicleDetail.tsx @@ -900,6 +900,53 @@ export function VehicleDetail({ onClick: () => onOpenMileage(resolvedVIN, activeProtocol) } ]; + const liveViewActions = [ + { + title: '实时位置', + value: formatLocation(latest), + detail: lastSeen === '-' ? '暂无最后上报时间' : `最后上报 ${lastSeen}`, + action: '查看实时', + color: online ? 'green' as const : 'orange' as const, + disabled: !hasResolvedVIN, + onClick: () => onOpenRealtime(resolvedVIN, activeProtocol) + }, + { + title: '轨迹历史', + value: `${formatCompactNumber(overview?.historyCount ?? detail?.history?.total ?? 0)} 条`, + detail: evidenceLocatedSourceCount > 0 ? `${evidenceLocatedSourceCount} 个定位证据` : '暂无有效定位', + action: '轨迹回放', + color: evidenceLocatedSourceCount > 0 ? 'blue' as const : 'grey' as const, + disabled: !hasResolvedVIN, + onClick: () => onOpenHistory(resolvedVIN, activeProtocol) + }, + { + title: '里程能源', + value: `${formatCompactNumber(latest?.totalMileageKm, ' km')}`, + detail: `SOC ${formatCompactNumber(latest?.socPercent, '%')} / 速度 ${formatCompactNumber(latest?.speedKmh, ' km/h')}`, + action: '里程统计', + color: isFiniteNumber(latest?.totalMileageKm) ? 'green' as const : 'orange' as const, + disabled: !hasResolvedVIN, + onClick: () => onOpenMileage(resolvedVIN, activeProtocol) + }, + { + title: '数据导出', + value: `${formatCompactNumber(overview?.rawCount ?? detail?.raw?.total ?? 0)} 帧`, + detail: latestRaw?.frameType ? `最新 ${latestRaw.frameType}` : '历史明细与字段', + action: '历史导出', + color: (overview?.rawCount ?? detail?.raw?.total ?? 0) > 0 ? 'blue' as const : 'grey' as const, + disabled: !hasResolvedVIN, + onClick: () => onOpenRaw(resolvedVIN, activeProtocol) + }, + { + title: '告警通知', + value: `${qualityCount.toLocaleString()} 项`, + detail: priorityQualityIssue ? `${qualityIssueLabel(priorityQualityIssue.issueType)} / ${priorityQualityIssue.lastSeen || '-'}` : '暂无待通知告警', + action: '告警闭环', + color: qualityCount > 0 ? 'orange' as const : 'green' as const, + disabled: !onOpenQuality || (!hasResolvedVIN && qualityCount <= 0), + onClick: () => onOpenQuality?.(qualityFiltersForCurrentVehicle()) + } + ]; const copyCustomerServicePackage = () => { if (!detail) { Toast.warning('当前没有可复制的客户服务交付包'); @@ -1030,6 +1077,67 @@ export function VehicleDetail({ ) : null} + +
+
+ + 单车 Live View + {online ? '在线' : hasResolvedVIN ? '离线' : '待绑定'} + {customerStatusTitle(serviceStatus?.title) || serviceConclusion.status} + + + {[archivePlate, displayVIN].filter((item) => item && item !== '-').join(' / ') || displayLookupKey} + + + 客户打开一辆车时,先看当前位置、最后上报、速度、SOC、总里程和告警,再进入轨迹回放、里程统计、数据导出和告警闭环。 + +
+ {[ + { label: '当前位置', value: formatLocation(latest) }, + { label: '最后上报', value: lastSeen }, + { label: '速度', value: formatCompactNumber(latest?.speedKmh, ' km/h') }, + { label: 'SOC', value: formatCompactNumber(latest?.socPercent, '%') }, + { label: '总里程', value: formatCompactNumber(latest?.totalMileageKm, ' km') } + ].map((item) => ( +
+ {item.label} + {item.value} +
+ ))} +
+ + + + + + +
+ +
+
+ {liveViewActions.map((item) => ( + + ))} +
+
+ 车辆运营总览} diff --git a/vehicle-data-platform/apps/web/src/styles/global.css b/vehicle-data-platform/apps/web/src/styles/global.css index 5080183c..03975cb1 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -4368,6 +4368,132 @@ button.vp-realtime-command-item:focus-visible { gap: 16px; } +.vp-vehicle-live-view { + margin-top: 16px; + overflow: hidden; + border-color: rgba(22, 100, 255, 0.18); +} + +.vp-vehicle-live-view .semi-card-body { + display: grid; + grid-template-columns: minmax(0, 1.2fr) minmax(340px, 0.8fr); + padding: 0; +} + +.vp-vehicle-live-main { + min-width: 0; + display: grid; + grid-template-columns: minmax(0, 0.95fr) minmax(320px, 0.75fr); + gap: 16px; + padding: 18px; + background: linear-gradient(90deg, rgba(22, 100, 255, 0.08), rgba(255, 255, 255, 0) 60%), #fff; +} + +.vp-vehicle-live-copy { + min-width: 0; + display: grid; + gap: 12px; + align-content: start; +} + +.vp-vehicle-live-copy .semi-typography { + font-size: 14px; + line-height: 22px; +} + +.vp-vehicle-live-facts { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 8px; +} + +.vp-vehicle-live-facts div { + min-height: 72px; + padding: 10px; + border: 1px solid var(--vp-border); + border-radius: var(--vp-radius); + background: #fbfcff; + display: grid; + gap: 4px; + align-content: start; +} + +.vp-vehicle-live-facts span { + color: var(--vp-text-muted); + font-size: 12px; + line-height: 18px; +} + +.vp-vehicle-live-facts strong { + color: var(--vp-text); + font-size: 14px; + line-height: 20px; + font-weight: 700; + word-break: break-word; +} + +.vp-vehicle-live-map { + min-height: 280px; + height: 280px; +} + +.vp-vehicle-live-actions { + padding: 18px; + border-left: 1px solid var(--vp-border); + background: #f8fbff; + display: grid; + gap: 10px; +} + +.vp-vehicle-live-action { + min-height: 92px; + padding: 12px; + border: 1px solid var(--vp-border); + border-radius: var(--vp-radius); + background: #fff; + text-align: left; + color: inherit; + font: inherit; + cursor: pointer; + display: grid; + gap: 7px; + align-content: start; +} + +.vp-vehicle-live-action:disabled { + cursor: not-allowed; + opacity: 0.58; +} + +.vp-vehicle-live-action:not(:disabled):hover, +.vp-vehicle-live-action:not(:disabled):focus-visible { + border-color: rgba(22, 100, 255, 0.42); + background: #f5f9ff; + outline: none; +} + +.vp-vehicle-live-action strong { + color: var(--vp-text); + font-size: 16px; + line-height: 22px; + font-weight: 700; + word-break: break-word; +} + +.vp-vehicle-live-action span { + color: var(--vp-text-muted); + font-size: 12px; + line-height: 18px; +} + +.vp-vehicle-live-action em { + color: var(--vp-primary); + font-size: 12px; + font-style: normal; + font-weight: 700; + line-height: 18px; +} + .vp-unified-service-main { min-height: 178px; padding: 14px; @@ -5268,6 +5394,9 @@ button.vp-realtime-command-item:focus-visible { @media (max-width: 900px) { .vp-evidence-grid, .vp-action-grid, + .vp-vehicle-live-view .semi-card-body, + .vp-vehicle-live-main, + .vp-vehicle-live-facts, .vp-unified-service-board, .vp-unified-service-grid, .vp-service-dossier, 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 bb25108e..85d1aaf1 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -11773,6 +11773,11 @@ test('copies vehicle service diagnostic summary', async () => { expect(screen.getByText('实时主数据通道')).toBeInTheDocument(); expect(screen.getByText('业务可用性')).toBeInTheDocument(); expect(await screen.findByText('车辆服务结论')).toBeInTheDocument(); + expect(screen.getByText('单车 Live View')).toBeInTheDocument(); + expect(screen.getByText('客户打开一辆车时,先看当前位置、最后上报、速度、SOC、总里程和告警,再进入轨迹回放、里程统计、数据导出和告警闭环。')).toBeInTheDocument(); + expect(screen.getByText('当前位置')).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();