import { IconCopy } from '@douyinfe/semi-icons'; import { Button, Card, Form, Select, Space, Table, Tag, Toast } from '@douyinfe/semi-ui'; import { useEffect, useMemo, useState } from 'react'; import { api } from '../api/client'; import type { VehicleCoverageRow, VehicleCoverageSummary } from '../api/types'; import { DataEmpty } from '../components/DataEmpty'; import { PageHeader } from '../components/PageHeader'; import { SourceStatusTags } from '../components/SourceStatusTags'; import { StatusTag } from '../components/StatusTag'; import { buildCsv, downloadCsv, type CsvColumn } from '../domain/csvExport'; import { summarizeVehicleService } from '../domain/vehicleService'; function vehicleServiceStatus(row: VehicleCoverageRow) { 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 }; } const verdict = summarizeVehicleService({ bindingStatus: row.bindingStatus, sourceCount: row.sourceCount, onlineSourceCount: row.onlineSourceCount, missingProtocols: row.missingProtocols, mileageDeltaKm: row.sourceConsistency?.mileageDeltaKm }); return { label: verdict.status, color: verdict.color }; } function sourceEvidenceText(row: VehicleCoverageRow) { return `${row.onlineSourceCount}/${row.sourceCount} 来源在线`; } function primaryRowProtocol(row: VehicleCoverageRow, fallbackProtocol?: string) { return (fallbackProtocol || row.sourceStatus?.find((source) => source.online)?.protocol || row.protocols?.[0] || '').trim(); } function rowWorkflowFilters(row: VehicleCoverageRow, fallbackProtocol?: string) { return { keyword: row.vin, ...(primaryRowProtocol(row, fallbackProtocol) ? { protocol: primaryRowProtocol(row, fallbackProtocol) } : {}) }; } function vehicleArchiveCompleteness(row: VehicleCoverageRow) { const fields = [row.vin, row.plate, row.phone, row.oem]; const completed = fields.filter((item) => Boolean(String(item ?? '').trim())).length; return { completed, total: fields.length, label: `${completed}/${fields.length}` }; } function vehicleArchiveMissingLabels(row: VehicleCoverageRow) { return [ { value: row.vin, label: '缺VIN' }, { value: row.plate, label: '缺车牌' }, { value: row.phone, label: '缺手机号' }, { value: row.oem, label: '缺OEM' } ].filter((item) => !String(item.value ?? '').trim()).map((item) => item.label); } const coverageLabel: Record = { single: '单源', multi: '多源' }; const serviceStatusLabel: Record = { healthy: '服务正常', degraded: '来源不完整', offline: '车辆离线', no_data: '暂无数据来源', identity_required: '身份未绑定' }; const onlineLabel: Record = { online: '在线', offline: '离线' }; const bindingStatusLabel: Record = { bound: '已绑定', unbound: '未绑定' }; const archiveStatusLabel: Record = { complete: '完整', incomplete: '不完整' }; const archiveMissingLabel: Record = { plate: '缺车牌', phone: '缺手机号', oem: '缺OEM' }; function sourceDiagnosisText(row: VehicleCoverageRow) { const parts = [sourceEvidenceText(row)]; if ((row.missingProtocols ?? []).length > 0) { parts.push(`缺 ${row.missingProtocols.join('、')}`); } else if (row.sourceCount > 0) { parts.push('来源完整'); } else { parts.push('暂无来源'); } return parts.join(','); } const vehicleExportColumns: 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.protocols?.join('|') }, { title: '缺失来源', value: (row) => row.missingProtocols?.join('|') }, { title: '来源覆盖', value: (row) => sourceEvidenceText(row) }, { title: '服务状态', value: (row) => vehicleServiceStatus(row).label }, { title: '诊断摘要', value: (row) => sourceDiagnosisText(row) }, { title: '档案完整度', value: (row) => vehicleArchiveCompleteness(row).label }, { title: '档案缺项', value: (row) => vehicleArchiveMissingLabels(row).join('|') }, { title: '在线', value: (row) => row.online ? '在线' : '离线' }, { title: '最后在线', value: (row) => row.lastSeen }, { title: '绑定状态', value: (row) => row.bindingStatus }, { title: '来源一致性', value: (row) => row.sourceConsistency?.title ?? '' } ]; function exportFileName(filters: Record) { const keyword = filters.keyword?.trim() || 'all'; const protocol = filters.protocol?.trim() || 'all-source'; return `vehicle-coverage-${keyword}-${protocol}.csv`; } type VehicleActionQueueItem = { label: string; count: number; filters: Record; color: 'orange' | 'red'; priority: 'P0' | 'P1'; detail: string; }; function vehicleFilterSummary(filters: Record) { return [ filters.keyword ? `关键词:${filters.keyword}` : '', filters.protocol ? `数据来源:${filters.protocol}` : '', filters.coverage ? `来源覆盖:${coverageLabel[filters.coverage] ?? filters.coverage}` : '', filters.missingProtocol ? `缺失来源:${filters.missingProtocol}` : '', filters.serviceStatus ? `服务状态:${serviceStatusLabel[filters.serviceStatus] ?? filters.serviceStatus}` : '', filters.online ? `在线:${onlineLabel[filters.online] ?? filters.online}` : '', filters.bindingStatus ? `绑定:${bindingStatusLabel[filters.bindingStatus] ?? filters.bindingStatus}` : '', filters.archiveStatus ? `档案:${archiveStatusLabel[filters.archiveStatus] ?? filters.archiveStatus}` : '', filters.archiveMissing ? `档案缺项:${archiveMissingLabel[filters.archiveMissing] ?? filters.archiveMissing}` : '' ].filter(Boolean); } function vehicleGovernanceSummaryText({ filters, summary, actionQueue }: { filters: Record; summary: VehicleCoverageSummary | null; actionQueue: VehicleActionQueueItem[]; }) { const archiveMissing = (summary?.archiveMissingFields ?? []) .filter((item) => item.count > 0) .map((item) => `${item.title} ${item.count.toLocaleString()}`) .join('、') || '-'; const missingSources = (summary?.missingSources ?? []) .filter((item) => item.count > 0) .map((item) => `${item.protocol} ${item.count.toLocaleString()}`) .join('、') || '-'; const actions = actionQueue.length > 0 ? actionQueue.map((item) => `[${item.priority}] ${item.label} ${item.count.toLocaleString()}:${item.detail}`).join('\n') : '暂无处置项'; return [ '【车辆治理摘要】', `当前筛选:${vehicleFilterSummary(filters).join(';') || '全部车辆'}`, `车辆总数:${(summary?.totalVehicles ?? 0).toLocaleString()},在线:${(summary?.onlineVehicles ?? 0).toLocaleString()}`, `单源:${(summary?.singleSourceVehicles ?? 0).toLocaleString()},多源:${(summary?.multiSourceVehicles ?? 0).toLocaleString()},暂无来源:${(summary?.noDataVehicles ?? 0).toLocaleString()}`, `待绑定:${(summary?.unboundVehicles ?? 0).toLocaleString()},档案不完整:${(summary?.archiveIncompleteVehicles ?? 0).toLocaleString()}`, `档案缺项:${archiveMissing}`, `缺失来源:${missingSources}`, '处置队列:', actions ].join('\n'); } type VehicleActionRecommendation = { label: string; color: 'green' | 'orange' | 'red'; filters: Record | null; }; function vehicleActionRecommendation(row: VehicleCoverageRow): VehicleActionRecommendation { const verdict = summarizeVehicleService({ bindingStatus: row.bindingStatus, sourceCount: row.sourceCount, onlineSourceCount: row.onlineSourceCount, missingProtocols: row.missingProtocols, mileageDeltaKm: row.sourceConsistency?.mileageDeltaKm }); if (row.bindingStatus !== 'bound') { return { label: '维护身份绑定', color: verdict.color, filters: { bindingStatus: 'unbound' } }; } if ((row.missingProtocols ?? []).length > 0) { return { label: `补齐 ${row.missingProtocols.join('、')} 来源`, color: verdict.color, filters: { missingProtocol: row.missingProtocols[0] } }; } if (row.sourceCount <= 0) { return { label: '确认平台转发配置', color: verdict.color, filters: { serviceStatus: 'no_data' } }; } if (row.onlineSourceCount <= 0) { return { label: '排查离线链路', color: verdict.color, filters: { online: 'offline' } }; } return { label: '持续观察', color: verdict.color, filters: null }; } 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}; } async function copyText(value: string, label: string) { try { await navigator.clipboard.writeText(value); Toast.success(`已复制${label}`); } catch { Toast.error(`复制${label}失败`); } } async function copyVehicleShareURL() { await copyText(`${window.location.origin}${window.location.pathname}${window.location.hash}`, '筛选链接'); } export function Vehicles({ onOpenVehicle, onOpenQuality, onOpenRealtime, onOpenHistory, onFiltersChange, initialFilters = {} }: { onOpenVehicle: (vin: string, protocol?: string) => void; onOpenQuality?: (filters: Record) => void; onOpenRealtime?: (filters: Record) => void; onOpenHistory?: (filters: Record) => void; onFiltersChange?: (filters: Record) => void; initialFilters?: Record; }) { const [rows, setRows] = useState([]); const [summary, setSummary] = useState(null); const [loading, setLoading] = useState(true); const [filters, setFilters] = useState>(initialFilters); const [pagination, setPagination] = useState({ currentPage: 1, pageSize: 20, total: 0 }); const resultSummary = useMemo }>>(() => { const items: Array<{ label: string; value: string; filters: Record }> = [ { label: '过滤车辆', value: (summary?.totalVehicles ?? pagination.total).toLocaleString(), filters: {} }, { label: '在线车辆', value: (summary?.onlineVehicles ?? 0).toLocaleString(), filters: { online: 'online' } }, { label: '单源车辆', value: (summary?.singleSourceVehicles ?? 0).toLocaleString(), filters: { coverage: 'single' } }, { label: '多源车辆', value: (summary?.multiSourceVehicles ?? 0).toLocaleString(), filters: { coverage: 'multi' } }, { label: '暂无来源车辆', value: (summary?.noDataVehicles ?? 0).toLocaleString(), filters: { serviceStatus: 'no_data' } }, { label: '待绑定', value: (summary?.unboundVehicles ?? 0).toLocaleString(), filters: { bindingStatus: 'unbound' } }, { label: '档案不完整', value: (summary?.archiveIncompleteVehicles ?? 0).toLocaleString(), filters: { archiveStatus: 'incomplete' } } ]; for (const field of summary?.archiveMissingFields ?? []) { if (field.count <= 0) continue; items.push({ label: field.title, value: field.count.toLocaleString(), filters: { archiveMissing: field.field } }); } for (const source of summary?.missingSources ?? []) { if (source.count <= 0) continue; items.push({ label: `缺 ${source.protocol}`, value: source.count.toLocaleString(), filters: { missingProtocol: source.protocol } }); } return items; }, [pagination.total, summary]); const actionQueue = useMemo(() => { const items: VehicleActionQueueItem[] = []; const unboundCount = summary?.unboundVehicles ?? 0; if (unboundCount > 0) { items.push({ label: '维护身份绑定', count: unboundCount, filters: { bindingStatus: 'unbound' }, color: 'orange', priority: 'P0', detail: '已有数据但无法归并到 VIN,先补绑定再看跨来源服务。' }); } const noDataCount = summary?.noDataVehicles ?? 0; if (noDataCount > 0) { items.push({ label: '确认平台转发', count: noDataCount, filters: { serviceStatus: 'no_data' }, color: 'orange', priority: 'P0', detail: '车辆没有任何来源证据,优先确认平台转发、端口和订阅配置。' }); } const archiveIncompleteCount = summary?.archiveIncompleteVehicles ?? 0; if (archiveIncompleteCount > 0) { items.push({ label: '完善车辆档案', count: archiveIncompleteCount, filters: { archiveStatus: 'incomplete' }, color: 'orange', priority: 'P1', detail: '车辆缺少车牌、手机号或 OEM 等基础档案,影响后续运营查询和治理。' }); } for (const field of summary?.archiveMissingFields ?? []) { if (field.count <= 0) continue; items.push({ label: `补齐${field.title}`, count: field.count, filters: { archiveMissing: field.field }, color: 'orange', priority: 'P1', detail: `${field.title}会影响车辆档案检索、绑定确认和运营侧筛选。` }); } for (const source of summary?.missingSources ?? []) { if (source.count <= 0) continue; items.push({ label: `补齐 ${source.protocol} 来源`, count: source.count, filters: { missingProtocol: source.protocol }, color: 'orange', priority: 'P1', detail: `${source.protocol} 来源缺失会降低跨来源定位、里程和实时判断可信度。` }); } return items; }, [summary]); const filterSummary = vehicleFilterSummary(filters); 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?.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?.archiveStatus) params.set('archiveStatus', values.archiveStatus); if (values?.archiveMissing) params.set('archiveMissing', values.archiveMissing); if (values?.serviceStatus) params.set('serviceStatus', values.serviceStatus); const summaryParams = new URLSearchParams(params); summaryParams.delete('limit'); summaryParams.delete('offset'); Promise.all([api.vehicleCoverage(params), api.vehicleCoverageSummary(summaryParams)]) .then(([nextPage, nextSummary]) => { setRows(nextPage.items); setSummary(nextSummary); setPagination({ currentPage: page, pageSize, total: nextPage.total }); }) .catch((error: Error) => Toast.error(error.message)) .finally(() => setLoading(false)); }; const applyFilters = (nextFilters: Record) => { setFilters(nextFilters); onFiltersChange?.(nextFilters); load(nextFilters, 1, pagination.pageSize); }; const exportVehicles = () => { if (rows.length === 0) { Toast.warning('当前没有可导出的车辆结果'); return; } downloadCsv(exportFileName(filters), buildCsv(vehicleExportColumns, rows)); Toast.success(`已导出 ${rows.length} 条车辆结果`); }; const copyGovernanceSummary = () => { copyText(vehicleGovernanceSummaryText({ filters, summary, actionQueue }), '治理摘要'); }; useEffect(() => { setFilters(initialFilters); load(initialFilters, 1, pagination.pageSize); }, [JSON.stringify(initialFilters)]); const columns = useMemo( () => [ { title: 'VIN', dataIndex: 'vin', width: 190 }, { title: '车牌', dataIndex: 'plate', width: 120 }, { title: '手机号', dataIndex: 'phone', width: 130 }, { title: 'OEM', dataIndex: 'oem', width: 120 }, { title: '档案完整度', width: 190, render: (_: unknown, row: VehicleCoverageRow) => { const archive = vehicleArchiveCompleteness(row); const missingLabels = vehicleArchiveMissingLabels(row); return ( {archive.label} {missingLabels.map((label) => ( {label} ))} ); } }, { title: '车辆服务状态', width: 130, render: (_: unknown, row: VehicleCoverageRow) => { const status = vehicleServiceStatus(row); return {status.label}; } }, { title: '来源证据', width: 300, render: (_: unknown, row: VehicleCoverageRow) => ( ) }, { title: '缺失来源', width: 220, render: (_: unknown, row: VehicleCoverageRow) => ( {(row.missingProtocols ?? []).length > 0 ? row.missingProtocols.map((protocol) => ( )) : 完整} ) }, { title: '来源一致性', width: 140, render: (_: unknown, row: VehicleCoverageRow) => sourceConsistencyAction(row, (nextFilters) => applyFilters({ ...filters, ...nextFilters })) }, { title: '诊断摘要', width: 240, render: (_: unknown, row: VehicleCoverageRow) => sourceDiagnosisText(row) }, { title: '建议动作', width: 180, render: (_: unknown, row: VehicleCoverageRow) => { const action = vehicleActionRecommendation(row); if (action.filters) { return ( ); } return {action.label}; } }, { title: '证据覆盖', width: 120, render: (_: unknown, row: VehicleCoverageRow) => sourceEvidenceText(row) }, { title: '在线', width: 90, render: (_: unknown, row: VehicleCoverageRow) => }, { title: '最后在线', dataIndex: 'lastSeen', width: 170 }, { title: '绑定', width: 90, render: (_: unknown, row: VehicleCoverageRow) => {row.bindingStatus === 'bound' ? '已绑定' : '未绑定'} }, { title: '操作', width: 300, render: (_: unknown, row: VehicleCoverageRow) => ( ) } ], [filters, onOpenHistory, onOpenQuality, onOpenRealtime, onOpenVehicle] ); return (
{ const nextFilters = values as Record; applyFilters(nextFilters); }}> GB32960 JT808 YUTONG_MQTT 单源 多源 缺 GB32960 缺 JT808 缺 YUTONG_MQTT 服务正常 来源不完整 车辆离线 暂无数据来源 身份未绑定 在线 离线 已绑定 未绑定 完整 不完整 缺车牌 缺手机号 缺OEM
{filterSummary.length > 0 ? ( {filterSummary.map((item) => ( {item} ))} ) : null} 当前车辆结果 )} style={{ marginTop: 16 }} >
{resultSummary.map((item) => ( ))}
{actionQueue.length > 0 ? (
{actionQueue.map((item) => (
{item.priority} {item.label} {item.count.toLocaleString()}
{item.detail}
))}
) : null} {rows.length === 0 && !loading ? ( ) : ( <>
当前页 {rows.length.toLocaleString()} 辆 总计 {pagination.total.toLocaleString()} 辆
load(filters, page, pagination.pageSize), onPageSizeChange: (pageSize) => load(filters, 1, pageSize) }} /> )} ); }