diff --git a/vehicle-data-platform/apps/web/src/App.tsx b/vehicle-data-platform/apps/web/src/App.tsx index 56e48bc0..4db56d30 100644 --- a/vehicle-data-platform/apps/web/src/App.tsx +++ b/vehicle-data-platform/apps/web/src/App.tsx @@ -392,7 +392,7 @@ export default function App() { }; const pages: Record = { - dashboard: , + dashboard: , vehicles: , realtime: , detail: , diff --git a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx index 1760e3f1..14e6ea23 100644 --- a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx @@ -112,12 +112,16 @@ export function Dashboard({ onOpenVehicle, onOpenQuality, onOpenRealtime, - onOpenVehicles + onOpenVehicles, + onOpenHistory, + onOpenMileage }: { onOpenVehicle: (vin: string, protocol?: string) => void; onOpenQuality: (filters?: Record) => void; onOpenRealtime: (filters?: Record) => void; onOpenVehicles: (filters?: Record) => void; + onOpenHistory: (filters?: Record) => void; + onOpenMileage: (filters?: Record) => void; }) { const [summary, setSummary] = useState(null); const [serviceSummary, setServiceSummary] = useState(null); @@ -239,6 +243,38 @@ export function Dashboard({ online: row.online, title: `${row.plate || row.vin || '-'} ${row.primaryProtocol || ''} ${row.lastSeen || ''}` })); + const capabilities = [ + { + title: '实时监控', + description: '按车辆聚合最新位置、在线状态和多来源实时字段,支持地图态势查看。', + action: '查看在线车辆', + onClick: () => onOpenRealtime({ online: 'online' }) + }, + { + title: '轨迹回放', + description: '围绕车辆回放历史位置轨迹,结合高德线路和 RAW 证据定位异常。', + action: '打开轨迹', + onClick: () => onOpenHistory() + }, + { + title: '历史数据查询', + description: '查询位置历史、RAW 帧和解析字段,为车辆问题复盘提供证据链。', + action: '查询历史', + onClick: () => onOpenHistory() + }, + { + title: '告警事件触发与通知', + description: '围绕断链、无来源、VIN 缺失、字段缺失形成告警和通知闭环。', + action: '查看告警', + onClick: () => onOpenQuality() + }, + { + title: '统计查询', + description: '提供里程等运营统计查询,保证区间总值与每日统计口径闭合。', + action: '查看统计', + onClick: () => onOpenMileage() + } + ]; return (
@@ -267,6 +303,21 @@ export function Dashboard({ 0 ? 'orange' : 'green'}>Kafka Lag {formatLag(summary?.kafkaLag)} + +
+ {capabilities.map((item) => ( +
+
+ {item.title} + {item.description} +
+ +
+ ))} +
+
{ expect(screen.getByText('7 质量关注')).toBeInTheDocument(); }); +test('dashboard exposes vehicle data center capability matrix', 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 } }, + 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; + } + if (path.includes('/api/mileage/summary')) { + return { + ok: true, + json: async () => ({ + data: { vehicleCount: 0, recordCount: 0, sourceCount: 0, totalMileageKm: 0, averageMileagePerVin: 0 }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + if (path.includes('/api/quality/summary')) { + return { + ok: true, + json: async () => ({ + data: { issueVehicleCount: 0, issueRecordCount: 0, errorCount: 0, warningCount: 0, protocols: [], issueTypes: [] }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + if (path.includes('/api/quality/notification-plan')) { + return { + ok: true, + json: async () => ({ + data: { rules: [], policies: [], priorityIssues: [], activeRuleCount: 0, p0RuleCount: 0 }, + 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; + }); + + const renderDashboard = async () => { + window.history.replaceState(null, '', '/#/dashboard'); + render(); + expect(await screen.findByText('车辆业务能力矩阵')).toBeInTheDocument(); + }; + + await renderDashboard(); + expect(screen.getAllByText('实时监控').length).toBeGreaterThanOrEqual(1); + expect(screen.getAllByText('轨迹回放').length).toBeGreaterThanOrEqual(1); + expect(screen.getAllByText('历史数据查询').length).toBeGreaterThanOrEqual(1); + expect(screen.getAllByText('告警事件触发与通知').length).toBeGreaterThanOrEqual(1); + expect(screen.getAllByText('统计查询').length).toBeGreaterThanOrEqual(1); + + fireEvent.click(screen.getByRole('button', { name: '能力入口 实时监控' })); + expect(window.location.hash).toBe('#/realtime?online=online'); + cleanup(); + + await renderDashboard(); + fireEvent.click(screen.getByRole('button', { name: '能力入口 轨迹回放' })); + expect(window.location.hash.startsWith('#/history')).toBe(true); + cleanup(); + + await renderDashboard(); + fireEvent.click(screen.getByRole('button', { name: '能力入口 历史数据查询' })); + expect(window.location.hash.startsWith('#/history')).toBe(true); + cleanup(); + + await renderDashboard(); + fireEvent.click(screen.getByRole('button', { name: '能力入口 告警事件触发与通知' })); + expect(window.location.hash).toBe('#/quality'); + cleanup(); + + await renderDashboard(); + fireEvent.click(screen.getByRole('button', { name: '能力入口 统计查询' })); + expect(window.location.hash.startsWith('#/mileage')).toBe(true); +}); + test('dashboard exposes vehicle command center map actions', async () => { window.history.replaceState(null, '', '/#/dashboard'); vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {