利用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)后的樣式要符合需求圖
需求圖:
前期解決過(guò)程
- 我在網(wǎng)上找到前端用于導(dǎo)出的插件js-xlsx象颖,按照操作說(shuō)明鼓搗完成后導(dǎo)出excel的樣子只是最基礎(chǔ)的模板
- 當(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
最后上效果圖:
總結(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)更方便使用的