关于前端:前端导出Excel之动态多级表头

37次阅读

共计 13957 个字符,预计需要花费 35 分钟才能阅读完成。

前阵子居家办公接到一个需要前端导出Excel,我兴高采烈的的和产品说这个货色没什么问题,接着产品就具体的把他的需要一字不差的和我讲了遍,过后我想发出我方才说过的话。

大抵需要如下:

页面中须要依据不同的查问条件展现不同的表头信息和表格数据,导出 Excel 的时候也须要依据以后表头信息导出数据。表头可能是一级,可能是二级,可能是 …N 级。我过后想拿大刀砍产品。

我和后端磋商了一下这个货色可不可以他们做,为了性能最初决定前后端都得做(谁也跑不了 …),数据量大的时候后端导出,数据量小的时候前端导出。

背景故事大略就是这样,因为笔者原来做过 Excel 导出,然而也只是简略利用而是,波及到二级表头都很少,更别说 N 级了。为了可能实现这个需要我从新看了一下原来应用的框架以及其余方向的调研,于是通过笔者不懈的致力,繁忙了两个小时的笔者,最初感觉关上淘宝 … 错了,重来。最终实现了两版,呈现的成果是一样的,然而两者仍存在差异。

导出 Excel 多级表头 1.0

最开始调研的时候应用的是 xlsx 这个工具包,因为这个包相对来说比拟是相熟,上手比拟快。过后在最开始要优先思考。

装置依赖:

npm install xlsx -S
or
yarn add xlsx -S

创立文件 DownExcel.js 在文件中引入 xlsx,废话不多说创立一个class,程序设计后期思考是这样的,在class 初始化的时候,须要接管一个 tableHead 即须要导出数据的表头,下载文件的时候须要调用 down 办法,在 down 办法中须要接管 data 和须要导出的文件名。

class DownExcel {constructor({ header = [] }) {this.tableHeader = header;}
  
  down(fileName, tableData = []){}}

最辣手其实不是表格数据的填写,而是如何让导出的 Excel 反对 N 级动静表头。因为 xlsx 框架在导出 excel 的时候反对 Csv 模式的,最初将 Csv 转换成 Sheet 之后再导出文件,那么也就是说我须要在整顿 Csv 数据之前,就应该晓得了表头的合并信息。然而 xlsx 框架的合并信息是独自存储的,大略内容如下:

const marge = [{s: { r: 0, c: 1},
    e: {r: 0, c: 2}
}];

上述内容中,s代表开始的节点地位,e代表的是完结的节点地位,r代表的是行,c代表的是列。通过剖析数据能够看出。别离确认了两个坐标点,按照两个坐标点进行单元格合并。那么如果想要反对多级表头数据那么就须要反对 marge 信息是什么,如果能够依据表头信息来生成岂不就行了吗?

页面表格中是存有表头信息的,导出 Excel 须要依据页面表头来生成一样的。表头数据信息大略是这个样子的:

const tableHeader = [{
  field: "a111",
  title: "a1"
},{
  field: "a333",
  title: "a3",
  children: [{
    field: "b111",
    title: "b1",
  },{
    field: "b222",
    title: "b2",
    children: [{
      field: "c1",
      title: "c111",
      children: [{
        file: "d444",
        title: "d4"
      }]
    }]
  }]
}];

以上就是表头的数据为嵌套数据格式,只有是上级存在 children 那么该级就须要进行合并,如果没有的话就须要合并到表头的底部。

创立办法resetMergeHeaderInfo

class DownExcel {down(fileName, tableData = []){this.resetMergeHeaderInfo();
  }

 // 表头数据                tableHeader
 // 表头深度                maxLevel
 // 合并表头长期存储信息    outMarge
 // 最终后果                result
  resetMergeHeaderInfo(tableHeader, maxLevel, outMarge, result = []){}}

在办法中其中有一个参数是 maxLevel,这个参数是以后表头信息的最大深度?也就是以后表头信息一共嵌套了多少层。有了这个参数,就能晓得最外层的数据,如果没有children 的时候单元格的合并范畴。那么也就须要确认哪些是最外层,哪些是内层的,哪些是有children,哪些是没有children。大抵上在表头中呈现的状况也就这几种了。

