vue+xlsx前端导出自定义多级表头

import XLSX from "xlsx-js-style";
/**
 * @author
 * @desciption: 用于嵌套表头的表格导出
 */
/**
 * @description: 边框样式
 */
const borderAll = {
  top: {
      style: 'thin',
  },
  bottom: {
      style: 'thin'
  },
  left: {
      style: 'thin'
  },
  right: {
      style: 'thin'
  }
}

/**
* @description: 默认单元格样式
*/
const defaultCellStyle = {
  /**
   * 表头区域样式配置
   */
  headerStyle: {
      border: borderAll,
      font: {name: '宋体', sz: 11, italic: false, underline: false, bold: true},
      alignment: {vertical: 'center', horizontal: 'center'},
      fill: {fgColor: {rgb: 'FFFFFF'}},
  },
  /**
   * 内容样式配置
   */
  dataStyle: {
      border: borderAll,
      font: {name: '宋体', sz: 11, italic: false, underline: false},
      alignment: {vertical: 'center', horizontal: 'left', wrapText: true},
      fill: {fgColor: {rgb: 'FFFFFF'}},
  }
}

/**
* 默认表头属性映射
* 用于从 tableHeader 对象中提取关键属性(如prop、label等)
*/
const defaultHeaderProps = {
  prop: 'prop',
  label: 'label',
  children: 'children'
}

export default class ExportExcelTool {

  constructor(options) {
      options = options ? options : {};

      /**
       * 表格数据
       */
      this.tableData = options.tableData

      /**
       * 表头结构
       */
      this.tableHeader = options.tableHeader;

      /**
       * 表头结构属性映射
       * 用于从 tableHeader 对象中提取关键属性(如prop、label等)
       */
      this.headerProps = options.headerProps || defaultHeaderProps

      /**
       * 导出文件名
       */
      this.fileName = options.fileName || 'exportExcel'

      /**
       * 工作表名称
       * @type {string}
       */
      this.sheetName = options.sheetName || 'Sheet1'

      /**
       * 表格基础样式
       */
      this.cellStyle = options.cellStyle || defaultCellStyle

      /**
       * 列宽 数组 按顺序为每一列设置宽度,未设置宽度的列默认为 columnWidth: 100
       * 例如 {0:200,2:200}  第一列宽度为200,第三列宽度为200 其余列默认
       */
      this.columnWidthsArray = options.columnWidthsArray || {}


      /**
       * 列宽 默认100
       */
      this.columnWidth = options.columnWidth || 100

      /**
       * 数据格式化方法
       * 例如:
       * formatterValue(value, header){
       *     return value
       * }
       */
      this.formatterValue = options.formatterValue
  }


  /**
   * 导出数据Excel
   */
  exportData() {
      if (!Array.isArray(this.tableHeader) || this.tableHeader.length < 1) {
          throw {type: 'error', message: '请选择需要导出的表头!'}
      }
      if (!Array.isArray(this.tableData) || this.tableData.length < 1) {
          throw {type: 'error', message: '请选择需要导出的数据!'}
      }
      let sheetName = this.sheetName
      // excel表头
      let excelHeader = this.buildHeader(this.tableHeader, this.headerProps)
      // 头部行数,用来固定表头
      let headerRows = excelHeader.length
      //
      let headerList = this.flat(this.tableHeader, this.headerProps)
      // 提取数据
      let dataList = this.extractData(this.tableData, this.headerProps, headerList)
      excelHeader.push(...dataList, [])
      // 计算合并
      let merges = this.doMerges(excelHeader)
      // 生成sheet
      let ws = this.aoa_to_sheet(excelHeader, headerRows)
      // 单元格合并
      ws['!merges'] = merges
      // 在生成sheet之后,为每一列设置宽度
      ws['!cols'] = [];
      const columnWidths = this.columnWidthsArray;
      for (let c = 0; c < headerList.length; c++) {
          // 如果没有指定宽度,则使用默认宽度
          ws['!cols'].push({wpx: columnWidths[c] || this.columnWidth});
      }
      let workbook = {
          SheetNames: [sheetName],
          Sheets: {},
      }
      workbook.Sheets[sheetName] = ws
      // excel样式
      let wopts = {
          bookType: 'xlsx',
          bookSST: false,
          type: 'binary',
          cellStyles: true,
      }
      let wbout = XLSX.write(workbook, wopts)
      let blob = new Blob([this.s2ab(wbout)], {type: 'application/octet-stream'})
      this.openDownloadXLSXDialog(blob, this.fileName + '.xlsx')
  }

