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.WorkbookreadFile、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 加入了更加复杂的多表头导入导出相关内容,比之前的文件还多了读取功能,喜欢的点赞收藏哈🤣

相关推荐
四棱子6 分钟前
炫酷!18.5kb实现流体动画,这个开源项目让个人主页瞬间高大上!
前端·开源
Sparkxuan7 分钟前
封装WebSocket
前端·websocket
工呈士7 分钟前
Redux 实践与中间件应用
前端·react.js·面试
Nano7 分钟前
深入解析 JavaScript 数据类型:从基础到高级应用
前端
无羡仙7 分钟前
浮动与BFC容器
前端
xphjj8 分钟前
树形数据模糊搜索
前端·javascript·算法
刺客_Andy8 分钟前
React 第三十四节 Router 开发中 useLocation Hook 的用法以及案例详解
前端·react.js
我的div丢了肿么办8 分钟前
HarmonyOS鸿蒙tabBar的详细讲解
前端·javascript·harmonyos
皓子9 分钟前
海狸IM桌面端:AI辅助开发的技术架构实践
前端·electron·ai编程
Nano10 分钟前
优雅处理 JavaScript 异步问题的终极指南
前端