import { Button, Card, Form, Select, Space, Table, Tabs, Tag, Toast, Typography } from '@douyinfe/semi-ui'; import { useEffect, useState } from 'react'; import { api } from '../api/client'; import type { VehicleRealtimeRow } from '../api/types'; import { DataEmpty } from '../components/DataEmpty'; 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'; function canOpenVehicle(vin?: string) { const value = vin?.trim(); return Boolean(value && value !== 'unknown'); } function vehicleServiceStatus(row: VehicleRealtimeRow) { 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: VehicleRealtimeRow) { return `${row.onlineSourceCount}/${row.sourceCount} 来源在线`; } function serviceStatusWeight(row: VehicleRealtimeRow) { const severity = row.serviceStatus?.severity; if (severity === 'error') return 4; if (severity === 'warning') return 3; if (row.onlineSourceCount <= 0) return 4; if (row.onlineSourceCount < row.sourceCount) return 3; return 1; } function isValidCoordinate(row: VehicleRealtimeRow) { return Number.isFinite(row.longitude) && Number.isFinite(row.latitude) && row.longitude !== 0 && row.latitude !== 0; } const onlineLabel: Record = { online: '在线', offline: '离线' }; const serviceStatusLabel: Record = { healthy: '服务正常', degraded: '来源不完整', offline: '车辆离线', identity_required: '身份未绑定' }; export function Realtime({ onOpenVehicle, onFiltersChange, initialFilters = {} }: { onOpenVehicle: (vin: string, protocol?: string) => void; onFiltersChange?: (filters: Record) => void; initialFilters?: Record; }) { const [rows, setRows] = useState([]); const [loading, setLoading] = useState(true); const [filters, setFilters] = useState>(initialFilters); const [pagination, setPagination] = useState({ currentPage: 1, pageSize: 50, total: 0 }); const amapConfigured = isAMapConfigured(); const load = (values: Record = filters, page = pagination.currentPage, pageSize = pagination.pageSize) => { setLoading(true); const params = new URLSearchParams({ limit: String(pageSize), offset: String((page - 1) * pageSize) }); if (values?.keyword) params.set('keyword', values.keyword); if (values?.protocol) params.set('protocol', values.protocol); if (values?.online) params.set('online', values.online); if (values?.serviceStatus) params.set('serviceStatus', values.serviceStatus); api.vehicleRealtime(params) .then((nextPage) => { setRows(nextPage.items); setPagination({ currentPage: page, pageSize, total: nextPage.total }); }) .catch((error: Error) => Toast.error(error.message)) .finally(() => setLoading(false)); }; useEffect(() => { setFilters(initialFilters); load(initialFilters, 1, pagination.pageSize); }, [JSON.stringify(initialFilters)]); const applyFilters = (nextFilters: Record) => { setFilters(nextFilters); onFiltersChange?.(nextFilters); load(nextFilters, 1, pagination.pageSize); }; const filterSummary = [ filters.keyword ? `关键词:${filters.keyword}` : '', filters.protocol ? `数据来源:${filters.protocol}` : '', filters.online ? `在线:${onlineLabel[filters.online] ?? filters.online}` : '', filters.serviceStatus ? `服务状态:${serviceStatusLabel[filters.serviceStatus] ?? filters.serviceStatus}` : '' ].filter(Boolean); const onlineCount = rows.filter((row) => row.online).length; const locatedCount = rows.filter(isValidCoordinate).length; const degradedCount = rows.filter((row) => row.onlineSourceCount < row.sourceCount).length; const primaryProtocols = new Set(rows.map((row) => row.primaryProtocol).filter(Boolean)); const mapServiceRows = rows .filter((row) => isValidCoordinate(row) && canOpenVehicle(row.vin)) .sort((a, b) => { const statusDelta = serviceStatusWeight(b) - serviceStatusWeight(a); if (statusDelta !== 0) return statusDelta; return String(b.lastSeen ?? '').localeCompare(String(a.lastSeen ?? '')); }) .slice(0, 5); const mapPoints: VehicleMapPoint[] = rows.map((row, index) => ({ id: row.vin || `${row.primaryProtocol}-${index}`, label: row.plate || row.vin || 'unknown', longitude: row.longitude, latitude: row.latitude, online: row.online, title: `${row.plate || row.vin || '-'} ${vehicleServiceStatus(row).label} ${row.primaryProtocol || ''} ${row.lastSeen || ''}` })); return (
{ const nextFilters = values as Record; applyFilters(nextFilters); }} style={{ marginBottom: 12 }}> GB32960 JT808 YUTONG_MQTT 在线 离线 服务正常 来源不完整 车辆离线 身份未绑定 {filterSummary.length > 0 ? ( {filterSummary.map((item) => ( {item} ))} ) : null}
{amapConfigured ? '高德地图配置就绪' : '高德地图待配置'} {locatedCount.toLocaleString()} 辆有定位 {onlineCount.toLocaleString()} 辆在线
{[ { label: '当前车辆', value: pagination.total.toLocaleString(), color: 'blue' as const }, { label: '在线车辆', value: onlineCount.toLocaleString(), color: 'green' as const }, { label: '定位有效', value: locatedCount.toLocaleString(), color: 'green' as const }, { label: '降级服务', value: degradedCount.toLocaleString(), color: degradedCount > 0 ? 'orange' as const : 'green' as const }, { label: '来源类型', value: primaryProtocols.size.toLocaleString(), color: 'blue' as const } ].map((item) => (
{item.label}
{item.value}
))} 高德 Web JS Key 和安全密钥通过运行环境配置,前端只读取公开 Key;安全密钥后续走服务端代理,避免明文下发。
地图车辆服务队列
{mapServiceRows.length === 0 ? ( 暂无可定位车辆 ) : ( mapServiceRows.map((row) => { const status = vehicleServiceStatus(row); return (
{row.plate || row.vin} {row.vin}
{status.label}
{row.serviceStatus?.detail || sourceEvidenceText(row)}
{row.lastSeen || '-'}
); }) )}
{rows.length === 0 && !loading ? ( ) : ( load(filters, page, pagination.pageSize), onPageSizeChange: (pageSize) => load(filters, 1, pageSize) }} columns={[ { title: '车牌', dataIndex: 'plate', width: 120 }, { title: 'VIN', dataIndex: 'vin', width: 190 }, { title: '车辆服务状态', width: 130, render: (_: unknown, row: VehicleRealtimeRow) => { const status = vehicleServiceStatus(row); return {status.label}; } }, { title: '车辆核心数据', width: 230, render: (_: unknown, row: VehicleRealtimeRow) => ( {row.speedKmh ?? '-'} km/h SOC {row.socPercent ?? '-'}% {row.totalMileageKm ?? '-'} km ) }, { title: '来源证据', width: 260, render: (_: unknown, row: VehicleRealtimeRow) => ( ) }, { title: '证据覆盖', width: 120, render: (_: unknown, row: VehicleRealtimeRow) => sourceEvidenceText(row) }, { title: '在线', width: 90, render: (_: unknown, row: VehicleRealtimeRow) => }, { title: '最后时间', dataIndex: 'lastSeen', width: 170 }, { title: '操作', width: 110, render: (_: unknown, row: VehicleRealtimeRow) => ( ) } ]} /> )} ); }