首先确认外层数据,如果想实现嵌套数据,比不少会用到递归,须要对外层书进行标记。

class DownExcel {

 // 表头数据                tableHeader
 // 表头深度                maxLevel
 // 合并表头长期存储信息    outMarge
 // 最终后果                result
  resetMergeHeaderInfo(tableHeader, maxLevel, outMarge, result = []){this.tagHeadIn();
  }    
  
  //  标记最外层数据
  tagHeadIn(){const { tableHeader} = this;
    tableHeader.forEach((el) => {
      el.isOut = true;
      return el;
    })
  }

}

先解决外层数据,方才也说过了,须要晓得最大的嵌套层级是多大,所以这里须要办法获取到所须要的这个参数。

class DownExcel {down(){const { tableHeader} = this;
    let maxLevel = this.maxLevel(tableHeader);
    this.resetMergeHeaderInfo(tableHeader,maxLevel);
  }
  
  resetMergeHeaderInfo(tableHeader, maxLevel, outMarge, result = []){this.tagHeadIn();
  }
    
 tagMaxLevel(tableHeader){const maxLevel = this.maxLevel(tableHeader, false);
    tableHeader.forEach((el) => {if(!el.children){el.maxLen = maxLevel;}
      else{this.tagMaxLevel(el.children);
        el.maxLen = maxLevel;
      }
    });
  }
    
}

在标记层级的的时候,在每一层都标注了,他上面的最大层级,这样就不须要每次都须要遍历去去获取最大深度。认为子级也是有嵌套,如果没有嵌套越须要晓得以后单元格向下合并几个,否则会导致合并不能对立。

接下来就是须要解决最外层的表格信息了:

class DownExcel {resetMergeHeaderInfo(tableHeader, maxLevel, outMarge, result = []){this.tagHeadIn();
    for(let i = 0; i < tableHeader.length; i++){let item = tableHeader[i];
         //  纵向跨度
         const {maxLen} = item;
        //  开始节点信息
        let s = {};
        //  完结节点信息
        let e = {};
        //  如果没有子级
        if(!item.children){
            //  如果是最外层元素
            if(item.isOut){
                //  当前列开始地位
                outMarge.startCell += 1;
                //  全局列开始地位
                outMarge.basisCell += 1;
                //  开始行
                s.r = 0;
                //  完结行
                e.r = maxLevel;
                //  开始列
                s.c = outMarge.startCell;
                //  完结列
                e.c = outMarge.startCell;
                result.push({s, e, item});
            }else{//  不是外层元素}
        }
        //  如果有子级
        if(item.children){
            //  如果是最外层元素
            if(item.isOut){}else{  //  不是外层元素}
        }
    }
  }
}

依据已知的信息对外层没有子级的单元格信息曾经获取到了,为什么要记录两个列开始信息?如果 A 单元格上面有两个子级,一个没有 children(A1)另一个有children(A2)确定完信息之后,开始列就会发生变化自增1A2 渲染的时候子级有多少个须要在全局记录的行信息上加上,这样进入到 B 单元格循环的时候才会从对应的中央开始进行下一次合并信息记录。

当有子级的时候就会有一个问题,那么就是以后单元格的横向合并多少个单元格?那么就能够依据以后单元格的 children 所有没有 children 的子级,就是以后单元格的横向跨度。

class DownExcel {

