feat(platform): enhance mileage statistics workspace

This commit is contained in:
lingniu
2026-07-04 10:56:10 +08:00
parent adb9d146d9
commit 312382c95d
3 changed files with 254 additions and 0 deletions

View File

@@ -27,6 +27,10 @@ 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 canOpenVehicle(vin?: string) {
const value = vin?.trim();
return Boolean(value && value !== 'unknown');
@@ -55,6 +59,33 @@ function mergeInitialFilters(initialVin: string, initialProtocol?: string, initi
};
}
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);
}
export function Mileage({
initialVin,
initialProtocol,
@@ -76,6 +107,13 @@ export function Mileage({
const [pagination, setPagination] = useState({ currentPage: 1, pageSize: 20, 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 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}` : '',
@@ -181,6 +219,68 @@ export function Mileage({
</Card>
))}
</div>
<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: `${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: summary.recordCount > 0 ? formatPercent((rows.length / summary.recordCount) * 100) : '0%', color: 'grey' as const, detail: '当前分页记录数 / 全量统计记录数。' }
].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-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>退 RAW </p>
</div>
</div>
</Card>
<Card bordered style={{ marginTop: 16 }}>
<Table
loading={loading}