vue頁(yè)面純前端導(dǎo)出excel表格(多級(jí)表頭,exceljs)

最近遇到一個(gè)比較麻煩的需求,前端實(shí)現(xiàn)excel導(dǎo)出,并且導(dǎo)出表格的表頭不確定,由用戶(hù)進(jìn)行設(shè)置。也就是說(shuō)表頭可能是一行也可能是兩行,并且有的標(biāo)題需要合并。導(dǎo)出效果如下圖所示:


導(dǎo)出表格部分內(nèi)容.png

查找對(duì)比

因?yàn)槭堑谝淮螌?shí)現(xiàn)這樣的功能,先在網(wǎng)上進(jìn)行了查找,發(fā)現(xiàn)了三種比較常用的方法:
1.安裝file-saver xlsx script-loader
如果想設(shè)置表格樣式的話(huà),需要同時(shí)安裝依賴(lài)xlsx-style,通常情況下安裝此依賴(lài)會(huì)報(bào)錯(cuò),需要進(jìn)行修改;
2.安裝vue-json-excel
這個(gè)插件看起來(lái)比較好上手,但是好像只適用于導(dǎo)出簡(jiǎn)單表頭,不支持多級(jí),如果導(dǎo)出表格簡(jiǎn)單的話(huà)大家可以嘗試一下。不過(guò)因?yàn)椴环衔业男枨?,所以我直接跳過(guò)了,不知道是否可以設(shè)置樣式;
3.安裝exceljs
只用安裝一個(gè)依賴(lài)并且支持修改樣式,我選擇了這個(gè)方法。

功能實(shí)現(xiàn)

1. 安裝依賴(lài)

npm install exceljs

2.設(shè)置表格數(shù)據(jù)格式

表格數(shù)據(jù)包含了表頭標(biāo)題和表里內(nèi)容,需要處理成固定格式。
請(qǐng)求后端拿到的表格數(shù)據(jù)格式:

[{
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" }
}]

需要拿到的表頭數(shù)據(jù)格式:

[{
  name: "name",
  label: "名稱(chēng)"
},{
  name: "",
  label: "價(jià)格指數(shù)",
  children: [{
    name: "",
    label: "2023年",
    children: [{
      name: "price1",
      label: "1月"
    },{
      name: "price2",
      label: "2月"
    }]
  }]
}]

其中name為綁定的字段,類(lèi)似于table表格中的prop;label為name對(duì)應(yīng)的字段名稱(chēng),類(lèi)似于table表格中的label。
需要注意的是,導(dǎo)出表格和頁(yè)面展示不一樣,頁(yè)面展示的時(shí)候prop值我們可以使用a.b或者a[0].b展示,但是導(dǎo)出的時(shí)候name值不可以,必須使用單個(gè)字段。

需要拿到的表中數(shù)據(jù)格式:
可以直接為請(qǐng)求后端拿到的表格數(shù)據(jù),如果有需要處理的數(shù)據(jù)進(jìn)行處理即可。我請(qǐng)求到的數(shù)據(jù)中有三類(lèi)數(shù)據(jù)需要進(jìn)行處理。
第一種是返回為字典key,需要轉(zhuǎn)換成字典value值;
第二種是返回?cái)?shù)據(jù)格式為list,需要轉(zhuǎn)換成單個(gè)的數(shù)據(jù);
第三種是對(duì)象數(shù)據(jù),取出其中的值

let list = data.map(item=>{
  // 一個(gè)一個(gè)判斷然后處理就可以,不要嫌麻煩
  // 字典轉(zhuǎn)換
  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;
  }
  // 對(duì)象取值
  item.staff = item.staffIndex ? item.staffIndex.indexValue : "";
})

不要嫌麻煩,前端實(shí)現(xiàn)導(dǎo)出本來(lái)就是一件麻煩的事情。

3.導(dǎo)出方法調(diào)用

this.exportMultiHeaderExcel(title, list);  // title為設(shè)置好的表頭數(shù)據(jù),list為表中數(shù)據(jù)

導(dǎo)出方法中判斷表頭有幾行,合并單元格
由于導(dǎo)出方法過(guò)長(zhǎng),所以對(duì)代碼塊進(jìn)行了分割,其實(shí)都在這個(gè)方法中

