前端生成Excel并下载

41 阅读4分钟

1、安装依赖

//如果是导出单个文件
npm install excelJs
npm install file-saver
 
//以压缩包形式导出
npm install excelJs
npm install jszip
npm install file-saver

2、实现

安装好相关依赖后,需要去设置文件的样式。

docFiles.js

//这里的data为每个文件的数据
/**
 * 设置配置项
 * @param {Array[Object]} data.list 表格数据
 * @param {String} sheetName sheet名称
 */
export const fundRelease = (data) => {
  let config = [];
  const header1 = [`${data.countyName}${data.townName}${data.villageName}农户参加农业生产托管财政补贴资金确认发放表`];
  let header2 = [
    `接受服务方(盖章):${data.villageGroup}    村级组织负责人签字:${data.villageGroupLeader}    服务组织签字(盖章):`,
  ];
  let header3 = ['序号', '姓名'];
  let oncItem = {
    //表格数据
    data: data.list,
    //表格每一列数据对应属性名
    fields: [
      "number",
      "name",
    ],
    headers: [header1, header2, header3],
    merges: [],
    attrs: [],
    view: [],
    //设置每列的宽度
    columnsWidth: [10, 10], 
    //sheet名
    sheetName: data.sheetName,
  };
  // 设置全表单元格边框,居中布局
  oncItem.attrs.push({
    rowStart: 0,
    rowEnd: data.farmers.length + 4,//这部分根据实际数据调整
    colStart: 0,
    colEnd: oncItem.fields.length - 1,
    attr: {
      alignment: { vertical: "middle", horizontal: "center", wrapText: true },
      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: 2,
    },
  ];
  oncItem.merges = tableItem;
  config.push(oncItem);
  return config;
};

这里定义了一个方法fundRelease ,接收到文件数据data后,对表格的行和列进行内容设置、合并等操作。表格需要的配置选项有以下几种:

配置项配置项配置项
headers[string]有几个表头就设置几个header,header以数组的形式展示,每一个元素代表一个单元格。
data[object]列表数据,循环部分。
fields[string]每一列数据对应属性名。
columnsWidth[number]每列的宽度。
sheetNamestringsheet名
attrs[object]表单样式设置。

文件的配置样式设置好之后,我们就可以去创建Excel文件了。

◇ 单个文件

这里我们先从创建单个文件开始,首先定义一个方法 createExcelByData用于创建excel文件,文件创建完之后通过saveAs()保存到本地。

// 封装exceljs
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)
  })
}

◇ 批量导出

import ExcelJS from 'exceljs'
import FileSaver from 'file-saver'
import JsZip from 'jszip'
//引入设置表格配置方法
import { fundRelease } from "@/utils/excel/fundRelease";
 
/**
 * 异步函数:批量下载包含多个文件夹的ZIP文件
 * 此函数用于将多个文件夹中的内容打包成一个ZIP文件,并下载到本地
 * 主要用途是当需要将一批文件(如报表)整理到一个文件夹中,并以ZIP格式下载时
 * 
 * @param {Array} folderList - 包含待处理文件夹信息的数组每个文件夹可能包含多个文件
 * @param {string} zipName - 打包后ZIP文件的名称,也是顶层文件夹的名称
 * @param {string} excelType - 文件类型,主要用于处理Excel文件
 */
 export async function downloadFilesZipWithFolder(folderList, zipName) {
  const zip = new JsZip();
   // 在ZIP中创建第一个文件夹
  const firstFolder = zip.folder(zipName);
 
  // 批量处理文件夹
  const exportPromise = folderList.map(async (item) => await handleFolder(firstFolder, item));
  await Promise.all(exportPromise);
 
  // 生成并下载 zip 文件
  zip.generateAsync({ type: "blob" }).then((blob) => {
    saveAs(blob, `${zipName}.zip`);
  });
}
 
/**
 * 生成文件夹
 * @param {*} preFolder 上一级目录的folder数据
 * @param {*} file 当前文件夹相关数据
 */
async function handleFolder(preFolder, file) {
  let nextFolder = preFolder.folder(file.name);
  let catalogue= file?.catalogue? file.catalogue: [];//文件夹一层数据
  let files = file?.sheets? file.sheets: [];//文件一层数据
  let folderPromises = []
  let promises = []
  if (catalogue.length) {
    folderPromises = catalogue.map(async (child) => {
      await handleFolder(nextFolder, child);  // 递归处理子文件夹
    });
  }
  if (files.length) {
    promises = files.map(async (file) => {
      await handleEachFile(nextFolder, file);  // 递归处理子文件夹
    });
  }
  await Promise.all([...promises, ...folderPromises]);
}
 