  /**
   * 构建excel表头
   * @param revealList 列表页面展示的表头
   * @param headerProps
   * @returns {[]} excel表格展示的表头
   */
  buildHeader(revealList, headerProps) {
      let excelHeader = []
      // 构建生成excel表头需要的数据结构
      this.getHeader(revealList, headerProps, excelHeader, 0, 0)
      let max = Math.max(...excelHeader.map((a) => a.length))
      excelHeader.filter((e) => e.length < max).forEach((e) => this.pushRowSpanPlaceHolder(e, max - e.length))
      return excelHeader
  }

  /**
   * 生成头部
   * @param headers 展示的头部
   * @param headerProps 属性映射
   * @param excelHeader excel头部
   * @param deep 深度
   * @param perOffset 前置偏移量
   * @returns {number}  后置偏移量
   */
  getHeader(headers, headerProps, excelHeader, deep, perOffset) {
      let offset = 0
      let cur = excelHeader[deep]
      if (!cur) {
          cur = excelHeader[deep] = []
      }
      // 填充行合并占位符
      this.pushRowSpanPlaceHolder(cur, perOffset - cur.length)
      for (let i = 0; i < headers.length; i++) {
          let head = headers[i]
          cur.push(head[headerProps.label])
          if (head.hasOwnProperty(headerProps.children) && Array.isArray(head[headerProps.children]) && head[headerProps.children].length > 0) {
              let childOffset = this.getHeader(head[headerProps.children], headerProps , excelHeader, deep + 1, cur.length - 1)
              // 填充列合并占位符
              this.pushColSpanPlaceHolder(cur, childOffset - 1)
              offset += childOffset
          } else {
              offset++
          }
      }
      return offset
  }

  /**
   * 根据选中的数据和展示的列,生成结果
   * @param selectionData
   * @param headerProps
   * @param headerList
   * @returns {*[]}
   */
  extractData(selectionData, headerProps, headerList) {
      // 导出的结果集
      let excelRows = []
      // 如果有child集合的话会用到
      let dataKeys = new Set(Object.keys(selectionData[0]))
      selectionData.some((e) => {
          if (e[headerProps.children] && e[headerProps.children].length > 0) {
              let childKeys = Object.keys(e[headerProps.children][0])
              for (let i = 0; i < childKeys.length; i++) {
                  dataKeys.delete(childKeys[i])
              }
              return true
          }
      })
      this.flatData(selectionData, headerProps, (list) => {
          excelRows.push(...this.buildExcelRow(dataKeys, headerList, headerProps, list))
      })
      return excelRows
  }

  /**
   * 根据列和数据生成excel行
   * @param mainKeys
   * @param headers
   * @param headerProps
   * @param rawDataList
   * @returns {*[]}
   */
  buildExcelRow(mainKeys, headers, headerProps, rawDataList) {
      // 数据行
      let rows = []
      for (let i = 0; i < rawDataList.length; i++) {
          let cols = []
          let rawData = rawDataList[i]
          // 提取数据
          for (let j = 0; j < headers.length; j++) {
              let header = headers[j]
              // 父元素键需要行合并
              if (rawData['rowSpan'] === 0 && mainKeys.has(header[headerProps.prop])) {
                  cols.push('!$ROW_SPAN_PLACEHOLDER')
              } else {
                  let value = rawData[header[headerProps.prop]]
                  // 这里可以格式化你的数据
                  if (this.formatterValue) {
                      value = this.formatterValue(value, header)
                  }
                  // push进已经格式化了的数据
                  cols.push(value)
              }
          }
          rows.push(cols)
      }
      return rows
  }