exportMultiHeaderExcel(column,data) {
      let keyArr = this.columns.map((item)=>{
        return item.label;
      })  // 一級(jí)表頭數(shù)組

      let singleLen = keyArr.length;  // 簡(jiǎn)單標(biāo)題長(zhǎng)度
      let multiLen = 0;  // 用來(lái)判斷是否有三級(jí)表頭
      let doubLen = 0;  // 用來(lái)判斷是否有二級(jí)表頭

      let row1 = JSON.parse(JSON.stringify(keyArr));
      let idx1 = row1.findIndex((item) => item == "價(jià)格指數(shù)");
      if (idx1 > -1) {
        row1.splice(idx1+1, 0, "", "", "", "", "", "", "", "", "", "", "");
        singleLen--;
        multiLen++;
      }
      let idx6 = row1.findIndex((item) => item == "期末人數(shù)");
      if (idx6 > -1) {
        singleLen--;
        doubLen = 1;
      }
      
      let row2 = [];
      let row3 = [];
    },
  • 導(dǎo)出方法中不同標(biāo)題處理
    每一行必須保持相同的長(zhǎng)度,為了之后合并單元格,沒(méi)有數(shù)據(jù)的設(shè)置為空
      // 判斷不同標(biāo)題進(jìn)行不同處理
      if (multiLen && doubLen) { // 所有標(biāo)題都有
        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 {...}
  • 導(dǎo)出方法中創(chuàng)建工作表
      // 創(chuàng)建excel
      const workbook = new ExcelJS.Workbook();
      // 創(chuàng)建工作表
      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;
      }
  • 導(dǎo)出方法中導(dǎo)出表頭設(shè)置
    因?yàn)槲疫@只有三級(jí)標(biāo)題所以進(jìn)行了三次循環(huán),循環(huán)次數(shù)和標(biāo)題行數(shù)有關(guān)系,這里應(yīng)該可以簡(jiǎn)化,還沒(méi)想到具體方法
      // 導(dǎo)出表頭設(shè)置
      const headers = [];
      column.forEach((item,index)=>{ //顯示標(biāo)題循環(huán)
        if(item.children){ //有子級(jí)
            item.children.forEach(itemChild=>{ //子級(jí)循環(huán)
              if(itemChild.children){ //有子級(jí)
                itemChild.children.forEach(itemChild1=>{ //子級(jí)循環(huán)
                    const obj = {
                        key:itemChild1.name, width: null
                    } //子級(jí)數(shù)據(jù)
                    const maxArr = [this.autoWidthAction(itemChild1.label)]
                    data.forEach(ite=>{ //顯示信息循環(huán)
                        const str = ite[itemChild1.name] ||'' //信息內(nèi)容
                        if(str){
                            maxArr.push(this.autoWidthAction(str))
                        }
                    })
                    obj.width = Math.max(...maxArr)+5
                    // 設(shè)置列名、鍵和寬度
                    headers.push(obj);
                })

              }else {
                const obj = {
                    key:itemChild.name, width: null
                } //子級(jí)數(shù)據(jù)
                const maxArr = [this.autoWidthAction(itemChild.label)]
                data.forEach(ite=>{ //顯示信息循環(huán)
                    const str = ite[itemChild.name] ||'' //信息內(nèi)容
                    if(str){
                        maxArr.push(this.autoWidthAction(str))
                    }
                })
                obj.width = Math.max(...maxArr)+5
                // 設(shè)置列名、鍵和寬度
                headers.push(obj);
              }
            })
        }else {
            const obj = {
                key:item.name, width: null
            } //標(biāo)題
            const maxArr = [this.autoWidthAction(item.label)]
            data.forEach(ite=>{
                const str = ite[item.name] ||''
                if(str){
                    maxArr.push(this.autoWidthAction(str))
                } //內(nèi)容
            })
            obj.width = Math.max(...maxArr)+5
            // 設(shè)置列名、鍵和寬度
            headers.push(obj);
        }
      })

      sheet.columns = headers;
      sheet.addRows(data);
  • 導(dǎo)出方法中合并單元格
    由于excel列的排序,this.exTabHeader是我寫(xiě)的一個(gè)排序數(shù)組["A","B","C","D","E","F","G","H","I","J","K","L","M"...],方便數(shù)字與字母的對(duì)應(yīng)
      // 合并單元格
      // 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 { ... }
  • 導(dǎo)出方法中設(shè)置表格標(biāo)題樣式
      // 表格樣式
      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' }
        }
      }
  • 導(dǎo)出方法中寫(xiě)入文件
      // 寫(xiě)入文件
      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標(biāo)簽下載
          link.href = window.URL.createObjectURL(blob); // href屬性指定下載鏈接
          link.download = "導(dǎo)出.xlsx"; // dowload屬性指定文件名
          link.click(); // click()事件觸發(fā)下載
          window.URL.revokeObjectURL(link.href); // 釋放內(nèi)存
        }
      });

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
}

如果想設(shè)置其他的屬性,也可以查閱exceljs文檔進(jìn)行設(shè)置。

  • 參考鏈接
    前端導(dǎo)出Excel(自定義樣式、多級(jí)表頭、普通導(dǎo)出)
    vue-admin-perfect
    exceljs中文文檔|exceljs js中文教程|解析
    ExcelJS 使用幫助文檔
  • 心得體會(huì)
    1.寫(xiě)這個(gè)功能之前其實(shí)先在網(wǎng)上查了兩天,因?yàn)橐郧岸际呛蠖俗鰧?dǎo)出。但是和我們經(jīng)理商量,他很堅(jiān)持的表示了由前端來(lái)做,沒(méi)辦法才開(kāi)始做。所以,如果沒(méi)做過(guò)還不得不做還是早開(kāi)始比較好。
    2.因?yàn)楸容^著急,所以代碼寫(xiě)得比較冗余比較亂,其實(shí)有很多地方可以改進(jìn),看之后時(shí)間吧,如果有時(shí)間會(huì)進(jìn)行優(yōu)化,不過(guò)一般都是不了了之。
    3.這個(gè)表格需求因?yàn)闃?biāo)題位置是固定的,也就是說(shuō)一定是一行標(biāo)題,三行標(biāo)題,兩行標(biāo)題排列。并且,三行標(biāo)題就是包含了12個(gè)子標(biāo)題,兩行標(biāo)題就是包含了一個(gè)子標(biāo)題,所以有些地方直接寫(xiě)的固定數(shù)值。如果是不確定的長(zhǎng)度,可能需要循環(huán)來(lái)判斷。如果只有一行簡(jiǎn)單標(biāo)題的話(huà)應(yīng)該會(huì)很棒,大家可以根據(jù)需求自行判斷使用什么方法。
    4.希望導(dǎo)出多由后端實(shí)現(xiàn)。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容