封装XLSX实现Excel导出功能,并将其封装成通用插件

81 阅读1分钟

/plugins/excel-export-plugin.js

import * as XLSX from 'xlsx'
import * as XLSX_STYLE from 'xlsx-style'

const ExcelExportPlugin = {
  install(Vue) {
    Vue.prototype.$exportExcel = function (name, dataMap, data) {
      const { headerList, keyList, colWidths } = parseHeader(dataMap)
      const list = [[name], headerList, ...data.map(row => keyList.map(key => row[key] ?? ''))]
      // 新建一个工作簿
      const wb = XLSX.utils.book_new()
      // 使用二维数组生成一个工作表
      const ws = sheet_from_array_of_arrays(list)
      // 合并单元格 => row为0  col0 至 col(headerList.length - 1)
      ws['!merges'] = [{ s: { r: 0, c: 0 }, e: { r: 0, c: headerList.length - 1 } }]
      // 设置每列的宽度(单位:px)
      ws['!cols'] = colWidths
      // 设置每行的高度(单位:px)
      const wsRows = []
      for (const i in list) {
        if (i == 0) {
          wsRows.push({ hpx: 100 }) // 首行高度为 100px
        } else if (i == 1) {
          wsRows.push({ hpx: 60 }) // 第二行高度为 60px
        } else {
          wsRows.push({ hpx: null }) // 其他行高度为 30px
        }
      }
      ws['!rows'] = wsRows

      // 设置单元格样式
      for (const key in ws) {
        if (['!ref', '!merges', '!cols', '!rows'].includes(key)) continue
        // 匹配表格第一行(注意 A1 单元已合并为一个单元),设置其样式
        if (key === 'A1') {
          ws[key] = {
            t: 's', // 设置单元格类型(type: b Boolean, e Error, n Number, d Date, s Text, z Stub)
            v: name, // 设置单元格内容(raw value (number, string, Date object, boolean))
            s: {
              // 设置单元格样式
              fill: {
                // 设置背景色
                fgColor: { rgb: '85ce61' }
              },
              font: {
                // 设置字体
                name: '等线', // 字体名称
                sz: 18, // 字体大小
                bold: true, // 字体是否加粗
                color: { rgb: '5e7ce0' } // 文字颜色
              },
              alignment: {
                // 设置居中
                horizontal: 'center', // 水平(向左、向右、居中)
                vertical: 'center', // 上下(向上、向下、居中)
                wrapText: false, // 设置单元格自动换行,目前仅对非合并单元格生效
                indent: 0 // 设置单元格缩进
              }
            }
          }
        } else if (key.match(/\d+/g)?.join('') === '2') {
          // 设置th单元格样式
          ws[key].s = getCellStyle('header')
        } else {
          // 设置td单元格样式
          ws[key].s = getCellStyle('body')
        }
      }

      // 在工作簿中添加工作表
      XLSX.utils.book_append_sheet(wb, ws, '第一页')
      // 使用 xlsx-style 写入文件方式,使得自定义样式生效
      const blob = new Blob([s2ab(XLSX_STYLE.write(wb, {
        bookType: 'xlsx', type: 'binary', cellStyles: true
      }))])
      // 导出 Excel 文件
      download(blob, `${name} - ${formatDate(new Date())}.xlsx`)
    }
  }
}

function parseHeader(dataMap) {
  const headerList = dataMap.map(item => item.label)
  const keyList = dataMap.map(item => item.key)
  const colWidths = dataMap.map(item => ({ wch: item.wch || 20 })) // 设置列宽 默认为20
  return { headerList, keyList, colWidths }
}

// 设置单元格样式
function getCellStyle(type) {
  return {
    // 单元格边框
    border: {
      top: { style: 'thin' }, bottom: { style: 'thin' },
      left: { style: 'thin' }, right: { style: 'thin' }
    },
    // 单元格背景色
    fill: { fgColor: { rgb: type === 'header' ? '85ce61' : 'ffffff' } },
    // 单元格字体
    font: { name: '微软雅黑', sz: type === 'header' ? 12 : 10 },
    // 设置单元格对齐方式
    alignment: { horizontal: 'center', vertical: 'center', wrapText: true }
  }
}

// 获取时间格式
function formatDate(date) {
  return date.getFullYear() +
    String(date.getMonth() + 1).padStart(2, '0') +
    String(date.getDate()).padStart(2, '0') +
    String(date.getHours()).padStart(2, '0') +
    String(date.getMinutes()).padStart(2, '0') +
    String(date.getSeconds()).padStart(2, '0')
}

// 文件流转换
function s2ab(s) {
  const buf = new ArrayBuffer(s.length)
  const view = new Uint8Array(buf)
  for (let i = 0; i < s.length; i++) view[i] = s.charCodeAt(i) & 0xff
  return buf
}

// 使用 a 标签下载文件
function download(blob, fileName) {
  const url = window.URL.createObjectURL(blob)
  const a = document.createElement('a')
  a.href = url
  a.download = fileName
  document.body.appendChild(a)
  a.click()
  document.body.removeChild(a)
  window.URL.revokeObjectURL(url)
}

// 使用二维数组生成一个工作表
function sheet_from_array_of_arrays(data) {
  const ws = {}
  const range = {
    s: { c: 10000000, r: 10000000 },
    e: { c: 0, r: 0 }
  }
  for (let R = 0; R !== data.length; ++R) {
    for (let C = 0; C !== data[R].length; ++C) {
      if (range.s.r > R) range.s.r = R
      if (range.s.c > C) range.s.c = C
      if (range.e.r < R) range.e.r = R
      if (range.e.c < C) range.e.c = C
      const cell = { v: data[R][C] }
      if (cell.v == null) continue
      const cell_ref = XLSX.utils.encode_cell({ c: C, r: R })
      cell.t = typeof cell.v === 'number' ? 'n' : 's'
      ws[cell_ref] = cell
    }
  }
  if (range.s.c < 10000000) ws['!ref'] = XLSX.utils.encode_range(range)
  return ws
}

export default ExcelExportPlugin

main.js 中注册插件

import Vue from 'vue'
import App from './App.vue'
import ExcelExportPlugin from './plugins/excel-export-plugin'

Vue.use(ExcelExportPlugin)

new Vue({
  render: h => h(App)
}).$mount('#app')

在组件中使用

this.$exportExcel(
  '订单理赔数据报表',
  [
    { label: '商品名称', key: 'goods_name', wch: 15 },
    { label: '商品价格', key: 'price' }, // 默认列宽 20
    { label: '商品数量', key: 'goods_num', wch: 10 },
  ],
  this.tableData
)