/**
 * 异步处理每个文件
 * 并将处理结果合并后导出为单个Excel文件
 * @param {string} preFolder - 前置文件夹路径,用于指定Excel文件的保存位置
 * @param {Object} file- 村庄对象,包含村庄名称和村庄小组信息
 * @param {boolean} excelType - Excel类型标志,决定处理村庄小组的方式
 */
async function handleEachFile(preFolder, file) {
  let tableArr = [];
  let excelPromise = [];
  for (let index = 0; index < file.group.length; index++) {
    let sheet= file.sheets[index];
    tableArr = tableArr.concat(fundRelease(sheet));
  }
  if (tableArr.length > 0) {
    await exportSingleExcel(tableArr, {
      fileName: `${file.name}`, rowHeightAble: true, rowHeight: 30
    }, preFolder);
  }
  tableArr = [];
}
 
/**
 * 导出单个Excel文件
 * @param {Object|Object[]} config - Excel配置对象或配置对象数组
 * @param {Object} opts - 选项对象
 * @param {Object} preFolder - 用于保存文件的前置文件夹对象
 */
 async function exportSingleExcel(config, opts, preFolder) {
  let {
    fileName,//文件名称
    rowHeightAble,//是否自定义行高度
    rowHeight,//行高度,rowHeightAble为true时生效
  } = Object.assign({
    fileName: `导出excel文件【${Date.now()}】.xlsx`,
    rowHeightAble: false,
  }, opts || {})
  if (!config) return;
  const options = {
    fileName,
    worksheets: [],
    rowHeightAble,
    rowHeight,
  }
  if (!Array.isArray(config)) {
    config = [config]
  } 
  for (let index = 0; index < config.length; index++) {
    const item = config[index];
    // 深拷贝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
    })
  }
  // 等待 writeBuffer 解析完成
  const writeBuffer = await createExcel(options);
 
  // 创建 blob 并添加到文件夹
  const blob = new Blob([writeBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
 
  // 使用 preFolder 对象的方法将文件保存
  preFolder.file(`${fileName}.xlsx`, blob);
 }
 
 /**
 * 异步函数,用于创建Excel文件
 * @param {Object} options - 包含工作表信息的选项对象
 * @returns {Promise<Buffer>} 返回生成的Excel文件的Buffer对象
 */
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]
      }
    }
    // 行高设置
    if (options.rowHeightAble) {
      sheet.eachRow({ includeEmpty: true }, function (row, rowNumber) {
        row.height = options.rowHeight;
      });
    }
  }
 
  // 生成excel文件
  let writeBuffer = await workbook.xlsx.writeBuffer()
  return await workbook.xlsx.writeBuffer();
}

应用页面

注释部分为批量导出代码

import { exportDataToExcel } from "@/utils/exportDataToExcel";
import { downloadFilesZipWithFolder} from '@/constants/excelsDownload.js'
import { fundRelease } from '@/utils/farmers'
 
/**导出的数据*/
let excelData={
    name:'sheet1',
    list:[]
}
 
/*let excelDataList=[
    {
        name:'目录1',
        catalogue:[
            {
                name:'目录1-1',
                catalogue:[
                    {
                        name:'文件4',
                        sheets:[
                            {
                                name:'sheet1',
                                list:[]
                            }
                        ]
                    }
                ]
            },
        ]
    },
    {
        name:'目录2',
        catalogue:[
            {
                name:'文件2',
                sheets:[
                    {
                        name:'sheet1',
                        list:[]
                    }
                ]
            },
            {
                name:'文件3',
                sheets:[
                    {
                        name:'sheet1',
                        list:[]
                    }
                ]
            },
        ]
    },
    {
        name:'文件1',
        sheets:[
            {
                name:'sheet1',
                list:[]
            }
        ]
    }
]*/
 
/**导出数据事件*/
function exportData(){
    let sheetName='sheet1'
    let tableArr = [];
    tableArr = tableArr.concat(fundRelease (excelData.value));
    let tableName = '信息表';
    exportDataToExcel(tableArr, `${tableName}.xlsx`);
    tableArr = [];
 
    //downloadFilesZipWithFolder(excelDataList, '确认表')
}

以上就是导出文件的全部内容了。