前端vue导出Excel(多sheet)

场景:

当导出文件的时候遇到结构复杂的表格(如:表头合并);或者数据分页的时候,后端就很难操作了,难以保持表格原有结构。所以就需要前端导出。

步骤如下:

1.组装数据
2.标记表头合并项,计算合并项
3.添加样式
4.转buffer,file-saver导出文件

组装数据

所谓组装数据的意义在于,手动决定他的结构
举个例子
表格外观是这样的

前端vue导出Excel(多sheet)_第1张图片

打印一下导出的数据结构是这样的

前端vue导出Excel(多sheet)_第2张图片

分别查看
这个!cols数组是每一列的宽度(第一行每一格的宽度)

前端vue导出Excel(多sheet)_第3张图片

这个!merges是表格所有的合并项

前端vue导出Excel(多sheet)_第4张图片

自行组装数据:
一个完整的例子,表格如图:

前端vue导出Excel(多sheet)_第5张图片

导出后的样子:

这个数据组装的有点问题,因为没那么多数据,应该是横着排的。主要是能填充数据,能导出,而且样式正常,就达到了目的,数据格式再慢慢调整

前端vue导出Excel(多sheet)_第6张图片

完整代码如下
安装:
npm install --save xlsx file-saver
npm install --save xlsx-style

vue:
<template>
  <div>
    <el-button type="primary" @click="setExport2Excel" icon="el-icon-download"
      >导出</el-button
    >
    <el-table
      :data="tableData3"
      :id="'mytable'"
      border
      style="width: 100%">
      <el-table-column
        prop="date"
        label="门架"
        width="150">
      </el-table-column>
      <el-table-column
        prop="date"
        label="识别车型"
        width="150">
      </el-table-column>
      <el-table-column label="车型不一致">
        <el-table-column
          prop="province"
          label="问题车数量"
          width="120">
        </el-table-column>
        <el-table-column
          prop="city"
          label="差额"
          width="120">
        </el-table-column>
      </el-table-column>
      <el-table-column label="交易缺失">
        <el-table-column
          prop="province"
          label="问题车数量"
          width="120">
        </el-table-column>
        <el-table-column
          prop="city"
          label="差额"
          width="120">
        </el-table-column>
      </el-table-column>
      <el-table-column label="清分缺失">
        <el-table-column
          prop="province"
          label="问题车数量"
          width="120">
        </el-table-column>
        <el-table-column
          prop="city"
          label="差额"
          width="120">
        </el-table-column>
      </el-table-column>
      <el-table-column
        prop="date"
        label="差额统计"
        width="150">
      </el-table-column>
      <el-table-column label="配送信息">
        <el-table-column
          prop="name"
          label="姓名"
          width="120">
        </el-table-column>
        <el-table-column label="地址">
          <el-table-column
            prop="province"
            label="省份"
            width="120">
          </el-table-column>
          <el-table-column
            prop="city"
            label="市区"
            width="120">
          </el-table-column>
        </el-table-column>
      </el-table-column>
    </el-table>
  </div>
 
</template>

