feat(platform): center console on vehicle service

This commit is contained in:
lingniu
2026-07-04 18:40:36 +08:00
parent bdad6f7bf7
commit 48642726aa
5 changed files with 161 additions and 16 deletions

View File

@@ -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: <IconHome /> },
{ itemKey: 'dashboard', text: '车辆服务总览', icon: <IconHome /> },
{ itemKey: 'vehicles', text: '车辆中心', icon: <IconServer /> },
{ itemKey: 'map', text: '地图态势', icon: <IconMapPin /> },
{ itemKey: 'realtime', text: '实时监控', icon: <IconActivity /> },
@@ -147,7 +147,7 @@ export function AppShell({
) : null}
<Tag color="blue"></Tag>
{platformRelease ? <Tag color="blue"> {platformRelease}</Tag> : null}
<Tag color="grey"> / </Tag>
<Tag color="grey"> / </Tag>
<Tag color={amapConfigured ? 'green' : 'orange'}>{amapConfigured ? '地图就绪' : '地图待配置'}</Tag>
<Button size="small" aria-label="顶部地图态势" onClick={() => onChange('map')}></Button>
<Button size="small" aria-label="顶部实时监控" onClick={() => onChange('realtime')}></Button>

View File

@@ -413,7 +413,7 @@ export function Dashboard({
return (
<div className="vp-page">
<PageHeader title="运营驾驶舱" description="围绕一个车辆服务汇总实时态势、轨迹证据、告警事件、统计查询和链路健康" />
<PageHeader title="车辆服务总览" description="三个数据源最终汇总为一个车辆服务,统一承载实时监控、轨迹回放、历史查询、告警通知、统计查询和链路质量" />
<Spin spinning={loading}>
<div className="vp-kpi-grid">
{kpis.map((item) => (
@@ -425,7 +425,7 @@ export function Dashboard({
</Card>
))}
</div>
<Card bordered title="统一车辆服务" style={{ marginBottom: 16 }}>
<Card bordered title="统一车辆服务入口" style={{ marginBottom: 16 }}>
<Space wrap>
<Tag color="blue">{vehicleServiceOnlineText(serviceSummary, summary)}</Tag>
<Button size="small" theme="light" type="primary" onClick={() => onOpenVehicles({ online: 'online' })}>线</Button>
@@ -522,7 +522,7 @@ export function Dashboard({
</div>
</Card>
) : null}
<Card bordered title="车联网能力矩阵" style={{ marginBottom: 16 }}>
<Card bordered title="车辆服务能力矩阵" style={{ marginBottom: 16 }}>
<div className="vp-capability-grid">
{capabilities.map((item) => (
<div key={item.title} className="vp-capability-item">

View File

@@ -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<OpsHealth | null>(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 (
<div className="vp-page">
@@ -111,13 +196,29 @@ export function OpsQuality() {
</Space>
</Card>
{health?.capacityFindings?.length ? (
{capacityRisks.length ? (
<Card bordered title="容量与风险发现" style={{ marginTop: 16 }}>
<Space wrap>
{health.capacityFindings.map((item) => (
<Tag key={item} color="orange">{item}</Tag>
<div className="vp-risk-grid">
{capacityRisks.map((item) => (
<div key={item.key} className="vp-risk-item">
<Space wrap>
<Tag color={item.severity === 'error' ? 'red' : 'orange'}>{item.category}</Tag>
<Tag color="grey">{item.finding}</Tag>
</Space>
<div className="vp-risk-value">{item.value}</div>
<Typography.Title heading={6} style={{ margin: 0 }}>{item.title}</Typography.Title>
<Typography.Text type="secondary">{item.action}</Typography.Text>
</div>
))}
</Space>
</div>
{bridgeRisks.length ? (
<div className="vp-risk-runbook">
<Tag color="blue"></Tag>
<Typography.Text>
Kafka broker ACK pending 0 consumer pending
</Typography.Text>
</div>
) : null}
</Card>
) : null}

View File

@@ -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,

View File

@@ -12,7 +12,7 @@ afterEach(() => {
test('renders vehicle platform shell', () => {
render(<App />);
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(<App />);
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(<App />);
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);
});