import { Button, Card, Form, Select, Space, Table, Tabs, Tag, Toast, Typography } from '@douyinfe/semi-ui'; import { IconCopy } from '@douyinfe/semi-icons'; 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 { getAMapConfig, isAMapConfigured } from '../config/appConfig'; import { buildCsv, downloadCsv, type CsvColumn } from '../domain/csvExport'; 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 formatPercent(value: number) { return Number.isFinite(value) ? `${value.toLocaleString(undefined, { maximumFractionDigits: 1 })}%` : '0%'; } 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 sourceIssueTags(row: VehicleRealtimeRow) { const tags = (row.sourceStatus ?? []) .filter((source) => !source.online || !source.hasRealtime) .map((source) => `${source.protocol} ${source.hasRealtime ? '离线' : '未接入'}`) ?? []; if (tags.length > 0) return tags; if (row.onlineSourceCount < row.sourceCount) return ['来源缺失']; return []; } function hasSourceIssue(row: VehicleRealtimeRow) { const severity = row.serviceStatus?.severity; return severity === 'warning' || severity === 'error' || row.onlineSourceCount < row.sourceCount || sourceIssueTags(row).length > 0; } function isValidCoordinate(row: VehicleRealtimeRow) { return Number.isFinite(row.longitude) && Number.isFinite(row.latitude) && row.longitude !== 0 && row.latitude !== 0; } function amapMarkerURL(row: VehicleRealtimeRow) { const name = encodeURIComponent(row.plate || row.vin || '车辆位置'); return `https://uri.amap.com/marker?position=${row.longitude},${row.latitude}&name=${name}&src=lingniu-vehicle-platform`; } const onlineLabel: Record = { online: '在线', offline: '离线' }; const serviceStatusLabel: Record = { healthy: '服务正常', degraded: '来源不完整', offline: '车辆离线', identity_required: '身份未绑定' }; const realtimeExportColumns: CsvColumn[] = [ { title: 'VIN', value: (row) => row.vin }, { title: '车牌', value: (row) => row.plate }, { title: '手机号', value: (row) => row.phone }, { title: 'OEM', value: (row) => row.oem }, { title: '主来源', value: (row) => row.primaryProtocol }, { title: '来源列表', value: (row) => row.protocols?.join('|') }, { title: '在线', value: (row) => row.online ? '在线' : '离线' }, { title: '车辆服务状态', value: (row) => vehicleServiceStatus(row).label }, { title: '来源在线', value: (row) => sourceEvidenceText(row) }, { title: '经度', value: (row) => row.longitude }, { title: '纬度', value: (row) => row.latitude }, { title: '速度km/h', value: (row) => row.speedKmh }, { title: 'SOC%', value: (row) => row.socPercent }, { title: '总里程km', value: (row) => row.totalMileageKm }, { title: '最后时间', value: (row) => row.lastSeen }, { title: '绑定状态', value: (row) => row.bindingStatus } ]; function realtimeExportFileName(filters: Record) { const keyword = filters.keyword?.trim() || 'all'; const protocol = filters.protocol?.trim() || 'all-source'; const online = filters.online?.trim() || 'all-online'; return `realtime-vehicles-${keyword}-${protocol}-${online}.csv`; } function realtimeFilterSummary(filters: Record) { return [ filters.keyword ? `关键词:${filters.keyword}` : '', filters.protocol ? `数据来源:${filters.protocol}` : '', filters.online ? `在线:${onlineLabel[filters.online] ?? filters.online}` : '', filters.serviceStatus ? `服务状态:${serviceStatusLabel[filters.serviceStatus] ?? filters.serviceStatus}` : '' ].filter(Boolean); } function realtimeOperationsSummaryText({ filters, rows, total, onlineCount, locatedCount, degradedCount, sourceTypeCount, amapConfigured, sourceIssueRows }: { filters: Record; rows: VehicleRealtimeRow[]; total: number; onlineCount: number; locatedCount: number; degradedCount: number; sourceTypeCount: number; amapConfigured: boolean; sourceIssueRows: VehicleRealtimeRow[]; }) { const issueLines = sourceIssueRows.length > 0 ? sourceIssueRows.map((row, index) => { const status = vehicleServiceStatus(row); return `${index + 1}. ${row.plate || row.vin} / ${row.primaryProtocol || '-'} / ${status.label} / ${row.serviceStatus?.detail || sourceEvidenceText(row)}`; }).join('\n') : '暂无重点车辆'; return [ '【实时监控摘要】', `当前筛选:${realtimeFilterSummary(filters).join(';') || '全部实时车辆'}`, `车辆总数:${total.toLocaleString()},当前页:${rows.length.toLocaleString()}`, `在线车辆:${onlineCount.toLocaleString()},定位有效:${locatedCount.toLocaleString()}`, `降级服务:${degradedCount.toLocaleString()},来源类型:${sourceTypeCount.toLocaleString()}`, `地图配置:${amapConfigured ? '已配置' : '未配置'}`, '重点车辆:', issueLines, `实时页面:${window.location.origin}${window.location.pathname}${window.location.hash}` ].join('\n'); } async function copyText(value: string, label: string) { const text = value.trim(); if (!text) { Toast.warning(`${label}为空`); return; } try { await navigator.clipboard.writeText(text); Toast.success(`已复制${label}`); } catch { Toast.error(`复制${label}失败`); } } export function Realtime({ onOpenVehicle, onOpenHistory, onOpenQuality, onFiltersChange, initialFilters = {} }: { onOpenVehicle: (vin: string, protocol?: string) => void; onOpenHistory?: (vin: string, protocol?: string) => void; onOpenQuality?: (filters: Record) => 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 amapConfig = getAMapConfig(); const amapConfigured = isAMapConfigured(amapConfig); 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 openQualityEvidence = (row: VehicleRealtimeRow) => { if (!canOpenVehicle(row.vin)) return; onOpenQuality?.({ keyword: row.vin, protocol: filters.protocol || row.primaryProtocol || '' }); }; const exportRealtime = () => { if (rows.length === 0) { Toast.warning('当前没有可导出的实时车辆'); return; } downloadCsv(realtimeExportFileName(filters), buildCsv(realtimeExportColumns, rows)); Toast.success(`已导出 ${rows.length} 条实时车辆`); }; const filterSummary = realtimeFilterSummary(filters); 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 onlineRate = rows.length > 0 ? (onlineCount / rows.length) * 100 : 0; const locatedRate = rows.length > 0 ? (locatedCount / rows.length) * 100 : 0; const degradedRate = rows.length > 0 ? (degradedCount / rows.length) * 100 : 0; const pageCoverageRate = pagination.total > 0 ? (rows.length / pagination.total) * 100 : 0; const primaryProtocols = new Set(rows.map((row) => row.primaryProtocol).filter(Boolean)); const sourceIssueRows = rows .filter((row) => canOpenVehicle(row.vin) && hasSourceIssue(row)) .sort((a, b) => { const statusDelta = serviceStatusWeight(b) - serviceStatusWeight(a); if (statusDelta !== 0) return statusDelta; return String(b.lastSeen ?? '').localeCompare(String(a.lastSeen ?? '')); }) .slice(0, 4); 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 || ''}` })); const mapIntegrationRows = [ { label: 'Web JS Key', value: amapConfigured ? '已配置' : '未配置', color: amapConfigured ? 'green' as const : 'orange' as const, detail: amapConfigured ? '前端可加载高德 Web JS API。' : '缺少公开 Key,地图将使用坐标预览。' }, { label: '安全代理', value: amapConfig.securityServiceHost || '未启用', color: amapConfig.securityServiceHost ? 'green' as const : 'grey' as const, detail: amapConfig.securityServiceHost ? '安全密钥由服务端追加,不下发到浏览器。' : '未配置代理时会退回前端安全码模式。' }, { label: '定位覆盖', value: `${locatedCount.toLocaleString()} / ${rows.length.toLocaleString()}`, color: locatedCount > 0 ? 'blue' as const : 'orange' as const, detail: '当前页有效经纬度车辆数。' }, { label: '高德 URI', value: '坐标跳转', color: 'blue' as const, detail: '车辆队列可直接打开高德坐标,轨迹页可打开线路。' } ]; const copyRealtimeSummary = () => copyText(realtimeOperationsSummaryText({ filters, rows, total: pagination.total, onlineCount, locatedCount, degradedCount, sourceTypeCount: primaryProtocols.size, amapConfigured, sourceIssueRows }), '实时摘要'); 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;安全密钥后续走服务端代理,避免明文下发。
地图接入状态
{mapIntegrationRows.map((item) => (
{item.label} {item.detail}
{item.value}
))}
多来源一致性检查
{sourceIssueRows.length === 0 ? ( 当前页车辆来源一致 ) : ( sourceIssueRows.map((row) => { const status = vehicleServiceStatus(row); return (
{row.plate || row.vin} {row.vin}
{status.label}
{sourceEvidenceText(row)} {sourceIssueTags(row).map((item) => ( 一致性:{item} ))} 一致性诊断:{row.serviceStatus?.detail || sourceEvidenceText(row)}
{row.lastSeen || '-'}
); }) )}
地图车辆服务队列
{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 || '-'}
); }) )}
{[ { label: '在线率', value: formatPercent(onlineRate), color: onlineRate >= 80 ? 'green' as const : 'orange' as const, detail: `${onlineCount.toLocaleString()} / ${rows.length.toLocaleString()} 辆在线。` }, { label: '定位有效率', value: formatPercent(locatedRate), color: locatedRate >= 80 ? 'green' as const : 'orange' as const, detail: `${locatedCount.toLocaleString()} / ${rows.length.toLocaleString()} 辆坐标有效。` }, { label: '降级率', value: formatPercent(degradedRate), color: degradedCount > 0 ? 'orange' as const : 'green' as const, detail: `${degradedCount.toLocaleString()} 辆存在来源不完整或离线。` }, { label: '分页覆盖率', value: formatPercent(pageCoverageRate), color: pageCoverageRate >= 100 ? 'green' as const : 'grey' as const, detail: '当前页车辆数 / 当前筛选总车辆数。' } ].map((item) => ( {item.label}
{item.value}
{item.detail}
))}
当前页 {rows.length.toLocaleString()} 条
{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: 190, render: (_: unknown, row: VehicleRealtimeRow) => ( ) } ]} /> )} ); }