From 48642726aae98ce76b1f0c4aad3a925593ba64ef Mon Sep 17 00:00:00 2001 From: lingniu Date: Sat, 4 Jul 2026 18:40:36 +0800 Subject: [PATCH] feat(platform): center console on vehicle service --- .../apps/web/src/layout/AppShell.tsx | 4 +- .../apps/web/src/pages/Dashboard.tsx | 6 +- .../apps/web/src/pages/OpsQuality.tsx | 113 +++++++++++++++++- .../apps/web/src/styles/global.css | 36 ++++++ .../apps/web/src/test/App.test.tsx | 18 ++- 5 files changed, 161 insertions(+), 16 deletions(-) diff --git a/vehicle-data-platform/apps/web/src/layout/AppShell.tsx b/vehicle-data-platform/apps/web/src/layout/AppShell.tsx index 5eebfed7..1e672c21 100644 --- a/vehicle-data-platform/apps/web/src/layout/AppShell.tsx +++ b/vehicle-data-platform/apps/web/src/layout/AppShell.tsx @@ -19,7 +19,7 @@ import { isAMapConfigured } from '../config/appConfig'; export type PageKey = 'dashboard' | 'vehicles' | 'map' | 'realtime' | 'detail' | 'history' | 'history-query' | 'mileage' | 'alert-events' | 'quality' | 'notification-rules' | 'ops-quality'; const navItems = [ - { itemKey: 'dashboard', text: '运营驾驶舱', icon: }, + { itemKey: 'dashboard', text: '车辆服务总览', icon: }, { itemKey: 'vehicles', text: '车辆中心', icon: }, { itemKey: 'map', text: '地图态势', icon: }, { itemKey: 'realtime', text: '实时监控', icon: }, @@ -147,7 +147,7 @@ export function AppShell({ ) : null} 生产环境 {platformRelease ? 版本 {platformRelease} : null} - 多源接入 / 一个车辆服务 + 三源归一 / 车辆服务 {amapConfigured ? '地图就绪' : '地图待配置'} diff --git a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx index dcbafd65..6dbae43d 100644 --- a/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Dashboard.tsx @@ -413,7 +413,7 @@ export function Dashboard({ return (
- +
{kpis.map((item) => ( @@ -425,7 +425,7 @@ export function Dashboard({ ))}
- + {vehicleServiceOnlineText(serviceSummary, summary)} @@ -522,7 +522,7 @@ export function Dashboard({
) : null} - +
{capabilities.map((item) => (
diff --git a/vehicle-data-platform/apps/web/src/pages/OpsQuality.tsx b/vehicle-data-platform/apps/web/src/pages/OpsQuality.tsx index e1119607..6828a991 100644 --- a/vehicle-data-platform/apps/web/src/pages/OpsQuality.tsx +++ b/vehicle-data-platform/apps/web/src/pages/OpsQuality.tsx @@ -1,4 +1,4 @@ -import { Button, Card, Space, Table, Tag, Toast } from '@douyinfe/semi-ui'; +import { Button, Card, Space, Table, Tag, Toast, Typography } from '@douyinfe/semi-ui'; import { useEffect, useState } from 'react'; import { api } from '../api/client'; import type { LinkHealth, OpsHealth } from '../api/types'; @@ -37,6 +37,89 @@ function amapSecurityColor(health: OpsHealth | null): 'green' | 'orange' | 'red' return 'orange'; } +type CapacityRisk = { + key: string; + title: string; + value: string; + category: string; + severity: 'warning' | 'error'; + finding: string; + action: string; +}; + +function firstNumber(value: string) { + const match = value.match(/[\d,.]+/); + return match ? match[0] : '-'; +} + +function capacityRisk(finding: string): CapacityRisk { + const lower = finding.toLowerCase(); + if (lower.includes('bridge') && lower.includes('ack pending')) { + return { + key: finding, + title: 'NATS 桥接 ACK 未确认', + value: firstNumber(finding), + category: 'NATS -> Kafka', + severity: 'error', + finding, + action: '优先检查 Kafka 是否监听 9092、磁盘是否打满,以及桥接服务是否持续写入成功。' + }; + } + if (lower.includes('bridge') && lower.includes('consumer pending')) { + return { + key: finding, + title: 'NATS 桥接待消费', + value: firstNumber(finding), + category: 'NATS -> Kafka', + severity: 'warning', + finding, + action: '确认 Kafka 已恢复后观察 pending 是否持续下降;若不下降,扩容桥接或降低 Kafka topic 保留压力。' + }; + } + if (lower.includes('kafka') || lower.includes('lag')) { + return { + key: finding, + title: 'Kafka 消费积压', + value: firstNumber(finding), + category: 'Kafka', + severity: 'warning', + finding, + action: '检查消费者组 lag、topic retention、broker 磁盘和生产写入错误。' + }; + } + if (lower.includes('redis')) { + return { + key: finding, + title: 'Redis 在线投影异常', + value: firstNumber(finding), + category: 'Redis', + severity: 'warning', + finding, + action: '核对 realtime KV、online TTL 和协议实时帧过滤,确认车辆在线统计来源。' + }; + } + if (lower.includes('connection')) { + return { + key: finding, + title: '网关连接压力', + value: firstNumber(finding), + category: 'Gateway', + severity: 'warning', + finding, + action: '查看 32960、808、MQTT 连接数、系统 FD、CPU 和接入端口队列。' + }; + } + return { + key: finding, + title: '容量风险', + value: firstNumber(finding), + category: 'General', + severity: 'warning', + finding, + action: '进入对应链路日志和指标继续定位,确认是否影响车辆实时服务。' + }; +} + export function OpsQuality() { const [health, setHealth] = useState(null); const [loading, setLoading] = useState(true); @@ -54,6 +137,8 @@ export function OpsQuality() { }, []); const runtime = health?.runtime; + const capacityRisks = (health?.capacityFindings ?? []).map(capacityRisk); + const bridgeRisks = capacityRisks.filter((item) => item.category === 'NATS -> Kafka'); return (
@@ -111,13 +196,29 @@ export function OpsQuality() { - {health?.capacityFindings?.length ? ( + {capacityRisks.length ? ( - - {health.capacityFindings.map((item) => ( - {item} +
+ {capacityRisks.map((item) => ( +
+ + {item.category} + {item.finding} + +
{item.value}
+ {item.title} + {item.action} +
))} - +
+ {bridgeRisks.length ? ( +
+ 桥接处置顺序 + + 先确认 Kafka broker 监听和磁盘空间,再观察 ACK pending 是否回到 0,最后确认 consumer pending 持续下降。 + +
+ ) : null}
) : null} diff --git a/vehicle-data-platform/apps/web/src/styles/global.css b/vehicle-data-platform/apps/web/src/styles/global.css index e2495d4a..9f61af25 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -542,6 +542,41 @@ body { line-height: 28px; } +.vp-risk-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 12px; +} + +.vp-risk-item { + min-height: 158px; + padding: 12px; + border: 1px solid var(--vp-border); + border-radius: var(--vp-radius); + background: #fbfcff; + display: grid; + gap: 10px; + align-content: start; +} + +.vp-risk-value { + color: var(--vp-text); + font-size: 26px; + font-weight: 700; + line-height: 32px; +} + +.vp-risk-runbook { + margin-top: 12px; + padding: 12px; + border: 1px solid var(--vp-border); + border-radius: var(--vp-radius); + background: #f5f9ff; + display: flex; + align-items: center; + gap: 10px; +} + .vp-alert-ops-grid { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(360px, 0.6fr); @@ -981,6 +1016,7 @@ body { .vp-conclusion-grid, .vp-monitor-layout, .vp-alert-flow, + .vp-risk-grid, .vp-alert-ops-grid, .vp-stat-workspace, .vp-stat-insight-grid, 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 9edc6f0a..92835777 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -12,7 +12,7 @@ afterEach(() => { test('renders vehicle platform shell', () => { render(); expect(screen.getByText('车辆服务中台')).toBeInTheDocument(); - expect(screen.getAllByText('运营驾驶舱').length).toBeGreaterThanOrEqual(1); + expect(screen.getAllByText('车辆服务总览').length).toBeGreaterThanOrEqual(1); }); test('exposes AMap operations shortcuts when map key is configured', async () => { @@ -345,7 +345,7 @@ test('dashboard presents one vehicle service operating posture', async () => { render(); - expect(await screen.findByText('统一车辆服务')).toBeInTheDocument(); + expect(await screen.findByText('统一车辆服务入口')).toBeInTheDocument(); expect(screen.getByText('208 / 1,033 在线')).toBeInTheDocument(); expect(screen.getByText('181 多源覆盖')).toBeInTheDocument(); expect(screen.getByText('7 告警事件')).toBeInTheDocument(); @@ -448,7 +448,7 @@ test('dashboard exposes vehicle data center capability matrix', async () => { const renderDashboard = async () => { window.history.replaceState(null, '', '/#/dashboard'); render(); - expect(await screen.findByText('车联网能力矩阵')).toBeInTheDocument(); + expect(await screen.findByText('车辆服务能力矩阵')).toBeInTheDocument(); }; await renderDashboard(); @@ -3374,7 +3374,12 @@ test('renders ops quality as a standalone runtime health page', async () => { ], kafkaLag: 42, activeConnections: 1234, - capacityFindings: ['Kafka lag 42', 'Redis online key below expected'], + capacityFindings: [ + 'Kafka lag 42', + 'bridge ack pending 4000 exceeds 100', + 'bridge consumer pending 1482047 exceeds 10000', + 'Redis online key below expected' + ], redisOnlineKeys: 368, tdengineWritable: true, mysqlWritable: false, @@ -3407,7 +3412,7 @@ test('renders ops quality as a standalone runtime health page', async () => { expect(await screen.findByRole('heading', { name: '运维质量' })).toBeInTheDocument(); expect(screen.getByText('platform-ops-test')).toBeInTheDocument(); - expect(screen.getByText('42')).toBeInTheDocument(); + expect(screen.getAllByText('42').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('1,234')).toBeInTheDocument(); expect(screen.getByText('368')).toBeInTheDocument(); expect(screen.getByText('TDengine 写入')).toBeInTheDocument(); @@ -3417,6 +3422,9 @@ test('renders ops quality as a standalone runtime health page', async () => { expect(screen.getByText('安全码未暴露')).toBeInTheDocument(); expect(screen.getByText('gb32960-gateway')).toBeInTheDocument(); expect(screen.getByText('Kafka lag 42')).toBeInTheDocument(); + expect(screen.getByText('NATS 桥接 ACK 未确认')).toBeInTheDocument(); + expect(screen.getByText('NATS 桥接待消费')).toBeInTheDocument(); + expect(screen.getByText('桥接处置顺序')).toBeInTheDocument(); expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/ops/health'), undefined); });