From e69d8ecd103133b7662029e999abc82283328d76 Mon Sep 17 00:00:00 2001 From: lingniu Date: Mon, 6 Jul 2026 02:56:08 +0800 Subject: [PATCH] feat(platform): add history delivery conclusion strip --- .../apps/web/src/pages/History.tsx | 107 +++++++++++++++++ .../apps/web/src/styles/global.css | 112 ++++++++++++++++++ .../apps/web/src/test/App.test.tsx | 10 ++ 3 files changed, 229 insertions(+) diff --git a/vehicle-data-platform/apps/web/src/pages/History.tsx b/vehicle-data-platform/apps/web/src/pages/History.tsx index 145eeabc..e3a2bfa7 100644 --- a/vehicle-data-platform/apps/web/src/pages/History.tsx +++ b/vehicle-data-platform/apps/web/src/pages/History.tsx @@ -1864,6 +1864,66 @@ export function History({ onClick: copyDeliveryPackage } ]; + const customerDeliveryConclusionItems = [ + { + label: '交付状态', + value: deliveryState, + detail: deliveryState === '可交付' ? '当前范围可直接生成客户证据包。' : deliveryState === '待查询数据' ? '先查询位置或明细数据后再交付。' : '可交付,但需要附带复核说明。', + color: deliveryStateColor, + onClick: () => copyDeliveryPackage() + }, + { + label: '位置历史', + value: `${locations.total.toLocaleString()} 条`, + detail: `${validLocations.length.toLocaleString()} 个有效坐标,可用于轨迹复盘。`, + color: locations.total > 0 ? 'green' as const : 'grey' as const, + onClick: () => openQueryTab('location') + }, + { + label: '明细证据', + value: `${(rawFrames.total ?? 0).toLocaleString()} 帧`, + detail: '用于解释位置、里程、字段值和原始上报来源。', + color: (rawFrames.total ?? 0) > 0 ? 'blue' as const : 'grey' as const, + onClick: () => openQueryTab('raw') + }, + { + label: '字段裁剪', + value: `${fieldEvidenceCount.toLocaleString()} 字段`, + detail: selectedFieldCount > 0 ? `已按 ${selectedFieldCount.toLocaleString()} 个字段裁剪。` : '按客户需要选择字段后再交付。', + color: fieldEvidenceCount > 0 ? 'green' as const : 'orange' as const, + onClick: () => openQueryTab('fields') + } + ]; + const customerDeliveryConclusionActions = [ + { + label: '导出位置', + action: '位置CSV', + color: locations.items.length > 0 ? 'green' as const : 'grey' as const, + disabled: locations.items.length === 0, + onClick: exportLocations + }, + { + label: '导出明细', + action: '明细CSV', + color: rawFrames.items.length > 0 ? 'blue' as const : 'grey' as const, + disabled: rawFrames.items.length === 0, + onClick: exportRawFrames + }, + { + label: '字段裁剪', + action: '字段CSV', + color: rawFieldRows.length > 0 ? 'green' as const : 'orange' as const, + disabled: rawFieldRows.length === 0, + onClick: rawFieldRows.length > 0 ? exportRawFields : () => openQueryTab('fields') + }, + { + label: '复制说明', + action: '交付包', + color: deliveryStateColor, + disabled: false, + onClick: copyDeliveryPackage + } + ]; const tripReviewSummaryItems = [ { label: '起点', @@ -1970,6 +2030,53 @@ export function History({ 来源证据:{currentProtocol || '全部来源证据'} {scopeDescription} + {mode === 'query' ? ( +
+
+ + 客户数据交付结论条 + {deliveryState} + 0 ? 'green' : 'orange'}>{fieldEvidenceCount.toLocaleString()} 字段 + + + 先判断这次查询能交付什么,再选择导出位置、明细证据、字段裁剪或复制说明。 + + + 当前范围:{deliveryScopeText},时间窗:{filters.dateFrom || '-'} 至 {filters.dateTo || '-'}。 + +
+
+ {customerDeliveryConclusionItems.map((item) => ( + + ))} +
+
+ {customerDeliveryConclusionActions.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 e858f40c..1af02d8e 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -10381,6 +10381,109 @@ button.vp-realtime-command-item:focus-visible { line-height: 18px; } +.vp-history-delivery-conclusion-strip { + margin-top: 16px; + border: 1px solid rgba(22, 100, 255, 0.18); + border-radius: var(--vp-radius); + background: #fff; + display: grid; + grid-template-columns: minmax(260px, 0.86fr) minmax(0, 1.36fr) minmax(190px, 0.58fr); + overflow: hidden; +} + +.vp-history-delivery-conclusion-copy { + padding: 18px; + border-right: 1px solid var(--vp-border); + background: #f7fbff; + display: grid; + gap: 10px; + align-content: start; +} + +.vp-history-delivery-conclusion-metrics { + padding: 14px; + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 10px; +} + +.vp-history-delivery-conclusion-metric { + min-height: 116px; + padding: 12px; + border: 1px solid rgba(22, 100, 255, 0.13); + border-radius: 8px; + background: #fbfcff; + color: inherit; + cursor: pointer; + font: inherit; + text-align: left; + display: grid; + gap: 8px; + align-content: start; +} + +.vp-history-delivery-conclusion-metric:hover, +.vp-history-delivery-conclusion-metric:focus-visible, +.vp-history-delivery-conclusion-action:hover, +.vp-history-delivery-conclusion-action:focus-visible { + border-color: rgba(22, 100, 255, 0.42); + background: #f5f9ff; + box-shadow: var(--vp-shadow-sm); + outline: none; +} + +.vp-history-delivery-conclusion-metric strong { + color: var(--vp-text); + font-size: 19px; + line-height: 24px; + word-break: break-word; +} + +.vp-history-delivery-conclusion-metric span { + color: var(--vp-text-muted); + font-size: 12px; + line-height: 18px; + word-break: break-word; +} + +.vp-history-delivery-conclusion-actions { + padding: 14px; + border-left: 1px solid var(--vp-border); + background: #fafcff; + display: grid; + grid-template-columns: 1fr; + gap: 8px; +} + +.vp-history-delivery-conclusion-action { + min-height: 46px; + padding: 8px 10px; + border: 1px solid rgba(22, 100, 255, 0.13); + border-radius: 8px; + background: #fff; + color: inherit; + cursor: pointer; + font: inherit; + text-align: left; + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.vp-history-delivery-conclusion-action:disabled { + cursor: not-allowed; + opacity: 0.62; +} + +.vp-history-delivery-conclusion-action span { + color: var(--vp-primary); + font-size: 12px; + font-weight: 700; + line-height: 18px; + white-space: nowrap; +} + .vp-history-evidence-chain { margin-top: 16px; border: 1px solid rgba(22, 100, 255, 0.18); @@ -13556,6 +13659,8 @@ button.vp-realtime-command-item:focus-visible { .vp-trajectory-anomaly-grid, .vp-trajectory-impact-board, .vp-trajectory-impact-grid, + .vp-history-delivery-conclusion-strip, + .vp-history-delivery-conclusion-metrics, .vp-history-evidence-chain, .vp-history-evidence-chain-grid, .vp-trip-review-summary-grid, @@ -13672,6 +13777,13 @@ button.vp-realtime-command-item:focus-visible { border-bottom: 1px solid var(--vp-border); } + .vp-history-delivery-conclusion-copy, + .vp-history-delivery-conclusion-actions { + border-left: 0; + border-right: 0; + border-bottom: 1px solid var(--vp-border); + } + .vp-customer-cockpit-workflow { grid-template-columns: 1fr; } 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 2910b633..8884bf25 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -7631,6 +7631,16 @@ test('shows parsed field history as a flattened evidence table', async () => { render(); expect(await screen.findByRole('heading', { name: '历史查询导出' })).toBeInTheDocument(); + expect(screen.getByText('客户数据交付结论条')).toBeInTheDocument(); + expect(screen.getByText('先判断这次查询能交付什么,再选择导出位置、明细证据、字段裁剪或复制说明。')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户数据交付结论 交付状态 可交付' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户数据交付结论 位置历史 0 条' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户数据交付结论 明细证据 1 帧' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户数据交付结论 字段裁剪 2 字段' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户数据交付动作 导出位置 位置CSV' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户数据交付动作 导出明细 明细CSV' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户数据交付动作 字段裁剪 字段CSV' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户数据交付动作 复制说明 交付包' })).toBeInTheDocument(); expect(screen.getByText('车辆历史服务台')).toBeInTheDocument(); expect(screen.getByText('客户证据包交付总览')).toBeInTheDocument(); expect(screen.getByText('先给客户一个结论:当前筛选能交付哪些证据、还缺哪一类证据、下一步应该导出还是补查。')).toBeInTheDocument();