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 () => {