feat(platform): add vehicle data management console

This commit is contained in:
lingniu
2026-07-03 20:55:54 +08:00
parent 0d8916df47
commit 859bc3e9ee
45 changed files with 6837 additions and 0 deletions

View File

@@ -0,0 +1,124 @@
import { Card, Col, Row, Spin, Table, Tag, Toast, Typography } from '@douyinfe/semi-ui';
import { useEffect, useState } from 'react';
import { api } from '../api/client';
import type { DashboardSummary, LinkHealth, ProtocolStat, RealtimeLocationRow, VehicleRow } from '../api/types';
import { PageHeader } from '../components/PageHeader';
const statusColor: Record<string, 'green' | 'orange' | 'red' | 'grey'> = {
ok: 'green',
warning: 'orange',
error: 'red'
};
export function Dashboard() {
const [summary, setSummary] = useState<DashboardSummary | null>(null);
const [vehicles, setVehicles] = useState<VehicleRow[]>([]);
const [locations, setLocations] = useState<RealtimeLocationRow[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
Promise.all([
api.dashboardSummary(),
api.vehicles(new URLSearchParams({ limit: '5' })),
api.realtimeLocations(new URLSearchParams({ limit: '8' }))
])
.then(([nextSummary, vehiclePage, locationPage]) => {
setSummary(nextSummary);
setVehicles(vehiclePage.items);
setLocations(locationPage.items);
})
.catch((error: Error) => Toast.error(error.message))
.finally(() => setLoading(false));
}, []);
const kpis = [
{ label: '在线车辆', value: summary?.onlineVehicles ?? 0 },
{ label: '今日活跃', value: summary?.activeToday ?? 0 },
{ label: '今日帧数', value: summary?.frameToday.toLocaleString() ?? '0' },
{ label: '问题车辆', value: summary?.issueVehicles ?? 0 }
];
return (
<div className="vp-page">
<PageHeader title="总览工作台" description="车辆在线、协议分布、数据质量和链路健康的统一入口" />
<Spin spinning={loading}>
<div className="vp-kpi-grid">
{kpis.map((item) => (
<Card key={item.label} bordered>
<div className="vp-kpi-value">{item.value}</div>
<div className="vp-kpi-label">{item.label}</div>
</Card>
))}
</div>
<Row gutter={16}>
<Col span={14}>
<Card title="协议在线分布" bordered>
<Table
pagination={false}
dataSource={summary?.protocols ?? []}
columns={[
{ title: '协议', dataIndex: 'protocol' },
{ title: '在线', dataIndex: 'online' },
{ title: '总数', dataIndex: 'total' },
{
title: '在线率',
render: (_: unknown, row: ProtocolStat) => `${Math.round((row.online / row.total) * 100)}%`
}
]}
/>
</Card>
</Col>
<Col span={10}>
<Card title="链路健康" bordered>
<Table
pagination={false}
dataSource={summary?.linkHealth ?? []}
columns={[
{ title: '链路', dataIndex: 'name' },
{
title: '状态',
render: (_: unknown, row: LinkHealth) => <Tag color={statusColor[row.status] ?? 'grey'}>{row.status}</Tag>
},
{ title: '说明', dataIndex: 'detail' }
]}
/>
</Card>
</Col>
</Row>
<Card title="实时积压" bordered style={{ marginTop: 16 }}>
<Typography.Text>Kafka {summary?.kafkaLag ?? 0}</Typography.Text>
</Card>
<Row gutter={16} style={{ marginTop: 16 }}>
<Col span={12}>
<Card title="实时位置预览" bordered>
<div className="vp-map" style={{ height: 260 }}>
{locations.map((row, index) => (
<span
key={row.vin}
className="vp-map-dot"
title={`${row.plate} ${row.protocol}`}
style={{ left: `${18 + index * 13}%`, top: `${24 + (index % 4) * 15}%` }}
/>
))}
</div>
</Card>
</Col>
<Col span={12}>
<Card title="最新车辆" bordered>
<Table
pagination={false}
dataSource={vehicles}
columns={[
{ title: '车牌', dataIndex: 'plate' },
{ title: 'VIN', dataIndex: 'vin' },
{ title: '协议', dataIndex: 'protocol' },
{ title: '最后时间', dataIndex: 'lastSeen' }
]}
/>
</Card>
</Col>
</Row>
</Spin>
</div>
);
}