diff --git a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx
index 27e7770c..5d77cf10 100644
--- a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx
+++ b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx
@@ -2739,6 +2739,70 @@ export function Dashboard({
onClick: openTimeMonitorRaw
}
];
+ const copyTodayVehicleServiceBrief = () => {
+ const rawFilters = { tab: 'raw', includeFields: 'true' };
+ const lines = [
+ '【客户今日车辆服务简报】',
+ `在线车辆:${formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles)} / ${formatCount(serviceSummary?.totalVehicles)}`,
+ `地图可见:${commandLocatedCount.toLocaleString()} 辆有坐标`,
+ `今日活跃:${formatCount(summary?.activeToday)}`,
+ `今日数据:${formatCount(summary?.frameToday)}`,
+ `告警车辆:${formatCount(summary?.issueVehicles)}`,
+ `当前时间窗:${timeMonitorWindowLabel}`,
+ highPriorityIssue
+ ? `优先告警:${qualityIssueLabel(highPriorityIssue.issueType)} / ${priorityIssueVehicleLabel(highPriorityIssue)}`
+ : '优先告警:暂无高优先级告警',
+ `车辆地图:${appURL(buildAppHash({ page: 'map', filters: { online: 'online' } }))}`,
+ `实时监控:${appURL(buildAppHash({ page: 'realtime', filters: { online: 'online' } }))}`,
+ `轨迹回放:${appURL(buildAppHash({ page: 'history', filters: timeMonitorScopeValue }))}`,
+ `里程报表:${appURL(buildAppHash({ page: 'mileage', filters: timeMonitorScopeValue }))}`,
+ `历史导出:${appURL(buildAppHash({ page: 'history-query', filters: rawFilters }))}`,
+ `告警通知:${appURL(buildAppHash({ page: 'alert-events', filters: highPriorityIssue?.issueType ? { issueType: highPriorityIssue.issueType } : {} }))}`
+ ];
+ copyText(lines.join('\n'), '今日服务简报');
+ };
+ const todayOperationsItems = [
+ {
+ label: '车辆在线',
+ value: `${formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles)} 在线`,
+ detail: `${formatCount(serviceSummary?.totalVehicles)} 辆车辆服务资产,先确认客户能看到多少车。`,
+ action: '打开地图',
+ color: 'green' as const,
+ onClick: () => onOpenMap({ online: 'online' })
+ },
+ {
+ label: '地图可见',
+ value: `${commandLocatedCount.toLocaleString()} 有坐标`,
+ detail: '有效坐标决定客户能不能直接看地图态势和位置分布。',
+ action: '实时地图',
+ color: commandLocatedCount > 0 ? 'blue' as const : 'orange' as const,
+ onClick: () => onOpenMap({ online: 'online' })
+ },
+ {
+ label: '轨迹复盘',
+ value: `${formatCount(summary?.activeToday)} 活跃`,
+ detail: '今日活跃车辆可按同一时间窗回放轨迹、速度和停驶片段。',
+ action: '回放轨迹',
+ color: 'blue' as const,
+ onClick: openTimeMonitorHistory
+ },
+ {
+ label: '里程报表',
+ value: timeMonitorWindowLabel,
+ detail: '从报表周期进入里程核对,避免客户只看到协议帧量。',
+ action: '核对里程',
+ color: 'blue' as const,
+ onClick: openTimeMonitorMileage
+ },
+ {
+ label: '告警通知',
+ value: `${formatCount(summary?.issueVehicles)} 告警`,
+ detail: highPriorityIssue ? `${qualityIssueLabel(highPriorityIssue.issueType)} / ${priorityIssueVehicleLabel(highPriorityIssue)}` : '暂无高优先级告警,保持巡检。',
+ action: '闭环处理',
+ color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const,
+ onClick: () => onOpenQuality(highPriorityIssue?.issueType ? { issueType: highPriorityIssue.issueType } : {})
+ }
+ ];
return (
@@ -2851,6 +2915,40 @@ export function Dashboard({
+
+
+
+ 客户今日运营台
+ {vehicleServiceOnlineText(serviceSummary, summary)}
+ 0 ? 'orange' : 'green'}>{formatCount(summary?.issueVehicles)} 告警
+
+
+ 客户今日运营台
+
+
+ 今天先看在线和地图,再处理告警、复盘轨迹、核对里程,最后把证据导出给客户。
+
+ } theme="solid" type="primary" onClick={copyTodayVehicleServiceBrief}>
+ 复制今日服务简报
+
+
+
+ {todayOperationsItems.map((item) => (
+
+ ))}
+
+
) : null}
{focusMode === 'time-window' ? (
diff --git a/vehicle-data-platform/apps/web/src/styles/global.css b/vehicle-data-platform/apps/web/src/styles/global.css
index 73b28532..5bba2840 100644
--- a/vehicle-data-platform/apps/web/src/styles/global.css
+++ b/vehicle-data-platform/apps/web/src/styles/global.css
@@ -1311,6 +1311,84 @@ body {
line-height: 18px;
}
+.vp-today-operations-home {
+ grid-column: 1 / -1;
+ border-top: 1px solid rgba(22, 100, 255, 0.14);
+ background: #f6f9ff;
+ display: grid;
+ grid-template-columns: minmax(280px, 0.36fr) minmax(0, 1fr);
+}
+
+.vp-today-operations-home-copy {
+ min-width: 0;
+ padding: 16px;
+ border-right: 1px solid rgba(22, 100, 255, 0.12);
+ display: grid;
+ gap: 10px;
+ align-content: start;
+}
+
+.vp-today-operations-home-copy .semi-button {
+ width: fit-content;
+}
+
+.vp-today-operations-home-grid {
+ min-width: 0;
+ padding: 12px;
+ display: grid;
+ grid-template-columns: repeat(5, minmax(0, 1fr));
+ gap: 10px;
+}
+
+.vp-today-operations-home-item {
+ min-width: 0;
+ min-height: 142px;
+ padding: 12px;
+ border: 1px solid rgba(22, 100, 255, 0.14);
+ border-radius: 8px;
+ background: #ffffff;
+ color: inherit;
+ cursor: pointer;
+ font: inherit;
+ text-align: left;
+ display: grid;
+ gap: 8px;
+ align-content: start;
+ transition: border-color 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease;
+}
+
+.vp-today-operations-home-item:hover,
+.vp-today-operations-home-item:focus-visible {
+ border-color: rgba(22, 100, 255, 0.42);
+ box-shadow: 0 10px 24px rgba(24, 39, 75, 0.08);
+ outline: none;
+ transform: translateY(-1px);
+}
+
+.vp-today-operations-home-item strong {
+ color: var(--vp-text);
+ font-size: 18px;
+ line-height: 24px;
+ font-weight: 800;
+ word-break: break-word;
+}
+
+.vp-today-operations-home-item span {
+ color: var(--vp-text-muted);
+ font-size: 12px;
+ line-height: 18px;
+ word-break: break-word;
+}
+
+.vp-today-operations-home-item em {
+ align-self: end;
+ color: var(--vp-primary);
+ font-size: 12px;
+ font-style: normal;
+ font-weight: 800;
+ line-height: 18px;
+}
+
.vp-customer-service-next {
grid-column: 1 / -1;
border-top: 1px solid var(--vp-border);
@@ -14833,6 +14911,8 @@ button.vp-realtime-command-item:focus-visible {
.vp-vehicle-question-grid,
.vp-customer-service-path,
.vp-customer-service-path-grid,
+ .vp-today-operations-home,
+ .vp-today-operations-home-grid,
.vp-customer-service-overview,
.vp-customer-service-overview-grid,
.vp-customer-service-next,
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 13c8ccc4..8f17c4c3 100644
--- a/vehicle-data-platform/apps/web/src/test/App.test.tsx
+++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx
@@ -1120,6 +1120,11 @@ test('dashboard presents one vehicle service operating posture', async () => {
test('dashboard prioritizes customer vehicle service command over data sources', async () => {
window.history.replaceState(null, '', '/#/dashboard');
+ 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')) {
@@ -1206,6 +1211,23 @@ test('dashboard prioritizes customer vehicle service command over data sources',
expect(screen.getByRole('button', { name: '车辆服务指挥台 查这段时间 轨迹统计' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆服务指挥台 导出数据 历史导出' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆服务指挥台 处理告警 告警事件' })).toBeInTheDocument();
+ expect(screen.getAllByText('客户今日运营台').length).toBeGreaterThanOrEqual(1);
+ expect(screen.getByText('今天先看在线和地图,再处理告警、复盘轨迹、核对里程,最后把证据导出给客户。')).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: '客户今日运营台 车辆在线 208 在线 打开地图' })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: '客户今日运营台 地图可见 0 有坐标 实时地图' })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: '客户今日运营台 轨迹复盘 4 活跃 回放轨迹' })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: '客户今日运营台 里程报表 1 天窗口 核对里程' })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: '客户今日运营台 告警通知 7 告警 闭环处理' })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: /复制今日服务简报/ })).toBeInTheDocument();
+ fireEvent.click(screen.getByRole('button', { name: /复制今日服务简报/ }));
+ expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【客户今日车辆服务简报】'));
+ expect(writeText).toHaveBeenCalledWith(expect.stringContaining('在线车辆:208 / 1,033'));
+ expect(writeText).toHaveBeenCalledWith(expect.stringContaining('地图可见:0 辆有坐标'));
+ expect(writeText).toHaveBeenCalledWith(expect.stringContaining('今日活跃:4'));
+ expect(writeText).toHaveBeenCalledWith(expect.stringContaining('告警车辆:7'));
+ expect(writeText).toHaveBeenCalledWith(expect.stringContaining('车辆地图:http://localhost:3000/#/map?online=online'));
+ expect(writeText).toHaveBeenCalledWith(expect.stringContaining('里程报表:http://localhost:3000/#/mileage'));
+ expect(writeText).toHaveBeenCalledWith(expect.stringContaining('历史导出:http://localhost:3000/#/history-query?tab=raw&includeFields=true'));
expect(screen.getByRole('button', { name: '车辆服务指挥台优先队列 告警待处理 7 辆 告警事件' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆服务指挥台优先队列 无有效定位 208 辆 打开地图' })).toBeInTheDocument();
expect(screen.getAllByText('车辆服务指挥台')[0].compareDocumentPosition(screen.getByText('车辆地图态势')) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();