  //  获取以后上面所有子级
  //  即:表头横向跨度单元格数量
  getLastChild (arr, result = []){for(let i = 0,item; item = arr[i++];){if(!item.children){result.push(item);
      }else{result = this.getLastChild(item.children, result);
      }
    }
    return result;
  }

}

晓得了横向跨度之后能够解决内部有子级的单元格合并信息的收集:

class DownExcel {resetMergeHeaderInfo(tableHeader, maxLevel, outMarge, result = []){this.tagHeadIn();
    for(let i = 0; i < tableHeader.length; i++){let item = tableHeader[i];
         //  纵向跨度
         const {maxLen} = item;
         //  横向跨度
        let lastChild = this.getLastChild(item.children || []);
        //  开始节点信息
        let s = {};
        //  完结节点信息
        let e = {};
        //  如果没有子级
        if(!item.children){
            //  如果是最外层元素
            if(item.isOut){//  .....}else{//  不是外层元素}
        }
        //  如果有子级
        if(item.children){
            //  如果是最外层元素
            if(item.isOut){
              // 开始行
              s.r = 0;
              // 完结行
              e.r = 0;
              // 部分开始列自增
              outMarge.startCell += 1;
              // 开始列
              s.c = outMarge.startCell;
              // 开始列加上横向跨度
              outMarge.startCell += lastChild.length - 1;
              // 完结列
              e.c = outMarge.startCell;
              result.push({s, e, item});
            }else{//  不是外层元素}
        }
    }
  }
}

因为横向开始地位是须要记录的,所以要加上以后的横向跨度,防止下次循环的开始地位呈现谬误。

class DownExcel {resetMergeHeaderInfo(tableHeader, maxLevel, outMarge, result = []){this.tagHeadIn();
    for(let i = 0; i < tableHeader.length; i++){let item = tableHeader[i];
         //  纵向跨度
         const {maxLen} = item;
         //  横向跨度
        let lastChild = this.getLastChild(item.children || []);
        //  开始节点信息
        let s = {};
        //  完结节点信息
        let e = {};
        //  如果没有子级
        if(!item.children){
            //  如果是最外层元素
            if(item.isOut){//  .....}else{  //  不是外层元素
              // 开始行 
              let r = maxLevel - (outMarge.basisRow + maxLen);
              r = Math.max(r, 0);
              s.c = outMarge.basisCell;
              e.c = outMarge.basisCell;
              s.r = outMarge.basisRow;
              e.r = r + outMarge.basisRow + maxLen;
              result.push({s, e, item});
              //  开始行数据 + 1
              outMarge.basisCell += 1;
            }
        }
        //  如果有子级
        if(item.children){
            //  如果是最外层元素
            if(item.isOut){//    ...}else{//  不是外层元素}
        }
    }
  }
}

解决完内部数据就能够解决外部了,解决外部数据其实和解决内部数据是差不多的。在解决子级的时候,如果单元格上面两个如果 A 单元格上面有两个子级,一个没有 children(A1)另一个有children(A2),这个时候A1 须要向下合并到最底部,用最大深度 –(开始地位 + 合并高度)失去差值,即以后单元个的完结地位。

class DownExcel {resetMergeHeaderInfo(tableHeader, maxLevel, outMarge, result = []){this.tagHeadIn();
    for(let i = 0; i < tableHeader.length; i++){let item = tableHeader[i];
         //  纵向跨度
         const {maxLen} = item;
         //  横向跨度
        let lastChild = this.getLastChild(item.children || []);
        //  开始节点信息
        let s = {};
        //  完结节点信息
        let e = {};
        //  如果没有子级
        if(!item.children){
            //  如果是最外层元素
            if(item.isOut){//  .....}else{  //  不是外层元素
               //   .....
            }
        }
        //  如果有子级
        if(item.children){
            //  如果是最外层元素
            if(item.isOut){//    ...}else{  //  不是外层元素
                s.c = outMarge.basisCell;
                e.c = outMarge.basisCell + lastChild.length - 1;
                s.r = outMarge.basisRow;
                e.r = outMarge.basisRow;
                result.push({s, e, item});
            }
        }
    }
  }
}

这里解决逻辑和外层有子级的解决逻辑是相似的。

class DownExcel {resetMergeHeaderInfo(tableHeader, maxLevel, outMarge, result = []){this.tagHeadIn();
    for(let i = 0; i < tableHeader.length; i++){let item = tableHeader[i];
         //  纵向跨度
         const {maxLen} = item;
         //  横向跨度
        let lastChild = this.getLastChild(item.children || []);
        //  开始节点信息
        let s = {};
        //  完结节点信息
        let e = {};
        //  如果没有子级
        if(!item.children){
            //  如果是最外层元素
            if(item.isOut){//  .....}else{  //  不是外层元素
               //   .....
            }
        }
        //  如果有子级
        if(item.children){
            //  如果是最外层元素
            if(item.isOut){//    ...}else{  //  不是外层元素
              // ....
            }
        }
        outMarge.basisRow += 1;
        this.resetMergeHeaderInfo(item.children, maxLevel, outMarge, result);
    }
    outMarge.basisRow -= 1;
    return result;
  }
}

解决完收集信息,须要把所有子级的信息收集一下,须要把本地的列数自增一下,保障所和合并的时候合并坐标值是随着遍历和递归是同步进行的。每次递归完须要减一,也就是递归一次须要加一次。

这样针对合并信息收集就实现了,失去的数据和 xlsx 框架所须要的是统一的。

class DownExcel {down(fileName, tableData = []){const { tableHeader, outMarge} = this;
    let maxLevel = this.maxLevel(tableHeader);
    const mergeInfo = this.resetMergeHeaderInfo(tableHeader, maxLevel, outMarge);
    const lastChild = this.getLastChild(tableHeader);
    const headCsv = this.getHeadCsv(tableHeader, lastChild, maxLevel, mergeInfo);
    const dataCsv = this.getDataCsv(tableData, lastChild);
    const allCsv = this.margeCsv(headCsv, dataCsv);
  }
  
  
  //  将数据转换成 Csv 格局
  getHeadCsv(tableHeader, lastChild, maxLevel, mergeInfo){let csvArr = [];
    let csv = "";
    for(let i = 0; i < (maxLevel + 1); i++){let item = [];
      for(let j = 0; j < lastChild.length; j++){item.push(null);
      }
      csvArr.push(item);
    }
    for(let i = 0; i < mergeInfo.length; i++){let info = mergeInfo[i];
      const {s, item} = info;
      const {c, r} = s;
      const {title} = item;
      csvArr[r] = title;
      console.log(mergeInfo);
    }
    csvArr = csvArr.map((el) => {return el.join("^");
    });
    return csvArr.join("~");
  }
  
