142 lines
5.4 KiB
TypeScript
142 lines
5.4 KiB
TypeScript
import type { DailyMileageRow } from '../../api/types';
|
|
import { downloadBlob } from './download';
|
|
|
|
export type MileageExportVehicle = { vin: string; plate: string; brandName?: string; modelName?: string };
|
|
export type MileageExportSource = { protocol: string; label: string; mileageType: string };
|
|
export type MileageWorkbookRow = Pick<DailyMileageRow, 'vin' | 'date' | 'dailyMileageKm'> & Partial<Omit<DailyMileageRow, 'vin' | 'date' | 'dailyMileageKm'>>;
|
|
|
|
export type MileageExportInput = {
|
|
metricView?: 'mileage' | 'hydrogen';
|
|
dateFrom: string;
|
|
dateTo: string;
|
|
dates: string[];
|
|
vehicles: MileageExportVehicle[];
|
|
mileageRows: MileageWorkbookRow[];
|
|
sources: MileageExportSource[];
|
|
exportedAt?: Date;
|
|
};
|
|
|
|
type MileageWorkerResponse = { ok: true; buffer: ArrayBuffer } | { ok: false; message: string };
|
|
type MileageWorkerRequest =
|
|
| { type: 'start'; input: Omit<MileageExportInput, 'vehicles' | 'mileageRows'> }
|
|
| { type: 'rows'; rows: MileageWorkbookRow[] }
|
|
| { type: 'finish'; vehicles: MileageExportVehicle[] };
|
|
|
|
function abortError() {
|
|
return new DOMException('里程导出已取消', 'AbortError');
|
|
}
|
|
|
|
function throwIfAborted(signal?: AbortSignal) {
|
|
if (signal?.aborted) throw abortError();
|
|
}
|
|
|
|
export type MileageExportStream = {
|
|
appendRows: (rows: readonly MileageWorkbookRow[]) => void;
|
|
finish: (vehicles: MileageExportVehicle[]) => Promise<void>;
|
|
dispose: () => void;
|
|
};
|
|
|
|
export function createMileageExportStream(input: Omit<MileageExportInput, 'vehicles' | 'mileageRows'>, signal?: AbortSignal): MileageExportStream {
|
|
throwIfAborted(signal);
|
|
if (typeof Worker === 'undefined') {
|
|
throw new Error('当前浏览器不支持后台 Excel 导出,请升级到最新版 Chrome、Edge 或 Safari 后重试');
|
|
}
|
|
|
|
const worker = new Worker(new URL('./mileageExport.worker.ts', import.meta.url), { type: 'module' });
|
|
let disposed = false;
|
|
let finishing = false;
|
|
let resolveFinish: (() => void) | undefined;
|
|
let rejectFinish: ((reason?: unknown) => void) | undefined;
|
|
let earlyError: Error | undefined;
|
|
const dispose = () => {
|
|
if (disposed) return;
|
|
disposed = true;
|
|
signal?.removeEventListener('abort', cancel);
|
|
worker.terminate();
|
|
};
|
|
const reject = (error: Error) => {
|
|
if (disposed) return;
|
|
if (rejectFinish) rejectFinish(error);
|
|
else earlyError = error;
|
|
dispose();
|
|
};
|
|
const cancel = () => reject(abortError());
|
|
worker.onmessage = (event: MessageEvent<MileageWorkerResponse>) => {
|
|
if (disposed) return;
|
|
if (!event.data.ok) {
|
|
reject(new Error(event.data.message));
|
|
return;
|
|
}
|
|
const buffer = event.data.buffer;
|
|
const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
|
downloadBlob(blob, `车辆${input.metricView === 'hydrogen' ? '氢耗明细' : '里程查询'}_${input.dateFrom.split('-').join('')}-${input.dateTo.split('-').join('')}_${finishVehicles.length}辆.xlsx`);
|
|
resolveFinish?.();
|
|
dispose();
|
|
};
|
|
worker.onerror = (event) => reject(new Error(event.message || 'Excel 工作线程执行失败'));
|
|
signal?.addEventListener('abort', cancel, { once: true });
|
|
let finishVehicles: MileageExportVehicle[] = [];
|
|
const post = (message: MileageWorkerRequest) => worker.postMessage(message);
|
|
post({ type: 'start', input });
|
|
|
|
return {
|
|
appendRows(rows) {
|
|
if (disposed || finishing) return;
|
|
post({
|
|
type: 'rows',
|
|
rows: rows.map((row) => ({
|
|
province: row.province, city: row.city, region: row.region, locationTime: row.locationTime, locationStatus: row.locationStatus,
|
|
vin: row.vin,
|
|
date: row.date,
|
|
dailyMileageKm: row.dailyMileageKm,
|
|
pureHydrogenMileageKm: row.pureHydrogenMileageKm,
|
|
hydrogenConsumptionKg: row.hydrogenConsumptionKg,
|
|
hydrogenPhysicalConsumptionKg: row.hydrogenPhysicalConsumptionKg,
|
|
hydrogenConsumptionKgPer100Km: row.hydrogenConsumptionKgPer100Km,
|
|
hydrogenSocBalancedKg: row.hydrogenSocBalancedKg,
|
|
pureElectricMileageKm: row.pureElectricMileageKm,
|
|
mixedMileageKm: row.mixedMileageKm,
|
|
batterySocDeltaPct: row.batterySocDeltaPct,
|
|
chargeCount: row.chargeCount,
|
|
chargeEnergyKWh: row.chargeEnergyKWh,
|
|
refuelCount: row.refuelCount,
|
|
refuelAmountKg: row.refuelAmountKg,
|
|
hydrogenQualityStatus: row.hydrogenQualityStatus,
|
|
hydrogenQualityReason: row.hydrogenQualityReason,
|
|
source: row.source
|
|
}))
|
|
});
|
|
},
|
|
finish(vehicles) {
|
|
if (disposed) return Promise.reject(earlyError ?? abortError());
|
|
if (finishing) return Promise.reject(new Error('里程导出已经进入生成阶段'));
|
|
finishing = true;
|
|
finishVehicles = vehicles;
|
|
return new Promise<void>((resolve, rejectPromise) => {
|
|
resolveFinish = resolve;
|
|
rejectFinish = rejectPromise;
|
|
if (earlyError) {
|
|
rejectPromise(earlyError);
|
|
dispose();
|
|
return;
|
|
}
|
|
post({ type: 'finish', vehicles });
|
|
});
|
|
},
|
|
dispose
|
|
};
|
|
}
|
|
|
|
export async function downloadMileageWorkbook(input: MileageExportInput, signal?: AbortSignal) {
|
|
const stream = createMileageExportStream({
|
|
metricView: input.metricView,
|
|
dateFrom: input.dateFrom,
|
|
dateTo: input.dateTo,
|
|
dates: input.dates,
|
|
sources: input.sources,
|
|
exportedAt: input.exportedAt
|
|
}, signal);
|
|
stream.appendRows(input.mileageRows);
|
|
await stream.finish(input.vehicles);
|
|
}
|