利用xlsx-syle前端导出excel且支持自定义样式

利用xlsx-syle前端导出excel且支持自定义样式

前言

本文的代码是基于react的。
本文仅用于记录我在前端导出excel遇到的问题的笔记整理。

需求描述

需要前端来实现对数据的导出,导出成excel格式。
excel打开后的样式要符合需求图
需求图:


导出1.png

前期解决过程

  • 我在网上找到前端用于导出的插件js-xlsx,按照操作说明鼓捣完成后导出excel的样子只是最基础的模板
导出2.png
  • 当我想进一步去修改样式,让他达到需求那样,结果发现xlsx插件开源版不支持修改excel的样式,需要付费版才有支持功能,然而付费要700美元,果断放弃。
  • 后来我找到了xlsx-style插件可以代替,它支持修改样式且是免费的。

中期解决过程

  • 然而xlsx-style插件不像js-xlsx插件那样乖巧,在安装和引入阶段也会有各种问题
  • import引入xlsx-stle时一般都会报错:This relative module was not found: ./cptable in ./node_modules/xlsx-style@0.8.13@xlsx-style/dist/cpexcel.js
    这个问题需要去修改xlsx-style插件里的源码,即要改动node_modules文件夹。
  • 然而对于非本地demo的项目来说,node_modules因是存放依赖的,且很大,代码提交或上传时都会忽略node_modules,需要使用时才会安装依赖,就算改动xlsx-style的源码,依赖重新安装后也会被覆盖掉。
  • 这个问题可以藉由将这个依赖的代码从node_modules拿出来放在项目里,在最外层index.html里引入即可。
  • 然而对于高版本的Hzero,最外层的index.html文件被隐藏了,无法引入,需要把node_modules/hzero-boot/public/index.html的index.html文件复制到外层public文件夹下,才可以使用。
    此时xlsx-style插件已经全局引入了,可以开始写导出功能了。

导出功能的代码

导出的方法

function saveAs(obj, fileName) {
    const tmpa = document.createElement('a');
    tmpa.download = fileName || '未命名';
    // 兼容ie
    if ('msSaveOrOpenBlob' in navigator) {
      window.navigator.msSaveOrOpenBlob(obj, '下载的文件名' + '.xlsx');
    } else {
      tmpa.href = URL.createObjectURL(obj);
    }
    tmpa.click();
    setTimeout(function() {
      URL.revokeObjectURL(obj);
    }, 100);
  }

function s2ab(s) {
    if (typeof ArrayBuffer !== 'undefined') {
      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;
    } else {
      const buf = new Array(s.length);
      for (let i = 0; i != s.length; ++i) buf[i] = s.charCodeAt(i) & 0xff;
      return buf;
    }
  }

// 获取26个英文字母用来表示excel的列
  function getCharCol(n) {
    const temCol = '';
    let s = '';
    let m = 0;
    while (n > 0) {
      m = (n % 26) + 1;
      s = String.fromCharCode(m + 64) + s;
      n = (n - m) / 26;
    }
    return s;
  }

