feat(platform): add customer mileage reconciliation command

This commit is contained in:
lingniu
2026-07-06 05:14:11 +08:00
parent 378706cc5c
commit ce3a6225ec
2 changed files with 168 additions and 0 deletions

View File

@@ -324,6 +324,58 @@ function mileageCustomerDecisionText({
].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,
@@ -1326,6 +1378,27 @@ export function Mileage({
'客户里程决策说明'
);
};
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 }))
@@ -1461,6 +1534,50 @@ export function Mileage({
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: '区间里程',
@@ -1735,6 +1852,43 @@ export function Mileage({
</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-conclusion-strip" aria-label="客户里程结论条">
<div className="vp-mileage-conclusion-copy">
<Space wrap>