上班以来第一次做导出表格,而且我觉得真的巨难,必须写下来留念一下

27 阅读6分钟

首先就是看看实现的效果,一个文件中有以下四种表

image.png 2. image.png 3. image.png 4. image.png sheet

image.png

这四个表我给分成两种类型,一种是上下结构(1、2、4),一种是半包围结构(3),下面就开始说一说我是怎么做的

首先,我这边需求是点击导出表格按钮,需要填写一个实验地点,填写完成之后导出表格

<n-button type="info" size="tiny" @click="exportData" >
            导出数据
</n-button>
// 导出数据返回数据
const excelData: any = ref([])//:any 是应付ts的类型报错
const exportData = () => {
  openExport.value = true//这里是我填写地址的弹窗
  //调用接口返回数据假设为res
  excelData.value = res
}

此时我已经填写完地址并点击确认提交按钮,表格被导出

import {exportDataToExcel} from '@/utils/exportExcel/exportExcel.js'
const onSubmit = () => {
  ruleForm.value?.validate((errors) => {
    if (errors) {
      if (!excelData.value) {
        message.error('暂无可导出数据');
        return
      } else {
        let keys = Object.keys(excelData.value)
        let tableArr = []
        keys.map(key => {
          //newExportConfig 是导出表格前处理数据的
          tableArr = tableArr.concat(newExportConfig(excelData.value[key], key))
        })
        exportDataToExcel(tableArr, `${prop.current?.testName}.xlsx`)//这个是导出表格的方法
        tableArr = []
      }
    } else {
      message.error('请填写完整信息');
    }
  })
}

导出表格前处理数据的方法,这里是复制的网上别人分享的,然后在这个基础上进行的改动(这里边的拖拉机可靠性使用试验综合汇总表是半包围式的)