<script>
  // import FileSaver from 'file-saver'
  import excel from './export2excel';
  export default {
    data() {
      return {
        tableData3: [{
          date: '2016-05-03',
          name: '王小虎',
          province: '上海',
          city: '普陀区',
          address: '上海市普陀区金沙江路 1518 弄',
          zip: 200333
        }, {
          date: '2016-05-02',
          name: '王小虎',
          province: '上海',
          city: '普陀区',
          address: '上海市普陀区金沙江路 1518 弄',
          zip: 200333
        }, {
          date: '2016-05-04',
          name: '王小虎',
          province: '上海',
          city: '普陀区',
          address: '上海市普陀区金沙江路 1518 弄',
          zip: 200333
        }, {
          date: '2016-05-01',
          name: '王小虎',
          province: '上海',
          city: '普陀区',
          address: '上海市普陀区金沙江路 1518 弄',
          zip: 200333
        }, {
          date: '2016-05-08',
          name: '王小虎',
          province: '上海',
          city: '普陀区',
          address: '上海市普陀区金沙江路 1518 弄',
          zip: 200333
        }, {
          date: '2016-05-06',
          name: '王小虎',
          province: '上海',
          city: '普陀区',
          address: '上海市普陀区金沙江路 1518 弄',
          zip: 200333
        }, {
          date: '2016-05-07',
          name: '王小虎',
          province: '上海',
          city: '普陀区',
          address: '上海市普陀区金沙江路 1518 弄',
          zip: 200333
        }, {
          date: '2016-05-07',
          name: '王小虎',
          province: '上海',
          city: '普陀区',
          address: '上海市普陀区金沙江路 1518 弄',
          zip: 200333
        }, {
          date: '2016-05-07',
          name: '王小虎',
          province: '上海',
          city: '普陀区',
          address: '上海市普陀区金沙江路 1518 弄',
          zip: 200333
        }, {
          date: '2016-05-07',
          name: '王小虎',
          province: '上海',
          city: '普陀区',
          address: '上海市普陀区金沙江路 1518 弄',
          zip: 200333
        }, {
          date: '2016-05-07',
          name: '王小虎',
          province: '上海',
          city: '普陀区',
          address: '上海市普陀区金沙江路 1518 弄',
          zip: 200333
        }, {
          date: '2016-05-07',
          name: '王小虎',
          province: '上海',
          city: '普陀区',
          address: '上海市普陀区金沙江路 1518 弄',
          zip: 200333
        }]
      }
    },
    methods:{
      setExport2Excel(){
        // 接口请求回来的数据
        let value = [[],[],[],[],[],[],[]]
        //把每个格子的数据结构出来  一行一行的排列
        this.tableData3.forEach((item,index)=>{
          value[0].push(item.date)
          value[1].push(item.name)
          value[2].push(item.province)
          value[3].push(item.city)
          value[4].push(item.address)
          value[5].push(item.zip)
        })
        //表示最后一行表头中,保持原状,没有进行合并的表头
        let header=[
          '',
          '',
          '',
          '',
          '',
          '',
          '',
          '',
          '',
          '',
          '省份',
          '市区',
        ]
        //文件名
        let filename= "2022年01月12日-报表"
        //包括标题在内的,上面几行,进行合并的表头
        let multiHeader = [
          ['第一个报表',
          '',
          '',
          '',
          '',
          '',
          '',
          '',
          '',
          '',
          '',
          '',],
          [
            '门架',
            '识别车型',
            '车型不一致',
            '',
            '交易缺失',
            '',
            '清分缺失',
            '',
            '差额统计',
            '配送信息',
            '',
            '',
          ],
          [
            '',
            '',
            '问题车数量',
            '差额',
            '问题车数量',
            '差额',
            '问题车数量',
            '差额',
            '差额统计',
            '姓名',
            '地址',
            ''
          ],
        ]
        //表示所有进行合并的项的起始
        let merges = [
          'A1:L1',
          'C2:D2',
          'E2:F2',
          'G2:H2',
          'J2:L2',
          'A2:A4',
          'B2:B4',
          'C3:C4',
          'D3:D4',
          'E3:E4',
          'F3:F4',
          'G3:G4',
          'H3:H4',
          'I2:I4',
          'J3:J4',
          'K3:L3',
        ]
        //sheet名
        let sheetname = "报表一"
        //表格宽度
        let mch = 12

        
        let obj1 = {
          data:value,
          header,
          filename,
          multiHeader,
          merges,
          sheetname,
          mch
        }
        let obj2 = {
          data:value,
          header,
          filename,
          multiHeader,
          merges,
          sheetname:'报表二',
          mch
        }
        console.log('看下结构')
        //这里数组,那么同理就可以随心所欲组装各种报表数据进行导出,岂不是很快活
        excel([obj1,obj2])
      }
    }
  }
</script>



export2excel.js
/* eslint-disable */
import {saveAs} from 'file-saver'
import XLSX from 'xlsx-style'

function datenum(v, date1904) {
  if (date1904) v += 1462;
  var epoch = Date.parse(v);
  return (epoch - new Date(Date.UTC(1899, 11, 30))) / (24 * 60 * 60 * 1000);
}

//这个方法把单个数据与表格坐标对应,也标记了内容的类型
function sheet_from_array_of_arrays(data, opts) {
  var ws = {};
  var range = {
    s: {
      c: 10000000,
      r: 10000000
    },
    e: {
      c: 0,
      r: 0
    }
  };
  for (var R = 0; R != data.length; ++R) {
    for (var C = 0; C != data[R].length; ++C) {
      if (range.s.r > R) range.s.r = R;
      if (range.s.c > C) range.s.c = C;
      if (range.e.r < R) range.e.r = R;
      if (range.e.c < C) range.e.c = C;
      var cell = {
        v: data[R][C]
      };
      if (cell.v == null) continue;
      var cell_ref = XLSX.utils.encode_cell({
        c: C,
        r: R
      });

      if (typeof cell.v === 'number') cell.t = 'n';
      else if (typeof cell.v === 'boolean') cell.t = 'b';
      else if (cell.v instanceof Date) {
        cell.t = 'n';
        cell.z = XLSX.SSF._table[14];
        cell.v = datenum(cell.v);
      }
      else cell.t = 's';
      ws[cell_ref] = cell;
    }
  }
  if (range.s.c < 10000000) ws['!ref'] = XLSX.utils.encode_range(range);
  return ws;
}

