From a0158e1ec8f66da47011a57945b48e1c494a5abd Mon Sep 17 00:00:00 2001 From: lingniu Date: Mon, 6 Jul 2026 02:37:12 +0800 Subject: [PATCH] feat(platform): add trip review summary --- .../apps/web/src/pages/History.tsx | 135 ++++++++++++++++++ .../apps/web/src/styles/global.css | 89 ++++++++++++ .../apps/web/src/test/App.test.tsx | 19 ++- 3 files changed, 239 insertions(+), 4 deletions(-) diff --git a/vehicle-data-platform/apps/web/src/pages/History.tsx b/vehicle-data-platform/apps/web/src/pages/History.tsx index 7c5712da..145eeabc 100644 --- a/vehicle-data-platform/apps/web/src/pages/History.tsx +++ b/vehicle-data-platform/apps/web/src/pages/History.tsx @@ -1864,6 +1864,92 @@ export function History({ onClick: copyDeliveryPackage } ]; + const tripReviewSummaryItems = [ + { + label: '起点', + value: firstLocation?.deviceTime || firstLocation?.serverTime || '-', + detail: firstLocation ? `${formatNumber(firstLocation.longitude)}, ${formatNumber(firstLocation.latitude)}` : '暂无起点', + color: firstLocation ? 'green' as const : 'grey' as const, + disabled: !firstLocation, + onClick: () => openQueryTab('location') + }, + { + label: '终点', + value: lastLocation?.deviceTime || lastLocation?.serverTime || '-', + detail: lastLocation ? `${formatNumber(lastLocation.longitude)}, ${formatNumber(lastLocation.latitude)}` : '暂无终点', + color: lastLocation ? 'green' as const : 'grey' as const, + disabled: !lastLocation, + onClick: () => openQueryTab('location') + }, + { + label: '行程里程', + value: formatNumber(mileageDelta, ' km'), + detail: `回放跨度 ${formatDurationMinutes(playbackSpanMinutes)},用于解释区间里程。`, + color: isFiniteNumber(mileageDelta) ? 'blue' as const : 'grey' as const, + disabled: !isFiniteNumber(mileageDelta), + onClick: () => onOpenMileage?.({ keyword: currentVehicleKeyword, protocol: currentProtocol, ...(filters.dateFrom ? { dateFrom: filters.dateFrom } : {}), ...(filters.dateTo ? { dateTo: filters.dateTo } : {}) }) + }, + { + label: '最高速度', + value: formatNumber(maxSpeed, ' km/h'), + detail: '用于复核超速、异常驾驶或位置跳点。', + color: isFiniteNumber(maxSpeed) ? 'blue' as const : 'grey' as const, + disabled: !isFiniteNumber(maxSpeed), + onClick: () => openQueryTab('location') + }, + { + label: '异常提示', + value: anomalyTotal > 0 ? `${anomalyTotal.toLocaleString()} 项` : '无明显异常', + detail: `断点 ${trajectoryAnomalies.gapCount.toLocaleString()} / 回退 ${trajectoryAnomalies.mileageRollbackCount.toLocaleString()} / 超速 ${trajectoryAnomalies.overspeedCount.toLocaleString()}`, + color: anomalyTotal > 0 ? 'orange' as const : 'green' as const, + disabled: false, + onClick: copyTrajectoryReviewPackage + } + ]; + const tripReviewActionItems = [ + { + label: '轨迹回放', + action: '播放轨迹', + detail: `${validLocations.length.toLocaleString()} 个有效定位点`, + color: validLocations.length > 0 ? 'green' as const : 'grey' as const, + disabled: validLocations.length === 0, + onClick: () => openQueryTab('location') + }, + { + label: '统计核对', + action: '统计查询', + detail: `区间里程 ${formatNumber(mileageDelta, ' km')}`, + color: isFiniteNumber(mileageDelta) ? 'blue' as const : 'grey' as const, + disabled: !onOpenMileage || !currentVehicleKeyword, + onClick: () => onOpenMileage?.({ keyword: currentVehicleKeyword, protocol: currentProtocol, ...(filters.dateFrom ? { dateFrom: filters.dateFrom } : {}), ...(filters.dateTo ? { dateTo: filters.dateTo } : {}) }) + }, + { + label: '导出证据', + action: '复制交付', + detail: `位置 ${locations.total.toLocaleString()} / 明细 ${(rawFrames.total ?? 0).toLocaleString()}`, + color: deliveryStateColor, + disabled: false, + onClick: copyDeliveryPackage + }, + { + label: '告警复盘', + action: '告警事件', + detail: anomalyTotal > 0 ? `${anomalyTotal.toLocaleString()} 项异常待说明` : '同一时间窗查看告警闭环', + color: anomalyTotal > 0 ? 'orange' as const : 'blue' as const, + disabled: false, + onClick: () => { + window.location.hash = buildAppHash({ + page: 'alert-events', + keyword: currentVehicleKeyword, + protocol: currentProtocol, + filters: { + ...(filters.dateFrom ? { dateFrom: filters.dateFrom } : {}), + ...(filters.dateTo ? { dateTo: filters.dateTo } : {}) + } + }); + } + } + ]; return (
@@ -1916,6 +2002,55 @@ export function History({ ))}
+ {mode === 'trajectory' ? ( +
+
+ + 行程复盘摘要 + {trajectoryDecisionState} + {deliveryState} + + + 把客户最关心的起止时间、路线跨度、里程差、最高速度和异常提示放在轨迹前面。 + + + 这层摘要对应 Trip History 的客户读法:先给结论,再让客户选择回放、统计、导出或告警复盘。 + +
+
+ {tripReviewSummaryItems.map((item) => ( + + ))} +
+
+ {tripReviewActionItems.map((item) => ( + + ))} +
+
+ ) : null} {mode === 'query' ? (
diff --git a/vehicle-data-platform/apps/web/src/styles/global.css b/vehicle-data-platform/apps/web/src/styles/global.css index 241fb81d..9e77bdd1 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -10259,6 +10259,93 @@ button.vp-realtime-command-item:focus-visible { line-height: 18px; } +.vp-trip-review-summary { + margin-top: 16px; + border: 1px solid rgba(22, 100, 255, 0.18); + border-radius: var(--vp-radius); + background: #fff; + box-shadow: var(--vp-shadow-sm); + display: grid; + gap: 0; + overflow: hidden; +} + +.vp-trip-review-summary-copy { + padding: 18px; + border-bottom: 1px solid var(--vp-border); + background: #f8fbff; + display: grid; + gap: 10px; +} + +.vp-trip-review-summary-copy .semi-typography-secondary { + font-size: 13px; + line-height: 20px; +} + +.vp-trip-review-summary-grid { + padding: 16px; + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 10px; +} + +.vp-trip-review-action-grid { + padding: 0 16px 16px; + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 10px; +} + +.vp-trip-review-summary-item, +.vp-trip-review-action { + min-height: 118px; + padding: 12px; + border: 1px solid rgba(22, 100, 255, 0.14); + border-radius: 8px; + background: #fbfcff; + color: inherit; + cursor: pointer; + font: inherit; + text-align: left; + display: grid; + align-content: start; + gap: 8px; +} + +.vp-trip-review-summary-item:hover, +.vp-trip-review-summary-item:focus-visible, +.vp-trip-review-action:hover, +.vp-trip-review-action:focus-visible { + border-color: rgba(22, 100, 255, 0.42); + background: #f5f9ff; + box-shadow: var(--vp-shadow-sm); + outline: none; +} + +.vp-trip-review-summary-item:disabled, +.vp-trip-review-action:disabled { + cursor: not-allowed; + opacity: 0.62; +} + +.vp-trip-review-summary-item strong, +.vp-trip-review-action strong { + color: var(--vp-text); + font-size: 18px; + line-height: 24px; + font-weight: 700; + word-break: break-word; +} + +.vp-trip-review-summary-item span, +.vp-trip-review-action span { + color: var(--vp-text-muted); + font-size: 12px; + line-height: 18px; + word-break: break-word; +} + .vp-history-service-desk { margin-top: 16px; border: 1px solid rgba(22, 100, 255, 0.18); @@ -13261,6 +13348,8 @@ button.vp-realtime-command-item:focus-visible { .vp-trajectory-impact-grid, .vp-history-evidence-chain, .vp-history-evidence-chain-grid, + .vp-trip-review-summary-grid, + .vp-trip-review-action-grid, .vp-history-evidence-package-overview, .vp-history-evidence-package-grid, .vp-history-delivery-readiness, 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 97a46458..abe605ac 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -6284,6 +6284,17 @@ test('copies trajectory playback summary from history page', async () => { expect(screen.getByRole('button', { name: '客户时间窗证据链 历史导出 明细证据' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户时间窗证据链 字段裁剪 字段导出' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户时间窗证据链 告警复盘 告警事件' })).toBeInTheDocument(); + expect(screen.getByText('行程复盘摘要')).toBeInTheDocument(); + expect(screen.getByText('把客户最关心的起止时间、路线跨度、里程差、最高速度和异常提示放在轨迹前面。')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '行程复盘摘要 起点 2026-07-03 10:00:00' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '行程复盘摘要 终点 2026-07-03 11:00:00' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '行程复盘摘要 行程里程 22.6 km' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '行程复盘摘要 最高速度 58.8 km/h' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '行程复盘摘要 异常提示 1 项' })).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.getByText('按车辆和时间窗复盘每一段行程')).toBeInTheDocument(); expect(screen.getByText('客户需要看到路线、起止时间、里程变化、速度异常和可导出的证据。明细证据和字段裁剪只作为轨迹复盘的依据。')).toBeInTheDocument(); expect(screen.getByRole('button', { name: '轨迹回放指标 轨迹点 2' })).toBeInTheDocument(); @@ -7908,10 +7919,10 @@ test('shows trajectory playback workspace from history locations', async () => { expect(screen.getByText('2 个有效轨迹点')).toBeInTheDocument(); expect(screen.getByText('区间里程')).toBeInTheDocument(); expect(screen.getAllByText('12.4 km').length).toBeGreaterThan(0); - expect(screen.getByText('最高速度')).toBeInTheDocument(); - expect(screen.getByText('42 km/h')).toBeInTheDocument(); - expect(screen.getByText('起点')).toBeInTheDocument(); - expect(screen.getByText('终点')).toBeInTheDocument(); + expect(screen.getAllByText('最高速度').length).toBeGreaterThan(0); + expect(screen.getAllByText('42 km/h').length).toBeGreaterThan(0); + expect(screen.getAllByText('起点').length).toBeGreaterThan(0); + expect(screen.getAllByText('终点').length).toBeGreaterThan(0); }); test('shows trajectory evidence quality on history playback workspace', async () => {