feat(platform): refocus dashboard on vehicle operations
This commit is contained in:
@@ -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, SourceReadinessPlan, SourceReadinessRow, 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';
|
||||
@@ -93,15 +93,6 @@ 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 {
|
||||
@@ -268,7 +259,6 @@ 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('');
|
||||
@@ -308,8 +298,7 @@ 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.sourceReadiness().then((plan) => setSourceReadiness(normalizeSourceReadiness(plan)))
|
||||
api.opsHealth().then(setOpsHealth)
|
||||
];
|
||||
Promise.allSettled(tasks)
|
||||
.then((results) => {
|
||||
@@ -394,6 +383,66 @@ export function Dashboard({
|
||||
online: row.online,
|
||||
title: `${row.plate || row.vin || '-'} ${row.primaryProtocol || ''} ${row.lastSeen || ''}`
|
||||
}));
|
||||
const customerHeroActions = [
|
||||
{
|
||||
title: '实时车辆地图',
|
||||
value: `${commandLocatedCount.toLocaleString()} 辆有定位`,
|
||||
detail: '按车查看实时位置、在线状态、速度和最新上报时间。',
|
||||
action: '进入地图',
|
||||
onClick: () => onOpenMap({ online: 'online' })
|
||||
},
|
||||
{
|
||||
title: '轨迹回放',
|
||||
value: `${formatCount(summary?.activeToday)} 今日活跃`,
|
||||
detail: '选择车辆和时间范围,回放历史路线并核对断点。',
|
||||
action: '回放轨迹',
|
||||
onClick: () => onOpenHistory()
|
||||
},
|
||||
{
|
||||
title: '里程统计',
|
||||
value: `${formatCount(serviceSummary?.totalVehicles)} 辆可统计`,
|
||||
detail: '按自定义时间查询区间里程、日里程和异常记录。',
|
||||
action: '查看统计',
|
||||
onClick: () => onOpenMileage({})
|
||||
},
|
||||
{
|
||||
title: '历史数据导出',
|
||||
value: `${formatCount(summary?.frameToday)} 今日数据`,
|
||||
detail: '按车辆、时间和字段裁剪查询历史位置与 RAW 证据。',
|
||||
action: '查询导出',
|
||||
onClick: () => onOpenHistory({ tab: 'raw', includeFields: 'true' })
|
||||
}
|
||||
];
|
||||
const customerHealthItems = [
|
||||
{
|
||||
label: '在线车辆',
|
||||
value: formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles),
|
||||
detail: vehicleServiceOnlineText(serviceSummary, summary),
|
||||
color: 'green' as const,
|
||||
onClick: () => onOpenVehicles({ online: 'online' })
|
||||
},
|
||||
{
|
||||
label: '有效定位',
|
||||
value: commandLocatedCount.toLocaleString(),
|
||||
detail: `当前抽样 ${locations.length.toLocaleString()} 辆,定位可用于地图和回放。`,
|
||||
color: commandLocatedCount > 0 ? 'blue' as const : 'orange' as const,
|
||||
onClick: () => onOpenMap({ online: 'online' })
|
||||
},
|
||||
{
|
||||
label: '需要关注',
|
||||
value: formatCount(summary?.issueVehicles),
|
||||
detail: highPriorityIssue ? `${qualityIssueLabel(highPriorityIssue.issueType)} / ${priorityIssueVehicleLabel(highPriorityIssue)}` : '暂无高优先级告警。',
|
||||
color: (summary?.issueVehicles ?? 0) > 0 ? 'orange' as const : 'green' as const,
|
||||
onClick: () => onOpenQuality()
|
||||
},
|
||||
{
|
||||
label: '时间范围监控',
|
||||
value: formatCount(summary?.activeToday),
|
||||
detail: '支持按任意时间窗查询轨迹、历史数据、里程和导出。',
|
||||
color: 'blue' as const,
|
||||
onClick: () => onOpenHistory()
|
||||
}
|
||||
];
|
||||
const capabilities = [
|
||||
{
|
||||
title: '实时地图',
|
||||
@@ -765,36 +814,6 @@ export function Dashboard({
|
||||
].filter(Boolean);
|
||||
copyText(lines.join('\n'), '运营交接摘要');
|
||||
};
|
||||
const copySourceReadinessHandoff = () => {
|
||||
const plan = sourceReadiness;
|
||||
if (!plan?.sources?.length) {
|
||||
Toast.warning('暂无数据源就绪度数据');
|
||||
return;
|
||||
}
|
||||
const lines = [
|
||||
'【三源接入到车辆服务交接摘要】',
|
||||
`车辆规模:${formatCount(plan.onlineVehicles)} / ${formatCount(plan.totalVehicles)} 在线`,
|
||||
`接入运行:连接 ${formatCount(plan.activeConnections)};Redis 在线 Key ${formatCount(plan.redisOnlineKeys)};Kafka Lag ${formatLag(plan.kafkaLag)}`,
|
||||
`平台版本:${plan.platformRelease || '-'}`,
|
||||
'',
|
||||
...plan.sources.map((item, index) => [
|
||||
`${index + 1}. ${item.protocol} - ${item.status}`,
|
||||
` 角色:${item.role}`,
|
||||
` 在线:${formatCount(item.online)} / ${formatCount(item.total)},在线率 ${item.onlineRate.toLocaleString(undefined, { maximumFractionDigits: 1 })}%`,
|
||||
` 缺失车辆:${formatCount(item.missingVehicles)}`,
|
||||
` 证据:${item.evidence}`,
|
||||
` 动作:${item.action}`,
|
||||
` 验收:${item.acceptance}`,
|
||||
` 实时:${appURL(item.realtimeHash)}`,
|
||||
` 车辆:${appURL(item.vehiclesHash)}`,
|
||||
` RAW:${appURL(item.historyHash)}`,
|
||||
` 告警:${appURL(item.alertHash)}`
|
||||
].join('\n')),
|
||||
'',
|
||||
`驾驶舱:${appURL(buildAppHash({ page: 'dashboard' }))}`
|
||||
];
|
||||
copyText(lines.join('\n'), '三源接入交接摘要');
|
||||
};
|
||||
const copyScenarioBlueprint = () => {
|
||||
const lines = [
|
||||
'【车辆数据中台功能蓝图】',
|
||||
@@ -968,8 +987,59 @@ export function Dashboard({
|
||||
|
||||
return (
|
||||
<div className="vp-page">
|
||||
<PageHeader title="运营驾驶舱" description="三个数据源最终汇总为一个车辆服务,统一承载实时监控、轨迹回放、历史查询、告警通知、统计查询和链路质量" />
|
||||
<PageHeader title="车辆运营监控台" description="面向客户的车辆地图、实时状态、轨迹回放、里程统计、历史查询和数据导出入口" />
|
||||
<Spin spinning={loading}>
|
||||
<Card bordered className="vp-customer-hero" bodyStyle={{ padding: 0 }}>
|
||||
<div className="vp-customer-hero-map">
|
||||
<div className="vp-customer-hero-copy">
|
||||
<Typography.Title heading={3} style={{ margin: 0 }}>先看车,再看数据来源</Typography.Title>
|
||||
<Typography.Text type="secondary">
|
||||
车辆是主对象。32960、808 和宇通 MQTT 只是证据来源,客户每天需要先确认车辆在哪里、是否在线、今天跑了多少、异常能否追溯和导出。
|
||||
</Typography.Text>
|
||||
<Space wrap>
|
||||
<Button theme="solid" type="primary" onClick={() => onOpenMap({ online: 'online' })}>打开实时地图</Button>
|
||||
<Button theme="light" type="primary" onClick={() => onOpenHistory()}>轨迹回放</Button>
|
||||
<Button theme="light" type="primary" onClick={() => onOpenMileage({})}>里程统计</Button>
|
||||
<Button theme="light" onClick={exportDashboardSnapshot}>导出概览</Button>
|
||||
</Space>
|
||||
</div>
|
||||
<VehicleMap
|
||||
points={commandMapPoints}
|
||||
maxFallbackPoints={80}
|
||||
heightClassName="vp-customer-hero-map-canvas"
|
||||
fallbackLabel="显示车辆实时坐标预览"
|
||||
/>
|
||||
</div>
|
||||
<div className="vp-customer-hero-side">
|
||||
<div className="vp-customer-hero-side-head">
|
||||
<Tag color={amapConfigured ? 'green' : 'orange'}>{amapConfigured ? '地图可用' : '地图待配置'}</Tag>
|
||||
<Tag color={(summary?.issueVehicles ?? 0) > 0 ? 'orange' : 'green'}>{formatCount(summary?.issueVehicles)} 个告警</Tag>
|
||||
</div>
|
||||
<div className="vp-customer-health-grid">
|
||||
{customerHealthItems.map((item) => (
|
||||
<button key={item.label} type="button" className="vp-customer-health-item" onClick={item.onClick} aria-label={`车辆运营 ${item.label} ${item.value}`}>
|
||||
<Tag color={item.color}>{item.label}</Tag>
|
||||
<strong>{item.value}</strong>
|
||||
<span>{item.detail}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card bordered title="客户常用工作流" style={{ marginBottom: 16 }}>
|
||||
<div className="vp-customer-workflow-grid">
|
||||
{customerHeroActions.map((item) => (
|
||||
<button key={item.title} type="button" className="vp-customer-workflow-item" onClick={item.onClick} aria-label={`客户工作流 ${item.title}`}>
|
||||
<div>
|
||||
<Typography.Title heading={6} style={{ margin: 0 }}>{item.title}</Typography.Title>
|
||||
<Typography.Text type="secondary">{item.detail}</Typography.Text>
|
||||
</div>
|
||||
<strong>{item.value}</strong>
|
||||
<Tag color="blue">{item.action}</Tag>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
<div className="vp-kpi-grid">
|
||||
{kpis.map((item) => (
|
||||
<Card key={item.label} bordered className="vp-kpi-card" bodyStyle={{ padding: 0 }}>
|
||||
@@ -995,52 +1065,6 @@ export function Dashboard({
|
||||
<Button size="small" theme="light" type="primary" onClick={exportDashboardSnapshot}>导出驾驶舱 CSV</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
<Card
|
||||
bordered
|
||||
title={<Space wrap><span>数据源到车辆服务就绪度</span><Button size="small" theme="light" disabled={!sourceReadiness?.sources?.length} onClick={copySourceReadinessHandoff}>复制接入交接</Button></Space>}
|
||||
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">数据源就绪度加载后会展示 32960、808 和宇通 MQTT 对车辆服务的支撑状态。</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
<Card bordered title="地图运营能力" style={{ marginBottom: 16 }}>
|
||||
<div className="vp-map-ops-board">
|
||||
<div className="vp-map-ops-readiness">
|
||||
|
||||
Reference in New Issue
Block a user