diff --git a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx
index 9c289dc2..c2d1ac24 100644
--- a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx
+++ b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx
@@ -881,12 +881,50 @@ export function Dashboard({
color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const
}
];
+ const timeMonitorHasRange = Boolean(timeMonitorScopeValue.dateFrom && timeMonitorScopeValue.dateTo);
+ const timeMonitorHasTrajectory = (summary?.activeToday ?? 0) > 0;
+ const timeMonitorHasHistory = (summary?.frameToday ?? 0) > 0;
+ const timeMonitorHasAlerts = (summary?.issueVehicles ?? 0) > 0;
+ const timeMonitorDeliveryStatus = !timeMonitorHasRange
+ ? { label: '待补时间', color: 'orange' as const, detail: '开始和结束时间不完整,交付前先锁定复盘窗口。' }
+ : !timeMonitorHasHistory
+ ? { label: '证据不足', color: 'red' as const, detail: '当前没有历史证据,无法支撑客户复盘或导出。' }
+ : timeMonitorHasAlerts
+ ? { label: '带风险交付', color: 'orange' as const, detail: '存在告警车辆,交付时需要附带告警闭环和原始证据。' }
+ : { label: '可交付', color: 'green' as const, detail: '时间窗、轨迹、历史证据和统计入口已准备好。' };
+ const timeMonitorDeliveryActions = [
+ {
+ label: '客户结论',
+ value: timeMonitorDeliveryStatus.label,
+ detail: timeMonitorDeliveryStatus.detail,
+ action: '复制复盘包',
+ color: timeMonitorDeliveryStatus.color,
+ onClick: () => copyTimeMonitorReviewPackage()
+ },
+ {
+ label: '证据准备',
+ value: timeMonitorHasTrajectory && timeMonitorHasHistory ? '证据可用' : '需要补证',
+ detail: `轨迹 ${formatCount(summary?.activeToday)} 活跃 / 历史 ${formatCount(summary?.frameToday)} 帧。`,
+ action: timeMonitorHasHistory ? '历史导出' : '查历史',
+ color: timeMonitorHasHistory ? 'blue' as const : 'orange' as const,
+ onClick: openTimeMonitorRaw
+ },
+ {
+ label: '异常说明',
+ value: timeMonitorHasAlerts ? `${formatCount(summary?.issueVehicles)} 告警` : '无高风险',
+ detail: highPriorityIssue ? `${qualityIssueLabel(highPriorityIssue.issueType)} / ${priorityIssueVehicleLabel(highPriorityIssue)}` : '当前没有需要额外说明的高优先级告警。',
+ action: '告警复盘',
+ color: timeMonitorHasAlerts ? 'orange' as const : 'green' as const,
+ onClick: openTimeMonitorAlerts
+ }
+ ];
const copyTimeMonitorReviewPackage = () => {
const scope = timeMonitorScope();
const rawFilters = { ...scope, tab: 'raw', includeFields: 'true' };
const lines = [
'【客户时间窗复盘包】',
`范围:${timeMonitorSummary}`,
+ `交付结论:${timeMonitorDeliveryStatus.label};${timeMonitorDeliveryStatus.detail}`,
`时间窗判定:${dayRangeText(scope.dateFrom, scope.dateTo)}`,
'平台能力:实时地图 / 轨迹回放 / 里程统计 / 历史数据查询 / 告警通知',
`车辆范围:${scope.keyword || '全部车辆'}`,
@@ -898,6 +936,8 @@ export function Dashboard({
`告警事件:${formatCount(summary?.issueVehicles)} 辆车存在告警`,
'交付核对:',
...timeMonitorChecklist.map((item, index) => `${index + 1}. ${item.label}:${item.value};${item.detail}`),
+ '下一步动作:',
+ ...timeMonitorDeliveryActions.map((item, index) => `${index + 1}. ${item.label}:${item.value};${item.detail}`),
`轨迹回放:${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 }))}`,
@@ -1618,6 +1658,32 @@ export function Dashboard({
))}
+
+
+
+ {timeMonitorDeliveryStatus.label}
+ {timeMonitorScopeValue.keyword ? '单车时间窗' : '车辆池时间窗'}
+
+ 时间窗交付决策
+ {timeMonitorDeliveryStatus.detail}
+
+
+ {timeMonitorDeliveryActions.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 eda199c8..8253f390 100644
--- a/vehicle-data-platform/apps/web/src/styles/global.css
+++ b/vehicle-data-platform/apps/web/src/styles/global.css
@@ -1334,6 +1334,74 @@ body {
line-height: 20px;
}
+.vp-time-delivery-strip {
+ margin-top: 14px;
+ display: grid;
+ grid-template-columns: minmax(260px, 0.58fr) minmax(0, 1.42fr);
+ gap: 12px;
+}
+
+.vp-time-delivery-summary {
+ min-height: 148px;
+ padding: 14px;
+ border: 1px solid rgba(10, 168, 107, 0.26);
+ border-radius: 8px;
+ background: #f6fbf8;
+ display: grid;
+ gap: 10px;
+ align-content: start;
+}
+
+.vp-time-delivery-grid {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 12px;
+}
+
+.vp-time-delivery-item {
+ min-height: 148px;
+ padding: 14px;
+ border: 1px solid var(--vp-border);
+ border-radius: 8px;
+ background: #fbfcff;
+ display: grid;
+ gap: 10px;
+ align-content: start;
+ text-align: left;
+ font: inherit;
+ cursor: pointer;
+}
+
+.vp-time-delivery-item:hover,
+.vp-time-delivery-item:focus-visible {
+ border-color: rgba(22, 100, 255, 0.42);
+ background: #f7fbff;
+ box-shadow: var(--vp-shadow-sm);
+ outline: none;
+}
+
+.vp-time-delivery-item strong {
+ color: var(--vp-text);
+ font-size: 19px;
+ line-height: 24px;
+ word-break: break-word;
+}
+
+.vp-time-delivery-item span {
+ color: var(--vp-text-muted);
+ font-size: 13px;
+ line-height: 20px;
+}
+
+.vp-time-delivery-item em {
+ color: var(--vp-primary);
+ font-style: normal;
+ font-size: 13px;
+ font-weight: 600;
+ line-height: 20px;
+ align-self: end;
+}
+
.vp-result-summary-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
@@ -6285,6 +6353,8 @@ button.vp-realtime-command-item:focus-visible {
.vp-time-review-grid,
.vp-time-audit-strip,
.vp-time-audit-grid,
+ .vp-time-delivery-strip,
+ .vp-time-delivery-grid,
.vp-realtime-customer-board .semi-card-body,
.vp-realtime-next-actions,
.vp-realtime-customer-steps,
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 2ea705ed..023cb721 100644
--- a/vehicle-data-platform/apps/web/src/test/App.test.tsx
+++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx
@@ -859,6 +859,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.getAllByText('带风险交付').length).toBeGreaterThanOrEqual(1);
+ expect(screen.getAllByText('存在告警车辆,交付时需要附带告警闭环和原始证据。').length).toBeGreaterThanOrEqual(1);
+ 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.getByText('这个范围会同步带入轨迹、里程、历史证据和告警复盘,适合客户问询、BI 核对和异常解释。')).toBeInTheDocument();
expect(screen.getByText('查看轨迹')).toBeInTheDocument();
@@ -971,9 +977,12 @@ test('dashboard exposes vehicle data center capability matrix', async () => {
fireEvent.click(screen.getByRole('button', { name: '复制复盘包' }));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【客户时间窗复盘包】'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('平台能力'));
+ expect(writeText).toHaveBeenCalledWith(expect.stringContaining('交付结论:带风险交付;存在告警车辆,交付时需要附带告警闭环和原始证据。'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('时间窗判定:1 天窗口'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('交付核对:'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('1. 范围可解释'));
+ expect(writeText).toHaveBeenCalledWith(expect.stringContaining('下一步动作:'));
+ expect(writeText).toHaveBeenCalledWith(expect.stringContaining('1. 客户结论:带风险交付'));
cleanup();
await renderDashboard();