feat(platform): surface source readiness on dashboard

This commit is contained in:
lingniu
2026-07-04 23:38:48 +08:00
parent 44c7748958
commit 1780039722
3 changed files with 413 additions and 2 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, OpsHealth, ProtocolStat, QualityIssueRow, ServiceStatusStat, VehicleCoverageRow, VehicleRealtimeRow, VehicleServiceSummary } from '../api/types';
import type { DashboardSummary, LinkHealth, OpsHealth, ProtocolStat, QualityIssueRow, ServiceStatusStat, SourceReadinessPlan, SourceReadinessRow, VehicleCoverageRow, VehicleRealtimeRow, VehicleServiceSummary } from '../api/types';
import { PageHeader } from '../components/PageHeader';
import { SourceStatusTags } from '../components/SourceStatusTags';
import { StatusTag } from '../components/StatusTag';
@@ -93,6 +93,15 @@ function formatProtocolRate(row: ProtocolStat) {
return `${Math.round((row.online / row.total) * 100)}%`;
}
function normalizeSourceReadiness(plan: SourceReadinessPlan | null): SourceReadinessPlan | null {
return plan && Array.isArray(plan.sources) ? plan : null;
}
function sourceReadinessColor(row: SourceReadinessRow): 'green' | 'orange' | 'red' {
if (row.severity === 'error') return 'red';
return row.severity === 'warning' ? 'orange' : 'green';
}
function rowServiceStatus(row: { serviceStatus?: { title: string; severity: string }; onlineSourceCount: number; sourceCount: number }) {
if (row.serviceStatus) {
return {
@@ -259,6 +268,7 @@ export function Dashboard({
const [locations, setLocations] = useState<VehicleRealtimeRow[]>([]);
const [qualityIssues, setQualityIssues] = useState<QualityIssueRow[]>([]);
const [opsHealth, setOpsHealth] = useState<OpsHealth | null>(null);
const [sourceReadiness, setSourceReadiness] = useState<SourceReadinessPlan | null>(null);
const [loading, setLoading] = useState(true);
const [coverageLoading, setCoverageLoading] = useState(false);
const [coverageServiceStatusTitle, setCoverageServiceStatusTitle] = useState('');
@@ -298,7 +308,8 @@ export function Dashboard({
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.opsHealth().then(setOpsHealth)
api.opsHealth().then(setOpsHealth),
api.sourceReadiness().then((plan) => setSourceReadiness(normalizeSourceReadiness(plan)))
];
Promise.allSettled(tasks)
.then((results) => {
@@ -954,6 +965,48 @@ 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-source-readiness-grid">
{(sourceReadiness?.sources ?? []).map((item) => (
<div key={item.protocol} className="vp-source-readiness-item">
<div className="vp-source-readiness-head">
<Space wrap>
<Tag color={sourceReadinessColor(item)}>{item.protocol}</Tag>
<Tag color={sourceReadinessColor(item)}>{item.status}</Tag>
</Space>
<Typography.Text type="secondary">{item.role}</Typography.Text>
</div>
<div className="vp-source-readiness-metrics">
<div>
<span>线</span>
<strong>{formatCount(item.online)} / {formatCount(item.total)}</strong>
</div>
<div>
<span>线</span>
<strong>{item.onlineRate.toLocaleString(undefined, { maximumFractionDigits: 1 })}%</strong>
</div>
<div>
<span></span>
<strong>{formatCount(item.missingVehicles)}</strong>
</div>
</div>
<Typography.Text type="secondary">{item.evidence}</Typography.Text>
<div className="vp-source-readiness-actions">
<Button size="small" theme="light" onClick={() => { window.location.hash = item.realtimeHash; }}></Button>
<Button size="small" theme="light" onClick={() => { window.location.hash = item.vehiclesHash; }}></Button>
<Button size="small" theme="light" onClick={() => { window.location.hash = item.historyHash; }}>RAW</Button>
<Button size="small" theme="light" type={item.severity === 'ok' ? 'tertiary' : 'warning'} onClick={() => { window.location.hash = item.alertHash; }}></Button>
</div>
</div>
))}
{sourceReadiness?.sources?.length ? null : (
<div className="vp-source-readiness-empty">
<Tag color="grey"></Tag>
<Typography.Text type="secondary"> 32960808 MQTT </Typography.Text>
</div>
)}
</div>
</Card>
<Card bordered title="地图运营能力" style={{ marginBottom: 16 }}>
<div className="vp-map-ops-board">
<div className="vp-map-ops-readiness">