前端导出excel文件多级表头
目录
前端导出excel文件(多级表头)
采用的是vue3的写法 需要安装 file-saver 和 pikaz-xlsx-style
以下是自定义的导出模板模板样式
<template>
<div class="excel-export" @click="!manual&&exportExcel()">
<slot></slot>
</div>
</template>
<script>
import {saveAs} from 'file-saver'
import XLSX from 'pikaz-xlsx-style'
export default {
props: {
// 文件类型
bookType: {
type: String,
default: 'xlsx'
},
// 文件名
filename: {
type: String,
default: 'excel'
},
// 是否手动导出
manual: {
type: Boolean,
default: false
},
// 表格配置
sheet: {
type: Array,
default: () => {
return []
}
},
// 处理数据前
beforeStart: {
type: Function,
// bookType:文件类型,filename:文件名,sheet:表格数据
default: (bookType, filename, sheet) => {
console.log(bookType, filename, sheet)
}
},
// 导出前
beforeExport: {
type: Function,
// filename:文件名,sheet:表格数据,blob:文件流
default: (bookType, filename, blob) => {
console.log(bookType, filename, blob)
}
},
// 导出错误
onError: {
type: Function,
// err:错误信息
default: (err) => {
return err
}
}
},
components: {},
data() {
return {
// 默认配置
default: {
sheetName: 'sheet',
globalStyle: {
border: {
top: {
style: 'thin',
color: {rgb: "000000"}
},
bottom: {
style: 'thin',
color: {rgb: "000000"}
},
left: {
style: 'thin',
color: {rgb: "000000"}
},
right: {
style: 'thin',
color: {rgb: "000000"}
}
},
font: {
name: '宋体',
sz: 12,
color: {rgb: "000000"},
bold: false,
italic: false,
underline: false,
shadow: false
},
alignment: {
horizontal: "center",
vertical: "center",
wrapText: false
},
fill: {
fgColor: {rgb: "ffffff"},
}
},
},
// 枚举类
enum: {
// 文件类型
bookType: ['xlsx', 'xls']
}
}
},
created() {
},
mounted() {
},
methods: {
/**
* @name: 导出excel函数
* @param {type}
* @return:
*/
pikaExportExcel() {
this.$nextTick(() => {
this.exportExcel()
})
},
/**
* @name:导出excel
* @param {type}
* @return:
*/
exportExcel() {
// 处理数据前
// const beforeStart = this.beforeStart(this.bookType, this.filename, this.sheet)
// if (beforeStart === false) {
// return
// }
if (!this.sheet || this.sheet.length <= 0) {
this.onError('Table data cannot be empty')
return
}
const wb = this.Workbook()
this.sheet.forEach((item, index) => {
let {
// 标题
title,
// 表头
tHeader,
// 多级表头
multiHeader,
// 表格数据
table,
// 合并项
merges,
// 数据键值
keys,
// 列宽
colWidth,
// 表名
sheetName,
// 全局样式
globalStyle,
// 单元格样式
cellStyle
} = item
sheetName = sheetName || this.default.sheetName
// 默认全局样式覆盖
const dgStyle = this.default.globalStyle
if (globalStyle) {
Object.keys(dgStyle).forEach(key => {
globalStyle[key] = {...dgStyle[key], ...globalStyle[key]}
})
} else {
globalStyle = dgStyle
}
// 处理标题格式
if (title || title === 0 || title === '') {
// 取表头、多级表头中的最大值
const tHeaderLength = tHeader && tHeader.length || 0
const multiHeaderLength = multiHeader && Math.max(...multiHeader.map(m => m.length)) || 0
const titleLength = Math.max(tHeaderLength, multiHeaderLength, keys.length)
// 第一个元素为title,剩余以空字符串填充
title = [title].concat(Array(titleLength - 1).fill(''))
// 处理标题的合并\
const cell = ['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 mergeSecond = 'A1'
if (titleLength > 26) {
const one = parseInt(titleLength / 26)
const two = titleLength % 26
mergeSecond = cell[one - 1] + cell[two - 1] + '1'
} else {
mergeSecond = cell[titleLength - 1] + '1'
}
const titleMerge = `A1:${mergeSecond}`
if (!merges) {
merges = [titleMerge]
} else {
if (merges.indexOf(titleMerge) === -1) {
merges.push(titleMerge)
}
}
}
//表头对应字段
let data = table.map(v => keys.map(j => v[j]))
//最前面的表头
tHeader && data.unshift(tHeader);
// 多级表头 二级表头
if (multiHeader) {
// 倒序循环
for (let i = multiHeader.length - 1; i >= 0; i--) {
data.unshift(multiHeader[i]);
}
}
title && data.unshift(title);
const ws = this.sheet_from_array_of_arrays(data);
if (merges && merges.length > 0) {
if (!ws['!merges']) ws['!merges'] = [];
merges.forEach(merge => {
ws['!merges'].push(XLSX.utils.decode_range(merge))
})
}
// colWidth=[32,32,32,32,32] //各列的宽度
// 如果没有列宽则自适应
if (!colWidth) {
// 基准比例,以12为标准
const benchmarkRate = globalStyle.font.sz && globalStyle.font.sz / 12 || 1
// 空字符长度
const nullstr = 10 * benchmarkRate + 2
// 单个中文字符长度
const chinese = 2 * benchmarkRate
// 单个非中文字符长度
const nChinese = benchmarkRate
//设置worksheet每列的最大宽度,并+2调整一点列宽
const sheetColWidth = data.map(row => row.map(val => {
//先判断是否为null/undefined
if (!val) {
return {
'wch': nullstr
};
} else {
const strArr = val.toString().split('')
const pattern = new RegExp("[\u4E00-\u9FA5]+")
let re = strArr.map(str => {
// 是否为中文
if (pattern.test(str)) {
return chinese
} else {
return nChinese
}
})
re = re.reduce((total, r) => total + r, 0)
return {
'wch': re + 10
};
}
}))
/*以第一行为初始值*/
let result = sheetColWidth[0];
for (let i = 1; i < sheetColWidth.length; i++) {
for (let j = 0; j < sheetColWidth[i].length; j++) {
if (result[j]['wch'] < sheetColWidth[i][j]['wch']) {
result[j]['wch'] = sheetColWidth[i][j]['wch'];
}
}
}
ws['!cols'] = result;
} else {
ws['!cols'] = colWidth.map(i => {
return {wch: i}
})
}
// 添加工作表
wb.SheetNames.push(sheetName);
wb.Sheets[sheetName] = ws;
let dataInfo = wb.Sheets[wb.SheetNames[index]];
//全局样式
(function () {
Object.keys(dataInfo).forEach(i => {
if (i == '!ref' || i == '!merges' || i == '!cols') {
// console.log(i);
} else {
dataInfo[i.toString()].s = globalStyle;
}
});
})();
// 单个样式
(function () {
if (!cellStyle || cellStyle.length <= 0) {
return
}
cellStyle.forEach(s => {
const {border, font, alignment, fill} = s;
dataInfo[s.cell].s = {
border: border === {} ? border : border || globalStyle.border,
font: font || globalStyle.font,
alignment: alignment || globalStyle.alignment,
fill: fill || globalStyle.fill
}
});
})();
})
// 类型默认为xlsx
let bookType = this.enum.bookType.filter(i => i === this.bookType)[0] || this.enum.bookType[0];
this.writeExcel(wb, bookType, this.filename)
},
/**
* @name: workbook对象
* @param {type}
* @return:
*/
Workbook() {
class WB {
constructor() {
this.SheetNames = [];
this.Sheets = {};
}
}
return new WB()
},
/**
* @name: 导出excel文件
* @param {type}
* @return:
*/
writeExcel(wb, bookType, filename) {
const wbout = XLSX.write(wb, {
bookType: bookType,
bookSST: false,
type: 'binary'
});
const blob = new Blob([this.s2ab(wbout)], {
type: "application/octet-stream"
})
// const beforeExport = this.beforeExport(blob, bookType, filename)
// if (beforeExport === false) {
// return
// }
saveAs(blob, `${filename}.${bookType}`);
},
/**
* @name: 转化时间格式
* @param {type}
* @return:
*/
datenum(v, date1904) {
if (date1904) v += 1462;
const epoch = Date.parse(v);
return (epoch - new Date(Date.UTC(1899, 11, 30))) / (24 * 60 * 60 * 1000);
},
/**
* @name: 设置数据类型
* @param {type}
* @return:
*/
sheet_from_array_of_arrays(data) {
let ws = {};
const range = {
s: {
c: 1000000000,
r: 1000000000
},
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;
let cell = {
v: data[R][C]
};
if (cell.v == null) continue;
let 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 = this.datenum(cell.v);
} else cell.t = 's';
ws[cell_ref] = cell;
}
}
if (range.s.c < 1000000000) ws['!ref'] = XLSX.utils.encode_range(range);
return ws;
},
/**
* @name: 转换格式
* @param {type}
* @return:
*/
s2ab(s) {
const b = new ArrayBuffer(s.length);
const v = new Uint8Array(b);
for (let i = 0; i < s.length; i++) {
v[i] = s.charCodeAt(i) & 0xFF
}
return b;
}
},
computed: {},
watch: {},
}
</script>
<style scoped>
div ::v-deep {
display: inline-block;
float: none !important;
}
</style>
主文件的导出模块
<template>
//导出模块
<excel-export :bookType="bookType" :filename="filename" :sheet="sheet" :on-error="onError">
<el-button type="primary">导出报表</el-button>
</excel-export>
</template>
<script>
import { reactive,defineAsyncComponent } from 'vue'
//引入模块
const ExcelExport = defineAsyncComponent(() =>import("../../../plugins/excelExport.vue"))
export default{
components: { ExcelExport }, //注册组件
const bookType='xlsx' //导出的文件格式
const filename = '报表明细' //导出的文件名称
const sheet = reactive([{
multiHeader: [['数量', '', '', '金额', '']], //多级表头
merges: ['A1:B1','D1:E1','C2:D2'], //合并
tHeader: ['项目名称', '项目单位', '数量', '金额'], //导出文件的标题
table: [], //后端返回的数据
keys: ['projectName', 'unit', 'number', 'price'] //根据table的数据的key 填充数据
}])
//后端数据返回
const datalist=()=>{
datalist().then(res=>{
sheet[0].table= res
})
}
//错误问题
const onError=(err)=> {
this.$message.error(err)
}
return {
dataList,sheet,bookType,onError
}
}
</script>