前言
excel 的导入导出功能,也是前端开发中比较常用的功能,但一般三方组件都比较偏向基础功能,因此不会定制过渡,从而限制开发者们的使用,本文将介绍并封装一下 exceljs的导入导出功能,其中包括多表头,方便实际使用
js-export-excel:仅仅支持导出 excel,有仅仅导出的需要的也可以用(不过有导入的就不推荐了,还要引入其他两个库,当然看个人)
xlsx:其支持导入和导出,缺少些样式,使用是没问题的
exceljs 同时支持导入和导出,也可以设置 xls 的样式,挺好的,之前也写了一个 简易版excel导出,直接根据 antd 的 table 模版导出的,可以参考
本文将介绍导入,以及更加详细一些的导出,并提供更方便的接口,导出单表头、多表头的导出功能
ps
:图片导出仍然小众,可以自行定制 参考这里
exceljs 准备工作
js
//安装 exceljs
yarn add exceljs
//需要用到 file-saver 的可以导入,本文也有自带导入的,但可能没有 file-saver 兼容性好,可以自行抉择
yarn add file-saver
excel导入
导入默认使用的是 excel.Workbook
的 readFile、read、load
方法来读取路径、文件、buffer
信息,可以自行抉择,然后根据读取到的数据信息,转化为我们想要的结构
默认灵活读取
一般选择文件结束后会返回 file 信息,下面就以 file 类型文件为例
读取 file,直接返回读取到的二维数组,表头和信息都在里面,如果只有一行,那么第一组数据就是表头,剩下的就都是是数据了,可以自行加工
js
import Excel from 'exceljs'
//读取Excel,结果返回json数组实际上是一个二维数据,可以自行处理,较为灵活
export const readExcelJson = async (file: File, sheetIndex = 0) => {
const workbook = new Excel.Workbook()
//是可以读取buff等信息的
await workbook.xlsx.read(file.stream())
const sheet = workbook.worksheets[sheetIndex]
const data: string[][] = []
//分别遍历行和列,取出里面的信息
sheet.eachRow((row) => {
const list: string[] = []
row.eachCell((cell) => {
list.push(cell.value?.toString() || '')
})
data.push(list)
})
return data
}
excel 导入单表头文件
有时候太灵活了也不是啥好事,毕竟要处理的代码多了,这里就封装一下单表头文件导入,直接可以提前设定好数据结构,将取出的数据直接加工成我们想要的样子
已知表格数据长这样
我们想简单设置一下,直接将内容映射一下取出来,使用如下所示
js
const result = await excel.readSingleExcel(file, [
{
prop: 'column3',
title: '表头3',
},
{
prop: 'column1',
title: '表头1',
},
{
prop: 'column2',
title: '表头2',
},
{
prop: 'column4',
title: '表头4',
},
{
prop: 'column5',
title: '表头5',
},
])
接下来就实现 readSingleExcel 方法,实现如下所示
js
//列方向顺序不一定,因此需要给出key、name可以按照规则映射出对应的内容
export type ReadSingleExcelType = {
prop: string //表格中的标题映射出的key,用于前端显示或者上传
title: string //表格中标题名称
}
//导入单表头表格,可以设置 sheet 的索引,sheet非必填默认第一个
//只映射匹配到的表头和内容
export const readSingleExcel = async (
file: File,
readRules: ReadSingleExcelType[],
sheetIndex = 0,
) => {
const workbook = new Excel.Workbook()
//是可以读取steam、buffer等信息的,可以根据情况自行改进
await workbook.xlsx.read(file.stream())
const sheet = workbook.worksheets[sheetIndex]
//row对应rule索引,后续可以直接用来赋值
const keys: (string | null)[] = []
const rules: (ReadSingleExcelType | null)[] = [] //用于外部返回原有顺序有效数组
//index从1开始
sheet.getRow(1).eachCell((cell, idx) => {
const value = cell.value?.toString()
const index = readRules.findIndex((e) => e.title === value)
if (index >= 0) {
rules.push(readRules[idx - 1])
keys.push(readRules[index].prop)
} else {
keys.push(null)
}
})
//生成内容
const data: Record<string, string>[] = []
sheet.eachRow((row, index) => {
if (index < 2) return
const obj: Record<string, string> = {}
row.eachCell((cell, idx) => {
const key = keys[idx - 1]
if (key) {
obj[key] = cell.value?.toString() || ''
}
})
data.push(obj)
})
return {
readRules: rules,
datasource: data,
}
}
打印结果如下所示
ps
:一次只能读取一个 sheet,那么如果有多个sheet,直接多调用几次就行了,这里就不额外封装了
excel 导入多表头文件(天然支持单表头)
有时候 excel 也会导入多表头文件excel,多表头就比单表头要复杂的多,其表头一般是嵌套结构,因此需要嵌套结构表示其表头结构
,内容肯定是一个线性列表,也就是以实际最后生效的那个表头为基础映射数据,所以复杂的只有表头,还有映射过程罢了
ps
:多表头逻辑天然支持单表头,毕竟不嵌套就是单表头结构了,因此其也为一个通用性实现
下面有这样一个多表头文件
我们使用时希望这么使用,设置一个嵌套结构用于映射,设置一个 sheet 的索引(这个案例是第二个sheet,所以第二个参数传递的是1)
js
excel
.readMultiExcel(
file,
[
{
title: '表头1',
children: [
{
title: '子表头1',
children: [
{
title: '孙子表头1',
prop: 'column1_child1_child1',
},
{
title: '孙子表头2',
prop: 'column1_child1_child2',
},
],
},
{
title: '子表头2',
prop: 'column1_child2',
},
],
},
{
title: '表头2',
children: [
{
title: '子表头1',
prop: 'column2_child1',
},
{
title: '子表头2',
prop: 'column2_child2',
},
],
},
{
title: '表头3',
prop: 'column3',
},
],
1,
)
.then((result) => {
console.log(result)
})
.catch((err) => {
console.log(err)
})
我们就按照上面实现,但是过程相对复杂,分解为下面几个步骤介绍
- 声明 column 的嵌套类型,根据这个类型嵌套传参,后续需要递归,用于获取宽度和深度,用于获取实际有效表头和内部映射数据
getExcelLevel
方法用于获取表头的嵌套层级,方便更准去确认表头一共有几行,这样无论是用于校验还是读取数据都是很方便的generateSheetTitles
方法,根据嵌套表头,生成一个和 excel 数据一样的多维数组表头,用于校验表头是否匹配
(由于多级表头相对复杂,暂不支持乱序映射,只能一模一样的表头进行映射),其中用到了getExcelColumn
方法,是获取子节点的占用的列数 column,方便横向填充内容getExcelRealKeys
用于获取实际映射的有效节点也就是涉及到最后一行表头的节点,其为实际映射节点,其必然要有 prop 参数用于映射- 已知表头、内容偏移 以及实际映射的 keys,根据读取出的表格数据,直接映射出数据即可
js
//多级表头必然是一个递归类型,无论是 antd,还是element,不然结构就会显得很混乱难以管理
export type ReadMultiExcelType = {
title: string //标题名字, 必填,也是用于校验使用
prop?: string //最深一层的propertyname可以不填写,但是最深一层对应实际数据的要填写,不能重复,否则会映射失败
children?: ReadMultiExcelType[]
}
//导入多级表头表格,需要确认表头数量,否则会导入出错
//由于多级表头相对复杂,暂不支持内部乱序,会严格按照 column、excel数据对应,表头不一样则直接报错
export const readMultiExcel = async (
file: File,
readRules: ReadMultiExcelType[],
sheetIndex = 0,
) => {
const workbook = new Excel.Workbook()
await workbook.xlsx.read(file.stream())
const sheet = workbook.worksheets[sheetIndex]
//先获取深度
const level = getExcelLevel(readRules)
//生成和 sheet 表头一样的结构,方便对比是否一样
const result = generateSheetTitles(readRules, level)
//对比校验表头
sheet.getRows(1, level)?.forEach((row, index) => {
row.eachCell((cell, idx) => {
const ruleTitle = result[index][idx - 1]
if (ruleTitle !== cell.value?.toString()) {
throw new Error('标题不一致')
}
})
})
//获取实际映射的 key
const keys = getExcelRealKeys(readRules)
const data: Record<string, string>[] = []
sheet.eachRow((row, index) => {
if (index <= level) return
const obj: Record<string, string> = {}
row.eachCell((cell, idx) => {
const key = keys[idx - 1]
if (key) {
obj[key] = cell.value?.toString() || ''
}
})
data.push(obj)
})
//结果返回rules和数据源
return {
readRules: readRules,
datasource: data,
}
}
//获取设定标题层级
const getExcelLevel = (rules?: ReadMultiExcelType[], max = -1) => {
if (!rules || rules.length < 1) return max
rules.forEach((e) => {
const level = getExcelLevel(e.children, max)
if (level > max) {
max = level
}
})
return max + 1
}
//获取设定中有效的数据个数
const getExcelColumn = (rules?: ReadMultiExcelType[], max = 0) => {
if (!rules || rules.length < 1) return max + 1
rules.forEach((e) => {
const level = getExcelColumn(e.children, max)
if (level > max) {
max = level
}
})
return max
}
//获取真实的映射关键key
const getExcelRealKeys = (rules?: ReadMultiExcelType[], result: string[] = []) => {
if (!rules || rules.length < 1) return result
rules.forEach((rule) => {
if (rule.children) {
getExcelRealKeys(rule.children, result)
} else if (rule.prop) {
result.push(rule.prop)
} else {
throw new Error('存在未设置 prop 的基础标题')
}
})
return result
}
//生成用于检测的二维数组,结果和sheet中取出的表头一样(合并单元格的格子中每一个都是一样的)
const generateSheetTitles = (
rules: ReadMultiExcelType[],
maxLevel: number = 1,
row: number = 0,
column: number = 0,
result: string[][] = [],
) => {
rules.forEach((rule) => {
const columns = getExcelColumn(rule.children)
if (rule.children) {
//处理行补全
let start = column
const end = column + columns
while (start < end) {
if (!result[row]) {
result[row] = []
}
const rowResult = result[row]
rowResult[start] = rule.title
start++
}
//处理子节点
generateSheetTitles(rule.children, maxLevel, row + 1, column, result)
} else if (row < maxLevel) {
//处理列补全
let temRow = row
do {
if (!result[temRow]) {
result[temRow] = []
}
const rowResult = result[temRow]
rowResult[column] = rule.title
temRow++
} while (temRow < maxLevel)
}
column += columns
})
return result
}
ps
:如果有多个
excel导出
excel 导出
也是实际用的比较多的功能,因此逻辑将会更多,同时还会有样式设计等,至少保证表头、内容能够分分辨
excel 也有单表头和多表头的区别,逻辑也不一样,和读取一样的道理,多表头也天然支持单表头,只不过单表头代码更少,使用更简单
excel导出单表头文件
先看看我们案例导出的excel长成这样,嫌长的丑可以略微调整颜色哈,毕竟以导出为基准,能看就行,有更多要求可以通过专业软件整理即可🤣
设计逻辑是,希望我们单表头结构也能同时支持深入映射取值,因此可以通过 . 和 数组
的方式来向下映射,同时也支持 transfrom
转化,希望取到的值能够加工一下导出,毕竟返回的数据不一定是我们想要显示的效果
js
excel.exportSingleExcel({
columns: [
{
title: '标题1',
prop: 'column1',
},
{
title: '标题2',
prop: 'column2',
},
{
title: '标题3',
prop: 'column3',
},
{
title: '标题4',
prop: 'column4',
},
{
title: '标题5',
prop: 'column5.name', //通过 . 的方式向下映射
},
{
title: '标题6',
prop: ['column6', 'subName'], //通过数组的方式向下映射
},
{
title: '标题7',
transform(_, item) {
return item['column7'] + '我是transfrom之后的内容'
},
},
{
title: '标题8',
prop: 'column8',
},
],
datasource,
})
下面就开始设计单表头导出功能了,主要就是 设置 column 的标题(表头内容)、映射 key、宽高、位置、转换transfrom等逻辑,比较简单
但还有样式,映射逻辑支持,因此实际代码量也不少,这里有扩展除了多 sheet 支持,因此多了一些代码和类型
js
import Excel from 'exceljs'
import { saveFile } from './save-file'
//映射基础结构
export type ExcelColumns = {
title: string //标题名称
prop?: string | string[] //索引,例如: 'child'、'child.name'、['children', 'name'],使用有 transform 可以不填写key,但回调第一个参数就是undefined了
width?: number //实际算是间接设置文字长度了,英文1个多一点,中文两个多一点最佳,默认按照像素数量 / 10,并预留出部分空间,一般为 12-16号字体之间,这个略长一点
align?: 'left' | 'right' | 'center'
transform?: (value: unknown, record: Record<string, unknown>, index: number) => string | number //转化改动后的生成的文本
}
//表格映射设置,用来设置多 sheet
type ExcelSingleExportParams = {
columns: ExcelColumns[]
datasource: Record<string, unknown>[]
sheetname?: string
}
//可以设置作者、文件名、以及使用外部的保存库保存到本地
type ExcelSingleExportOptions = {
creator?: string
filename?: string
saveFile?: (buffer: Excel.Buffer) => void
}
//基本导出功能
export async function exportSingleExcel(
params: ExcelSingleExportParams[] | ExcelSingleExportParams,
options?: ExcelSingleExportOptions,
): Promise<void> {
//归一化处理
if (!Array.isArray(params)) {
params = [params]
}
if (!params.find((e) => Array.isArray(e.datasource))) {
throw new Error('datasource数据源不存在')
}
//过滤掉不符合条件的标题
params.forEach((item) => {
item.columns = item.columns.filter((e) => e.title && (e.transform || e.prop))
})
const workbook = new Excel.Workbook()
workbook.creator = options?.creator ?? '剪刀石头布啊'
workbook.created = new Date()
// 添加工作表
params.forEach((param, index) => {
//开始映射内容到指定工作表,也就是核心逻辑在这里
addSingleSheets(param, workbook, index)
})
//保存,可以通过外部传入的方法保存,这里回传出 buffer 对象
const buffer = await workbook.xlsx.writeBuffer()
if (options?.saveFile) {
options.saveFile(buffer)
} else {
//自己的 saveFile 最后在介绍
saveFile(buffer, options?.filename)
}
}
//这里才是核心的单表头映射逻辑
function addSingleSheets(params: ExcelSingleExportParams, workbook: Excel.Workbook, index: number) {
const sheet = workbook.addWorksheet(params?.sheetname ?? 'sheet' + index)
const headerColumns: Partial<Excel.Column>[] = []
//key相关
const infos: ColumnKeyType[] = []
//设置表头,表头需要有标题,key,宽度,这里宽度就是列宽了
//表头设置完毕key之后,后续读取子内容时,就是根据表头的 key 进行映射,因此key很重要
params.columns.forEach((item) => {
//处理我们自定义的key,因为其同时支持默认key,.和数组向下取参的方式
const info = generateColumnKey(item)
infos.push(info)
headerColumns.push({
header: item.title,
key: info.key,
width: item.width ? item.width / 10 : 24, //不填写默认20长度
})
})
sheet.columns = headerColumns
const data: Record<string, unknown>[] = []
//根据获取的 key 信息,用于映射数据
params.datasource.forEach((item, index) => {
const obj: Record<string, unknown> = {}
params.columns.forEach((e, idx) => {
//映射数据,有key、keylist 就映射出实际的值
const key = infos[idx].key
const list = infos[idx].list
let value: unknown
if (list) {
let temValue = item
list.forEach((itm) => {
temValue = temValue[itm] as Record<string, unknown>
})
value = temValue
} else if (key) {
value = item[key]
}
//transfrom设置prop就映射内容,不设置就不映射,只能用第二个参数了
if (e.transform) {
//没有设置key
obj[key] = e.transform(item[key] as unknown, item, index)
} else {
obj[key] = value
}
})
data.push(obj)
})
sheet.addRows(data)
//设置标题样式,第1行就是标题,没有0行
sheet.getRow(1).eachCell({ includeEmpty: true }, (cell: Excel.Cell, colNumber: number) => {
cell.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: {
argb: 'FFDEE6F0',
},
}
cell.font = {
size: 14,
color: { argb: 'FF000000' }, // 黑色
bold: true,
}
cell.border = {
top: { style: 'thin' },
left: { style: 'thin' },
bottom: { style: 'thin' },
right: { style: 'thin' },
}
cell.alignment = {
horizontal: params.columns[colNumber - 1].align ?? 'center',
vertical: 'middle',
}
})
//设置内容样式
params.datasource.forEach((item, index) => {
sheet
.getRow(index + 2)
.eachCell({ includeEmpty: true }, (cell: Excel.Cell, colNumber: number) => {
cell.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: {
argb: 'FFFFFFFF',
},
}
cell.font = {
size: 12,
color: { argb: 'FF000000' }, // 黑色
}
cell.border = {
top: { style: 'thin' },
left: { style: 'thin' },
bottom: { style: 'thin' },
right: { style: 'thin' },
}
cell.alignment = {
horizontal: params.columns[colNumber - 1].align ?? 'center',
vertical: 'middle',
}
})
})
}
type ColumnKeyType = {
key: string
list?: string[]
}
//生成key
function generateColumnKey(column: ExcelColumns): ColumnKeyType {
const info: ColumnKeyType = {
key: '',
}
if (column.transform) {
if (!column.prop) return info
} else if (!column.prop) {
throw new Error('需要设置prop属性,或使用transform函数转化')
}
let list: string[]
if (Array.isArray(column.prop)) {
list = column.prop
} else {
list = column.prop.split('.')
}
if (list.length < 2) {
info.key = list[0]
} else {
info.key = list.join('__')
info.list = list
}
return info
}
这样就实现了单表头的导出逻辑
excel导出多表头文件(天然支持单表头)
先看看导出效果
再看看使用效果,按照使用代码,也是嵌套式的,实际映射的那一列必然又要 prop,也就是没有 children 的那一列
js
excel.exportMultiExcel({
columns: [
{
title: '表头1',
children: [
{
title: '子表头1',
children: [
{
title: '孙子表头1',
prop: 'column1',
},
{
title: '孙子表头2',
prop: 'column2',
},
],
},
{
title: '子表头2',
prop: 'column3',
},
],
},
{
title: '表头2',
children: [
{
title: '子表头1',
prop: 'column4',
},
{
title: '子表头2',
prop: 'column5.name',
},
],
},
{
title: '表头3',
prop: ['column6', 'subName'],
},
],
datasource,
})
多表头相比于单表头逻辑相对复杂,就和读取单表头类似,下面分为几个步骤
- 声明 column 的嵌套类型,根据这个类型嵌套传参,后续需要递归,用于获取宽度和深度,用于获取实际有效表头和内部映射数据
getExcelLevel
方法用于获取表头的嵌套层级,方便更准去确认表头一共有几行,这样无论是用于校验还是读取数据都是很方便的addSheetTitles
方法,则类似于读取表头的方法,会根据嵌套情况横向、纵向合并单元格,getExcelColumn
方法,是获取子节点的占用的列数 column,方便横向填充内容getExcelRealKeys
用于获取实际映射的有效节点也就是涉及到最后一行表头的节点,其为实际映射节点,其必然要有 prop 参数用于映射,因此也使用了generateColumnKey
生成需要的映射关系 keyinfo 信息,方便后续取值- 已知表头、内容偏移 以及实际映射的 keys,根据读取出的表格数据,直接映射出数据导出即可
js
import Excel from 'exceljs'
import { saveFile } from './save-file'
//基础映射结构,多个个children用于前台
export type ExportExcelMultiColumns = {
title: string //标题名称 除了标题非必填,最深一层次的 prop 或者 transform还是要填写的
prop?: string | string[] //索引,例如: 'child'、'child.name'、['children', 'name'],使用有 transform 可以不填写key,但回调第一个参数就是undefined了
width?: number //实际算是间接设置文字长度了,英文1个多一点,中文两个多一点最佳,默认按照像素数量 / 10,并预留出部分空间,一般为 12-16号字体之间,这个略长一点
align?: 'left' | 'right' | 'center'
transform?: (value: unknown, record: Record<string, unknown>, index: number) => string | number //转化改动后的生成的文本
children?: ExportExcelMultiColumns[]
}
//支持多sheet 的结构
type ExcelMultiExportParams = {
columns: ExportExcelMultiColumns[]
datasource: Record<string, unknown>[]
sheetname?: string
}
type ExcelMultiExportOptions = {
creator?: string
filename?: string
saveFile?: (buffer: Excel.Buffer) => void
}
//column 最深一层的 prop或者transform必填,父节点只需要传递 title、children即可
export const exportMultiExcel = async (
params: ExcelMultiExportParams | ExcelMultiExportParams[],
options?: ExcelMultiExportOptions,
) => {
//归一化处理
if (!Array.isArray(params)) {
params = [params]
}
if (!params.find((e) => Array.isArray(e.datasource))) {
throw new Error('datasource数据源不存在')
}
const workbook = new Excel.Workbook()
workbook.creator = options?.creator ?? '剪刀石头布啊'
workbook.created = new Date()
//同时设置多个 sheet
params.forEach((param, index) => {
addMultiSheet(param, workbook, index)
})
//导出数据到本地
const buffer = await workbook.xlsx.writeBuffer()
if (options?.saveFile) {
options.saveFile(buffer)
} else {
saveFile(buffer, options?.filename)
}
}
//多表头导入的核心实现逻辑
function addMultiSheet(params: ExcelMultiExportParams, workbook: Excel.Workbook, index: number) {
// 添加工作表
const sheet = workbook.addWorksheet(params?.sheetname ?? 'sheet' + index)
//key相关
const infos = getExcelRealKeys(params.columns)
//对比校验表头
const headerColumns: Partial<Excel.Column>[] = []
//生成表头的数据
//设置表头,表头需要有标题,key,宽度,这里宽度就是列宽了
//表头设置完毕key之后,后续读取子内容时,就是根据表头的 key 进行映射,因此key很重要
infos.forEach((item) => {
headerColumns.push({
header: item.title,
key: item.keyInfo.key,
width: item.width ? item.width / 10 : 24, //不填写默认24长度
})
})
sheet.columns = headerColumns
//根据获取的 key 信息,用于映射数据
const data: Record<string, unknown>[] = []
params.datasource.forEach((item, index) => {
const obj: Record<string, unknown> = {}
infos.forEach((e, idx) => {
//映射数据,有key、keylist 就映射出实际的值
const key = infos[idx].keyInfo.key
const list = infos[idx].keyInfo.list
let value: unknown
if (list) {
let temValue = item
list.forEach((itm) => {
temValue = temValue[itm] as Record<string, unknown>
})
value = temValue
} else if (key) {
value = item[key]
}
//transfrom设置prop就映射内容,不设置就不映射,只能用第二个参数了
if (e.transform) {
//没有设置key
obj[key] = e.transform(item[key] as unknown, item, index)
} else {
obj[key] = value
}
})
data.push(obj)
})
sheet.addRows(data)
//更新第一级标题为空,在添加剩下的标题列
sheet.getRow(1).values = []
const level = getExcelLevel(params.columns)
for (let idx = 1; idx < level; idx++) {
sheet.insertRow(1, [])
}
//生成标题
addSheetTitles(sheet, params.columns, level)
//设置标题样式,第1行就是标题,没有0行,从 1~level行就是表头
sheet.getRows(1, level)?.forEach((row, index) => {
row.eachCell({ includeEmpty: true }, (cell: Excel.Cell, colNumber: number) => {
cell.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: {
argb: 'FFDEE6F0',
},
}
cell.font = {
size: 14,
color: { argb: 'FF000000' }, // 黑色
bold: true,
}
cell.border = {
top: { style: 'thin' },
left: { style: 'thin' },
bottom: { style: 'thin' },
right: { style: 'thin' },
}
cell.alignment = {
horizontal: index === level ? infos[colNumber - 1].align || 'center' : 'center',
vertical: 'middle',
}
})
})
sheet.getRows(level + 1, params.datasource.length)?.forEach((row) => {
row.eachCell({ includeEmpty: true }, (cell: Excel.Cell, colNumber: number) => {
cell.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: {
argb: 'FFFFFFFF',
},
}
cell.font = {
size: 12,
color: { argb: 'FF000000' }, // 黑色
}
cell.border = {
top: { style: 'thin' },
left: { style: 'thin' },
bottom: { style: 'thin' },
right: { style: 'thin' },
}
cell.alignment = {
horizontal: infos[colNumber - 1].align ?? 'center',
vertical: 'middle',
}
})
})
}
type ColumnKeyType = {
key: string
list?: string[]
}
type ColumnKeyInfoType = ExportExcelMultiColumns & {
keyInfo: ColumnKeyType
}
//生成 key、keylist 映射时使用
function generateColumnKey(column: ExportExcelMultiColumns): ColumnKeyType {
const info: ColumnKeyType = {
key: '',
}
if (column.transform) {
if (!column.prop) return info
} else if (!column.prop) {
throw new Error('需要设置prop属性,或使用transform函数转化')
}
let list: string[]
if (Array.isArray(column.prop)) {
list = column.prop
} else {
list = column.prop.split('.')
}
if (list.length < 2) {
info.key = list[0]
} else {
info.key = list.join('__')
info.list = list
}
return info
}
//获取设定标题层级
const getExcelLevel = (rules?: ExportExcelMultiColumns[], max = -1) => {
if (!rules || rules.length < 1) return max
rules.forEach((e) => {
const level = getExcelLevel(e.children, max)
if (level > max) {
max = level
}
})
return max + 1
}
//获取设定中有效的数据个数
const getExcelColumn = (rules?: ExportExcelMultiColumns[], max = 0) => {
if (!rules || rules.length < 1) return max + 1
rules.forEach((e) => {
const level = getExcelColumn(e.children, max)
if (level > max) {
max = level
}
})
return max
}
//递归遍历,生成实际有效的映射列
const getExcelRealKeys = (
rules?: ExportExcelMultiColumns[],
result: ColumnKeyInfoType[] = [],
level: number = 0,
) => {
if (!rules || rules.length < 1) return result
rules.forEach((rule) => {
if (rule.children) {
getExcelRealKeys(rule.children, result, level + 1)
} else if (rule.prop) {
//使用 generateColumnKey 加入映射关系
result.push({
title: rule.title,
width: rule.width,
align: rule.align,
transform: rule.transform,
keyInfo: generateColumnKey(rule),
})
} else {
throw new Error('存在未设置 prop 的基础标题')
}
})
return result
}
//生成用于检测的二维数组,结果和sheet中取出的表头一样(合并单元格的格子中每一个都是一样的)
const addSheetTitles = (
sheet: Excel.Worksheet,
rules: ExportExcelMultiColumns[],
maxLevel: number = 1,
row: number = 1,
column: number = 1,
) => {
rules.forEach((rule) => {
const columns = getExcelColumn(rule.children)
//设置标题,并横向纵向合并单元格,有子节点,就横向合并,没有就看是否到尾部,向下纵向合并
sheet.getCell(row, column).value = rule.title
if (rule.children) {
sheet.mergeCells([row, column, row, column + columns - 1, rule.title])
addSheetTitles(sheet, rule.children, maxLevel, row + 1, column)
} else if (row <= maxLevel) {
sheet.mergeCells([row, column, maxLevel, column, rule.title])
}
column += columns
})
}
ps
:导出多表头逻辑虽然相对复杂,但是也完成了,就算是十级表头也照导不误
导出到本地
这里导出到本地,自己写了一个简易的 saveFile
方法导出 .xls、xlsx
文件到本地,根据 filename
手动或者自动导出文件到本地,实际效果还不错,如果有想要有更好的方案
js
import Excel from 'exceljs'
export const saveFile = (buffer: Excel.Buffer, filename?: string) => {
//.xls 为 application/vnd.ms-excel
//.xlsx 为 application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
let type = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8'
if (filename) {
const list = filename.split('.')
//如果扩展名部位xls、xlsx 则默认改成 .xlsx
if (list.pop() === 'xls') {
type = 'application/vnd.ms-excel'
} else {
filename = list.join('.') + '.xlsx'
}
}
const blob = new Blob([buffer], {
type,
})
const url = URL.createObjectURL(blob)
const aLink = document.createElement('a')
aLink.setAttribute('download', filename ? filename : `${new Date().getTime()}.xlsx`)
aLink.setAttribute('href', url)
document.body.appendChild(aLink)
aLink.click()
document.body.removeChild(aLink)
URL.revokeObjectURL(url)
}
如果觉得上面的代码兼容性还是不够,可以考虑自己导入 file-saver
js
yarn add file-saver
js
import { saveAs } from 'file-saver';
saveAs(blob, '文件名.xlsx')
由于前面 options.saveFile
方法 返回的是 Buffer
类型数据,可以参考上面使用 new Blob
方法转化为 Blob 类型进行转化下载,也可以改进一下方即可
最后
本文给 exceljs 加入了更加复杂的多表头导入导出相关内容,比之前的文件还多了读取功能,喜欢的点赞收藏哈🤣