From 9a16daae00ce8a29cb550f11faff93bfe430f1ee Mon Sep 17 00:00:00 2001 From: lingniu Date: Sun, 5 Jul 2026 13:20:55 +0800 Subject: [PATCH] feat(platform): sharpen customer vehicle cockpit --- .../apps/web/src/pages/Dashboard.tsx | 135 +++++++++++++ .../apps/web/src/styles/global.css | 180 ++++++++++++++++++ .../apps/web/src/test/App.test.tsx | 15 +- 3 files changed, 327 insertions(+), 3 deletions(-) diff --git a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx index 099f93c8..38765e8d 100644 --- a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx @@ -1319,11 +1319,146 @@ export function Dashboard({ ]; copyText(lines.join('\n'), '客户服务说明'); }; + const customerCockpitMetrics = [ + { + label: '车辆总数', + value: formatCount(serviceSummary?.totalVehicles), + detail: '以 VIN 聚合车牌、手机号、OEM 和来源证据', + color: 'blue' as const, + onClick: () => onOpenVehicles({}) + }, + { + label: '在线车辆', + value: formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles), + detail: vehicleServiceOnlineText(serviceSummary, summary), + color: 'green' as const, + onClick: () => onOpenMap({ online: 'online' }) + }, + { + label: '有效定位', + value: commandLocatedCount.toLocaleString(), + detail: '用于实时地图、轨迹回放和客户位置问询', + color: commandLocatedCount > 0 ? 'green' as const : 'orange' as const, + onClick: () => onOpenRealtime({ online: 'online' }) + }, + { + label: '告警车辆', + value: formatCount(summary?.issueVehicles), + detail: highPriorityIssue ? `${qualityIssueLabel(highPriorityIssue.issueType)} / ${priorityIssueVehicleLabel(highPriorityIssue)}` : '暂无高优先级告警', + color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const, + onClick: () => onOpenQuality() + } + ]; + const customerCockpitWorkflows = [ + { + title: '实时看车', + description: '打开地图确认车辆在哪里、是否在线、最后上报是否新鲜。', + action: '实时地图', + evidence: `${commandLocatedCount.toLocaleString()} 辆有定位`, + onClick: () => onOpenMap({ online: 'online' }) + }, + { + title: '轨迹复盘', + description: '用同一辆车和同一时间窗回放位置、速度、里程断点。', + action: '轨迹回放', + evidence: `${formatCount(summary?.activeToday)} 今日活跃`, + onClick: openTimeMonitorHistory + }, + { + title: '里程核对', + description: '按车辆口径核对区间里程、日统计和异常来源。', + action: '里程统计', + evidence: `${formatCount(serviceSummary?.totalVehicles)} 辆可统计`, + onClick: openTimeMonitorMileage + }, + { + title: '数据交付', + description: '按车辆、时间和字段导出位置历史、原始记录和解析字段。', + action: '历史导出', + evidence: `${formatCount(summary?.frameToday)} 今日数据`, + onClick: openTimeMonitorRaw + }, + { + title: '告警通知', + description: '将断链、离线、定位异常和字段缺失变成可通知事件。', + action: '告警事件', + evidence: `${formatCount(summary?.issueVehicles)} 告警车辆`, + onClick: () => onOpenQuality() + } + ]; + const customerTrustSignals = [ + { + label: '数据来源', + value: `${formatCount(serviceSummary?.multiSourceVehicles)} 多源 / ${formatCount(serviceSummary?.singleSourceVehicles)} 单源`, + detail: '来源只作为可信度证据,不作为客户主导航。' + }, + { + label: '地图能力', + value: amapConfigured ? '高德地图可用' : '坐标预览', + detail: amapConfigured ? '支持实时看车和轨迹底图。' : '地图配置缺失时仍可查看坐标证据。' + }, + { + label: '时间窗', + value: dayRangeText(timeMonitorScopeValue.dateFrom, timeMonitorScopeValue.dateTo), + detail: '同一时间范围贯穿轨迹、里程、历史和告警。' + } + ]; return (
+ +
+
+ + 客户车辆监控中心 + 按车服务 + 0 ? 'orange' : 'green'}>{formatCount(summary?.issueVehicles)} 告警车辆 + + 客户只需要回答:车在哪、跑了多少、发生了什么 + + 32960、808、宇通 MQTT 都只是车辆数据来源。产品主线围绕车辆本身提供实时地图、轨迹回放、里程统计、时间窗监控、历史查询、告警通知和数据导出。 + + + + + + + +
+
+ {customerCockpitMetrics.map((item) => ( + + ))} +
+
+
+
+ {customerCockpitWorkflows.map((item) => ( + + ))} +
+
+ {customerTrustSignals.map((item) => ( +
+ {item.label} + {item.value} + {item.detail} +
+ ))} +
+
+
diff --git a/vehicle-data-platform/apps/web/src/styles/global.css b/vehicle-data-platform/apps/web/src/styles/global.css index 1f70fdcd..a1fe4491 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -350,6 +350,179 @@ body { margin-top: 12px; } +.vp-customer-cockpit { + margin-bottom: 16px; + overflow: hidden; + border-color: rgba(22, 100, 255, 0.18); +} + +.vp-customer-cockpit .semi-card-body { + display: grid; + grid-template-columns: minmax(0, 1.08fr) minmax(420px, 0.92fr); + min-height: 360px; + background: #ffffff; +} + +.vp-customer-cockpit-main { + min-width: 0; + padding: 24px; + background: + linear-gradient(90deg, rgba(22, 100, 255, 0.08) 0%, rgba(255, 255, 255, 0) 62%), + #ffffff; + display: grid; + gap: 20px; + align-content: start; +} + +.vp-customer-cockpit-copy { + max-width: 760px; + display: grid; + gap: 14px; +} + +.vp-customer-cockpit-copy .semi-typography { + max-width: 720px; + font-size: 14px; + line-height: 22px; +} + +.vp-customer-cockpit-metrics { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; +} + +.vp-customer-cockpit-metric { + min-height: 130px; + padding: 14px; + border: 1px solid var(--vp-border); + border-radius: 8px; + background: rgba(255, 255, 255, 0.82); + text-align: left; + cursor: pointer; + font: inherit; + display: grid; + align-content: start; + gap: 10px; +} + +.vp-customer-cockpit-metric:hover, +.vp-customer-cockpit-metric:focus-visible { + border-color: rgba(22, 100, 255, 0.45); + box-shadow: var(--vp-shadow-sm); + outline: none; +} + +.vp-customer-cockpit-metric strong { + color: var(--vp-text); + font-size: 26px; + line-height: 32px; +} + +.vp-customer-cockpit-metric span { + color: var(--vp-text-muted); + font-size: 12px; + line-height: 18px; +} + +.vp-customer-cockpit-rail { + min-width: 0; + padding: 18px; + border-left: 1px solid var(--vp-border); + background: #f8fbff; + display: grid; + gap: 14px; +} + +.vp-customer-cockpit-workflows { + display: grid; + gap: 8px; +} + +.vp-customer-cockpit-workflow { + min-height: 58px; + padding: 10px 12px; + border: 1px solid rgba(22, 100, 255, 0.14); + border-radius: 8px; + background: #ffffff; + cursor: pointer; + text-align: left; + font: inherit; + display: grid; + grid-template-columns: 76px 84px minmax(0, 1fr) auto; + gap: 10px; + align-items: center; +} + +.vp-customer-cockpit-workflow:hover, +.vp-customer-cockpit-workflow:focus-visible { + border-color: rgba(22, 100, 255, 0.45); + background: #f5f9ff; + outline: none; +} + +.vp-customer-cockpit-workflow span { + color: var(--vp-text-muted); + font-size: 12px; + font-weight: 700; + line-height: 18px; +} + +.vp-customer-cockpit-workflow strong { + color: #1664ff; + font-size: 14px; + line-height: 20px; +} + +.vp-customer-cockpit-workflow small { + color: var(--vp-text-muted); + font-size: 12px; + line-height: 18px; +} + +.vp-customer-cockpit-workflow em { + color: var(--vp-text); + font-size: 12px; + font-style: normal; + font-weight: 700; + white-space: nowrap; +} + +.vp-customer-cockpit-trust { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px; +} + +.vp-customer-cockpit-trust-item { + min-height: 88px; + padding: 10px; + border: 1px solid var(--vp-border); + border-radius: 8px; + background: #ffffff; + display: grid; + gap: 5px; + align-content: start; +} + +.vp-customer-cockpit-trust-item span { + color: var(--vp-text-muted); + font-size: 12px; + line-height: 18px; +} + +.vp-customer-cockpit-trust-item strong { + color: var(--vp-text); + font-size: 14px; + line-height: 20px; +} + +.vp-customer-cockpit-trust-item small { + color: var(--vp-text-muted); + font-size: 12px; + line-height: 17px; +} + .vp-customer-hero { margin-bottom: 16px; overflow: hidden; @@ -5037,6 +5210,9 @@ button.vp-realtime-command-item:focus-visible { .vp-map-ops-board, .vp-map-ops-readiness, .vp-map-ops-work, + .vp-customer-cockpit .semi-card-body, + .vp-customer-cockpit-metrics, + .vp-customer-cockpit-trust, .vp-time-monitor-grid, .vp-command-center-grid, .vp-customer-task-grid, @@ -5148,6 +5324,10 @@ button.vp-realtime-command-item:focus-visible { grid-template-columns: 1fr; } + .vp-customer-cockpit-workflow { + grid-template-columns: 1fr; + } + .vp-map-commandbar, .vp-mileage-console-head, .vp-map-canvas-toolbar { 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 5c154553..f4efddc7 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -507,7 +507,7 @@ test('dashboard renders vehicle service summary metrics', async () => { expect(screen.getAllByText('1,033').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('今日活跃')).toBeInTheDocument(); expect(screen.getAllByText('有效定位').length).toBeGreaterThanOrEqual(1); - expect(screen.getByText('告警车辆')).toBeInTheDocument(); + expect(screen.getAllByText('告警车辆').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('今日数据')).toBeInTheDocument(); expect(screen.getByText('档案不完整')).toBeInTheDocument(); expect(screen.getByText('366')).toBeInTheDocument(); @@ -797,6 +797,15 @@ test('dashboard exposes vehicle data center capability matrix', async () => { await renderDashboard(); expect(screen.getByText('车辆服务工作台')).toBeInTheDocument(); + expect(screen.getByText('客户车辆监控中心')).toBeInTheDocument(); + expect(screen.getByText('客户只需要回答:车在哪、跑了多少、发生了什么')).toBeInTheDocument(); + expect(screen.getByText('32960、808、宇通 MQTT 都只是车辆数据来源。产品主线围绕车辆本身提供实时地图、轨迹回放、里程统计、时间窗监控、历史查询、告警通知和数据导出。')).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.getByText('来源只作为可信度证据,不作为客户主导航。')).toBeInTheDocument(); expect(screen.getByText('先看车在哪里,再看车发生了什么')).toBeInTheDocument(); expect(screen.getByText('车辆服务指挥台')).toBeInTheDocument(); expect(screen.getByText('全域车辆监控')).toBeInTheDocument(); @@ -856,7 +865,7 @@ test('dashboard exposes vehicle data center capability matrix', async () => { expect(screen.getAllByText('统计里程').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('历史查询导出')).toBeInTheDocument(); expect(screen.getAllByText('轨迹回放').length).toBeGreaterThanOrEqual(1); - expect(screen.getByText('里程核对')).toBeInTheDocument(); + expect(screen.getAllByText('里程核对').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('历史证据').length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText('告警复盘').length).toBeGreaterThanOrEqual(1); expect(screen.getByRole('button', { name: '时间窗作业 轨迹回放 打开轨迹' })).toBeInTheDocument(); @@ -1805,7 +1814,7 @@ test('dashboard keeps core vehicle service metrics when preview requests fail', expect(await screen.findByText('总车辆')).toBeInTheDocument(); expect(screen.getAllByText('1,033').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('今日活跃')).toBeInTheDocument(); - expect(screen.getByText('告警车辆')).toBeInTheDocument(); + expect(screen.getAllByText('告警车辆').length).toBeGreaterThanOrEqual(1); }); test('opens dashboard coverage from service action queue', async () => {