利用xlsx-syle前端導(dǎo)出excel且支持自定義樣式

利用xlsx-syle前端導(dǎo)出excel且支持自定義樣式

前言

本文的代碼是基于react的媒峡。
本文僅用于記錄我在前端導(dǎo)出excel遇到的問(wèn)題的筆記整理忍啸。

需求描述

需要前端來(lái)實(shí)現(xiàn)對(duì)數(shù)據(jù)的導(dǎo)出嗜价,導(dǎo)出成excel格式鸭廷。
excel打開(kāi)后的樣式要符合需求圖
需求圖:


導(dǎo)出1.png

前期解決過(guò)程

  • 我在網(wǎng)上找到前端用于導(dǎo)出的插件js-xlsx象颖,按照操作說(shuō)明鼓搗完成后導(dǎo)出excel的樣子只是最基礎(chǔ)的模板
導(dǎo)出2.png
  • 當(dāng)我想進(jìn)一步去修改樣式,讓他達(dá)到需求那樣豺瘤,結(jié)果發(fā)現(xiàn)xlsx插件開(kāi)源版不支持修改excel的樣式吆倦,需要付費(fèi)版才有支持功能,然而付費(fèi)要700美元坐求,果斷放棄蚕泽。
  • 后來(lái)我找到了xlsx-style插件可以代替,它支持修改樣式且是免費(fèi)的桥嗤。

中期解決過(guò)程

  • 然而xlsx-style插件不像js-xlsx插件那樣乖巧须妻,在安裝和引入階段也會(huì)有各種問(wèn)題
  • import引入xlsx-stle時(shí)一般都會(huì)報(bào)錯(cuò):This relative module was not found: ./cptable in ./node_modules/xlsx-style@0.8.13@xlsx-style/dist/cpexcel.js
    這個(gè)問(wèn)題需要去修改xlsx-style插件里的源碼,即要改動(dòng)node_modules文件夾砸逊。
  • 然而對(duì)于非本地demo的項(xiàng)目來(lái)說(shuō)璧南,node_modules因是存放依賴的,且很大师逸,代碼提交或上傳時(shí)都會(huì)忽略node_modules司倚,需要使用時(shí)才會(huì)安裝依賴,就算改動(dòng)xlsx-style的源碼篓像,依賴重新安裝后也會(huì)被覆蓋掉动知。
  • 這個(gè)問(wèn)題可以藉由將這個(gè)依賴的代碼從node_modules拿出來(lái)放在項(xiàng)目里,在最外層index.html里引入即可员辩。
  • 然而對(duì)于高版本的Hzero盒粮,最外層的index.html文件被隱藏了,無(wú)法引入奠滑,需要把node_modules/hzero-boot/public/index.html的index.html文件復(fù)制到外層public文件夾下丹皱,才可以使用。
    此時(shí)xlsx-style插件已經(jīng)全局引入了宋税,可以開(kāi)始寫(xiě)導(dǎo)出功能了摊崭。

導(dǎo)出功能的代碼

