vue页面纯前端导出excel表格(多级表头,exceljs)

最近遇到一个比较麻烦的需求,前端实现excel导出,并且导出表格的表头不确定,由用户进行设置。也就是说表头可能是一行也可能是两行,并且有的标题需要合并。导出效果如下图所示:


导出表格部分内容.png

查找对比

因为是第一次实现这样的功能,先在网上进行了查找,发现了三种比较常用的方法:
1.安装file-saver xlsx script-loader
如果想设置表格样式的话,需要同时安装依赖xlsx-style,通常情况下安装此依赖会报错,需要进行修改;
2.安装vue-json-excel
这个插件看起来比较好上手,但是好像只适用于导出简单表头,不支持多级,如果导出表格简单的话大家可以尝试一下。不过因为不符合我的需求,所以我直接跳过了,不知道是否可以设置样式;
3.安装exceljs
只用安装一个依赖并且支持修改样式,我选择了这个方法。

功能实现

1. 安装依赖

npm install exceljs

2.设置表格数据格式

表格数据包含了表头标题和表里内容,需要处理成固定格式。
请求后端拿到的表格数据格式:

[{
rate: 0.65,
name: "变更版本八",
avgs: 10,
priceIndexList: [
 {indexYear: "2023", indexMonth: "01", indexValue: "1"}
 {indexYear: "2023", indexMonth: "02", indexValue: "2"}
],
status: 0,
staffIndex: { indexYear: "2023", indexValue: "1" }
}]

需要拿到的表头数据格式:

[{
  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 = [];
    },
  • 导出方法中不同标题处理
    每一行必须保持相同的长度,为了之后合并单元格,没有数据的设置为空
      // 判断不同标题进行不同处理
      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.希望导出多由后端实现。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容