diff --git a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx
index 285bc3b8..85d61e9b 100644
--- a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx
+++ b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx
@@ -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({
))}
+
+
+ {customerTaskBoard.map((item) => (
+
+
+ {item.title}
+ {item.value}
+
+
{item.detail}
+
+
+
+
+
+ ))}
+
+
{kpis.map((item) => (
diff --git a/vehicle-data-platform/apps/web/src/styles/global.css b/vehicle-data-platform/apps/web/src/styles/global.css
index 2d1d6835..a7c3e816 100644
--- a/vehicle-data-platform/apps/web/src/styles/global.css
+++ b/vehicle-data-platform/apps/web/src/styles/global.css
@@ -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,
diff --git a/vehicle-data-platform/apps/web/src/test/App.test.tsx b/vehicle-data-platform/apps/web/src/test/App.test.tsx
index 51c1bafb..780f1f22 100644
--- a/vehicle-data-platform/apps/web/src/test/App.test.tsx
+++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx
@@ -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();
- 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);