feat(platform): add vehicle data flow workbench
This commit is contained in:
@@ -618,6 +618,62 @@ export function Dashboard({
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
const dataFlowStages = [
|
||||||
|
{
|
||||||
|
stage: '01',
|
||||||
|
title: '协议接入',
|
||||||
|
owner: '网关接收',
|
||||||
|
evidence: `32960 / 808 / MQTT,连接 ${formatCount(opsHealth?.activeConnections)}`,
|
||||||
|
detail: '只接收真实实时数据帧和必要鉴权注册帧,协议不是最终产品。',
|
||||||
|
action: '运维质量',
|
||||||
|
onClick: () => onOpenQuality()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stage: '02',
|
||||||
|
title: '统一解析',
|
||||||
|
owner: '字段映射',
|
||||||
|
evidence: `今日帧 ${formatCount(summary?.frameToday)},Kafka Lag ${formatLag(summary?.kafkaLag)}`,
|
||||||
|
detail: '一次解析生成扁平字段、原始证据和车辆身份关联,避免三处重复解析。',
|
||||||
|
action: '历史字段',
|
||||||
|
onClick: () => onOpenHistory({ tab: 'raw', includeFields: 'true' })
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stage: '03',
|
||||||
|
title: '实时投影',
|
||||||
|
owner: 'Redis KV',
|
||||||
|
evidence: `在线 Key ${formatCount(opsHealth?.redisOnlineKeys)},在线车辆 ${formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles)}`,
|
||||||
|
detail: '快速写入车辆最新状态、在线 TTL 和全量协议字段,支撑 0-1 秒监控。',
|
||||||
|
action: '实时监控',
|
||||||
|
onClick: () => onOpenRealtime({ online: 'online' })
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stage: '04',
|
||||||
|
title: '车辆归并',
|
||||||
|
owner: 'Vehicle Service',
|
||||||
|
evidence: `多源 ${formatCount(serviceSummary?.multiSourceVehicles)},单源 ${formatCount(serviceSummary?.singleSourceVehicles)}`,
|
||||||
|
detail: '以 VIN 合并来源证据,车牌、手机号和 OEM 只是车辆服务的检索键。',
|
||||||
|
action: '车辆中心',
|
||||||
|
onClick: () => onOpenVehicles({})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stage: '05',
|
||||||
|
title: '历史证据',
|
||||||
|
owner: 'TDengine / MySQL',
|
||||||
|
evidence: `轨迹 ${formatCount(summary?.activeToday)} 活跃,RAW ${formatCount(summary?.frameToday)} 帧`,
|
||||||
|
detail: '位置、RAW 和解析字段可分页查询,并能回跳轨迹、车辆服务和告警。',
|
||||||
|
action: '轨迹回放',
|
||||||
|
onClick: () => onOpenHistory()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stage: '06',
|
||||||
|
title: '运营闭环',
|
||||||
|
owner: '告警 / 统计',
|
||||||
|
evidence: `告警 ${formatCount(summary?.issueVehicles)},统计车辆 ${formatCount(serviceSummary?.totalVehicles)}`,
|
||||||
|
detail: '断链、缺字段、里程异常和离线时长进入通知、统计和复核工作流。',
|
||||||
|
action: '统计查询',
|
||||||
|
onClick: () => onOpenMileage()
|
||||||
|
}
|
||||||
|
];
|
||||||
const scenarioNavigation = [
|
const scenarioNavigation = [
|
||||||
{
|
{
|
||||||
title: '实时监控',
|
title: '实时监控',
|
||||||
@@ -716,6 +772,28 @@ export function Dashboard({
|
|||||||
];
|
];
|
||||||
copyText(lines.join('\n'), '功能蓝图');
|
copyText(lines.join('\n'), '功能蓝图');
|
||||||
};
|
};
|
||||||
|
const copyDataFlowBlueprint = () => {
|
||||||
|
const lines = [
|
||||||
|
'【车辆数据中台数据流转图】',
|
||||||
|
'原则:32960 / 808 / 宇通 MQTT 是接入证据,最终都归并为同一辆车的车辆服务。',
|
||||||
|
`运行态:连接 ${formatCount(opsHealth?.activeConnections)};在线 Key ${formatCount(opsHealth?.redisOnlineKeys)};Kafka Lag ${formatLag(summary?.kafkaLag)};今日帧 ${formatCount(summary?.frameToday)}`,
|
||||||
|
'',
|
||||||
|
...dataFlowStages.map((item) => [
|
||||||
|
`${item.stage}. ${item.title} / ${item.owner}`,
|
||||||
|
` 证据:${item.evidence}`,
|
||||||
|
` 说明:${item.detail}`
|
||||||
|
].join('\n')),
|
||||||
|
'',
|
||||||
|
`实时监控:${appURL(buildAppHash({ page: 'realtime', filters: { online: 'online' } }))}`,
|
||||||
|
`车辆中心:${appURL(buildAppHash({ page: 'vehicles' }))}`,
|
||||||
|
`轨迹回放:${appURL(buildAppHash({ page: 'history' }))}`,
|
||||||
|
`历史查询:${appURL(buildAppHash({ page: 'history-query', filters: { tab: 'raw', includeFields: 'true' } }))}`,
|
||||||
|
`告警事件:${appURL(buildAppHash({ page: 'alert-events' }))}`,
|
||||||
|
`统计查询:${appURL(buildAppHash({ page: 'mileage' }))}`,
|
||||||
|
`运维质量:${appURL(buildAppHash({ page: 'ops-quality' }))}`
|
||||||
|
];
|
||||||
|
copyText(lines.join('\n'), '数据流转图');
|
||||||
|
};
|
||||||
const copyFocusVehicleService = () => {
|
const copyFocusVehicleService = () => {
|
||||||
if (!focusVehicle) {
|
if (!focusVehicle) {
|
||||||
Toast.warning('当前没有重点车辆服务可复制');
|
Toast.warning('当前没有重点车辆服务可复制');
|
||||||
@@ -976,6 +1054,26 @@ export function Dashboard({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
<Card
|
||||||
|
bordered
|
||||||
|
title={<Space><span>数据流转作业链</span><Button size="small" onClick={copyDataFlowBlueprint}>复制流转图</Button></Space>}
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
>
|
||||||
|
<div className="vp-data-flow-grid">
|
||||||
|
{dataFlowStages.map((item) => (
|
||||||
|
<button key={item.stage} className="vp-data-flow-item" type="button" onClick={item.onClick} aria-label={`数据流转 ${item.title}`}>
|
||||||
|
<div className="vp-data-flow-head">
|
||||||
|
<span>{item.stage}</span>
|
||||||
|
<Tag color="blue">{item.owner}</Tag>
|
||||||
|
</div>
|
||||||
|
<Typography.Title heading={6} style={{ margin: 0 }}>{item.title}</Typography.Title>
|
||||||
|
<div className="vp-data-flow-evidence">{item.evidence}</div>
|
||||||
|
<Typography.Text type="secondary">{item.detail}</Typography.Text>
|
||||||
|
<Tag color="grey">{item.action}</Tag>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
<Card
|
<Card
|
||||||
bordered
|
bordered
|
||||||
title={<Space><span>车联网场景导航</span><Button size="small" onClick={copyScenarioBlueprint}>复制功能蓝图</Button></Space>}
|
title={<Space><span>车联网场景导航</span><Button size="small" onClick={copyScenarioBlueprint}>复制功能蓝图</Button></Space>}
|
||||||
|
|||||||
@@ -1980,6 +1980,64 @@ button.vp-realtime-command-item:focus-visible {
|
|||||||
line-height: 32px;
|
line-height: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vp-data-flow-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-data-flow-item {
|
||||||
|
min-height: 216px;
|
||||||
|
padding: 14px;
|
||||||
|
border: 1px solid var(--vp-border);
|
||||||
|
border-radius: var(--vp-radius);
|
||||||
|
background: #fbfcff;
|
||||||
|
color: inherit;
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
align-content: start;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.16s ease, box-shadow 0.16s ease, background 0.16s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-data-flow-item:hover,
|
||||||
|
.vp-data-flow-item:focus-visible {
|
||||||
|
border-color: rgba(22, 100, 255, 0.42);
|
||||||
|
box-shadow: 0 0 0 3px rgba(22, 100, 255, 0.08);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-data-flow-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-data-flow-head > span {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 7px;
|
||||||
|
background: rgba(22, 100, 255, 0.09);
|
||||||
|
color: #1664ff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-data-flow-evidence {
|
||||||
|
min-height: 54px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: rgba(10, 168, 107, 0.08);
|
||||||
|
color: var(--vp-text);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
.vp-scenario-grid {
|
.vp-scenario-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||||
@@ -2252,6 +2310,7 @@ button.vp-realtime-command-item:focus-visible {
|
|||||||
.vp-scenario-grid,
|
.vp-scenario-grid,
|
||||||
.vp-operation-flow,
|
.vp-operation-flow,
|
||||||
.vp-service-model-grid,
|
.vp-service-model-grid,
|
||||||
|
.vp-data-flow-grid,
|
||||||
.vp-capability-grid,
|
.vp-capability-grid,
|
||||||
.vp-conclusion-grid,
|
.vp-conclusion-grid,
|
||||||
.vp-monitor-layout,
|
.vp-monitor-layout,
|
||||||
|
|||||||
@@ -546,11 +546,18 @@ test('dashboard exposes vehicle data center capability matrix', async () => {
|
|||||||
await renderDashboard();
|
await renderDashboard();
|
||||||
expect(screen.getByText('车辆服务作业台')).toBeInTheDocument();
|
expect(screen.getByText('车辆服务作业台')).toBeInTheDocument();
|
||||||
expect(screen.getByText('地图运营能力')).toBeInTheDocument();
|
expect(screen.getByText('地图运营能力')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('数据流转作业链')).toBeInTheDocument();
|
||||||
expect(screen.getByText('车联网场景导航')).toBeInTheDocument();
|
expect(screen.getByText('车联网场景导航')).toBeInTheDocument();
|
||||||
expect(screen.getByText('高德 Web JS')).toBeInTheDocument();
|
expect(screen.getByText('高德 Web JS')).toBeInTheDocument();
|
||||||
expect(screen.getByText('服务端 API')).toBeInTheDocument();
|
expect(screen.getByText('服务端 API')).toBeInTheDocument();
|
||||||
expect(screen.getByText('安全代理')).toBeInTheDocument();
|
expect(screen.getByText('安全代理')).toBeInTheDocument();
|
||||||
expect(screen.getByText('安全码暴露')).toBeInTheDocument();
|
expect(screen.getByText('安全码暴露')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('协议接入')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('统一解析')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('实时投影')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('车辆归并')).toBeInTheDocument();
|
||||||
|
expect(screen.getAllByText('历史证据').length).toBeGreaterThanOrEqual(1);
|
||||||
|
expect(screen.getByText('运营闭环')).toBeInTheDocument();
|
||||||
expect(screen.getAllByText('已配置').length).toBeGreaterThanOrEqual(2);
|
expect(screen.getAllByText('已配置').length).toBeGreaterThanOrEqual(2);
|
||||||
expect(screen.getByText('已启用')).toBeInTheDocument();
|
expect(screen.getByText('已启用')).toBeInTheDocument();
|
||||||
expect(screen.getByText('未暴露')).toBeInTheDocument();
|
expect(screen.getByText('未暴露')).toBeInTheDocument();
|
||||||
@@ -581,6 +588,9 @@ test('dashboard exposes vehicle data center capability matrix', async () => {
|
|||||||
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('高德地图:Web JS 已配置;服务端 API 已配置;安全代理 已启用;安全码未暴露'));
|
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('高德地图:Web JS 已配置;服务端 API 已配置;安全代理 已启用;安全码未暴露'));
|
||||||
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('1. 实时监控'));
|
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('1. 实时监控'));
|
||||||
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('SLA:目标 0-1 秒内进入实时视图'));
|
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('SLA:目标 0-1 秒内进入实时视图'));
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: '复制流转图' }));
|
||||||
|
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【车辆数据中台数据流转图】'));
|
||||||
|
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('03. 实时投影 / Redis KV'));
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: '地图运营 实时监控' }));
|
fireEvent.click(screen.getByRole('button', { name: '地图运营 实时监控' }));
|
||||||
expect(window.location.hash).toBe('#/map?online=online');
|
expect(window.location.hash).toBe('#/map?online=online');
|
||||||
@@ -592,6 +602,17 @@ test('dashboard exposes vehicle data center capability matrix', async () => {
|
|||||||
expect(new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('includeFields')).toBe('true');
|
expect(new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('includeFields')).toBe('true');
|
||||||
cleanup();
|
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: '数据流转 统一解析' }));
|
||||||
|
expect(window.location.hash.startsWith('#/history-query')).toBe(true);
|
||||||
|
expect(new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('includeFields')).toBe('true');
|
||||||
|
cleanup();
|
||||||
|
|
||||||
await renderDashboard();
|
await renderDashboard();
|
||||||
fireEvent.click(screen.getByRole('button', { name: '作业台 实时监控 打开实时地图' }));
|
fireEvent.click(screen.getByRole('button', { name: '作业台 实时监控 打开实时地图' }));
|
||||||
expect(window.location.hash).toBe('#/map?online=online');
|
expect(window.location.hash).toBe('#/map?online=online');
|
||||||
|
|||||||
Reference in New Issue
Block a user