Files
lingniu-vehicle-ingest/vehicle-data-platform/apps/web/src/pages/Mileage.tsx

3087 lines
141 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<MileageSummary> | 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<string, string>, 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<string, string> = {}) {
return {
keyword: initialVin,
protocol: initialProtocol ?? '',
...initialFilters
};
}
function groupMileageByDate(rows: DailyMileageRow[]) {
const bucket = new Map<string, number>();
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<string, number>();
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<DailyMileageRow | undefined>((max, row) => {
if (!max || row.dailyMileageKm > max.dailyMileageKm) return row;
return max;
}, undefined);
}
const mileageExportColumns: CsvColumn<DailyMileageRow>[] = [
{ 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<string, string>) {
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<string, string>;
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<string, string>;
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<string, string>;
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 mileageReconciliationControlText({
filters,
summary,
pageMileageTotal,
closureStatus,
closureDeltaKm,
anomalyCount,
pageCoverageRate,
onlineSummary,
peakMileage,
statisticsURL,
vehicleURL
}: {
filters: Record<string, string>;
summary: MileageSummary;
pageMileageTotal: number;
closureStatus: string;
closureDeltaKm?: number;
anomalyCount: number;
pageCoverageRate: number;
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`
: '-';
return [
'【客户里程对账总控包】',
`服务链路:区间里程 -> 每日闭合 -> 轨迹证明 -> 报表导出`,
`对账范围:${keyword} / ${protocol} / ${dateRange}`,
`区间里程:${formatKm(summary.totalMileageKm)} km`,
`明细合计:${formatKm(pageMileageTotal)} km`,
`闭合状态:${closureStatus}${closureDeltaKm == null ? '' : `,差值 ${formatKm(Math.abs(closureDeltaKm))} km`}`,
`异常记录:${anomalyCount.toLocaleString()}`,
`覆盖率:${formatPercent(pageCoverageRate)}`,
`在线影响:${onlineSummary.onlineVehicleCount.toLocaleString()} 在线 / ${onlineSummary.offlineVehicleCount.toLocaleString()} 离线 / 在线率 ${formatPercent(onlineSummary.onlineRatePercent)}`,
`轨迹证明:${peak}`,
`里程统计:${statisticsURL}`,
`报表导出:${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<string, string>;
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<string, string>;
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<string, string>;
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<string, string>;
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<string, string>;
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<OnlineVehicleStatusRow | undefined>((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<string, string>;
onFiltersChange?: (filters: Record<string, string>) => void;
onOpenVehicle: (vin: string, protocol?: string) => void;
onOpenHistory?: (filters: Record<string, string>) => void;
onOpenRaw?: (filters: Record<string, string>) => void;
}) {
const [rows, setRows] = useState<DailyMileageRow[]>([]);
const [onlineRows, setOnlineRows] = useState<OnlineVehicleStatusRow[]>([]);
const [summary, setSummary] = useState<MileageSummary>(emptySummary);
const [onlineSummary, setOnlineSummary] = useState<OnlineStatisticsSummary>(emptyOnlineSummary);
const [loading, setLoading] = useState(true);
const [onlineLoading, setOnlineLoading] = useState(true);
const [summaryLoading, setSummaryLoading] = useState(true);
const [filters, setFilters] = useState<Record<string, string>>(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<OnlineVehicleStatusRow | undefined>((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 mileageConclusionItems = [
{
label: '区间累计',
value: `${formatKm(summary.totalMileageKm)} km`,
detail: `${summary.vehicleCount.toLocaleString()} 辆车,${mileageRangeText}`,
color: 'blue' as const,
onClick: () => copyStatisticsSummary()
},
{
label: '明细合计',
displayLabel: '当前明细',
value: `${formatKm(pageMileageTotal)} km`,
detail: `${rows.length.toLocaleString()} 条当前页明细,覆盖 ${formatPercent(pageCoverageRate)}`,
color: pageCoverageRate >= 100 ? 'green' as const : 'orange' as const,
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,
onClick: () => anomalyRows[0] ? copyMileageEvidence(anomalyRows[0]) : copyMileageDecision()
},
{
label: '在线影响',
value: `${offlineVehicleCount.toLocaleString()} 离线`,
detail: `${onlineVehicleCount.toLocaleString()} 在线,在线率 ${formatPercent(onlineRatePercent)}`,
color: offlineVehicleCount > 0 ? 'orange' as const : 'green' as const,
onClick: () => copyOnlineOperationsReport()
}
];
const mileageConclusionActions = [
{
label: '复制结论',
action: '复制摘要',
color: 'blue' as const,
disabled: false,
onClick: () => copyMileageDeliveryPackage()
},
{
label: '轨迹复核',
action: '轨迹回放',
color: currentVehicleKeyword ? 'blue' as const : 'grey' as const,
disabled: !currentVehicleKeyword || !onOpenHistory,
onClick: () => onOpenHistory?.({ keyword: currentVehicleKeyword, protocol: currentProtocol, dateFrom: filters.dateFrom ?? '', dateTo: filters.dateTo ?? '' })
},
{
label: '导出明细',
action: '导出CSV',
color: rows.length > 0 ? 'blue' as const : 'grey' as const,
disabled: rows.length === 0,
onClick: () => exportMileage()
},
{
label: '告警通知',
action: '在线态势',
color: offlineVehicleCount > 0 ? 'orange' as const : 'green' as const,
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<HTMLInputElement>('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 mileageReconciliationConclusionItems = [
{
label: '区间总里程',
value: `${formatKm(summary.totalMileageKm)} km`,
detail: `${summary.vehicleCount.toLocaleString()} 辆车,${mileageRangeText}`,
action: '复制摘要',
color: 'blue' as const,
disabled: false,
onClick: () => copyStatisticsSummary()
},
{
label: '闭合状态',
value: closureStatus,
detail: closureActionText,
action: '发布审计',
color: closureStatusColor,
disabled: false,
onClick: () => copyPublishAudit()
},
{
label: '异常记录',
value: `${anomalyRows.length.toLocaleString()}`,
detail: anomalyRows[0]
? `${anomalyRows[0].plate || anomalyRows[0].vin} / ${anomalyRows[0].date}`
: '当前明细没有异常标记。',
action: '复制决策',
color: anomalyRows.length > 0 ? 'orange' as const : 'green' as const,
disabled: false,
onClick: () => copyMileageDecision()
},
{
label: '下一步',
value: mileageDeliveryState,
detail: mileageDeliveryState === '可交付' ? '可以进入客户交付、BI复核或导出。' : '先补齐闭合、覆盖、异常或在线影响证据。',
action: '查看证据',
color: mileageDeliveryColor,
disabled: !currentVehicleKeyword || (!onOpenHistory && !onOpenRaw),
onClick: () => {
if (currentVehicleKeyword && onOpenHistory) {
onOpenHistory({ keyword: currentVehicleKeyword, protocol: currentProtocol, dateFrom: filters.dateFrom ?? '', dateTo: filters.dateTo ?? '' });
return;
}
if (currentVehicleKeyword && onOpenRaw) {
onOpenRaw({ keyword: currentVehicleKeyword, protocol: currentProtocol, dateFrom: filters.dateFrom ?? '', dateTo: filters.dateTo ?? '', includeFields: 'true' });
return;
}
copyMileageDeliveryPackage();
}
}
];
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 mileageAcceptanceItems = [
{
label: '范围确认',
value: currentVehicleKeyword || '全部车辆',
detail: `${mileageRangeText} / ${currentProtocol || '全部数据通道'}`,
color: currentVehicleKeyword ? 'blue' as const : 'orange' as const,
disabled: false,
onClick: () => copyStatisticsSummary()
},
{
label: '区间闭合',
value: closureStatus,
detail: closureDeltaKm == null ? '当前为分页抽样,交付前需要拉齐全量明细。' : `闭合差值 ${formatKm(Math.abs(closureDeltaKm))} km。`,
color: closureStatusColor,
disabled: false,
onClick: () => copyPublishAudit()
},
{
label: '明细覆盖',
value: formatPercent(pageCoverageRate),
detail: `${rows.length.toLocaleString()} / ${summary.recordCount.toLocaleString()} 条记录,覆盖全量后再交付更稳。`,
color: pageCoverageRate >= 100 ? 'green' as const : 'orange' as const,
disabled: rows.length === 0,
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,
disabled: false,
onClick: () => anomalyRows[0] ? openMileageEvidence(anomalyRows[0]) : copyMileageDecision()
},
{
label: '在线影响',
value: `${offlineVehicleCount.toLocaleString()} 离线`,
detail: offlineVehicleCount > 0 ? '离线车辆会影响当日解释和客户通知。' : '在线状态未形成主要统计风险。',
color: offlineVehicleCount > 0 ? 'orange' as const : 'green' as const,
disabled: false,
onClick: () => copyOnlineOperationsReport()
}
];
const mileageAcceptanceActions = [
{
label: '复制验收',
action: '交付说明',
color: mileageDeliveryColor,
disabled: false,
onClick: () => copyMileageDeliveryPackage()
},
{
label: '复核轨迹',
action: '轨迹',
color: currentVehicleKeyword ? 'blue' as const : 'grey' as const,
disabled: !currentVehicleKeyword || !onOpenHistory,
onClick: () => onOpenHistory?.({ keyword: currentVehicleKeyword, protocol: currentProtocol, dateFrom: filters.dateFrom ?? '', dateTo: filters.dateTo ?? '' })
},
{
label: '导出明细',
action: 'CSV',
color: rows.length > 0 ? 'blue' as const : 'grey' as const,
disabled: rows.length === 0,
onClick: () => exportMileage()
}
];
const loadSummary = (values: Record<string, string> = 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<string, string> = 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<string, string> = 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<string, string>) => {
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 copyMileageReconciliationControl = () => {
const vehicleURL = currentVehicleKeyword
? appURL(buildAppHash({ page: 'detail', keyword: currentVehicleKeyword, protocol: currentProtocol }))
: undefined;
copyText(
mileageReconciliationControlText({
filters,
summary,
pageMileageTotal,
closureStatus,
closureDeltaKm,
anomalyCount: anomalyRows.length,
pageCoverageRate,
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 copyMileageReportJourneyPackage = () => {
const vehicleURL = currentVehicleKeyword
? appURL(buildAppHash({ page: 'detail', keyword: currentVehicleKeyword, protocol: currentProtocol }))
: undefined;
const reportPeriod = mileageRangeText;
const trajectoryProof = peakMileage
? `${peakMileage.plate || peakMileage.vin} / ${peakMileage.date} / ${formatKm(peakMileage.dailyMileageKm)} km`
: '暂无轨迹证明样本';
copyText([
'【里程报表交付旅程包】',
`报表周期:${reportPeriod}`,
`车辆范围:${currentVehicleKeyword || '全部车辆'}`,
`数据通道:${currentProtocol || '全部数据通道'}`,
`区间里程:${formatKm(summary.totalMileageKm)} km`,
`区间闭合:${closureStatus}`,
`记录覆盖:${formatPercent(pageCoverageRate)}`,
`异常记录:${anomalyRows.length.toLocaleString()}`,
`轨迹证明:${trajectoryProof}`,
`导出发布:${mileageDeliveryState} / ${publishAuditConclusion}`,
`在线影响:${onlineVehicleCount.toLocaleString()} 在线 / ${offlineVehicleCount.toLocaleString()} 离线 / 在线率 ${formatPercent(onlineRatePercent)}`,
`里程统计:${appURL(window.location.hash || buildAppHash({ page: 'mileage', keyword: currentVehicleKeyword, protocol: currentProtocol }))}`,
`轨迹回放:${appURL(buildAppHash({ page: 'history', keyword: currentVehicleKeyword, protocol: currentProtocol, filters: { ...(filters.dateFrom ? { dateFrom: filters.dateFrom } : {}), ...(filters.dateTo ? { dateTo: filters.dateTo } : {}) } }))}`,
`历史明细:${appURL(buildAppHash({ page: 'history-query', keyword: currentVehicleKeyword, protocol: currentProtocol, filters: { ...(filters.dateFrom ? { dateFrom: filters.dateFrom } : {}), ...(filters.dateTo ? { dateTo: filters.dateTo } : {}), tab: 'raw', includeFields: 'true' } }))}`,
...(vehicleURL ? [`车辆服务:${vehicleURL}`] : [])
].join('\n'), '里程报表交付旅程包');
};
const mileageReportJourneyItems = [
{
step: '1',
title: '报表周期',
value: mileageRangeText,
detail: `${currentVehicleKeyword || '全部车辆'} / ${currentProtocol || '全部数据通道'},先确认客户要看的车辆和时间段。`,
color: currentVehicleKeyword ? 'blue' as const : 'orange' as const,
disabled: false,
onClick: copyStatisticsSummary
},
{
step: '2',
title: '区间闭合',
value: closureStatus,
detail: closureDeltaKm == null ? `当前明细覆盖 ${formatPercent(pageCoverageRate)},发布前需要拉齐全量。` : `闭合差值 ${formatKm(Math.abs(closureDeltaKm))} km。`,
color: closureStatusColor,
disabled: false,
onClick: copyPublishAudit
},
{
step: '3',
title: '轨迹证明',
value: peakMileage?.plate || peakMileage?.vin || '待选车辆',
detail: peakMileage ? `${peakMileage.date} 最大单日 ${formatKm(peakMileage.dailyMileageKm)} km建议回放轨迹证明。` : '选择车辆后可进入同一时间窗轨迹回放。',
color: currentVehicleKeyword ? 'blue' as const : 'grey' as const,
disabled: !currentVehicleKeyword || !onOpenHistory,
onClick: () => {
if (peakMileage) {
openMileageEvidence(peakMileage);
return;
}
onOpenHistory?.({ keyword: currentVehicleKeyword, protocol: currentProtocol, dateFrom: filters.dateFrom ?? '', dateTo: filters.dateTo ?? '' });
}
},
{
step: '4',
title: '导出发布',
value: mileageDeliveryState,
detail: `${publishAuditConclusion},导出明细和交付包给客户或 BI 复核。`,
color: mileageDeliveryColor,
disabled: false,
onClick: copyMileageReportJourneyPackage
}
];
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 mileageReconciliationControlItems = [
{
title: '区间里程',
value: `${formatKm(summary.totalMileageKm)} km`,
detail: `${summary.vehicleCount.toLocaleString()} 辆车,${mileageRangeText}`,
action: '复制摘要',
color: 'blue' as const,
disabled: false,
onClick: copyStatisticsSummary
},
{
title: '每日闭合',
value: closureStatus,
detail: closureDeltaKm == null ? `当前页覆盖 ${formatPercent(pageCoverageRate)},交付前需要拉齐全量明细。` : `闭合差值 ${formatKm(Math.abs(closureDeltaKm))} km。`,
action: '闭合审计',
color: closureStatusColor,
disabled: false,
onClick: copyPublishAudit
},
{
title: '轨迹证明',
value: peakMileage?.date || mileageRangeText,
detail: peakMileage ? `${peakMileage.plate || peakMileage.vin} 最大单日 ${formatKm(peakMileage.dailyMileageKm)} km。` : '选择车辆后按同一时间窗回放轨迹。',
action: '轨迹复核',
color: currentVehicleKeyword ? 'blue' as const : 'grey' as const,
disabled: !currentVehicleKeyword || !onOpenHistory,
onClick: () => {
if (peakMileage) {
openMileageEvidence(peakMileage);
return;
}
onOpenHistory?.({ keyword: currentVehicleKeyword, protocol: currentProtocol, dateFrom: filters.dateFrom ?? '', dateTo: filters.dateTo ?? '' });
}
},
{
title: '报表导出',
value: `${rows.length.toLocaleString()}`,
detail: '导出当前里程明细和口径说明,作为客户对账附件。',
action: '导出报表',
color: rows.length > 0 ? 'blue' as const : 'grey' as const,
disabled: rows.length === 0,
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 mileageDefinitionMetrics = [
{
label: '区间总值',
value: `${formatKm(summary.totalMileageKm)} km`,
detail: `来自汇总口径,覆盖 ${summary.vehicleCount.toLocaleString()} 辆车。`,
color: 'blue' as const,
onClick: copyStatisticsSummary
},
{
label: '明细合计',
value: `${formatKm(pageMileageTotal)} km`,
detail: `${rows.length.toLocaleString()} / ${summary.recordCount.toLocaleString()} 条明细,覆盖 ${formatPercent(pageCoverageRate)}`,
color: hasFullMileagePage ? 'green' as const : 'orange' as const,
onClick: copyPublishAudit
},
{
label: '闭合差值',
value: closureDeltaKm == null ? '需全量' : `${formatKm(Math.abs(closureDeltaKm))} km`,
detail: closureDeltaKm == null ? '当前为分页抽样,交付前需要拉齐全量明细。' : closureActionText,
color: closureStatusColor,
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,
onClick: () => anomalyRows[0] ? copyMileageEvidence(anomalyRows[0]) : copyMileageDecision()
}
];
const mileageDefinitionActions = [
{
label: '复制口径',
action: '决策说明',
color: 'blue' as const,
disabled: false,
onClick: copyMileageDecision
},
{
label: '复制交付',
action: '交付包',
color: mileageDeliveryColor,
disabled: false,
onClick: copyMileageDeliveryPackage
},
{
label: '轨迹复核',
action: '轨迹',
color: currentVehicleKeyword ? 'blue' as const : 'grey' as const,
disabled: !currentVehicleKeyword || !onOpenHistory,
onClick: () => onOpenHistory?.({ keyword: currentVehicleKeyword, protocol: currentProtocol, dateFrom: filters.dateFrom ?? '', dateTo: filters.dateTo ?? '' })
},
{
label: '导出明细',
action: 'CSV',
color: rows.length > 0 ? 'blue' as const : 'grey' as const,
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 (
<div className="vp-page">
<PageHeader
title="里程统计"
description="按车辆查询里程、在线影响、区间闭合、异常复核和数据交付"
actions={(
<Button disabled={!currentVehicleKeyword} onClick={() => onOpenVehicle(currentVehicleKeyword, currentProtocol)}>
</Button>
)}
/>
<section className="vp-mileage-conclusion-strip" aria-label="客户里程对账总控">
<div className="vp-mileage-conclusion-copy">
<Space wrap>
<Tag color="blue"></Tag>
<Tag color={mileageDeliveryColor}>{mileageDeliveryState}</Tag>
<Tag color={closureStatusColor}>{closureStatus}</Tag>
</Space>
<Typography.Title heading={5} style={{ margin: 0 }}>
</Typography.Title>
<Typography.Text type="secondary">
</Typography.Text>
<Space wrap>
<Button size="small" theme="solid" type="primary" onClick={copyMileageReconciliationControl}></Button>
<Button size="small" disabled={!currentVehicleKeyword || !onOpenHistory} onClick={() => onOpenHistory?.({ keyword: currentVehicleKeyword, protocol: currentProtocol, dateFrom: filters.dateFrom ?? '', dateTo: filters.dateTo ?? '' })}></Button>
<Button size="small" disabled={rows.length === 0} onClick={exportMileage}></Button>
</Space>
</div>
<div className="vp-mileage-conclusion-metrics">
{mileageReconciliationControlItems.map((item) => (
<button
key={item.title}
type="button"
className="vp-mileage-conclusion-metric"
disabled={item.disabled}
onClick={item.onClick}
aria-label={`客户里程对账总控 ${item.title} ${item.value} ${item.action}`}
>
<Tag color={item.color}>{item.title}</Tag>
<strong>{item.value}</strong>
<span>{item.detail}</span>
<em>{item.action}</em>
</button>
))}
</div>
</section>
<section className="vp-mileage-report-journey" aria-label="里程报表交付旅程">
<div className="vp-mileage-report-journey-copy">
<Space wrap>
<Tag color="blue"></Tag>
<Tag color={mileageDeliveryColor}>{mileageDeliveryState}</Tag>
<Tag color={publishAuditColor}>{publishAuditConclusion}</Tag>
</Space>
<Typography.Title heading={5} style={{ margin: 0 }}>
</Typography.Title>
<Typography.Text type="secondary">
</Typography.Text>
<Button size="small" theme="solid" type="primary" onClick={copyMileageReportJourneyPackage}>
</Button>
</div>
<div className="vp-mileage-report-journey-steps">
{mileageReportJourneyItems.map((item) => (
<button
key={item.step}
type="button"
className="vp-mileage-report-journey-step"
disabled={item.disabled}
onClick={item.onClick}
aria-label={`里程报表交付旅程 ${item.step} ${item.title} ${item.value}`}
>
<Tag color={item.color}>{item.step}</Tag>
<strong>{item.title}</strong>
<span>{item.value}</span>
<small>{item.detail}</small>
</button>
))}
</div>
</section>
<section className="vp-mileage-conclusion-strip" aria-label="客户里程结论条">
<div className="vp-mileage-conclusion-copy">
<Space wrap>
<Tag color="blue"></Tag>
<Tag color={mileageDeliveryColor}>{mileageDeliveryState}</Tag>
<Tag color={closureStatusColor}>{closureStatus}</Tag>
</Space>
<Typography.Title heading={5} style={{ margin: 0 }}>
</Typography.Title>
<Typography.Text type="secondary">
{currentVehicleKeyword || '全部车辆'} / {mileageRangeText}
</Typography.Text>
</div>
<div className="vp-mileage-conclusion-metrics">
{mileageConclusionItems.map((item) => (
<button
key={item.label}
type="button"
className="vp-mileage-conclusion-metric"
onClick={item.onClick}
aria-label={`客户里程结论 ${item.label} ${item.value}`}
>
<Tag color={item.color}>{item.displayLabel ?? item.label}</Tag>
<strong>{item.value}</strong>
<span>{item.detail}</span>
</button>
))}
</div>
<div className="vp-mileage-conclusion-actions">
{mileageConclusionActions.map((item) => (
<button
key={item.label}
type="button"
className="vp-mileage-conclusion-action"
disabled={item.disabled}
onClick={item.onClick}
aria-label={`客户里程动作 ${item.label} ${item.action}`}
>
<Tag color={item.color}>{item.label}</Tag>
<span>{item.action}</span>
</button>
))}
</div>
</section>
<section className="vp-mileage-definition-strip" aria-label="客户里程口径交付条">
<div className="vp-mileage-definition-copy">
<Space wrap>
<Tag color="blue"></Tag>
<Tag color={closureStatusColor}>{closureStatus}</Tag>
<Tag color={publishAuditColor}>{publishAuditConclusion}</Tag>
</Space>
<Typography.Title heading={5} style={{ margin: 0 }}>
</Typography.Title>
<Typography.Text type="secondary">
BI
</Typography.Text>
</div>
<div className="vp-mileage-definition-metrics">
{mileageDefinitionMetrics.map((item) => (
<button
key={item.label}
type="button"
className="vp-mileage-definition-metric"
onClick={item.onClick}
aria-label={`客户里程口径交付 ${item.label} ${item.value}`}
>
<Tag color={item.color}>{item.label}</Tag>
<strong>{item.value}</strong>
<span>{item.detail}</span>
</button>
))}
</div>
<div className="vp-mileage-definition-actions">
{mileageDefinitionActions.map((item) => (
<button
key={item.label}
type="button"
className="vp-mileage-definition-action"
disabled={item.disabled}
onClick={item.onClick}
aria-label={`客户里程口径动作 ${item.label} ${item.action}`}
>
<Tag color={item.color}>{item.label}</Tag>
<span>{item.action}</span>
</button>
))}
</div>
</section>
<section className="vp-mileage-customer-workbench" aria-label="客户里程统计工作台">
<div className="vp-mileage-customer-workbench-main">
<Space wrap>
<Tag color="blue"></Tag>
<Tag color={mileageDeliveryColor}>{mileageDeliveryState}</Tag>
<Tag color={publishAuditColor}>{publishAuditConclusion}</Tag>
</Space>
<Typography.Title heading={5} style={{ margin: 0 }}>
</Typography.Title>
<div className="vp-mileage-customer-workbench-meta">
<span>{currentVehicleKeyword || '全部车辆'}</span>
<span>{mileageRangeText}</span>
<span>{currentProtocol || '全部数据通道'}</span>
</div>
</div>
<div className="vp-mileage-customer-workbench-grid">
{mileageCustomerWorkbenchItems.map((item) => (
<button
key={item.label}
type="button"
className="vp-mileage-customer-workbench-item"
disabled={item.disabled}
onClick={item.onClick}
aria-label={`客户里程统计工作台 ${item.label} ${item.value} ${item.action}`}
>
<Tag color={item.color}>{item.label}</Tag>
<strong>{item.value}</strong>
<span>{item.detail}</span>
<em>{item.action}</em>
</button>
))}
</div>
</section>
<section className="vp-mileage-reconciliation-conclusion" aria-label="里程对账结论">
<div className="vp-mileage-reconciliation-conclusion-summary">
<Space wrap>
<Tag color="blue"></Tag>
<Tag color={mileageDeliveryColor}>{mileageDeliveryState}</Tag>
</Space>
<Typography.Title heading={5} style={{ margin: 0 }}>
</Typography.Title>
</div>
<div className="vp-mileage-reconciliation-conclusion-grid">
{mileageReconciliationConclusionItems.map((item) => (
<button
key={item.label}
type="button"
className="vp-mileage-reconciliation-conclusion-item"
disabled={item.disabled}
onClick={item.onClick}
aria-label={`里程对账结论 ${item.label} ${item.value} ${item.action}`}
>
<Tag color={item.color}>{item.label}</Tag>
<strong>{item.value}</strong>
<span>{item.detail}</span>
<em>{item.action}</em>
</button>
))}
</div>
</section>
<section className="vp-mileage-reconciliation-desk" aria-label="客户里程对账台">
<div className="vp-mileage-reconciliation-summary">
<Space wrap>
<Tag color="blue"></Tag>
<Tag color={mileageDeliveryColor}>{mileageDeliveryState}</Tag>
<Tag color={publishAuditColor}>{publishAuditConclusion}</Tag>
</Space>
<Typography.Title heading={5} style={{ margin: 0 }}></Typography.Title>
<Typography.Text type="secondary">
线 CSV
</Typography.Text>
</div>
<div className="vp-mileage-reconciliation-grid">
{mileageReconciliationItems.map((item) => (
<button
key={item.label}
type="button"
className="vp-mileage-reconciliation-item"
disabled={item.disabled}
onClick={item.onClick}
aria-label={`客户里程对账台 ${item.label} ${item.value} ${item.action}`}
>
<Tag color={item.color}>{item.label}</Tag>
<strong>{item.value}</strong>
<span>{item.detail}</span>
<em>{item.action}</em>
</button>
))}
</div>
</section>
<section className="vp-mileage-delivery-console" aria-label="客户里程交付台">
<div className="vp-mileage-delivery-console-summary">
<Space wrap>
<Tag color="blue"></Tag>
<Tag color={mileageDeliveryColor}>{mileageDeliveryState}</Tag>
<Tag color={closureStatusColor}>{closureStatus}</Tag>
</Space>
<strong></strong>
<span> CSV </span>
</div>
<div className="vp-mileage-delivery-console-grid">
{mileageDeliveryConsoleItems.map((item) => (
<button
key={item.label}
type="button"
className="vp-mileage-delivery-console-item"
onClick={item.onClick}
aria-label={`客户里程交付台 ${item.label} ${item.value} ${item.action}`}
>
<Tag color={item.color}>{item.label}</Tag>
<strong>{item.value}</strong>
<span>{item.detail}</span>
<em>{item.action}</em>
</button>
))}
</div>
</section>
<div className="vp-scope-bar">
<span className="vp-scope-label">{currentVehicleKeyword || '-'}</span>
<Tag color={currentProtocol ? 'blue' : 'green'}>{currentProtocol || '全部数据通道'}</Tag>
<Typography.Text type="tertiary">线</Typography.Text>
</div>
<Card bordered className="vp-mileage-customer-query-board" bodyStyle={{ padding: 0 }}>
<div className="vp-mileage-customer-query-summary">
<Space wrap>
<Tag color="blue"></Tag>
<Tag color={mileageDeliveryColor}>{mileageDeliveryState}</Tag>
<Tag color={closureStatusColor}>{closureStatus}</Tag>
</Space>
<Typography.Title heading={5} style={{ margin: 0 }}></Typography.Title>
<Typography.Text type="secondary">
</Typography.Text>
<Space wrap>
<Button size="small" theme="solid" type="primary" onClick={copyStatisticsSummary}></Button>
<Button size="small" theme="light" type="primary" onClick={copyMileageDecision}></Button>
<Button size="small" disabled={!currentVehicleKeyword || !onOpenHistory} onClick={() => onOpenHistory?.({ keyword: currentVehicleKeyword, protocol: currentProtocol, dateFrom: filters.dateFrom ?? '', dateTo: filters.dateTo ?? '' })}></Button>
<Button size="small" disabled={rows.length === 0} onClick={exportMileage}></Button>
</Space>
</div>
<div className="vp-mileage-customer-query-steps">
{mileageCustomerQuerySteps.map((item) => (
<button
key={item.step}
type="button"
className="vp-mileage-customer-query-step"
disabled={item.disabled}
onClick={item.onClick}
aria-label={`客户里程统计 ${item.title} ${item.action}`}
>
<span>{item.step}</span>
<Tag color={item.color}>{item.title}</Tag>
<strong>{item.value}</strong>
<small>{item.detail}</small>
<em>{item.action}</em>
</button>
))}
</div>
</Card>
<Card bordered title="客户里程交付导航" style={{ marginTop: 16 }}>
<div className="vp-mileage-delivery-nav">
<div className="vp-mileage-delivery-nav-summary">
<Space wrap>
<Tag color={mileageDeliveryColor}>{mileageDeliveryState}</Tag>
<Tag color={publishAuditColor}>{publishAuditConclusion}</Tag>
<Tag color={closureStatusColor}>{closureStatus}</Tag>
</Space>
<Typography.Title heading={5} style={{ margin: 0 }}></Typography.Title>
<Typography.Text type="secondary">
</Typography.Text>
</div>
<div className="vp-mileage-delivery-nav-grid">
{mileageDeliveryNavigation.map((item) => (
<button
key={item.title}
type="button"
className="vp-mileage-delivery-nav-item"
disabled={item.disabled}
onClick={item.onClick}
aria-label={`客户里程交付导航 ${item.title} ${item.action}`}
>
<Tag color={item.color}>{item.title}</Tag>
<strong>{item.value}</strong>
<span>{item.detail}</span>
<em>{item.action}</em>
</button>
))}
</div>
</div>
</Card>
<section className="vp-mileage-acceptance-strip" aria-label="里程客户验收清单">
<div className="vp-mileage-acceptance-copy">
<Space wrap>
<Tag color="blue"></Tag>
<Tag color={mileageDeliveryColor}>{mileageDeliveryState}</Tag>
<Tag color={publishAuditColor}>{publishAuditConclusion}</Tag>
</Space>
<Typography.Title heading={5} style={{ margin: 0 }}>
线
</Typography.Title>
<Typography.Text type="secondary">
</Typography.Text>
</div>
<div className="vp-mileage-acceptance-grid">
{mileageAcceptanceItems.map((item) => (
<button
key={item.label}
type="button"
className="vp-mileage-acceptance-item"
disabled={item.disabled}
onClick={item.onClick}
aria-label={`里程客户验收 ${item.label} ${item.value}`}
>
<Tag color={item.color}>{item.label}</Tag>
<strong>{item.value}</strong>
<span>{item.detail}</span>
</button>
))}
</div>
<div className="vp-mileage-acceptance-actions">
{mileageAcceptanceActions.map((item) => (
<button
key={item.label}
type="button"
className="vp-mileage-acceptance-action"
disabled={item.disabled}
onClick={item.onClick}
aria-label={`里程验收动作 ${item.label} ${item.action}`}
>
<Tag color={item.color}>{item.label}</Tag>
<span>{item.action}</span>
</button>
))}
</div>
</section>
<Card bordered title="客户里程问题" style={{ marginTop: 16 }}>
<div className="vp-mileage-question-grid">
{mileageCustomerQuestions.map((item) => (
<button
key={item.question}
type="button"
className="vp-mileage-question-item"
disabled={item.disabled}
onClick={item.onClick}
aria-label={`客户里程问题 ${item.question} ${item.action}`}
>
<strong>{item.question}</strong>
<span>{item.answer}</span>
<Tag color={item.color}>{item.action}</Tag>
</button>
))}
</div>
</Card>
<Card bordered title="里程报告建议" style={{ marginTop: 16 }}>
<div className="vp-mileage-next-actions">
<div className="vp-mileage-next-summary">
<Space wrap>
<Tag color={publishAuditColor}>{publishAuditConclusion}</Tag>
<Tag color={mileageDeliveryColor}>{mileageDeliveryState}</Tag>
<Tag color={confidenceColor}>{confidenceStatus}</Tag>
</Space>
<Typography.Title heading={5} style={{ margin: 0 }}></Typography.Title>
<Typography.Text type="secondary">
GB32960JT808MQTT 线
</Typography.Text>
</div>
<div className="vp-mileage-next-grid">
{mileageNextActions.map((item) => (
<button
key={item.title}
type="button"
className="vp-mileage-next-action"
disabled={item.disabled}
onClick={item.onClick}
aria-label={`里程报告建议 ${item.title} ${item.action}`}
>
<Tag color={item.color}>{item.title}</Tag>
<strong>{item.value}</strong>
<span>{item.detail}</span>
<em>{item.action}</em>
</button>
))}
</div>
</div>
</Card>
<Card bordered title="车辆统计服务总览" style={{ marginTop: 16 }}>
<div className="vp-table-toolbar">
<Space wrap>
<Tag color="blue"></Tag>
<Typography.Text strong>{currentEvidenceText}</Typography.Text>
<Typography.Text type="secondary"></Typography.Text>
</Space>
</div>
<div className="vp-stat-insight-grid">
{[
{
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) => (
<div key={item.title} className="vp-monitor-metric">
<Tag color={item.color}>{item.title}</Tag>
<div className="vp-monitor-metric-value">{item.value}</div>
<Typography.Text type="secondary">{item.detail}</Typography.Text>
</div>
))}
</div>
</Card>
<Card
bordered
title={<Space><span></span><Button size="small" onClick={copyMileageDecision}></Button></Space>}
style={{ marginTop: 16 }}
>
<div className="vp-mileage-decision-board">
<div className="vp-mileage-decision-summary">
<Space wrap>
<Tag color={mileageDeliveryColor}>{mileageDeliveryState}</Tag>
<Tag color={publishAuditColor}>{publishAuditConclusion}</Tag>
<Tag color={confidenceColor}>{confidenceStatus}</Tag>
</Space>
<Typography.Title heading={5} style={{ margin: 0 }}></Typography.Title>
<Typography.Text type="secondary">
线
</Typography.Text>
<Space wrap>
<Button size="small" theme="solid" type="primary" onClick={copyMileageDecision}></Button>
<Button size="small" disabled={!currentVehicleKeyword || !onOpenHistory} onClick={() => onOpenHistory?.({ keyword: currentVehicleKeyword, protocol: currentProtocol, dateFrom: filters.dateFrom ?? '', dateTo: filters.dateTo ?? '' })}></Button>
<Button size="small" disabled={!currentVehicleKeyword || !onOpenRaw} onClick={() => onOpenRaw?.({ keyword: currentVehicleKeyword, protocol: currentProtocol, dateFrom: filters.dateFrom ?? '', dateTo: filters.dateTo ?? '', includeFields: 'true' })}></Button>
<Button size="small" disabled={rows.length === 0} onClick={exportMileage}></Button>
</Space>
</div>
<div className="vp-mileage-decision-grid">
{mileageDecisionItems.map((item) => (
<button
key={item.label}
type="button"
className="vp-mileage-decision-item"
aria-label={`客户里程决策 ${item.label} ${item.action}`}
onClick={item.onClick}
>
<Tag color={item.color}>{item.label}</Tag>
<strong>{item.value}</strong>
<span>{item.detail}</span>
<em>{item.action}</em>
</button>
))}
</div>
</div>
</Card>
<Card bordered title="客户里程复核交付包" style={{ marginTop: 16 }}>
<div className="vp-mileage-delivery-board">
<div className="vp-mileage-delivery-summary">
<Space wrap>
<Tag color={mileageDeliveryColor}>{mileageDeliveryState}</Tag>
<Tag color={closureStatusColor}>{closureStatus}</Tag>
</Space>
<div className="vp-monitor-metric-value">{formatKm(summary.totalMileageKm)} km</div>
<Typography.Text type="secondary">
线
</Typography.Text>
<Space wrap>
<Button size="small" theme="solid" type="primary" onClick={copyMileageDeliveryPackage}></Button>
<Button size="small" disabled={rows.length === 0} onClick={exportMileage}></Button>
<Button
size="small"
disabled={!currentVehicleKeyword || !onOpenHistory}
onClick={() => onOpenHistory?.({ keyword: currentVehicleKeyword, protocol: currentProtocol, dateFrom: filters.dateFrom ?? '', dateTo: filters.dateTo ?? '' })}
>
</Button>
</Space>
</div>
<div className="vp-mileage-delivery-grid">
{[
{
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) => (
<button
key={item.label}
type="button"
className="vp-mileage-delivery-item"
aria-label={`里程交付 ${item.label} ${item.action}`}
onClick={item.onClick}
>
<span>
<Tag color={item.color}>{item.label}</Tag>
<strong>{item.value}</strong>
</span>
<Typography.Text type="secondary">{item.detail}</Typography.Text>
<em>{item.action}</em>
</button>
))}
</div>
</div>
</Card>
<Card
bordered
title={<Space><span></span><Button size="small" onClick={copyStatisticsImpact}></Button></Space>}
style={{ marginTop: 16 }}
>
<div className="vp-stat-impact-board">
<div className="vp-stat-impact-summary">
<Tag color={statisticsImpactColor}>{statisticsImpactLevel}</Tag>
<div className="vp-monitor-metric-value">{summary.vehicleCount.toLocaleString()} </div>
<Typography.Text type="secondary">
{summary.recordCount.toLocaleString()} {formatKm(summary.totalMileageKm)} km {offlineVehicleCount.toLocaleString()} 线
</Typography.Text>
<Space wrap>
<Tag color={confidenceColor}>{confidenceStatus}</Tag>
<Tag color={closureStatusColor}>{closureStatus}</Tag>
<Tag color={publishAuditColor}>{publishAuditConclusion}</Tag>
</Space>
</div>
<div className="vp-stat-impact-grid">
{statisticsImpactItems.map((item) => (
<div key={item.label} className="vp-stat-impact-item">
<Tag color={item.color}>{item.label}</Tag>
<strong>{item.value}</strong>
<Typography.Text type="secondary">{item.detail}</Typography.Text>
</div>
))}
</div>
</div>
</Card>
<Card bordered title="里程服务任务板" style={{ marginTop: 16 }}>
<div className="vp-mileage-task-grid">
{mileageServiceTasks.map((item) => (
<div key={item.title} className="vp-mileage-task-item">
<div className="vp-mileage-task-head">
<Tag color={item.color}>{item.title}</Tag>
<strong>{item.value}</strong>
</div>
<Typography.Text type="secondary">{item.detail}</Typography.Text>
<Space wrap>
<Button
size="small"
theme="solid"
type="primary"
disabled={item.disabled}
aria-label={`里程服务任务 ${item.title} ${item.primaryAction}`}
onClick={item.onPrimary}
>
{item.primaryAction}
</Button>
<Button
size="small"
theme="light"
type="primary"
disabled={item.secondaryDisabled}
aria-label={`里程服务任务 ${item.title} ${item.secondaryAction}`}
onClick={item.onSecondary}
>
{item.secondaryAction}
</Button>
</Space>
</div>
))}
</div>
</Card>
<Card bordered>
<Form key={JSON.stringify(filters)} initValues={filters} layout="horizontal" onSubmit={(values) => {
const nextFilters = values as Record<string, string>;
applyFilters(nextFilters);
}}>
<Form.Input field="keyword" label="车辆关键词" placeholder="VIN / 车牌 / 手机号 / OEM" style={{ width: 260 }} />
<Form.Select field="protocol" label="数据通道" placeholder="全部数据通道" style={{ width: 180 }}>
<Select.Option value="GB32960">GB32960</Select.Option>
<Select.Option value="JT808">JT808</Select.Option>
<Select.Option value="YUTONG_MQTT">YUTONG_MQTT</Select.Option>
</Form.Select>
<Form.Input field="dateFrom" label="开始日期" placeholder="2026-07-01" style={{ width: 160 }} />
<Form.Input field="dateTo" label="结束日期" placeholder="2026-07-03" style={{ width: 160 }} />
<Space>
<Button htmlType="submit" theme="solid" type="primary"></Button>
<Button onClick={() => {
const nextFilters = mergeInitialFilters(initialVin, initialProtocol, {});
applyFilters(nextFilters);
}}></Button>
</Space>
</Form>
</Card>
{filterSummary.length > 0 ? (
<Card bordered title="当前里程筛选" style={{ marginTop: 16 }}>
<Space wrap>
{filterSummary.map((item) => (
<Tag key={item} color="blue">{item}</Tag>
))}
<Button size="small" onClick={clearRangeFilters}></Button>
</Space>
</Card>
) : null}
<Card bordered loading={summaryLoading || loading} title="区间里程核对台" style={{ marginTop: 16 }}>
<div className="vp-mileage-console">
<div className="vp-mileage-console-main">
<div className="vp-mileage-console-head">
<div>
<Typography.Title heading={5} style={{ margin: 0 }}></Typography.Title>
<Typography.Text type="secondary">
线
</Typography.Text>
</div>
<Space wrap>
<Tag color={closureStatusColor}>{closureStatus}</Tag>
<Tag color={publishAuditColor}>{publishAuditConclusion}</Tag>
</Space>
</div>
<div className="vp-mileage-kpi-grid">
{mileageCustomerKpis.map((item) => (
<button
key={item.label}
type="button"
className="vp-mileage-kpi-item"
onClick={() => {
if (item.label === '异常记录' && anomalyRows[0]) copyMileageEvidence(anomalyRows[0]);
}}
>
<Tag color={item.color}>{item.label}</Tag>
<strong>{item.value}</strong>
<span>{item.detail}</span>
</button>
))}
</div>
<div className="vp-mileage-mini-charts">
<div>
<div className="vp-mileage-mini-title"></div>
<div className="vp-mileage-mini-chart">
{dateSeries.length > 0 ? dateSeries.slice(0, 10).map((item) => (
<div key={item.date} className="vp-mileage-mini-row">
<span>{item.date}</span>
<div><i style={{ width: `${maxDateMileage > 0 ? Math.max(4, (item.value / maxDateMileage) * 100) : 0}%` }} /></div>
<strong>{formatKm(item.value)} km</strong>
</div>
)) : <Typography.Text type="tertiary"></Typography.Text>}
</div>
</div>
<div>
<div className="vp-mileage-mini-title"></div>
<div className="vp-mileage-mini-chart">
{sourceSeries.length > 0 ? sourceSeries.slice(0, 5).map((item) => (
<div key={item.source} className="vp-mileage-mini-row">
<span>{item.source}</span>
<div><i className="vp-mileage-mini-row-green" style={{ width: `${maxSourceMileage > 0 ? Math.max(4, (item.value / maxSourceMileage) * 100) : 0}%` }} /></div>
<strong>{formatKm(item.value)} km</strong>
</div>
)) : <Typography.Text type="tertiary"></Typography.Text>}
</div>
</div>
</div>
</div>
<aside className="vp-mileage-console-side">
<div className="vp-mileage-side-card">
<div className="vp-mileage-side-title"></div>
{mileageCustomerActions.map((item) => (
<Button key={item.label} disabled={item.disabled} onClick={item.action}>{item.label}</Button>
))}
</div>
<div className="vp-mileage-side-card">
<div className="vp-mileage-side-title"></div>
{peakMileage ? (
<>
<Typography.Text strong>{peakMileage.plate || peakMileage.vin}</Typography.Text>
<Typography.Text type="secondary">{peakMileage.date} / {peakMileage.source}</Typography.Text>
<div className="vp-monitor-metric-value">{formatKm(peakMileage.dailyMileageKm)} km</div>
<Space wrap>
<Button size="small" onClick={() => openMileageEvidence(peakMileage)}></Button>
<Button size="small" onClick={() => copyMileageEvidence(peakMileage)}></Button>
</Space>
</>
) : (
<Typography.Text type="tertiary"></Typography.Text>
)}
</div>
<div className="vp-mileage-side-card">
<div className="vp-mileage-side-title"></div>
<Typography.Text type="secondary">
</Typography.Text>
</div>
</aside>
</div>
</Card>
<div className="vp-kpi-grid" style={{ marginTop: 16 }}>
{[
{ 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) => (
<Card key={item.label} bordered loading={summaryLoading}>
<div className="vp-kpi-value">{item.value}</div>
<div className="vp-kpi-label">{item.label}</div>
</Card>
))}
</div>
<Card
bordered
loading={onlineLoading}
title={<Space><span>线</span><Button size="small" onClick={copyOnlineOperationsReport}>线</Button></Space>}
style={{ marginTop: 16 }}
>
<div className="vp-online-ops-board">
<div className="vp-online-ops-summary">
<Space wrap>
<Tag color={onlineRatePercent >= 90 ? 'green' : onlineRatePercent >= 60 ? 'orange' : 'red'}>
线 {formatPercent(onlineRatePercent)}
</Tag>
<Tag color={onlineSummary.redisOnlineKeys == null ? 'grey' : 'blue'}>
Redis {onlineSummary.redisOnlineKeys == null ? '-' : onlineSummary.redisOnlineKeys.toLocaleString()}
</Tag>
</Space>
<div className="vp-monitor-metric-value">{onlineVehicleCount.toLocaleString()} 线</div>
<Typography.Text type="secondary">
线线
</Typography.Text>
</div>
<div className="vp-online-ops-grid">
{[
{
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) => (
<div key={item.label} className="vp-online-ops-item">
<Tag color={item.color}>{item.label}</Tag>
<strong>{item.value}</strong>
<Typography.Text type="secondary">{item.detail}</Typography.Text>
</div>
))}
</div>
</div>
</Card>
<Card bordered title="统计闭合工作台" loading={summaryLoading || loading} style={{ marginTop: 16 }}>
<div className="vp-stat-closure-board">
{[
{ 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) => (
<div key={item.label} className="vp-stat-closure-item">
<Tag color={item.color}>{item.label}</Tag>
<div className="vp-monitor-metric-value">{item.value}</div>
<Typography.Text type="secondary">{item.detail}</Typography.Text>
</div>
))}
</div>
<div className="vp-stat-closure-action">
<Tag color={closureStatusColor}>{closureStatus}</Tag>
<Typography.Text type="secondary">
{closurePassed && anomalyRows.length === 0 ? '当前里程口径可直接用于 BI 里程口径;若存在跨通道差异,再进入车辆服务查看统计依据。' : '统计进入 BI 前需要结合异常记录、轨迹回放和历史明细完成复核。'}
</Typography.Text>
<Space wrap>
<Button size="small" onClick={copyStatisticsSummary}></Button>
<Button
size="small"
disabled={!currentVehicleKeyword || !onOpenHistory}
onClick={() => onOpenHistory?.({ keyword: currentVehicleKeyword, protocol: currentProtocol, dateFrom: filters.dateFrom ?? '', dateTo: filters.dateTo ?? '' })}
>
</Button>
<Button
size="small"
disabled={!currentVehicleKeyword || !onOpenRaw}
onClick={() => onOpenRaw?.({ keyword: currentVehicleKeyword, protocol: currentProtocol, dateFrom: filters.dateFrom ?? '', dateTo: filters.dateTo ?? '', includeFields: 'true' })}
>
</Button>
</Space>
</div>
</Card>
<Card bordered title="BI发布口径" style={{ marginTop: 16 }}>
<div className="vp-bi-publish-board">
<div className="vp-bi-publish-main">
<Tag color={confidenceColor}>{confidenceStatus}</Tag>
<div className="vp-monitor-metric-value">
{confidenceStatus === '可用于 BI' && closureStatus === '闭合通过' ? '可发布' : '需复核后发布'}
</div>
<Typography.Text type="secondary">
</Typography.Text>
</div>
<div className="vp-bi-publish-grid">
<div><span></span><strong>{closureStatus}</strong></div>
<div><span></span><strong>{formatPercent(pageCoverageRate)}</strong></div>
<div><span></span><strong>{anomalyRows.length.toLocaleString()}</strong></div>
<div><span></span><strong>{sourceConsistencyText}</strong></div>
</div>
<Space wrap>
<Button size="small" onClick={copyBIPublishText}>BI发布口径</Button>
<Button size="small" onClick={copyStatisticsSummary}></Button>
</Space>
</div>
</Card>
<Card bordered title="统计发布审计" style={{ marginTop: 16 }}>
<div className="vp-stat-audit-board">
<div className="vp-stat-audit-summary">
<Tag color={publishAuditColor}>{publishAuditConclusion}</Tag>
<div className="vp-monitor-metric-value">
{publishAuditItems.length - publishReviewCount - publishBlockedCount} / {publishReviewCount} / {publishBlockedCount}
</div>
<Typography.Text type="secondary">
线
</Typography.Text>
<Space wrap>
<Button size="small" onClick={copyPublishAudit}></Button>
<Button size="small" disabled={!currentVehicleKeyword} onClick={() => onOpenVehicle(currentVehicleKeyword, currentProtocol)}></Button>
</Space>
</div>
<div className="vp-stat-audit-grid">
{publishAuditItems.map((item) => (
<div key={item.key} className="vp-stat-audit-item">
<Space wrap>
<Tag color={auditStatusColor(item.status)}>{auditStatusLabel(item.status)}</Tag>
<Tag color="blue">{item.label}</Tag>
</Space>
<strong>{item.value}</strong>
<Typography.Text type="secondary">{item.detail}</Typography.Text>
<div className="vp-stat-audit-action">{item.action}</div>
</div>
))}
</div>
</div>
</Card>
<div className="vp-stat-workspace">
<Card bordered title="区间趋势">
<div className="vp-stat-chart">
{dateSeries.length > 0 ? dateSeries.map((item) => (
<div key={item.date} className="vp-stat-bar-row">
<span className="vp-stat-bar-label">{item.date}</span>
<div className="vp-stat-bar-track">
<span className="vp-stat-bar-fill" style={{ width: `${maxDateMileage > 0 ? Math.max(4, (item.value / maxDateMileage) * 100) : 0}%` }} />
</div>
<span className="vp-stat-bar-value">{formatKm(item.value)} km</span>
</div>
)) : (
<Typography.Text type="secondary"></Typography.Text>
)}
</div>
</Card>
<Card bordered title="来源贡献">
<div className="vp-stat-chart">
{sourceSeries.length > 0 ? sourceSeries.map((item) => (
<div key={item.source} className="vp-stat-bar-row">
<span className="vp-stat-bar-label">{item.source}</span>
<div className="vp-stat-bar-track">
<span className="vp-stat-bar-fill vp-stat-bar-fill-green" style={{ width: `${maxSourceMileage > 0 ? Math.max(4, (item.value / maxSourceMileage) * 100) : 0}%` }} />
</div>
<span className="vp-stat-bar-value">{formatKm(item.value)} km</span>
</div>
)) : (
<Typography.Text type="secondary"></Typography.Text>
)}
</div>
</Card>
</div>
<div className="vp-stat-insight-grid">
{[
{ 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) => (
<Card key={item.label} bordered>
<Tag color={item.color}>{item.label}</Tag>
<div className="vp-monitor-metric-value">{item.value}</div>
<Typography.Text type="secondary">{item.detail}</Typography.Text>
</Card>
))}
</div>
<Card bordered title="在线状态明细" style={{ marginTop: 16 }}>
<div className="vp-table-toolbar">
<Space wrap>
<Tag color="green">线 {onlineVehicleCount.toLocaleString()} </Tag>
<Tag color="orange">线 {offlineVehicleCount.toLocaleString()} </Tag>
<Typography.Text type="secondary"> VIN线</Typography.Text>
<Button size="small" onClick={copyOnlineStatusHandoff}>线</Button>
</Space>
</div>
<Table
loading={onlineLoading}
rowKey={(row?: OnlineVehicleStatusRow) => 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) => <Tag color={row.online ? 'green' : 'orange'}>{row.online ? '在线' : '离线'}</Tag>
},
{ 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) => (
<Space spacing={4} wrap>
{(row.protocols ?? []).map((protocol) => <Tag key={protocol} color="blue">{protocol}</Tag>)}
</Space>
)
},
{ title: '绑定状态', dataIndex: 'bindingStatus', width: 130 },
{
title: '操作',
width: 120,
render: (_: unknown, row: OnlineVehicleStatusRow) => (
<Button disabled={!canOpenVehicle(row.vin)} onClick={() => onOpenVehicle(row.vin, currentProtocol)}></Button>
)
}
]}
/>
</Card>
<Card bordered title="里程口径" style={{ marginTop: 16 }}>
<div className="vp-table-toolbar">
<Space wrap>
<Tag color="blue"></Tag>
<Button size="small" onClick={copyStatisticsSummary}></Button>
</Space>
</div>
<div className="vp-stat-definition-grid">
<div>
<Tag color="blue"></Tag>
<p></p>
</div>
<div>
<Tag color="green"></Tag>
<p> BI </p>
</div>
<div>
<Tag color="orange"></Tag>
<p>退</p>
</div>
</div>
</Card>
<Card bordered style={{ marginTop: 16 }}>
<div className="vp-table-toolbar">
<Space wrap>
<Tag color="blue"> {rows.length.toLocaleString()} </Tag>
<Button size="small" onClick={exportMileage}> CSV</Button>
</Space>
</div>
<Table
loading={loading}
rowKey={(row?: DailyMileageRow) => `${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 ? <Tag color="orange">{row.anomalySeverity}</Tag> : <Tag color="green"></Tag> },
{
title: '处置建议',
width: 260,
render: (_: unknown, row: DailyMileageRow) => {
const action = mileageActionRecommendation(row);
return (
<div>
<Tag color={action.color}>{action.label}</Tag>
<div style={{ marginTop: 6 }}>{action.detail}</div>
</div>
);
}
},
{
title: '操作',
width: 360,
render: (_: unknown, row: DailyMileageRow) => (
<Space spacing={4} wrap>
<Button disabled={!canOpenVehicle(row.vin)} onClick={() => copyMileageEvidence(row)}></Button>
<Button disabled={!canOpenVehicle(row.vin) || !onOpenHistory} onClick={() => openMileageEvidence(row)}></Button>
<Button disabled={!canOpenVehicle(row.vin) || !onOpenRaw} onClick={() => openMileageRawEvidence(row)}></Button>
<Button disabled={!canOpenVehicle(row.vin)} onClick={() => onOpenVehicle(row.vin, row.source)}></Button>
</Space>
)
}
]}
/>
</Card>
</div>
);
}