const newExportConfig = (data, key) => {
  // 如果导出前要处理数据,需要深克隆一份表格数据,然后进行处理
  let config: any = []
  if (key === 'maintenanceRecordSummary') {//保养信息	
    for (let i in data) {
    //header就是表头,有几个header就有几个表头
      const header1 = ['保养信息'];
      let header2: any = [`累计保养次数:${data[i].numberOfMaintenances}`, '', `累计保养时间:${toTimeCycle(data[i].cumulativeMaintenanceTime) || 0}`, ''];
      const header3 = ['序号', '保养时间', '保养时长', '保养内容'];
      data[i].maintenanceRecordList.forEach(item => {
      //toTimeCycle 是个时间转换方法,我别的文章有写,想看可以去扒拉扒拉
        item.maintenanceDuration = toTimeCycle(item.maintenanceDuration) || 0
      })
      let oncItem: any = ({
        data: getIndex(data[i].maintenanceRecordList),//内容
        fields: ["serialNumber", "maintenanceTime", "maintenanceDuration", 'maintenanceSubstance'],//每列内容对应的字段
        headers: [header1, header2, header3],//表头
        merges: [],
        attrs: [],
        view: [],
        columnsWidth: [20, 20, 30, 20],//表格宽度
        sheetName: `${i}-保养信息`//sheet名(见上图sheet)
      });

      // 设置全表单元格边框,居中布局
      oncItem.attrs.push({
        rowStart: 0,
        rowEnd: oncItem.data.length + 2,
        colStart: 0,
        colEnd: oncItem.fields.length - 1,
        attr: {
          alignment: { vertical: "middle", horizontal: "center" },
          border: {
            top: { style: "thin" },
            left: { style: "thin" },
            bottom: { style: "thin" },
            right: { style: "thin" }
          }
        }
      });
      // 设置表头填充颜色,字体加粗
      oncItem.attrs.push({
        rowStart: 0,
        rowEnd: 0,
        colStart: 0,
        colEnd: oncItem.fields.length - 1,
        attr: {
          fill: {
            type: "pattern",
            pattern: "solid",
            // fgColor: { argb: "99CCFF" }
            fgColor: {}
          },
          font: {
            bold: true
          }
        }
      });
      // row:行, col:列, rowspan: 合并的行数 , colspan: 合并的列数
      let tableItem = [
        {
          row: 0, col: 0, rowspan: 1, colspan: 4
        },
        {
          row: 1, col: 0, rowspan: 1, colspan: 2,
        },
        {
          row: 1, col: 2, rowspan: 1, colspan: 2,
        }
      ]

      oncItem.merges = tableItem
      config.push(oncItem)
    }
  }
  if (key === 'shiftSummary') { //拖拉机可靠性使用试验班次记录汇总表	
    let oncItem: any = {}
    for (let i in data) {
      // 处理表格内容
      data[i] = data[i].map(item => {
        return {
          timeFrame: item.timeFrame,//时间范围
          workingHours: timeCycleTo(item.workingHours) || 0,//	工作时长
          fuelConsumption: item.fuelConsumption,//燃油消耗量
          aveHourlyFuelConsumption: item.aveHourlyFuelConsumption,//平均小时油耗
          aveSpeed: item.aveSpeed,//平均速度
          area: item.area,//工作量
          workStatus0: item.workInformation.workStatus,
          stateAveHourlyFuelConsumption0: item.workInformation.stateAveHourlyFuelConsumption,
          duration0: timeCycleTo(item.workInformation.duration) || 0,
          stateAveSpeed0: item.workInformation.stateAveSpeed,
          workStatus1: item.blankLineInformation.workStatus,
          stateAveHourlyFuelConsumption1: item.blankLineInformation.stateAveHourlyFuelConsumption,
          duration1: timeCycleTo(item.blankLineInformation.duration) || 0,
          stateAveSpeed1: item.blankLineInformation.stateAveSpeed,
          workStatus2: item.idlingInformation.workStatus,
          stateAveHourlyFuelConsumption2: item.idlingInformation.stateAveHourlyFuelConsumption,
          duration2: timeCycleTo(item.idlingInformation.duration) || 0,
          stateAveSpeed2: item.idlingInformation.stateAveSpeed,
        }
      })
      const header1 = ['拖拉机可靠性使用试验班次记录汇总表']
      const header2 = ['序号', '时间', '工作时长', '燃油消耗量(kg)', '平均小时油耗(kg/h)', '平均速度(km/h)', '工作量(ha)', '状态', '时长', '平均小时油耗(kg/h)', '平均速度(km/h)', '状态', '时长', '平均小时油耗(kg/h)', '平均速度(km/h)', '状态', '时长', '平均小时油耗(kg/h)', '平均速度(km/h)']
      oncItem = ({
        data: getIndex(data[i]),
        fields: ['serialNumber', 'timeFrame', 'workingHours', 'fuelConsumption', 'aveHourlyFuelConsumption', 'aveSpeed', 'area', 'workStatus0', 'duration0', 'stateAveHourlyFuelConsumption0', 'aveSpeed0', 'workStatus1', 'duration1', 'stateAveHourlyFuelConsumption1', 'aveSpeed1', 'workStatus2', 'duration2', 'stateAveHourlyFuelConsumption2', 'aveSpeed2'],
        headers: [header1, header2],
        merges: [],
        attrs: [],
        view: [],
        columnsWidth: Array.from({ length: 20 }, () => 20),
        // protect: {},
        sheetName: `${i}-拖拉机可靠性使用试验班次记录汇总表`
      });
      // 设置全表单元格边框,居中布局
      oncItem.attrs.push({
        rowStart: 0,
        rowEnd: oncItem.data.length + 1,
        colStart: 0,
        colEnd: oncItem.fields.length - 1,
        attr: {
          alignment: { vertical: "middle", horizontal: "center" },
          border: {
            top: { style: "thin" },
            left: { style: "thin" },
            bottom: { style: "thin" },
            right: { style: "thin" }
          }
        }
      });
      // // 设置表头填充颜色,字体加粗
      oncItem.attrs.push({
        rowStart: 0,
        rowEnd: 0,
        colStart: 0,
        colEnd: oncItem.fields.length - 1,
        attr: {
          fill: {
            type: "pattern",
            pattern: "solid",
            fgColor: {}
          },
          font: {
            bold: true
          }
        }
      });

      // row:行, col:列, rowspan: 合并的行数 , colspan: 合并的列数
      let tableItem = [
        {
          row: 0, col: 0, rowspan: 1, colspan: 19
        },
      ]

      oncItem.merges = tableItem
      config.push(oncItem)
    }
  }
  if (key === 'testFailureSummary') {//拖拉机可靠性使用试验故障汇总表
    const header1 = ['拖拉机可靠性使用试验故障汇总表'];
    let header2: any = [`拖拉机型号:${data.modelName}       样机编号:${data.protoNumber}       试验日期:${data.testTime}`];
    const header3 = [`试验起止日期:${data.testStartAndEndTime}       制造厂:${data.manufacturersName}`];
    const header4 = [`试验地点:${form.value.position}       样机台数:${data.protoQuantity}       规定试验结尾时间:${data.endTime}h`];
    const header5 = ['序号', '试验样机编号', '故障名称', '拖拉机累计工作时间', '故障原因', '故障类别', '修复工作时间', '危害度系数K', '时间系数E']
    //处理时间
    data.failureSummaryLists.forEach(item => {
      item.workingHoursSum = toTimeCycle(item.workingHoursSum) || 0
      item.repairWorkingHours = toTimeCycle(item.repairWorkingHours) || 0
    })
    let oncItem: any = ({
      data: getIndex(data.failureSummaryLists),
      fields: ['serialNumber', 'protoNumber', 'failureName', 'workingHoursSum', 'failureCause', 'failureType', 'repairWorkingHours', 'hazardFactor', 'timeCoefficient'],
      headers: [header1, header2, header3, header4, header5],
      merges: [],
      attrs: [],
      view: [],
      columnsWidth: Array.from({ length: 10 }, () => 20),
      sheetName: '拖拉机可靠性使用试验故障汇总表'
    });

    // 设置全表单元格边框,居中布局
    oncItem.attrs.push({
      rowStart: 0,
      rowEnd: oncItem.data.length + 4,
      colStart: 0,
      colEnd: header5.length - 1,
      attr: {
        alignment: { vertical: "middle", horizontal: "center" },
        border: {
          top: { style: "thin" },
          left: { style: "thin" },
          bottom: { style: "thin" },
          right: { style: "thin" }
        }
      }
    });
    // 设置表头填充颜色,字体加粗
    oncItem.attrs.push({
      rowStart: 0,
      rowEnd: 0,
      colStart: 0,
      colEnd: oncItem.fields.length - 1,
      attr: {
        fill: {
          type: "pattern",
          pattern: "solid",
          // fgColor: { argb: "99CCFF" }
          fgColor: {}
        },
        font: {
          bold: true
        }
      }
    });
    // row:行, col:列, rowspan: 合并的行数 , colspan: 合并的列数
    let tableItem = [
      {
        row: 0, col: 0, rowspan: 1, colspan: header5.length
      },
      {
        row: 1, col: 0, rowspan: 1, colspan: header5.length,
      },
      {
        row: 2, col: 0, rowspan: 1, colspan: header5.length,
      },
      {
        row: 3, col: 0, rowspan: 1, colspan: header5.length,
      },
    ]
    oncItem.merges = tableItem
    config.push(oncItem)

  }
  if (key === 'synthesisSummary') {//拖拉机可靠性使用试验综合汇总表
    let newDate: any = []
    let header1: any = []
    let header2: any = []
    let header3: any = []
    data.forEach(item => {
      header1 = ['拖拉机可靠性使用试验综合汇总表'];
      header2 = [`拖拉机型号:${item.modelName}       制造厂:${item.manufacturersName}       试验起止日期:${item.testStartAndEndTime}`];
      header3 = [`试验样机编号`, '', '1', '2', '3'];
      //处理表格内容
      // 侧标表头的对应关系
      let labelList = [
        {
          key: "totalWorkingHours",
          label: "累计工作时间/h"
        },
        {
          key: "totalIdleTime",
          label: "累计空转时间/h"
        },
        {
          key: "totalMaintainingTime",
          label: "累计保养时间/h"
        },
        {
          key: "totalRepairingTime",
          label: "累计修复工作时间/h"
        },
        {
          key: "totalFuelConsumption",
          label: "累计耗油量/kg|燃油"
        },
        {
          key: "totalWorkLoadField",
          label: "累计工作量|田间/ha"
        },
        {
          key: "avgHourlyFuelConsumptionField",
          label: "平均小时燃油耗/(kg/h)|田间"
        },
        {
          key: "avgUnitWorkLoadFuelConsumptionField",
          label: "平均单位工作量燃油耗|田间/(kg/ha)"
        },
        {
          key: "workingLoadCoefficient",
          label: "田间作业平均负荷系数/%"
        },
        {
          key: "workingTimePercentage",
          label: "田间作业时间占总时间的百分比/%"
        }
      ]

      /**
       * 遍历循环  表头对应关系 
       * 根据后端返回数据的长度进行for in 遍历,得到每一列的数据
       * 设置每一列的字段 colItemData加上当前列的索引
       * 获取表头需要显示的内容,在 表头对应关系的 label中设置  如果有多个使用 | 进行分开
       * 设置每一行的数据, 需要判断 当前这一行是否有值,因为前面遍历了synthesisSummary 会先执行完synthesisSummary的第一项在执行第二项,如果有值就是他本身,如果没有值就是当前行的表头
       * 然后对当前行添加数据
       */

      labelList.forEach((item, index) => {
        for (let i = 0; i < data.length; i++) {
          let keys = 'colItemData' + (i + 1)
          let labelList = item.label.split('|')
          let header = {
            col1: labelList[0],
            col2: labelList[1] || '',
          }
          newDate[index] = newDate[index] ? newDate[index] : header
          newDate[index][keys] = data[i][item['key']]
        }
      })
    })

    let oncItem: any = ({
      data: newDate,
      fields: ['col1', 'col2', 'colItemData1', 'colItemData2', 'colItemData3'],
      headers: [header1, header2, header3],
      merges: [],
      attrs: [],
      view: [],
      columnsWidth: Array.from({ length: 10 }, () => 20),
      // protect: {},
      sheetName: '拖拉机可靠性使用试验综合汇总表'
    });

    // 设置全表单元格边框,居中布局
    oncItem.attrs.push({
      rowStart: 0,
      rowEnd: oncItem.data.length + 2,
      colStart: 0,
      colEnd: oncItem.fields.length - 1,
      attr: {
        alignment: { vertical: "middle", horizontal: "center" },
        border: {
          top: { style: "thin" },
          left: { style: "thin" },
          bottom: { style: "thin" },
          right: { style: "thin" }
        }
      }
    });
    // 设置表头填充颜色,字体加粗
    oncItem.attrs.push({
      rowStart: 0,
      rowEnd: 0,
      colStart: 0,
      colEnd: oncItem.fields.length - 1,
      attr: {
        fill: {
          type: "pattern",
          pattern: "solid",
          // fgColor: { argb: "99CCFF" }
          fgColor: {}
        },
        font: {
          bold: true
        }
      }
    });
    // row:行, col:列, rowspan: 合并的行数 , colspan: 合并的列数
    let tableItem = [
      {
        row: 0, col: 0, rowspan: 1, colspan: oncItem.fields.length
      },
      {
        row: 1, col: 0, rowspan: 1, colspan: oncItem.fields.length,
      },
      {
        row: 2, col: 0, rowspan: 1, colspan: 2,
      },
      {
        row: 3, col: 0, rowspan: 1, colspan: 2,
      },
      {
        row: 4, col: 0, rowspan: 1, colspan: 2,
      },
      {
        row: 5, col: 0, rowspan: 1, colspan: 2,
      },
      {
        row: 6, col: 0, rowspan: 1, colspan: 2,
      },
      {
        row: 11, col: 0, rowspan: 1, colspan: 2,
      },
      {
        row: 12, col: 0, rowspan: 1, colspan: 2,
      },
    ]
    oncItem.merges = tableItem
    config.push(oncItem)

  }
  return config;


}

