feat(platform): add customer mileage review console

This commit is contained in:
lingniu
2026-07-05 01:24:56 +08:00
parent 11710ec6fa
commit 72a4761bdb
3 changed files with 310 additions and 7 deletions

View File

@@ -616,15 +616,70 @@ export function Mileage({
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 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 loadSummary = (values: Record<string, string> = filters) => {
setSummaryLoading(true);
api.mileageSummary(mileageParams(values))
.then(setSummary)
.then((nextSummary) => setSummary(nextSummary ?? emptySummary))
.catch((error: Error) => Toast.error(error.message))
.finally(() => setSummaryLoading(false));
api.onlineStatisticsSummary(mileageParams(values))
.then(setOnlineSummary)
.then((nextSummary) => setOnlineSummary({
...emptyOnlineSummary,
...(nextSummary ?? {}),
protocolStats: Array.isArray(nextSummary?.protocolStats) ? nextSummary.protocolStats : []
}))
.catch((error: Error) => Toast.error(error.message));
};
@@ -922,6 +977,96 @@ export function Mileage({
</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() },