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 { SourceStatusTags } from '../components/SourceStatusTags'; 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 vehicleServiceOnlineText(serviceSummary: VehicleServiceSummary | null, summary: DashboardSummary | null) { const onlineVehicles = serviceSummary?.onlineVehicles ?? summary?.onlineVehicles; const totalVehicles = serviceSummary?.totalVehicles; if (onlineVehicles == null || totalVehicles == null) { return '未接入'; } return `${onlineVehicles.toLocaleString()} / ${totalVehicles.toLocaleString()} 在线`; } function formatProtocolRate(row: ProtocolStat) { if (!Number.isFinite(row.total) || row.total <= 0) { return '0%'; } return `${Math.round((row.online / row.total) * 100)}%`; } 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} 来源在线`; } function sourceConsistencyAction(row: VehicleCoverageRow, onFilter: (filters: Record) => void) { const consistency = row.sourceConsistency; if (!consistency) { return 未诊断; } const color = consistency.severity === 'ok' ? 'green' as const : consistency.severity === 'error' ? 'red' as const : 'orange' as const; const label = consistency.title || consistency.status; if ((consistency.missingProtocols ?? []).length > 0) { return ( ); } return {label}; } export function Dashboard({ onOpenVehicle, onOpenQuality, onOpenVehicles }: { onOpenVehicle: (vin: string, protocol?: 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 [coverageFilters, setCoverageFilters] = useState>({}); const loadCoverage = (values?: Record) => { const nextValues = values ?? {}; setCoverageLoading(true); setCoverageFilters(nextValues); const scopeTitle = [ nextValues.serviceStatus ? serviceStatusTitle[nextValues.serviceStatus] ?? nextValues.serviceStatus : '', nextValues.missingProtocol ? missingProtocolTitle[nextValues.missingProtocol] ?? `缺 ${nextValues.missingProtocol}` : '' ].filter(Boolean).join(' / '); setCoverageServiceStatusTitle(scopeTitle); const params = new URLSearchParams({ limit: '8' }); if (nextValues.keyword) params.set('keyword', nextValues.keyword); if (nextValues.coverage) params.set('coverage', nextValues.coverage); if (nextValues.missingProtocol) params.set('missingProtocol', nextValues.missingProtocol); if (nextValues.online) params.set('online', nextValues.online); if (nextValues.bindingStatus) params.set('bindingStatus', nextValues.bindingStatus); if (nextValues.serviceStatus) params.set('serviceStatus', nextValues.serviceStatus); api.vehicleCoverage(params) .then((page) => setCoverage(page.items)) .catch((error: Error) => Toast.error(error.message)) .finally(() => setCoverageLoading(false)); }; useEffect(() => { const tasks = [ api.dashboardSummary().then(setSummary), api.vehicleServiceSummary().then(setServiceSummary), api.vehicleCoverage(new URLSearchParams({ limit: '8' })).then((page) => setCoverage(page.items)), api.vehicleRealtime(new URLSearchParams({ limit: '8' })).then((page) => setLocations(page.items)), api.qualityIssues(new URLSearchParams({ limit: '5' })).then((page) => setQualityIssues(page.items)) ]; Promise.allSettled(tasks) .then((results) => { const failed = results.find((result) => result.status === 'rejected'); if (failed?.status === 'rejected') { const reason = failed.reason; Toast.error(reason instanceof Error ? reason.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) => ( ))}
{vehicleServiceOnlineText(serviceSummary, summary)} {formatCount(serviceSummary?.multiSourceVehicles)} 多源覆盖 0 ? 'orange' : 'green'}> {formatCount(summary?.issueVehicles)} 质量关注 0 ? 'orange' : 'green'}>Kafka Lag {formatLag(summary?.kafkaLag)} {row.title} }, { title: '车辆数', dataIndex: 'count' }, { title: '操作', width: 90, render: (_: unknown, row: ServiceStatusStat) => (
formatProtocolRate(row) }, { 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 ; } } ]} /> 车辆服务覆盖} bordered style={{ marginTop: 16 }} > {coverageServiceStatusTitle ? (
当前筛选:{coverageServiceStatusTitle}
) : null}
loadCoverage(values as Record)} style={{ marginBottom: 12 }}> 单源 多源 缺 GB32960 缺 JT808 缺 YUTONG_MQTT 在线 离线 已绑定 未绑定 服务正常 来源不完整 车辆离线 暂无数据来源 身份未绑定
( ) }, { 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: 140, render: (_: unknown, row: VehicleCoverageRow) => sourceConsistencyAction(row, loadCoverage) }, { 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) => } ]} /> ); }