  //  获取 data 的 Csv
  getDataCsv(data, lastChild){let result = [];
    for(let j = 0, ele; ele = data[j++];){let value = [];
      for(let i = 0, item; item = lastChild[i++];){value.push(ele[item.field] || "-");
      };
      result.push(value);
    }
    result = result.map((el) => {return el.join("^");
    });
    return result.join("~");
  }
  
  //  合并 Csv
  margeCsv(headCsv, dataCsv){return `${headCsv}~${dataCsv}`;
  }
  
}

合并信息处理完了之后须要把以后的表头数据和列表数据处理成 csv 格局,不便导出 excel,首先思考的是表头合并信息局部数据须要用空数据代替,不能把所有的数据间接填写进去,否则在导出的时候会产生数据地位错乱的问题。

class DownExcel {down(fileName, tableData = []){const { tableHeader, outMarge} = this;
    let maxLevel = this.maxLevel(tableHeader);
    const mergeInfo = this.resetMergeHeaderInfo(tableHeader, maxLevel, outMarge);
    const lastChild = this.getLastChild(tableHeader);
    const headCsv = this.getHeadCsv(tableHeader, lastChild, maxLevel, mergeInfo);
    const dataCsv = this.getDataCsv(tableData, lastChild);
    const allCsv = this.margeCsv(headCsv, dataCsv);
    
    const cscSeet = this.csv2sheet(allCsv);
    console.log(cscSeet);
    let blob = this.sheet2blob(cscSeet);
    this.openDownloadDialog(blob,`${fileName}.xlsx`);
  }
  
  //  将 csv 转换成 sheet 数据
  csv2sheet(csv) {csv = csv.split('~');
    //  缓存
    let arr = [];
    //  剪切未数组
    csv.forEach((el) => {
      //  剪切数据并增加答题 arr
      arr.push(el.split("^"));
    });
    //  调用办法
    return XLSX.utils.aoa_to_sheet(arr);
  }
  
