公司项目需要导出表格为报表,尝试了xlsx、sheetjs以及js-xlsx各种库,但是总有不满足需求的点(比如设置行高列宽、居中、合并单元格、添加背景色等等)或者API太少写起来太繁琐,最后找到ExcelJS能够实现所有需求。
本人简书链接:https://www.jianshu.com/p/0c688c97719e
(csdn抄袭狗是真的恶心https://blog.csdn.net/weixin_50543490/article/details/131207514)
ExcelJS:读取,操作并写入电子表格数据和样式到 XLSX 和 JSON 文件。一个 Excel 电子表格文件逆向工程项目。
github中文文档:https://github.com/exceljs/exceljs/blob/master/README_zh.md
我把用到的exceljs的导入导出功能按照需求封装成一个类,具体可以根据自己的项目需求去调整。
- 一、定义Excel类
exportclassExcel{ blob?: Blob;// 导入的blob文件 worksheet?: ExcelJS.Worksheet;// 当前工作表 header:string[];// 表头字段数组constructor(blob?: Blob){this.blob = blob;this.worksheet =undefined;this.header =[];}}
- 二、导入excel方法
/** * @description: blob转ArrayBuffer(用于后续生成文件数据) * @return {Promise<ArrayBuffer>} ArrayBuffer */privatereadFile():Promise<ArrayBuffer>{returnnewPromise<ArrayBuffer>((resolve, reject)=>{let reader =newFileReader();if(!this.blob){reject('上传文件异常!');}else{ reader.readAsArrayBuffer(this.blob); reader.onload=(ev)=>{resolve(ev.target!.result as ArrayBuffer);};}});}/** * @description: 导入excel文件获取workbook(workbook属性方法参考exceljs文档) * @return {Promise<ExcelJS.Workbook>} */publicasyncgetWorkBook():Promise<ExcelJS.Workbook>{let buffer =awaitthis.readFile();const workbook =newExcelJS.Workbook();returnawait workbook.xlsx.load(buffer);}/** * @description: 将 excel 第i张工作表的数据转为对象数据 * @param {number} i 工作表序号 * @param {string[]} header 表头字段数组 * @return {Promise<Record<(typeof header)[number], string>[]>} * 传入表头作为字段的对象数组(每个元素对象对应每一行) */publicasyncimportExcel(options: importExcelType):Promise<Record<(typeof header)[number],string>[]>{const{ i =1, header }= options;const workbook =awaitthis.getWorkBook();const worksheet = workbook.getWorksheet(i);// excel导入后返回的数组const excelList: Record<(typeof header)[number],string>[]=[]; worksheet.getSheetValues().filter(temp =>!!temp?.length).forEach(item =>{// 移除空行// 移除每行首个空元素(item asstring[]).shift();// 定义临时对象存储每一行内容let tempObj: Record<(typeof header)[number],string>={};(item asstring[]).forEach((item2, index2)=>{ tempObj[header[index2]]= item2;}) excelList.push(tempObj);})return excelList }
- 三、导出excel方法
/** * @description: 导出excel,参数信息参考exceljs * @param {*} data 数据 * @param {*} name 文件名 * @param {*} header 表头字段 * @param {*} customHeader 表头字段对应中文 * @param {*} sheetName 工作表名 * @param {*} title 标题 * @param {*} subTitle 副标题(日期) * @param {*} password 冻结表格密码 * @param {*} mergeList 合并单元格数组 * @param {*} titleStyle 标题样式(按需补充方法) * @param {*} subTitleStyle 小标题样式(按需补充方法) * @param {*} headerStyle 表头字段样式(按需补充方法) * @param {*} cellStyle 单元格样式(按需补充方法) * @return {*} */publicasyncexportExcel(options: exportExcelType):Promise<void>{const{ data, name, header, customHeader, sheetName ='sheet1', title ='', subTitle ='', password ='', mergeList =[], titleStyle, subTitleStyle, headerStyle, cellStyle }= options;// 创建工作簿const workbook =newExcelJS.Workbook(); workbook.creator ='侃侃'; workbook.created =newDate();// 添加sheetthis.worksheet = workbook.addWorksheet(sheetName,{ properties:{ tabColor:{ argb:'FF00FF00'}}});this.header = header;// 表头行序号let headerRowId =1;if(!!title) headerRowId++;if(!!subTitle) headerRowId++;// 插入单元格数据this.setCells(data, customHeader, cellStyle);// 插入大标题this.getTitle(title, titleStyle);// 插入小标题this.getSubTitle(subTitle, subTitleStyle);// 处理表头this.setHeaderStyle(headerRowId, data, headerStyle);// 更多处理this.handleDealExcel(password, mergeList, headerRowId);// 导出excel(此处也可用file-saver将blob导出到excel文件 fs.saveAs(blob, name+'.xlsx');) workbook.xlsx.writeBuffer().then((data)=>{let blob =newBlob([data],{ type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'});const a = document.createElement('a'); a.href =URL.createObjectURL(blob); a.download = name +'.xlsx'; document.body.appendChild(a); a.click(); document.body.removeChild(a); window.URL.revokeObjectURL(a.href);});}
- 四、导出数据及格式方法汇总(按需补充)- 1.插入单元格数据
/** * @description:单元格数据处理 * @param {any} data 表格数据 * @param {string} customHeader 表头中文字段 * @return {*} */privatesetCells(data: exportExcelType['data'], customHeader:string[], style?: Partial<ExcelJS.Row & ExcelJS.Column>):void{// 设置列,插入中文表头const column: Partial<ExcelJS.Column>[]=[];this.header.forEach((item, index)=>{ column.push({ header: customHeader[index], key: item, width: style?.width ||25,})})this.worksheet!.columns = column;// 设置行,添加数据this.worksheet?.addRows(data);// 设置行高this.worksheet?.eachRow({ includeEmpty:true},(row, rowNumber)=>{ row.height = style?.height ||20;})// 获取每一列数据,再依次对齐this.worksheet!.columns.forEach(column => column.alignment = style?.alignment ||{ vertical:'middle', horizontal:'center', wrapText:true})}
- 2.添加大标题/** * @description: 添加大标题 * @param {string} title 标题 * @param {any} style 标题样式 * @return {*} */privategetTitle(title:string, style?: Partial<ExcelJS.Row>):void{if(!!title){// 插入标题this.worksheet?.spliceRows(1,0,[title]);this.worksheet?.mergeCells(1,1,1,this.header.length);// 调整标题样式const titleRow =this.worksheet!.getRow(1);// 高度 titleRow.height = style?.height ||40;// 字体设置 titleRow.font = style?.font ||{ size:20, bold:true}// 背景色 titleRow.fill = style?.fill ||{ bgColor:{'argb':'FFFFFF00'}, type:'pattern', pattern:'none'}// 对齐方式 titleRow.alignment = style?.alignment ||{ horizontal:"center", vertical:"middle"};}}
- 3.添加小标题/** * @description: 添加小标题 * @param {string} subTitle 标题 * @param {Partial} style 小标题样式 * @return {*} */privategetSubTitle(subTitle:string, style?: Partial<ExcelJS.Row>):void{if(!!subTitle){this.worksheet?.spliceRows(2,0,[subTitle]);this.worksheet?.mergeCells(2,1,2,this.header.length);// 调整标题样式const subtitleRow =this.worksheet!.getRow(2);// 高度 subtitleRow.height = style?.height ||20;// 字体设置 subtitleRow.font = style?.font ||{ size:14,}// 背景色 subtitleRow.fill = style?.fill ||{ bgColor:{'argb':'FFFFFF00'}, type:'pattern', pattern:'none'}// 对齐方式 subtitleRow.alignment = style?.alignment ||{ horizontal:"right", vertical:"middle"};}}
- 4.设置表头样式/** * @description: 设置表头样式 * @param {number} num 表头在第几行 * @param {number} data 总数据 * @param {number} style 表头样式 * @return {*} */privatesetHeaderStyle(num:number, data:any, style?: Partial<ExcelJS.Row>):void{// 自动筛选器this.worksheet!.autoFilter ={ from:{ row: num, column:1}, to:{ row: data.length, column:this.header.length }}// 给表头添加背景色let headerRow =this.worksheet!.getRow(num); headerRow!.height = style?.height ||30;// 通过 cell 设置背景色,更精准 headerRow?.eachCell((cell)=>{ cell.fill = style?.fill ||{ type:'pattern', pattern:'solid', fgColor:{ argb:'dde0e7'},} cell.font = style?.font ||{ size:12}})}
- 1. 其他处理/** * @description: 其他内容处理 * @param {string} password 密码 * @param {mergeListType} mergeList 合并行列数组 * @param {number} headerRowId 表头行序号 * @return {*} */privatehandleDealExcel(password:string, mergeList: mergeListType[], headerRowId:number){// 添加工作表保护if(!!password){this.worksheet?.protect(password,{ autoFilter:true, selectLockedCells:false,});}// 合并单元格 mergeList.forEach(item =>{// 行数为表格数据所在行行数+表头行序号headerRowIdconst startRow = item.startRow + headerRowId;const endRow = item.endRow + headerRowId;this.worksheet?.mergeCells(startRow, item.startColumn, endRow, item.endColumn);})// 冻结前几行this.worksheet!.views =[{ state:'frozen', xSplit:0, ySplit: headerRowId }];}
- 6.合并单元格结构处理方法/** * @description: 合并单元格数组 * @param {Partial} merge1 需要合并的行/列起止对象数组1 * 例:[{ startRow: 1, endRow: 1},{ startRow: 10, endRow: 10}] * @param {Partial} merge2 需要合并的列/行起止对象数组2 * 例:[{ startColumn: 1, endColumn: 2 }] * @return {*} mergeArr 合并后数组 * 例:[{ startRow: 1, endRow: 1, startColumn: 1, endColumn: 2},{ startRow: 10, endRow: 10, startColumn: 1, endColumn: 2}] */publicmerge(merge1: Partial<mergeListType>[], merge2: Partial<mergeListType>[]): mergeListType[]{const mergeArr:any[]=[]; merge1.forEach(item1 =>{ mergeArr.push(...merge2.map(item2 =>{return{...item2,...item1 };}))})return mergeArr;}
- 五、具体使用:- 1.导入excel
// import.vue<template><div class="import"><el-upload ref="uploadExcelRef" action="" drag :auto-upload="false":on-change="uploadChange":limit="1"><i class="el-icon-upload"></i><div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div></el-upload></div></template><script setup lang="ts">import{ Excel }from'@/utils/excel';import{ ElMessage }from'element-plus';import{ ref }from'vue';// 上传组件refconst uploadExcelRef =ref();constuploadChange=async(file:any)=>{// 表头字段数组const header =['id','name','age'];if(file.raw?.type !=='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'&& file.raw?.type !=='application/vnd.ms-excel'){ ElMessage.error('文件格式错误,请重新上传!')// 移除上传的文件 uploadExcelRef.value!.handleRemove(file);returnfalse}// 对excel文件进行处理try{const excel =newExcel(file.raw);// 导入文件获取数据const res =await excel.importExcel({ header });console.log(res);}catch(err){console.log(err);}}</script>
excel文件- 最终导入生成数据:[{ id:"id", name:"姓名", age:"年龄"},{ id:1, name:"张三", age:18},{ id:2, name:"李四", age:20},{ id:3, name:"王五", age:24},{ id:4, name:"赵六", age:26},{ id:5, name:"刘七", age:28}]
- 2.导出excel// export.vue<template><div class="export"><button @click="exportExcel">导出</button></div></template><script setup lang="ts">import{ Excel,typemergeListType}from'@/utils/excel';import{ reactive }from'vue';const tableData =reactive([{ id:1, name:"张三", age:18},{ id:2, name:"张三", age:18},{ id:3, name:"王五", age:24},{ id:4, name:"赵六", age:24},{ id:5, name:"刘七", age:24}])const header =['id','name','age'];const customHeader =['id','姓名','年龄'];const mergeList: mergeListType[]=reactive([{ startRow:1, endRow:2, startColumn:3, endColumn:3},{ startRow:3, endRow:5, startColumn:3, endColumn:3}])constexportExcel=()=>{const excel =newExcel(); excel.exportExcel({ name:'个人信息', title:'标题', data: tableData, header, customHeader, mergeList })}</script>
最终生成的excel: - 六、excel.ts完整代码
import ExcelJS from'exceljs'// 导入参数数据类型exportinterfaceimportExcelType{/** * 第 i 张工作表 */ i?:number;/** * 表格表头字段数组 */ header:readonlyany[];}// 导出参数数据类型exportinterfaceexportExcelType{/** * 数据 */ data:{[key:string]:any}[];/** * 文件名 */ name:string;/** * 表头字段 */ header:string[];/** * 表头字段对应中文 */ customHeader:string[];/** * 工作表名 */ sheetName?:string;/** * 标题 */ title?:string;/** * 小标题 */ subTitle?:string;/** * 工作表保护密码 */ password?:string;/** * 对齐方式 */ alignment?: Partial<ExcelJS.Alignment>;/** * 合并单元格 */ mergeList?: mergeListType[];/** * 标题样式 */ titleStyle?: Partial<ExcelJS.Row>;/** * 小标题样式 */ subTitleStyle?: Partial<ExcelJS.Row>;/** * 表头样式 */ headerStyle?: Partial<ExcelJS.Row>;/** * 单元格统一样式 */ cellStyle?: Partial<ExcelJS.Row & ExcelJS.Column>;}// 合并单元格数据类型exportinterfacemergeListType{ startRow:number; startColumn:number; endRow:number; endColumn:number;}// exceljs相关方法exportclassExcel{ blob?: Blob;// 导入的blob文件 worksheet?: ExcelJS.Worksheet;// 当前工作表 header:string[];// 表头字段数组constructor(blob?: Blob){this.blob = blob;this.worksheet =undefined;this.header =[];}/** * @description: blob转ArrayBuffer(用于后续生成文件数据) * @return {Promise<ArrayBuffer>} ArrayBuffer */privatereadFile():Promise<ArrayBuffer>{returnnewPromise<ArrayBuffer>((resolve, reject)=>{let reader =newFileReader();if(!this.blob){reject('上传文件异常!');}else{ reader.readAsArrayBuffer(this.blob); reader.onload=(ev)=>{resolve(ev.target!.result as ArrayBuffer);};}});}/** * @description: 导入excel文件获取workbook(workbook属性方法参考exceljs文档) * @return {Promise<ExcelJS.Workbook>} */publicasyncgetWorkBook():Promise<ExcelJS.Workbook>{let buffer =awaitthis.readFile();const workbook =newExcelJS.Workbook();returnawait workbook.xlsx.load(buffer);}/** * @description: 获取第i张工作表 * @param {number} i 工作表序号 * @return {Promise<ExcelJS.Worksheet>} 返回第 i 个工作表 */publicasyncgetWorkSheet(i:number=0):Promise<ExcelJS.Worksheet>{const workbook =awaitthis.getWorkBook();return workbook.getWorksheet(i);}/** * @description: 将 excel 第i张工作表的数据转为对象数据 * @param {number} i 工作表序号 * @param {string[]} header 表头字段数组 * @return {Promise<Record<(typeof header)[number], string>[]>} 传入表头作为字段的对象数组(每个元素对象对应每一行) */publicasyncimportExcel(options: importExcelType):Promise<Record<(typeof header)[number],string>[]>{const{ i =1, header }= options;const workbook =awaitthis.getWorkBook();const worksheet = workbook.getWorksheet(i);// excel导入后返回的数组const excelList: Record<(typeof header)[number],string>[]=[]; worksheet.getSheetValues().filter(temp =>!!temp?.length).forEach(item =>{// 移除空行// 移除每行首个空元素(item asstring[]).shift();// 定义临时对象存储每一行内容let tempObj: Record<(typeof header)[number],string>={};(item asstring[]).forEach((item2, index2)=>{ tempObj[header[index2]]= item2;}) excelList.push(tempObj);})return excelList }/** * @description: 导出excel,参数信息参考exceljs * @param {*} data 数据 * @param {*} name 文件名 * @param {*} header 表头字段 * @param {*} customHeader 表头字段对应中文 * @param {*} sheetName 工作表名 * @param {*} title 标题 * @param {*} subTitle 副标题(日期) * @param {*} password 冻结表格密码 * @param {*} mergeList 合并单元格数组 * @param {*} titleStyle 标题样式(按需补充方法) * @param {*} subTitleStyle 小标题样式(按需补充方法) * @param {*} headerStyle 表头字段样式(按需补充方法) * @param {*} cellStyle 单元格样式(按需补充方法) * @return {*} */publicasyncexportExcel(options: exportExcelType):Promise<void>{const{ data, name, header, customHeader, sheetName ='sheet1', title ='', subTitle ='', password ='', mergeList =[], titleStyle, subTitleStyle, headerStyle, cellStyle }= options;// 创建工作簿const workbook =newExcelJS.Workbook(); workbook.creator ='侃侃'; workbook.created =newDate();// 添加sheetthis.worksheet = workbook.addWorksheet(sheetName,{ properties:{ tabColor:{ argb:'FF00FF00'}}});this.header = header;// 表头行序号let headerRowId =1;if(!!title) headerRowId++;if(!!subTitle) headerRowId++;// 插入单元格数据this.setCells(data, customHeader, cellStyle);// 插入大标题this.getTitle(title, titleStyle);// 插入小标题this.getSubTitle(subTitle, subTitleStyle);// 处理表头this.setHeaderStyle(headerRowId, data, headerStyle);// 更多处理this.handleDealExcel(password, mergeList, headerRowId);// 导出excel(此处也可用file-saver将blob导出到excel文件 fs.saveAs(blob, name+'.xlsx');) workbook.xlsx.writeBuffer().then((data)=>{let blob =newBlob([data],{ type:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'});const a = document.createElement('a'); a.href =URL.createObjectURL(blob); a.download = name +'.xlsx'; document.body.appendChild(a); a.click(); document.body.removeChild(a); window.URL.revokeObjectURL(a.href);});}/** * @description: 合并单元格数组 * @param {Partial} merge1 需要合并的行/列起止对象数组1 * 例:[{ startRow: 1, endRow: 1},{ startRow: 10, endRow: 10}] * @param {Partial} merge2 需要合并的列/行起止对象数组2 * 例:[{ startColumn: 1, endColumn: 2 }] * @return {*} mergeArr 合并后数组 * 例:[{ startRow: 1, endRow: 1, startColumn: 1, endColumn: 2},{ startRow: 10, endRow: 10, startColumn: 1, endColumn: 2}] */publicmerge(merge1: Partial<mergeListType>[], merge2: Partial<mergeListType>[]): mergeListType[]{const mergeArr:any[]=[]; merge1.forEach(item1 =>{ mergeArr.push(...merge2.map(item2 =>{return{...item2,...item1 };}))})return mergeArr;}/** * @description:单元格数据处理 * @param {any} data 表格数据 * @param {string} customHeader 表头中文字段 * @return {*} */privatesetCells(data: exportExcelType['data'], customHeader:string[], style?: Partial<ExcelJS.Row & ExcelJS.Column>):void{// 设置列,插入中文表头const column: Partial<ExcelJS.Column>[]=[];this.header.forEach((item, index)=>{ column.push({ header: customHeader[index], key: item, width: style?.width ||25,})})this.worksheet!.columns = column;// 设置行,添加数据this.worksheet?.addRows(data);// 设置行高this.worksheet?.eachRow({ includeEmpty:true},(row, rowNumber)=>{ row.height = style?.height ||20;})// 获取每一列数据,再依次对齐this.worksheet!.columns.forEach(column =>{ column.alignment = style?.alignment ||{ vertical:'middle', horizontal:'center', wrapText:true}})}/** * @description: 添加大标题 * @param {string} title 标题 * @param {any} style 标题样式 * @return {*} */privategetTitle(title:string, style?: Partial<ExcelJS.Row>):void{if(!!title){// 插入标题this.worksheet?.spliceRows(1,0,[title]);this.worksheet?.mergeCells(1,1,1,this.header.length);// 调整标题样式const titleRow =this.worksheet!.getRow(1);// 高度 titleRow.height = style?.height ||40;// 字体设置 titleRow.font = style?.font ||{ size:20, bold:true}// 背景色 titleRow.fill = style?.fill ||{ bgColor:{'argb':'FFFFFF00'}, type:'pattern', pattern:'none'}// 对齐方式 titleRow.alignment = style?.alignment ||{ horizontal:"center", vertical:"middle"};}}/** * @description: 添加小标题 * @param {string} subTitle 标题 * @param {Partial} style 小标题样式 * @return {*} */privategetSubTitle(subTitle:string, style?: Partial<ExcelJS.Row>):void{if(!!subTitle){this.worksheet?.spliceRows(2,0,[subTitle]);this.worksheet?.mergeCells(2,1,2,this.header.length);// 调整标题样式const subtitleRow =this.worksheet!.getRow(2);// 高度 subtitleRow.height = style?.height ||20;// 字体设置 subtitleRow.font = style?.font ||{ size:14,}// 背景色 subtitleRow.fill = style?.fill ||{ bgColor:{'argb':'FFFFFF00'}, type:'pattern', pattern:'none'}// 对齐方式 subtitleRow.alignment = style?.alignment ||{ horizontal:"right", vertical:"middle"};}}/** * @description: 设置表头样式 * @param {number} num 表头在第几行 * @param {number} data 总数据 * @param {number} style 表头样式 * @return {*} */privatesetHeaderStyle(num:number, data:any, style?: Partial<ExcelJS.Row>):void{// 自动筛选器this.worksheet!.autoFilter ={ from:{ row: num, column:1}, to:{ row: data.length, column:this.header.length }}// 给表头添加背景色let headerRow =this.worksheet!.getRow(num); headerRow!.height = style?.height ||30;// 通过 cell 设置背景色,更精准 headerRow?.eachCell((cell)=>{ cell.fill = style?.fill ||{ type:'pattern', pattern:'solid', fgColor:{ argb:'dde0e7'},} cell.font = style?.font ||{ size:12}})}/** * @description: 其他内容处理 * @param {string} password 密码 * @param {mergeListType} mergeList 合并行列数组 * @param {number} headerRowId 表头行序号 * @return {*} */privatehandleDealExcel(password:string, mergeList: mergeListType[], headerRowId:number){// 添加工作表保护if(!!password){this.worksheet?.protect(password,{ autoFilter:true, selectLockedCells:false,});}// 合并单元格 mergeList.forEach(item =>{// 行数为表格数据所在行行数+表头行序号headerRowIdconst startRow = item.startRow + headerRowId;const endRow = item.endRow + headerRowId;this.worksheet?.mergeCells(startRow, item.startColumn, endRow, item.endColumn);})// 冻结前几行this.worksheet!.views =[{ state:'frozen', xSplit:0, ySplit: headerRowId }];}}
版权归原作者 侃侃vae 所有, 如有侵权,请联系我们删除。