function downloadExl(json, type, options) {
    var tmpdata = json[0];
    // 定制化改动地方
    json.unshift({}, {}, {}, {}); // 向表格数据中插入4行位置(标题和参数)
    const keyMap = []; // 获取keys
    for (const k in tmpdata) { // 为插入的4行位置添加数据
      keyMap.push(k);
      // 定制化改动地方
      json[3][k] = k; // json[3][k] = k  3为插入4行的最后一行索引,用于展示列头
    }
    var tmpdata = []; // 用来保存转换好的json
    json
      .map((v, i) => {
        const data = keyMap.map((k, j) => {
          return Object.assign(
            {},
            {
              v: v[k],
              position: (j > 25 ? getCharCol(j) : String.fromCharCode(65 + j)) + (i + 2), // 表格数据的位置
            }
          );
        });
        return data;
      })
      .reduce((prev, next) => prev.concat(next))
      .forEach(
        (v, i) =>
          (tmpdata[v.position] = {
            v: v.v,
          })
      );
    let outputPos = Object.keys(tmpdata); // 设置区域,比如表格从A1到D10
    // 定制化改动地方
    tmpdata.A1 = { v: options.dataTitle };  // A1-A4区域的内容
    tmpdata.A2 = { v: options.reportCompany };
    tmpdata.A3 = { v: options.date };
    tmpdata.A4 = { v: options.reportType };
    // 定制化改动地方
    outputPos = ['A1'].concat(['A2'], ['A3'], ['A4'], outputPos);
    // 定制化改动地方
    tmpdata.A1.s = {
      font: { sz: 14, bold: true, vertAlign: true },
      alignment: { vertical: 'center', horizontal: 'center' }, // 垂直、水平
      fill: { bgColor: { rgb: 'E8E8E8' }, fgColor: { rgb: 'E8E8E8' } },
    }; // <====设置xlsx单元格样式
    tmpdata.A2.s = {
      font: { sz: 12, bold: true, vertAlign: true },
      alignment: { vertical: 'center', horizontal: 'bottom' },
    }; // <====设置xlsx单元格样式
    tmpdata.A3.s = {
      font: { sz: 12, bold: true, vertAlign: true },
      alignment: { vertical: 'center', horizontal: 'bottom' },
    }; // <====设置xlsx单元格样式
    tmpdata.A4.s = {
      font: { sz: 12, bold: true, vertAlign: true },
      alignment: { vertical: 'center', horizontal: 'bottom' },
    }; // <====设置xlsx单元格样式
    // s-e 代表区域 c-r 代表列-行的索引
    // 定制化改动地方
    tmpdata['!merges'] = [
      {
        s: { c: 0, r: 0 },
        e: { c: 3, r: 0 },
      },
      {
        s: { c: 0, r: 1 },
        e: { c: 3, r: 1 },
      },
      {
        s: { c: 0, r: 2 },
        e: { c: 3, r: 2 },
      },
      {
        s: { c: 0, r: 3 },
        e: { c: 3, r: 3 },
      },
    ]; // <====合并单元格
    let dataArrWidth = []
    // 定制化改动地方
    json.forEach(item => {
      dataArrWidth.push({ wpx: 100 })
    })
    tmpdata['!cols'] = dataArrWidth;// <====设置一列宽度, 代表20列都是300宽
    const tmpWB = {
      SheetNames: ['mySheet'], // 保存的表标题
      Sheets: {
        mySheet: Object.assign(
          {},
          tmpdata, // 内容
          {
            '!ref': `${outputPos[0]}:${outputPos[outputPos.length - 1]}`, // 设置填充区域(表格渲染区域)
          }
        ),
      },
    };
    const tmpDown = new Blob(
      [
        s2ab(
          XLSX.write(
            tmpWB,
            { bookType: type == undefined ? 'xlsx' : type.bookType, bookSST: false, type: 'binary' } // 这里的数据是用来定义导出的格式类型
          )
        ),
      ],
      {
        type: '',
      }
    );
    // 定制化改动地方
    saveAs(tmpDown, `${'超合同清账&明细' + '.'}${type.bookType == 'biff2' ? 'xls' : type.bookType}`);
  }
  function s2ab(s) {
    if (typeof ArrayBuffer !== 'undefined') {
      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;
    } else {
      const buf = new Array(s.length);
      for (let i = 0; i != s.length; ++i) buf[i] = s.charCodeAt(i) & 0xff;
      return buf;
    }
  }

downloadExl方法里进行excel导出后的样式自定义
所有注释 // 定制化改定地方处即为样式自定义时需要改动的内容,
常用样式处也在代码中进行了注释。

导出方法的使用

function handleClick() {
   // 模拟数据
    // 定制化改动地方
    let data = [];
    for (let i = 0; i < 10; i++) {
      let obj = {
        '合同编号': '移YZ合同[2017]1491号',
        '合同所在地市': '扬州',
        '合同名称': '扬州分公司与仪征技师学院签订的仪征技师学院室内分布系统合作协议书',
        '是否补充协议': '否',
        '原合同编号': '',
        '原合同名称': '',
        '供应商编号': 'MDM_200123316',
        '供应商名称': '乐山市中心城区星飞探汽车装饰用品店',
        '是否集采': 'PURCHASING_03',
        '主执行部门名称': '江苏\扬州\仪征分公司\技术业务支撑中心',
        '合同性质': 'SINGLE_CONTRACT',
        '合同性质': '单项合同',
        '关联类型': '未知',
        '收支类型': '',
        '相对方类型': '供应商',
        '合同状态': 7,
        '省公司所在部门': '江苏\扬州\仪征分公司',
        '合同总金额': 0,
        '调整后合同总金额': '',
        '在途支付金额': 0,
        '合同累计报账金额': 217158.15,
        '合同累计支付金额': 217158.15,
        '超付金额=累计支付-合同总金额': 217158.15,
        '期初累计报账金额': 91472.7,
        '期初累计支付金额': 91472.7,
        '总计': 125685.45,
      };
      data.push(obj);
    }
      // 表格标题
      // 定制化改动地方
      const options = {
        dataTitle: '超合同清账&明细',
        reportCompany: `报账公司: XXX`,
        date: `日期: XXXX-XX-XX`,
        reportType: `报账单类型: XXX`,
      }
      // 配置文件类型
      const wopts = { bookType: 'xlsx', bookSST: true, type: 'binary', cellStyles: true };
      downloadExl(data, wopts, options);
    }

options 配置非表格数据的所有内容
然后在downloadExl方法中对options的内容进行样式排版
如此样式不能满足所需,修改 // 定制化改定地方注释处即可,
更多内容可以参考官网文档 https://www.npmjs.com/package/xlsx-style
最后上效果图:

导出3.png

总结

因为时间原因,本文只说了下xlsx-style插件的使用前置和给出了个导出demo的部分代码,其实这个导出还有很多问题没有处理,比如导出20列5W条数据时就会程序崩溃,数据越多导出越慢,理想是1-2万条。导出后的excel中数字不是数字格式,需要手动修改成数字。这个导出功能我现在还只是做了个demo,很多数据都是写死的,等这段时间忙完后,我会抽空把导出功能抽离成组件来更方便使用的


image.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,463评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,868评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,213评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,666评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,759评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,725评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,716评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,484评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,928评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,233评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,393评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,073评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,718评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,308评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,538评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,338评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,260评论 2 352