diff --git a/vehicle-data-platform/apps/web/src/pages/History.tsx b/vehicle-data-platform/apps/web/src/pages/History.tsx index 488f6685..62a3ca10 100644 --- a/vehicle-data-platform/apps/web/src/pages/History.tsx +++ b/vehicle-data-platform/apps/web/src/pages/History.tsx @@ -416,6 +416,45 @@ function deliveryPackageText({ ].join('\n'); } +function scheduledHistoryReportPlanText({ + filters, + locationCount, + rawCount, + fieldCount, + validPointCount, + anomalySummary +}: { + filters: HistoryFilters; + locationCount: number; + rawCount: number; + fieldCount: number; + validPointCount: number; + anomalySummary: TrajectoryAnomalySummary; +}) { + const vehicle = filters.keyword?.trim() || ''; + const protocol = filters.protocol?.trim() || ''; + const evidenceFilters = { + ...(filters.dateFrom?.trim() ? { dateFrom: filters.dateFrom.trim() } : {}), + ...(filters.dateTo?.trim() ? { dateTo: filters.dateTo.trim() } : {}) + }; + return [ + '【历史数据周期交付计划】', + `车辆范围:${vehicle || '全部车辆'}`, + `数据通道:${protocol || '全部数据通道'}`, + `时间范围:${filters.dateFrom?.trim() || '-'} 至 ${filters.dateTo?.trim() || '-'}`, + '推荐频率:日报用于运营复盘,周报用于客户对账,临时导出用于问题解释。', + '交付内容:轨迹报告 / 位置历史 CSV / 历史明细 CSV / 字段明细 CSV / 里程复核链接 / 质量提示。', + `当前数据量:位置 ${locationCount.toLocaleString()} 条,有效定位 ${validPointCount.toLocaleString()},历史明细 ${rawCount.toLocaleString()} 帧,字段 ${fieldCount.toLocaleString()} 个。`, + `质量提示:断点 ${anomalySummary.gapCount.toLocaleString()} / 里程回退 ${anomalySummary.mileageRollbackCount.toLocaleString()} / 超速 ${anomalySummary.overspeedCount.toLocaleString()}`, + '交付前检查:先锁定车辆和时间窗,再确认字段裁剪、分页覆盖和异常说明。', + `轨迹报告:${appURL(buildAppHash({ page: 'history', keyword: vehicle, protocol, filters: evidenceFilters }))}`, + `位置历史:${appURL(buildAppHash({ page: 'history-query', keyword: vehicle, protocol, filters: { ...evidenceFilters, tab: 'location' } }))}`, + `历史明细:${appURL(buildAppHash({ page: 'history-query', keyword: vehicle, protocol, filters: { ...evidenceFilters, tab: 'raw', includeFields: 'true' } }))}`, + `字段明细:${appURL(buildAppHash({ page: 'history-query', keyword: vehicle, protocol, filters: { ...evidenceFilters, tab: 'fields', includeFields: 'true', ...(filters.fields?.trim() ? { fields: filters.fields.trim() } : {}) } }))}`, + `里程复核:${appURL(buildAppHash({ page: 'mileage', keyword: vehicle, protocol, filters: evidenceFilters }))}` + ].join('\n'); +} + function trajectoryReviewPackageText({ filters, locationCount, @@ -967,6 +1006,44 @@ export function History({ onClick: () => copyDeliveryPackage() } ]; + const historyReportTemplates = [ + { + title: '轨迹报告', + value: validLocations.length > 0 ? `${validLocations.length.toLocaleString()} 点` : '待查询', + detail: '适合回答某辆车某段时间在哪里、怎么走、是否断点。', + color: validLocations.length > 0 ? 'green' as const : 'orange' as const, + action: '复制报告', + disabled: false, + onClick: () => copyTrajectoryReviewPackage() + }, + { + title: '历史明细包', + value: `${(rawFrames.total ?? 0).toLocaleString()} 帧`, + detail: '适合客户或BI追溯位置、里程、速度、SOC等字段来源。', + color: (rawFrames.total ?? 0) > 0 ? 'blue' as const : 'grey' as const, + action: '明细导出', + disabled: false, + onClick: () => openQueryTab('raw') + }, + { + title: '字段裁剪包', + value: selectedFieldCount > 0 ? `${selectedFieldCount.toLocaleString()} 字段` : `${rawFieldRows.length.toLocaleString()} 字段`, + detail: selectedFieldCount > 0 ? '按客户指定字段裁剪,减少导出体积。' : '先填写字段裁剪或切换字段明细,再生成字段级交付。', + color: rawFieldRows.length > 0 || selectedFieldCount > 0 ? 'green' as const : 'orange' as const, + action: '字段配置', + disabled: false, + onClick: () => openQueryTab('fields') + }, + { + title: '周期交付计划', + value: '日报/周报', + detail: '把同一查询范围沉淀为日报、周报或临时问题解释模板。', + color: 'blue' as const, + action: '复制计划', + disabled: false, + onClick: () => copyScheduledHistoryReportPlan() + } + ]; const clearRangeFilters = () => { applyFilters(currentVehicleKeyword ? { keyword: currentVehicleKeyword } : {}); }; @@ -1040,6 +1117,19 @@ export function History({ '客户查询导出包' ); }; + const copyScheduledHistoryReportPlan = () => { + copyText( + scheduledHistoryReportPlanText({ + filters, + locationCount: locations.total ?? 0, + rawCount: rawFrames.total ?? 0, + fieldCount: rawFieldRows.length, + validPointCount: validLocations.length, + anomalySummary: trajectoryAnomalies + }), + '历史数据周期交付计划' + ); + }; const copyTrajectoryReviewPackage = () => { copyText( trajectoryReviewPackageText({ @@ -1330,6 +1420,42 @@ export function History({ ) : null} + {mode === 'query' ? ( + +
+
+ + 报告模板 + {deliveryState} + 0 ? 'green' : 'grey'}> + {selectedFieldCount > 0 ? `${selectedFieldCount.toLocaleString()} 个字段裁剪` : '未裁剪字段'} + + + 按客户问题选择导出模板 + + 车队报表不只是下载 CSV:先选择用途,再生成轨迹报告、历史明细包、字段裁剪包或周期交付计划。 + +
+
+ {historyReportTemplates.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 598e86a5..fed0d01d 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -4629,6 +4629,76 @@ button.vp-realtime-command-item:focus-visible { align-self: end; } +.vp-history-report-templates { + display: grid; + grid-template-columns: minmax(300px, 0.7fr) minmax(0, 1.3fr); + gap: 14px; +} + +.vp-history-report-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-report-template-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; +} + +.vp-history-report-template { + min-height: 158px; + 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-report-template:hover, +.vp-history-report-template: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-report-template:disabled { + cursor: not-allowed; + opacity: 0.62; +} + +.vp-history-report-template strong { + color: var(--vp-text); + font-size: 18px; + line-height: 24px; + font-weight: 700; + word-break: break-word; +} + +.vp-history-report-template span { + color: var(--vp-text-muted); + font-size: 12px; + line-height: 18px; +} + +.vp-history-report-template 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); @@ -6170,6 +6240,8 @@ button.vp-realtime-command-item:focus-visible { .vp-trajectory-impact-grid, .vp-history-customer-export-board .semi-card-body, .vp-history-customer-export-steps, + .vp-history-report-templates, + .vp-history-report-template-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 a2804c06..9293b05f 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -6487,6 +6487,13 @@ test('shows parsed field history as a flattened evidence table', 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.getByText('车队报表不只是下载 CSV:先选择用途,再生成轨迹报告、历史明细包、字段裁剪包或周期交付计划。')).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.getAllByRole('button', { name: /复制决策说明/ }).length).toBeGreaterThanOrEqual(1); expect(screen.getByRole('button', { name: '客户轨迹决策 明细复核 历史明细' })).toBeInTheDocument(); @@ -6506,6 +6513,12 @@ test('shows parsed field history as a flattened evidence table', async () => { await waitFor(() => { expect(writeText).toHaveBeenCalledWith(expect.stringContaining('交付物:位置历史 / 轨迹回放 / 里程复核 / 历史明细 / 字段明细 / 质量提示')); }); + fireEvent.click(screen.getByRole('button', { name: '报表交付模板 周期交付计划 复制计划' })); + await waitFor(() => { + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【历史数据周期交付计划】')); + }); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('推荐频率:日报用于运营复盘,周报用于客户对账,临时导出用于问题解释。')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('交付内容:轨迹报告 / 位置历史 CSV / 历史明细 CSV / 字段明细 CSV / 里程复核链接 / 质量提示。')); expect(await screen.findByRole('tab', { name: '字段明细' })).toHaveAttribute('aria-selected', 'true'); expect(fetchMock).toHaveBeenCalledWith('/api/history/raw-frames/query', expect.objectContaining({ method: 'POST',