feat(platform): add vehicle service decision board

This commit is contained in:
lingniu
2026-07-05 02:36:59 +08:00
parent e8d745949e
commit 7bef3c7e33
3 changed files with 131 additions and 3 deletions

View File

@@ -599,6 +599,53 @@ export function Vehicles({
onClick: exportVehicles
}
], [exportVehicles, filters.keyword, onOpenHistory, onOpenMap, onOpenMileage, onOpenRealtime, rows.length, serviceScopeFilters, summary?.onlineVehicles]);
const primaryAction = actionQueue[0];
const vehicleDecisionItems = [
{
title: '在线车辆',
value: `${(summary?.onlineVehicles ?? 0).toLocaleString()}`,
detail: '先看当前仍在上报的车辆,直接进入地图和实时状态。',
color: 'green' as const,
primaryAction: '实时监控',
secondaryAction: '车辆地图',
onPrimary: () => onOpenRealtime?.({ ...serviceScopeFilters, online: 'online' }),
onSecondary: () => onOpenMap?.({ ...serviceScopeFilters, online: 'online' }),
disabled: !onOpenRealtime
},
{
title: '异常车辆',
value: `${(summary?.noDataVehicles ?? 0).toLocaleString()} 无来源`,
detail: primaryAction ? primaryAction.detail : '当前没有必须立即处理的车辆服务事项。',
color: (summary?.noDataVehicles ?? 0) > 0 || primaryAction ? 'orange' as const : 'green' as const,
primaryAction: primaryAction?.label ?? '查看结果',
secondaryAction: '告警事件',
onPrimary: () => primaryAction ? applyFilters({ ...filters, ...primaryAction.filters }) : applyFilters(filters),
onSecondary: () => onOpenQuality?.(serviceScopeFilters),
disabled: !primaryAction
},
{
title: '单车服务',
value: filters.keyword?.trim() || '输入车辆',
detail: filters.keyword?.trim() ? '当前已进入单车范围,可查看车辆详情、轨迹和里程。' : '输入 VIN、车牌或手机号后进入完整车辆服务。',
color: filters.keyword?.trim() ? 'blue' as const : 'grey' as const,
primaryAction: '车辆详情',
secondaryAction: '轨迹回放',
onPrimary: () => filters.keyword?.trim() && onOpenVehicle(filters.keyword.trim(), filters.protocol),
onSecondary: () => onOpenHistory?.({ ...serviceScopeFilters, tab: 'location' }),
disabled: !filters.keyword?.trim()
},
{
title: '数据交付',
value: `${rows.length.toLocaleString()} 当前页`,
detail: '导出当前车辆清单继续进入历史页交付轨迹、RAW 和字段证据。',
color: rows.length > 0 ? 'blue' as const : 'grey' as const,
primaryAction: '导出清单',
secondaryAction: '历史导出',
onPrimary: exportVehicles,
onSecondary: () => onOpenHistory?.({ ...serviceScopeFilters, tab: 'raw', includeFields: 'true' }),
disabled: rows.length === 0
}
];
useEffect(() => {
setFilters(initialFilters);
@@ -742,6 +789,41 @@ export function Vehicles({
</div>
</div>
</Card>
<Card bordered title="车辆服务决策板" style={{ marginTop: 16 }}>
<div className="vp-vehicle-decision-grid">
{vehicleDecisionItems.map((item) => (
<div key={item.title} className="vp-vehicle-decision-item">
<div className="vp-vehicle-decision-head">
<Tag color={item.color}>{item.title}</Tag>
<strong>{item.value}</strong>
</div>
<span>{item.detail}</span>
<Space wrap>
<Button
size="small"
theme="solid"
type="primary"
disabled={item.disabled}
aria-label={`车辆服务决策 ${item.title} ${item.primaryAction}`}
onClick={item.onPrimary}
>
{item.primaryAction}
</Button>
<Button
size="small"
theme="light"
type="primary"
disabled={item.title === '单车服务' && item.disabled}
aria-label={`车辆服务决策 ${item.title} ${item.secondaryAction}`}
onClick={item.onSecondary}
>
{item.secondaryAction}
</Button>
</Space>
</div>
))}
</div>
</Card>
<Card bordered>
<Form key={JSON.stringify(filters)} initValues={filters} layout="horizontal" onSubmit={(values) => {
const nextFilters = values as Record<string, string>;

View File

@@ -874,6 +874,43 @@ button.vp-realtime-command-item:focus-visible {
line-height: 20px;
}
.vp-vehicle-decision-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
}
.vp-vehicle-decision-item {
min-height: 172px;
padding: 14px;
border: 1px solid var(--vp-border);
border-radius: 8px;
background: #fbfcff;
display: grid;
gap: 10px;
align-content: start;
}
.vp-vehicle-decision-head {
display: grid;
gap: 8px;
min-width: 0;
}
.vp-vehicle-decision-head strong {
color: var(--vp-text);
font-size: 20px;
line-height: 26px;
font-weight: 700;
word-break: break-word;
}
.vp-vehicle-decision-item span {
color: var(--vp-text-muted);
font-size: 13px;
line-height: 20px;
}
.vp-result-summary-item {
min-height: 72px;
padding: 12px;
@@ -3456,6 +3493,7 @@ button.vp-realtime-command-item:focus-visible {
.vp-current-service-grid,
.vp-vehicle-service-desk,
.vp-vehicle-service-grid,
.vp-vehicle-decision-grid,
.vp-realtime-command-board,
.vp-realtime-command-grid,
.vp-realtime-impact-board,

View File

@@ -1979,7 +1979,15 @@ test('shows vehicle service result summary on vehicle list filters', async () =>
expect(await screen.findByText('当前车辆结果')).toBeInTheDocument();
expect(screen.getByText('车辆服务台')).toBeInTheDocument();
expect(screen.getByText('客户视角')).toBeInTheDocument();
expect(screen.getByText('车辆地图')).toBeInTheDocument();
expect(screen.getByText('车辆服务决策板')).toBeInTheDocument();
expect(screen.getAllByText('异常车辆').length).toBeGreaterThan(0);
expect(screen.getByText('4 无来源')).toBeInTheDocument();
expect(screen.getByText('单车服务')).toBeInTheDocument();
expect(screen.getByText('输入车辆')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆服务决策 在线车辆 实时监控' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆服务决策 异常车辆 维护身份绑定' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆服务决策 数据交付 导出清单' })).toBeInTheDocument();
expect(screen.getAllByText('车辆地图').length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText('里程统计').length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText('数据导出').length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('当前页服务判读')).toBeInTheDocument();
@@ -1990,7 +1998,7 @@ test('shows vehicle service result summary on vehicle list filters', async () =>
expect(screen.getAllByText('181').length).toBeGreaterThanOrEqual(2);
expect(screen.getByText('过滤车辆')).toBeInTheDocument();
expect(screen.getByText('73')).toBeInTheDocument();
expect(screen.getByText('在线车辆')).toBeInTheDocument();
expect(screen.getAllByText('在线车辆').length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('单源车辆')).toBeInTheDocument();
expect(screen.getByText('暂无来源车辆')).toBeInTheDocument();
expect(screen.getByText('待绑定')).toBeInTheDocument();
@@ -1999,7 +2007,7 @@ test('shows vehicle service result summary on vehicle list filters', async () =>
expect(screen.getByText('处置队列')).toBeInTheDocument();
expect(screen.getAllByText('P0').length).toBeGreaterThanOrEqual(2);
expect(screen.getAllByText('P1').length).toBeGreaterThanOrEqual(2);
expect(screen.getByText('已有数据但无法归并到 VIN先补绑定再看跨来源服务。')).toBeInTheDocument();
expect(screen.getAllByText('已有数据但无法归并到 VIN先补绑定再看跨来源服务。').length).toBeGreaterThan(0);
expect(screen.getByText('车辆没有任何来源证据,优先确认平台转发、端口和订阅配置。')).toBeInTheDocument();
expect(screen.getByText('缺手机号会影响车辆档案检索、绑定确认和运营侧筛选。')).toBeInTheDocument();
expect(screen.getByText('YUTONG_MQTT 来源缺失会降低跨来源定位、里程和实时判断可信度。')).toBeInTheDocument();