结合xlsx-style导出样式好看的表格

591 阅读3分钟

写在开头

话说某天开需求会,其中某个需求涉及到导出的,页面有两个table,其中上面的table有合并单元格;因为需求紧急,页面又不需要分页,最后一致决定由前端来实现这个导出功能(6个后端对一个前端,没办法o(╥﹏╥)o);为了快速实现功能上线,决定用页面Html来导出execl,头发又掉了不少。。。,页面大概长这样子:

image.png

具体实现过程

之前没有搞过用html导出execl,之前都是用数据处理逻辑实现导出的;然后在网上查了一下,发现file-saver+xlsx可以用dom来导出,说干就干。

虽然确实是可以导出了,但是导出效果太素了,不好看;然后再去度娘,发现xlsx-style这个库可以加样式,天助我也,但是xlsx-style有好几个问题,最后都一一解决了。

常见问题

1. 网上说要改源码,但是我这边没有涉及到改源码,不知是不是版本问题,还是官方已修复?
  • "vue": "^2.6.14"
  • "xlsx": "^0.17.5"
  • "xlsx-style": "^0.8.13"
  • "file-saver": "^2.0.5"
2. jszip 不是构造函数

TypeError: jszip is not a constructor at zip_read

安装node-polyfill-webpack-plugin, npm install node-polyfill-webpack-plugin

然后在vue.config.js引进这个库

const NodePolyfillPlugin = require("node-polyfill-webpack-plugin")
module.exports = {
    plugins: [
        new NodePolyfillPlugin()
    ]
}
3. vue引入xlsx-style一直报错找不到“fs”
Module not found: 
Error: Can't resolve 'fs' in

在vue.config.js添加如下代码

module.exports = {
    configureWebpack: {
      resolve: { 
          fallback: {
            fs: false,
            crypto: false
          } 
      },
      externals: {
          './cptable': 'var cptable'
      },
    }
}
4. 合并单元格会丢失边框

image.png 经排查,发现合并单元格之后数据格子会没有了,只需要你把格子数据补回来就行,比如下面的图缺少了B3,然后把这个B3数据补一下

image.png

最后

安装库

npm install file-saver
npm install xlsx
npm install xlsx-style

封装代码,tableDomExport.js

import FileSaver from 'file-saver'
import XLSX from 'xlsx'
import XLSXS from 'xlsx-style'

function s2ab(s) {
  let cuf
  let i
  if (typeof ArrayBuffer !== 'undefined') {
    cuf = new ArrayBuffer(s.length)
    const view = new Uint8Array(cuf)
    for (i = 0; i !== s.length; i++) {
      view[i] = s.charCodeAt(i) & 0xff
    }
    return cuf
  } else {
    cuf = new Array(s.length)
    for (i = 0; i !== s.length; ++i) {
      cuf[i] = s.charCodeAt(i) & 0xff
    }
    return cuf
  }
}

function setExlStyle(data, style, boldHead, removeCells) {
  const { 
    borderType = 'thin',
    horizontal = 'center',
    vertical = 'center',
    wrapText = true,
    numFmt = 0,
    headBold = true, // 头部加粗
    cellBold = false,
    isHeadFgColor = true,
    headFgColor = 'eeeeef', // 头部背景颜色
    isCellFgColor = false,
    cellFgColor = '',
    wpx = 100
  } = style
  let borderAll = {
    // 单元格外侧框线
    top: {
      style: borderType
    },
    bottom: {
      style: borderType
    },
    left: {
      style: borderType
    },
    right: {
      style: borderType
    }
  }
  data['!cols'] = []
  for (let key in data) {
    if (data[key] instanceof Object) {
      const s = {
        border: borderAll,
        alignment: {
          horizontal, // 水平居中对其,
          vertical,
          wrapText
        },
        numFmt,
        font: {
          bold: cellBold
        }
      }
      if (isCellFgColor) {
        s.fill = {
          fgColor: { rgb: cellFgColor }
        }
      }
      if (/^[A-Z]{1,}\d{1,}$/.test(key)) {
        if (boldHead.includes(data[key].v)) {
          if (headBold) {
            s.font = {
              bold: true
            }
          }
          if (isHeadFgColor) {
            s.fill = {
              fgColor: { rgb: headFgColor }
            }
          }
        }
      }
      data[key].s = s
      data['!cols'].push({ wpx })
    }
  }

  const letterObj = {}
  for (let key in data) {
    if (/^[A-Z]{1,}\d{1,}$/.test(key)) {
      const mathchLet = String(key).match(/^[A-Z]{1,}/)
      let cutNum = 0
      let firstLet = ''
      if (mathchLet && mathchLet[0]) {
        cutNum = mathchLet[0].length
        firstLet = mathchLet[0]
      }
      if (!letterObj[firstLet]) {
        letterObj[firstLet] = []
      }
      letterObj[firstLet].push(Number(String(key).substring(cutNum)))
    }
  }
  // eslint-disable-next-line guard-for-in
  for (let lkey in letterObj) {
    const letArr = letterObj[lkey]
    const maxNum = Math.max(...letArr)
    for (let i = 0; i < maxNum; i++) {
      const coor = lkey + i
      if (!data[coor] && !removeCells.includes(coor)) {
        data[coor] = {
          s: {
            border: borderAll
          }
        }
      }
    }
  }
  return data
}

