import { Button, Card, Col, Form, Row, Select, Space, Spin, Table, Tag, Toast, Typography } from '@douyinfe/semi-ui'; import { IconSearch } from '@douyinfe/semi-icons'; import { useEffect, useState } from 'react'; import { api } from '../api/client'; import type { DashboardSummary, LinkHealth, ProtocolStat, QualityIssueRow, ServiceStatusStat, VehicleCoverageRow, VehicleRealtimeRow, VehicleServiceSummary } from '../api/types'; import { PageHeader } from '../components/PageHeader'; import { StatusTag } from '../components/StatusTag'; import { qualityIssueVehicleLookup } from '../domain/vehicleLookup'; const statusColor: Record = { ok: 'green', warning: 'orange', error: 'red' }; const serviceStatusColor: Record = { healthy: 'green', degraded: 'orange', offline: 'red', no_data: 'orange', identity_required: 'orange' }; const serviceStatusTitle: Record = { healthy: '服务正常', degraded: '来源异常', offline: '车辆离线', no_data: '暂无数据来源', identity_required: '身份未绑定' }; const missingProtocolTitle: Record = { GB32960: '缺 GB32960', JT808: '缺 JT808', YUTONG_MQTT: '缺 YUTONG_MQTT' }; function formatLag(value?: number | null) { return value == null ? '未接入' : value.toLocaleString(); } function formatCount(value?: number | null) { return value == null ? '0' : value.toLocaleString(); } function rowServiceStatus(row: { serviceStatus?: { title: string; severity: string }; onlineSourceCount: number; sourceCount: number }) { if (row.serviceStatus) { return { label: row.serviceStatus.title, color: row.serviceStatus.severity === 'ok' ? 'green' as const : row.serviceStatus.severity === 'error' ? 'red' as const : 'orange' as const }; } if (row.onlineSourceCount <= 0) { return { label: '车辆离线', color: 'red' as const }; } if (row.onlineSourceCount < row.sourceCount) { return { label: '来源异常', color: 'orange' as const }; } return { label: '服务正常', color: 'green' as const }; } function sourceEvidenceText(row: { onlineSourceCount: number; sourceCount: number }) { return `${row.onlineSourceCount}/${row.sourceCount} 来源在线`; } export function Dashboard({ onOpenVehicle, onOpenQuality, onOpenVehicles }: { onOpenVehicle: (vin: string) => void; onOpenQuality: () => void; onOpenVehicles: (filters?: Record) => void }) { const [summary, setSummary] = useState(null); const [serviceSummary, setServiceSummary] = useState(null); const [coverage, setCoverage] = useState([]); const [locations, setLocations] = useState([]); const [qualityIssues, setQualityIssues] = useState([]); const [loading, setLoading] = useState(true); const [coverageLoading, setCoverageLoading] = useState(false); const [coverageServiceStatusTitle, setCoverageServiceStatusTitle] = useState(''); const loadCoverage = (values?: Record) => { setCoverageLoading(true); const scopeTitle = [ values?.serviceStatus ? serviceStatusTitle[values.serviceStatus] ?? values.serviceStatus : '', values?.missingProtocol ? missingProtocolTitle[values.missingProtocol] ?? `缺 ${values.missingProtocol}` : '' ].filter(Boolean).join(' / '); setCoverageServiceStatusTitle(scopeTitle); const params = new URLSearchParams({ limit: '8' }); if (values?.keyword) params.set('keyword', values.keyword); if (values?.coverage) params.set('coverage', values.coverage); if (values?.missingProtocol) params.set('missingProtocol', values.missingProtocol); if (values?.online) params.set('online', values.online); if (values?.bindingStatus) params.set('bindingStatus', values.bindingStatus); if (values?.serviceStatus) params.set('serviceStatus', values.serviceStatus); api.vehicleCoverage(params) .then((page) => setCoverage(page.items)) .catch((error: Error) => Toast.error(error.message)) .finally(() => setCoverageLoading(false)); }; useEffect(() => { Promise.all([ api.dashboardSummary(), api.vehicleServiceSummary(), api.vehicleCoverage(new URLSearchParams({ limit: '8' })), api.vehicleRealtime(new URLSearchParams({ limit: '8' })), api.qualityIssues(new URLSearchParams({ limit: '5' })) ]) .then(([nextSummary, nextServiceSummary, coveragePage, locationPage, qualityPage]) => { setSummary(nextSummary); setServiceSummary(nextServiceSummary); setCoverage(coveragePage.items); setLocations(locationPage.items); setQualityIssues(qualityPage.items); }) .catch((error: Error) => Toast.error(error.message)) .finally(() => setLoading(false)); }, []); const kpis: Array<{ label: string; value: string; filters: Record }> = [ { label: '总车辆', value: formatCount(serviceSummary?.totalVehicles), filters: {} }, { label: '在线车辆', value: formatCount(serviceSummary?.onlineVehicles ?? summary?.onlineVehicles), filters: { online: 'online' } }, { label: '单源车辆', value: formatCount(serviceSummary?.singleSourceVehicles), filters: { coverage: 'single' } }, { label: '多源车辆', value: formatCount(serviceSummary?.multiSourceVehicles), filters: { coverage: 'multi' } }, { label: '暂无来源车辆', value: formatCount(serviceSummary?.noDataVehicles), filters: { serviceStatus: 'no_data' } }, { label: '身份未绑定', value: formatCount(serviceSummary?.identityRequiredVehicles), filters: { serviceStatus: 'identity_required' } } ]; const missingSourceCounts = new Map((serviceSummary?.missingSources ?? []).map((item) => [item.protocol, item.count])); return (
{kpis.map((item) => ( ))}
{row.title} }, { title: '车辆数', dataIndex: 'count' }, { title: '操作', width: 90, render: (_: unknown, row: ServiceStatusStat) => (
`${Math.round((row.online / row.total) * 100)}%` }, { title: '缺失车辆', render: (_: unknown, row: ProtocolStat) => { const missingCount = missingSourceCounts.get(row.protocol); if (missingCount == null) { return '-'; } return ( ); } } ]} />
{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: 'phone', width: 130 }, { title: '来源地址', dataIndex: 'sourceEndpoint', width: 180 }, { 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: '操作', width: 130, render: (_: unknown, row: QualityIssueRow) => { const lookup = qualityIssueVehicleLookup(row); return ; } } ]} /> {coverageServiceStatusTitle ? (
当前筛选:{coverageServiceStatusTitle}
) : null}
loadCoverage(values as Record)} style={{ marginBottom: 12 }}> 单源 多源 缺 GB32960 缺 JT808 缺 YUTONG_MQTT 在线 离线 已绑定 未绑定 服务正常 来源异常 车辆离线 暂无数据来源 身份未绑定
( {row.protocols.map((protocol) => {protocol})} ) }, { title: '证据覆盖', width: 130, render: (_: unknown, row: VehicleCoverageRow) => sourceEvidenceText(row) }, { title: '服务状态', width: 130, render: (_: unknown, row: VehicleCoverageRow) => { const status = rowServiceStatus(row); return {status.label}; } }, { 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) => ( ))}
( {row.protocols.map((protocol) => {protocol})} ) }, { title: '服务状态', render: (_: unknown, row: VehicleRealtimeRow) => { const status = rowServiceStatus(row); return {status.label}; } }, { title: '最后时间', dataIndex: 'lastSeen' }, { title: '操作', width: 110, render: (_: unknown, row: VehicleRealtimeRow) => } ]} /> ); }