perf(mileage): stream workbook export rows

This commit is contained in:
lingniu
2026-07-16 09:08:04 +08:00
parent b78cf1696f
commit 61c1bbc365
8 changed files with 191 additions and 62 deletions

View File

@@ -4,7 +4,7 @@ import { FormEvent, useEffect, useMemo, useRef, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { api } from '../../api/client';
import type { DailyMileageRow, MileageStatistics, Page, VehicleRow } from '../../api/types';
import { downloadMileageWorkbook } from '../domain/mileageExport';
import { createMileageExportStream, type MileageExportStream } from '../domain/mileageExport';
import { formatZhNumber } from '../domain/formatters';
import { InlineError } from '../shared/AsyncState';
import { QUERY_MEMORY, retainPreviousPageWithinScope } from '../queryPolicy';
@@ -347,6 +347,7 @@ export default function StatisticsPage() {
};
setIsExporting(true);
setExportFeedback('');
let exportStream: MileageExportStream | undefined;
try {
let vehicles: VehicleOption[] = criteria.vehicles.map((vehicle) => ({ ...vehicle }));
if (!vehicles.length) {
@@ -362,7 +363,14 @@ export default function StatisticsPage() {
if (offset >= result.total) break;
}
}
const mileageRows: DailyMileageRow[] = [];
const plateByVin = new Map(vehicles.filter((vehicle) => vehicle.plate).map((vehicle) => [vehicle.vin, vehicle.plate]));
exportStream = createMileageExportStream({
dateFrom: criteria.dateFrom,
dateTo: criteria.dateTo,
dates,
sources: criteria.sources.filter((source) => source.enabled),
exportedAt: new Date()
}, controller.signal);
const vehicleBatches = criteria.vehicles.length
? Array.from({ length: Math.ceil(vehicles.length / EXPORT_VIN_BATCH_SIZE) }, (_, index) => vehicles.slice(index * EXPORT_VIN_BATCH_SIZE, (index + 1) * EXPORT_VIN_BATCH_SIZE))
: [[] as VehicleOption[]];
@@ -375,24 +383,17 @@ export default function StatisticsPage() {
while (true) {
const params = mileageParams({ ...criteria, vehicles: vehicleBatch }, offset);
const result = await api.dailyMileage(params, controller.signal);
mileageRows.push(...result.items);
for (const row of result.items) if (row.plate) plateByVin.set(row.vin, row.plate);
exportStream.appendRows(result.items);
offset += result.items.length;
reportProgress({ label: mileageLabel, completed: offset, total: result.total });
if (!result.items.length || offset >= result.total) break;
}
}
const plateByVin = new Map(mileageRows.filter((row) => row.plate).map((row) => [row.vin, row.plate]));
vehicles = vehicles.map((vehicle) => ({ ...vehicle, plate: vehicle.plate || plateByVin.get(vehicle.vin) || '' }));
reportProgress({ label: '正在后台生成 Excel 文件' });
await downloadMileageWorkbook({
dateFrom: criteria.dateFrom,
dateTo: criteria.dateTo,
dates,
vehicles,
mileageRows,
sources: criteria.sources.filter((source) => source.enabled),
exportedAt: new Date()
}, controller.signal);
await exportStream.finish(vehicles);
exportStream = undefined;
if (mountedRef.current) setExportFeedback(`已导出 ${vehicles.length} 辆车`);
} catch (error) {
if (mountedRef.current) {
@@ -401,6 +402,7 @@ export default function StatisticsPage() {
: error instanceof Error ? `导出失败:${error.message}` : '导出失败,请稍后重试');
}
} finally {
exportStream?.dispose();
if (exportControllerRef.current === controller) exportControllerRef.current = null;
if (mountedRef.current) {
setIsExporting(false);