feat(platform): add vehicle scenario navigation
This commit is contained in:
@@ -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) => (
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user