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, useMemo, 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 { VehicleMap, type VehicleMapPoint } from '../components/VehicleMap'; import { isAMapConfigured } from '../config/appConfig'; 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 hasValidCoordinate(row: VehicleRealtimeRow) { return Number.isFinite(row.longitude) && Number.isFinite(row.latitude) && row.longitude !== 0 && row.latitude !== 0; } 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, onOpenRealtime, onOpenVehicles }: { onOpenVehicle: (vin: string, protocol?: string) => void; onOpenQuality: (filters?: Record) => void; onOpenRealtime: (filters?: Record) => 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 amapConfigured = isAMapConfigured(); 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' } }, { label: '档案不完整', value: formatCount(serviceSummary?.archiveIncompleteVehicles), filters: { archiveStatus: 'incomplete' } } ]; const missingSourceCounts = new Map((serviceSummary?.missingSources ?? []).map((item) => [item.protocol, item.count])); const serviceActionQueue = useMemo; detail: string }>>(() => { const items: Array<{ label: string; count: number; filters: Record; detail: string }> = []; if ((serviceSummary?.noDataVehicles ?? 0) > 0) { items.push({ label: '确认平台转发', count: serviceSummary?.noDataVehicles ?? 0, filters: { serviceStatus: 'no_data' }, detail: '车辆没有形成任何来源证据,优先确认上游平台是否持续转发。' }); } if ((serviceSummary?.identityRequiredVehicles ?? 0) > 0) { items.push({ label: '维护身份绑定', count: serviceSummary?.identityRequiredVehicles ?? 0, filters: { serviceStatus: 'identity_required' }, detail: '已有数据但无法稳定归并到 VIN,会影响车辆服务聚合。' }); } if ((serviceSummary?.archiveIncompleteVehicles ?? 0) > 0) { items.push({ label: '完善车辆档案', count: serviceSummary?.archiveIncompleteVehicles ?? 0, filters: { archiveStatus: 'incomplete' }, detail: '车辆缺少车牌、手机号或 OEM 等基础档案,影响后续运营查询和治理。' }); } for (const field of serviceSummary?.archiveMissingFields ?? []) { if (field.count <= 0) continue; items.push({ label: `补齐${field.title}`, count: field.count, filters: { archiveMissing: field.field }, detail: `${field.title}会影响车辆档案检索、绑定确认和运营侧筛选。` }); } for (const source of serviceSummary?.missingSources ?? []) { if (source.count <= 0) continue; items.push({ label: `补齐 ${source.protocol} 来源`, count: source.count, filters: { serviceStatus: 'degraded', missingProtocol: source.protocol }, detail: `${source.protocol} 来源缺失会降低跨来源定位、里程和实时判断可信度。` }); } return items; }, [serviceSummary]); const commandOnlineCount = locations.filter((row) => row.online).length; const commandLocatedCount = locations.filter(hasValidCoordinate).length; const commandDegradedCount = locations.filter((row) => row.onlineSourceCount <= 0 || row.onlineSourceCount < row.sourceCount).length; const highPriorityIssue = qualityIssues.find((item) => item.severity === 'error') ?? qualityIssues[0]; const commandMapPoints: VehicleMapPoint[] = locations.map((row, index) => ({ id: row.vin || `${row.primaryProtocol || 'source'}-${index}`, label: row.plate || row.vin || 'unknown', longitude: row.longitude, latitude: row.latitude, online: row.online, title: `${row.plate || row.vin || '-'} ${row.primaryProtocol || ''} ${row.lastSeen || ''}` })); return (
{kpis.map((item) => ( ))}
{vehicleServiceOnlineText(serviceSummary, summary)} {formatCount(serviceSummary?.multiSourceVehicles)} 多源覆盖 0 ? 'orange' : 'green'}> {formatCount(summary?.issueVehicles)} 质量关注 0 ? 'orange' : 'green'}>Kafka Lag {formatLag(summary?.kafkaLag)}
{amapConfigured ? '高德地图配置就绪' : '高德地图待配置'} 在线 {commandOnlineCount.toLocaleString()} / {locations.length.toLocaleString()} 有效定位 {commandLocatedCount.toLocaleString()} 0 ? 'orange' : 'green'}>降级/离线 {commandDegradedCount.toLocaleString()}
实时作业
{commandOnlineCount.toLocaleString()}
当前预览车辆在线数,进入实时监控可按车辆、来源和服务状态筛选。
0 ? 'orange' : 'green'}>处置优先级
{commandDegradedCount.toLocaleString()}
优先处理离线、无来源、身份未绑定和来源不完整车辆。
{serviceActionQueue.length > 0 ? (
{serviceActionQueue.map((item) => (
{item.label} {item.count.toLocaleString()} {item.detail}
))}
) : null} {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) => } ]} /> ); }