import { Button, Card, Form, Select, Space, Table, Tag, Toast, Typography } from '@douyinfe/semi-ui'; import { useEffect, useState } from 'react'; import { api } from '../api/client'; import type { DailyMileageRow, MileageSummary, OnlineStatisticsSummary, OnlineVehicleStatusRow } from '../api/types'; import { PageHeader } from '../components/PageHeader'; import { buildAppHash } from '../domain/appRoute'; import { buildCsv, downloadCsv, type CsvColumn } from '../domain/csvExport'; const emptySummary: MileageSummary = { vehicleCount: 0, recordCount: 0, sourceCount: 0, totalMileageKm: 0, averageMileagePerVin: 0 }; const emptyOnlineSummary: OnlineStatisticsSummary = { vehicleCount: 0, onlineVehicleCount: 0, offlineVehicleCount: 0, onlineRatePercent: 0, redisOnlineKeys: null, protocolStats: [], evidence: '' }; function mileageParams(values: Record, pageSize?: number, offset?: number) { const params = new URLSearchParams(); if (pageSize != null) params.set('limit', String(pageSize)); if (offset != null) params.set('offset', String(offset)); if (values?.keyword) params.set('keyword', values.keyword); if (values?.protocol) params.set('protocol', values.protocol); if (values?.dateFrom) params.set('dateFrom', values.dateFrom); if (values?.dateTo) params.set('dateTo', values.dateTo); return params; } function formatKm(value: number) { return Number.isFinite(value) ? value.toLocaleString(undefined, { maximumFractionDigits: 1 }) : '0'; } function formatPercent(value: number) { return Number.isFinite(value) ? `${value.toLocaleString(undefined, { maximumFractionDigits: 1 })}%` : '0%'; } function formatOfflineDuration(minutes?: number | null) { if (minutes == null || !Number.isFinite(Number(minutes))) return '-'; const value = Math.max(0, Number(minutes)); if (value < 60) return `${value.toLocaleString()} 分钟`; const hours = Math.floor(value / 60); const rest = Math.round(value % 60); return rest > 0 ? `${hours.toLocaleString()} 小时 ${rest} 分钟` : `${hours.toLocaleString()} 小时`; } function canOpenVehicle(vin?: string) { const value = vin?.trim(); return Boolean(value && value !== 'unknown'); } function nextDate(value: string) { const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value.trim()); if (!match) return ''; const date = new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]) + 1)); return date.toISOString().slice(0, 10); } function mileageActionRecommendation(row: DailyMileageRow) { if (row.anomalySeverity) { return { label: '核对里程来源', color: 'orange' as const, detail: '日里程异常,优先核对当天首末总里程、来源断链和补传数据。' }; } return { label: '可用于日报统计', color: 'green' as const, detail: '该日里程未发现异常,可进入车辆服务查看来源证据。' }; } function mergeInitialFilters(initialVin: string, initialProtocol?: string, initialFilters: Record = {}) { return { keyword: initialVin, protocol: initialProtocol ?? '', ...initialFilters }; } function groupMileageByDate(rows: DailyMileageRow[]) { const bucket = new Map(); rows.forEach((row) => { bucket.set(row.date || '-', (bucket.get(row.date || '-') ?? 0) + row.dailyMileageKm); }); return [...bucket.entries()] .map(([date, value]) => ({ date, value })) .sort((a, b) => a.date.localeCompare(b.date)); } function groupMileageBySource(rows: DailyMileageRow[]) { const bucket = new Map(); rows.forEach((row) => { bucket.set(row.source || 'UNKNOWN', (bucket.get(row.source || 'UNKNOWN') ?? 0) + row.dailyMileageKm); }); return [...bucket.entries()] .map(([source, value]) => ({ source, value })) .sort((a, b) => b.value - a.value); } function maxMileageRow(rows: DailyMileageRow[]) { return rows.reduce((max, row) => { if (!max || row.dailyMileageKm > max.dailyMileageKm) return row; return max; }, undefined); } const mileageExportColumns: CsvColumn[] = [ { title: '日期', value: (row) => row.date }, { title: 'VIN', value: (row) => row.vin }, { title: '车牌', value: (row) => row.plate }, { title: '来源', value: (row) => row.source }, { title: '起始里程km', value: (row) => row.startMileageKm }, { title: '结束里程km', value: (row) => row.endMileageKm }, { title: '日里程km', value: (row) => row.dailyMileageKm }, { title: '异常', value: (row) => row.anomalySeverity || '正常' } ]; function exportFileName(filters: Record) { const keyword = filters.keyword?.trim() || 'all'; const protocol = filters.protocol?.trim() || 'all-source'; return `daily-mileage-${keyword}-${protocol}.csv`; } function appURL(hash: string) { return `${window.location.origin}${window.location.pathname}${hash}`; } function mileageSummaryText({ filters, summary, pageMileageTotal, anomalyCount, peakMileage, statisticsURL, vehicleURL }: { filters: Record; summary: MileageSummary; pageMileageTotal: number; anomalyCount: number; peakMileage?: DailyMileageRow; statisticsURL: string; vehicleURL?: string; }) { const keyword = filters.keyword?.trim() || '全部车辆'; const protocol = filters.protocol?.trim() || '全部来源'; const dateFrom = filters.dateFrom?.trim(); const dateTo = filters.dateTo?.trim(); const dateRange = dateFrom || dateTo ? `${dateFrom || '-'} 至 ${dateTo || '-'}` : '全部日期'; const peak = peakMileage ? `${peakMileage.plate || peakMileage.vin} / ${peakMileage.date} / ${formatKm(peakMileage.dailyMileageKm)} km` : '-'; return [ '【里程统计摘要】', `车辆范围:${keyword}`, `数据来源:${protocol}`, `日期范围:${dateRange}`, `统计车辆:${summary.vehicleCount.toLocaleString()},记录:${summary.recordCount.toLocaleString()},来源:${summary.sourceCount.toLocaleString()}`, `累计里程:${formatKm(summary.totalMileageKm)} km`, `单车平均:${formatKm(summary.averageMileagePerVin)} km`, `当前页里程:${formatKm(pageMileageTotal)} km`, `异常记录:${anomalyCount.toLocaleString()}`, `最大单日:${peak}`, '统计口径:区间总值等于各日里程之和', `统计查询:${statisticsURL}`, ...(vehicleURL ? [`车辆服务:${vehicleURL}`] : []) ].join('\n'); } function mileageBIPublishText({ filters, summary, pageMileageTotal, pageCoverageRate, anomalyCount, closureStatus, closureDeltaKm, confidenceStatus, sourceConsistencyText, statisticsURL, vehicleURL }: { filters: Record; summary: MileageSummary; pageMileageTotal: number; pageCoverageRate: number; anomalyCount: number; closureStatus: string; closureDeltaKm?: number; confidenceStatus: string; sourceConsistencyText: string; statisticsURL: string; vehicleURL?: string; }) { const keyword = filters.keyword?.trim() || '全部车辆'; const protocol = filters.protocol?.trim() || '全部来源'; const dateFrom = filters.dateFrom?.trim(); const dateTo = filters.dateTo?.trim(); const dateRange = dateFrom || dateTo ? `${dateFrom || '-'} 至 ${dateTo || '-'}` : '全部日期'; const publishable = confidenceStatus === '可用于 BI' && closureStatus === '闭合通过'; return [ '【BI里程发布口径】', `发布结论:${publishable ? '可发布' : '需复核后发布'}`, `车辆范围:${keyword}`, `数据来源:${protocol}`, `日期范围:${dateRange}`, `统计口径:日里程按首末总里程差值计算,区间总值等于每日里程之和`, `累计里程:${formatKm(summary.totalMileageKm)} km`, `明细合计:${formatKm(pageMileageTotal)} km`, `闭合状态:${closureStatus}${closureDeltaKm == null ? '' : `,差值 ${formatKm(Math.abs(closureDeltaKm))} km`}`, `记录覆盖率:${formatPercent(pageCoverageRate)}`, `异常记录:${anomalyCount.toLocaleString()}`, `来源一致性:${sourceConsistencyText}`, `统计车辆:${summary.vehicleCount.toLocaleString()},记录:${summary.recordCount.toLocaleString()},来源:${summary.sourceCount.toLocaleString()}`, `发布风险:${publishable ? '当前筛选范围闭合且无异常记录' : '存在分页未全量、异常记录或闭合差值,需要先复核轨迹和 RAW 证据'}`, `统计查询:${statisticsURL}`, ...(vehicleURL ? [`车辆服务:${vehicleURL}`] : []) ].join('\n'); } type MileagePublishAuditItem = { key: string; label: string; status: 'pass' | 'review' | 'blocked'; value: string; detail: string; action: string; }; function auditStatusColor(status: MileagePublishAuditItem['status']): 'green' | 'orange' | 'red' { if (status === 'pass') return 'green'; if (status === 'blocked') return 'red'; return 'orange'; } function auditStatusLabel(status: MileagePublishAuditItem['status']) { if (status === 'pass') return '通过'; if (status === 'blocked') return '阻断'; return '复核'; } function mileagePublishAuditReport({ filters, items, summary, statisticsURL, vehicleURL }: { filters: Record; items: MileagePublishAuditItem[]; summary: MileageSummary; statisticsURL: string; vehicleURL?: string; }) { const blocked = items.filter((item) => item.status === 'blocked').length; const review = items.filter((item) => item.status === 'review').length; const pass = items.filter((item) => item.status === 'pass').length; const keyword = filters.keyword?.trim() || '全部车辆'; const protocol = filters.protocol?.trim() || '全部来源'; return [ '【统计发布审计】', `发布结论:${blocked > 0 ? '阻断发布' : review > 0 ? '复核后发布' : '可发布'}`, `车辆范围:${keyword}`, `数据来源:${protocol}`, `统计规模:${summary.vehicleCount.toLocaleString()} 车 / ${summary.recordCount.toLocaleString()} 条 / ${summary.sourceCount.toLocaleString()} 来源`, `审计结果:通过 ${pass.toLocaleString()} / 复核 ${review.toLocaleString()} / 阻断 ${blocked.toLocaleString()}`, '', ...items.map((item, index) => [ `${index + 1}. [${auditStatusLabel(item.status)}] ${item.label}:${item.value}`, ` 说明:${item.detail}`, ` 动作:${item.action}` ].join('\n')), '', `统计查询:${statisticsURL}`, ...(vehicleURL ? [`车辆服务:${vehicleURL}`] : []) ].join('\n'); } function mileageEvidencePackageText(row: DailyMileageRow) { const dateFrom = row.date?.trim() ?? ''; const dateTo = nextDate(dateFrom); const evidenceFilters = { ...(dateFrom ? { dateFrom } : {}), ...(dateTo ? { dateTo } : {}) }; const vehicle = row.plate?.trim() ? `${row.plate.trim()} / ${row.vin}` : row.vin; return [ '【里程异常证据包】', `车辆:${vehicle}`, `数据来源:${row.source || '未知来源'}`, `统计日期:${row.date || '-'}`, `起始里程:${formatKm(row.startMileageKm)} km`, `结束里程:${formatKm(row.endMileageKm)} km`, `日里程:${formatKm(row.dailyMileageKm)} km`, `异常等级:${row.anomalySeverity || '正常'}`, `处置建议:${mileageActionRecommendation(row).detail}`, `车辆服务:${appURL(buildAppHash({ page: 'detail', keyword: row.vin, protocol: row.source }))}`, `轨迹证据:${appURL(buildAppHash({ page: 'history', keyword: row.vin, protocol: row.source, filters: evidenceFilters }))}`, `RAW证据:${appURL(buildAppHash({ page: 'history-query', keyword: row.vin, protocol: row.source, filters: { ...evidenceFilters, tab: 'raw', includeFields: 'true' } }))}`, `统计查询:${appURL(buildAppHash({ page: 'mileage', keyword: row.vin, protocol: row.source, filters: evidenceFilters }))}` ].join('\n'); } function onlineStatusHandoffText({ filters, summary, rows, total }: { filters: Record; summary: OnlineStatisticsSummary; rows: OnlineVehicleStatusRow[]; total: number; }) { const keyword = filters.keyword?.trim() || '全部车辆'; const protocol = filters.protocol?.trim() || '全部来源'; const handoffRows = Array.isArray(rows) ? rows : []; const offlineRows = handoffRows.filter((row) => !row.online); const protocolStats = Array.isArray(summary.protocolStats) ? summary.protocolStats : []; const protocolLines = protocolStats.length > 0 ? protocolStats.map((item) => `${item.protocol} ${item.online.toLocaleString()}/${item.total.toLocaleString()}`).join(';') : '-'; return [ '【车辆在线状态交接】', `车辆范围:${keyword}`, `数据来源:${protocol}`, `在线概览:${summary.onlineVehicleCount.toLocaleString()} 在线 / ${summary.offlineVehicleCount.toLocaleString()} 离线 / 在线率 ${formatPercent(summary.onlineRatePercent)}`, `Redis 在线 Key:${summary.redisOnlineKeys == null ? '-' : summary.redisOnlineKeys.toLocaleString()}`, `协议在线:${protocolLines}`, `统计证据:${summary.evidence || '-'}`, `当前页离线:${offlineRows.length.toLocaleString()} 辆 / 当前筛选 ${total.toLocaleString()} 辆`, '', '离线车辆:', ...(offlineRows.length > 0 ? offlineRows.map((row, index) => [ `${index + 1}. ${row.plate || '-'} / ${row.vin || '-'} / ${row.oem || '-'}`, ` 离线时长:${formatOfflineDuration(row.offlineDurationMinutes)};最后上报:${row.lastSeen || '-'}`, ` 来源覆盖:${row.onlineSourceCount}/${row.sourceCount};协议:${(row.protocols ?? []).join('、') || '-'}`, ` 车辆服务:${appURL(buildAppHash({ page: 'detail', keyword: row.vin, protocol: filters.protocol?.trim() || row.protocols?.[0] || '' }))}`, ` 实时监控:${appURL(buildAppHash({ page: 'realtime', keyword: row.vin, protocol: filters.protocol?.trim() || row.protocols?.[0] || '' }))}` ].join('\n')) : ['当前页暂无离线车辆']) ].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 Mileage({ initialVin, initialProtocol, initialFilters = {}, onFiltersChange, onOpenVehicle, onOpenHistory, onOpenRaw }: { initialVin: string; initialProtocol?: string; initialFilters?: Record; onFiltersChange?: (filters: Record) => void; onOpenVehicle: (vin: string, protocol?: string) => void; onOpenHistory?: (filters: Record) => void; onOpenRaw?: (filters: Record) => void; }) { const [rows, setRows] = useState([]); const [onlineRows, setOnlineRows] = useState([]); const [summary, setSummary] = useState(emptySummary); const [onlineSummary, setOnlineSummary] = useState(emptyOnlineSummary); const [loading, setLoading] = useState(true); const [onlineLoading, setOnlineLoading] = useState(true); const [summaryLoading, setSummaryLoading] = useState(true); const [filters, setFilters] = useState>(mergeInitialFilters(initialVin, initialProtocol, initialFilters)); const [pagination, setPagination] = useState({ currentPage: 1, pageSize: 20, total: 0 }); const [onlinePagination, setOnlinePagination] = useState({ currentPage: 1, pageSize: 10, total: 0 }); const currentVehicleKeyword = filters.keyword?.trim() ?? ''; const currentProtocol = filters.protocol?.trim() ?? ''; const dateSeries = groupMileageByDate(rows); const sourceSeries = groupMileageBySource(rows); const anomalyRows = rows.filter((row) => row.anomalySeverity); const peakMileage = maxMileageRow(rows); const pageMileageTotal = rows.reduce((total, row) => total + row.dailyMileageKm, 0); const anomalyRate = rows.length > 0 ? (anomalyRows.length / rows.length) * 100 : 0; const peakMileageShare = pageMileageTotal > 0 && peakMileage ? (peakMileage.dailyMileageKm / pageMileageTotal) * 100 : 0; const pageCoverageRate = summary.recordCount > 0 ? (rows.length / summary.recordCount) * 100 : 0; const hasFullMileagePage = summary.recordCount > 0 && rows.length === summary.recordCount; const closureDeltaKm = hasFullMileagePage ? summary.totalMileageKm - pageMileageTotal : undefined; const closurePassed = closureDeltaKm != null && Math.abs(closureDeltaKm) < 0.01; const closureStatus = closureDeltaKm == null ? '分页抽样' : closurePassed ? '闭合通过' : '闭合异常'; const closureStatusColor = closurePassed ? 'green' as const : closureDeltaKm == null ? 'grey' as const : 'orange' as const; const closureActionText = closureDeltaKm == null ? '当前页未覆盖全量记录,请翻页、导出或缩小筛选范围后核验。' : closurePassed ? '当前筛选范围汇总总里程与明细合计一致,可作为统计证据。' : '汇总总里程与明细合计不一致,请核对分页、来源和异常记录。'; const confidenceStatus = anomalyRows.length > 0 || pageCoverageRate < 100 ? '需复核' : '可用于 BI'; const confidenceColor = confidenceStatus === '可用于 BI' ? 'green' as const : 'orange' as const; const currentEvidenceText = `${summary.vehicleCount.toLocaleString()} 车 / ${summary.sourceCount.toLocaleString()} 来源 / ${rows.length.toLocaleString()} 条明细`; const sourceConsistencyText = summary.sourceCount > 1 ? '可做跨来源核对' : summary.sourceCount === 1 ? '单来源车辆需关注' : '等待来源证据'; const onlineVehicleCount = Number(onlineSummary.onlineVehicleCount ?? 0); const offlineVehicleCount = Number(onlineSummary.offlineVehicleCount ?? 0); const onlineRatePercent = Number(onlineSummary.onlineRatePercent ?? 0); const redisOnlineKeyText = onlineSummary.redisOnlineKeys == null ? '' : `Redis 在线 ${onlineSummary.redisOnlineKeys.toLocaleString()} key`; const onlineStatsDetail = [ `在线 ${onlineVehicleCount.toLocaleString()} 车`, `离线 ${offlineVehicleCount.toLocaleString()} 车`, redisOnlineKeyText, onlineSummary.evidence ].filter(Boolean).join(','); const publishAuditItems: MileagePublishAuditItem[] = [ { key: 'closure', label: '区间闭合', status: closureDeltaKm == null ? 'review' : closurePassed ? 'pass' : 'blocked', value: closureStatus, detail: closureDeltaKm == null ? '当前分页未覆盖全量记录,无法直接证明区间总值等于每日之和。' : `闭合差值 ${formatKm(Math.abs(closureDeltaKm))} km。`, action: closurePassed ? '保留当前统计证据,可继续审计异常和来源。' : '缩小筛选范围、翻页或导出后重新核验闭合。' }, { key: 'coverage', label: '记录覆盖', status: pageCoverageRate >= 100 ? 'pass' : summary.recordCount > rows.length ? 'review' : 'pass', value: formatPercent(pageCoverageRate), detail: `${rows.length.toLocaleString()} / ${summary.recordCount.toLocaleString()} 条明细在当前页。`, action: pageCoverageRate >= 100 ? '当前明细已覆盖统计范围。' : '发布前需要导出或拉齐全量明细。' }, { key: 'anomaly', label: '异常记录', status: anomalyRows.length > 0 ? 'review' : 'pass', value: anomalyRows.length.toLocaleString(), detail: anomalyRows.length > 0 ? '存在日里程异常标记,需核对首末总里程、补传和断链。' : '当前筛选范围未发现异常里程记录。', action: anomalyRows.length > 0 ? '优先打开异常行的轨迹和 RAW 证据复核。' : '继续检查来源一致性和在线证据。' }, { key: 'source', label: '来源一致性', status: summary.sourceCount > 1 ? 'pass' : summary.sourceCount === 1 ? 'review' : 'blocked', value: sourceConsistencyText, detail: summary.sourceCount > 1 ? '可使用多协议来源交叉验证车辆统计。' : summary.sourceCount === 1 ? '只有单一来源,统计可用但可信度低于多来源。' : '当前统计缺少来源证据。', action: summary.sourceCount > 1 ? '保留来源贡献作为审计附件。' : '核对 32960、808、MQTT 是否有缺失来源。' }, { key: 'online', label: '在线证据', status: onlineVehicleCount > 0 ? 'pass' : offlineVehicleCount > 0 ? 'review' : 'review', value: `${onlineVehicleCount.toLocaleString()} 在线 / ${offlineVehicleCount.toLocaleString()} 离线`, detail: onlineStatsDetail || '等待在线统计证据。', action: onlineVehicleCount > 0 ? '实时在线证据可辅助统计发布。' : '统计发布前建议检查离线时长和平台转发状态。' } ]; const publishBlockedCount = publishAuditItems.filter((item) => item.status === 'blocked').length; const publishReviewCount = publishAuditItems.filter((item) => item.status === 'review').length; const publishAuditConclusion = publishBlockedCount > 0 ? '阻断发布' : publishReviewCount > 0 ? '复核后发布' : '可发布'; const publishAuditColor = publishBlockedCount > 0 ? 'red' as const : publishReviewCount > 0 ? 'orange' as const : 'green' as const; const maxDateMileage = Math.max(...dateSeries.map((item) => item.value), 0); const maxSourceMileage = Math.max(...sourceSeries.map((item) => item.value), 0); const filterSummary = [ currentVehicleKeyword ? `车辆:${currentVehicleKeyword}` : '', currentProtocol ? `数据来源:${currentProtocol}` : '', filters.dateFrom?.trim() ? `开始日期:${filters.dateFrom.trim()}` : '', filters.dateTo?.trim() ? `结束日期:${filters.dateTo.trim()}` : '' ].filter(Boolean); const loadSummary = (values: Record = filters) => { setSummaryLoading(true); api.mileageSummary(mileageParams(values)) .then(setSummary) .catch((error: Error) => Toast.error(error.message)) .finally(() => setSummaryLoading(false)); api.onlineStatisticsSummary(mileageParams(values)) .then(setOnlineSummary) .catch((error: Error) => Toast.error(error.message)); }; const load = (values: Record = filters, page = pagination.currentPage, pageSize = pagination.pageSize) => { setLoading(true); api.dailyMileage(mileageParams(values, pageSize, (page - 1) * pageSize)) .then((nextPage) => { setRows(Array.isArray(nextPage.items) ? nextPage.items : []); setPagination({ currentPage: page, pageSize, total: nextPage.total }); }) .catch((error: Error) => Toast.error(error.message)) .finally(() => setLoading(false)); }; const loadOnlineRows = (values: Record = filters, page = onlinePagination.currentPage, pageSize = onlinePagination.pageSize) => { setOnlineLoading(true); api.onlineVehicleStatuses(mileageParams(values, pageSize, (page - 1) * pageSize)) .then((nextPage) => { setOnlineRows(Array.isArray(nextPage.items) ? nextPage.items : []); setOnlinePagination({ currentPage: page, pageSize, total: nextPage.total }); }) .catch((error: Error) => Toast.error(error.message)) .finally(() => setOnlineLoading(false)); }; const applyFilters = (nextFilters: Record) => { setFilters(nextFilters); onFiltersChange?.(nextFilters); loadSummary(nextFilters); load(nextFilters, 1, pagination.pageSize); loadOnlineRows(nextFilters, 1, onlinePagination.pageSize); }; const clearRangeFilters = () => { applyFilters(currentVehicleKeyword ? { keyword: currentVehicleKeyword } : {}); }; const openMileageEvidence = (row: DailyMileageRow) => { const dateFrom = row.date?.trim() ?? ''; const dateTo = nextDate(dateFrom); onOpenHistory?.({ keyword: row.vin, protocol: row.source, ...(dateFrom ? { dateFrom } : {}), ...(dateTo ? { dateTo } : {}) }); }; const openMileageRawEvidence = (row: DailyMileageRow) => { const dateFrom = row.date?.trim() ?? ''; const dateTo = nextDate(dateFrom); onOpenRaw?.({ keyword: row.vin, protocol: row.source, ...(dateFrom ? { dateFrom } : {}), ...(dateTo ? { dateTo } : {}) }); }; const copyMileageEvidence = (row: DailyMileageRow) => { copyText(mileageEvidencePackageText(row), '里程证据包'); }; const exportMileage = () => { if (rows.length === 0) { Toast.warning('当前没有可导出的里程明细'); return; } downloadCsv(exportFileName(filters), buildCsv(mileageExportColumns, rows)); Toast.success(`已导出 ${rows.length} 条里程明细`); }; const copyStatisticsSummary = () => { const vehicleURL = currentVehicleKeyword ? appURL(buildAppHash({ page: 'detail', keyword: currentVehicleKeyword, protocol: currentProtocol })) : undefined; copyText( mileageSummaryText({ filters, summary, pageMileageTotal, anomalyCount: anomalyRows.length, peakMileage, statisticsURL: appURL(window.location.hash || buildAppHash({ page: 'mileage', keyword: currentVehicleKeyword, protocol: currentProtocol })), vehicleURL }), '统计摘要' ); }; const copyBIPublishText = () => { const vehicleURL = currentVehicleKeyword ? appURL(buildAppHash({ page: 'detail', keyword: currentVehicleKeyword, protocol: currentProtocol })) : undefined; copyText( mileageBIPublishText({ filters, summary, pageMileageTotal, pageCoverageRate, anomalyCount: anomalyRows.length, closureStatus, closureDeltaKm, confidenceStatus, sourceConsistencyText, statisticsURL: appURL(window.location.hash || buildAppHash({ page: 'mileage', keyword: currentVehicleKeyword, protocol: currentProtocol })), vehicleURL }), 'BI里程发布口径' ); }; const copyPublishAudit = () => { const vehicleURL = currentVehicleKeyword ? appURL(buildAppHash({ page: 'detail', keyword: currentVehicleKeyword, protocol: currentProtocol })) : undefined; copyText( mileagePublishAuditReport({ filters, items: publishAuditItems, summary, statisticsURL: appURL(window.location.hash || buildAppHash({ page: 'mileage', keyword: currentVehicleKeyword, protocol: currentProtocol })), vehicleURL }), '统计发布审计' ); }; const copyOnlineStatusHandoff = () => { copyText( onlineStatusHandoffText({ filters, summary: onlineSummary, rows: onlineRows, total: onlinePagination.total }), '在线状态交接' ); }; useEffect(() => { const nextFilters = mergeInitialFilters(initialVin, initialProtocol, initialFilters); setFilters(nextFilters); loadSummary(nextFilters); load(nextFilters, 1, pagination.pageSize); loadOnlineRows(nextFilters, 1, onlinePagination.pageSize); }, [initialVin, initialProtocol, JSON.stringify(initialFilters)]); return (
onOpenVehicle(currentVehicleKeyword, currentProtocol)}> 当前车辆服务 )} />
当前车辆:{currentVehicleKeyword || '-'} 当前来源:{currentProtocol || '全部来源'} 里程统计按当前车辆与来源范围汇总。
当前统计证据 {currentEvidenceText} 三类数据源最终汇总为一个车辆服务统计视图
{[ { title: '里程统计', value: `${formatKm(summary.totalMileageKm)} km`, color: 'blue' as const, detail: '每日里程、区间里程、异常差值和可审计证据。' }, { title: '在线率与离线时长', value: formatPercent(onlineRatePercent), color: 'green' as const, detail: onlineStatsDetail || '按 VIN 统计在线率、离线时间和无更新时长。' }, { title: '数据完整性', value: confidenceStatus, color: confidenceColor, detail: '围绕位置、里程、SOC、速度和来源新鲜度判断统计可信度。' }, { title: '来源一致性', value: sourceConsistencyText, color: summary.sourceCount > 1 ? 'green' as const : 'orange' as const, detail: 'GB32960、JT808、Yutong MQTT 只作为同一车辆服务的来源证据。' } ].map((item) => (
{item.title}
{item.value}
{item.detail}
))}
{ const nextFilters = values as Record; applyFilters(nextFilters); }}> GB32960 JT808 YUTONG_MQTT
{filterSummary.length > 0 ? ( {filterSummary.map((item) => ( {item} ))} ) : null}
{[ { label: '车辆数', value: summary.vehicleCount.toLocaleString() }, { label: '累计里程 km', value: formatKm(summary.totalMileageKm) }, { label: '单车平均 km', value: formatKm(summary.averageMileagePerVin) }, { label: '来源 / 记录', value: `${summary.sourceCount}/${summary.recordCount.toLocaleString()}` } ].map((item) => (
{item.value}
{item.label}
))}
{[ { label: '汇总总里程', value: `${formatKm(summary.totalMileageKm)} km`, color: 'blue' as const, detail: '后端按当前筛选范围返回的区间里程总值。' }, { label: '明细合计', value: `${formatKm(pageMileageTotal)} km`, color: 'green' as const, detail: '当前页日里程明细求和,用于快速复核。' }, { label: '闭合差值', value: closureDeltaKm == null ? '需全量' : `${formatKm(Math.abs(closureDeltaKm))} km`, color: closureStatusColor, detail: closureActionText }, { label: '记录覆盖率', value: formatPercent(pageCoverageRate), color: pageCoverageRate >= 100 ? 'green' as const : 'grey' as const, detail: `${rows.length.toLocaleString()} / ${summary.recordCount.toLocaleString()} 条明细在当前页。` } ].map((item) => (
{item.label}
{item.value}
{item.detail}
))}
{closureStatus} {closurePassed && anomalyRows.length === 0 ? '当前统计口径可直接用于 BI 里程口径;若存在跨来源差异,再进入车辆服务查看来源证据。' : '统计进入 BI 前需要结合异常记录、轨迹回放和 RAW 解析字段完成复核。'}
{confidenceStatus}
{confidenceStatus === '可用于 BI' && closureStatus === '闭合通过' ? '可发布' : '需复核后发布'}
日里程按首末总里程差值计算,区间总值必须等于每日里程之和;发布前同步检查异常记录、分页覆盖和来源一致性。
闭合状态{closureStatus}
记录覆盖{formatPercent(pageCoverageRate)}
异常记录{anomalyRows.length.toLocaleString()}
来源一致性{sourceConsistencyText}
{publishAuditConclusion}
通过 {publishAuditItems.length - publishReviewCount - publishBlockedCount} / 复核 {publishReviewCount} / 阻断 {publishBlockedCount}
发布前统一审计闭合、覆盖、异常、来源和在线证据;不满足时先回到轨迹和 RAW 证据复核。
{publishAuditItems.map((item) => (
{auditStatusLabel(item.status)} {item.label} {item.value} {item.detail}
{item.action}
))}
{dateSeries.length > 0 ? dateSeries.map((item) => (
{item.date}
0 ? Math.max(4, (item.value / maxDateMileage) * 100) : 0}%` }} />
{formatKm(item.value)} km
)) : ( 当前筛选范围暂无可展示趋势。 )}
{sourceSeries.length > 0 ? sourceSeries.map((item) => (
{item.source}
0 ? Math.max(4, (item.value / maxSourceMileage) * 100) : 0}%` }} />
{formatKm(item.value)} km
)) : ( 当前筛选范围暂无来源贡献。 )}
{[ { label: '统计可信度', value: confidenceStatus, color: confidenceColor, detail: anomalyRows.length > 0 ? '存在异常里程记录,建议先核对来源证据。' : '当前分页未发现异常记录。' }, { label: '异常率', value: formatPercent(anomalyRate), color: anomalyRows.length > 0 ? 'orange' as const : 'green' as const, detail: `${anomalyRows.length.toLocaleString()} / ${rows.length.toLocaleString()} 条明细命中异常标记。` }, { label: '最大单日占比', value: formatPercent(peakMileageShare), color: peakMileageShare > 50 ? 'orange' as const : 'blue' as const, detail: peakMileage ? `${peakMileage.plate || peakMileage.vin} 贡献最高单日里程。` : '暂无明细。' }, { label: '当前页里程', value: `${formatKm(pageMileageTotal)} km`, color: 'blue' as const, detail: '用于快速核对当前分页明细合计。' }, { label: '异常记录', value: anomalyRows.length.toLocaleString(), color: anomalyRows.length > 0 ? 'orange' as const : 'green' as const, detail: '来自异常标记,优先核对首末总里程和断链。' }, { label: '最大单日', value: peakMileage ? `${formatKm(peakMileage.dailyMileageKm)} km` : '-', color: 'blue' as const, detail: peakMileage ? `${peakMileage.plate || peakMileage.vin} / ${peakMileage.date}` : '暂无明细。' }, { label: '分页覆盖率', value: formatPercent(pageCoverageRate), color: pageCoverageRate >= 100 ? 'green' as const : 'grey' as const, detail: '当前分页记录数 / 全量统计记录数。' }, { label: '闭合校验', value: closureDeltaKm == null ? '需全量核验' : `${formatKm(Math.abs(closureDeltaKm))} km`, color: closurePassed ? 'green' as const : closureDeltaKm == null ? 'grey' as const : 'orange' as const, detail: closureDeltaKm == null ? '当前分页未覆盖全量记录,需翻页或导出全量后核验。' : '汇总总里程与当前明细合计的差值。' } ].map((item) => ( {item.label}
{item.value}
{item.detail}
))}
在线 {onlineVehicleCount.toLocaleString()} 车 离线 {offlineVehicleCount.toLocaleString()} 车 按车辆服务归并 VIN,展示最近上报时间、离线时长和来源覆盖。
row?.vin ?? ''} dataSource={onlineRows} pagination={{ currentPage: onlinePagination.currentPage, pageSize: onlinePagination.pageSize, total: onlinePagination.total, showSizeChanger: true, onPageChange: (page) => loadOnlineRows(filters, page, onlinePagination.pageSize), onPageSizeChange: (pageSize) => loadOnlineRows(filters, 1, pageSize) }} columns={[ { title: 'VIN', dataIndex: 'vin', width: 190 }, { title: '车牌', dataIndex: 'plate', width: 120 }, { title: 'OEM', dataIndex: 'oem', width: 120 }, { title: '在线', width: 90, render: (_: unknown, row: OnlineVehicleStatusRow) => {row.online ? '在线' : '离线'} }, { title: '最近上报', dataIndex: 'lastSeen', width: 180 }, { title: '离线时长', width: 130, render: (_: unknown, row: OnlineVehicleStatusRow) => formatOfflineDuration(row.offlineDurationMinutes) }, { title: '来源覆盖', width: 150, render: (_: unknown, row: OnlineVehicleStatusRow) => `${row.onlineSourceCount}/${row.sourceCount}` }, { title: '协议', width: 240, render: (_: unknown, row: OnlineVehicleStatusRow) => ( {(row.protocols ?? []).map((protocol) => {protocol})} ) }, { title: '绑定状态', dataIndex: 'bindingStatus', width: 130 }, { title: '操作', width: 120, render: (_: unknown, row: OnlineVehicleStatusRow) => ( ) } ]} />
可复制日报口径
日里程