function Workbook() {
  if (!(this instanceof Workbook)) return new Workbook();
  this.SheetNames = [];
  this.Sheets = {};
}

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;
}

let excel = function export_json_to_excel(excelList) {

  // 兼容原来的对象写法
  // 多表格是数组格式
  if (!Array.isArray(excelList)) {
    excelList = [excelList, excelList]
  }
  var wb = new Workbook(),ws = [],bookType='xlsx',blobResult = excelList[0].blobResult,filename = excelList[0].filename || '列表';

  excelList.forEach((excelData, index) => {
    let multiHeader = excelData.multiHeader || [], // 第二行表头
    wch = excelData.wch,
    describe = excelData.describe, // 最后一行设置特殊样式
    header = excelData.header,	// 第四行表头
    sheetname = excelData.sheetname, //需要多填写一个sheet名
    merges = excelData.merges || [], // 合并
    autoWidth = true
    /* original data */
    //接口数据   包含数据首列行名  
    let data = [...excelData.data]
    //把表头插入
    data.unshift(header);
    //把上面几行包含标题在内的几行合并表格的表头插入
    if (multiHeader.length && multiHeader[0].length > 1) {
      for (let i = multiHeader.length - 1; i > -1; i--) {
        data.unshift(multiHeader[i])
      }
    }
    //sheet_from_array_of_arrays这个方法使所有数据与excel坐标对应   以及内容类型 
    // A1: {v: '2022年01月12日-每日稽核报表', t: 's'}  
    // A2: {v: '时间/类型', t: 's'}
    // A3: {v: '', t: 's'}
    // A4: {v: 20220112, t: 'n'}
    ws.push(sheet_from_array_of_arrays(data))
    //这里还没有去标记合并项  只是列出待合并区域为''
    if (merges.length > 0) {
      if (!ws[index]['!merges']) ws[index]['!merges'] = [];
      merges.forEach(item => {
        //把自己列出的合并规则  转换为坐标系
        //'A1:AD1'    这就是标题行  从第一格到29个
        // =>
        // {
        //   e: {c: 29, r: 0}    结束点
        //   s: {c: 0, r: 0}     起始点
        // }   
        ws[index]['!merges'].push(XLSX.utils.decode_range(item))
      })
    }
    if (autoWidth) {
      /*设置worksheet每列的最大宽度*/
      const colWidth = data.map(row => row.map(val => {
        if (wch) {
          return {
            'wch': wch
          }
        } else {
          /*先判断是否为null/undefined*/
          if (val == null) {
            return {
              'wch': 10
            };
          }
          /*再判断是否为中文*/
          else if (val.toString().charCodeAt(0) > 255) {
            return {
              'wch': val.toString().length * 2 > 10 ? val.toString().length * 2 : 10
            };
          } else {
            return {
              'wch': val.toString().length > 10 ? val.toString().length : 10
            };
          }
        }
      }))
      /*以第一行为初始值*/
      //标记每一个格子的宽度
      // colWidth是个数组   表格多少行他就有多少个元素  每个元素也是数组,存放这一行中所有格子的宽度
      // [[{wch:12},{wch:12}],[],...]
      //但是呢基本上表格十分规则  第一行就可以决定表格宽度了  所以这里取0就得了
      let result = colWidth[0];
      for (let i = 1; i < colWidth.length; i++) {
        for (let j = 0; j < colWidth[i].length; j++) {
          if (result[j]['wch'] < colWidth[i][j]['wch']) {
            result[j]['wch'] = colWidth[i][j]['wch'];
          }
        }
      }
      ws[index]['!cols'] = result;
    }

    /* add worksheet to workbook */
    // for (var k = 0; k < sheetname.length; k++) {
    //   wb.SheetNames.push(sheetname[k])
    //   wb.Sheets[sheetname[k]] = ws[k]
    // }
    /* add worksheet to workbook */
    var ws_name = sheetname || ('Sheet' + (index +1))
    wb.SheetNames.push(ws_name);
    //这里就分sheet了
    wb.Sheets[ws_name] = ws[index];
    var dataInfo = wb.Sheets[wb.SheetNames[index]];
    // 设置单元格框线
    const borderAll = {
      top: {
        style: "thin"
      },
      bottom: {
        style: "thin"
      },
      left: {
        style: "thin"
      },
      right: {
        style: "thin"
      }
    };
    // 给所有单元格加上边框,内容居中,字体,字号,标题表头特殊格式部分后面替换
    for (var i in dataInfo) {
      if (
        i == "!ref" ||
        i == "!merges" ||
        i == "!cols" ||
        i == "!rows" ||
        i == "A1"
      ) { } else {
        dataInfo[i + ""].s = {
          border: borderAll,
          alignment: {
            horizontal: "center",
            vertical: "center"
          },
          font: {
            name: "微软雅黑",
            sz: 10
          }
        };
      }
    }

    // 设置表格样式
    let arrabc = []
    const abc = ["A",
      "B",
      "C",
      "D",
      "E",
      "F",
      "G",
      "H",
      "I",
      "J",
      "K",
      "L",
      "M",
      "N",
      "O",
      "P",
      "Q",
      "R",
      "S",
      "T",
      "U",
      "V",
      "W",
      "X",
      "Y",
      "Z"
    ]
    let arrabcLength = data[0].length
    for (let i = 0;i < arrabcLength;i++) {
      let mergesNum = parseInt(i / 26)
      let l = abc[mergesNum-1] ? abc[mergesNum-1] : ''
      arrabc.push(l+abc[i % 26])
    }

    //这个arrabc算是给第一行标题行标记excel字母坐标

    // 给标题、表格描述信息、表头等部分加上特殊格式  居中之类的
    arrabc.some(function (v, index) {
      describe && describe.forEach((d) => {
        let alignment = {
          horizontal: "left",
          vertical: "center"
        }
        if (index === 0) {
          alignment = {
            horizontal: "center",
            vertical: "center"
          }
        }
        dataInfo[v + d].s = {
          border: borderAll,
          font: {
            name: "微软雅黑",
            sz: 10,
          },
          alignment: alignment
        }
      })
      for (let j = 1; j < multiHeader.length + 2; j++) {
        const _v = v + j
        if (dataInfo[_v]) {
          dataInfo[_v].s = {};
          // 标题部分A1-Z1
          if (j == 1) {
            dataInfo[v + j].s = {
              font: {
                name: "微软雅黑",
                sz: 12,
                color: {
                  rgb: "000000"
                },
                bold: true,
                italic: false,
                underline: false
              },
              alignment: {
                horizontal: "center",
                vertical: "center"
              }
            };
          } else {
            // 表头部分,根据表头特殊格式设置
            if (multiHeader.length == 0) {
              // multiHeader.length = 0 时表头没有合并单元格,表头只占1行A2-Z2

              const fv = v + (multiHeader.length + 2)
              dataInfo[fv].s = {
                border: borderAll,
                font: {
                  name: "微软雅黑",
                  sz: 11,
                  bold: true
                },
                alignment: {
                  horizontal: "center",
                  vertical: "center"
                },
                fill: {
                  fgColor: {
                    rgb: "f0f0f0"
                  },
                },
              }
            } else if (multiHeader.length == 1) {
              // multiHeader.length = 0 时表头有合并单元格,表头只占2行A2-Z2,A3-Z3,这是没有描述信息只有表头合并的
              dataInfo[v + j].s = {
                border: borderAll,
                font: {
                  name: "微软雅黑",
                  sz: 11,
                },
                alignment: {
                  horizontal: "center",
                  vertical: "center"
                },
                fill: {
                  fgColor: {
                    rgb: "f0f0f0"
                  }
                },
              }
            } else {
              // multiHeader.length = 0 时表头有合并单元格,表头多行
              dataInfo[v + j].s = {
                border: borderAll,
                font: {
                  name: "微软雅黑",
                  sz: 10,
                },
                alignment: {
                  horizontal: "center",
                  vertical: "center"
                }
              }
            }
          }
          // multiHeader.length + 2 是表头的最后1行
          // if (!dataInfo[v + (multiHeader.length + 2)]) return
          // dataInfo[v + (multiHeader.length + 2)].s = {
          //   border: borderAll,
          //   font: {
          //     name: "微软雅黑",
          //     sz: 10,
          //   },
          //   alignment: {
          //     horizontal: "center",
          //     vertical: "center"
          //   },
          //   fill: {
          //     fgColor: {
          //       rgb: "f0f0f0"
          //     }
          //   },
          // }
        }
      }
    });
  })

  console.log('咋回事',wb)
  //循环之后使用插件写入文件数据
  var wbout = XLSX.write(wb, {
    bookType: bookType,
    bookSST: false,
    type: 'binary'
  });
  //转buffer然后bold导出
  if (blobResult) {
    return new Blob([s2ab(wbout)], {
      type: "application/octet-stream"
    })
  } else {
    saveAs(new Blob([s2ab(wbout)], {
      type: "application/octet-stream"
    }), `${filename}.${bookType}`);
  }
}
export default excel

你可能感兴趣的:(vue,javascript,前端,vue.js,javascript)