  //  sheet 转 blob 文件
  sheet2blob(sheet, sheetName) {
    //  导出文件类型
    sheetName = sheetName || 'sheet1';
    var workbook = {SheetNames: [sheetName],
      Sheets: {}};
    workbook.Sheets[sheetName] = sheet;
    var wopts = {
      bookType: "xlsx",
      bookSST: false,
      type: 'binary'
    };
    var wbout = XLSX.write(workbook, wopts);
    var blob = new Blob([s2ab(wbout)], {type:"application/octet-stream"});
    // 字符串转 ArrayBuffer
    function s2ab(s) {var buf = new ArrayBuffer(s.length);
      var view = new Uint8Array(buf);
      for (var i=0; i!=s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF;
      return buf;
    }
    return blob;
  }
  
  // 导出 excel
  openDownloadDialog(url, saveName){if(typeof url == 'object' && url instanceof Blob){url = URL.createObjectURL(url); // 创立 blob 地址
    }
    var aLink = document.createElement('a');
    aLink.href = url;
    aLink.download = saveName || '';
    var event;
    if(window.MouseEvent) event = new MouseEvent('click');
    else
    {event = document.createEvent('MouseEvents');
      event.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
    }
    aLink.dispatchEvent(event);
  }
  
}

Csv 转换成 sheet 数据,并把合并信息赋值给 sheet 就能够就能够通过调用框架的办法转换成 blob 对象实现最初的数据导出。

残缺代码:

import * as XLSX from "xlsx";

export default class DownExcel {constructor({ header = [] }) {
    this.tableHeader = header;
    this.outMarge = {
      startCell: -1,
      basisRow: 0,
      basisCell: 0,
      maxRow: 0
    };
  }

  down(fileName, tableData = []){const { tableHeader, outMarge} = this;
    let maxLevel = this.maxLevel(tableHeader);
    const mergeInfo = this.resetMergeHeaderInfo(tableHeader, maxLevel, outMarge);
    const lastChild = this.getLastChild(tableHeader);
    const headCsv = this.getHeadCsv(tableHeader, lastChild, maxLevel, mergeInfo);
    const dataCsv = this.getDataCsv(tableData, lastChild);
    const allCsv = this.margeCsv(headCsv, dataCsv);
    const cscSeet = this.csv2sheet(allCsv);
    cscSeet['!merges'] = mergeInfo;
    console.log(cscSeet);
    let blob = this.sheet2blob(cscSeet);
    this.openDownloadDialog(blob,`${fileName}.xlsx`);
  }