按车辆、来源、日期聚合,以总里程差值作为日统计结果,避免中间断链导致分段累计不闭合。

区间里程

按当前筛选范围汇总日里程,区间总值应等于各日里程之和,用于 BI 和运营报表口径。

异常复核

异常优先检查补传、来源切换、总里程回退和跨协议差异,必要时回到车辆服务查看 RAW 证据。

当前页 {rows.length.toLocaleString()} 条
`${row?.date ?? ''}-${row?.vin ?? ''}-${row?.source ?? ''}`} dataSource={rows} pagination={{ currentPage: pagination.currentPage, pageSize: pagination.pageSize, total: pagination.total, showSizeChanger: true, onPageChange: (page) => load(filters, page, pagination.pageSize), onPageSizeChange: (pageSize) => load(filters, 1, pageSize) }} columns={[ { title: '日期', dataIndex: 'date' }, { title: 'VIN', dataIndex: 'vin' }, { title: '车牌', dataIndex: 'plate' }, { title: '起始里程', dataIndex: 'startMileageKm' }, { title: '结束里程', dataIndex: 'endMileageKm' }, { title: '日里程', dataIndex: 'dailyMileageKm' }, { title: '来源', dataIndex: 'source' }, { title: '异常', render: (_: unknown, row: DailyMileageRow) => row.anomalySeverity ? {row.anomalySeverity} : 正常 }, { title: '处置建议', width: 260, render: (_: unknown, row: DailyMileageRow) => { const action = mileageActionRecommendation(row); return (
{action.label}
{action.detail}
); } }, { title: '操作', width: 360, render: (_: unknown, row: DailyMileageRow) => ( ) } ]} /> ); }