diff --git a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx
index a1498900..27e7770c 100644
--- a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx
+++ b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx
@@ -1,4 +1,5 @@
import { Button, Card, Col, Form, Row, Select, Space, Spin, Table, Tag, Toast, Typography } from '@douyinfe/semi-ui';
+import { IconCopy } from '@douyinfe/semi-icons';
import { useEffect, useMemo, useState } from 'react';
import { api } from '../api/client';
import type { DashboardSummary, OpsHealth, QualityIssueRow, VehicleCoverageRow, VehicleRealtimeRow, VehicleServiceSummary } from '../api/types';
@@ -1186,6 +1187,58 @@ export function Dashboard({
onClick: () => copyTimeMonitorReviewPackage()
}
];
+ const copyTimeWindowSlaJob = () => {
+ const scope = timeMonitorScope();
+ const rawFilters = { ...scope, tab: 'raw', includeFields: 'true' };
+ const lines = [
+ '【客户时间窗复盘作业单】',
+ `作业范围:${timeMonitorSummary}`,
+ `交付SLA:${timeMonitorWindowLabel}内完成轨迹复盘、统计核对、历史导出和告警说明`,
+ `交付状态:${timeMonitorDeliveryStatus.label};${timeMonitorDeliveryStatus.detail}`,
+ '交付物:轨迹回放链接 / 统计查询链接 / 历史导出链接 / 告警复盘链接',
+ `升级条件:${formatCount(summary?.issueVehicles)} 告警车辆需要先进入告警复盘`,
+ `责任动作:${timeMonitorHasAlerts ? '先处理告警,再交付证据' : '先完成客户复盘,再按需导出证据'}`,
+ `轨迹回放:${appURL(buildAppHash({ page: 'history', keyword: scope.keyword, protocol: scope.protocol, filters: scope }))}`,
+ `统计查询:${appURL(buildAppHash({ page: 'mileage', keyword: scope.keyword, protocol: scope.protocol, filters: scope }))}`,
+ `历史导出:${appURL(buildAppHash({ page: 'history-query', keyword: scope.keyword, protocol: scope.protocol, filters: rawFilters }))}`,
+ `告警复盘:${appURL(buildAppHash({ page: 'alert-events', keyword: scope.keyword, protocol: scope.protocol, filters: scope }))}`
+ ];
+ copyText(lines.join('\n'), '时间窗作业单');
+ };
+ const timeWindowSlaItems = [
+ {
+ label: '责任动作',
+ value: '客户复盘',
+ detail: timeMonitorHasAlerts ? '存在告警车辆,先完成告警说明和通知闭环。' : '围绕同一车辆和时间窗完成客户复盘。',
+ action: '复制作业',
+ color: timeMonitorHasAlerts ? 'orange' as const : 'blue' as const,
+ onClick: copyTimeWindowSlaJob
+ },
+ {
+ label: '交付时限',
+ value: timeMonitorWindowLabel,
+ detail: '在该时间窗内完成轨迹、统计、历史证据和异常说明。',
+ action: '轨迹复盘',
+ color: timeMonitorHasRange ? 'green' as const : 'orange' as const,
+ onClick: openTimeMonitorHistory
+ },
+ {
+ label: '交付物',
+ value: '轨迹/统计/导出/告警',
+ detail: '交付轨迹回放、统计查询、历史导出和告警复盘四个入口。',
+ action: '导出证据',
+ color: timeMonitorHasHistory ? 'blue' as const : 'orange' as const,
+ onClick: openTimeMonitorRaw
+ },
+ {
+ label: '升级条件',
+ value: `${formatCount(summary?.issueVehicles)} 告警`,
+ detail: timeMonitorHasAlerts ? '存在告警车辆,交付前先进入告警复盘。' : '无高风险时按常规复盘交付。',
+ action: '告警复盘',
+ color: timeMonitorHasAlerts ? 'orange' as const : 'green' as const,
+ onClick: openTimeMonitorAlerts
+ }
+ ];
const dedicatedTimeMonitorActions = [
{
label: '轨迹回放',
@@ -2830,6 +2883,42 @@ export function Dashboard({
))}
+
+
+
+ 时间窗复盘 SLA 作业台
+ {timeMonitorDeliveryStatus.label}
+ {timeMonitorHasAlerts ? `${formatCount(summary?.issueVehicles)} 告警` : '无高风险'}
+
+
+ 把一个时间窗变成可交付作业:谁处理、多久交付、交付哪些证据、异常如何升级。
+
+
+ 作业范围:{timeMonitorSummary};SLA:{timeMonitorWindowLabel}。
+
+
+ } onClick={copyTimeWindowSlaJob}>
+ 复制时间窗作业单
+
+
+
+
+
+ {timeWindowSlaItems.map((item) => (
+
+ ))}
+
+
) : null}
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 02b0f6c3..6599eef2 100644
--- a/vehicle-data-platform/apps/web/src/test/App.test.tsx
+++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx
@@ -1662,6 +1662,11 @@ test('dashboard guides customer custom time window service path', async () => {
test('time monitor route exposes a dedicated customer time window workspace', async () => {
window.history.replaceState(null, '', '/#/time-monitor?keyword=%E7%B2%A4A%E6%97%B6%E9%97%B4%E7%AA%97&protocol=JT808&dateFrom=2026-07-01&dateTo=2026-07-03');
+ const writeText = vi.fn(() => Promise.resolve());
+ Object.defineProperty(navigator, 'clipboard', {
+ configurable: true,
+ value: { writeText }
+ });
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
const path = String(input);
if (path.includes('/api/ops/health')) {
@@ -1743,6 +1748,21 @@ test('time monitor route exposes a dedicated customer time window workspace', as
expect(screen.getByRole('button', { name: '时间窗监控入口 统计查询 3 天窗口' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '时间窗监控入口 历史导出 9,988 帧' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '时间窗监控入口 告警复盘 2 告警' })).toBeInTheDocument();
+ expect(screen.getByText('时间窗复盘 SLA 作业台')).toBeInTheDocument();
+ expect(screen.getByText('把一个时间窗变成可交付作业:谁处理、多久交付、交付哪些证据、异常如何升级。')).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: '时间窗复盘SLA 责任动作 客户复盘 复制作业' })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: '时间窗复盘SLA 交付时限 3 天窗口 轨迹复盘' })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: '时间窗复盘SLA 交付物 轨迹/统计/导出/告警 导出证据' })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: '时间窗复盘SLA 升级条件 2 告警 告警复盘' })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: /复制时间窗作业单/ })).toBeInTheDocument();
+ fireEvent.click(screen.getByRole('button', { name: /复制时间窗作业单/ }));
+ await waitFor(() => {
+ expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【客户时间窗复盘作业单】'));
+ });
+ expect(writeText).toHaveBeenCalledWith(expect.stringContaining('作业范围:粤A时间窗 / JT808 / 2026-07-01 至 2026-07-03'));
+ expect(writeText).toHaveBeenCalledWith(expect.stringContaining('交付SLA:3 天窗口内完成轨迹复盘、统计核对、历史导出和告警说明'));
+ expect(writeText).toHaveBeenCalledWith(expect.stringContaining('交付物:轨迹回放链接 / 统计查询链接 / 历史导出链接 / 告警复盘链接'));
+ expect(writeText).toHaveBeenCalledWith(expect.stringContaining('升级条件:2 告警车辆需要先进入告警复盘'));
});
test('dashboard exposes vehicle data center capability matrix', async () => {