使用xlsx-style导出Excel文件并设置样式

5,402 阅读5分钟

前言

​ 最近工作中接到一个需求,是将页面中的表格数据给整合成Excel文件给用户下载,这个问题最开始的想法是后端直接生成一个文件流就好了,但是他要求要有文件的格式和部分文本的样式。但是后端觉得麻烦,就丢给可怜又无助的前端来做。 ​ 本来先想着就直接使用sheetJS提供的js-xlsx来实现就好了,可是发现无法对表格进行样式渲染。(其实是可以实现的,不过要使用js-xlsx的Pro版本,这个版本是要钱的,作为一个贫穷公司的贫穷前端码农来说这个是可以的。没有啥是手写不能解决的。如果真的有,那就和产品硬钢换需求)而我就找到一个xlsx-style的开源库。 这个库其实是js-xlsx的一个分支,它可以对导出的Excel文件进行编辑样式,但是目前为止,还是不能编辑单元格的高度。不过这也不影响我们使用,毕竟还是可以编辑单元格的宽度,字体的样式,填充颜色,边框等等样式,还是可以使用的哦~

安装

因为我们开发的环境是electron+vue的,本身就自带node,所以就不用担心node相关的库我们使用不了。

yarn

yarn add xlsx-style

In the browser:

<script lang="javascript" src="dist/xlsx.core.min.js"></script>

With bower:

bower install js-xlsx-style#beta
导出Excel文件说明
单元格对象(cell-Object)
cellObject = {c:C,r:R}

其中c代表列号,r代表行号,单元格B5,用对象表示就是{c:1,r:5}。如果要是想表示单元格的范围,则可以使用{s:S,e:E},其中s表示第一个单元格即start,e表示最后一个单元格即end,这里面的s和e都是cellObject对象;;例如要表示A1:B5则可以用对象就是{s:{c:0,r:0},e:{c:1,r:4}}

工作表格对象(sheet-object)
sheetObject = {
  A1: {
    v: '单元格',
    t: 's',
    s: {
      font: {},
      fill: {},
      numFmt: {},
      alignment: {},
      border: {}
    }
  }
}

V: 表示单元格的值;

t:表示单元格值的类型,b:表示Boolean布尔值,n表示number数组,e表示error错误信息,s表示string字符串,d:表示date日期

s: 表示单元格的样式,后面会重点讲这个

当然单元格的属性肯定不会只有这些,但是对于一般的简单导出,这些其实就已经足够了,剩下的属性其实我们用的也不多

单元格属性

ok,看完这些基础信息配置后我们看看如何将一个数组给变成Excel文件的,只需要三步:打开冰箱,把大象放进冰箱,关上冰箱

//  表格中展示的原始数据
const tableArray = [
  {
    name: '张三',
    age: 14,
    gender: '男'
  },
  {
    name: '李四',
    age: 23,
    gender: '女'
  }
]

const title = ['姓名','年龄','性别']
第一步: 打开冰箱 => 现将原始数据转换成导出Excel需要的JSON格式
/**
	* 转换数据的函数
	* tableArray 需要转换的数据
	* title 表格展示的title
	* excludeKey 不需要导出的数据
	* backgroundRed 背景颜色为红色
*/
const ProcessingData = ({tableArray,title,excludeKey,backgroundRed}) => {
  if(!tableArray?.length) {
     return null
     }
  const excelTable = [title]
  // 获取转换的的数组
  const sheetData = tableArray.map((item, index) => {
    // lodash 方法自己按需引用
    const copyItem = cloneDeep(item)
    if (excludeKey) {
      delete copyItem[excludeKey]
    }
    const colArray = Object.values(copyItem)
    colArray.unshift(index + 1)
    return colArray
  })
  sheetData.unshift(...excelTable)
  const sheet = {}
  sheetData.forEach((item, index) => {
    item.forEach((sheetItem, key) => {
      const itemIndex = `${EnLetter[key]}${index + 1}`
      const s = { alignment: { vertical: 'center', horizontal: 'center', wrapText: true } }
      if (backgroundRed && backgroundRed.length && index === 1) {
        if (backgroundRed.some(item => item === key)) {
          // 注意设置颜色的时候要是ARGB格式
          s.fill = { fgColor: { rgb: 'FFFF0023' } }
          s.font = { color: { rgb: 'FFFFFFFF' } }
          s.alignment.vertical = 'left'
        }
      }
      sheet[itemIndex] = { v: sheetItem, t: 's', s }
    })
  })
  const sheetKeys = Object.keys(sheet)
  const ref = `${sheetKeys[0]}:${sheetKeys.pop()}`
  sheet['!ref'] = ref
  return { sheet, sheetData }
}

第二步:把大象放进冰箱 => 将JSON格式转换成blob格式文件

const base64ToBlob = s => {
  const bstr = atob(s)
  let n = bstr.length
  const u8Arr = new Uint8Array(n)
  while (n--) {
    u8Arr[n] = bstr.charCodeAt(n)
  }
  return new Blob([u8Arr], { type: 'xlsx' })
}

