feat(platform): add vehicle service task board
This commit is contained in:
@@ -445,6 +445,67 @@ export function Dashboard({
|
||||
onClick: () => onOpenHistory()
|
||||
}
|
||||
];
|
||||
const taskTimeMonitorSummary = (() => {
|
||||
const vehicle = String(timeMonitorFilters.keyword ?? '').trim() || '全部车辆';
|
||||
const source = String(timeMonitorFilters.protocol ?? '').trim() || '全部数据通道';
|
||||
const dateFrom = String(timeMonitorFilters.dateFrom ?? '').trim();
|
||||
const dateTo = String(timeMonitorFilters.dateTo ?? '').trim();
|
||||
const range = dateFrom || dateTo ? `${dateFrom || '-'} 至 ${dateTo || '-'}` : '全部时间';
|
||||
return `${vehicle} / ${source} / ${range}`;
|
||||
})();
|
||||
const customerTaskBoard = [
|
||||
{
|
||||
title: '先看全域在线',
|
||||
value: `${formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles)} 在线`,
|
||||
detail: `地图有效定位 ${commandLocatedCount.toLocaleString()} 辆,优先确认车辆是否仍在上报。`,
|
||||
color: 'green' as const,
|
||||
primaryAction: '打开实时地图',
|
||||
secondaryAction: '查看在线车辆',
|
||||
onPrimary: () => onOpenMap({ online: 'online' }),
|
||||
onSecondary: () => onOpenRealtime({ online: 'online' })
|
||||
},
|
||||
{
|
||||
title: '再看重点车辆',
|
||||
value: focusVehicle?.label ?? '暂无重点车辆',
|
||||
detail: focusVehicle ? `${focusVehicle.reason},${focusVehicle.realtimeEvidence}` : '当前没有高优先级车辆,保持日常巡检。',
|
||||
color: focusVehicle?.statusColor ?? 'green' as const,
|
||||
primaryAction: '车辆服务',
|
||||
secondaryAction: '复制处置卡',
|
||||
onPrimary: () => focusVehicle?.lookupKey && onOpenVehicle(focusVehicle.lookupKey, focusVehicle.protocol),
|
||||
onSecondary: () => copyFocusVehicleService(),
|
||||
disabled: !focusVehicle?.lookupKey
|
||||
},
|
||||
{
|
||||
title: '按时间窗复盘',
|
||||
value: taskTimeMonitorSummary,
|
||||
detail: '同一时间窗贯穿轨迹、里程、RAW 和告警,不需要跨页面重复输入。',
|
||||
color: 'blue' as const,
|
||||
primaryAction: '轨迹回放',
|
||||
secondaryAction: '里程统计',
|
||||
onPrimary: () => openTimeMonitorHistory(),
|
||||
onSecondary: () => openTimeMonitorMileage()
|
||||
},
|
||||
{
|
||||
title: '导出证据数据',
|
||||
value: `${formatCount(summary?.frameToday)} 今日数据`,
|
||||
detail: '面向 BI、客户复盘和异常定位,按车辆、时间、字段裁剪导出。',
|
||||
color: 'blue' as const,
|
||||
primaryAction: '历史导出',
|
||||
secondaryAction: '导出概览',
|
||||
onPrimary: () => openTimeMonitorRaw(),
|
||||
onSecondary: () => exportDashboardSnapshot()
|
||||
},
|
||||
{
|
||||
title: '闭环告警通知',
|
||||
value: `${formatCount(summary?.issueVehicles)} 告警车辆`,
|
||||
detail: highPriorityIssue ? `${qualityIssueLabel(highPriorityIssue.issueType)} / ${priorityIssueVehicleLabel(highPriorityIssue)}` : '暂无高优先级告警。',
|
||||
color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const,
|
||||
primaryAction: '告警事件',
|
||||
secondaryAction: '复制交接',
|
||||
onPrimary: () => onOpenQuality(highPriorityIssue?.issueType ? { issueType: highPriorityIssue.issueType } : {}),
|
||||
onSecondary: () => copyOperationsHandoff()
|
||||
}
|
||||
];
|
||||
const capabilities = [
|
||||
{
|
||||
title: '实时地图',
|
||||
@@ -1039,6 +1100,41 @@ export function Dashboard({
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
<Card bordered title="今日车辆服务任务板" style={{ marginBottom: 16 }}>
|
||||
<div className="vp-customer-task-grid">
|
||||
{customerTaskBoard.map((item) => (
|
||||
<div key={item.title} className="vp-customer-task-item">
|
||||
<div className="vp-customer-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.secondaryAction !== '复制处置卡'}
|
||||
aria-label={`今日任务 ${item.title} ${item.secondaryAction}`}
|
||||
onClick={item.onSecondary}
|
||||
>
|
||||
{item.secondaryAction}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
<div className="vp-kpi-grid">
|
||||
{kpis.map((item) => (
|
||||
<Card key={item.label} bordered className="vp-kpi-card" bodyStyle={{ padding: 0 }}>
|
||||
|
||||
@@ -410,6 +410,41 @@ body {
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
.vp-customer-task-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.vp-customer-task-item {
|
||||
min-height: 172px;
|
||||
padding: 14px;
|
||||
border: 1px solid var(--vp-border);
|
||||
border-radius: 8px;
|
||||
background: #fbfcff;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
align-content: space-between;
|
||||
}
|
||||
|
||||
.vp-customer-task-head {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.vp-customer-task-head strong {
|
||||
color: var(--vp-text);
|
||||
font-size: 18px;
|
||||
line-height: 24px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.vp-customer-task-item .semi-typography {
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.vp-time-monitor-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
@@ -3105,6 +3140,7 @@ button.vp-realtime-command-item:focus-visible {
|
||||
.vp-map-ops-readiness,
|
||||
.vp-map-ops-work,
|
||||
.vp-time-monitor-grid,
|
||||
.vp-customer-task-grid,
|
||||
.vp-current-service-board,
|
||||
.vp-current-service-grid,
|
||||
.vp-vehicle-service-desk,
|
||||
|
||||
@@ -791,6 +791,12 @@ test('dashboard exposes vehicle data center capability matrix', async () => {
|
||||
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.getByText('自定义时间监控')).toBeInTheDocument();
|
||||
expect(screen.getByText('应用时间窗')).toBeInTheDocument();
|
||||
@@ -844,6 +850,23 @@ test('dashboard exposes vehicle data center capability matrix', async () => {
|
||||
expect(window.location.hash).toBe('#/map?online=online');
|
||||
cleanup();
|
||||
|
||||
await renderDashboard();
|
||||
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.startsWith('#/mileage')).toBe(true);
|
||||
expect(new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('dateFrom')).toBeTruthy();
|
||||
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();
|
||||
fireEvent.click(screen.getByRole('button', { name: '客户工作流 历史数据导出' }));
|
||||
expect(window.location.hash.startsWith('#/history-query')).toBe(true);
|
||||
@@ -5528,7 +5551,10 @@ test('drills into quality issues by issue type', async () => {
|
||||
|
||||
render(<App />);
|
||||
|
||||
fireEvent.click(await screen.findByRole('button', { name: /主要问题 暂无数据来源 1/ }));
|
||||
const issueButtons = await screen.findAllByRole('button', { name: /主要问题 暂无数据来源 1/ });
|
||||
const issueSummaryButton = issueButtons[issueButtons.length - 1];
|
||||
expect(issueButtons.length).toBeGreaterThanOrEqual(1);
|
||||
fireEvent.click(issueSummaryButton as HTMLElement);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/alert-events?issueType=NO_SOURCE&limit=20&offset=0'), undefined);
|
||||
|
||||
Reference in New Issue
Block a user