feat(platform): add single vehicle task board
This commit is contained in:
@@ -786,6 +786,63 @@ export function VehicleDetail({
|
|||||||
];
|
];
|
||||||
copyText(lines.join('\n'), '单车处理清单');
|
copyText(lines.join('\n'), '单车处理清单');
|
||||||
};
|
};
|
||||||
|
const detailTaskBoard = [
|
||||||
|
{
|
||||||
|
title: '看实时位置',
|
||||||
|
value: lastSeen,
|
||||||
|
detail: evidenceSourceCount > 0 ? `${evidenceOnlineSourceCount}/${evidenceSourceCount} 来源在线,先确认车辆当前是否仍在上报。` : '暂无来源在线证据,优先确认数据接入。',
|
||||||
|
color: online ? 'green' as const : 'orange' as const,
|
||||||
|
primaryAction: '实时监控',
|
||||||
|
secondaryAction: '复制摘要',
|
||||||
|
disabled: !hasResolvedVIN,
|
||||||
|
onPrimary: () => onOpenRealtime(resolvedVIN, activeProtocol),
|
||||||
|
onSecondary: () => copyUnifiedVehicleService()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '回放轨迹证据',
|
||||||
|
value: `${formatCompactNumber(overview?.historyCount ?? detail?.history?.total ?? 0)} 条历史`,
|
||||||
|
detail: evidenceLocatedSourceCount > 0 ? `${evidenceLocatedSourceCount} 个位置来源,可回放定位、速度和里程断点。` : '暂无有效位置来源,先检查实时坐标。',
|
||||||
|
color: evidenceLocatedSourceCount > 0 ? 'blue' as const : 'grey' as const,
|
||||||
|
primaryAction: '轨迹回放',
|
||||||
|
secondaryAction: '处理清单',
|
||||||
|
disabled: !hasResolvedVIN,
|
||||||
|
onPrimary: () => onOpenHistory(resolvedVIN, activeProtocol),
|
||||||
|
onSecondary: () => copyVehicleRunbook()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '导出原始证据',
|
||||||
|
value: `${formatCompactNumber(overview?.rawCount ?? detail?.raw?.total ?? 0)} 帧`,
|
||||||
|
detail: latestRaw?.frameType ? `最新原始记录类型 ${latestRaw.frameType},用于核对解析字段。` : '按车辆和来源导出 RAW 与解析字段。',
|
||||||
|
color: (overview?.rawCount ?? detail?.raw?.total ?? 0) > 0 ? 'blue' as const : 'grey' as const,
|
||||||
|
primaryAction: '数据导出',
|
||||||
|
secondaryAction: '导出档案',
|
||||||
|
disabled: !hasResolvedVIN,
|
||||||
|
onPrimary: () => onOpenRaw(resolvedVIN, activeProtocol),
|
||||||
|
onSecondary: () => exportVehicleReport()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '复核里程统计',
|
||||||
|
value: `${formatCompactNumber(overview?.mileageCount ?? detail?.mileage?.total ?? 0)} 条统计`,
|
||||||
|
detail: `来源里程差 ${formatCompactNumber(evidenceMileageDelta, ' km')},确认区间和日统计能闭合。`,
|
||||||
|
color: isFiniteNumber(evidenceMileageDelta) && Math.abs(evidenceMileageDelta) > 1 ? 'orange' as const : 'green' as const,
|
||||||
|
primaryAction: '里程统计',
|
||||||
|
secondaryAction: '诊断摘要',
|
||||||
|
disabled: !hasResolvedVIN,
|
||||||
|
onPrimary: () => onOpenMileage(resolvedVIN, activeProtocol),
|
||||||
|
onSecondary: () => copyVehicleDiagnosticSummary()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '闭环告警事件',
|
||||||
|
value: `${qualityCount.toLocaleString()} 项`,
|
||||||
|
detail: priorityQualityIssue ? `${qualityIssueLabel(priorityQualityIssue.issueType)} / ${priorityQualityIssue.lastSeen || '-'}` : '暂无高优先级告警,保持观察。',
|
||||||
|
color: qualityCount > 0 ? 'orange' as const : 'green' as const,
|
||||||
|
primaryAction: '告警事件',
|
||||||
|
secondaryAction: '运营摘要',
|
||||||
|
disabled: !onOpenQuality || (!hasResolvedVIN && qualityCount <= 0),
|
||||||
|
onPrimary: () => onOpenQuality?.(qualityFiltersForCurrentVehicle()),
|
||||||
|
onSecondary: () => copyVehicleOperationsSummary()
|
||||||
|
}
|
||||||
|
];
|
||||||
const qualityTable = (
|
const qualityTable = (
|
||||||
<Table
|
<Table
|
||||||
loading={loading}
|
loading={loading}
|
||||||
@@ -1007,6 +1064,42 @@ export function VehicleDetail({
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<Card bordered title="今日单车服务任务板" style={{ marginTop: 16 }}>
|
||||||
|
<div className="vp-vehicle-task-grid">
|
||||||
|
{detailTaskBoard.map((item) => (
|
||||||
|
<div key={item.title} className="vp-vehicle-task-item">
|
||||||
|
<div className="vp-vehicle-task-head">
|
||||||
|
<Tag color={item.color}>{item.title}</Tag>
|
||||||
|
<strong>{item.value}</strong>
|
||||||
|
</div>
|
||||||
|
<Typography.Text type="secondary">{item.detail}</Typography.Text>
|
||||||
|
<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.disabled && item.title !== '闭环告警事件'}
|
||||||
|
aria-label={`单车任务 ${item.title} ${item.secondaryAction}`}
|
||||||
|
onClick={item.onSecondary}
|
||||||
|
>
|
||||||
|
{item.secondaryAction}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Card bordered title="单车运营工作台" style={{ marginTop: 16 }}>
|
<Card bordered title="单车运营工作台" style={{ marginTop: 16 }}>
|
||||||
<div className="vp-workbench-grid">
|
<div className="vp-workbench-grid">
|
||||||
{detailWorkbench.map((item) => (
|
{detailWorkbench.map((item) => (
|
||||||
|
|||||||
@@ -2472,6 +2472,41 @@ button.vp-realtime-command-item:focus-visible {
|
|||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vp-vehicle-task-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-vehicle-task-item {
|
||||||
|
min-height: 172px;
|
||||||
|
padding: 14px;
|
||||||
|
border: 1px solid var(--vp-border);
|
||||||
|
border-radius: var(--vp-radius);
|
||||||
|
background: #fbfcff;
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
align-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-vehicle-task-head {
|
||||||
|
min-width: 0;
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-vehicle-task-head strong {
|
||||||
|
color: var(--vp-text);
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 24px;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-vehicle-task-item .semi-typography {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.vp-evidence-grid {
|
.vp-evidence-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||||
@@ -3134,6 +3169,7 @@ button.vp-realtime-command-item:focus-visible {
|
|||||||
.vp-unified-service-grid,
|
.vp-unified-service-grid,
|
||||||
.vp-service-dossier,
|
.vp-service-dossier,
|
||||||
.vp-service-dossier-assets,
|
.vp-service-dossier-assets,
|
||||||
|
.vp-vehicle-task-grid,
|
||||||
.vp-source-readiness-grid,
|
.vp-source-readiness-grid,
|
||||||
.vp-source-readiness-metrics,
|
.vp-source-readiness-metrics,
|
||||||
.vp-map-ops-board,
|
.vp-map-ops-board,
|
||||||
|
|||||||
@@ -10896,6 +10896,12 @@ test('shows unified service overview on vehicle detail', async () => {
|
|||||||
expect(await screen.findByText('车辆服务概览')).toBeInTheDocument();
|
expect(await screen.findByText('车辆服务概览')).toBeInTheDocument();
|
||||||
expect(screen.getByText('车辆服务档案总览')).toBeInTheDocument();
|
expect(screen.getByText('车辆服务档案总览')).toBeInTheDocument();
|
||||||
expect(screen.getByRole('button', { name: /复制档案/ })).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: /复制档案/ })).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.getByText('单车服务处理清单')).toBeInTheDocument();
|
expect(screen.getByText('单车服务处理清单')).toBeInTheDocument();
|
||||||
expect(screen.getAllByText('档案完整度').length).toBeGreaterThanOrEqual(1);
|
expect(screen.getAllByText('档案完整度').length).toBeGreaterThanOrEqual(1);
|
||||||
@@ -10916,12 +10922,17 @@ test('shows unified service overview on vehicle detail', async () => {
|
|||||||
expect(screen.getByText('复核统计口径')).toBeInTheDocument();
|
expect(screen.getByText('复核统计口径')).toBeInTheDocument();
|
||||||
expect(screen.getByText('车辆在线状态、最新时间、有效坐标均可解释。')).toBeInTheDocument();
|
expect(screen.getByText('车辆在线状态、最新时间、有效坐标均可解释。')).toBeInTheDocument();
|
||||||
expect(screen.getByText('2026-07-03 20:12:10')).toBeInTheDocument();
|
expect(screen.getByText('2026-07-03 20:12:10')).toBeInTheDocument();
|
||||||
expect(screen.getByText('12 条历史')).toBeInTheDocument();
|
expect(screen.getAllByText('12 条历史').length).toBeGreaterThanOrEqual(1);
|
||||||
expect(screen.getByText('34 帧')).toBeInTheDocument();
|
expect(screen.getAllByText('34 帧').length).toBeGreaterThanOrEqual(1);
|
||||||
expect(screen.getAllByText('1 项').length).toBeGreaterThan(0);
|
expect(screen.getAllByText('1 项').length).toBeGreaterThan(0);
|
||||||
expect(screen.getByText('7 条统计')).toBeInTheDocument();
|
expect(screen.getAllByText('7 条统计').length).toBeGreaterThanOrEqual(1);
|
||||||
expect(screen.getByText('2/3')).toBeInTheDocument();
|
expect(screen.getByText('2/3')).toBeInTheDocument();
|
||||||
expect(screen.getByText('轨迹 12 / 原始记录 34 / 里程 7')).toBeInTheDocument();
|
expect(screen.getByText('轨迹 12 / 原始记录 34 / 里程 7')).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('button', { name: '单车任务 看实时位置 实时监控' })).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('button', { name: '单车任务 回放轨迹证据 轨迹回放' })).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('button', { name: '单车任务 导出原始证据 数据导出' })).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('button', { name: '单车任务 复核里程统计 里程统计' })).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('button', { name: '单车任务 闭环告警事件 告警事件' })).toBeInTheDocument();
|
||||||
fireEvent.click(screen.getByRole('button', { name: '单车运营工作台 原始记录 查看原始记录' }));
|
fireEvent.click(screen.getByRole('button', { name: '单车运营工作台 原始记录 查看原始记录' }));
|
||||||
expect(window.location.hash).toBe('#/history-query?keyword=VIN001&tab=raw');
|
expect(window.location.hash).toBe('#/history-query?keyword=VIN001&tab=raw');
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user