导出表格的方法,是直接复制使用的别人写好的

// 封装exceljs
// const ExcelJS = require('exceljs');
// const FileSaver = require('file-saver');
import ExcelJS from 'exceljs'
import FileSaver from 'file-saver'

/**
 * 导出数据到Excel方法
 * @param {Array[Object]} config.data 表格数据
 * @param {Array[String]} config.fields 字段列表
 * @param {Array[String]} config.headers excel表头列表[[]],可以是多级表头[['A1','B1'],['A2','B2']]
 * @param {Array[Object]} config.merges 需要合并的单元格,需要考虑表头的行数[{row:1, col:1, rowspan: 1, colspan: 2}]
 * @param {Array[Object]} config.attrs 单元格样式配置
 * @param {Array[Object]} config.views 工作表视图配置
 * @param {Array[Number]} columnsWidth 每个字段列对应的宽度
 * @param {Object} config.protect 工作表保护【此配置会保护全表,一般推荐只针对单元格进行保护配置】
 * @param {String} sheetName 工作表名称,默认从sheet1开始
 * @param {String} fileName excel文件名称
 */
export function exportDataToExcel(config, fileName) {
 if (!config) return;
 const options = {
  fileName: fileName || `导出excel文件【${Date.now()}】.xlsx`,
  worksheets: []
  }
 if (!Array.isArray(config)) {
  config = [config]
  }
 config.forEach((item) => {
  // 深拷贝data【JSON.stringify有缺陷,可自行换成_.cloneDeep】
  const data = JSON.parse(JSON.stringify(item.data));
  const results = data.map(obj => {
   return item.fields.map(key => {
    return obj[key]
    })
   })
  // 生成完整excel数据
  let excelData = [];
  excelData = excelData.concat(item.headers).concat(results);
  // 单元格合并处理【excel数据的第一行/列是从1开始】
  let excelMerges = [];
  excelMerges = item.merges.map(m => {
   return [m.row + 1, m.col + 1, m.row + m.rowspan, m.col + m.colspan]
   })
  // 单元格配置处理【excel数据的第一行/列是从1开始】
  let excelAttrs = [];
  excelAttrs = item.attrs.map(attr => {
   attr.rowStart += 1;
   attr.rowEnd += 1;
   attr.colStart += 1;
   attr.colEnd += 1;
   return attr
   })
  options.worksheets.push({
   data: excelData,
   merges: excelMerges,
   attrs: excelAttrs,
   views: item.views,
   columnsWidth: item.columnsWidth,
   protect: item.protect,
   sheetName: item.sheetName
   })
  })
 createExcel(options)
}

