vue页面纯前端导出excel表格(多级表头,exceljs)
查找对比
因为是第一次实现这样的功能,先在网上进行了查找,发现了三种比较常用的方法:
1.安装file-saver xlsx script-loader
如果想设置表格样式的话,需要同时安装依赖xlsx-style,通常情况下安装此依赖会报错,需要进行修改;
2.安装vue-json-excel
这个插件看起来比较好上手,但是好像只适用于导出简单表头,不支持多级,如果导出表格简单的话大家可以尝试一下。不过因为不符合我的需求,所以我直接跳过了,不知道是否可以设置样式;
3.安装exceljs
只用安装一个依赖并且支持修改样式,我选择了这个方法。
功能实现
1. 安装依赖
npm install exceljs
2.设置表格数据格式
表格数据包含了表头标题和表里内容,需要处理成固定格式。
请求后端拿到的表格数据格式:
需要拿到的表头数据格式:
[{ name: "name", label: "名称" },{ name: "", label: "价格指数", children: [{ name: "", label: "2023年", children: [{ name: "price1", label: "1月" },{ name: "price2", label: "2月" }] }] }]
其中name为绑定的字段,类似于table表格中的prop;label为name对应的字段名称,类似于table表格中的label。
需要注意的是,导出表格和页面展示不一样,页面展示的时候prop值我们可以使用a.b或者a[0].b展示,但是导出的时候name值不可以,必须使用单个字段。
需要拿到的表中数据格式:
可以直接为请求后端拿到的表格数据,如果有需要处理的数据进行处理即可。我请求到的数据中有三类数据需要进行处理。
第一种是返回为字典key,需要转换成字典value值;
第二种是返回数据格式为list,需要转换成单个的数据;
第三种是对象数据,取出其中的值
let list = data.map(item=>{ // 一个一个判断然后处理就可以,不要嫌麻烦 // 字典转换 for (let key in item) { if (key == "status" { item[key] = this.selectDictLabel(this.yesOrNo, item[key]); } } // list拆解 for (let i=0; i < 12; i++) { item["price"+i] = item.priceIndexList[i].indexValue; } // 对象取值 item.staff = item.staffIndex ? item.staffIndex.indexValue : ""; })
不要嫌麻烦,前端实现导出本来就是一件麻烦的事情。
3.导出方法调用
this.exportMultiHeaderExcel(title, list); // title为设置好的表头数据,list为表中数据
导出方法中判断表头有几行,合并单元格
由于导出方法过长,所以对代码块进行了分割,其实都在这个方法中
exportMultiHeaderExcel(column,data) { let keyArr = this.columns.map((item)=>{ return item.label; }) // 一级表头数组 let singleLen = keyArr.length; // 简单标题长度 let multiLen = 0; // 用来判断是否有三级表头 let doubLen = 0; // 用来判断是否有二级表头 let row1 = JSON.parse(JSON.stringify(keyArr)); let idx1 = row1.findIndex((item) => item == "价格指数"); if (idx1 > -1) { row1.splice(idx1+1, 0, "", "", "", "", "", "", "", "", "", "", ""); singleLen--; multiLen++; } let idx6 = row1.findIndex((item) => item == "期末人数"); if (idx6 > -1) { singleLen--; doubLen = 1; } let row2 = []; let row3 = []; },
导出方法中判断表头有几行,合并单元格
由于导出方法过长,所以对代码块进行了分割,其实都在这个方法中
exportMultiHeaderExcel(column,data) { let keyArr = this.columns.map((item)=>{ return item.label; }) // 一级表头数组 let singleLen = keyArr.length; // 简单标题长度 let multiLen = 0; // 用来判断是否有三级表头 let doubLen = 0; // 用来判断是否有二级表头 let row1 = JSON.parse(JSON.stringify(keyArr)); let idx1 = row1.findIndex((item) => item == "价格指数"); if (idx1 > -1) { row1.splice(idx1+1, 0, "", "", "", "", "", "", "", "", "", "", ""); singleLen--; multiLen++; } let idx6 = row1.findIndex((item) => item == "期末人数"); if (idx6 > -1) { singleLen--; doubLen = 1; } let row2 = []; let row3 = []; },
导出方法中不同标题处理
每一行必须保持相同的长度,为了之后合并单元格,没有数据的设置为空
// 判断不同标题进行不同处理 if (multiLen && doubLen) { // 所有标题都有 for (let i = 0; i < singleLen; i++) { row2.push(""); row3.push(""); }; if (idx1 > -1) { row2.splice(idx1+1, 0, this.indexYear+"年", "", "", "", "", "", "", "", "", "", "", ""); row3.splice(idx1+1, 0, "1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"); } if (idx6 > -1) { row2.push(this.indexYear+"年"); row3.push(""); } } else {...}
导出方法中创建工作表
// 创建excel const workbook = new ExcelJS.Workbook(); // 创建工作表 const sheet = workbook.addWorksheet("sheet1"); // 添加表头 sheet.getRow(1).values = row1; if (row2 && row2.length) { sheet.getRow(2).values = row2; } if (row3 && row3.length) { sheet.getRow(3).values = row3; }
导出方法中导出表头设置
因为我这只有三级标题所以进行了三次循环,循环次数和标题行数有关系,这里应该可以简化,还没想到具体方法
// 导出表头设置 const headers = []; column.forEach((item,index)=>{ //显示标题循环 if(item.children){ //有子级 item.children.forEach(itemChild=>{ //子级循环 if(itemChild.children){ //有子级 itemChild.children.forEach(itemChild1=>{ //子级循环 const obj = { key:itemChild1.name, width: null } //子级数据 const maxArr = [this.autoWidthAction(itemChild1.label)] data.forEach(ite=>{ //显示信息循环 const str = ite[itemChild1.name] ||'' //信息内容 if(str){ maxArr.push(this.autoWidthAction(str)) } }) obj.width = Math.max(...maxArr)+5 // 设置列名、键和宽度 headers.push(obj); }) }else { const obj = { key:itemChild.name, width: null } //子级数据 const maxArr = [this.autoWidthAction(itemChild.label)] data.forEach(ite=>{ //显示信息循环 const str = ite[itemChild.name] ||'' //信息内容 if(str){ maxArr.push(this.autoWidthAction(str)) } }) obj.width = Math.max(...maxArr)+5 // 设置列名、键和宽度 headers.push(obj); } }) }else { const obj = { key:item.name, width: null } //标题 const maxArr = [this.autoWidthAction(item.label)] data.forEach(ite=>{ const str = ite[item.name] ||'' if(str){ maxArr.push(this.autoWidthAction(str)) } //内容 }) obj.width = Math.max(...maxArr)+5 // 设置列名、键和宽度 headers.push(obj); } }) sheet.columns = headers; sheet.addRows(data);
由于excel列的排序,this.exTabHeader是我写的一个排序数组["A","B","C","D","E","F","G","H","I","J","K","L","M"...],方便数字与字母的对应
// 合并单元格 // sheet.mergeCells(`D1:F1`); // 表示合并D列1行到F列1行 if (doubLen && multiLen) { for (let i = 0; i < singleLen; i++) { let start1 = this.exTabHeader[i]+1; let end1 = this.exTabHeader[i]+3; sheet.mergeCells(`${start1}`+":"+`${end1}`); } let start2 = this.exTabHeader[idx6]+2; let end2 = this.exTabHeader[idx6]+3; sheet.mergeCells(`${start2}`+":"+`${end2}`); if (idx1 > -1) { let start1 = this.exTabHeader[idx1]+1; let end1 = this.exTabHeader[idx1+11]+1; sheet.mergeCells(`${start1}`+":"+`${end1}`); let start2 = this.exTabHeader[idx1]+2; let end2 = this.exTabHeader[idx1+11]+2; sheet.mergeCells(`${start2}`+":"+`${end2}`); } } else { ... }
导出方法中设置表格标题样式
// 表格样式 for (let i = 0; i < row1.length; i++) { let one = this.exTabHeader[i]+1; sheet.getCell(`${one}`).font = { // 字体 color: { argb:'FFFFFFFF' }, bold: true } sheet.getCell(`${one}`).fill = { // 背景色 type: 'pattern', pattern:'solid', fgColor:{ argb:'FF808080' } } }
导出方法中写入文件
// 写入文件 workbook.xlsx.writeBuffer().then((data) => { const blob = new Blob([data, { type: "application/vnd.ms-excel" }]); if (window.navigator.msSaveOrOpenBlob) { // msSaveOrOpenBlob方法返回boolean值 navigator.msSaveBlob(blob, filename + ".xlsx"); // 本地保存 } else { const link = document.createElement("a"); // a标签下载 link.href = window.URL.createObjectURL(blob); // href属性指定下载链接 link.download = "导出.xlsx"; // dowload属性指定文件名 link.click(); // click()事件触发下载 window.URL.revokeObjectURL(link.href); // 释放内存 } });
autoWidthAction方法
autoWidthAction(val,width) { if (val == null) { width = 10; } else if (val.toString().charCodeAt(0) > 255) { /*if chinese*/ width = val.toString().length * 2; } else { width = val.toString().length; } return width }
如果想设置其他的属性,也可以查阅exceljs文档进行设置。
- 参考链接
前端导出Excel(自定义样式、多级表头、普通导出)
vue-admin-perfect
exceljs中文文档|exceljs js中文教程|解析
ExcelJS 使用帮助文档 - 心得体会
1.写这个功能之前其实先在网上查了两天,因为以前都是后端做导出。但是和我们经理商量,他很坚持的表示了由前端来做,没办法才开始做。所以,如果没做过还不得不做还是早开始比较好。
2.因为比较着急,所以代码写得比较冗余比较乱,其实有很多地方可以改进,看之后时间吧,如果有时间会进行优化,不过一般都是不了了之。
3.这个表格需求因为标题位置是固定的,也就是说一定是一行标题,三行标题,两行标题排列。并且,三行标题就是包含了12个子标题,两行标题就是包含了一个子标题,所以有些地方直接写的固定数值。如果是不确定的长度,可能需要循环来判断。如果只有一行简单标题的话应该会很棒,大家可以根据需求自行判断使用什么方法。
4.希望导出多由后端实现。
链接:https://www.jianshu.com/p/8f47c7e2f67b
来源:简书