diff --git a/vehicle-data-platform/apps/web/src/pages/VehicleDetail.tsx b/vehicle-data-platform/apps/web/src/pages/VehicleDetail.tsx index 6676c569..3e2bf9a4 100644 --- a/vehicle-data-platform/apps/web/src/pages/VehicleDetail.tsx +++ b/vehicle-data-platform/apps/web/src/pages/VehicleDetail.tsx @@ -843,6 +843,81 @@ export function VehicleDetail({ onSecondary: () => copyVehicleOperationsSummary() } ]; + const customerServicePackageItems = [ + { + title: '实时监控', + value: online ? '在线' : hasResolvedVIN ? '离线' : '待绑定', + detail: lastSeen === '-' ? '暂无最后上报时间' : `最后上报 ${lastSeen}`, + color: online ? 'green' as const : 'orange' as const, + action: '打开监控', + disabled: !hasResolvedVIN, + onClick: () => onOpenRealtime(resolvedVIN, activeProtocol) + }, + { + title: '轨迹回放', + value: `${formatCompactNumber(overview?.historyCount ?? detail?.history?.total ?? 0)} 条`, + detail: evidenceLocatedSourceCount > 0 ? `${evidenceLocatedSourceCount} 个来源可定位` : '暂无可用定位证据', + color: evidenceLocatedSourceCount > 0 ? 'blue' as const : 'grey' as const, + action: '回放轨迹', + disabled: !hasResolvedVIN, + onClick: () => onOpenHistory(resolvedVIN, activeProtocol) + }, + { + title: '历史数据查询', + value: `${formatCompactNumber(overview?.rawCount ?? detail?.raw?.total ?? 0)} 帧`, + detail: latestRaw?.frameType ? `最新 ${latestRaw.frameType}` : 'RAW 与解析字段证据', + color: (overview?.rawCount ?? detail?.raw?.total ?? 0) > 0 ? 'blue' as const : 'grey' as const, + action: '查询历史', + disabled: !hasResolvedVIN, + onClick: () => onOpenRaw(resolvedVIN, activeProtocol) + }, + { + title: '告警通知', + value: `${qualityCount.toLocaleString()} 项`, + detail: priorityQualityIssue ? `${qualityIssueLabel(priorityQualityIssue.issueType)} / ${priorityQualityIssue.lastSeen || '-'}` : '暂无待通知告警', + color: qualityCount > 0 ? 'orange' as const : 'green' as const, + action: '告警闭环', + disabled: !onOpenQuality || (!hasResolvedVIN && qualityCount <= 0), + onClick: () => onOpenQuality?.(qualityFiltersForCurrentVehicle()) + }, + { + title: '统计查询', + value: `${formatCompactNumber(overview?.mileageCount ?? detail?.mileage?.total ?? 0)} 条`, + detail: `里程差 ${formatCompactNumber(evidenceMileageDelta, ' km')}`, + color: isFiniteNumber(evidenceMileageDelta) && Math.abs(evidenceMileageDelta) > 1 ? 'orange' as const : 'green' as const, + action: '统计复核', + disabled: !hasResolvedVIN, + onClick: () => onOpenMileage(resolvedVIN, activeProtocol) + } + ]; + const copyCustomerServicePackage = () => { + if (!detail) { + Toast.warning('当前没有可复制的客户服务交付包'); + return; + } + const vehicleName = [archivePlate, displayVIN].filter((item) => item && item !== '-').join(' / ') || displayLookupKey; + const lines = [ + '【单车客户服务交付包】', + `车辆:${vehicleName}`, + `当前范围:${scopeText}`, + `平台能力:实时监控 / 轨迹回放 / 历史数据查询 / 告警通知 / 统计查询`, + `服务状态:${serviceStatus?.title ?? serviceConclusion.status}`, + `服务说明:${serviceStatus?.detail ?? serviceConclusion.risk}`, + `在线来源:${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')}`, + `告警事件:${qualityCount.toLocaleString()} 项`, + `车辆服务:${vehicleServiceURL(vehicleServiceHash('detail'))}`, + `实时监控:${vehicleServiceURL(vehicleServiceHash('realtime'))}`, + `轨迹回放:${vehicleServiceURL(vehicleServiceHash('history'))}`, + `历史数据查询:${vehicleServiceURL(vehicleServiceHash('history-query', { tab: 'raw', includeFields: 'true' }))}`, + `告警通知:${vehicleServiceURL(buildAppHash({ page: 'alert-events', keyword: resolvedVIN, protocol: activeProtocol }))}`, + `统计查询:${vehicleServiceURL(vehicleServiceHash('mileage'))}` + ]; + copyText(lines.join('\n'), '客户服务交付包'); + }; const qualityTable = ( + 单车客户服务交付包} + style={{ marginTop: 16 }} + > +
+
+ + {serviceStatus?.title ?? serviceConclusion.status} + {scopeText} + {hasResolvedVIN ? 'VIN 已解析' : '身份待绑定'} + + + {[archivePlate, displayVIN].filter((item) => item && item !== '-').join(' / ') || displayLookupKey} + + + 面向客户交付时,以车辆为主对象提供实时监控、轨迹回放、历史数据查询、告警通知和统计查询,协议来源只作为可追溯证据。 + +
+ + + + + +
+
+
+ {customerServicePackageItems.map((item) => ( + + ))} +
+
+
+ {overview ? ( { expect(screen.getByText('业务可用性')).toBeInTheDocument(); expect(await screen.findByText('车辆服务结论')).toBeInTheDocument(); expect(screen.getByText('车辆服务档案总览')).toBeInTheDocument(); + expect(screen.getByText('单车客户服务交付包')).toBeInTheDocument(); + expect(screen.getByText('面向客户交付时,以车辆为主对象提供实时监控、轨迹回放、历史数据查询、告警通知和统计查询,协议来源只作为可追溯证据。')).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: /复制归一摘要/ })); await waitFor(() => { expect(writeText.mock.calls.some((call) => String(call[0]).includes('【统一车辆服务摘要】'))).toBe(true); @@ -11531,6 +11538,19 @@ test('copies vehicle service diagnostic summary', async () => { 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: /复制交付包/ })); + await waitFor(() => { + expect(writeText.mock.calls.some((call) => String(call[0]).includes('【单车客户服务交付包】'))).toBe(true); + }); + const customerPackageCopied = String(writeText.mock.lastCall?.[0] ?? ''); + expect(customerPackageCopied).toContain('【单车客户服务交付包】'); + expect(customerPackageCopied).toContain('车辆:粤A诊断1 / VIN-SUMMARY-001'); + expect(customerPackageCopied).toContain('平台能力:实时监控 / 轨迹回放 / 历史数据查询 / 告警通知 / 统计查询'); + expect(customerPackageCopied).toContain('实时监控:http://localhost:3000/#/realtime?keyword=VIN-SUMMARY-001&protocol=JT808'); + expect(customerPackageCopied).toContain('轨迹回放:http://localhost:3000/#/history?keyword=VIN-SUMMARY-001&protocol=JT808'); + expect(customerPackageCopied).toContain('历史数据查询:http://localhost:3000/#/history-query?keyword=VIN-SUMMARY-001&protocol=JT808&tab=raw&includeFields=true'); + expect(customerPackageCopied).toContain('告警通知:http://localhost:3000/#/alert-events?keyword=VIN-SUMMARY-001&protocol=JT808'); + expect(customerPackageCopied).toContain('统计查询:http://localhost:3000/#/mileage?keyword=VIN-SUMMARY-001&protocol=JT808'); fireEvent.click(screen.getByRole('button', { name: /复制诊断摘要/ })); await waitFor(() => {