背景
最近项目有一个功能,导出表格,要前端实现导出功能,翻了一圈文档,发现如果要实现前端导出功能,使用最多的包就是xlsx,奈何这个文档是英文,阅读有障碍,翻了很多文档,看了半天才看懂个七七八八,也找到了个中文版本文档。 现在把使用的功能总结一下。
导出表格
项目表格组件是基于element-ui进行的封装。如果前端实现导出有2种实现方式:
-
通过
table_to_sheet根据表格dom元素导出,把DOM TABLE元素转换为工作表。 -
通过数据构造表格数据,有两种数据格式可以转换
- JS二维数组结构,对应的api是
aoa_to_sheet,把转换JS数据数组的数组为工作表。 - JS对象数组结构,对应的api是
json_to_sheet,把JS对象数组转换为工作表 - 构造完表格数据后,如果想追加数据,则有对应的api
sheet_add_aoa把JS数据数组的数组添加到已存在的工作表中。
sheet_add_json把JS对象数组添加到已存在的工作表中。
- JS二维数组结构,对应的api是
1. table_to_sheet
这种方式导出表格,遇到了两个问题:
-
后台表格都是分页的,如果使用这个函数,则只能导出第一页内容,又查了下相应的解决方案。有2种:
-
表格带样式,xlsx这个社区版本是不支持表格设置样式的,如果要设置样式需要pro版本,那pro版本是要花钱的,那肯定不行。所以还需要 xlsx-styles 这个npm包,一引入就有问题,需要修改webpack配置。通过设置表格的样式属性,然后用这个包的XLSX对应来导出excel,就可以导出带有样式的表格了。
其实样式这个问题不光是table_to_sheet方式导出表格的问题,其他用数据直接构造表格导出也是遇到一样的问题,如果要带有样式的表格,都需要引用到这个库。或者换一个库exceljs
基础导出demo。
exportByTable(dom, filename) {
if (!dom) {
throw new Error('dom为空')
}
const workbook = XLSX.utils.book_new();
// dom生成sheet表
const worksheet = XLSX.utils.table_to_sheet(dom);
// 如果要添加样式,border等可以对worksheet做修改
// 将工作表对象添加到工作簿对象中
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
// 导出文件
this.writeFile(workbook, filename);
}
writeFile(workbook, filename) {
// 导出Excel, 注意这里用到的是XLSXS对象
let wbout = XLSXS.write(workbook, {
bookType: 'xlsx',
bookSST: false,
type: 'binary',
})
FileSaver.saveAs(
new Blob([s2ab(wbout)], {
type: 'application/octet-stream'
}),
filename // 保存的文件名
)
// 将工作簿对象保存为Excel文件, xlsx的导出无法带样式导出
// XLSX.writeFile(workbook, 'exported_data.xlsx');
}
2. 数据直接构造表格
因为后台接口返回的数据般来说都是数组对象的格式,所以拿到数据之后可以用这个方法来构造工作worksheet。
json_to_sheet基础导出用法:
// 插入表格数据
// const data = [
// { name: 'John Doe', age: 30, email: 'john.doe@example.com', gender: '男' },
// { name: 'Jane Smith', age: 35, email: 'jane.smith@example.com', gender: '女' }
// ];
// const columnHeaders = {
// name: '姓名',
// age: '年龄',
// email: '邮箱',
// gender: '性别'
// };
exportFile(data, header) {
// 创建一个工作簿对象
const workbook = XLSX.utils.book_new();
const newData = [headers, ...data];
// 创建一个工作表对象
const worksheet = XLSX.utils.json_to_sheet(newData, {
skipHeader: true
});
// 添加边框
this.addBorder(worksheet);
// 将工作表对象添加到工作簿对象中
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
// 导出文件
this.writeFile(workbook, filename);
}
- 已有表格后面添加文案
sheet_add_aoa,
content为而为数组,每一项是每一格的数据,空行的话直接添加空数组,第三个参数 origin:-1代表在最后一行添加。
insertAfterTable(worksheet, content) {
// contentDemo
// const arr = [
// [],
// [],
// ['说明:'],
// ['1、', '管理费不含期限管理费'],
// ['2、', '管理费'],
// ['3、', '管理费管理费']
// ]
// 表格后面插入说明
XLSX.utils.sheet_add_aoa(worksheet, content, { origin: -1 });
}
- 已有表格前面添加标题
sheet_add_aoa
这样直接插入会把前面行的内容直接替换掉,如何不替换掉?
// 已有表格前面插入title等
insertBeforeTable(worksheet, content) {
// 表头插入
// const contentDemo = [
// [],
// ['管理费率数据', '', '', ''],
// ['', '', '', '单位:税前数,bp'],
// ['主投x部门', '', '资产管理x部门', '']
// ];
// 表格后面插入说明, 需要留出插入内容的空白行
XLSX.utils.sheet_add_aoa(worksheet, content, { origin: 0 });
}
- 合并单元格
数据格式是excel文档单元格的号,merges为数组,每一项代表要合并大单元格的起始和终止位置。例如A2:D2代表合并第一行的A到D列。
这里思考如何简单的实现复杂嵌套表单的标题?
mergeCell(ws, merges) {
// const merges = [
// "A2:D2", // 合并一级表头的单元格
// "A4:B4", // 合并二级表头的单元格
// "C4:D4"
// ];
if (merges) {
if (!ws['!merges']) {
ws['!merges'] = [];
}
merges.forEach(item => {
ws['!merges'].push(XLSX.utils.decode_range(item));
})
}
}
- 添加表格样式和边框
变态的是需要一个一个单元格的进行遍历,然后设置每一个单元格的边框。真是变态啊,这api用法真难。单元格的样式也是通过设置cell.s中的属性。
如何简单的设置样式?
addBorder(worksheet, borderRange) {
// 设置表格边框样式
const borderStyle = {
top: { style: 'thin', color: { rgb: '000' } },
bottom: { style: 'thin', color: { rgb: '000' } },
left: { style: 'thin', color: { rgb: '000' } },
right: { style: 'thin', color: { rgb: '000' } }
};
borderRange = borderRange || worksheet[!ref];
var range = XLSX.utils.decode_range(borderRange);
for (let R = range.s.r; R <= range.e.r; ++R) {
for (let C = range.s.c; C <= range.e.c; ++C) {
const cellAddress = XLSX.utils.encode_cell({ r: R, c: C });
const cell = worksheet[cellAddress];
if (!cell) continue; // 跳过非字符串单元格
cell.s = {
border: borderStyle
};
if (R === 3 || R === 4) {
cell.s = {
...cell.s,
fill: {
fgColor: {
rgb: "eeeeee"
}
},
font: {
name: "黑体",
sz: "15",
bold: true
}
}
}
}
}
}
Todo
-
- 表头插入如何不覆盖?
函数加参数options,options beforeHeader:在表格前插入的数据
tableTitle: { value = [ [], // 空行 ['管理费率数据', '', '', ''], ['', '', '', '单位:税前数,bp'], ['主投x部门', '', '资产管理x部门', ''] ], merge: ['A2:H2'] }, afterTable: { value = [ [], [], ['说明:'], ['1、', '管理费不含期限管理费'], ['2、', '管理费'], ['3、', '管理费管理费'] ], } -
- 复杂表头如何设计传入结构?
children表格嵌套,根据以下格式转换为merges数组。
const columns = [
{
title: 'Name',
dataIndex: 'name',
key: 'name',
width: 100,
},
{
title: 'Other',
children: [
{
title: 'Age',
dataIndex: 'age',
key: 'age',
width: 150,
},
{
title: 'Address',
children: [
{
title: 'Street',
dataIndex: 'street',
key: 'street',
width: 150,
},
{
title: 'Block',
children: [
{
title: 'Building',
dataIndex: 'building',
key: 'building',
width: 100,
},
{
title: 'Door No.',
dataIndex: 'number',
key: 'number',
width: 100,
},
],
},
],
},
],
},
-
- 表格样式修改格式如何传入?
表格整体样式,只针对表格部分增加样式,不包含表格前后插入的数据,取出最后表格的范围,设置以下样式。
cellStyle: {
borderColorRgb: '333'
},
headerCellStyle: {
fontColorRgb: 'FF8040'
},
bodyCellStyle: {
fillFgColorRgb: 'EEEEE0'
},
针对单个表头单元格也可以设置样式
cellStyle: {
borderColorRgb: '333'
},
后续
今天找到一个库,觉得也的不错的,pengchen96.github.io/table-xlsx/… 表格导出的功能,基本满足了复杂表头、样式设置的功能,但是没有在表头插入和表后面插入内容的功能,拉下来带后,一顿操作猛如虎,加了这2个基本操作。好了,完成度90%了,还差表格头部的样式设置了,节后在搞。😄
新增两个参数:
- tableTitle: 表格前面添加标题,demo如下
{
value: [
[],
['投资部门', '', '', ''],
['', '', '', '单位:税前数,bp']
],
merges: [
'B1:D4'
]
=}
- afterTable:表格后面插入说明语句
const arr = [
[],
[],
['说明:'],
['1、', '管理费包含'],
['2、', '管理费金额'],
['3、', '管理费管理费']
]
逻辑是在之前计算完表格的数据后,再进行表头和表后面的数据插入,表后的插入比较简单,表头的插入复杂,目前简单处理是,如果有表头:
- 修改数据结果,sheet数据全部下移,修改过程中要注意从下往上复制,否则会出错。
- 修改merges数组结构,将合并单元格的row信息都加上title占用的行
if (tableTitle) {
offset = tableTitle?.value?.length || 0;
// sheet整体偏移计算
const row = sheet['!rows'].length;
const col = sheet['!cols'].length;
for (let i = row; i > 0; i--) {
for (let j = 65; j <= 65 + col; j++) {
const char = String.fromCharCode(j);
sheet[`${char}${i + 3}`] = sheet[`${char}${i}`];
if (i <= offset) {
sheet[`${char}${i}`] = {v: '', t: 's'};
}
}
}
// 设置merges
sheet['!merges'].forEach(item => {
item.s.r += offset;
item.e.r += offset;
})
const [start, end] = sheet['!ref'].split(':');
const newEnd = end.slice(0, 1) + (row + offset);
sheet['!ref'] = `A1:${newEnd}`;
insertBeforeTable(sheet, tableTitle);
}
if (afterTable.length) {
insertAfterTable(sheet, afterTable);
}
最后结果:
总结
一个简单的表格导出功能,竟然也耗费了很长时间,最开始用的最基础的导出功能,以为完事了,结果一看导出模版,竟然这么多样式,好吧,那咱查查怎么弄这样的表格,可是看了半天sheetjs的文档,硬是没看出眉目,最后发现这api简直反人类,我以为我只要调用一个api就可以添加border,结果没有,需要一个一个单元格设置===,而我想要的效果就是像通常调用组件一样传入好多options,然后就导出来对应的样式了,结果没有,幸好找到了一个库,发现写法还是不错的,那直接拿来用吧,再改改,就差不多了。😂