feat(platform): add vehicle capability matrix
This commit is contained in:
@@ -392,7 +392,7 @@ export default function App() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const pages: Record<PageKey, JSX.Element> = {
|
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} />,
|
vehicles: <Vehicles onOpenVehicle={openVehicle} onOpenQuality={openQuality} onFiltersChange={updateVehicleFilters} initialFilters={vehicleFilters} />,
|
||||||
realtime: <Realtime onOpenVehicle={openVehicle} onOpenHistory={openHistoryForVehicle} onOpenQuality={openQuality} onFiltersChange={updateRealtimeFilters} initialFilters={realtimeFilters} />,
|
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} />,
|
detail: <VehicleDetail vin={activeVin} protocol={activeProtocol} onOpenRealtime={openRealtimeForVehicle} onOpenHistory={openHistoryForVehicle} onOpenRaw={openRawForVehicle} onOpenMileage={openMileageForVehicle} onOpenVehicles={openVehicles} onOpenQuality={openQuality} onQueryChange={updateVehicleDetailQuery} />,
|
||||||
|
|||||||
@@ -112,12 +112,16 @@ export function Dashboard({
|
|||||||
onOpenVehicle,
|
onOpenVehicle,
|
||||||
onOpenQuality,
|
onOpenQuality,
|
||||||
onOpenRealtime,
|
onOpenRealtime,
|
||||||
onOpenVehicles
|
onOpenVehicles,
|
||||||
|
onOpenHistory,
|
||||||
|
onOpenMileage
|
||||||
}: {
|
}: {
|
||||||
onOpenVehicle: (vin: string, protocol?: string) => void;
|
onOpenVehicle: (vin: string, protocol?: string) => void;
|
||||||
onOpenQuality: (filters?: Record<string, string>) => void;
|
onOpenQuality: (filters?: Record<string, string>) => void;
|
||||||
onOpenRealtime: (filters?: Record<string, string>) => void;
|
onOpenRealtime: (filters?: Record<string, string>) => void;
|
||||||
onOpenVehicles: (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 [summary, setSummary] = useState<DashboardSummary | null>(null);
|
||||||
const [serviceSummary, setServiceSummary] = useState<VehicleServiceSummary | null>(null);
|
const [serviceSummary, setServiceSummary] = useState<VehicleServiceSummary | null>(null);
|
||||||
@@ -239,6 +243,38 @@ export function Dashboard({
|
|||||||
online: row.online,
|
online: row.online,
|
||||||
title: `${row.plate || row.vin || '-'} ${row.primaryProtocol || ''} ${row.lastSeen || ''}`
|
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 (
|
return (
|
||||||
<div className="vp-page">
|
<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>
|
<Tag color={(summary?.kafkaLag ?? 0) > 0 ? 'orange' : 'green'}>Kafka Lag {formatLag(summary?.kafkaLag)}</Tag>
|
||||||
</Space>
|
</Space>
|
||||||
</Card>
|
</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
|
<Card
|
||||||
bordered
|
bordered
|
||||||
title="车辆态势指挥台"
|
title="车辆态势指挥台"
|
||||||
|
|||||||
@@ -699,6 +699,31 @@ body {
|
|||||||
min-width: 0;
|
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 {
|
.vp-vehicle-map-layout {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) 280px;
|
grid-template-columns: minmax(0, 1fr) 280px;
|
||||||
@@ -775,6 +800,7 @@ body {
|
|||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.vp-evidence-grid,
|
.vp-evidence-grid,
|
||||||
.vp-action-grid,
|
.vp-action-grid,
|
||||||
|
.vp-capability-grid,
|
||||||
.vp-conclusion-grid,
|
.vp-conclusion-grid,
|
||||||
.vp-monitor-layout,
|
.vp-monitor-layout,
|
||||||
.vp-alert-flow,
|
.vp-alert-flow,
|
||||||
|
|||||||
@@ -281,6 +281,137 @@ test('dashboard presents one vehicle service operating posture', async () => {
|
|||||||
expect(screen.getByText('7 质量关注')).toBeInTheDocument();
|
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 () => {
|
test('dashboard exposes vehicle command center map actions', async () => {
|
||||||
window.history.replaceState(null, '', '/#/dashboard');
|
window.history.replaceState(null, '', '/#/dashboard');
|
||||||
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
|
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user