  //  sheet 转 blob 文件
  sheet2blob(sheet, sheetName) {
    //  导出文件类型
    sheetName = sheetName || 'sheet1';
    var workbook = {SheetNames: [sheetName],
      Sheets: {}};
    workbook.Sheets[sheetName] = sheet;
    var wopts = {
      bookType: "xlsx",
      bookSST: false,
      type: 'binary'
    };
    var wbout = XLSX.write(workbook, wopts);
    var blob = new Blob([s2ab(wbout)], {type:"application/octet-stream"});
    // 字符串转 ArrayBuffer
    function s2ab(s) {var buf = new ArrayBuffer(s.length);
      var view = new Uint8Array(buf);
      for (var i=0; i!=s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF;
      return buf;
    }
    return blob;
  }

  //  导出 Excel
  openDownloadDialog(url, saveName){if(typeof url == 'object' && url instanceof Blob){url = URL.createObjectURL(url); // 创立 blob 地址
    }
    var aLink = document.createElement('a');
    aLink.href = url;
    aLink.download = saveName || '';
    var event;
    if(window.MouseEvent) event = new MouseEvent('click');
    else
    {event = document.createEvent('MouseEvents');
      event.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
    }
    aLink.dispatchEvent(event);
  }

  //  获取 data 的 Csv
  getDataCsv(data, lastChild){let result = [];
    for(let j = 0, ele; ele = data[j++];){let value = [];
      for(let i = 0, item; item = lastChild[i++];){value.push(ele[item.field] || "-");
      };
      result.push(value);
    }
    result = result.map((el) => {return el.join("^");
    });
    return result.join("~");
  }

  //  将数据转换成 Csv 格局
  getHeadCsv(tableHeader, lastChild, maxLevel, mergeInfo){let csvArr = [];
    let csv = "";
    for(let i = 0; i < (maxLevel + 1); i++){let item = [];
      for(let j = 0; j < lastChild.length; j++){item.push(null);
      }
      csvArr.push(item);
    }
    for(let i = 0; i < mergeInfo.length; i++){let info = mergeInfo[i];
      const {s, item} = info;
      const {c, r} = s;
      const {title} = item;
      csvArr[r] = title;
      console.log(mergeInfo);
    }
    csvArr = csvArr.map((el) => {return el.join("^");
    });
    return csvArr.join("~");
  }

  //  合并 Csv
  margeCsv(headCsv, dataCsv){return `${headCsv}~${dataCsv}`;
  }

  //  将 csv 转换成 sheet 数据
  csv2sheet(csv) {csv = csv.split('~');
    //  缓存
    let arr = [];
    //  剪切未数组
    csv.forEach((el) => {
      //  剪切数据并增加答题 arr
      arr.push(el.split("^"));
    });
    //  调用办法
    return XLSX.utils.aoa_to_sheet(arr);
  }

  resetMergeHeaderInfo(tableHeader, maxLevel, outMarge, result = []){this.tagHeadIn();
    this.tagMaxLevel(tableHeader);
    for(let i = 0; i < tableHeader.length; i++){let item = tableHeader[i];
      //  纵向跨度
      const {maxLen} = item;
      //  横向跨度
      let lastChild = this.getLastChild(item.children || []);
      //  s :  开始  e : 完结
      //  c : 列(横向)//  r:行(纵向)let s = {};
      let e = {};
      if(!item.children){if(item.isOut){
          outMarge.startCell += 1;
          outMarge.basisCell += 1;
          s.r = 0;
          e.r = maxLevel;
          s.c = outMarge.startCell;
          e.c = outMarge.startCell;
          result.push({s, e, item});
        }
        else{let r = maxLevel - (outMarge.basisRow + maxLen);
          r = Math.max(r, 0);
          s.c = outMarge.basisCell;
          e.c = outMarge.basisCell;
          s.r = outMarge.basisRow;
          e.r = r + outMarge.basisRow + maxLen;
          result.push({s, e, item});
          outMarge.basisCell += 1;
        }
      };
      if(item.children){if(item.isOut){
          s.r = 0;
          e.r = 0;
          outMarge.startCell += 1;
          s.c = outMarge.startCell;
          outMarge.startCell += lastChild.length - 1;
          e.c = outMarge.startCell;
          result.push({s, e, item});
        }else{
          s.c = outMarge.basisCell;
          e.c = outMarge.basisCell + lastChild.length - 1;
          s.r = outMarge.basisRow;
          e.r = outMarge.basisRow;
          result.push({s, e, item});
        }
        outMarge.basisRow += 1;
        this.resetMergeHeaderInfo(item.children, maxLevel, outMarge, result);
      };
    };
    outMarge.basisRow -= 1;
    return result;
  }

  tagHeadIn(){const { tableHeader} = this;
    tableHeader.forEach((el) => {
      el.isOut = true;
      return el;
    })
  }

  //  标记最大层级
  tagMaxLevel(tableHeader){const maxLevel = this.maxLevel(tableHeader, false);
    tableHeader.forEach((el) => {if(!el.children){el.maxLen = maxLevel;}
      else{this.tagMaxLevel(el.children);
        el.maxLen = maxLevel;
      }
    });
  }

  //  获取最大层级
  //  只蕴含子级最大层级(不蕴含本级)
  maxLevel(arr, isSetFloor = true){
    let floor = -1;
    let max = -1;
    function each (data, floor) {
      data.forEach(e => {max = Math.max(floor, max);
        isSetFloor && (e.floor = (floor + 1));
        if (e.children) {each(e.children, floor + 1)
        }
      })
    }
    each(arr,0)
    return max;
  }

  //  获取以后上面所有子级
  //  即:表头横向跨度单元格数量
  getLastChild (arr, result = []){for(let i = 0,item; item = arr[i++];){if(!item.children){result.push(item);
      }else{result = this.getLastChild(item.children, result);
      }
    }
    return result;
  }

};

应用:

new DownExcel({
  header:  [{
    field: "c1",
    title: "c111",
    children: [{
      field: "c2",
      title: "c222"
    },{
      field: "c3",
      title: "c333"
    },{
      field: "c4",
      title: "c444"
    }]
  }]
}).down(`${+new Date()}`,[{
  a111: "LLL",
  c1: "HHH",
  c2: "AAA",
  a444: "III"
}]);

感激大家破费很长时间来浏览这篇文章,若文章中有谬误灰常感激大家提出斧正,我会尽快做出批改的。如果大家喜爱的话接下来会更新一下,导出带入款式并且反对多级表头。

正文完
 0