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}。 + + + + + +
+
+ {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 () => {