ExcelJS +file-saver 实现excel文件导出(前端,纯JS方式)

1. 版本说明

//  "exceljs": "^4.2.1"      https://www.npmjs.com/package/exceljs
//  "file-saver": "^2.0.5",
import ExcelJS from "exceljs";
import { saveAs } from "file-saver";

2. 简单导出(无合并单元格)

// 前置数据
const resList = [
  {applyForName:"张三",title:"报销主题1",/*....其他数据*/},
  {applyForName:"李四",title:"报销主题2",/*....其他数据*/},
  /*....其他数据*/
];
const columns = [
   {header:"序号",key:"index",width:10},
   {header:"报销人",key:"applyForName",width:20},
   {header:"报销主题",key:"title",width:40},
];
// ----------------------------------------------
   handleExport() {
    const resList =   this.resList.map((i, idx) => ({
          index: idx + 1,
          applyForName:i.applyForName,//报销人
          title:i.expenseTitle,//报销主题
          // ...其他数据
        }));

      this.commonExport("expenseTableRef", resList  , "报销单明细数据");

      // this.commonExport(columns, resList  , "报销单明细数据");
    },
    /**
     * 简单数据(无合并单元格)excel导出
     * @param elTableRef elTable ref对象
     * @param tableData elTable 数据
     * @param fileName 文件名
     */
    commonExport(elTableRef, tableData, fileName) {
      // 返回表头数组
      const getTableHeader = () => {
        return this.$refs[elTableRef].columns.map(column => column.label);
      };
      // 返回表格行数据数组
      const getTableRow = row => {
        return this.$refs[elTableRef].columns.map(
          column => row[column.property]
        );
      };
   
      const workbook = new ExcelJS.Workbook();  // 创建工作簿
      const worksheet = workbook.addWorksheet("Sheet");  // 创建工作表
      worksheet.addRow(getTableHeader());  // 添加表头

      // 添加表格数据
      tableData.forEach(row => { worksheet.addRow(getTableRow(row));   });

      // 定义表格样式
      worksheet.eachRow({ includeEmpty: true }, function(row, rowNumber) {
        row.eachCell({ includeEmpty: true }, function(cell, colNumber) {
          cell.border = { style: "thin" };
        });
      });

      // 定义每列的宽度
      worksheet.columns.forEach((column, index) => {
        column.width = index < 1 ? 10 : 20; // 假设前三列宽度为20,其余为30
      });

      // 定义工作表视图
      workbook.xlsx.writeBuffer().then(data => {
        // 使用 FileSaver 保存文件
        const blob = new Blob([data], {
          type:  "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8"
        });
        saveAs(blob, (fileName || "数据导出") + ".xlsx");
      });
    }

/**
     * 简单数据(无合并单元格)excel导出
     * @param columns 表头数据
     * @param tableData elTable 数据
     * @param fileName 文件名
     */
    commonExport2(columns, tableData, fileName) {
      // 返回表格行数据数组
      const getTableRow = row => {
        return columns.map(c=> row[c.key]);
      };
   
      const workbook = new ExcelJS.Workbook();  // 创建工作簿
      const worksheet = workbook.addWorksheet("Sheet");  // 创建工作表
      //// 添加表头 方式1
      // worksheet.addRow(columns.map(c=>c.header));  
      // 添加表头 方式2 ,自动读取width,style等参数,具体参照官网 https://www.npmjs.com/package/exceljs#styles
      worksheet.column = column;
      // 添加表格数据
      tableData.forEach(row => { worksheet.addRow(getTableRow(row));   });

      // 定义表格样式 
      worksheet.eachRow({ includeEmpty: true }, function(row, rowNumber) {
        if(rowNumber <= 1) {
            row.eachCell({ includeEmpty: true }, function(cell, colNumber) {
                 cell.border = { style: "thin" };
                  cell.font = {
                       bold: true, // 字体加粗
                       size: 12, // 字体大小
                       color: { argb: "FF000000" } // 字体颜色
                 };
                 cell.fill = {
                       type: "pattern",
                       pattern: "solid",
                       fgColor: { argb: "FFF0F0F0" } // 背景颜色
                  };
                 cell.alignment = {
                       horizontal: "center", // 水平居中
                       vertical: "middle" // 垂直居中
                  };
            });
        }else{
            row.eachCell({ includeEmpty: true }, function(cell, colNumber) {
              cell.border = { style: "thin" };
            });
        }
      });

      // 定义工作表视图
      workbook.xlsx.writeBuffer().then(data => {
        // 使用 FileSaver 保存文件
        const blob = new Blob([data], {
          type:  "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8"
        });
        saveAs(blob, (fileName || "数据导出") + ".xlsx");
      });
    }

