diff --git a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx index 8df75e17..30240566 100644 --- a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx @@ -2079,6 +2079,66 @@ export function Dashboard({ onClick: openTimeMonitorAlerts } ]; + const customerPrimaryFlowItems = [ + { + label: '实时地图', + value: `${formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles)} 在线`, + detail: '先确认车辆在哪里、是否在线、是否有有效坐标。', + action: '打开地图', + color: 'green' as const, + onClick: () => onOpenMap({ online: 'online' }) + }, + { + label: '时间窗复盘', + value: dayRangeText(timeMonitorScopeValue.dateFrom, timeMonitorScopeValue.dateTo), + detail: '用同一时间范围串起轨迹、位置、里程、历史明细和导出。', + action: '查询导出', + color: 'blue' as const, + onClick: openTimeMonitorHistory + }, + { + label: '里程统计', + value: `${formatCount(serviceSummary?.totalVehicles)} 车辆`, + detail: '以车辆为对象核对区间里程、每日统计和 BI 口径。', + 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: openTimeMonitorAlerts + } + ]; + const operationsEvidenceItems = [ + { + label: '链路健康', + value: unhealthyLinkCount > 0 ? `${formatCount(unhealthyLinkCount)} 异常` : '正常', + detail: '解释车辆实时、轨迹、统计或导出不可用时的链路证据。', + action: '查看告警', + color: unhealthyLinkCount > 0 ? 'orange' as const : 'green' as const, + onClick: () => onOpenQuality(highPriorityIssue?.issueType ? { issueType: highPriorityIssue.issueType } : {}) + }, + { + label: '消息积压', + value: formatLag(summary?.kafkaLag), + detail: '判断接收、解析和历史落地是否存在排队。', + action: '查看运维', + color: (summary?.kafkaLag ?? 0) > 0 ? 'orange' as const : 'green' as const, + onClick: () => onOpenQuality() + }, + { + label: '缓存在线', + value: formatCount(opsHealth?.redisOnlineKeys), + detail: '辅助解释在线车辆数和实时状态是否可信。', + action: '缓存证据', + color: (opsHealth?.redisOnlineKeys ?? 0) > 0 ? 'blue' as const : 'orange' as const, + onClick: () => onOpenRealtime({ online: 'online' }) + } + ]; return (
@@ -2216,6 +2276,60 @@ export function Dashboard({ ))}
+
+
+
+ + 客户主流程 + 一个车辆服务 + + 默认展示车辆地图、时间窗、里程、导出和告警;三类数据源只作为二级证据。 +
+
+ {customerPrimaryFlowItems.map((item) => ( + + ))} +
+
+
+
+ + 运维证据层 + 0 ? 'orange' : 'green'}>{unhealthyLinkCount > 0 ? `${formatCount(unhealthyLinkCount)} 异常` : '链路正常'} + + + 用于解释车辆服务不可用的链路、队列、缓存、存储和来源证据,不作为客户默认入口。 + +
+
+ {operationsEvidenceItems.map((item) => ( + + ))} +
+
+
{ + 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: 281, + 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: 73, + 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: '客户主流程 实时地图 208 在线 打开地图' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户主流程 时间窗复盘 1 天窗口 查询导出' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户主流程 里程统计 1,033 车辆 统计查询' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户主流程 告警通知 7 告警 通知处理' })).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: '运维证据层 缓存在线 281 缓存证据' })).toBeInTheDocument(); +}); + test('dashboard guides customer custom time window service path', async () => { window.history.replaceState(null, '', '/#/dashboard'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {