exceljs导入导出excel

前言

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)
    })

我们就按照上面实现,但是过程相对复杂,分解为下面几个步骤介绍

  1. 声明 column 的嵌套类型,根据这个类型嵌套传参,后续需要递归,用于获取宽度和深度,用于获取实际有效表头和内部映射数据
  2. getExcelLevel 方法用于获取表头的嵌套层级,方便更准去确认表头一共有几行,这样无论是用于校验还是读取数据都是很方便的
  3. generateSheetTitles 方法,根据嵌套表头,生成一个和 excel 数据一样的多维数组表头,用于校验表头是否匹配(由于多级表头相对复杂,暂不支持乱序映射,只能一模一样的表头进行映射),其中用到了 getExcelColumn方法,是获取子节点的占用的列数 column,方便横向填充内容
  4. getExcelRealKeys用于获取实际映射的有效节点也就是涉及到最后一行表头的节点,其为实际映射节点,其必然要有 prop 参数用于映射
  5. 已知表头、内容偏移 以及实际映射的 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,
  })

多表头相比于单表头逻辑相对复杂,就和读取单表头类似,下面分为几个步骤

  1. 声明 column 的嵌套类型,根据这个类型嵌套传参,后续需要递归,用于获取宽度和深度,用于获取实际有效表头和内部映射数据
  2. getExcelLevel 方法用于获取表头的嵌套层级,方便更准去确认表头一共有几行,这样无论是用于校验还是读取数据都是很方便的
  3. addSheetTitles 方法,则类似于读取表头的方法,会根据嵌套情况横向、纵向合并单元格, getExcelColumn方法,是获取子节点的占用的列数 column,方便横向填充内容
  4. getExcelRealKeys用于获取实际映射的有效节点也就是涉及到最后一行表头的节点,其为实际映射节点,其必然要有 prop 参数用于映射,因此也使用了 generateColumnKey 生成需要的映射关系 keyinfo 信息,方便后续取值
  5. 已知表头、内容偏移 以及实际映射的 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 加入了更加复杂的多表头导入导出相关内容,比之前的文件还多了读取功能,喜欢的点赞收藏哈🤣

相关推荐
卸任15 小时前
Vibe coding工具使用心得
前端·ai编程
brien916 小时前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance16 小时前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M16 小时前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写16 小时前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李16 小时前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易16 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
风骏时光牛马17 小时前
前沿视界
前端
kyriewen17 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员
陈随易17 小时前
3000 万人在用的翻译插件,重写一遍就是一门生意
前端·后端·程序员