  /**
   * @description: 需要合并的数据
   * @param {*} arr
   * @return {*}
   */
  doMerges(arr) {
      // 要么横向合并 要么纵向合并
      let deep = arr.length
      let merges = []
      for (let y = 0; y < deep; y++) {
          // 先处理横向合并
          let row = arr[y]
          let colSpan = 0
          for (let x = 0; x < row.length; x++) {
              if (row[x] === '!$COL_SPAN_PLACEHOLDER') {
                  row[x] = undefined
                  if (x + 1 === row.length) {
                      merges.push({s: {r: y, c: x - colSpan - 1}, e: {r: y, c: x}})
                  }
                  colSpan++
              } else if (colSpan > 0 && x > colSpan) {
                  merges.push({s: {r: y, c: x - colSpan - 1}, e: {r: y, c: x - 1}})
                  colSpan = 0
              } else {
                  colSpan = 0
              }
          }
      }
      // 再处理纵向合并
      let colLength = arr[0].length
      for (let x = 0; x < colLength; x++) {
          let rowSpan = 0
          for (let y = 0; y < deep; y++) {
              if (arr[y][x] === '!$ROW_SPAN_PLACEHOLDER') {
                  arr[y][x] = undefined
                  if (y + 1 === deep) {
                      merges.push({s: {r: y - rowSpan, c: x}, e: {r: y, c: x}})
                  }
                  rowSpan++
              } else if (rowSpan > 0 && y > rowSpan) {
                  merges.push({s: {r: y - rowSpan - 1, c: x}, e: {r: y - 1, c: x}})
                  rowSpan = 0
              } else {
                  rowSpan = 0
              }
          }
      }
      return merges
  }