function defaultStyle() {
  return {
    borderType: 'thin', // 边框类型;参考:https://www.npmjs.com/package/xlsx-style/v/0.8.13
    horizontal: 'center',
    vertical: 'center',
    wrapText: true,
    numFmt: 0,
    headBold: true, // 头部加粗
    cellBold: false,
    isHeadFgColor: true,
    headFgColor: 'eeeeef', // 头部背景颜色
    isCellFgColor: false,
    cellFgColor: '',
    wpx: 100 // 每列的宽度
  }
}
/*
 * @function dom导出excel的方法
 * @param el 节点,可以传id或class;字符串
 * @param fileName 导出excel文件名;字符串
 * @param actionBtnClass 如果table有操作按钮,然后不想也导出,可以把操作这一列的class传进来;字符串
 * @param style 设置一些样式;对象
 * @param boldHead 需求加粗的头部;数组,如: ['张思','李四']
 * @param removeCells 需求移除格子的边框,execl坐标;数组,如:['A2', 'A3']
 */
export const exportExcel = ({
  el,
  fileName = new Date().getTime(),
  actionBtnClass,
  style = {},
  boldHead = [], // 头部需要加粗的
  removeCells = [] // 需要移除那些格子的边框
}) => {
  if (!el) return
  let divDom = document.createElement('div')
  divDom.style.position = 'absolute'
  divDom.style.left = '-100%'
  const cloneDom = document.querySelector(el).cloneNode(true)
  if (actionBtnClass) {
    cloneDom.querySelectorAll(actionBtnClass).forEach((item) => {
      item.remove()
    })
  }
  // table头部滚动条
  const gutter = cloneDom.querySelector('.gutter')
  if (gutter) {
    gutter.remove()
  }
  divDom.innerHTML = cloneDom.innerHTML
  let wb = XLSX.utils.table_to_book(divDom)
  const newStyle = Object.assign(defaultStyle(), style)
  setExlStyle(wb['Sheets']['Sheet1'], newStyle, boldHead, removeCells)
  // 得到二进制字符串作为输出
  var wbout = XLSXS.write(wb, { bookType: 'xlsx', type: 'binary' })
  FileSaver.saveAs(
    new Blob([s2ab(wbout)], { type: 'application/octet-stream' }),
    `${fileName}.xlsx`
  )
  divDom.remove()
  divDom = null
}

调用

import { exportExcel } from '@/utils/tableDomExport'
exportExcel({
    el: '.table',
    fileName: '我是个execl',
    actionBtnClass: '.td-action',
    boldHead: [
      '饭堂',
      '退热贴 ',
      '人头',
      '退热贴',
      '特',
      '退热贴',
      '让他人',
      '热天',
      '热天',
      '尔特',
      '让他人',
      '热天',
      '热天',
      '热天',
      '突然',
      '热天',
      '让他人',
      '热天'
    ],
    removeCells: ['A5', 'A6', 'B5', 'B6', 'C5', 'C6']
})

效果如下:

image.png