feat(platform): add vehicle scenario navigation

This commit is contained in:
lingniu
2026-07-04 20:47:29 +08:00
parent e8f0ebff35
commit 431f4a4505
3 changed files with 159 additions and 0 deletions

View File

@@ -504,6 +504,58 @@ export function Dashboard({
]
}
];
const scenarioNavigation = [
{
title: '实时监控',
objective: '确认车辆是否在线、位置是否可信、三类来源是否齐全。',
evidence: `Redis 在线态 / 最新定位 / 有效坐标 ${commandLocatedCount.toLocaleString()}`,
sla: '目标 0-1 秒内进入实时视图',
primaryAction: '实时监控',
secondaryAction: '地图态势',
onPrimary: () => onOpenRealtime({ online: 'online' }),
onSecondary: () => onOpenMap({ online: 'online' })
},
{
title: '轨迹回放',
objective: '按车辆复盘位置、速度、里程和断点,定位平台转发或车辆异常。',
evidence: `高德轨迹底座 / 今日活跃 ${formatCount(summary?.activeToday)}`,
sla: '支持按车辆和时间窗快速回放',
primaryAction: '轨迹回放',
secondaryAction: 'RAW证据',
onPrimary: () => onOpenHistory(),
onSecondary: () => onOpenHistory({ tab: 'raw', includeFields: 'true' })
},
{
title: '历史数据查询',
objective: '围绕车辆查询位置、RAW、解析字段给 BI、运维和业务复盘提供证据。',
evidence: `TDengine / 今日帧 ${formatCount(summary?.frameToday)} / Kafka Lag ${formatLag(summary?.kafkaLag)}`,
sla: '优先返回必要字段,避免大 JSON 拖慢查询',
primaryAction: '历史查询',
secondaryAction: '字段证据',
onPrimary: () => onOpenHistory({ tab: 'location' }),
onSecondary: () => onOpenHistory({ tab: 'raw', includeFields: 'true' })
},
{
title: '告警事件触发和通知',
objective: '把断链、无来源、VIN 缺失、字段缺失转换为可通知、可闭环事件。',
evidence: `告警车辆 ${formatCount(summary?.issueVehicles)} / 最高优先级 ${highPriorityIssue ? qualityIssueLabel(highPriorityIssue.issueType) : '暂无'}`,
sla: 'P0 进入通知队列,超时升级',
primaryAction: '告警事件',
secondaryAction: '通知规则',
onPrimary: () => onOpenQuality(),
onSecondary: () => onOpenQuality(highPriorityIssue?.issueType ? { issueType: highPriorityIssue.issueType } : {})
},
{
title: '统计查询',
objective: '按车辆口径查询里程等指标,保证区间统计和日统计可闭合。',
evidence: `车辆口径 ${formatCount(serviceSummary?.totalVehicles)} / 多源覆盖 ${formatCount(serviceSummary?.multiSourceVehicles)}`,
sla: '统计值必须能追溯轨迹和 RAW 证据',
primaryAction: '统计查询',
secondaryAction: '车辆中心',
onPrimary: () => onOpenMileage(),
onSecondary: () => onOpenVehicles({})
}
];
const copyOperationsHandoff = () => {
const unhealthyLinks = (summary?.linkHealth ?? []).filter((item) => item.status !== 'ok');
const priorityAction = serviceActionQueue[0];
@@ -532,6 +584,24 @@ export function Dashboard({
].filter(Boolean);
copyText(lines.join('\n'), '运营交接摘要');
};
const copyScenarioBlueprint = () => {
const lines = [
'【车辆数据中台功能蓝图】',
'定位32960 / 808 / 宇通 MQTT 都只是车辆服务证据源,最终围绕一辆车提供实时、轨迹、历史、告警、统计能力。',
`高德地图:${amapConfigured ? '已配置,前端只使用运行时配置和安全代理' : '待配置'}`,
`车辆规模:${formatCount(serviceSummary?.totalVehicles)};在线:${formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles)};告警:${formatCount(summary?.issueVehicles)}`,
'',
...scenarioNavigation.map((item, index) => [
`${index + 1}. ${item.title}`,
` 目标:${item.objective}`,
` 证据:${item.evidence}`,
` SLA${item.sla}`
].join('\n')),
'',
`入口:${appURL(buildAppHash({ page: 'dashboard' }))}`
];
copyText(lines.join('\n'), '功能蓝图');
};
const buildDashboardSnapshotRows = () => {
const unhealthyLinks = (summary?.linkHealth ?? []).filter((item) => item.status !== 'ok');
const priorityAction = serviceActionQueue[0];
@@ -707,6 +777,32 @@ export function Dashboard({
))}
</div>
</Card>
<Card
bordered
title={<Space><span></span><Button size="small" onClick={copyScenarioBlueprint}></Button></Space>}
style={{ marginBottom: 16 }}
>
<div className="vp-scenario-grid">
{scenarioNavigation.map((item) => (
<div key={item.title} className="vp-scenario-item">
<div className="vp-scenario-main">
<Typography.Title heading={6} style={{ margin: 0 }}>{item.title}</Typography.Title>
<Typography.Text type="secondary">{item.objective}</Typography.Text>
</div>
<div className="vp-scenario-evidence">{item.evidence}</div>
<Tag color="blue">{item.sla}</Tag>
<Space wrap>
<Button size="small" theme="solid" type="primary" aria-label={`场景导航 ${item.title} ${item.primaryAction}`} onClick={item.onPrimary}>
{item.primaryAction}
</Button>
<Button size="small" theme="light" aria-label={`场景导航 ${item.title} ${item.secondaryAction}`} onClick={item.onSecondary}>
{item.secondaryAction}
</Button>
</Space>
</div>
))}
</div>
</Card>
<Card bordered title="全链路值班闭环" style={{ marginBottom: 16 }}>
<div className="vp-operation-flow">
{workflowSteps.map((item, index) => (

View File

@@ -1160,6 +1160,42 @@ button.vp-realtime-command-item:focus-visible {
line-height: 32px;
}
.vp-scenario-grid {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 12px;
}
.vp-scenario-item {
min-height: 220px;
padding: 14px;
border: 1px solid var(--vp-border);
border-radius: var(--vp-radius);
background: #fbfcff;
display: grid;
gap: 12px;
align-content: space-between;
}
.vp-scenario-main {
display: grid;
gap: 8px;
}
.vp-scenario-main .semi-typography {
line-height: 20px;
}
.vp-scenario-evidence {
min-height: 42px;
padding: 10px;
border-radius: 6px;
background: rgba(22, 100, 255, 0.06);
color: var(--vp-text);
font-size: 12px;
line-height: 18px;
}
.vp-operation-step {
min-height: 176px;
padding: 14px;
@@ -1317,6 +1353,7 @@ button.vp-realtime-command-item:focus-visible {
.vp-realtime-command-board,
.vp-realtime-command-grid,
.vp-workbench-grid,
.vp-scenario-grid,
.vp-operation-flow,
.vp-service-model-grid,
.vp-capability-grid,

View File

@@ -408,6 +408,11 @@ test('dashboard presents one vehicle service operating posture', async () => {
test('dashboard exposes vehicle data center capability matrix', async () => {
window.history.replaceState(null, '', '/#/dashboard');
const writeText = vi.fn(() => Promise.resolve());
Object.defineProperty(navigator, 'clipboard', {
configurable: true,
value: { writeText }
});
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
const path = String(input);
if (path.includes('/api/ops/health')) {
@@ -508,6 +513,7 @@ test('dashboard exposes vehicle data center capability matrix', async () => {
await renderDashboard();
expect(screen.getByText('车辆服务作业台')).toBeInTheDocument();
expect(screen.getByText('车联网场景导航')).toBeInTheDocument();
expect(screen.getAllByText('实时地图').length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText('实时监控').length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText('轨迹回放').length).toBeGreaterThanOrEqual(1);
@@ -525,11 +531,31 @@ test('dashboard exposes vehicle data center capability matrix', async () => {
expect(screen.getAllByText('今日帧 1,286,320').length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText('告警 7').length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText('统计车辆 1,033').length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('确认车辆是否在线、位置是否可信、三类来源是否齐全。')).toBeInTheDocument();
expect(screen.getByText('按车辆复盘位置、速度、里程和断点,定位平台转发或车辆异常。')).toBeInTheDocument();
expect(screen.getByText('优先返回必要字段,避免大 JSON 拖慢查询')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '复制功能蓝图' }));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【车辆数据中台功能蓝图】'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('32960 / 808 / 宇通 MQTT 都只是车辆服务证据源'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆规模1,033在线208告警7'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('1. 实时监控'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('SLA目标 0-1 秒内进入实时视图'));
fireEvent.click(screen.getByRole('button', { name: '作业台 实时监控 打开实时地图' }));
expect(window.location.hash).toBe('#/map?online=online');
cleanup();
await renderDashboard();
fireEvent.click(screen.getByRole('button', { name: '场景导航 实时监控 实时监控' }));
expect(window.location.hash).toBe('#/realtime?online=online');
cleanup();
await renderDashboard();
fireEvent.click(screen.getByRole('button', { name: '场景导航 轨迹回放 RAW证据' }));
expect(window.location.hash.startsWith('#/history-query')).toBe(true);
expect(new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('includeFields')).toBe('true');
cleanup();
await renderDashboard();
fireEvent.click(screen.getByRole('button', { name: '能力入口 实时监控' }));
expect(window.location.hash).toBe('#/realtime?online=online');