導(dǎo)出的方法

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個(gè)英文字母用來(lái)表示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];
    // 定制化改動(dòng)地方
    json.unshift({}, {}, {}, {}); // 向表格數(shù)據(jù)中插入4行位置(標(biāo)題和參數(shù))
    const keyMap = []; // 獲取keys
    for (const k in tmpdata) { // 為插入的4行位置添加數(shù)據(jù)
      keyMap.push(k);
      // 定制化改動(dòng)地方
      json[3][k] = k; // json[3][k] = k  3為插入4行的最后一行索引,用于展示列頭
    }
    var tmpdata = []; // 用來(lái)保存轉(zhuǎn)換好的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), // 表格數(shù)據(jù)的位置
            }
          );
        });
        return data;
      })
      .reduce((prev, next) => prev.concat(next))
      .forEach(
        (v, i) =>
          (tmpdata[v.position] = {
            v: v.v,
          })
      );
    let outputPos = Object.keys(tmpdata); // 設(shè)置區(qū)域,比如表格從A1到D10
    // 定制化改動(dòng)地方
    tmpdata.A1 = { v: options.dataTitle };  // A1-A4區(qū)域的內(nèi)容
    tmpdata.A2 = { v: options.reportCompany };
    tmpdata.A3 = { v: options.date };
    tmpdata.A4 = { v: options.reportType };
    // 定制化改動(dòng)地方
    outputPos = ['A1'].concat(['A2'], ['A3'], ['A4'], outputPos);
    // 定制化改動(dòng)地方
    tmpdata.A1.s = {
      font: { sz: 14, bold: true, vertAlign: true },
      alignment: { vertical: 'center', horizontal: 'center' }, // 垂直杰赛、水平
      fill: { bgColor: { rgb: 'E8E8E8' }, fgColor: { rgb: 'E8E8E8' } },
    }; // <====設(shè)置xlsx單元格樣式
    tmpdata.A2.s = {
      font: { sz: 12, bold: true, vertAlign: true },
      alignment: { vertical: 'center', horizontal: 'bottom' },
    }; // <====設(shè)置xlsx單元格樣式
    tmpdata.A3.s = {
      font: { sz: 12, bold: true, vertAlign: true },
      alignment: { vertical: 'center', horizontal: 'bottom' },
    }; // <====設(shè)置xlsx單元格樣式
    tmpdata.A4.s = {
      font: { sz: 12, bold: true, vertAlign: true },
      alignment: { vertical: 'center', horizontal: 'bottom' },
    }; // <====設(shè)置xlsx單元格樣式
    // s-e 代表區(qū)域 c-r 代表列-行的索引
    // 定制化改動(dòng)地方
    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 = []
    // 定制化改動(dòng)地方
    json.forEach(item => {
      dataArrWidth.push({ wpx: 100 })
    })
    tmpdata['!cols'] = dataArrWidth;// <====設(shè)置一列寬度呢簸, 代表20列都是300寬
    const tmpWB = {
      SheetNames: ['mySheet'], // 保存的表標(biāo)題
      Sheets: {
        mySheet: Object.assign(
          {},
          tmpdata, // 內(nèi)容
          {
            '!ref': `${outputPos[0]}:${outputPos[outputPos.length - 1]}`, // 設(shè)置填充區(qū)域(表格渲染區(qū)域)
          }
        ),
      },
    };
    const tmpDown = new Blob(
      [
        s2ab(
          XLSX.write(
            tmpWB,
            { bookType: type == undefined ? 'xlsx' : type.bookType, bookSST: false, type: 'binary' } // 這里的數(shù)據(jù)是用來(lái)定義導(dǎo)出的格式類(lèi)型
          )
        ),
      ],
      {
        type: '',
      }
    );
    // 定制化改動(dòng)地方
    saveAs(tmpDown, `${'超合同清賬&明細(xì)' + '.'}${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方法里進(jìn)行excel導(dǎo)出后的樣式自定義
所有注釋 // 定制化改定地方處即為樣式自定義時(shí)需要改動(dòng)的內(nèi)容,
常用樣式處也在代碼中進(jìn)行了注釋乏屯。

導(dǎo)出方法的使用

function handleClick() {
   // 模擬數(shù)據(jù)
    // 定制化改動(dòng)地方
    let data = [];
    for (let i = 0; i < 10; i++) {
      let obj = {
        '合同編號(hào)': '移YZ合同[2017]1491號(hào)',
        '合同所在地市': '揚(yáng)州',
        '合同名稱': '揚(yáng)州分公司與儀征技師學(xué)院簽訂的儀征技師學(xué)院室內(nèi)分布系統(tǒng)合作協(xié)議書(shū)',
        '是否補(bǔ)充協(xié)議': '否',
        '原合同編號(hào)': '',
        '原合同名稱': '',
        '供應(yīng)商編號(hào)': 'MDM_200123316',
        '供應(yīng)商名稱': '樂(lè)山市中心城區(qū)星飛探汽車(chē)裝飾用品店',
        '是否集采': 'PURCHASING_03',
        '主執(zhí)行部門(mén)名稱': '江蘇\揚(yáng)州\儀征分公司\技術(shù)業(yè)務(wù)支撐中心',
        '合同性質(zhì)': 'SINGLE_CONTRACT',
        '合同性質(zhì)': '單項(xiàng)合同',
        '關(guān)聯(lián)類(lèi)型': '未知',
        '收支類(lèi)型': '',
        '相對(duì)方類(lèi)型': '供應(yīng)商',
        '合同狀態(tài)': 7,
        '省公司所在部門(mén)': '江蘇\揚(yáng)州\儀征分公司',
        '合同總金額': 0,
        '調(diào)整后合同總金額': '',
        '在途支付金額': 0,
        '合同累計(jì)報(bào)賬金額': 217158.15,
        '合同累計(jì)支付金額': 217158.15,
        '超付金額=累計(jì)支付-合同總金額': 217158.15,
        '期初累計(jì)報(bào)賬金額': 91472.7,
        '期初累計(jì)支付金額': 91472.7,
        '總計(jì)': 125685.45,
      };
      data.push(obj);
    }
      // 表格標(biāo)題
      // 定制化改動(dòng)地方
      const options = {
        dataTitle: '超合同清賬&明細(xì)',
        reportCompany: `報(bào)賬公司: XXX`,
        date: `日期: XXXX-XX-XX`,
        reportType: `報(bào)賬單類(lèi)型: XXX`,
      }
      // 配置文件類(lèi)型
      const wopts = { bookType: 'xlsx', bookSST: true, type: 'binary', cellStyles: true };
      downloadExl(data, wopts, options);
    }

options 配置非表格數(shù)據(jù)的所有內(nèi)容
然后在downloadExl方法中對(duì)options的內(nèi)容進(jìn)行樣式排版
如此樣式不能滿足所需根时,修改 // 定制化改定地方注釋處即可,
更多內(nèi)容可以參考官網(wǎng)文檔 https://www.npmjs.com/package/xlsx-style
最后上效果圖:

導(dǎo)出3.png

總結(jié)

因?yàn)闀r(shí)間原因辰晕,本文只說(shuō)了下xlsx-style插件的使用前置和給出了個(gè)導(dǎo)出demo的部分代碼蛤迎,其實(shí)這個(gè)導(dǎo)出還有很多問(wèn)題沒(méi)有處理,比如導(dǎo)出20列5W條數(shù)據(jù)時(shí)就會(huì)程序崩潰伞芹,數(shù)據(jù)越多導(dǎo)出越慢囱井,理想是1-2萬(wàn)條。導(dǎo)出后的excel中數(shù)字不是數(shù)字格式花墩,需要手動(dòng)修改成數(shù)字。這個(gè)導(dǎo)出功能我現(xiàn)在還只是做了個(gè)demo召川,很多數(shù)據(jù)都是寫(xiě)死的,等這段時(shí)間忙完后胸遇,我會(huì)抽空把導(dǎo)出功能抽離成組件來(lái)更方便使用的


image.png
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末荧呐,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子纸镊,更是在濱河造成了極大的恐慌倍阐,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,386評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件逗威,死亡現(xiàn)場(chǎng)離奇詭異峰搪,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)凯旭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門(mén)概耻,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人罐呼,你說(shuō)我怎么就攤上這事鞠柄。” “怎么了嫉柴?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,704評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵厌杜,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我计螺,道長(zhǎng)夯尽,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,702評(píng)論 1 294
  • 正文 為了忘掉前任登馒,我火速辦了婚禮呐萌,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘谊娇。我一直安慰自己,他們只是感情好罗晕,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,716評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布济欢。 她就那樣靜靜地躺著,像睡著了一般小渊。 火紅的嫁衣襯著肌膚如雪法褥。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,573評(píng)論 1 305
  • 那天酬屉,我揣著相機(jī)與錄音半等,去河邊找鬼揍愁。 笑死,一個(gè)胖子當(dāng)著我的面吹牛杀饵,可吹牛的內(nèi)容都是我干的莽囤。 我是一名探鬼主播,決...
    沈念sama閱讀 40,314評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼切距,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼朽缎!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起谜悟,我...
    開(kāi)封第一講書(shū)人閱讀 39,230評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤话肖,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后葡幸,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體最筒,經(jīng)...
    沈念sama閱讀 45,680評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,873評(píng)論 3 336
  • 正文 我和宋清朗相戀三年蔚叨,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了床蜘。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,991評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡缅叠,死狀恐怖悄泥,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情肤粱,我是刑警寧澤弹囚,帶...
    沈念sama閱讀 35,706評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站领曼,受9級(jí)特大地震影響鸥鹉,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜庶骄,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,329評(píng)論 3 330
  • 文/蒙蒙 一毁渗、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧单刁,春花似錦灸异、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,910評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至逻淌,卻和暖如春么伯,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背卡儒。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,038評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工田柔, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留俐巴,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,158評(píng)論 3 370
  • 正文 我出身青樓硬爆,卻偏偏與公主長(zhǎng)得像欣舵,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子摆屯,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,941評(píng)論 2 355