  /**
   * 将二维数组转换为一个sheet
   * @param data
   * @param headerRows
   * @returns {{}}
   */
  aoa_to_sheet(data, headerRows) {
      const ws = {}
      const range = {s: {c: 10000000, r: 10000000}, e: {c: 0, r: 0}}
      for (let R = 0; R !== data.length; ++R) {
          // 处理每个单元格的样式
          for (let 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
              }
              /// 这里生成cell的时候,使用上面定义的默认样式
              const cell = {
                  v: data[R][C] || ''
              }
              if (R < headerRows) {
                  // 头部列表样式
                  cell.s = this.cellStyle.headerStyle
              } else {
                  // 数据列表样式
                  cell.s = this.cellStyle.dataStyle
              }
              const 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 {
                  cell.t = 's'
              }
              ws[cell_ref] = cell
          }
      }
      // console.log(ws, range)
      if (range.s.c < 10000000) {
          ws['!ref'] = XLSX.utils.encode_range(range)
      }
      return ws
  }

  /**
   * 填充行合并占位符
   * @param arr
   * @param count
   */
  pushRowSpanPlaceHolder(arr, count) {
      for (let i = 0; i < count; i++) {
          arr.push('!$ROW_SPAN_PLACEHOLDER')
      }
  }

  /**
   * 填充列合并占位符
   * @param arr
   * @param count
   */
  pushColSpanPlaceHolder(arr, count) {
      for (let i = 0; i < count; i++) {
          arr.push('!$COL_SPAN_PLACEHOLDER')
      }
  }

  /**
   * @description:铺平数组
   * @param {*} list 表格数据
   * @param headerProps
   * @param {*} eachDataCallBack 构建的行数据
   * @return {*}
   */
  flatData(list, headerProps, eachDataCallBack) {
      let resultList = []
      for (let i = 0; i < list.length; i++) {
          let data = list[i]
          let rawDataList = []
          // 每个子元素都和父元素合并成一条数据
          if (data[headerProps.children] && data[headerProps.children].length > 0) {
              for (let j = 0; j < data[headerProps.children].length; j++) {
                  delete data[headerProps.children][j].bsm
                  let copy = Object.assign({}, data, data[headerProps.children][j])
                  rawDataList.push(copy)
                  copy['rowSpan'] = j > 0 ? 0 : data[headerProps.children].length
              }
          } else {
              data['rowSpan'] = 1
              rawDataList.push(data)
          }
          resultList.push(...rawDataList)
          if (typeof eachDataCallBack === 'function') {
              eachDataCallBack(rawDataList)
          }
      }
      return resultList
  }


  /**
   * 将嵌套的列表展平为一维数组。
   * @param {Array} revealList - 嵌套的列表,其中每个元素可能包含子元素。
   * @param {Object} headerProps - 描述列表项中子元素属性的对象。预期包含一个'children'属性,指明子元素在父元素中的键名。
   * @returns {Array} 返回展平后的列表,其中不包含任何嵌套结构。
   */
  flat(revealList, headerProps) {
      let result = []
      revealList.forEach((e) => {
          // 检查当前元素是否包含子元素属性
          if (e.hasOwnProperty(headerProps.children)) {
              // 当前元素确实包含子元素
              if (e[headerProps.children]) {
                  // 子元素非空
                  if (e[headerProps.children].length > 0) {
                      // 递归展平子元素,并将结果合并到结果数组中
                      result.push(...this.flat(e[headerProps.children], headerProps))
                  } else {
                      // 子元素为空,将当前元素添加到结果数组
                      result.push(e)
                  }
              } else {
                  // 子元素属性存在,但值为空,将当前元素添加到结果数组
                  result.push(e)
              }
          } else if (e.hasOwnProperty(headerProps.prop)) {
              // 如果元素不包含子元素属性但包含'code'属性,将其添加到结果数组
              result.push(e)
          }
      })
      return result
  }



  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
  }

  /**
   * 打开下载XLSX文件的对话框
   * @param {string|Blob} url - 文件的URL或者Blob对象
   * @param {string} saveName - 保存文件时的名称,默认为空字符串
   */
  openDownloadXLSXDialog(url, saveName) {
      if (typeof url == 'object' && url instanceof Blob) {
          // 如果url是Blob对象,则创建一个blob URL
          url = URL.createObjectURL(url)
      }
      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) // 触发链接的点击事件,开始下载
  }

}

exportHandle() {
   
      const exportExcelTool = new ExportExcelTool({
        tableData: this.tableData, // 必须
        tableHeader: _titleTable,// 必须
        // 下面参数都有默认值,可选择修改
        fileName: this.tableTitle,
        sheetName: "学生信息",
        columnWidth: 80,
        columnWidthsArray: {2: 150, 3: 200, 6: 200},
        headerProps: {
          prop: "prop",
          label: "label",
          children: "children",
        },
        cellStyle: {
          /**
           * 表头区域样式配置
           */
          headerStyle: {
            border: {top: {style: 'thin',}, bottom: {style: 'thin'}, left: {style: 'thin'}, right: {style: 'thin'}},
            font: {name: '宋体', sz: 13, italic: false, underline: false, bold: true},
            alignment: {vertical: 'center', horizontal: 'center'},
            fill: {fgColor: {rgb: '719aad'}},
          },
          /**
           * 内容样式配置
           */
          dataStyle: {
            border: {top: {style: 'thin',}, bottom: {style: 'thin'}, left: {style: 'thin'}, right: {style: 'thin'}},
            font: {name: '宋体', sz: 11, italic: true, underline: false},
            alignment: {vertical: 'center', horizontal: 'left', wrapText: true},
            fill: {fgColor: {rgb: 'ceffca'}},
          }
        },
        /**
         * 数据格式化
         * @param value 值
         * @param header 表头信息
         */
        formatterValue(value, header) {
          if (header.prop === 'date') {
            return value.replace(/-/g, "/")
          }
          return value
        }
      })
      // 调用方法导出 excel
      exportExcelTool.exportData()
    }

只是做一个记录 前端导出xlsx 并学习

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值