diff --git a/vehicle-data-platform/apps/web/src/pages/Mileage.tsx b/vehicle-data-platform/apps/web/src/pages/Mileage.tsx index fa3e58c7..8c330311 100644 --- a/vehicle-data-platform/apps/web/src/pages/Mileage.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Mileage.tsx @@ -1227,6 +1227,48 @@ export function Mileage({ onClick: copyOnlineOperationsReport } ]; + const mileageCustomerQuestions = [ + { + question: '这段时间总共跑了多少?', + answer: `${formatKm(summary.totalMileageKm)} km / ${summary.vehicleCount.toLocaleString()} 辆车`, + action: '复制摘要', + color: 'blue' as const, + disabled: false, + onClick: copyStatisticsSummary + }, + { + question: '这份里程能给客户或 BI 吗?', + answer: `${mileageDeliveryState} / ${publishAuditConclusion}`, + action: mileageDeliveryState === '可交付' ? '复制交付包' : '发布审计', + color: mileageDeliveryColor, + disabled: false, + onClick: mileageDeliveryState === '可交付' ? copyMileageDeliveryPackage : copyPublishAudit + }, + { + question: '哪天或哪辆车有异常?', + answer: anomalyRows[0] ? `${anomalyRows[0].plate || anomalyRows[0].vin} / ${anomalyRows[0].date}` : '当前明细无异常', + action: anomalyRows[0] ? '异常证据' : '复制决策', + color: anomalyRows.length > 0 ? 'orange' as const : 'green' as const, + disabled: false, + onClick: () => anomalyRows[0] ? copyMileageEvidence(anomalyRows[0]) : copyMileageDecision() + }, + { + question: '这段路能回放轨迹吗?', + answer: currentVehicleKeyword ? mileageRangeText : '请先选择车辆', + action: '轨迹复核', + color: currentVehicleKeyword ? 'blue' as const : 'grey' as const, + disabled: !currentVehicleKeyword || !onOpenHistory, + onClick: () => onOpenHistory?.({ keyword: currentVehicleKeyword, protocol: currentProtocol, dateFrom: filters.dateFrom ?? '', dateTo: filters.dateTo ?? '' }) + }, + { + question: '明细数据能导出吗?', + answer: `${rows.length.toLocaleString()} 条当前页 / 覆盖 ${formatPercent(pageCoverageRate)}`, + action: '导出明细', + color: rows.length > 0 ? 'blue' as const : 'grey' as const, + disabled: rows.length === 0, + onClick: exportMileage + } + ]; useEffect(() => { const nextFilters = mergeInitialFilters(initialVin, initialProtocol, initialFilters); @@ -1289,6 +1331,24 @@ export function Mileage({ ))} + +
+ {mileageCustomerQuestions.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 f59358c6..de34b325 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -3942,6 +3942,53 @@ button.vp-realtime-command-item:focus-visible { align-self: end; } +.vp-mileage-question-grid { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 12px; +} + +.vp-mileage-question-item { + min-height: 118px; + padding: 12px; + border: 1px solid rgba(22, 100, 255, 0.16); + border-radius: var(--vp-radius); + background: #fbfcff; + color: inherit; + cursor: pointer; + font: inherit; + text-align: left; + display: grid; + gap: 9px; + align-content: start; +} + +.vp-mileage-question-item:hover, +.vp-mileage-question-item:focus-visible { + border-color: rgba(22, 100, 255, 0.42); + background: #f5f9ff; + outline: none; +} + +.vp-mileage-question-item:disabled { + cursor: not-allowed; + opacity: 0.62; +} + +.vp-mileage-question-item strong { + color: var(--vp-text); + font-size: 14px; + line-height: 20px; + word-break: break-word; +} + +.vp-mileage-question-item span { + color: var(--vp-text-muted); + font-size: 12px; + line-height: 18px; + word-break: break-word; +} + .vp-mileage-delivery-board { display: grid; grid-template-columns: minmax(280px, 0.64fr) minmax(0, 1.36fr); @@ -6544,6 +6591,7 @@ button.vp-realtime-command-item:focus-visible { .vp-stat-insight-grid, .vp-mileage-next-actions, .vp-mileage-next-grid, + .vp-mileage-question-grid, .vp-mileage-decision-board, .vp-mileage-decision-grid, .vp-stat-definition-grid, 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 a3ed6570..d37e227d 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -7354,6 +7354,12 @@ test('shows vehicle and source scope on mileage hash', async () => { expect(await screen.findByText('当前车辆:VIN-MILEAGE-001')).toBeInTheDocument(); expect(screen.getAllByText('数据通道:GB32960').length).toBeGreaterThan(0); + expect(screen.getByText('客户里程问题')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户里程问题 这段时间总共跑了多少? 复制摘要' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户里程问题 这份里程能给客户或 BI 吗? 发布审计' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户里程问题 哪天或哪辆车有异常? 复制决策' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户里程问题 这段路能回放轨迹吗? 轨迹复核' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户里程问题 明细数据能导出吗? 导出明细' })).toBeInTheDocument(); }); test('applies mileage date range from shareable hash to API requests', async () => {