前端导出可修改样式的Excel表格

时间:2024-02-17 20:01:39

最近实现了一个纯前端下载Excel,并可以修改Excel样式的功能。由于实现过程比较曲折,没搜到较完整的示例,英文文档看起来也比较吃力,所以这里分享一个完整的示例。下面是两种方法分别介绍纯前端实现不修改样式和修改样式时导出Excel的方法:

  1. 使用SheetJS/js-xlsx(https://github.com/SheetJS/js-xlsx#input-type)导出Excel表格。
    1. 优点:简单。
    2. 缺点:免费版不支持修改表格样式。
    3. 安装:npm install xlsx。
    4. 性能:经测试,导出30多列,几百条数据的表格比较快;上千条大概需要等待3-5秒。
    5. 补充:支持很多种类的数据解析和导出,这里仅涉及导入二维数组,导出xlsx。持续更新中,最新的更新日期是2019年8月。
    6. 实现:
      1. 引入:
        import XLSX from \'xlsx\'
      2. 输入(数据源):
        const sheetDatas = [
           [\'序号, \'姓名\', \'性别\'],
           [1, \'Lily\', \'女\'],
           [2, \'John\', \'男\'],
           [3, \'Mary\', \'女\']
        ]
      3. 调用方法:
        const wb = XLSX.utils.book_new() // 创建一个工作簿
        const ws = XLSX.utils.aoa_to_sheet(sheetDatas) // 使用二维数组创建一个工作表对象
        XLSX.utils.book_append_sheet(wb, ws, \'人员信息\') // 向工作簿追加一个工作表
        XLSX.writeFile(wb, `人员信息${moment().format(\'YYYYMMDD\')}.xlsx`) // 写入文件
      4. 输出(Excel):

         

          
  2. 使用protobi/js-xlsx(https://github.com/protobi/js-xlsx)导出Excel表格。
    1. 优点:可修改表格样式(如字体、合并单元格、颜色等)。
    2. 缺点:比较复杂。
    3. 安装:npm install xlsx-style --save。
    4. 性能:经测试,导出30多列,几百条数据的表格比较快;上千条大概需要等待3-5秒。
    5. 补充:基于SheetJS/js-xlsx开发(注意:安装依赖的时候不需要装SheetJS/js-xlsx),开发停留在2017年,后面没有更新,没有维护,SheetJS/js-xlsx中的新方法不支持。
    6. 实现:
      1. 引入:
        import XLSXStyle from \'js-xlsx\'
        1. 我是在Vue中引入的这个依赖,引入之后会报错,解决方法:
          1. 修改源码:修改xlsx-style/dist/cpexcel.js文件中的第807行
            var cpt = require(\'./cpt\' + \'able\');
            修改为 
            var cpt = cptable;
          2. 修改webpack配置,与entry同一级新增一个字段,如图:
      2. 输入:
        const sheetTitle = ["序号", "姓名", "性别"] // 表格第一行的标题

        const sheetDatas = [ // 表格内容
           [1, \'Lily\', \'女\'],
           [2, \'John\', \'男\'],
           [3, \'Mary\', \'女\']
        ]
      3. 完整代码(在Vue中的实现):
        <template>
        <div>
          <Button type="primary" @click=\'handleClick\'>
            <slot></slot>
          </Button>
        </div>  
        </template>
        <script>
          import moment from \'moment\'
          import XLSXStyle from \'js-xlsx\'
        
          export default {
            data() {
              return {
                sheetTitle: ["序号", "姓名", "性别"],
                sheetDatas: [
                  [1,\'Lily\',\'女\'],
                  [2,\'John\',\'男\'],
                  [3,\'Mary\',\'女\']
                ]
              }
            },
            methods: {
              handleClick() {
                this.downloadExl(this.sheetDatas)
              },
              downloadExl(sourceData) {
                const wopts = { bookType: \'xlsx\', bookSST: true, type: \'binary\', cellHeadStyles: true };
        
                // excel表格样式
                let fontBold = {
                  font: { 
                    sz: 10, 
                    bold: true, // 粗字体
                    color: { rgb: "000000" },
                    name: "Times New Roman" 
                  }
                }
                let fontThin = {
                  font: { 
                    sz: 10, 
                    bold: false, 
                    color: { rgb: "000000" },
                    name: "Times New Roman" 
                  }
                }
                let alignmentCenter = { alignment: { horizontal: \'center\' } } // 居中对齐
                let alignmentLeft = { alignment: { horizontal: \'left\' } }
                let borderStyle = { // 边框粗细和颜色
                  border: {
                    right: { 
                      style: \'thin\',
                      color: { rgb: "000000" } 
                    },
                    bottom: { 
                      style: \'thin\',
                      color: { rgb: "000000" } 
                    },
                  }
                }
                let cellBaseStyle = Object.assign({}, {
                  fill: { bgColor: { indexed: 64 }, fgColor: { rgb: "FFFFFF" } },
                }, borderStyle, alignmentCenter)
                let cellHeadStyle = Object.assign({}, cellBaseStyle, fontBold)
                let cellBodyStyle = Object.assign({}, cellBaseStyle, fontThin)
        
                // 数据
                let tmpdata = sourceData[0],
                    keyMap = [],
                    outputPos1 = [],
                    outputPos2 = [], 
                    noteStyle = {},
                    titleStyle = {},
                    cols = Object.keys(this.sheetDatas[0]).length
        
                if(sourceData[0] instanceof Array) sourceData.unshift({})
                for (let k in tmpdata) {
                  keyMap.push(k)
                  sourceData[0][k] = k
                }
                tmpdata = [];//用来保存转换好的sourceData
                sourceData.map((v, i) => keyMap.map((k, j) => Object.assign({}, {
                  v: v[k],
                  position: (j > 25 ? this.getCharCol(j) : String.fromCharCode(65 + j)) + (i + 1)
                }))).reduce((prev, next) => prev.concat(next)).forEach((v, i) => tmpdata[v.position] = {
                  v: v.v,
                  s: cellBodyStyle
                });
                outputPos1 = Object.keys(tmpdata) // 第一部分数据区域
                for(let i=0; i<cols; i++) {
                  let len = this.sheetDatas.length,
                      letter = i > 25 ? this.getCharCol(i) : String.fromCharCode(65 + i), // A~AI
                      noteObj = {},
                      titleObj = {}
        
                  outputPos2.push(letter+(this.sheetDatas.length+2)) // 第二部分数据区域,用于添加备注行
        
                  // 第一行标题样式 
                  titleObj = {
                    [letter+\'1\']: {
                      v: this.sheetTitle[i],
                      s: cellHeadStyle
                    }
                  }
                  Object.assign(titleStyle, titleObj)
        
                  // 最后一行备注信息样式
                  if(i == 0) { // 第一个cell
                    noteObj = {
                      [letter+(len+1)]: {
                        v: \'备注:数据生成于\' + this.sheetTime,
                        t: \'s\',
                        s: Object.assign({}, cellBodyStyle, alignmentLeft)
                      },
                    }
                  } else if(i == (cols-1)) { // 最后一个cell
                    noteObj = { [letter+(len+1)]: { s: borderStyle } }
                  } else {
                    noteObj = {
                      [letter+(len+1)]: {
                        s: {
                          border: {
                            bottom: { 
                              style: \'thin\',
                              color: { rgb: "000000" } 
                            },
                          }
                        }
                      }
                    }
                  }
                  Object.assign(noteStyle, noteObj)
                }
        
                let outputPos = [...outputPos1, ...outputPos2]  //设置区域,比如表格从A1到D10
                let tmpdataStyle = {
                  \'!merges\': [{
                    s: {c: 0, r: this.sheetDatas.length},
                    e: {c: cols-1, r: this.sheetDatas.length}
                  }],
                  \'!cols\': [ //设置列宽
                    {wpx: 45}, /*a*/  {wpx: 85}, /*b*/  {wpx: 85}
                  ]
                };
                let tmpdataAll = Object.assign({}, tmpdata, tmpdataStyle, noteStyle, titleStyle)
                let tmpWB = {
                  SheetNames: [\'人员信息\'], //保存的表标题
                  Sheets: {
                    \'人员信息\': Object.assign({}, tmpdataAll, //内容
                    {
                      \'!ref\': outputPos[0] + \':\' + outputPos[outputPos.length-1] //设置填充区域
                    })
                  }
                };
                let tmpDown = new Blob([this.s2ab(XLSXStyle.write(tmpWB,
                  { bookType: \'xlsx\', bookSST: false, type: \'binary\' } //定义导出的格式类型
                ))], { type: "" });
                this.saveAs(tmpDown, `人员信息${moment().format(\'YYYYMMDD\')}` + \'.\' + (wopts.bookType == "biff2" ? "xlsx" : wopts.bookType));
              },
              saveAs(obj, fileName) {
                let tmpa = document.createElement("a");
                tmpa.download = fileName;
                tmpa.href = URL.createObjectURL(obj);
                tmpa.click();
                setTimeout(function () {
                  URL.revokeObjectURL(obj);
                }, 100);
              },
              s2ab(s) {
                let buf = new ArrayBuffer(s.length);
                let view = new Uint8Array(buf);
                for (let i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF;
                return buf;
              },
              getCharCol(n) {
                let temCol = \'\',
                    s = \'\',
                    m = 0
                while (n > 0) {
                  m = n % 26 + 1
                  s = String.fromCharCode(m + 64) + s
                  n = (n - m) / 26
                }
                return s
              }
            },
            mounted() {}
          }
        </script>
      4. 输出: