feat(platform): add dashboard map operations panel

This commit is contained in:
lingniu
2026-07-04 21:59:41 +08:00
parent c73c46ca5d
commit ae42a4092b
3 changed files with 212 additions and 8 deletions

View File

@@ -2,7 +2,7 @@ import { Button, Card, Col, Form, Row, Select, Space, Spin, Table, Tag, Toast, T
import { IconSearch } from '@douyinfe/semi-icons';
import { useEffect, useMemo, useState } from 'react';
import { api } from '../api/client';
import type { DashboardSummary, LinkHealth, ProtocolStat, QualityIssueRow, ServiceStatusStat, VehicleCoverageRow, VehicleRealtimeRow, VehicleServiceSummary } from '../api/types';
import type { DashboardSummary, LinkHealth, OpsHealth, ProtocolStat, QualityIssueRow, ServiceStatusStat, VehicleCoverageRow, VehicleRealtimeRow, VehicleServiceSummary } from '../api/types';
import { PageHeader } from '../components/PageHeader';
import { SourceStatusTags } from '../components/SourceStatusTags';
import { StatusTag } from '../components/StatusTag';
@@ -258,11 +258,16 @@ export function Dashboard({
const [coverage, setCoverage] = useState<VehicleCoverageRow[]>([]);
const [locations, setLocations] = useState<VehicleRealtimeRow[]>([]);
const [qualityIssues, setQualityIssues] = useState<QualityIssueRow[]>([]);
const [opsHealth, setOpsHealth] = useState<OpsHealth | null>(null);
const [loading, setLoading] = useState(true);
const [coverageLoading, setCoverageLoading] = useState(false);
const [coverageServiceStatusTitle, setCoverageServiceStatusTitle] = useState('');
const [coverageFilters, setCoverageFilters] = useState<Record<string, string>>({});
const amapConfigured = isAMapConfigured();
const localAMapConfigured = isAMapConfigured();
const amapConfigured = opsHealth?.runtime?.amapWebJsConfigured ?? localAMapConfigured;
const amapApiConfigured = opsHealth?.runtime?.amapApiConfigured ?? false;
const amapSecurityProxyEnabled = opsHealth?.runtime?.amapSecurityProxyEnabled ?? false;
const amapSecurityCodeExposed = opsHealth?.runtime?.amapSecurityCodeExposed ?? false;
const loadCoverage = (values?: Record<string, string>) => {
const nextValues = values ?? {};
@@ -292,7 +297,8 @@ export function Dashboard({
api.vehicleServiceSummary().then(setServiceSummary),
api.vehicleCoverage(new URLSearchParams({ limit: '8' })).then((page) => setCoverage(page.items)),
api.vehicleRealtime(new URLSearchParams({ limit: '8' })).then((page) => setLocations(page.items)),
api.alertEvents(new URLSearchParams({ limit: '5' })).then((page) => setQualityIssues(page.items))
api.alertEvents(new URLSearchParams({ limit: '5' })).then((page) => setQualityIssues(page.items)),
api.opsHealth().then(setOpsHealth)
];
Promise.allSettled(tasks)
.then((results) => {
@@ -499,6 +505,62 @@ export function Dashboard({
onClick: () => onOpenMileage()
}
];
const mapReadiness = [
{
title: '高德 Web JS',
value: amapConfigured ? '已配置' : '待配置',
color: amapConfigured ? 'green' as const : 'orange' as const,
detail: amapConfigured ? '实时监控和轨迹回放可加载高德底图。' : '缺少 Web JS Key 时降级为坐标预览。'
},
{
title: '安全代理',
value: amapSecurityProxyEnabled ? '已启用' : '未启用',
color: amapSecurityProxyEnabled ? 'green' as const : 'orange' as const,
detail: amapSecurityProxyEnabled ? '安全码由服务端代理追加,不下发到浏览器。' : '生产环境建议启用 /_AMapService。'
},
{
title: '服务端 API',
value: amapApiConfigured ? '已配置' : '待配置',
color: amapApiConfigured ? 'green' as const : 'orange' as const,
detail: amapApiConfigured ? '逆地理、路线、围栏等服务端地图能力可扩展。' : '后端地图服务会降级。'
},
{
title: '安全码暴露',
value: amapSecurityCodeExposed ? '需处理' : '未暴露',
color: amapSecurityCodeExposed ? 'red' as const : 'green' as const,
detail: amapSecurityCodeExposed ? '浏览器端可见安全码,需要切换到安全代理。' : '符合生产密钥收敛要求。'
}
];
const mapWorkItems = [
{
title: '实时监控',
value: `${commandLocatedCount.toLocaleString()} 个有效坐标`,
detail: '在线车辆进入地图态势,点击车辆后继续查看车辆服务和实时字段。',
action: '实时地图',
onClick: () => onOpenMap({ online: 'online' })
},
{
title: '轨迹回放',
value: `${formatCount(summary?.activeToday)} 今日活跃`,
detail: '按车辆和时间窗回放历史位置,支持打开高德线路和 RAW 证据。',
action: '轨迹回放',
onClick: () => onOpenHistory()
},
{
title: '历史证据',
value: `${formatCount(summary?.frameToday)} 今日帧`,
detail: '地图点位异常时直接回查位置历史、解析字段和原始帧。',
action: '历史查询',
onClick: () => onOpenHistory({ tab: 'raw', includeFields: 'true' })
},
{
title: '告警通知',
value: `${formatCount(summary?.issueVehicles)} 告警车辆`,
detail: '断链、无来源、坐标异常进入告警队列并触发通知闭环。',
action: '告警事件',
onClick: () => onOpenQuality()
}
];
const operationWorkbench = [
{
title: '实时监控',
@@ -621,7 +683,7 @@ export function Dashboard({
`实时地图有效定位:${commandLocatedCount.toLocaleString()}`,
`告警车辆:${formatCount(summary?.issueVehicles)}`,
`Kafka Lag${formatLag(summary?.kafkaLag)}`,
`高德地图:${amapConfigured ? '已配置' : '待配置'}`,
`高德地图:Web JS ${amapConfigured ? '已配置' : '待配置'} / 服务端 API ${amapApiConfigured ? '已配置' : '待配置'} / 安全代理 ${amapSecurityProxyEnabled ? '已启用' : '未启用'}`,
`链路健康:${unhealthyLinks.length > 0 ? unhealthyLinks.map((item) => `${item.name}=${item.status}`).join('') : '正常'}`,
`优先动作:${priorityAction ? `${priorityAction.label} ${priorityAction.count.toLocaleString()}辆 - ${priorityAction.detail}` : '暂无待办'}`,
highPriorityIssue ? `最高告警:${highPriorityIssue.severity === 'error' ? 'P0' : 'P1'} ${qualityIssueLabel(highPriorityIssue.issueType)} / ${priorityIssueVehicleLabel(highPriorityIssue)} / ${highPriorityIssue.lastSeen || '-'}` : '最高告警:暂无',
@@ -640,7 +702,7 @@ export function Dashboard({
const lines = [
'【车辆数据中台功能蓝图】',
'定位32960 / 808 / 宇通 MQTT 都只是车辆服务证据源,最终围绕一辆车提供实时、轨迹、历史、告警、统计能力。',
`高德地图:${amapConfigured ? '已配置,前端只使用运行时配置和安全代理' : '待配置'}`,
`高德地图:Web JS ${amapConfigured ? '已配置' : '待配置'};服务端 API ${amapApiConfigured ? '已配置' : '待配置'};安全代理 ${amapSecurityProxyEnabled ? '已启用' : '未启用'};安全码${amapSecurityCodeExposed ? '已暴露' : '未暴露'}`,
`车辆规模:${formatCount(serviceSummary?.totalVehicles)};在线:${formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles)};告警:${formatCount(summary?.issueVehicles)}`,
'',
...scenarioNavigation.map((item, index) => [
@@ -814,6 +876,31 @@ export function Dashboard({
<Button size="small" theme="light" type="primary" onClick={exportDashboardSnapshot}> CSV</Button>
</Space>
</Card>
<Card bordered title="地图运营能力" style={{ marginBottom: 16 }}>
<div className="vp-map-ops-board">
<div className="vp-map-ops-readiness">
{mapReadiness.map((item) => (
<div key={item.title} className="vp-map-ops-readiness-item">
<Tag color={item.color}>{item.title}</Tag>
<div className="vp-map-ops-value">{item.value}</div>
<Typography.Text type="secondary">{item.detail}</Typography.Text>
</div>
))}
</div>
<div className="vp-map-ops-work">
{mapWorkItems.map((item) => (
<button key={item.title} className="vp-map-ops-work-item" type="button" onClick={item.onClick} aria-label={`地图运营 ${item.title}`}>
<div className="vp-map-ops-work-head">
<Typography.Title heading={6} style={{ margin: 0 }}>{item.title}</Typography.Title>
<Tag color="blue">{item.action}</Tag>
</div>
<div className="vp-map-ops-work-value">{item.value}</div>
<Typography.Text type="secondary">{item.detail}</Typography.Text>
</button>
))}
</div>
</div>
</Card>
{focusVehicle ? (
<Card
bordered