import { Button, Card, Col, Form, Row, Select, Space, Spin, Table, Tag, Toast, Typography } from '@douyinfe/semi-ui'; import { useEffect, useState } from 'react'; import { api } from '../api/client'; import type { DashboardSummary, LinkHealth, ProtocolStat, QualityIssueRow, VehicleCoverageRow, VehicleRealtimeRow, VehicleRow } from '../api/types'; import { PageHeader } from '../components/PageHeader'; import { StatusTag } from '../components/StatusTag'; const statusColor: Record = { ok: 'green', warning: 'orange', error: 'red' }; function formatLag(value?: number | null) { return value == null ? '未接入' : value.toLocaleString(); } export function Dashboard({ onOpenVehicle, onOpenQuality }: { onOpenVehicle: (vin: string) => void; onOpenQuality: () => void }) { const [summary, setSummary] = useState(null); const [vehicles, setVehicles] = useState([]); const [coverage, setCoverage] = useState([]); const [locations, setLocations] = useState([]); const [qualityIssues, setQualityIssues] = useState([]); const [loading, setLoading] = useState(true); const [coverageLoading, setCoverageLoading] = useState(false); const loadCoverage = (values?: Record) => { setCoverageLoading(true); const params = new URLSearchParams({ limit: '8' }); if (values?.keyword) params.set('keyword', values.keyword); if (values?.coverage) params.set('coverage', values.coverage); if (values?.online) params.set('online', values.online); if (values?.bindingStatus) params.set('bindingStatus', values.bindingStatus); api.vehicleCoverage(params) .then((page) => setCoverage(page.items)) .catch((error: Error) => Toast.error(error.message)) .finally(() => setCoverageLoading(false)); }; useEffect(() => { Promise.all([ api.dashboardSummary(), api.vehicles(new URLSearchParams({ limit: '5' })), api.vehicleCoverage(new URLSearchParams({ limit: '8' })), api.vehicleRealtime(new URLSearchParams({ limit: '8' })), api.qualityIssues(new URLSearchParams({ limit: '5' })) ]) .then(([nextSummary, vehiclePage, coveragePage, locationPage, qualityPage]) => { setSummary(nextSummary); setVehicles(vehiclePage.items); setCoverage(coveragePage.items); setLocations(locationPage.items); setQualityIssues(qualityPage.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 (
{kpis.map((item) => (
{item.value}
{item.label}
))}
`${Math.round((row.online / row.total) * 100)}%` } ]} />
{row.status} }, { title: '说明', dataIndex: 'detail' } ]} /> Kafka 当前消费积压:{formatLag(summary?.kafkaLag)} 质量问题预览} bordered style={{ marginTop: 16 }} >
`${row?.protocol ?? ''}-${row?.issueType ?? ''}-${row?.lastSeen ?? ''}-${row?.detail ?? ''}`} dataSource={qualityIssues} columns={[ { title: 'VIN', dataIndex: 'vin', width: 160 }, { title: '车牌', dataIndex: 'plate', width: 110 }, { title: '来源', dataIndex: 'protocol', width: 110 }, { title: '问题', dataIndex: 'issueType', width: 140 }, { title: '级别', width: 90, render: (_: unknown, row: QualityIssueRow) => {row.severity} }, { title: '最后时间', dataIndex: 'lastSeen', width: 170 }, { title: '说明', dataIndex: 'detail' } ]} />
loadCoverage(values as Record)} style={{ marginBottom: 12 }}> 单源 多源 在线 离线 已绑定 未绑定
( {row.protocols.map((protocol) => {protocol})} ) }, { title: '覆盖', width: 110, render: (_: unknown, row: VehicleCoverageRow) => `${row.onlineSourceCount}/${row.sourceCount}` }, { title: '在线', width: 90, render: (_: unknown, row: VehicleCoverageRow) => }, { title: '绑定', width: 90, render: (_: unknown, row: VehicleCoverageRow) => {row.bindingStatus === 'bound' ? '已绑定' : '未绑定'} }, { title: '最后时间', dataIndex: 'lastSeen', width: 170 }, { title: '操作', width: 110, render: (_: unknown, row: VehicleCoverageRow) => } ]} />
{locations.map((row, index) => ( ))}
); }