From 0f6b47e97e8eac9b4f55b591882dc9b048ed6979 Mon Sep 17 00:00:00 2001 From: lingniu Date: Sun, 5 Jul 2026 16:00:29 +0800 Subject: [PATCH] feat(platform): add history customer question flow --- .../apps/web/src/pages/History.tsx | 72 +++++++++++++++++++ .../apps/web/src/styles/global.css | 72 +++++++++++++++++++ .../apps/web/src/test/App.test.tsx | 6 ++ 3 files changed, 150 insertions(+) diff --git a/vehicle-data-platform/apps/web/src/pages/History.tsx b/vehicle-data-platform/apps/web/src/pages/History.tsx index 7ff1eae6..e68c8493 100644 --- a/vehicle-data-platform/apps/web/src/pages/History.tsx +++ b/vehicle-data-platform/apps/web/src/pages/History.tsx @@ -1044,6 +1044,44 @@ export function History({ onClick: () => copyScheduledHistoryReportPlan() } ]; + const customerHistoryQuestions = [ + { + question: '这辆车这段时间在哪里?', + answer: validLocations.length > 0 ? `${validLocations.length.toLocaleString()} 个有效轨迹点` : '先查询位置历史', + evidence: `${filters.dateFrom || '-'} 至 ${filters.dateTo || '-'}`, + action: '轨迹回放', + color: validLocations.length > 0 ? 'green' as const : 'orange' as const, + disabled: false, + onClick: () => openQueryTab('location') + }, + { + question: '这段时间跑了多少公里?', + answer: formatNumber(mileageDelta, ' km'), + evidence: `最高速度 ${formatNumber(maxSpeed, ' km/h')}`, + action: '统计查询', + color: isFiniteNumber(mileageDelta) ? 'green' as const : 'grey' as const, + disabled: !onOpenMileage, + onClick: () => onOpenMileage?.({ keyword: currentVehicleKeyword, protocol: currentProtocol, ...(filters.dateFrom ? { dateFrom: filters.dateFrom } : {}), ...(filters.dateTo ? { dateTo: filters.dateTo } : {}) }) + }, + { + question: '为什么定位或里程不连续?', + answer: anomalyTotal > 0 ? `${anomalyTotal.toLocaleString()} 项需复核` : '当前页无明显异常', + evidence: `断点 ${trajectoryAnomalies.gapCount.toLocaleString()} / 里程回退 ${trajectoryAnomalies.mileageRollbackCount.toLocaleString()} / 超速 ${trajectoryAnomalies.overspeedCount.toLocaleString()}`, + action: '复核证据', + color: anomalyTotal > 0 ? 'orange' as const : 'green' as const, + disabled: false, + onClick: () => copyTrajectoryDecision() + }, + { + question: '明细能导出给客户吗?', + answer: deliveryState, + evidence: `位置 ${locations.total.toLocaleString()} / 明细 ${(rawFrames.total ?? 0).toLocaleString()} / 字段 ${rawFieldRows.length.toLocaleString()}`, + action: '导出交付包', + color: deliveryStateColor, + disabled: false, + onClick: () => copyDeliveryPackage() + } + ]; const clearRangeFilters = () => { applyFilters(currentVehicleKeyword ? { keyword: currentVehicleKeyword } : {}); }; @@ -1456,6 +1494,40 @@ export function History({ ) : null} + {mode === 'query' ? ( + +
+
+ + 客户问题 + {deliveryState} + {currentVehicleKeyword ? '单车范围' : '全部车辆'} + + 用客户语言回答路线、里程、异常和导出问题 + + 查询页不要求客户理解协议和字段表,先把问题转成轨迹、统计、证据和交付包,再进入对应明细。 + +
+
+ {customerHistoryQuestions.map((item) => ( + + ))} +
+
+
+ ) : null}
diff --git a/vehicle-data-platform/apps/web/src/styles/global.css b/vehicle-data-platform/apps/web/src/styles/global.css index 0206d95e..7ba6da1c 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -5063,6 +5063,76 @@ button.vp-realtime-command-item:focus-visible { font-weight: 700; } +.vp-history-question-board { + display: grid; + grid-template-columns: minmax(300px, 0.7fr) minmax(0, 1.3fr); + gap: 14px; +} + +.vp-history-question-summary { + padding: 14px; + border: 1px solid rgba(22, 100, 255, 0.18); + border-radius: var(--vp-radius); + background: #f7faff; + display: grid; + gap: 10px; + align-content: start; +} + +.vp-history-question-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; +} + +.vp-history-question-item { + min-height: 150px; + padding: 12px; + border: 1px solid var(--vp-border); + border-radius: var(--vp-radius); + background: #fbfcff; + color: inherit; + text-align: left; + font: inherit; + cursor: pointer; + display: grid; + gap: 8px; + align-content: start; +} + +.vp-history-question-item:hover, +.vp-history-question-item:focus-visible { + border-color: rgba(22, 100, 255, 0.42); + box-shadow: 0 10px 24px rgba(24, 39, 75, 0.08); + outline: none; +} + +.vp-history-question-item:disabled { + cursor: not-allowed; + opacity: 0.62; +} + +.vp-history-question-item strong { + color: var(--vp-text); + font-size: 18px; + line-height: 24px; + font-weight: 700; + word-break: break-word; +} + +.vp-history-question-item span { + color: var(--vp-text-muted); + font-size: 12px; + line-height: 18px; +} + +.vp-history-question-item em { + color: var(--vp-primary); + font-size: 12px; + font-style: normal; + font-weight: 700; +} + .vp-history-query-workbench { display: grid; grid-template-columns: minmax(260px, 0.68fr) minmax(0, 1.32fr); @@ -6663,6 +6733,8 @@ button.vp-realtime-command-item:focus-visible { .vp-history-customer-export-steps, .vp-history-report-templates, .vp-history-report-template-grid, + .vp-history-question-board, + .vp-history-question-grid, .vp-history-query-workbench, .vp-history-query-grid, .vp-history-package-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 da3c8dcc..2835570d 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -6552,6 +6552,12 @@ test('shows parsed field history as a flattened evidence table', async () => { expect(screen.getByText('报表交付模板')).toBeInTheDocument(); expect(screen.getByText('按客户问题选择导出模板')).toBeInTheDocument(); expect(screen.getByText('车队报表不只是下载 CSV:先选择用途,再生成轨迹报告、明细证据包、字段裁剪包或周期交付计划。')).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(); expect(screen.getByRole('button', { name: '报表交付模板 明细证据包 导出证据' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '报表交付模板 字段裁剪包 字段配置' })).toBeInTheDocument();