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 }; function numberOrZero(value: unknown) { const numeric = Number(value); return Number.isFinite(numeric) ? numeric : 0; } function normalizeMileageSummary(summary?: Partial | null): MileageSummary { return { ...emptySummary, ...(summary ?? {}), vehicleCount: numberOrZero(summary?.vehicleCount), recordCount: numberOrZero(summary?.recordCount), sourceCount: numberOrZero(summary?.sourceCount), totalMileageKm: numberOrZero(summary?.totalMileageKm), averageMileagePerVin: numberOrZero(summary?.averageMileagePerVin) }; } 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 mileageDeliveryPackageText({ filters, summary, pageMileageTotal, pageCoverageRate, anomalyCount, closureStatus, closureDeltaKm, confidenceStatus, publishAuditConclusion, onlineSummary, peakMileage, statisticsURL, vehicleURL }: { filters: Record; summary: MileageSummary; pageMileageTotal: number; pageCoverageRate: number; anomalyCount: number; closureStatus: string; closureDeltaKm?: number; confidenceStatus: string; publishAuditConclusion: string; onlineSummary: OnlineStatisticsSummary; 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` : '-'; const deliveryState = publishAuditConclusion === '可发布' && confidenceStatus === '可用于 BI' ? '可交付' : publishAuditConclusion === '阻断发布' ? '暂缓交付' : '复核后交付'; return [ '【客户里程复核交付包】', `交付状态:${deliveryState}`, `车辆范围:${keyword}`, `数据通道:${protocol}`, `日期范围:${dateRange}`, `累计里程:${formatKm(summary.totalMileageKm)} km`, `当前页明细合计:${formatKm(pageMileageTotal)} km`, `闭合状态:${closureStatus}${closureDeltaKm == null ? '' : `,差值 ${formatKm(Math.abs(closureDeltaKm))} km`}`, `记录覆盖率:${formatPercent(pageCoverageRate)}`, `异常记录:${anomalyCount.toLocaleString()}`, `最大单日:${peak}`, `在线影响:${onlineSummary.onlineVehicleCount.toLocaleString()} 在线 / ${onlineSummary.offlineVehicleCount.toLocaleString()} 离线 / 在线率 ${formatPercent(onlineSummary.onlineRatePercent)}`, `发布判断:${publishAuditConclusion}`, `可信度:${confidenceStatus}`, '统计口径:日里程按首末总里程差值计算,区间总值应等于每日里程之和', `里程统计:${statisticsURL}`, `轨迹复核:${appURL(buildAppHash({ page: 'history', keyword: filters.keyword?.trim() || '', protocol: filters.protocol?.trim() || '', filters: { ...(dateFrom ? { dateFrom } : {}), ...(dateTo ? { dateTo } : {}) } }))}`, `历史明细:${appURL(buildAppHash({ page: 'history-query', keyword: filters.keyword?.trim() || '', protocol: filters.protocol?.trim() || '', filters: { ...(dateFrom ? { dateFrom } : {}), ...(dateTo ? { dateTo } : {}), tab: 'raw', includeFields: 'true' } }))}`, ...(vehicleURL ? [`车辆服务:${vehicleURL}`] : []) ].join('\n'); } function mileageCustomerDecisionText({ filters, summary, pageMileageTotal, pageCoverageRate, anomalyCount, closureStatus, closureDeltaKm, confidenceStatus, publishAuditConclusion, onlineSummary, peakMileage, statisticsURL, vehicleURL }: { filters: Record; summary: MileageSummary; pageMileageTotal: number; pageCoverageRate: number; anomalyCount: number; closureStatus: string; closureDeltaKm?: number; confidenceStatus: string; publishAuditConclusion: string; onlineSummary: OnlineStatisticsSummary; 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 deliveryState = publishAuditConclusion === '可发布' && confidenceStatus === '可用于 BI' ? '可交付' : publishAuditConclusion === '阻断发布' ? '暂缓交付' : '复核后交付'; const peak = peakMileage ? `${peakMileage.plate || peakMileage.vin} / ${peakMileage.date} / ${formatKm(peakMileage.dailyMileageKm)} km` : '-'; return [ '【客户里程决策说明】', `决策结论:${deliveryState}`, `车辆范围:${keyword}`, `数据通道:${protocol}`, `时间范围:${dateRange}`, `区间总里程:${formatKm(summary.totalMileageKm)} km`, `当前明细合计:${formatKm(pageMileageTotal)} km`, `区间闭合:${closureStatus}${closureDeltaKm == null ? ',当前为分页抽样' : `,差值 ${formatKm(Math.abs(closureDeltaKm))} km`}`, `记录覆盖:${formatPercent(pageCoverageRate)}`, `异常记录:${anomalyCount.toLocaleString()} 条`, `最高单日:${peak}`, `在线影响:${onlineSummary.onlineVehicleCount.toLocaleString()} 在线 / ${onlineSummary.offlineVehicleCount.toLocaleString()} 离线 / 在线率 ${formatPercent(onlineSummary.onlineRatePercent)}`, `BI发布判断:${publishAuditConclusion}`, '', '客户判断路径:', '1. 先看区间闭合:区间总值必须等于每日里程之和。', '2. 再看记录覆盖:发布前需要覆盖全量明细或导出全量。', '3. 再看异常记录:异常日必须回看轨迹和历史明细。', '4. 最后看在线影响:离线车辆会影响当天里程解释。', `里程统计:${statisticsURL}`, `轨迹复核:${appURL(buildAppHash({ page: 'history', keyword: filters.keyword?.trim() || '', protocol: filters.protocol?.trim() || '', filters: { ...(dateFrom ? { dateFrom } : {}), ...(dateTo ? { dateTo } : {}) } }))}`, `历史明细:${appURL(buildAppHash({ page: 'history-query', keyword: filters.keyword?.trim() || '', protocol: filters.protocol?.trim() || '', filters: { ...(dateFrom ? { dateFrom } : {}), ...(dateTo ? { dateTo } : {}), tab: 'raw', includeFields: 'true' } }))}`, ...(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 ? '当前筛选范围闭合且无异常记录' : '存在分页未全量、异常记录或闭合差值,需要先复核轨迹和历史明细'}`, `统计查询:${statisticsURL}`, ...(vehicleURL ? [`车辆服务:${vehicleURL}`] : []) ].join('\n'); } function statisticsImpactText({ filters, summary, onlineSummary, closureStatus, confidenceStatus, anomalyCount, pageCoverageRate, publishAuditConclusion, statisticsURL, vehicleURL }: { filters: Record; summary: MileageSummary; onlineSummary: OnlineStatisticsSummary; closureStatus: string; confidenceStatus: string; anomalyCount: number; pageCoverageRate: number; publishAuditConclusion: 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 || '-'}` : '全部日期'; return [ '【统计影响范围】', `车辆范围:${keyword}`, `数据通道:${protocol}`, `日期范围:${dateRange}`, `影响车辆:${summary.vehicleCount.toLocaleString()} 辆`, `统计记录:${summary.recordCount.toLocaleString()} 条`, `累计里程:${formatKm(summary.totalMileageKm)} km`, `在线状态:${onlineSummary.onlineVehicleCount.toLocaleString()} 在线 / ${onlineSummary.offlineVehicleCount.toLocaleString()} 离线 / 在线率 ${formatPercent(onlineSummary.onlineRatePercent)}`, `闭合状态:${closureStatus}`, `发布判断:${publishAuditConclusion}`, `可信度:${confidenceStatus}`, `异常记录:${anomalyCount.toLocaleString()}`, `当前页覆盖:${formatPercent(pageCoverageRate)}`, `业务影响:${publishAuditConclusion === '可发布' ? '当前统计范围可进入运营和BI口径' : '当前统计范围需要先复核轨迹、历史明细和离线车辆影响'}`, `统计查询:${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 }))}`, `历史明细:${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'); } function onlineOperationsReportText({ filters, summary, onlineRows, total, statisticsURL, vehicleURL }: { filters: Record; summary: OnlineStatisticsSummary; onlineRows: OnlineVehicleStatusRow[]; total: number; statisticsURL: string; vehicleURL?: string; }) { const keyword = filters.keyword?.trim() || '全部车辆'; const protocol = filters.protocol?.trim() || '全部数据通道'; const protocolStats = Array.isArray(summary.protocolStats) ? summary.protocolStats : []; const offlineRows = onlineRows.filter((row) => !row.online); const longestOffline = offlineRows.reduce((current, row) => { if (!current) return row; return Number(row.offlineDurationMinutes ?? 0) > Number(current.offlineDurationMinutes ?? 0) ? row : current; }, undefined); return [ '【在线统计运营态势】', `车辆范围:${keyword}`, `数据通道:${protocol}`, `在线概览:${summary.onlineVehicleCount.toLocaleString()} 在线 / ${summary.offlineVehicleCount.toLocaleString()} 离线 / 在线率 ${formatPercent(summary.onlineRatePercent)}`, `Redis 在线 Key:${summary.redisOnlineKeys == null ? '-' : summary.redisOnlineKeys.toLocaleString()}`, `统计证据:${summary.evidence || '-'}`, `当前页车辆:${onlineRows.length.toLocaleString()} / 当前筛选 ${total.toLocaleString()}`, `当前页离线:${offlineRows.length.toLocaleString()}`, `最长离线:${longestOffline ? `${longestOffline.plate || longestOffline.vin} / ${formatOfflineDuration(longestOffline.offlineDurationMinutes)} / ${longestOffline.lastSeen || '-'}` : '-'}`, '', '来源在线:', ...(protocolStats.length > 0 ? protocolStats.map((item, index) => { const rate = item.total > 0 ? (item.online / item.total) * 100 : 0; return `${index + 1}. ${item.protocol}:${item.online.toLocaleString()}/${item.total.toLocaleString()},在线率 ${formatPercent(rate)}`; }) : ['暂无来源在线统计']), '', '处置建议:', '1. 在线率下降时先检查实时在线状态、数据接入和最后上报时间', '2. 离线车辆优先打开车辆服务,核对实时、轨迹、历史明细和告警记录', '3. 统计发布前确认离线车辆是否影响当日里程和区间闭合', `统计查询:${statisticsURL}`, ...(vehicleURL ? [`车辆服务:${vehicleURL}`] : []) ].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 onlineProtocolStats = Array.isArray(onlineSummary.protocolStats) ? onlineSummary.protocolStats : []; const topOfflineRow = onlineRows.filter((row) => !row.online).reduce((current, row) => { if (!current) return row; return Number(row.offlineDurationMinutes ?? 0) > Number(current.offlineDurationMinutes ?? 0) ? row : current; }, undefined); 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 ? '优先打开异常行的轨迹和历史明细复核。' : '继续检查通道一致性和在线状态。' }, { 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 statisticsImpactColor = publishAuditColor; const statisticsImpactLevel = publishAuditConclusion === '可发布' ? '可运营发布' : publishAuditConclusion === '阻断发布' ? '发布阻断' : '影响待复核'; const mileageDeliveryState = publishAuditConclusion === '可发布' && confidenceStatus === '可用于 BI' ? '可交付' : publishAuditConclusion === '阻断发布' ? '暂缓交付' : '复核后交付'; const mileageDeliveryColor = mileageDeliveryState === '可交付' ? 'green' as const : mileageDeliveryState === '暂缓交付' ? 'red' as const : 'orange' as const; const statisticsImpactItems = [ { label: '影响车辆', value: `${summary.vehicleCount.toLocaleString()} 辆`, detail: `${onlineVehicleCount.toLocaleString()} 在线 / ${offlineVehicleCount.toLocaleString()} 离线`, color: offlineVehicleCount > 0 ? 'orange' as const : 'green' as const }, { label: '统计规模', value: `${summary.recordCount.toLocaleString()} 条`, detail: `${summary.sourceCount.toLocaleString()} 个来源,累计 ${formatKm(summary.totalMileageKm)} km`, color: summary.recordCount > 0 ? 'blue' as const : 'grey' as const }, { label: '闭合影响', value: closureStatus, detail: closureActionText, color: closureStatusColor }, { label: '异常影响', value: `${anomalyRows.length.toLocaleString()} 条`, detail: anomalyRows.length > 0 ? '异常会影响BI与运营日报,需要回到证据复核。' : '当前明细未命中异常标记。', color: anomalyRows.length > 0 ? 'orange' as const : 'green' as const }, { label: '发布判断', value: publishAuditConclusion, detail: publishAuditConclusion === '可发布' ? '统计范围闭合且审计项通过。' : '发布前需要补齐审计项证据。', color: publishAuditColor } ]; const mileageDecisionItems = [ { label: '交付结论', value: mileageDeliveryState, detail: mileageDeliveryState === '可交付' ? '可进入客户查询、BI发布和运营日报。' : mileageDeliveryState === '暂缓交付' ? '存在阻断项,先完成证据复核。' : '需补齐闭合、覆盖、异常或在线证据后交付。', color: mileageDeliveryColor, action: '复制决策', onClick: () => copyMileageDecision() }, { label: '区间闭合', value: closureStatus, detail: closureDeltaKm == null ? '当前页是分页抽样,发布前请拉齐全量明细。' : `闭合差值 ${formatKm(Math.abs(closureDeltaKm))} km。`, color: closureStatusColor, action: '统计摘要', onClick: () => copyStatisticsSummary() }, { label: '覆盖率', value: formatPercent(pageCoverageRate), detail: `${rows.length.toLocaleString()} / ${summary.recordCount.toLocaleString()} 条记录,交付前建议覆盖全量。`, color: pageCoverageRate >= 100 ? 'green' as const : pageCoverageRate >= 60 ? 'orange' as const : 'red' as const, action: '导出明细', onClick: () => exportMileage() }, { label: '异常复核', value: `${anomalyRows.length.toLocaleString()} 条`, detail: anomalyRows[0] ? `${anomalyRows[0].plate || anomalyRows[0].vin} / ${anomalyRows[0].date} 需要回看轨迹。` : '当前明细未命中异常标记。', color: anomalyRows.length > 0 ? 'orange' as const : 'green' as const, action: '异常明细', onClick: () => anomalyRows[0] ? copyMileageEvidence(anomalyRows[0]) : copyMileageDecision() }, { label: '在线影响', value: formatPercent(onlineRatePercent), detail: `${onlineVehicleCount.toLocaleString()} 在线 / ${offlineVehicleCount.toLocaleString()} 离线。`, color: onlineRatePercent >= 90 ? 'green' as const : onlineRatePercent >= 60 ? 'orange' as const : 'red' as const, action: '在线态势', onClick: () => copyOnlineOperationsReport() } ]; 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 mileageRangeText = filters.dateFrom?.trim() || filters.dateTo?.trim() ? `${filters.dateFrom?.trim() || '-'} 至 ${filters.dateTo?.trim() || '-'}` : '全部日期'; const mileageCustomerKpis = [ { label: '区间总里程', value: `${formatKm(summary.totalMileageKm)} km`, color: 'blue' as const, detail: `范围:${mileageRangeText}` }, { label: '统计车辆', value: `${summary.vehicleCount.toLocaleString()} 辆`, color: summary.vehicleCount > 0 ? 'green' as const : 'grey' as const, detail: `${summary.recordCount.toLocaleString()} 条日里程记录` }, { label: '异常记录', value: anomalyRows.length.toLocaleString(), color: anomalyRows.length > 0 ? 'orange' as const : 'green' as const, detail: anomalyRows.length > 0 ? '建议先核对轨迹和历史明细' : '当前页未发现异常' }, { label: '在线影响', value: formatPercent(onlineRatePercent), color: onlineRatePercent >= 90 ? 'green' as const : onlineRatePercent >= 60 ? 'orange' as const : 'red' as const, detail: `${onlineVehicleCount.toLocaleString()} 在线 / ${offlineVehicleCount.toLocaleString()} 离线` } ]; const mileageCustomerWorkbenchItems = [ { label: '区间里程', value: `${formatKm(summary.totalMileageKm)} km`, detail: `${summary.vehicleCount.toLocaleString()} 辆车,${mileageRangeText}。`, color: 'blue' as const, action: '复制摘要', disabled: false, onClick: () => copyStatisticsSummary() }, { label: '每日闭合', value: closureStatus, detail: closureDeltaKm == null ? `当前页覆盖 ${formatPercent(pageCoverageRate)},发布前需要拉齐全量。` : `闭合差值 ${formatKm(Math.abs(closureDeltaKm))} km。`, color: closureStatusColor, action: '发布审计', disabled: false, onClick: () => copyPublishAudit() }, { label: '轨迹回放', value: peakMileage?.date || mileageRangeText, detail: peakMileage ? `${peakMileage.plate || peakMileage.vin} 最大单日 ${formatKm(peakMileage.dailyMileageKm)} km。` : '选择车辆后用同一时间窗回放轨迹。', color: currentVehicleKeyword ? 'blue' as const : 'grey' as const, action: '回放轨迹', disabled: !currentVehicleKeyword || !onOpenHistory, onClick: () => { if (peakMileage) { openMileageEvidence(peakMileage); return; } onOpenHistory?.({ keyword: currentVehicleKeyword, protocol: currentProtocol, dateFrom: filters.dateFrom ?? '', dateTo: filters.dateTo ?? '' }); } }, { label: '历史导出', value: `${rows.length.toLocaleString()} 条`, detail: `当前页覆盖 ${formatPercent(pageCoverageRate)},用于客户对账和 BI 复核。`, color: rows.length > 0 ? 'blue' as const : 'grey' as const, action: '导出明细', disabled: rows.length === 0, onClick: () => exportMileage() }, { label: '告警通知', value: `${offlineVehicleCount.toLocaleString()} 离线`, detail: offlineVehicleCount > 0 ? '离线会影响定位、里程解释和客户通知。' : '当前在线状态未形成主要统计风险。', color: offlineVehicleCount > 0 ? 'orange' as const : 'green' as const, action: '在线态势', disabled: false, onClick: () => copyOnlineOperationsReport() } ]; const mileageCustomerActions = [ { label: '导出当前明细', disabled: rows.length === 0, action: () => exportMileage() }, { label: '查看轨迹', disabled: !currentVehicleKeyword || !onOpenHistory, action: () => onOpenHistory?.({ keyword: currentVehicleKeyword, protocol: currentProtocol, dateFrom: filters.dateFrom ?? '', dateTo: filters.dateTo ?? '' }) }, { label: '查看历史明细', disabled: !currentVehicleKeyword || !onOpenRaw, action: () => onOpenRaw?.({ keyword: currentVehicleKeyword, protocol: currentProtocol, dateFrom: filters.dateFrom ?? '', dateTo: filters.dateTo ?? '', includeFields: 'true' }) }, { label: '车辆档案', disabled: !currentVehicleKeyword, action: () => onOpenVehicle(currentVehicleKeyword, currentProtocol) } ]; const mileageCustomerQuerySteps = [ { step: '01', title: '选择范围', value: currentVehicleKeyword || '全部车辆', detail: `时间范围 ${mileageRangeText},数据通道 ${currentProtocol || '全部数据通道'}。`, action: '调整筛选', color: currentVehicleKeyword ? 'green' as const : 'blue' as const, disabled: false, onClick: () => { const input = document.querySelector('input[name="keyword"]'); input?.focus(); } }, { step: '02', title: '区间里程', value: `${formatKm(summary.totalMileageKm)} km`, detail: `${summary.vehicleCount.toLocaleString()} 辆车,${summary.recordCount.toLocaleString()} 条日里程记录。`, action: '复制摘要', color: 'blue' as const, disabled: false, onClick: () => copyStatisticsSummary() }, { step: '03', title: '发布判断', value: publishAuditConclusion, detail: `闭合 ${closureStatus},覆盖 ${formatPercent(pageCoverageRate)}。`, action: '发布审计', color: publishAuditColor, disabled: false, onClick: () => copyPublishAudit() }, { step: '04', title: '复核证据', value: anomalyRows.length > 0 ? `${anomalyRows.length.toLocaleString()} 异常` : '轨迹/原始', detail: anomalyRows.length > 0 ? '异常日优先回看轨迹和历史明细。' : '可从同一时间窗进入轨迹和历史明细。', action: '轨迹复核', color: anomalyRows.length > 0 ? 'orange' as const : 'green' as const, disabled: !currentVehicleKeyword || !onOpenHistory, onClick: () => onOpenHistory?.({ keyword: currentVehicleKeyword, protocol: currentProtocol, dateFrom: filters.dateFrom ?? '', dateTo: filters.dateTo ?? '' }) }, { step: '05', title: '明细导出', value: `${rows.length.toLocaleString()} 当前页`, detail: '导出明细并附带统计口径,支撑BI、客户复盘和运营日报。', action: '导出明细', color: rows.length > 0 ? 'blue' as const : 'grey' as const, disabled: rows.length === 0, onClick: () => exportMileage() } ]; const mileageDeliveryNavigation = [ { title: '确认统计口径', value: `${formatKm(summary.totalMileageKm)} km`, detail: `范围 ${mileageRangeText},闭合状态 ${closureStatus},发布判断 ${publishAuditConclusion}。`, action: '复制统计摘要', color: publishAuditColor, disabled: false, onClick: () => copyStatisticsSummary() }, { title: '回放里程轨迹', value: currentVehicleKeyword || '请选择车辆', detail: '用同一车辆和时间窗回看位置、速度、停驶和里程断点。', action: '轨迹复核', color: currentVehicleKeyword ? 'blue' as const : 'grey' as const, disabled: !currentVehicleKeyword || !onOpenHistory, onClick: () => onOpenHistory?.({ keyword: currentVehicleKeyword, protocol: currentProtocol, dateFrom: filters.dateFrom ?? '', dateTo: filters.dateTo ?? '' }) }, { title: '导出明细证据', value: `${rows.length.toLocaleString()} 条当前页`, detail: `覆盖 ${formatPercent(pageCoverageRate)},用于客户复盘、BI核对和运营日报附件。`, action: '导出CSV', color: rows.length > 0 ? 'blue' as const : 'grey' as const, disabled: rows.length === 0, onClick: () => exportMileage() }, { title: '发送交付结论', value: mileageDeliveryState, detail: mileageDeliveryState === '可交付' ? '复制交付包后可直接发给客户或BI使用。' : '交付前需要补齐闭合、异常、覆盖或在线影响说明。', action: '复制交付包', color: mileageDeliveryColor, disabled: false, onClick: () => copyMileageDeliveryPackage() } ]; const loadSummary = (values: Record = filters) => { setSummaryLoading(true); api.mileageSummary(mileageParams(values)) .then((nextSummary) => setSummary(normalizeMileageSummary(nextSummary))) .catch((error: Error) => Toast.error(error.message)) .finally(() => setSummaryLoading(false)); api.onlineStatisticsSummary(mileageParams(values)) .then((nextSummary) => setOnlineSummary({ ...emptyOnlineSummary, ...(nextSummary ?? {}), protocolStats: Array.isArray(nextSummary?.protocolStats) ? nextSummary.protocolStats : [] })) .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 copyMileageDeliveryPackage = () => { const vehicleURL = currentVehicleKeyword ? appURL(buildAppHash({ page: 'detail', keyword: currentVehicleKeyword, protocol: currentProtocol })) : undefined; copyText( mileageDeliveryPackageText({ filters, summary, pageMileageTotal, pageCoverageRate, anomalyCount: anomalyRows.length, closureStatus, closureDeltaKm, confidenceStatus, publishAuditConclusion, onlineSummary, peakMileage, statisticsURL: appURL(window.location.hash || buildAppHash({ page: 'mileage', keyword: currentVehicleKeyword, protocol: currentProtocol })), vehicleURL }), '客户里程复核交付包' ); }; const copyMileageDecision = () => { const vehicleURL = currentVehicleKeyword ? appURL(buildAppHash({ page: 'detail', keyword: currentVehicleKeyword, protocol: currentProtocol })) : undefined; copyText( mileageCustomerDecisionText({ filters, summary, pageMileageTotal, pageCoverageRate, anomalyCount: anomalyRows.length, closureStatus, closureDeltaKm, confidenceStatus, publishAuditConclusion, onlineSummary, 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 copyStatisticsImpact = () => { const vehicleURL = currentVehicleKeyword ? appURL(buildAppHash({ page: 'detail', keyword: currentVehicleKeyword, protocol: currentProtocol })) : undefined; copyText( statisticsImpactText({ filters, summary, onlineSummary, closureStatus, confidenceStatus, anomalyCount: anomalyRows.length, pageCoverageRate, publishAuditConclusion, 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 }), '在线状态交接' ); }; const copyOnlineOperationsReport = () => { const vehicleURL = currentVehicleKeyword ? appURL(buildAppHash({ page: 'detail', keyword: currentVehicleKeyword, protocol: currentProtocol })) : undefined; copyText( onlineOperationsReportText({ filters, summary: onlineSummary, onlineRows, total: onlinePagination.total, statisticsURL: appURL(window.location.hash || buildAppHash({ page: 'mileage', keyword: currentVehicleKeyword, protocol: currentProtocol })), vehicleURL }), '在线统计运营态势' ); }; const mileageDeliveryConsoleItems = [ { label: '交付状态', value: mileageDeliveryState, detail: mileageDeliveryState === '可交付' ? '可进入客户查询、BI发布和运营日报。' : mileageDeliveryState === '暂缓交付' ? '存在阻断项,先完成闭合和异常复核。' : '复核闭合、覆盖、异常或在线影响后再交付。', color: mileageDeliveryColor, action: '复制交付', onClick: copyMileageDeliveryPackage }, { label: '区间里程', value: `${formatKm(summary.totalMileageKm)} km`, detail: `范围 ${mileageRangeText},统计车辆 ${summary.vehicleCount.toLocaleString()} 辆。`, color: 'blue' as const, action: '复制摘要', onClick: copyStatisticsSummary }, { label: '闭合校验', value: closureStatus, detail: closureDeltaKm == null ? '当前页未覆盖全量记录,发布前需要导出或缩小范围。' : `闭合差值 ${formatKm(Math.abs(closureDeltaKm))} km。`, color: closureStatusColor, action: '发布审计', onClick: copyPublishAudit }, { label: '异常复核', value: `${anomalyRows.length.toLocaleString()} 条`, detail: anomalyRows[0] ? `${anomalyRows[0].plate || anomalyRows[0].vin} / ${anomalyRows[0].date} 需要复核。` : '当前明细没有异常标记。', color: anomalyRows.length > 0 ? 'orange' as const : 'green' as const, action: '轨迹复核', onClick: () => { if (anomalyRows[0]) { openMileageEvidence(anomalyRows[0]); return; } onOpenHistory?.({ keyword: currentVehicleKeyword, protocol: currentProtocol, dateFrom: filters.dateFrom ?? '', dateTo: filters.dateTo ?? '' }); } }, { label: '数据导出', value: `${rows.length.toLocaleString()} 条明细`, detail: `当前页覆盖 ${formatPercent(pageCoverageRate)},用于客户复盘和BI核对。`, color: rows.length > 0 ? 'blue' as const : 'grey' as const, action: '导出明细', onClick: exportMileage } ]; const mileageReconciliationItems = [ { label: '区间里程', value: `${formatKm(summary.totalMileageKm)} km`, detail: `车辆 ${summary.vehicleCount.toLocaleString()} 辆,时间窗 ${mileageRangeText}。`, color: 'blue' as const, action: '复制摘要', onClick: copyStatisticsSummary }, { label: '日报闭合', value: closureStatus, detail: closureDeltaKm == null ? `当前页覆盖 ${formatPercent(pageCoverageRate)},交付前需要拉齐明细。` : `闭合差值 ${formatKm(Math.abs(closureDeltaKm))} km。`, color: closureStatusColor, action: '闭合审计', onClick: copyPublishAudit }, { label: '轨迹证据', value: peakMileage?.date || mileageRangeText, detail: peakMileage ? `${peakMileage.plate || peakMileage.vin} 最大单日 ${formatKm(peakMileage.dailyMileageKm)} km。` : '选择车辆后可按同一时间窗回放轨迹。', color: currentVehicleKeyword ? 'blue' as const : 'grey' as const, action: '轨迹回放', disabled: !currentVehicleKeyword || !onOpenHistory, onClick: () => { if (peakMileage) { openMileageEvidence(peakMileage); return; } onOpenHistory?.({ keyword: currentVehicleKeyword, protocol: currentProtocol, dateFrom: filters.dateFrom ?? '', dateTo: filters.dateTo ?? '' }); } }, { label: '告警通知', value: `${offlineVehicleCount.toLocaleString()} 离线`, detail: offlineVehicleCount > 0 ? '离线车辆会影响当日里程、定位和客户通知。' : '当前在线状态未形成主要统计风险。', color: offlineVehicleCount > 0 ? 'orange' as const : 'green' as const, action: '在线态势', onClick: copyOnlineOperationsReport }, { label: '报表导出', value: `${rows.length.toLocaleString()} 条`, detail: '导出当前明细,作为客户对账、BI核对和运营日报附件。', color: rows.length > 0 ? 'blue' as const : 'grey' as const, action: '导出CSV', disabled: rows.length === 0, onClick: exportMileage } ]; const mileageServiceTasks = [ { title: '发布判断', value: publishAuditConclusion, detail: publishAuditConclusion === '可发布' ? '当前统计范围可进入运营日报或 BI 口径。' : '发布前需要补齐闭合、覆盖、异常或在线证据。', color: publishAuditColor, primaryAction: 'BI口径', secondaryAction: '发布审计', disabled: false, secondaryDisabled: false, onPrimary: copyBIPublishText, onSecondary: copyPublishAudit }, { title: '区间复核', value: closureStatus, detail: `范围 ${mileageRangeText},${closureActionText}`, color: closureStatusColor, primaryAction: '轨迹证据', secondaryAction: '历史明细', disabled: !currentVehicleKeyword || !onOpenHistory, secondaryDisabled: !currentVehicleKeyword || !onOpenRaw, onPrimary: () => onOpenHistory?.({ keyword: currentVehicleKeyword, protocol: currentProtocol, dateFrom: filters.dateFrom ?? '', dateTo: filters.dateTo ?? '' }), onSecondary: () => onOpenRaw?.({ keyword: currentVehicleKeyword, protocol: currentProtocol, dateFrom: filters.dateFrom ?? '', dateTo: filters.dateTo ?? '', includeFields: 'true' }) }, { title: '异常处理', value: `${anomalyRows.length.toLocaleString()} 条异常`, detail: anomalyRows[0] ? `${anomalyRows[0].plate || anomalyRows[0].vin} / ${anomalyRows[0].date} 需要复核。` : '当前页未发现异常里程记录。', color: anomalyRows.length > 0 ? 'orange' as const : 'green' as const, primaryAction: '复制证据', secondaryAction: '车辆档案', disabled: !anomalyRows[0], secondaryDisabled: !currentVehicleKeyword, onPrimary: () => anomalyRows[0] && copyMileageEvidence(anomalyRows[0]), onSecondary: () => currentVehicleKeyword && onOpenVehicle(currentVehicleKeyword, currentProtocol) }, { title: '明细导出', value: `${rows.length.toLocaleString()} 条当前页`, detail: `累计 ${formatKm(summary.totalMileageKm)} km,当前页 ${formatKm(pageMileageTotal)} km。`, color: 'blue' as const, primaryAction: '导出明细', secondaryAction: '统计摘要', disabled: rows.length === 0, secondaryDisabled: false, onPrimary: exportMileage, onSecondary: copyStatisticsSummary } ]; const mileageNextActions = [ { title: publishAuditConclusion === '阻断发布' ? '先解除发布阻断' : publishAuditConclusion === '复核后发布' ? '先完成发布复核' : '可交付里程报告', value: publishAuditConclusion, detail: publishAuditConclusion === '可发布' ? '闭合、覆盖和异常检查通过后,可复制交付包或导出明细给客户。' : '先处理闭合、覆盖、异常和在线影响,再进入客户交付。', color: publishAuditColor, action: publishAuditConclusion === '可发布' ? '复制交付包' : '发布审计', disabled: false, onClick: publishAuditConclusion === '可发布' ? copyMileageDeliveryPackage : copyPublishAudit }, { title: anomalyRows.length > 0 ? '优先复核异常日' : '按时间窗核对轨迹', value: anomalyRows.length > 0 ? `${anomalyRows.length.toLocaleString()} 条异常` : mileageRangeText, detail: anomalyRows[0] ? `${anomalyRows[0].plate || anomalyRows[0].vin} / ${anomalyRows[0].date} 建议先回放轨迹。` : '用同一个车辆和时间窗进入轨迹回放,核对位置、速度和里程变化。', color: anomalyRows.length > 0 ? 'orange' as const : 'blue' as const, action: '轨迹回放', disabled: !currentVehicleKeyword || !onOpenHistory, onClick: () => { if (anomalyRows[0]) { openMileageEvidence(anomalyRows[0]); return; } onOpenHistory?.({ keyword: currentVehicleKeyword, protocol: currentProtocol, dateFrom: filters.dateFrom ?? '', dateTo: filters.dateTo ?? '' }); } }, { title: pageCoverageRate >= 100 ? '明细覆盖完整' : '拉齐全量明细', value: formatPercent(pageCoverageRate), detail: pageCoverageRate >= 100 ? '当前页已经覆盖筛选范围内全部明细,可直接导出复核。' : `${rows.length.toLocaleString()} / ${summary.recordCount.toLocaleString()} 条记录在当前页,导出前建议拉齐。`, color: pageCoverageRate >= 100 ? 'green' as const : 'orange' as const, action: '导出明细', disabled: rows.length === 0, onClick: exportMileage }, { title: offlineVehicleCount > 0 ? '关注离线影响' : '在线状态正常', value: `${offlineVehicleCount.toLocaleString()} 离线`, detail: offlineVehicleCount > 0 ? '离线车辆会影响实时位置、当日里程和告警触达,建议同步运营处理。' : '当前筛选范围内在线状态未形成主要统计风险。', color: offlineVehicleCount > 0 ? 'orange' as const : 'green' as const, action: '在线态势', disabled: false, onClick: copyOnlineOperationsReport } ]; const mileageCustomerQuestions = [ { question: '这段时间总共跑了多少?', answer: `${formatKm(summary.totalMileageKm)} km / ${summary.vehicleCount.toLocaleString()} 辆车`, action: '复制摘要', color: 'blue' as const, disabled: false, onClick: copyStatisticsSummary }, { question: '这份里程能给客户或 BI 吗?', answer: `${mileageDeliveryState} / ${publishAuditConclusion}`, action: mileageDeliveryState === '可交付' ? '复制交付包' : '发布审计', color: mileageDeliveryColor, disabled: false, onClick: mileageDeliveryState === '可交付' ? copyMileageDeliveryPackage : copyPublishAudit }, { question: '哪天或哪辆车有异常?', answer: anomalyRows[0] ? `${anomalyRows[0].plate || anomalyRows[0].vin} / ${anomalyRows[0].date}` : '当前明细无异常', action: anomalyRows[0] ? '异常证据' : '复制决策', color: anomalyRows.length > 0 ? 'orange' as const : 'green' as const, disabled: false, onClick: () => anomalyRows[0] ? copyMileageEvidence(anomalyRows[0]) : copyMileageDecision() }, { question: '这段路能回放轨迹吗?', answer: currentVehicleKeyword ? mileageRangeText : '请先选择车辆', action: '轨迹复核', color: currentVehicleKeyword ? 'blue' as const : 'grey' as const, disabled: !currentVehicleKeyword || !onOpenHistory, onClick: () => onOpenHistory?.({ keyword: currentVehicleKeyword, protocol: currentProtocol, dateFrom: filters.dateFrom ?? '', dateTo: filters.dateTo ?? '' }) }, { question: '明细数据能导出吗?', answer: `${rows.length.toLocaleString()} 条当前页 / 覆盖 ${formatPercent(pageCoverageRate)}`, action: '导出明细', color: rows.length > 0 ? 'blue' as const : 'grey' as const, disabled: rows.length === 0, onClick: exportMileage } ]; 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)}> 当前车辆服务 )} />
客户里程统计工作台 {mileageDeliveryState} {publishAuditConclusion} 统计页先回答客户这段时间跑了多少、每日是否闭合、异常在哪里、能否导出。
车辆范围:{currentVehicleKeyword || '全部车辆'} 时间范围:{mileageRangeText} 来源证据只用于统计可信度:{currentProtocol || '全部数据通道'}
{mileageCustomerWorkbenchItems.map((item) => ( ))}
客户里程对账台 {mileageDeliveryState} {publishAuditConclusion} 用同一辆车和同一时间窗完成区间里程、日报闭合、轨迹证据、告警通知和报表导出。 面向客户的统计页只围绕车辆服务展开:先看里程结论,再用轨迹和在线态势解释风险,最后交付 CSV 或复核说明。
{mileageReconciliationItems.map((item) => ( ))}
客户里程交付台 {mileageDeliveryState} {closureStatus} 按自定义时间范围交付里程、闭合校验、异常复核和导出证据。 客户看统计页时先拿到可交付结论,再按同一车辆和时间窗进入轨迹、历史明细、发布审计和 CSV 导出。
{mileageDeliveryConsoleItems.map((item) => ( ))}
当前车辆:{currentVehicleKeyword || '-'} 数据通道:{currentProtocol || '全部数据通道'} 统计查询以车辆为对象,当前先覆盖里程、在线影响、闭合审计和异常复核。
客户统计查询 {mileageDeliveryState} {closureStatus} 先定车辆和时间范围,再判断里程能否交付 统计查询从车辆服务出发:选择车辆或车队、设定时间窗、判断区间里程和发布状态,再进入轨迹、历史明细和导出明细。
{mileageCustomerQuerySteps.map((item) => ( ))}
{mileageDeliveryState} {publishAuditConclusion} {closureStatus} 把统计结果变成客户可交付的里程说明 把里程统计变成客户能看懂的交付路径:先确认口径,再回放轨迹,最后导出明细和结论。
{mileageDeliveryNavigation.map((item) => ( ))}
{mileageCustomerQuestions.map((item) => ( ))}
{publishAuditConclusion} {mileageDeliveryState} {confidenceStatus} 从车辆服务视角安排下一步 里程页不再让客户先理解 GB32960、JT808、MQTT 的差异,而是直接给出交付、轨迹回放、历史查询、导出和在线影响的下一步动作。
{mileageNextActions.map((item) => ( ))}
当前统计证据 {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}
))}
客户里程决策台} style={{ marginTop: 16 }} >
{mileageDeliveryState} {publishAuditConclusion} {confidenceStatus} 先判断能不能交付,再进入轨迹、历史明细和导出复核 客户关心的是区间里程是否可信、异常是否解释清楚、离线是否影响统计。数据通道只作为可追溯依据,不作为页面主入口。
{mileageDecisionItems.map((item) => ( ))}
{mileageDeliveryState} {closureStatus}
{formatKm(summary.totalMileageKm)} km
面向客户交付里程时,先确认区间里程、闭合状态、异常记录和在线影响,再导出明细或进入轨迹与历史明细复核。
{[ { label: '区间里程', value: `${formatKm(summary.totalMileageKm)} km`, detail: `范围 ${mileageRangeText},当前页明细 ${formatKm(pageMileageTotal)} km。`, color: 'blue' as const, action: '复制交付', onClick: copyMileageDeliveryPackage }, { label: '闭合校验', value: closureStatus, detail: closureDeltaKm == null ? '当前页未覆盖全量记录,交付前需导出或缩小范围复核。' : `闭合差值 ${formatKm(Math.abs(closureDeltaKm))} km。`, color: closureStatusColor, action: '闭合摘要', onClick: copyStatisticsSummary }, { label: '异常记录', value: anomalyRows.length.toLocaleString(), detail: anomalyRows[0] ? `${anomalyRows[0].plate || anomalyRows[0].vin} / ${anomalyRows[0].date} 需要复核。` : '当前页没有异常里程记录。', color: anomalyRows.length > 0 ? 'orange' as const : 'green' as const, action: '异常明细', onClick: () => anomalyRows[0] ? copyMileageEvidence(anomalyRows[0]) : copyMileageDeliveryPackage }, { label: '在线影响', value: formatPercent(onlineRatePercent), detail: `${onlineVehicleCount.toLocaleString()} 在线 / ${offlineVehicleCount.toLocaleString()} 离线,影响当日里程完整性判断。`, color: onlineRatePercent >= 90 ? 'green' as const : onlineRatePercent >= 60 ? 'orange' as const : 'red' as const, action: '在线态势', onClick: copyOnlineOperationsReport } ].map((item) => ( ))}
统计影响范围} style={{ marginTop: 16 }} >
{statisticsImpactLevel}
{summary.vehicleCount.toLocaleString()} 辆车
当前筛选会影响 {summary.recordCount.toLocaleString()} 条里程记录、{formatKm(summary.totalMileageKm)} km 统计口径和 {offlineVehicleCount.toLocaleString()} 辆离线车辆判断。 {confidenceStatus} {closureStatus} {publishAuditConclusion}
{statisticsImpactItems.map((item) => (
{item.label} {item.value} {item.detail}
))}
{mileageServiceTasks.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}
车辆里程核对 面向车辆运营查看区间里程、异常记录、在线影响和复核动作。
{closureStatus} {publishAuditConclusion}
{mileageCustomerKpis.map((item) => ( ))}
日里程趋势
{dateSeries.length > 0 ? dateSeries.slice(0, 10).map((item) => (
{item.date}
0 ? Math.max(4, (item.value / maxDateMileage) * 100) : 0}%` }} />
{formatKm(item.value)} km
)) : 暂无日里程趋势。}
来源贡献
{sourceSeries.length > 0 ? sourceSeries.slice(0, 5).map((item) => (
{item.source}
0 ? Math.max(4, (item.value / maxSourceMileage) * 100) : 0}%` }} />
{formatKm(item.value)} km
)) : 暂无来源贡献。}
{[ { 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}
))}
在线统计运营态势} style={{ marginTop: 16 }} >
= 90 ? 'green' : onlineRatePercent >= 60 ? 'orange' : 'red'}> 在线率 {formatPercent(onlineRatePercent)} Redis {onlineSummary.redisOnlineKeys == null ? '-' : onlineSummary.redisOnlineKeys.toLocaleString()}
{onlineVehicleCount.toLocaleString()} 在线
在线状态用于解释统计缺口、离线车辆影响和当日里程是否需要延迟发布。
{[ { label: '离线车辆', value: offlineVehicleCount.toLocaleString(), detail: '离线车辆会影响实时统计、位置查询和当日里程完整性。', color: offlineVehicleCount > 0 ? 'orange' as const : 'green' as const }, { label: '来源覆盖', value: onlineProtocolStats.length.toLocaleString(), detail: onlineProtocolStats.length > 0 ? onlineProtocolStats.map((item) => `${item.protocol} ${item.online}/${item.total}`).join(';') : '暂无来源在线统计。', color: onlineProtocolStats.length > 0 ? 'blue' as const : 'grey' as const }, { label: '当前页离线', value: onlineRows.filter((row) => !row.online).length.toLocaleString(), detail: `${onlineRows.length.toLocaleString()} 条在线状态明细参与当前页复核。`, color: onlineRows.some((row) => !row.online) ? 'orange' as const : 'green' as const }, { label: '最长离线', value: topOfflineRow ? formatOfflineDuration(topOfflineRow.offlineDurationMinutes) : '-', detail: topOfflineRow ? `${topOfflineRow.plate || topOfflineRow.vin} / ${topOfflineRow.lastSeen || '-'}` : '当前页暂无离线车辆。', color: topOfflineRow ? 'orange' as const : 'green' as const }, { label: '统计证据', value: onlineSummary.evidence || '-', detail: '后端在线统计证据,可与 Redis key 和车辆快照互相校验。', color: onlineSummary.evidence ? 'blue' as const : 'grey' as const } ].map((item) => (
{item.label} {item.value} {item.detail}
))}
{[ { 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 前需要结合异常记录、轨迹回放和历史明细完成复核。'}
{confidenceStatus}
{confidenceStatus === '可用于 BI' && closureStatus === '闭合通过' ? '可发布' : '需复核后发布'}
日里程按首末总里程差值计算,区间总值必须等于每日里程之和;发布前同步检查异常记录、分页覆盖和通道一致性。
闭合状态{closureStatus}
记录覆盖{formatPercent(pageCoverageRate)}
异常记录{anomalyRows.length.toLocaleString()}
通道一致性{sourceConsistencyText}
{publishAuditConclusion}
通过 {publishAuditItems.length - publishReviewCount - publishBlockedCount} / 复核 {publishReviewCount} / 阻断 {publishBlockedCount}
发布前统一审计闭合、覆盖、异常、数据通道和在线状态;不满足时先回到轨迹和历史明细复核。
{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 和运营报表口径。

异常复核

异常优先检查补传、通道切换、总里程回退和跨通道差异,必要时回到车辆服务查看历史明细。

当前页 {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) => ( ) } ]} /> ); }