import excel from 'exceljs'; class Worksheet { readonly _ws: excel.Worksheet; readonly workbook: Workbook; constructor(ws: excel.Worksheet, wb: Workbook) { this._ws = ws; this.workbook = wb; } async readJson(data: Record[], options: { headers?: Header[] } = {}) { const { headers = data.length > 0 ? Object.keys(data[0]).map((k) => ({ label: k, prop: k }) as Header) : [], } = options; const rows = await Promise.all( data.map(async (item, i) => { const record: Record = {}; const cols = headers.filter((h) => h.ignore?.out !== true); for (let j = 0; j < cols.length; j++) { const header = cols[j]; if (header.ignore?.out) { continue; } const value = getAttribute(item, header.prop); if (header.formatOutputValue) { record[header.label] = await header.formatOutputValue(value, i); } else if (['string', 'number', 'bigint', 'boolean'].includes(typeof value)) { record[header.label] = value; } else { record[header.label] = JSON.stringify(value); } } return record; }), ); this._ws.columns = headers.map((e) => { return { header: e.label, key: e.label }; }); this._ws.addRows(rows); this._ws.autoFilter = { from: { row: 1, column: 1, }, to: { row: rows.length + 1, column: headers.length, }, }; } async toJson(options: { headers?: Header[] } = {}) { const { headers } = options; let jsonData: Record[] = []; this._ws.eachRow((row) => { const rowData: Record = {}; row.eachCell((cell, colNumber) => { const header = this._ws.getRow(1).getCell(colNumber).value?.toString()!; rowData[header] = cell.value; }); jsonData.push(rowData); }); jsonData = jsonData.slice(1); // Remove Headers if (headers) { jsonData = await Promise.all( jsonData.map(async (item, i) => { const mappedItem: Record = {}; for (const header of headers) { if (header.ignore?.in) { continue; } const value = header.parseImportValue ? await header.parseImportValue(item[header.label], i) : item[header.label]; setAttribute(mappedItem, header.prop, value); } return mappedItem; }), ); } return jsonData as T[]; } async addImage(img: { data: ArrayBuffer; ext: 'jpeg' | 'png' | 'gif' }) { const imgId = this.workbook._wb.addImage({ buffer: img.data, extension: img.ext, }); return imgId; } } class Workbook { _wb: excel.Workbook; constructor(wb: excel.Workbook) { this._wb = wb; } get sheetCount() { return this._wb.worksheets.length; } static createWorkbook() { return new Workbook(new excel.Workbook()); } async loadArrayBuffer(bf: ArrayBuffer) { this._wb = await this._wb.xlsx.load(bf); } getSheet(index: number): Worksheet | undefined { const ws = this._wb.getWorksheet(index + 1); // Align the index return ws ? new Worksheet(ws, this) : undefined; } addSheet(name?: string) { const ws = this._wb.addWorksheet(name); return new Worksheet(ws, this); } async exportFile(fileName: string) { const bf = await this._wb.xlsx.writeBuffer(); const blob = new Blob([bf], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = fileName; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); } } function getAttribute( obj: Record, path: string, ): T | undefined { const keys = path.split('.'); let result: unknown = obj; for (const key of keys) { if (result && typeof result === 'object' && key in result) { result = (result as Record)[key]; } else { return undefined; } } return result as T; } function setAttribute(obj: Record, path: string, value: unknown): void { const keys = path.split('.'); let current: Record = obj; for (const key of keys.slice(0, keys.length - 1)) { if (!current[key] || typeof current[key] !== 'object') { current[key] = {}; } current = current[key] as Record; } const finalKey = keys[keys.length - 1]; current[finalKey] = value; } export type Header = { label: string; prop: string; parseImportValue?: (val: any, index: number) => any; formatOutputValue?: (val: any, index: number) => any; ignore?: { in?: boolean; out?: boolean; }; }; export type ExportBaseOptions = { fileName?: string; sheetName?: string; headers?: Header[]; }; export type ImportBaseOptions = { headers?: Header[]; }; /** * 导出为XLSX * @param data 数据数组 * @param options 导出选项 */ export async function exportToXLSX( data: Record[], options: ExportBaseOptions = {}, ) { const { headers, sheetName, fileName = `export_${new Date().toISOString().slice(0, 10)}.xlsx`, } = options; const workbook = Workbook.createWorkbook(); const worksheet = workbook.addSheet(sheetName); await worksheet.readJson(data, { headers }); await workbook.exportFile(fileName); } /** * 从XLSX文件导入数据 * @param file XLSX文件对象 * @param options 导入选项 * @returns 导入的数据数组 */ export async function importFromXLSX>( file: File, options?: ImportBaseOptions, ): Promise; export async function importFromXLSX(file: File, options: { asWorkBook: true }): Promise; export async function importFromXLSX>( file: File, options: ImportBaseOptions & { asWorkBook?: boolean } = {}, ) { const { headers, asWorkBook } = options; return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (event) => { try { const wb = Workbook.createWorkbook(); wb.loadArrayBuffer(event.target?.result as ArrayBuffer).then(() => { if (asWorkBook) { resolve(wb); } else { const ws = wb.getSheet(0)!; // 默认读取第一个工作表 resolve(ws.toJson({ headers })); } }); } catch (error) { reject(error); } }; reader.onerror = (error) => { reject(error); }; reader.readAsArrayBuffer(file); }); } /** * 创建Excel文件对象 */ export function createWorkbook() { return Workbook.createWorkbook(); }