前端复杂表格导出excel,一键导出 Antd Table 看这篇就够了(附源码)
前端导出 excel 的需求很多,但市面上好用的库并不多,讲明白复杂使用场景的文章更少。
本文将以文字 + demo 源码的形式,力求讲清楚满足 99% 使用场景的终极 excel 导出方案。
如果项目中用到了 AntD,那就更简单了,因为 Table 本身已经设置好了 column 和 dataSource,只需解析 column 和 dataSource 即可快速导出 Excel。
实现功能:
- 简单表格导出
- 为表格添加样式(更改背景色、更换字体、字号、颜色)
- 设置行高、列宽
- 解析 ant-design 的 Table 直接导出excel,根据 antd 页面中设置的列宽动态计算 excel 中的列宽
- 多级表头(行合并、列合并)
- 一个 sheet 中放多张表,并实现每张表的列宽不同
源码地址:https://github.com/cachecats/excel-export-demo
第二篇文章:js 批量导出 excel 为zip压缩包, 对导出方法进行了封装,还实现了使用 exceljs
、file-saver
、jszip
实现下载包含多层级文件夹、多个 excel、每个 excel 支持多个 sheet 的 zip 压缩包。
一、技术选型
xlsx
呼声最高的是 xlsx,又叫 SheetJS
,也是下载量最高和 star
最多的库。试用了一下很强大,但是!默认不支持改变样式,想要支持改变样式,需要使用它的收费版本。
本着勤俭节约的原则,很多人使用了另一个第三方库:xlsx-style,但是使用起来极其复杂,还需要改 node_modules 源码,这个库最后更新时间也定格在了 6年前。还有一些其他的第三方样式拓展库,质量参差不齐。
使用成本和后期的维护成本很高,不得不放弃。
ExcelJS
ExcelJS 周下载量 450k,github star 9k,并且拥有中文文档,对国内开发者很友好。虽然文档是以README 的形式,可读性不太好,但重在内容,常用的功能基本都有覆盖。
最近更新时间是6个月内,试用了一下,集成很简单,再加之文档丰富,就选它了。
安装:
npm install exceljs
下载到本地还需要另一个库:file-saver
npm install file-saver
二、基本概念
先了解下基本概念,更详细的介绍参考官方文档:https://github.com/exceljs/exceljs/blob/HEAD/README_zh.md
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
- font
- alignment
- border
- fill
添加背景色
我们先给表头添加背景。因为表头是第一行,可以通过 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 还支持导出 csv
格式。此外还有设置页眉页脚、操作视图、添加公式、使用富文本等功能,非常的强大。
官方的文档也很详细,不懂的地方直接看文档即可。
源码地址:https://github.com/cachecats/excel-export-demo
前端复杂表格导出excel,一键导出 Antd Table 看这篇就够了(附源码)相关推荐
- Java 中如何解决 POI 读写 excel 几万行数据时内存溢出的问题?(附源码)
>>号外:关注"Java精选"公众号,菜单栏->聚合->干货分享,回复关键词领取视频资料.开源项目. 1. Excel2003与Excel2007 两个版本 ...
- 上传图片到linux返回url,Springboot 将前端传递的图片上传至Linux服务器并返回图片的url(附源码)...
问题由来: 用户个人信息需要添加头像功能 当前端程序是微信小程序时,前端将直接将图片 url 传送至服务端 但是当前端是 Web 页面时,前端传递的参数是一张图片,服务端需要将图片保存至 Linux ...
- 今天不抠图,Python实现一键换底片!想换什么换什么(附源码)
前言 本文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理. 生活中我们会拍很多的证件照,有的要求红底,有的是白底,有的是蓝底,今天不通 ...
- Python数据分析实战-提取DataFrame(Excel)某列(字段)最全操作(附源码和实现效果)
实现功能: Python数据分析实战-提取DataFrame(Excel)某列(字段)最全操作,代码演示了单列提取和多列提取两种情况,其中单列提取有返回series格式和dataframe两种情况,在 ...
- Python脚本一键找出哪些微信好友删了你(附源码)
查看被删的微信好友 原理就是新建群组,如果加不进来就是被删好友了(不要在群组里讲话,别人是看不见的) 用的是微信网页版的接口 查询结果可能会引起一些心理上的不适,请小心使用-(逃 还有些小问题: 结果 ...
- 用Python玩弄微信朋友圈,一键制作好友图片墙酷炸朋友圈(附源码)
上午发了张我微信近 2000 位好友的头像拼图,让大伙儿看能不能快速找到自己的头像,没想到反响很强烈,引得阵阵惊呼与膜拜,没有料到. 有没有犯密集恐惧症?这并不震撼,如果你有 5000 位好友的话,做 ...
- 前端复杂表格一键导出看这篇就够了(附源码)
大厂技术 高级前端 Node进阶 点击上方 程序员成长指北,关注公众号 回复1,加入高级Node交流群 前端导出 excel 的需求很多,但市面上好用的库并不多,讲明白复杂使用场景的文章更少. 本 ...
- php页面表格导出excel表格数据类型,php页面表格导出excel表格数据类型-php导出excel是不是导出整个表的?可不可以导出指......
php 怎么把数据导出到excel表格 昨天项目里有个新需求,客户希望把一些数据能导出成为Excel表格,刚开始用PHP原生输入Excel表格,发现效果不是很理想,于是找到一个比较著名的库:PHPEx ...
- C#开发的高性能EXCEL导入、导出工具DataPie(支持MSSQL、ORACLE、ACCESS,附源码下载地址)...
作 为财务数据核算人员,面对大量的业务与财务数据,借助于传统的EXCEL表格,已经力不从心.最近几个月,利用周末及下班的空闲时间,写了一个数据库导入 导出工具,以方便业务逻辑密集型的数据处理.目前,D ...
最新文章
- 编写可调模板并使用Auto-tuner自动调谐器
- 应用交换技术的负载均衡算法
- Xcode 7.0正式版发布了
- java中wait方法使用实例_java中wait、notify和notifyAll的概念用法和例子?
- 20155201 网络攻防技术 实验六 信息搜集与漏洞
- jQuery-事件委托(基本概述+实例)
- option标签selected=selected属性失效的问题
- mybatis 打印SQL
- loopback接口、router ID详解
- python抓取贴吧_python抓取百度贴吧-校花吧,网页图片
- Jersey客户端API调用REST风格的Web服务
- [解决]RESTEASY003215: could not find writer for content-type text/html type: java.lang.String
- u9系统的使用方法仓库_用友ERP系统,U9操作流程图
- 捷联惯导系统学习7.5(简化的捷联惯导算法及误差方程 )
- 性能服务器闹钟功能,发一个目前功能和性能最全的时钟!
- centos服务器无法上网
- RecyclerView条目复用导致混乱的解决方案之一
- 凸优化工具包CVX快速入门
- Unity发布WebGL不显示中文字体问题
- 改变tiff图片像素大小