feat(platform): add vehicle capability matrix

This commit is contained in:
lingniu
2026-07-04 14:23:50 +08:00
parent d6c2edce7e
commit c03e890096
4 changed files with 210 additions and 2 deletions

View File

@@ -392,7 +392,7 @@ export default function App() {
};
const pages: Record<PageKey, JSX.Element> = {
dashboard: <Dashboard onOpenVehicle={openVehicle} onOpenQuality={openQuality} onOpenRealtime={openRealtime} onOpenVehicles={openVehicles} />,
dashboard: <Dashboard onOpenVehicle={openVehicle} onOpenQuality={openQuality} onOpenRealtime={openRealtime} onOpenVehicles={openVehicles} onOpenHistory={openHistoryWithFilters} onOpenMileage={openMileageWithFilters} />,
vehicles: <Vehicles onOpenVehicle={openVehicle} onOpenQuality={openQuality} onFiltersChange={updateVehicleFilters} initialFilters={vehicleFilters} />,
realtime: <Realtime onOpenVehicle={openVehicle} onOpenHistory={openHistoryForVehicle} onOpenQuality={openQuality} onFiltersChange={updateRealtimeFilters} initialFilters={realtimeFilters} />,
detail: <VehicleDetail vin={activeVin} protocol={activeProtocol} onOpenRealtime={openRealtimeForVehicle} onOpenHistory={openHistoryForVehicle} onOpenRaw={openRawForVehicle} onOpenMileage={openMileageForVehicle} onOpenVehicles={openVehicles} onOpenQuality={openQuality} onQueryChange={updateVehicleDetailQuery} />,

View File

@@ -112,12 +112,16 @@ export function Dashboard({
onOpenVehicle,
onOpenQuality,
onOpenRealtime,
onOpenVehicles
onOpenVehicles,
onOpenHistory,
onOpenMileage
}: {
onOpenVehicle: (vin: string, protocol?: string) => void;
onOpenQuality: (filters?: Record<string, string>) => void;
onOpenRealtime: (filters?: Record<string, string>) => void;
onOpenVehicles: (filters?: Record<string, string>) => void;
onOpenHistory: (filters?: Record<string, string>) => void;
onOpenMileage: (filters?: Record<string, string>) => void;
}) {
const [summary, setSummary] = useState<DashboardSummary | null>(null);
const [serviceSummary, setServiceSummary] = useState<VehicleServiceSummary | null>(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 (
<div className="vp-page">
@@ -267,6 +303,21 @@ export function Dashboard({
<Tag color={(summary?.kafkaLag ?? 0) > 0 ? 'orange' : 'green'}>Kafka Lag {formatLag(summary?.kafkaLag)}</Tag>
</Space>
</Card>
<Card bordered title="车辆业务能力矩阵" style={{ marginBottom: 16 }}>
<div className="vp-capability-grid">
{capabilities.map((item) => (
<div key={item.title} className="vp-capability-item">
<div>
<Typography.Title heading={6} style={{ margin: 0 }}>{item.title}</Typography.Title>
<Typography.Text type="secondary">{item.description}</Typography.Text>
</div>
<Button size="small" theme="light" type="primary" aria-label={`能力入口 ${item.title}`} onClick={item.onClick}>
{item.action}
</Button>
</div>
))}
</div>
</Card>
<Card
bordered
title="车辆态势指挥台"

View File

@@ -699,6 +699,31 @@ body {
min-width: 0;
}
.vp-capability-grid {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 12px;
}
.vp-capability-item {
min-height: 154px;
padding: 14px;
border: 1px solid var(--vp-border);
border-radius: var(--vp-radius);
background: #fbfcff;
display: grid;
gap: 12px;
align-content: space-between;
}
.vp-capability-item .semi-typography {
line-height: 20px;
}
.vp-capability-item .semi-button {
justify-self: start;
}
.vp-vehicle-map-layout {
display: grid;
grid-template-columns: minmax(0, 1fr) 280px;
@@ -775,6 +800,7 @@ body {
@media (max-width: 900px) {
.vp-evidence-grid,
.vp-action-grid,
.vp-capability-grid,
.vp-conclusion-grid,
.vp-monitor-layout,
.vp-alert-flow,

View File

@@ -281,6 +281,137 @@ test('dashboard presents one vehicle service operating posture', async () => {
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(<App />);
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) => {