前端复杂表格一键导出看这篇就够了(附源码)
大厂技术 高级前端 Node进阶
点击上方 程序员成长指北,关注公众号
回复1,加入高级Node交流群
前端导出 excel 的需求很多,但市面上好用的库并不多,讲明白复杂使用场景的文章更少。
本文将以文字 + demo 源码的形式,力求讲清楚满足 99% 使用场景的终极 excel 导出方案。
如果项目中用到了 AntD,那就更简单了,因为 Table 本身已经设置好了 column 和 dataSource,只需解析 column 和 dataSource 即可快速导出 Excel。
实现功能:
简单表格导出
为表格添加样式(更改背景色、更换字体、字号、颜色)
设置行高、列宽
解析 ant-design 的 Table 直接导出excel,根据 antd 页面中设置的列宽动态计算 excel 中的列宽
多级表头(行合并、列合并)
一个 sheet 中放多张表,并实现每张表的列宽不同
源码地址:github.com/cachecats/excel-export-demo[1]
第二篇文章:js 批量导出 excel 为zip压缩包:https://juejin.cn/post/7080169896209809445/[2], 对导出方法进行了封装,还实现了使用 exceljs
、file-saver
、jszip
实现下载包含多层级文件夹、多个 excel、每个 excel 支持多个 sheet 的 zip 压缩包。
一、技术选型
xlsx
呼声最高的是 xlsx[3],又叫 SheetJS
,也是下载量最高和 star
最多的库。试用了一下很强大,但是!默认不支持改变样式,想要支持改变样式,需要使用它的收费版本。
本着勤俭节约的原则,很多人使用了另一个第三方库:xlsx-style[4],但是使用起来极其复杂,还需要改 node_modules 源码,这个库最后更新时间也定格在了 6年前。还有一些其他的第三方样式拓展库,质量参差不齐。
使用成本和后期的维护成本很高,不得不放弃。
ExcelJS
ExcelJS[5] 周下载量 450k,github star 9k,并且拥有中文文档,对国内开发者很友好。虽然文档是以README 的形式,可读性不太好,但重在内容,常用的功能基本都有覆盖。
最近更新时间是6个月内,试用了一下,集成很简单,再加之文档丰富,就选它了。
安装:
npm install exceljs
复制代码
下载到本地还需要另一个库:file-saver
npm install file-saver
复制代码
二、基本概念
先了解下基本概念,更详细的介绍参考官方文档:github.com/exceljs/exc…[6]
workbook
workbook:工作簿,可以理解为整个 excel 表格。
通过 const workbook = new ExcelJS.Workbook()
创建工作簿,还可以设置工作簿的属性:
workbook.creator = 'Me';
workbook.lastModifiedBy = 'Her';
workbook.created = new Date(1985, 8, 30);
workbook.modified = new Date();
workbook.lastPrinted = new Date(2016, 9, 27);
复制代码
worksheet
工作表,即 Excel 表格中的 sheet 页。
通过 const sheet = workbook.addWorksheet('My Sheet')
创建工作表,每个 workbook 可添加多个 worksheet。
使用 addWorksheet 函数的第二个参数来指定工作表的选项。
// 创建带有红色标签颜色的工作表
const sheet = workbook.addWorksheet('My Sheet', {properties:{tabColor:{argb:'FFC0000'}}});// 创建一个隐藏了网格线的工作表
const sheet = workbook.addWorksheet('My Sheet', {views: [{showGridLines: false}]});// 创建一个第一行和列冻结的工作表
const sheet = workbook.addWorksheet('My Sheet', {views:[{xSplit: 1, ySplit:1}]});// 使用A4设置的页面设置设置创建新工作表 - 横向
const worksheet = workbook.addWorksheet('My Sheet', {pageSetup:{paperSize: 9, orientation:'landscape'}
});// 创建一个具有页眉页脚的工作表
const sheet = workbook.addWorksheet('My Sheet', {headerFooter:{firstHeader: "Hello Exceljs", firstFooter: "Hello World"}
});// 创建一个冻结了第一行和第一列的工作表
const sheet = workbook.addWorksheet('My Sheet', {views:[{state: 'frozen', xSplit: 1, ySplit:1}]});
复制代码
columns
列,通过 worksheet.columns
可设置表头。
// 添加列标题并定义列键和宽度
// 注意:这些列结构仅是构建工作簿的方便之处,除了列宽之外,它们不会完全保留。
worksheet.columns = [{ header: 'Id', key: 'id', width: 10 },{ header: 'Name', key: 'name', width: 32 },{ header: 'D.O.B.', key: 'DOB', width: 10, outlineLevel: 1 }
];// 通过键,字母和基于1的列号访问单个列
const idCol = worksheet.getColumn('id');
const nameCol = worksheet.getColumn('B');
const dobCol = worksheet.getColumn(3);// 设置列属性// 注意:将覆盖 C1 单元格值
dobCol.header = 'Date of Birth';// 注意:这将覆盖 C1:C2 单元格值
dobCol.header = ['Date of Birth', 'A.K.A. D.O.B.'];// 从现在开始,此列将以 “dob” 而不是 “DOB” 建立索引
dobCol.key = 'dob';dobCol.width = 15;// 如果需要,隐藏列
dobCol.hidden = true;
复制代码
还可对列进行各种操作。
// 遍历此列中的所有当前单元格
dobCol.eachCell(function(cell, rowNumber) {// ...
});// 遍历此列中的所有当前单元格,包括空单元格
dobCol.eachCell({ includeEmpty: true }, function(cell, rowNumber) {// ...
});// 添加一列新值
worksheet.getColumn(6).values = [1,2,3,4,5];// 添加稀疏列值
worksheet.getColumn(7).values = [,,2,3,,5,,7,,,,11];// 剪切一列或多列(右边的列向左移动)
// 如果定义了列属性,则会相应地对其进行切割或移动
// 已知问题:如果拼接导致任何合并的单元格移动,结果可能是不可预测的
worksheet.spliceColumns(3,2);// 删除一列,再插入两列。
// 注意:第4列及以上的列将右移1列。
// 另外:如果工作表中的行数多于列插入项中的值,则行将仍然被插入,就好像值存在一样。
const newCol3Values = [1,2,3,4,5];
const newCol4Values = ['one', 'two', 'three', 'four', 'five'];
worksheet.spliceColumns(3, 1, newCol3Values, newCol4Values);
复制代码
row
行,可以添加一行或者同时添加多行数据,是使用最频繁的属性。
// 通过 json 添加一行数据,需要先设置 columns
worksheet.addRow({id: 1, name: 'John Doe', dob: new Date(1970,1,1)});
worksheet.addRow({id: 2, name: 'Jane Doe', dob: new Date(1965,1,7)});
// 通过数组添加一行数据
worksheet.addRow([3, 'Sam', new Date()]);// 同时添加多行数据
worksheet.addRows(list);// 遍历工作表中具有值的所有行
worksheet.eachRow(function(row, rowNumber) {console.log('Row ' + rowNumber + ' = ' + JSON.stringify(row.values));
});// 遍历工作表中的所有行(包括空行)
worksheet.eachRow({ includeEmpty: true }, function(row, rowNumber) {console.log('Row ' + rowNumber + ' = ' + JSON.stringify(row.values));
});// 连续遍历所有非空单元格
row.eachCell(function(cell, colNumber) {console.log('Cell ' + colNumber + ' = ' + cell.value);
});// 遍历一行中的所有单元格(包括空单元格)
row.eachCell({ includeEmpty: true }, function(cell, colNumber) {console.log('Cell ' + colNumber + ' = ' + cell.value);
});
复制代码
三、简单表格导出
本文所有示例都使用 React + AntD。
先看效果,我们用 AntD 的 Table 写个简单的表格页面,并设置不同的列宽:
点击导出 excel,然后打开得到以下结果:
可以看到,导出的 excel 列宽比例跟在线的表格是一致的。
贴源码:
// 简单 demo
import React, {useEffect, useState} from 'react'
import {Button, Card, Table} from "antd";
import {ColumnsType} from "antd/lib/table/interface";
import * as ExcelJs from 'exceljs';
import {generateHeaders, saveWorkbook} from "../utils";interface SimpleDemoProps {
}interface StudentInfo {id: number;name: string;age: number;gender: string;
}const SimpleDemo: React.FC<SimpleDemoProps> = () => {const [list, setList] = useState<StudentInfo[]>([]);useEffect(() => {generateData();}, [])function generateData() {let arr: StudentInfo[] = [];for (let i = 0; i < 10; i++) {arr.push({id: i,name: `小明${i}号`,age: i,gender: i % 2 === 0 ? '男' : '女'})}setList(arr);}const columns: ColumnsType<any> = [{width: 50,dataIndex: 'id',key: 'id',title: 'ID',},{width: 100,dataIndex: 'name',key: 'name',title: '姓名',},{width: 50,dataIndex: 'age',key: 'age',title: '年龄',},{width: 80,dataIndex: 'gender',key: 'gender',title: '性别',},];function onExportBasicExcel() {// 创建工作簿const workbook = new ExcelJs.Workbook();// 添加sheetconst worksheet = workbook.addWorksheet('demo sheet');// 设置 sheet 的默认行高worksheet.properties.defaultRowHeight = 20;// 设置列worksheet.columns = generateHeaders(columns);// 添加行worksheet.addRows(list);// 导出excelsaveWorkbook(workbook, 'simple-demo.xlsx');}return (<Card><h3>简单表格</h3><Button type={'primary'} style={{marginBottom: 10}} onClick={onExportBasicExcel}>导出excel</Button><Tablecolumns={columns}dataSource={list}/></Card>);
}export default SimpleDemo
复制代码
真正导出的代码只有几行,重点看 onExportBasicExcel
方法:
先创建工作簿和 sheet 页,这两行是固定代码。如果需要多 sheet,则创建多个 sheet 即可。后续对表格的所有操作,都是对 worksheet 的操作。
设置表格的默认行高。这步非必要,但是设置了更美观。否则会出现有内容的行跟没有内容的行行高不一致的情况。
设置列数据(表头)和每行的数据。
导出 excel。
解析 AntD Table 的 columns 和 dataSource
因为我们是用 AntD 的 Table,其实已经构造出了表头和具体的表格数据,所以只需解析即可。
generateHeaders()
方法是自己封装的,将 Table 的 columns 转换为 ExcelJS
的表头格式的方法:
import {ITableHeader} from "src/types";
import {ColumnsType} from "antd/lib/table/interface";const DEFAULT_COLUMN_WIDTH = 20;// 根据 antd 的 column 生成 exceljs 的 column
export function generateHeaders(columns: any[]) {return columns?.map(col => {const obj: ITableHeader = {// 显示的 nameheader: col.title,// 用于数据匹配的 keykey: col.dataIndex,// 列宽width: col.width / 5 || DEFAULT_COLUMN_WIDTH,};return obj;})
}
复制代码
在ExcelJS
中,header 字段表示显示的表头内容,key 是用于匹配数据的 key,width 是列宽。在 Table 的 column 中都有对应的字段,取出来赋值即可。
注意设置列宽的时候,在线表格和 excel 的单位可能不一致,需要除以一个系数才不至于太宽。至于具体除多少,可以不断试验得出个最佳值,我试的除以 5 效果比较好。
通过 worksheet.addRows()
方法可以为工作表添加多行数据,因为上面我们已经设置了表头,程序知道了每列数据应该匹配哪个字段,所以这里直接传入 Table 的 dataSource 即可。
也可以通过 worksheet.addRow()
逐行添加数据。
下载 excel
saveWorkbook()
也是自己封装的方法,接收 workbook 和文件名来下载 excel 到本地。
下载是使用 file-saver
库。
import {saveAs} from "file-saver";
import {Workbook} from "exceljs";export function saveWorkbook(workbook: Workbook, fileName: string) {// 导出文件workbook.xlsx.writeBuffer().then((data => {const blob = new Blob([data], {type: ''});saveAs(blob, fileName);}))
}
复制代码
到此,可以通过短短几行代码实现 AntD 的 Table 导出啦。
四、修改样式
单元格,行和列均支持一组丰富的样式和格式,这些样式和格式会影响单元格的显示方式。
通过分配以下属性来设置样式:
numFmt[7]
font[8]
alignment[9]
border[10]
fill[11]
添加背景色
我们先给表头添加背景。因为表头是第一行,可以通过 getRow(1) 来获取表头这一行:
// 给表头添加背景色
let headerRow = worksheet.getRow(1);
headerRow.fill = {type: 'pattern',pattern: 'solid',fgColor: {argb: 'dff8ff'},
}
复制代码
可以直接用 row.fill
为整行设置背景色,这样的话这一行没有内容的单元格也会有颜色,如图:
从 E 列开始其实就没有数据了,如果只想给非空单元格设置背景呢?
很遗憾 row 暴露的方法不支持直接这样设置,但可以曲线救国,遍历本行的所有非空单元格,再给每个单元格设置背景即可。
// 通过 cell 设置背景色,更精准
headerRow.eachCell((cell, colNum) => {cell.fill = {type: 'pattern',pattern: 'solid',fgColor: {argb: 'dff8ff'},}
})
复制代码
使用单元格控制会更加的精准,可以看到空的单元格已经没有背景色了。
修改字体样式
可以设置文字的字体、字号、颜色等属性,支持的属性如下表:
字体属性 | 描述 | 示例值 |
---|---|---|
name | 字体名称。 | 'Arial', 'Calibri', etc. |
family | 备用字体家族。整数值。 | 1 - Serif, 2 - Sans Serif, 3 - Mono, Others - unknown |
scheme | 字体方案。 | 'minor', 'major', 'none' |
charset | 字体字符集。整数值。 | 1, 2, etc. |
size | 字体大小。整数值。 | 9, 10, 12, 16, etc. |
color | 颜色描述,一个包含 ARGB 值的对象。 | { argb: 'FFFF0000'} |
bold | 字体 粗细 | true, false |
italic | 字体 倾斜 | true, false |
underline | 字体 下划线 样式 | true, false, 'none', 'single', 'double', 'singleAccounting', 'doubleAccounting' |
strike | 字体 删除线 | true, false |
outline | 字体轮廓 | true, false |
vertAlign | 垂直对齐 | 'superscript', 'subscript' |
与设置背景色相同,可以通过 row 或 cell 来设置。示例将通过 cell 设置。
修改表头的字体为微软雅黑,字号12号,颜色为红色,加粗斜体。
// 通过 cell 设置样式,更精准
headerRow.eachCell((cell, colNum) => {// 设置背景色cell.fill = {type: 'pattern',pattern: 'solid',fgColor: {argb: 'dff8ff'},}// 设置字体cell.font = {bold: true,italic: true,size: 12,name: '微软雅黑',color: {argb: 'ff0000'},};
})
复制代码
设置对齐方式
有效的对齐属性:
horizontal | vertical | wrapText | shrinkToFit | indent | readingOrder | textRotation |
---|---|---|---|---|---|---|
left | top | true | true | integer | rtl | 0 to 90 |
center | middle | false | false | ltr | -1 to -90 | |
right | bottom | vertical | ||||
fill | distributed | |||||
justify | justify | |||||
centerContinuous | ||||||
distributed |
表格默认的对齐方式是靠下对齐,一般都会设置为垂直方向居中对齐,文本靠左对齐,数字靠右对齐。这里为了方便都设置为水平方向靠左对齐,垂直方向居中对齐。
// 添加行
let rows = worksheet.addRows(list);
rows?.forEach(row => {// 设置字体row.font = {size: 11,name: '微软雅黑',};// 设置对齐方式row.alignment = {vertical: 'middle', horizontal: 'left', wrapText: false,};
})
复制代码
addRows()
的返回值是被添加的行的数组,然后循环对每行设置字体和对齐方式,就完成了对整个 excel 的样式自定义。
当然也可以对每个 cell 进行设置,效果是一样的。
设置边框也是同样的方法,这里不做介绍啦。
完整的导出带样式的 excel 代码:
// 导出function onExportBasicExcelWithStyle() {// 创建工作簿const workbook = new ExcelJs.Workbook();// 添加sheetconst worksheet = workbook.addWorksheet('demo sheet');// 设置 sheet 的默认行高worksheet.properties.defaultRowHeight = 20;// 设置列worksheet.columns = generateHeaders(columns);// 给表头添加背景色。因为表头是第一行,可以通过 getRow(1) 来获取表头这一行let headerRow = worksheet.getRow(1);// 直接给这一行设置背景色// headerRow.fill = {// type: 'pattern',// pattern: 'solid',// fgColor: {argb: 'dff8ff'},// }// 通过 cell 设置样式,更精准headerRow.eachCell((cell, colNum) => {// 设置背景色cell.fill = {type: 'pattern',pattern: 'solid',fgColor: {argb: 'dff8ff'},}// 设置字体cell.font = {bold: true,italic: true,size: 12,name: '微软雅黑',color: {argb: 'ff0000'},};// 设置对齐方式cell.alignment = {vertical: 'middle', horizontal: 'left', wrapText: false,};})// 添加行let rows = worksheet.addRows(list);// 设置每行的样式rows?.forEach(row => {// 设置字体row.font = {size: 11,name: '微软雅黑',};// 设置对齐方式row.alignment = {vertical: 'middle', horizontal: 'left', wrapText: false,};})// 导出excelsaveWorkbook(workbook, 'simple-demo.xlsx');}
复制代码
五、行合并&列合并
先看在线表格的效果:
导出的 excel:
这个表格涉及到多级表头、行合并、列合并。
涉及到以下几个重难点:
Table 表头的解析。多级表头有 children,要解析 Table 的 columns 为想要的数据结构。
列合并。一块内容占用了多个单元格,要进行一行中多个列的列合并,如成绩和老师评语列。
行合并。表头其实是占了两行,除了成绩外,其他的列都应该把两行合并为一行。
行和列同时合并。如果一个单元格合并过一次,就不能再合并,所以如果有行和列都需要合并的单元格,必须一次性同时进行行和列合并,不能拆开为两步。如老师评语列。
表头和数据的样式调整。
先贴出完整的代码
import React, {useEffect, useState} from 'react'
import {Button, Card, Space, Table} from "antd";
import {ColumnsType} from "antd/lib/table/interface";
import {ITableHeader, StudentInfo} from "../types";
import * as ExcelJs from "exceljs";
import {addHeaderStyle,DEFAULT_COLUMN_WIDTH, DEFAULT_ROW_HEIGHT,generateHeaders,getColumnNumber,mergeColumnCell,mergeRowCell,saveWorkbook
} from "../utils";
import {Worksheet} from "exceljs";interface MultiHeaderProps {
}const columns: ColumnsType<any> = [{width: 50,dataIndex: 'id',key: 'id',title: 'ID',},{width: 100,dataIndex: 'name',key: 'name',title: '姓名',},{width: 50,dataIndex: 'age',key: 'age',title: '年龄',},{width: 80,dataIndex: 'gender',key: 'gender',title: '性别',},{dataIndex: 'score',key: 'score',title: '成绩',children: [{width: 80,dataIndex: 'english',key: 'english',title: '英语',},{width: 80,dataIndex: 'math',key: 'math',title: '数学',},{width: 80,dataIndex: 'physics',key: 'physics',title: '物理',},]},{width: 250,dataIndex: 'comment',key: 'comment',title: '老师评语',},
];const MultiHeader: React.FC<MultiHeaderProps> = () => {const [list, setList] = useState<StudentInfo[]>([]);useEffect(() => {generateData();}, [])function generateData() {let arr: StudentInfo[] = [];for (let i = 0; i < 5; i++) {arr.push({id: i,name: `小明${i}号`,age: 8+i,gender: i % 2 === 0 ? '男' : '女',english: 80 + i,math: 60 + i,physics: 70 + i,comment: `小明${i}号同学表现非常好,热心助人,成绩优秀,是社会主义接班人`})}setList(arr);}function onExportMultiHeaderExcel() {// 创建工作簿const workbook = new ExcelJs.Workbook();// 添加sheetconst worksheet = workbook.addWorksheet('demo sheet');// 设置 sheet 的默认行高worksheet.properties.defaultRowHeight = 20;// 解析 AntD Table 的 columnsconst headers = generateHeaders(columns);console.log({headers})// 第一行表头const names1: string[] = [];// 第二行表头const names2: string[] = [];// 用于匹配数据的 keysconst headerKeys: string[] = [];headers.forEach(item => {if (item.children) {// 有 children 说明是多级表头,header name 需要两行item.children.forEach(child => {names1.push(item.header);names2.push(child.header);headerKeys.push(child.key);});} else {const columnNumber = getColumnNumber(item.width);for (let i = 0; i < columnNumber; i++) {names1.push(item.header);names2.push(item.header);headerKeys.push(item.key);}}});handleHeader(worksheet, headers, names1, names2);// 添加数据addData2Table(worksheet, headerKeys, headers);// 给每列设置固定宽度worksheet.columns = worksheet.columns.map(col => ({ ...col, width: DEFAULT_COLUMN_WIDTH }));// 导出excelsaveWorkbook(workbook, 'simple-demo.xlsx');}function handleHeader(worksheet: Worksheet,headers: ITableHeader[],names1: string[],names2: string[],) {// 判断是否有 children, 有的话是两行表头const isMultiHeader = headers?.some(item => item.children);if (isMultiHeader) {// 加表头数据const rowHeader1 = worksheet.addRow(names1);const rowHeader2 = worksheet.addRow(names2);// 添加表头样式addHeaderStyle(rowHeader1, {color: 'dff8ff'});addHeaderStyle(rowHeader2, {color: 'dff8ff'});mergeColumnCell(headers, rowHeader1, rowHeader2, names1, names2, worksheet);return;}// 加表头数据const rowHeader = worksheet.addRow(names1);// 表头根据内容宽度合并单元格mergeRowCell(headers, rowHeader, worksheet);// 添加表头样式addHeaderStyle(rowHeader, {color: 'dff8ff'});}function addData2Table(worksheet: Worksheet, headerKeys: string[], headers: ITableHeader[]) {list?.forEach((item: any) => {const rowData = headerKeys?.map(key => item[key]);const row = worksheet.addRow(rowData);mergeRowCell(headers, row, worksheet);row.height = DEFAULT_ROW_HEIGHT;// 设置行样式, wrapText: 自动换行row.alignment = { vertical: 'middle', wrapText: false, shrinkToFit: false };row.font = { size: 11, name: '微软雅黑' };})}return (<Card><h3>多表头表格</h3><Space style={{marginBottom: 10}}><Button type={'primary'} onClick={onExportMultiHeaderExcel}>导出excel</Button></Space><Tablekey={'id'}columns={columns}dataSource={list}/></Card>);
}export default MultiHeader
复制代码
前面几步创建 workbook 和 worksheet 都是一样的,从解析表头 generateHeaders()
开始逻辑会有所不同。
表头解析
我们修改上一节的generateHeaders()
方法,添加有 children 时的逻辑。多级表头时我们也构造出 children。
// 根据 antd 的 column 生成 exceljs 的 column
export function generateHeaders(columns: any[]) {return columns?.map(col => {const obj: ITableHeader = {// 显示的 nameheader: col.title,// 用于数据匹配的 keykey: col.dataIndex,// 列宽width: col.width / 5 || DEFAULT_COLUMN_WIDTH,};if (col.children) {obj.children = col.children?.map((item: any) => ({key: item.dataIndex,header: item.title,width: item.width,parentKey: col.dataIndex,}));}return obj;})
}
复制代码
构造出来的数据结构如下:
上一节简单表格中我们用 worksheet.columns = generateHeaders(columns)
设置每一个表头列所要显示的信息和应该匹配的 key,但是它无法设置多级表头,所以需要换一种思路,摒弃列(表头)的概念,把表头也当成一行数据来自己写入。下面的每行数据,也都自己通过计算匹配出应该在什么位置显示什么内容。
先来看这段代码:
// 解析 AntD Table 的 columns
const headers = generateHeaders(columns);
// 第一行表头
const names1: string[] = [];
// 第二行表头
const names2: string[] = [];
// 用于匹配数据的 keys
const headerKeys: string[] = [];
headers.forEach(item => {if (item.children) {// 有 children 说明是多级表头,header name 需要两行item.children.forEach(child => {names1.push(item.header);names2.push(child.header);headerKeys.push(child.key);});} else {const columnNumber = getColumnNumber(item.width);for (let i = 0; i < columnNumber; i++) {names1.push(item.header);names2.push(item.header);headerKeys.push(item.key);}}
});
复制代码
这个例子有两级表头,所以需要两行来设置每一级表头,分别命名为 names1
和 names2
,它们里面存的是展示出来的 name,如:ID、姓名、年龄等。还需要一个headerKeys
用来存储每一列需要匹配的 key,如:id、name、age 等 json 的 key。
注意一点,headerKeys
是以第二行表头为准,因为第二行才是真正显示的内容。
构造出了 names1
、names2
和headerKeys
,就可以开始生成真正的表头了:
function handleHeader(worksheet: Worksheet,headers: ITableHeader[],names1: string[],names2: string[],) {// 判断是否有 children, 有的话是两行表头const isMultiHeader = headers?.some(item => item.children);if (isMultiHeader) {// 加表头数据const rowHeader1 = worksheet.addRow(names1);const rowHeader2 = worksheet.addRow(names2);// 添加表头样式addHeaderStyle(rowHeader1, {color: 'dff8ff'});addHeaderStyle(rowHeader2, {color: 'dff8ff'});mergeColumnCell(headers, rowHeader1, rowHeader2, names1, names2, worksheet);return;}// 加表头数据const rowHeader = worksheet.addRow(names1);// 表头根据内容宽度合并单元格mergeRowCell(headers, rowHeader, worksheet);// 添加表头样式addHeaderStyle(rowHeader, {color: 'dff8ff'});}
复制代码
先判断有没有多级表头,单行表头和多行表头执行的逻辑不同。
通过 worksheet.addRow()
将表头添加为一行数据,多行表头就添加两次。然后通过 addHeaderStyle()
给表头添加样式,这是自己封装的方法,在 utils
里。最后也是最重要的是合并单元格,
合并同一行多列
合并单元格的方法是 worksheet.mergeCells()
,可以有很多种合并方式:
// 合并一系列单元格
worksheet.mergeCells('A4:B5');// ...合并的单元格被链接起来了
worksheet.getCell('B5').value = 'Hello, World!';
expect(worksheet.getCell('B5').value).toBe(worksheet.getCell('A4').value);
expect(worksheet.getCell('B5').master).toBe(worksheet.getCell('A4'));// ...合并的单元格共享相同的样式对象
expect(worksheet.getCell('B5').style).toBe(worksheet.getCell('A4').style);
worksheet.getCell('B5').style.font = myFonts.arial;
expect(worksheet.getCell('A4').style.font).toBe(myFonts.arial);// 取消单元格合并将打破链接的样式
worksheet.unMergeCells('A4');
expect(worksheet.getCell('B5').style).not.toBe(worksheet.getCell('A4').style);
expect(worksheet.getCell('B5').style.font).not.toBe(myFonts.arial);// 按左上,右下合并
worksheet.mergeCells('K10', 'M12');// 按开始行,开始列,结束行,结束列合并(相当于 K10:M12)
worksheet.mergeCells(10,11,12,13);
复制代码
先看合并同一行多列的算法,核心在于先设置一个索引,从1开始,代表第一列。然后循环 headers
,如果当前 header 有 children,则每个子级占一列,然后索引值加1。如果没有 children,计算这一个数据的宽度将会占用几个单元格,也就是几列,这个列数就是需要合并的列数,合并完之后索引值加1。
// 行合并单元格
export function mergeRowCell(headers: ITableHeader[], row: Row, worksheet: Worksheet) {// 当前列的索引let colIndex = 1;headers.forEach(header => {const { width, children } = header;if (children) {children.forEach(child => {colIndex += 1;});} else {// 需要的列数,四舍五入const colNum = getColumnNumber(width);// 如果 colNum > 1 说明需要合并if (colNum > 1) {worksheet.mergeCells(Number(row.number), colIndex, Number(row.number), colIndex + colNum - 1);}colIndex += colNum;}});
}export function getColumnNumber(width: number) {// 需要的列数,四舍五入return Math.round(width / DEFAULT_COLUMN_WIDTH);
}
复制代码
合并单元格的方法是:
worksheet.mergeCells(Number(row.number), colIndex, Number(row.number), colIndex + colNum \- 1);
四个参数分别是合并的开始行、开始列、结束行、结束列。
通过 row.number
得到当前行的行数,因为是同一行的多列合并,所以开始结束行一致,开始列是索引值 colIndex
,结束列是 colIndex + colNum \- 1
。
同时合并行和列
如果是多级表头,需要同时处理行和列合并,用到了封装的 mergeColumnCell
方法。
基本思路是先判断合并的类型,一共有三种情况:
只有行合并
只有列合并
同时进行行和列合并
然后计算出起始的行和列,以及结束的行和列。
// 合并行和列,用于处理表头合并
export function mergeColumnCell(headers: ITableHeader[],rowHeader1: Row,rowHeader2: Row,nameRow1: string[],nameRow2: string[],worksheet: Worksheet,
) {// 当前 index 的指针let pointer = -1;nameRow1.forEach((name, index) => {// 当 index 小于指针时,说明这一列已经被合并过了,不能再合并if (index <= pointer) return;// 是否应该列合并const shouldVerticalMerge = name === nameRow2[index];// 是否应该行合并const shouldHorizontalMerge = index !== nameRow1.lastIndexOf(name);pointer = nameRow1.lastIndexOf(name);if (shouldVerticalMerge && shouldHorizontalMerge) {// 两个方向都合并worksheet.mergeCells(Number(rowHeader1.number),index + 1,Number(rowHeader2.number),nameRow1.lastIndexOf(name) + 1,);} else if (shouldVerticalMerge && !shouldHorizontalMerge) {// 只在垂直方向上同一列的两行合并worksheet.mergeCells(Number(rowHeader1.number), index + 1, Number(rowHeader2.number), index + 1);} else if (!shouldVerticalMerge && shouldHorizontalMerge) {// 只有水平方向同一行的多列合并worksheet.mergeCells(Number(rowHeader1.number),index + 1,Number(rowHeader1.number),nameRow1.lastIndexOf(name) + 1,);// eslint-disable-next-line no-param-reassignconst cell = rowHeader1.getCell(index + 1);cell.alignment = { vertical: 'middle', horizontal: 'center' };}});
}
复制代码
添加数据行
在计算表头时,已经得到了每列的 key 值列表 headerKeys
,通过headerKeys
可以取出每一列对应的具体数据。
function addData2Table(worksheet: Worksheet, headerKeys: string[], headers: ITableHeader[]) {list?.forEach((item: any) => {const rowData = headerKeys?.map(key => item[key]);const row = worksheet.addRow(rowData);mergeRowCell(headers, row, worksheet);row.height = DEFAULT_ROW_HEIGHT;// 设置行样式, wrapText: 自动换行row.alignment = { vertical: 'middle', wrapText: false, shrinkToFit: false };row.font = { size: 11, name: '微软雅黑' };})
}
复制代码
先循环数据列表,然后循环 headerKeys
取出对应的值,再通过 worksheet.addRow
将这一行数据添加进表格中。由于可能出现一个字段占用多列的情况,所以还需要进行合并单元格操作,可以复用 mergeRowCell()
方法。最后设置每行的样式,即可得到最终的数据。
一个 sheet 中放多张表
在导出多级表头表格的时候,我们写表头和数据行都是用的worksheet.addRow
方法,而没有用 worksheet.column
设置表格的表头,这样更加灵活,每一列想显示什么内容完全自己控制。
处理多个表格时,也可以用同样的方法。因为每一行数据都是自己写入的,所以不管有几张表都没有关系,我们关心的只有每一行的数据。
同时我们做了行和列合并算法,可以实现每一张表的每一列都能定制宽度。
可以将上面两个例子结合起来,导出到一个 sheet
里,就实现了一个sheet
中放多张表的需求。
结语
除了导出 xlsx
,ExcelJS[12] 还支持导出 csv
格式。此外还有设置页眉页脚、操作视图、添加公式、使用富文本等功能,非常的强大。
官方的文档也很详细,不懂的地方直接看文档即可。
源码地址:github.com/cachecats/excel-export-demo[13]
关于本文
作者:solocoder
https://juejin.cn/post/7071882317953761316
Node 社群我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。如果你觉得这篇内容对你有帮助,我想请你帮我2个小忙:1. 点个「在看」,让更多人也能看到这篇文章2. 订阅官方博客 www.inode.club 让我们一起成长点赞和在看就是最大的支持
前端复杂表格一键导出看这篇就够了(附源码)相关推荐
- C#使用Xamarin开发可移植移动应用(2.Xamarin.Forms布局,本篇很长,注意)附源码
前言 源码地址:https://github.com/l2999019/DemoApp 可以Star一下,随意 - - 一点感想 很意外的,第一篇博文被博客园的编辑大哥置顶了.感谢. 评论也很多,褒贬 ...
- C# 30分钟完成百度人脸识别——进阶篇(文末附源码)
距离上次入门篇时隔两个月才出这进阶篇,小编惭愧,对不住关注我的卡哇伊的小伙伴们,为此小编用这篇博来谢罪. 前面的准备工作我就不说了,注册百度账号api,创建web网站项目,引入动态链接库引入. 不了解 ...
- 如何解决和异地女朋友一起看电影的需求?(内附源码)
一.这是一件悲伤的故事 我是一名程序员,我和我的女友是异地恋,so sad!!! 一次视频时,她突然来了一句:"我们已经很久没有一起看电影了吧......" "呃...好 ...
- List转Page对象,看这篇就够了,附代码、实验结果分页自己编程实现
List 转 Page 对象 处理统计等数据时分页展示.第三方接口仅返回list,需分页显示等需求 public class ConvertList2PageVOUtil {/*** List 转 P ...
- 遥遥领先一键导出微信小程序、UNIAPP低代码源码使用介绍
1.1添加组件至设计器 在左侧基本组件库选择要使用的组件,按住左键不放,拖动到设计器适合的位置上松开. 1.2修改组件图层名称 修改组件图层名称,方便快速定位组件位置.找到图层面板按钮,点击显示图层面 ...
- 2018年强化学习领域十篇重要论文(附源码)
2019-06-06 09:47:57 来自网络 与其他机器学习方法相比,比如监督式学习.迁移学习.甚至非监督式学习学习相比,深度强化学习方法极其需要大量数据,而且常常不稳定,从性能上来说可能不是最好 ...
- 【超全汇总】学习数据结构与算法,计算机基础知识,看这篇就够了【ZT帅地】2020-3-7
https://blog.csdn.net/m0_37907797/article/details/104029002 由于文章有点多,并且发的文章也不是一个系列一个系列发的,不过我的文章大部分都是围 ...
- C#使用Xamarin开发可移植移动应用(4.进阶篇MVVM双向绑定和命令绑定)附源码
今天的学习内容? 今天我们讲讲Xamarin中的MVVM双向绑定,嗯..需要有一定的MVVM基础.,具体什么是MVVM - -,请百度,我就不多讲了 效果如下: 正文 1.简单的入门Demo 这个时间 ...
- C#使用Xamarin开发可移植移动应用进阶篇(8.打包生成安卓APK并精简大小),附源码
我记得,之前在写安卓方面的文章的时候,有人就问过我.Xamarin.Android为什么打包出来这么大?随便一个HelloWord就20-30MB? 嗯..今天我们就来解决这个问题.. 我们先从指定一 ...
最新文章
- mac 香港购买分享
- VML编程之------VML语言入门《VML极道教程》原著:沐缘华
- dos下的edit命令使用详解
- hadoop程序实例
- 用Java编辑实现warshall算法_warshall算法 java实现
- 使用sourcetree 的git flow
- 【路径规划】基于matlab粒子群融合遗传算法栅格地图路径规划【含Matlab源码 526期】
- 从JavaEye社区被迫改名说起(转载他人博客)
- 谷歌浏览器Vue插件
- css3实现缺角四边形_利用CSS3实现折角效果实例源码
- pdf旋转后如何保存?操作工具有这些
- 建立FTP服务器的免费软件
- 301跳转有什么用?为什么要做301跳转?
- HDU6130 Kolakoski
- SAP MR21修改标准价
- java八大基本数据类型基本用法(含数据输入输出)
- ps切片 html作用,ps切片有什么用,PS切片是什么
- 肠道菌群检测粪便样本的采集保存方法
- 这里有一个url=https://www/.baidu.com/s?id=111name=yourname,写一个函数获取query的参数和值存放在一个对象...
- Hibernate Validator 后台数据规则校验拓展