// 创建Excel文件方法
async function createExcel(options) {
 if (!options.worksheets.length) return;
 // 创建工作簿
 const workbook = new ExcelJS.Workbook();
 for (let i = 0; i < options.worksheets.length; i++) {
  const sheetOption = options.worksheets[i];
  // 创建工作表
  const sheet = workbook.addWorksheet(sheetOption.sheetName || 'sheet' + (i + 1));
  // 添加数据行
  sheet.addRows(sheetOption.data);
  // 配置视图
  sheet.views = sheetOption.views;
  // 单元格合并处理【开始行,开始列,结束行,结束列】
  if (sheetOption.merges) {
   sheetOption.merges.forEach((item) => {
    sheet.mergeCells(item);
    });
   }
  // 工作表保护
  if (sheetOption.protect) {
   const res = await sheet.protect(sheetOption.protect.password, sheetOption.protect.options);
   }
  // 单元格样式处理
  if (sheetOption.attrs.length) {
   sheetOption.attrs.forEach((item) => {
    const attr = item.attr || {};
    // 获取开始行-结束行; 开始列-结束列
    const rowStart = item.rowStart;
    const rowEnd = item.rowEnd;
    const colStart = item.colStart;
    const colEnd = item.colEnd;
    if (rowStart) { // 设置行
     for (let r = rowStart; r <= rowEnd; r++) {
      // 获取当前行
      const row = sheet.getRow(r);
      if (colStart) { // 列设置
       for (let c = colStart; c <= colEnd; c++) {
        // 获取当前单元格
        const cell = row.getCell(c);
        Object.keys(attr).forEach((key) => {
         // 给当前单元格设置定义的样式
         cell[key] = attr[key];
         });
        }
       } else {
       // 未设置列,整行设置【大纲级别】
       Object.keys(attr).forEach((key) => {
        row[key] = attr[key];
        });
       }
      }
     } else if (colStart) { // 未设置行,只设置了列
     for (let c = colStart; c <= colEnd; c++) {
      // 获取当前列,整列设置【大纲级别】
      const column = sheet.getColumn(c);
      Object.keys(attr).forEach((key) => {
       column[key] = attr[key];
       });
      }
     } else {
     // 没有设置具体的行列,则为整表设置
     Object.keys(attr).forEach((key) => {
      sheet[key] = attr[key];
      });
     }
    })
   }
  // 列宽设置
  if (sheetOption.columnsWidth) {
   for (let i = 0; i < sheet.columns.length; i++) {
    sheet.columns[i].width = sheetOption.columnsWidth[i]
    }
   }
  }

 // 生成excel文件
 workbook.xlsx.writeBuffer().then(buffer => {
  // application/octet-stream 二进制数据
  FileSaver.saveAs(new Blob([buffer], { type: 'application/octet-stream' }), options.fileName)
  })
}

参考网站:juejin.cn/post/722951…