2. 合并行导出(合并行)

// 前置数据
const resList = [
  {applyForName:"张三",title:"报销主题1",/*....其他数据*/,itemList:[
    {ticketNum:"发票号1",money:"20元",time:"2025-01-17"},
    {ticketNum:"发票号2",money:"25.5元",time:"2025-01-27"},
    {ticketNum:"发票号3",money:"55元",time:"2025-01-27"},
]},
  {applyForName:"李四",title:"报销主题2",/*....其他数据*/,itemList:[]},
  /*....其他数据*/
];
const columns = [
   {header:"序号",key:"index",width:10,fixed:true},// fixed表示是否合并行
   {header:"报销人",key:"applyForName",width:20,fixed:true},
   {header:"报销主题",key:"title",width:40,fixed:true},
   {header:"发票号",key:"ticketNum",width:20,},
   {header:"发票金额",key:"money",width:20,},
   {header:"开票时间",key:"time",width:20,},
];
// ----------------------------------------------
handleExport() {
   this.commonExport(columns, resList, "报销单明细数据");
},
commonExport(columns, tableData, fileName) {
  // 生成行数据
  const getRowData = row => {
    return columns.map(i => row[i.key]);
  };
  // 创建工作簿
  const workbook = new ExcelJS.Workbook(); 
 // 创建工作表
  const worksheet = workbook.addWorksheet("Sheet");  
 // 添加表头 此种方式自动添加表头(header:表头,key:对应值)
  worksheet.columns = columns.map(i => ({ ...i /* style: headerStyle */ })); 

  // 添加表格数据
  let currentIndex = 2, xh = 1; //从此行添加数据 这个是下表(from 0 to ~)
  tableData.forEach(rowData => {
          //rowData 第一层,itemList.item 第二层
      let itemList = rowData.itemList|| [];

      if (itemList.length == 0) { // 无子集数据
        worksheet.insertRow( currentIndex, getRowData({  xh, ...rowData }) );
        currentIndex++;
        xh++;
      } else { // 存在子集数据
        itemList.forEach((itemData, idx2) => {
                   //在currentIndex插入行数据
          worksheet.insertRow( currentIndex,
                         getRowData({ xh, ...rowData, ...itemData }) ); //如果存在相同key,需自行调整
                  // 子集数据全部写入完成后,合并行数据
          if (idx2 == itemList.length - 1) {
            // 写入完成后行合并,merge by start row, start column, end row, end column (equivalent to K10:M12)
            // 参考 https://www.npmjs.com/package/exceljs#merged-cells
            columns.forEach((c, cidx) => {
              if (c.fixed) {
               // merge by start row, start column, end row, end column (equivalent to K10:M12)
                worksheet.mergeCells(
                   currentIndex - itemList.length + 1, cidx + 1,
                  currentIndex,cidx + 1
                );
              }
            });
          }
          // ++
          currentIndex++;
          xh++;
        });
      }
    });

  // 定义表格样式
  worksheet.eachRow({ includeEmpty: true }, function(row, rowNumber) {
    if (rowNumber <= 1) { //表头加粗、背景色、居中
      row.eachCell({ includeEmpty: true }, function(cell, colNumber) {
        cell.border = { style: "thin" };
        cell.font = {
          bold: true, // 字体加粗
          size: 12, // 字体大小
          color: { argb: "FF000000" } // 字体颜色
        };
        cell.fill = {
          type: "pattern",
          pattern: "solid",
          fgColor: { argb: "FFF0F0F0" } // 背景颜色
        };
        cell.alignment = {
          horizontal: "center", // 水平居中
          vertical: "middle" // 垂直居中
        };
      });
    } else { //表数据居中
      row.eachCell({ includeEmpty: true }, function(cell, colNumber) {
        cell.border = { style: "thin" };
        cell.alignment = { vertical: "middle", horizontal: "center" };
      });
    }
  });

  // 定义工作表视图
  workbook.xlsx.writeBuffer().then(data => {
    // 使用 FileSaver 保存文件
    const blob = new Blob([data], {
      type:"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8"
    });
    saveAs(blob, (fileName || "数据导出") + ".xlsx");
  });
},
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,826评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,968评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,234评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,562评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,611评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,482评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,271评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,166评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,608评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,814评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,926评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,644评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,249评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,866评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,991评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,063评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,871评论 2 354

推荐阅读更多精彩内容