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 & Partial>; 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 } | { 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; dispose: () => void; }; export function createMileageExportStream(input: Omit, 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) => { 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((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); }