/**
 * 转换为文件
 * @param {Object} params
 * @param {Array} params.tableArray 表格中展示的数据
 * @param {Boolean} params.autoSerial 是否为自动序号
 * @param {String} params.fileName 文件名
 * @param {Array} params.title 文件表格展示的title
 * @param {Boolean} params.autoWidth 是否为自适应宽度
 * @param {Array} params.description 文件的描述
 * @param {Array} params.backgroundRed 背景标红的序号一般指title
 */
const JSON2ExcelFile = ({
  tableArray,
  title,
  autoWidth = true,
  errorToRed = true,
  description,
  backgroundRed,
  excludeKey
}) => {
  const { sheet, sheetData: excelArray } = ProcessingData({
    tableArray,
    title,
    description,
    excludeKey,
    errorToRed,
    backgroundRed
  })
  // 合并单元格描述的
  sheet['!merges'] = [
    {
      s: {
        c: 0,
        r: 0
      },
      e: {
        c: 7,
        r: 0
      }
    }
  ]
  // 设置自动高度
  if (autoWidth) {
    /* 设置worksheet每列的最大宽度 */
    excelArray.shift()
    const colWidth = excelArray.map(row =>
      row.map(val => {
        /* 先判断是否为null/undefined */
        if (val == null) {
          return {
            wch: 20
          }
        }
        if (val.toString().charCodeAt(0) > 255) {
          /* 再判断是否为中文 */
          return {
            wch: val.toString().length * 2 > 20 ? val.toString().length * 2 : 10
          }
        }
        return {
          wch: val.toString().length > 20 ? val.toString().length * 2 : 20
        }
      })
    )
    // /* 以第一行为初始值 */
    const result = colWidth[0]
    for (let i = 1; i < colWidth.length; i += 1) {
      for (let j = 0; j < colWidth[i].length; j += 1) {
        if (result[j].wch < colWidth[i][j].wch) {
          result[j].wch = colWidth[i][j].wch
        }
      }
    }
    sheet['!cols'] = result
  }
  const workbook = { Sheets: { 员工信息表: sheet }, SheetNames: ['员工信息表'] }
  const bouts = XLSXStyle.write(workbook, { type: 'base64', bookType: 'xlsx', cellStyles: true })
  const file = base64ToBlob(bouts)
  return file
}

第三步: 关上冰箱 => 将blob格式数据转化成文件

/**
 * 将错误信息导出为Excel表格,用于下载使用
 * @param {Object} params
 * @param {Array} params.tableArray 表格中展示的数据
 * @param {String} params.fileName 文件名
 * @param {Array} params.title 文件表格展示的title
 * @param {Boolean} params.autoWidth 是否为自适应宽度
 * @param {Array} params.description 文件的描述
 * @param {Array} params.backgroundRed 背景标红的序号
 */
const ExcelFile = ({
  tableArray,
  filename,
  title,
  autoWidth = true,
  errorToRed = true,
  description,
  backgroundRed,
  excludeKey
}) => {
 const file = JSON2ExcelFile({
    tableArray,
    autoWidth,
    title,
    errorToRed,
    description,
    backgroundRed,
    excludeKey
  })
  const homeDir = homedir()
  const options = {
    title: '保存Excel',
    defaultPath: `${homeDir}\\downloads\\${filename}.xlsx`,
    filters: [{ name: 'excel', extensions: ['xlsx'] }]
  }
  remote.dialog.showSaveDialog(options).then(({ filePath: filename }) => {
    if (!filename) return
    const reader = new FileReader()
    reader.readAsArrayBuffer(file)
    reader.onload = () => {
      const buffer = Buffer.from(reader.result)
      fs.writeFile(filename, buffer, {}, err => {
        if (err) {
          Message({
            showClose: true,
            message: '下载失败',
            type: 'error'
          })
          throw err
        }
        const vm = new Vue()
        const h = vm.$createElement
        const fileName = h('p', basename(filename))
        const notifier = Notification.success({
          title: '下载完成',
          duration: 0,
          dangerouslyUseHTMLString: true,
          message: h('div', [
            fileName,
            h('span', {
              style: {
                display: 'inline-block',
                color: '#409eff',
                cursor: 'pointer',
                margin: '10px 0 0'
              },
              domProps: {
                innerHTML: '在文件夹中显示'
              },
              on: {
                click: () => {
                  fs.access(filename, err => {
                    if (err) {
                      vm.$message.error({
                        message: '该文件不存在',
                        showClose: true
                      })
                    } else {
                      remote.shell.showItemInFolder(filename)
                    }
                    notifier.close()
                  })
                }
              }
            })
          ])
        })
      })
    }
  })
}

前端实现导出Excel文件,基本的实现逻辑是在现将展示的table的数据转换成Excel能够识别的json数据,将这些数据转换成文件需要的blob数据。将这个blob数据写成文件;

@startuml
start
:获取需要导出的数据;
:将数据转换为Excel能识别的json数据;
:将json转化为blob数据;
:将blob文件写成文件;
:导出文件;
stop
@enduml