写在开头
话说某天开需求会,其中某个需求涉及到导出的,页面有两个table,其中上面的table有合并单元格;因为需求紧急,页面又不需要分页,最后一致决定由前端来实现这个导出功能(6个后端对一个前端,没办法o(╥﹏╥)o);为了快速实现功能上线,决定用页面Html来导出execl,头发又掉了不少。。。,页面大概长这样子:
具体实现过程
之前没有搞过用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. 合并单元格会丢失边框
经排查,发现合并单元格之后数据格子会没有了,只需要你把格子数据补回来就行,比如下面的图缺少了B3,然后把这个B3数据补一下
最后
安装库
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']
})
效果如下: