diff --git a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx index 08561900..c0eb4e3e 100644 --- a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx @@ -2019,6 +2019,40 @@ export function Dashboard({ onClick: openTimeMonitorRaw } ]; + const customerSlaItems = [ + { + label: '断链发现', + value: '1 分钟内', + detail: 'Redis 在线状态按 1 分钟 TTL 形成准实时断链判断,优先确认车辆是否仍在上报。', + action: '在线状态', + color: 'green' as const, + onClick: () => onOpenRealtime({ online: 'offline' }) + }, + { + label: '异常通知', + value: `${formatCount(summary?.issueVehicles)} 告警`, + detail: highPriorityIssue ? `${qualityIssueLabel(highPriorityIssue.issueType)} 进入通知和升级闭环。` : '暂无高优先级告警,保持例行巡检。', + action: '通知闭环', + color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const, + onClick: () => onOpenQuality(highPriorityIssue?.issueType ? { issueType: highPriorityIssue.issueType } : {}) + }, + { + label: '恢复验收', + value: '轨迹/统计可查', + detail: '恢复不是只看链路 active,还要确认实时、轨迹、里程统计和历史证据都能打开。', + action: '验收证据', + color: 'blue' as const, + onClick: openTimeMonitorHistory + }, + { + label: '数据交付', + value: `${formatCount(summary?.frameToday)} 今日数据`, + detail: '按客户车辆和时间窗导出历史位置、原始记录、解析字段和统计证据。', + action: '导出证据', + color: 'blue' as const, + onClick: openTimeMonitorRaw + } + ]; const dispatchOperationsHighlights = [ { label: '实时地图', @@ -2512,6 +2546,35 @@ export function Dashboard({ ))} +
+
+ + 客户服务 SLA 承诺 + 0 ? 'orange' : 'green'}>{formatCount(summary?.issueVehicles)} 告警 + 0 ? 'orange' : 'green'}>{unhealthyLinkCount.toLocaleString()} 链路异常 + + 把断链发现、异常通知、恢复验收和数据交付变成客户能看懂的服务承诺。 + + 运维证据仍然独立展示;客户首页只看到哪些承诺正在被满足、哪些异常需要通知和验收。 + +
+
+ {customerSlaItems.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 36e5d857..0cf845db 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -2155,6 +2155,74 @@ body { font-weight: 700; } +.vp-customer-service-sla { + margin-bottom: 16px; + padding: 18px; + border: 1px solid rgba(245, 158, 11, 0.22); + border-radius: 8px; + background: #fff; + display: grid; + grid-template-columns: minmax(280px, 0.82fr) minmax(0, 1.5fr); + gap: 16px; +} + +.vp-customer-service-sla-copy { + min-width: 0; + display: grid; + align-content: start; + gap: 10px; +} + +.vp-customer-service-sla-grid { + min-width: 0; + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 10px; +} + +.vp-customer-service-sla-item { + min-height: 136px; + padding: 13px; + border: 1px solid var(--vp-border); + border-radius: 8px; + background: linear-gradient(180deg, #ffffff 0%, #fffbf3 100%); + color: inherit; + cursor: pointer; + text-align: left; + font: inherit; + display: grid; + align-content: space-between; + gap: 8px; +} + +.vp-customer-service-sla-item:hover, +.vp-customer-service-sla-item:focus-visible { + border-color: rgba(245, 158, 11, 0.45); + box-shadow: var(--vp-shadow-sm); + outline: none; +} + +.vp-customer-service-sla-item strong { + color: var(--vp-text); + font-size: 20px; + line-height: 25px; + word-break: break-word; +} + +.vp-customer-service-sla-item span { + color: var(--vp-text-muted); + font-size: 12px; + line-height: 18px; + word-break: break-word; +} + +.vp-customer-service-sla-item em { + color: #1664ff; + font-style: normal; + font-size: 12px; + font-weight: 700; +} + .vp-customer-cockpit-summary { padding: 18px; border-right: 1px solid var(--vp-border); @@ -11606,6 +11674,8 @@ button.vp-realtime-command-item:focus-visible { .vp-customer-service-command-grid, .vp-customer-fleet-groups, .vp-customer-fleet-groups-grid, + .vp-customer-service-sla, + .vp-customer-service-sla-grid, .vp-customer-cockpit, .vp-customer-cockpit-actions, .vp-amap-service-foundation, 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 165c7a4c..01e04661 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -766,6 +766,12 @@ test('dashboard prioritizes customer vehicle service command over data sources', expect(screen.getByRole('button', { name: '客户车队分组工作台 告警处置组 7 告警 告警通知' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户车队分组工作台 身份维护组 9 待绑定 维护车辆' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: '客户车队分组工作台 报表交付组 1,286,320 今日数据 历史导出' })).toBeInTheDocument(); + expect(screen.getByText('客户服务 SLA 承诺')).toBeInTheDocument(); + expect(screen.getByText('把断链发现、异常通知、恢复验收和数据交付变成客户能看懂的服务承诺。')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户服务 SLA 承诺 断链发现 1 分钟内 在线状态' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户服务 SLA 承诺 异常通知 7 告警 通知闭环' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户服务 SLA 承诺 恢复验收 轨迹/统计可查 验收证据' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '客户服务 SLA 承诺 数据交付 1,286,320 今日数据 导出证据' })).toBeInTheDocument(); }); test('dashboard presents a customer dispatch operations home', async () => {