From 8b7614d2d6c81770e662bfc1b6d8c0af5b8100f9 Mon Sep 17 00:00:00 2001 From: lingniu Date: Mon, 6 Jul 2026 05:52:22 +0800 Subject: [PATCH] feat(platform): add customer daily operations home --- .../apps/web/src/pages/Dashboard.tsx | 98 +++++++++++++++++++ .../apps/web/src/styles/global.css | 80 +++++++++++++++ .../apps/web/src/test/App.test.tsx | 22 +++++ 3 files changed, 200 insertions(+) diff --git a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx index 27e7770c..5d77cf10 100644 --- a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx @@ -2739,6 +2739,70 @@ export function Dashboard({ onClick: openTimeMonitorRaw } ]; + const copyTodayVehicleServiceBrief = () => { + const rawFilters = { tab: 'raw', includeFields: 'true' }; + const lines = [ + '【客户今日车辆服务简报】', + `在线车辆:${formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles)} / ${formatCount(serviceSummary?.totalVehicles)}`, + `地图可见:${commandLocatedCount.toLocaleString()} 辆有坐标`, + `今日活跃:${formatCount(summary?.activeToday)}`, + `今日数据:${formatCount(summary?.frameToday)}`, + `告警车辆:${formatCount(summary?.issueVehicles)}`, + `当前时间窗:${timeMonitorWindowLabel}`, + highPriorityIssue + ? `优先告警:${qualityIssueLabel(highPriorityIssue.issueType)} / ${priorityIssueVehicleLabel(highPriorityIssue)}` + : '优先告警:暂无高优先级告警', + `车辆地图:${appURL(buildAppHash({ page: 'map', filters: { online: 'online' } }))}`, + `实时监控:${appURL(buildAppHash({ page: 'realtime', filters: { online: 'online' } }))}`, + `轨迹回放:${appURL(buildAppHash({ page: 'history', filters: timeMonitorScopeValue }))}`, + `里程报表:${appURL(buildAppHash({ page: 'mileage', filters: timeMonitorScopeValue }))}`, + `历史导出:${appURL(buildAppHash({ page: 'history-query', filters: rawFilters }))}`, + `告警通知:${appURL(buildAppHash({ page: 'alert-events', filters: highPriorityIssue?.issueType ? { issueType: highPriorityIssue.issueType } : {} }))}` + ]; + copyText(lines.join('\n'), '今日服务简报'); + }; + const todayOperationsItems = [ + { + label: '车辆在线', + value: `${formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles)} 在线`, + detail: `${formatCount(serviceSummary?.totalVehicles)} 辆车辆服务资产,先确认客户能看到多少车。`, + action: '打开地图', + color: 'green' as const, + onClick: () => onOpenMap({ online: 'online' }) + }, + { + label: '地图可见', + value: `${commandLocatedCount.toLocaleString()} 有坐标`, + detail: '有效坐标决定客户能不能直接看地图态势和位置分布。', + action: '实时地图', + color: commandLocatedCount > 0 ? 'blue' as const : 'orange' as const, + onClick: () => onOpenMap({ online: 'online' }) + }, + { + label: '轨迹复盘', + value: `${formatCount(summary?.activeToday)} 活跃`, + detail: '今日活跃车辆可按同一时间窗回放轨迹、速度和停驶片段。', + action: '回放轨迹', + color: 'blue' as const, + onClick: openTimeMonitorHistory + }, + { + label: '里程报表', + value: timeMonitorWindowLabel, + detail: '从报表周期进入里程核对,避免客户只看到协议帧量。', + action: '核对里程', + color: 'blue' as const, + onClick: openTimeMonitorMileage + }, + { + label: '告警通知', + value: `${formatCount(summary?.issueVehicles)} 告警`, + detail: highPriorityIssue ? `${qualityIssueLabel(highPriorityIssue.issueType)} / ${priorityIssueVehicleLabel(highPriorityIssue)}` : '暂无高优先级告警,保持巡检。', + action: '闭环处理', + color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const, + onClick: () => onOpenQuality(highPriorityIssue?.issueType ? { issueType: highPriorityIssue.issueType } : {}) + } + ]; return (
@@ -2851,6 +2915,40 @@ export function Dashboard({
+
+
+ + 客户今日运营台 + {vehicleServiceOnlineText(serviceSummary, summary)} + 0 ? 'orange' : 'green'}>{formatCount(summary?.issueVehicles)} 告警 + + + 客户今日运营台 + + + 今天先看在线和地图,再处理告警、复盘轨迹、核对里程,最后把证据导出给客户。 + + +
+
+ {todayOperationsItems.map((item) => ( + + ))} +
+
) : null} {focusMode === 'time-window' ? ( diff --git a/vehicle-data-platform/apps/web/src/styles/global.css b/vehicle-data-platform/apps/web/src/styles/global.css index 73b28532..5bba2840 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -1311,6 +1311,84 @@ body { line-height: 18px; } +.vp-today-operations-home { + grid-column: 1 / -1; + border-top: 1px solid rgba(22, 100, 255, 0.14); + background: #f6f9ff; + display: grid; + grid-template-columns: minmax(280px, 0.36fr) minmax(0, 1fr); +} + +.vp-today-operations-home-copy { + min-width: 0; + padding: 16px; + border-right: 1px solid rgba(22, 100, 255, 0.12); + display: grid; + gap: 10px; + align-content: start; +} + +.vp-today-operations-home-copy .semi-button { + width: fit-content; +} + +.vp-today-operations-home-grid { + min-width: 0; + padding: 12px; + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 10px; +} + +.vp-today-operations-home-item { + min-width: 0; + min-height: 142px; + padding: 12px; + border: 1px solid rgba(22, 100, 255, 0.14); + border-radius: 8px; + background: #ffffff; + color: inherit; + cursor: pointer; + font: inherit; + text-align: left; + display: grid; + gap: 8px; + align-content: start; + transition: border-color 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease; +} + +.vp-today-operations-home-item:hover, +.vp-today-operations-home-item:focus-visible { + border-color: rgba(22, 100, 255, 0.42); + box-shadow: 0 10px 24px rgba(24, 39, 75, 0.08); + outline: none; + transform: translateY(-1px); +} + +.vp-today-operations-home-item strong { + color: var(--vp-text); + font-size: 18px; + line-height: 24px; + font-weight: 800; + word-break: break-word; +} + +.vp-today-operations-home-item span { + color: var(--vp-text-muted); + font-size: 12px; + line-height: 18px; + word-break: break-word; +} + +.vp-today-operations-home-item em { + align-self: end; + color: var(--vp-primary); + font-size: 12px; + font-style: normal; + font-weight: 800; + line-height: 18px; +} + .vp-customer-service-next { grid-column: 1 / -1; border-top: 1px solid var(--vp-border); @@ -14833,6 +14911,8 @@ button.vp-realtime-command-item:focus-visible { .vp-vehicle-question-grid, .vp-customer-service-path, .vp-customer-service-path-grid, + .vp-today-operations-home, + .vp-today-operations-home-grid, .vp-customer-service-overview, .vp-customer-service-overview-grid, .vp-customer-service-next, 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 13c8ccc4..8f17c4c3 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -1120,6 +1120,11 @@ test('dashboard presents one vehicle service operating posture', async () => { test('dashboard prioritizes customer vehicle service command over data sources', async () => { window.history.replaceState(null, '', '/#/dashboard'); + const writeText = vi.fn(() => Promise.resolve()); + Object.defineProperty(navigator, 'clipboard', { + configurable: true, + value: { writeText } + }); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { const path = String(input); if (path.includes('/api/ops/health')) { @@ -1206,6 +1211,23 @@ test('dashboard prioritizes customer vehicle service command over data sources', expect(screen.getByRole('button', { name: '车辆服务指挥台 查这段时间 轨迹统计' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '车辆服务指挥台 导出数据 历史导出' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '车辆服务指挥台 处理告警 告警事件' })).toBeInTheDocument(); + expect(screen.getAllByText('客户今日运营台').length).toBeGreaterThanOrEqual(1); + expect(screen.getByText('今天先看在线和地图,再处理告警、复盘轨迹、核对里程,最后把证据导出给客户。')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户今日运营台 车辆在线 208 在线 打开地图' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户今日运营台 地图可见 0 有坐标 实时地图' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户今日运营台 轨迹复盘 4 活跃 回放轨迹' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户今日运营台 里程报表 1 天窗口 核对里程' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户今日运营台 告警通知 7 告警 闭环处理' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /复制今日服务简报/ })).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: /复制今日服务简报/ })); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【客户今日车辆服务简报】')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('在线车辆:208 / 1,033')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('地图可见:0 辆有坐标')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('今日活跃:4')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('告警车辆:7')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆地图:http://localhost:3000/#/map?online=online')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('里程报表:http://localhost:3000/#/mileage')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('历史导出:http://localhost:3000/#/history-query?tab=raw&includeFields=true')); expect(screen.getByRole('button', { name: '车辆服务指挥台优先队列 告警待处理 7 辆 告警事件' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '车辆服务指挥台优先队列 无有效定位 208 辆 打开地图' })).toBeInTheDocument(); expect(screen.getAllByText('车辆服务指挥台')[0].compareDocumentPosition(screen.getByText('车辆地图态势')) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();