From 52a73cba2d46d46f0db799eb9facce6173a907cd Mon Sep 17 00:00:00 2001 From: lingniu Date: Sun, 5 Jul 2026 21:44:10 +0800 Subject: [PATCH] feat(platform): add time window service path --- .../apps/web/src/pages/Dashboard.tsx | 78 +++++++++++++++ .../apps/web/src/styles/global.css | 96 +++++++++++++++++++ .../apps/web/src/test/App.test.tsx | 92 +++++++++++++++++- 3 files changed, 265 insertions(+), 1 deletion(-) diff --git a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx index 12276288..f3b113f3 100644 --- a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx @@ -980,6 +980,53 @@ export function Dashboard({ onClick: openTimeMonitorAlerts } ]; + const timeWindowServicePathItems = [ + { + step: '1', + label: '锁定范围', + value: dayRangeText(timeMonitorScopeValue.dateFrom, timeMonitorScopeValue.dateTo), + detail: timeMonitorScopeValue.keyword ? `单车 ${timeMonitorScopeValue.keyword}` : '先确认车辆或车辆池,再应用同一时间窗。', + action: '应用时间窗', + color: timeMonitorHasRange ? 'green' as const : 'orange' as const, + onClick: () => setTimeMonitorFilters(timeMonitorScopeValue) + }, + { + step: '2', + label: '回放轨迹', + value: `${formatCount(summary?.activeToday)} 活跃`, + detail: '查看时间窗内位置、速度、停驶和里程断点。', + action: '轨迹回放', + color: timeMonitorHasTrajectory ? 'blue' as const : 'orange' as const, + onClick: openTimeMonitorHistory + }, + { + step: '3', + label: '核对里程', + value: `${formatCount(serviceSummary?.totalVehicles)} 车辆`, + detail: '用同一范围核对区间里程、日统计和 BI 口径。', + action: '统计查询', + color: 'green' as const, + onClick: openTimeMonitorMileage + }, + { + step: '4', + label: '导出证据', + value: `${formatCount(summary?.frameToday)} 帧`, + detail: '导出轨迹、原始记录和解析字段,形成客户证据包。', + action: '数据导出', + color: timeMonitorHasHistory ? 'blue' as const : 'orange' as const, + onClick: openTimeMonitorRaw + }, + { + step: '5', + label: '告警说明', + value: `${formatCount(summary?.issueVehicles)} 告警`, + detail: highPriorityIssue ? `${qualityIssueLabel(highPriorityIssue.issueType)} / ${priorityIssueVehicleLabel(highPriorityIssue)}` : '没有高优先级告警时,交付时说明当前无异常。', + action: '告警通知', + color: timeMonitorHasAlerts ? 'orange' as const : 'green' as const, + onClick: openTimeMonitorAlerts + } + ]; const dataFlowStages = [ { stage: '01', @@ -2392,6 +2439,37 @@ export function Dashboard({ ))} +
+
+ + 时间窗服务路径 + {timeMonitorDeliveryStatus.label} + + 锁定同一辆车和同一时间范围后,按轨迹、里程、历史证据和告警说明形成客户可复盘的交付路径。 + + 这条路径把自定义时间窗从查询条件变成客户交付流程:先确定范围,再按同一范围打开轨迹、统计、导出和告警。 + +
+
+ {timeWindowServicePathItems.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 04c6bc87..502c3a68 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -2392,6 +2392,100 @@ body { line-height: 20px; } +.vp-time-service-path { + margin-top: 14px; + display: grid; + grid-template-columns: minmax(280px, 0.58fr) minmax(0, 1.42fr); + gap: 12px; +} + +.vp-time-service-path-copy { + min-height: 174px; + padding: 14px; + border: 1px solid rgba(22, 100, 255, 0.24); + border-radius: 8px; + background: #f6f9ff; + display: grid; + gap: 10px; + align-content: start; +} + +.vp-time-service-path-copy .semi-typography { + line-height: 21px; +} + +.vp-time-service-path-grid { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 10px; +} + +.vp-time-service-path-item { + min-height: 174px; + padding: 12px; + border: 1px solid var(--vp-border); + border-radius: 8px; + background: #fff; + color: inherit; + cursor: pointer; + font: inherit; + text-align: left; + display: grid; + grid-template-columns: 30px minmax(0, 1fr); + gap: 10px; + align-content: start; +} + +.vp-time-service-path-item:hover, +.vp-time-service-path-item:focus-visible { + border-color: rgba(22, 100, 255, 0.42); + background: #f7fbff; + box-shadow: var(--vp-shadow-sm); + outline: none; +} + +.vp-time-service-path-item > span { + width: 28px; + height: 28px; + border-radius: 999px; + background: #eef4ff; + color: var(--vp-primary); + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 12px; + font-weight: 800; +} + +.vp-time-service-path-item div { + min-width: 0; + display: grid; + gap: 8px; + align-content: start; +} + +.vp-time-service-path-item strong { + color: var(--vp-text); + font-size: 17px; + line-height: 22px; + word-break: break-word; +} + +.vp-time-service-path-item small { + color: var(--vp-text-muted); + font-size: 12px; + line-height: 18px; + word-break: break-word; +} + +.vp-time-service-path-item em { + color: var(--vp-primary); + font-size: 12px; + font-style: normal; + font-weight: 700; + line-height: 18px; +} + .vp-time-audit-strip { margin-top: 14px; padding: 14px; @@ -10643,6 +10737,8 @@ button.vp-realtime-command-item:focus-visible { .vp-mileage-kpi-grid, .vp-mileage-task-grid, .vp-mileage-mini-charts, + .vp-time-service-path, + .vp-time-service-path-grid, .vp-playback-layout, .vp-playback-timeline { 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 034d5eeb..df95846e 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -734,6 +734,96 @@ test('dashboard prioritizes customer vehicle service command over data sources', expect(screen.getByRole('button', { name: '客户车队分组工作台 报表交付组 1,286,320 今日数据 历史导出' })).toBeInTheDocument(); }); +test('dashboard guides customer custom time window service path', async () => { + window.history.replaceState(null, '', '/#/dashboard'); + vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { + const path = String(input); + if (path.includes('/api/ops/health')) { + return { + ok: true, + json: async () => ({ + data: { + linkHealth: [], + kafkaLag: 0, + activeConnections: 0, + capacityFindings: [], + redisOnlineKeys: 0, + tdengineWritable: true, + mysqlWritable: true, + runtime: { + requestTimeoutMs: 5000, + amapWebJsConfigured: true, + amapApiConfigured: true, + amapSecurityProxyEnabled: true, + amapSecurityCodeExposed: false + } + }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + if (path.includes('/api/dashboard/summary')) { + return { + ok: true, + json: async () => ({ + data: { + onlineVehicles: 3, + activeToday: 4, + frameToday: 1286320, + issueVehicles: 7, + kafkaLag: 0, + protocols: [], + serviceStatuses: [], + linkHealth: [] + }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + if (path.includes('/api/vehicle-service/summary')) { + return { + ok: true, + json: async () => ({ + data: { + totalVehicles: 1033, + boundVehicles: 1024, + onlineVehicles: 208, + singleSourceVehicles: 391, + multiSourceVehicles: 181, + noDataVehicles: 461, + identityRequiredVehicles: 9, + serviceStatuses: [], + protocols: [], + missingSources: [] + }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + return { + ok: true, + json: async () => ({ + data: { items: [], total: 0, limit: 8, offset: 0 }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + }); + + render(); + + expect(await screen.findByText('时间窗服务路径')).toBeInTheDocument(); + expect(screen.getByText('锁定同一辆车和同一时间范围后,按轨迹、里程、历史证据和告警说明形成客户可复盘的交付路径。')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '时间窗服务路径 1 锁定范围 1 天窗口 应用时间窗' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '时间窗服务路径 2 回放轨迹 4 活跃 轨迹回放' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '时间窗服务路径 3 核对里程 1,033 车辆 统计查询' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '时间窗服务路径 4 导出证据 1,286,320 帧 数据导出' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '时间窗服务路径 5 告警说明 7 告警 告警通知' })).toBeInTheDocument(); +}); + test('dashboard exposes vehicle data center capability matrix', async () => { window.history.replaceState(null, '', '/#/dashboard'); const writeText = vi.fn(() => Promise.resolve()); @@ -1020,7 +1110,7 @@ test('dashboard exposes vehicle data center capability matrix', async () => { expect(screen.getByRole('button', { name: '车辆服务入口 看位置 实时地图' })).toBeInTheDocument(); expect(screen.getAllByRole('button', { name: '复制客户服务说明' }).length).toBeGreaterThanOrEqual(1); expect(screen.getByText('自定义时间窗复盘')).toBeInTheDocument(); - expect(screen.getByText('应用时间窗')).toBeInTheDocument(); + expect(screen.getAllByText('应用时间窗').length).toBeGreaterThan(0); expect(screen.getByText('重置最近一天')).toBeInTheDocument(); expect(screen.getByText('当前时间窗')).toBeInTheDocument(); expect(screen.getByText('客户时间窗复盘包')).toBeInTheDocument();