最近遇到一個(gè)比較麻煩的需求,前端實(shí)現(xiàn)excel導(dǎo)出,并且導(dǎo)出表格的表頭不確定,由用戶(hù)進(jìn)行設(shè)置。也就是說(shuō)表頭可能是一行也可能是兩行,并且有的標(biāo)題需要合并。導(dǎo)出效果如下圖所示:

查找對(duì)比
因?yàn)槭堑谝淮螌?shí)現(xiàn)這樣的功能,先在網(wǎng)上進(jìn)行了查找,發(fā)現(xiàn)了三種比較常用的方法:
1.安裝file-saver xlsx script-loader
如果想設(shè)置表格樣式的話(huà),需要同時(shí)安裝依賴(lài)xlsx-style,通常情況下安裝此依賴(lài)會(huì)報(bào)錯(cuò),需要進(jìn)行修改;
2.安裝vue-json-excel
這個(gè)插件看起來(lái)比較好上手,但是好像只適用于導(dǎo)出簡(jiǎn)單表頭,不支持多級(jí),如果導(dǎo)出表格簡(jiǎn)單的話(huà)大家可以嘗試一下。不過(guò)因?yàn)椴环衔业男枨?,所以我直接跳過(guò)了,不知道是否可以設(shè)置樣式;
3.安裝exceljs
只用安裝一個(gè)依賴(lài)并且支持修改樣式,我選擇了這個(gè)方法。
功能實(shí)現(xiàn)
1. 安裝依賴(lài)
npm install exceljs
2.設(shè)置表格數(shù)據(jù)格式
表格數(shù)據(jù)包含了表頭標(biāo)題和表里內(nèi)容,需要處理成固定格式。
請(qǐng)求后端拿到的表格數(shù)據(jù)格式:
[{
rate: 0.65,
name: "變更版本八",
avgs: 10,
priceIndexList: [
{indexYear: "2023", indexMonth: "01", indexValue: "1"}
{indexYear: "2023", indexMonth: "02", indexValue: "2"}
],
status: 0,
staffIndex: { indexYear: "2023", indexValue: "1" }
}]
需要拿到的表頭數(shù)據(jù)格式:
[{
name: "name",
label: "名稱(chēng)"
},{
name: "",
label: "價(jià)格指數(shù)",
children: [{
name: "",
label: "2023年",
children: [{
name: "price1",
label: "1月"
},{
name: "price2",
label: "2月"
}]
}]
}]
其中name為綁定的字段,類(lèi)似于table表格中的prop;label為name對(duì)應(yīng)的字段名稱(chēng),類(lèi)似于table表格中的label。
需要注意的是,導(dǎo)出表格和頁(yè)面展示不一樣,頁(yè)面展示的時(shí)候prop值我們可以使用a.b或者a[0].b展示,但是導(dǎo)出的時(shí)候name值不可以,必須使用單個(gè)字段。
需要拿到的表中數(shù)據(jù)格式:
可以直接為請(qǐng)求后端拿到的表格數(shù)據(jù),如果有需要處理的數(shù)據(jù)進(jìn)行處理即可。我請(qǐng)求到的數(shù)據(jù)中有三類(lèi)數(shù)據(jù)需要進(jìn)行處理。
第一種是返回為字典key,需要轉(zhuǎn)換成字典value值;
第二種是返回?cái)?shù)據(jù)格式為list,需要轉(zhuǎn)換成單個(gè)的數(shù)據(jù);
第三種是對(duì)象數(shù)據(jù),取出其中的值
let list = data.map(item=>{
// 一個(gè)一個(gè)判斷然后處理就可以,不要嫌麻煩
// 字典轉(zhuǎn)換
for (let key in item) {
if (key == "status" {
item[key] = this.selectDictLabel(this.yesOrNo, item[key]);
}
}
// list拆解
for (let i=0; i < 12; i++) {
item["price"+i] = item.priceIndexList[i].indexValue;
}
// 對(duì)象取值
item.staff = item.staffIndex ? item.staffIndex.indexValue : "";
})
不要嫌麻煩,前端實(shí)現(xiàn)導(dǎo)出本來(lái)就是一件麻煩的事情。
3.導(dǎo)出方法調(diào)用
this.exportMultiHeaderExcel(title, list); // title為設(shè)置好的表頭數(shù)據(jù),list為表中數(shù)據(jù)
導(dǎo)出方法中判斷表頭有幾行,合并單元格
由于導(dǎo)出方法過(guò)長(zhǎng),所以對(duì)代碼塊進(jìn)行了分割,其實(shí)都在這個(gè)方法中
exportMultiHeaderExcel(column,data) {
let keyArr = this.columns.map((item)=>{
return item.label;
}) // 一級(jí)表頭數(shù)組
let singleLen = keyArr.length; // 簡(jiǎn)單標(biāo)題長(zhǎng)度
let multiLen = 0; // 用來(lái)判斷是否有三級(jí)表頭
let doubLen = 0; // 用來(lái)判斷是否有二級(jí)表頭
let row1 = JSON.parse(JSON.stringify(keyArr));
let idx1 = row1.findIndex((item) => item == "價(jià)格指數(shù)");
if (idx1 > -1) {
row1.splice(idx1+1, 0, "", "", "", "", "", "", "", "", "", "", "");
singleLen--;
multiLen++;
}
let idx6 = row1.findIndex((item) => item == "期末人數(shù)");
if (idx6 > -1) {
singleLen--;
doubLen = 1;
}
let row2 = [];
let row3 = [];
},
- 導(dǎo)出方法中不同標(biāo)題處理
每一行必須保持相同的長(zhǎng)度,為了之后合并單元格,沒(méi)有數(shù)據(jù)的設(shè)置為空
// 判斷不同標(biāo)題進(jìn)行不同處理
if (multiLen && doubLen) { // 所有標(biāo)題都有
for (let i = 0; i < singleLen; i++) {
row2.push("");
row3.push("");
};
if (idx1 > -1) {
row2.splice(idx1+1, 0, this.indexYear+"年", "", "", "", "", "", "", "", "", "", "", "");
row3.splice(idx1+1, 0, "1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月");
}
if (idx6 > -1) {
row2.push(this.indexYear+"年");
row3.push("");
}
} else {...}
- 導(dǎo)出方法中創(chuàng)建工作表
// 創(chuàng)建excel
const workbook = new ExcelJS.Workbook();
// 創(chuàng)建工作表
const sheet = workbook.addWorksheet("sheet1");
// 添加表頭
sheet.getRow(1).values = row1;
if (row2 && row2.length) {
sheet.getRow(2).values = row2;
}
if (row3 && row3.length) {
sheet.getRow(3).values = row3;
}
- 導(dǎo)出方法中導(dǎo)出表頭設(shè)置
因?yàn)槲疫@只有三級(jí)標(biāo)題所以進(jìn)行了三次循環(huán),循環(huán)次數(shù)和標(biāo)題行數(shù)有關(guān)系,這里應(yīng)該可以簡(jiǎn)化,還沒(méi)想到具體方法
// 導(dǎo)出表頭設(shè)置
const headers = [];
column.forEach((item,index)=>{ //顯示標(biāo)題循環(huán)
if(item.children){ //有子級(jí)
item.children.forEach(itemChild=>{ //子級(jí)循環(huán)
if(itemChild.children){ //有子級(jí)
itemChild.children.forEach(itemChild1=>{ //子級(jí)循環(huán)
const obj = {
key:itemChild1.name, width: null
} //子級(jí)數(shù)據(jù)
const maxArr = [this.autoWidthAction(itemChild1.label)]
data.forEach(ite=>{ //顯示信息循環(huán)
const str = ite[itemChild1.name] ||'' //信息內(nèi)容
if(str){
maxArr.push(this.autoWidthAction(str))
}
})
obj.width = Math.max(...maxArr)+5
// 設(shè)置列名、鍵和寬度
headers.push(obj);
})
}else {
const obj = {
key:itemChild.name, width: null
} //子級(jí)數(shù)據(jù)
const maxArr = [this.autoWidthAction(itemChild.label)]
data.forEach(ite=>{ //顯示信息循環(huán)
const str = ite[itemChild.name] ||'' //信息內(nèi)容
if(str){
maxArr.push(this.autoWidthAction(str))
}
})
obj.width = Math.max(...maxArr)+5
// 設(shè)置列名、鍵和寬度
headers.push(obj);
}
})
}else {
const obj = {
key:item.name, width: null
} //標(biāo)題
const maxArr = [this.autoWidthAction(item.label)]
data.forEach(ite=>{
const str = ite[item.name] ||''
if(str){
maxArr.push(this.autoWidthAction(str))
} //內(nèi)容
})
obj.width = Math.max(...maxArr)+5
// 設(shè)置列名、鍵和寬度
headers.push(obj);
}
})
sheet.columns = headers;
sheet.addRows(data);
- 導(dǎo)出方法中合并單元格
由于excel列的排序,this.exTabHeader是我寫(xiě)的一個(gè)排序數(shù)組["A","B","C","D","E","F","G","H","I","J","K","L","M"...],方便數(shù)字與字母的對(duì)應(yīng)
// 合并單元格
// sheet.mergeCells(`D1:F1`); // 表示合并D列1行到F列1行
if (doubLen && multiLen) {
for (let i = 0; i < singleLen; i++) {
let start1 = this.exTabHeader[i]+1;
let end1 = this.exTabHeader[i]+3;
sheet.mergeCells(`${start1}`+":"+`${end1}`);
}
let start2 = this.exTabHeader[idx6]+2;
let end2 = this.exTabHeader[idx6]+3;
sheet.mergeCells(`${start2}`+":"+`${end2}`);
if (idx1 > -1) {
let start1 = this.exTabHeader[idx1]+1;
let end1 = this.exTabHeader[idx1+11]+1;
sheet.mergeCells(`${start1}`+":"+`${end1}`);
let start2 = this.exTabHeader[idx1]+2;
let end2 = this.exTabHeader[idx1+11]+2;
sheet.mergeCells(`${start2}`+":"+`${end2}`);
}
} else { ... }
- 導(dǎo)出方法中設(shè)置表格標(biāo)題樣式
// 表格樣式
for (let i = 0; i < row1.length; i++) {
let one = this.exTabHeader[i]+1;
sheet.getCell(`${one}`).font = { // 字體
color: { argb:'FFFFFFFF' },
bold: true
}
sheet.getCell(`${one}`).fill = { // 背景色
type: 'pattern',
pattern:'solid',
fgColor:{ argb:'FF808080' }
}
}
- 導(dǎo)出方法中寫(xiě)入文件
// 寫(xiě)入文件
workbook.xlsx.writeBuffer().then((data) => {
const blob = new Blob([data, { type: "application/vnd.ms-excel" }]);
if (window.navigator.msSaveOrOpenBlob) {
// msSaveOrOpenBlob方法返回boolean值
navigator.msSaveBlob(blob, filename + ".xlsx");
// 本地保存
} else {
const link = document.createElement("a"); // a標(biāo)簽下載
link.href = window.URL.createObjectURL(blob); // href屬性指定下載鏈接
link.download = "導(dǎo)出.xlsx"; // dowload屬性指定文件名
link.click(); // click()事件觸發(fā)下載
window.URL.revokeObjectURL(link.href); // 釋放內(nèi)存
}
});
autoWidthAction方法
autoWidthAction(val,width) {
if (val == null) {
width = 10;
} else if (val.toString().charCodeAt(0) > 255) {
/*if chinese*/
width = val.toString().length * 2;
} else {
width = val.toString().length;
}
return width
}
如果想設(shè)置其他的屬性,也可以查閱exceljs文檔進(jìn)行設(shè)置。
- 參考鏈接
前端導(dǎo)出Excel(自定義樣式、多級(jí)表頭、普通導(dǎo)出)
vue-admin-perfect
exceljs中文文檔|exceljs js中文教程|解析
ExcelJS 使用幫助文檔 - 心得體會(huì)
1.寫(xiě)這個(gè)功能之前其實(shí)先在網(wǎng)上查了兩天,因?yàn)橐郧岸际呛蠖俗鰧?dǎo)出。但是和我們經(jīng)理商量,他很堅(jiān)持的表示了由前端來(lái)做,沒(méi)辦法才開(kāi)始做。所以,如果沒(méi)做過(guò)還不得不做還是早開(kāi)始比較好。
2.因?yàn)楸容^著急,所以代碼寫(xiě)得比較冗余比較亂,其實(shí)有很多地方可以改進(jìn),看之后時(shí)間吧,如果有時(shí)間會(huì)進(jìn)行優(yōu)化,不過(guò)一般都是不了了之。
3.這個(gè)表格需求因?yàn)闃?biāo)題位置是固定的,也就是說(shuō)一定是一行標(biāo)題,三行標(biāo)題,兩行標(biāo)題排列。并且,三行標(biāo)題就是包含了12個(gè)子標(biāo)題,兩行標(biāo)題就是包含了一個(gè)子標(biāo)題,所以有些地方直接寫(xiě)的固定數(shù)值。如果是不確定的長(zhǎng)度,可能需要循環(huán)來(lái)判斷。如果只有一行簡(jiǎn)單標(biāo)題的話(huà)應(yīng)該會(huì)很棒,大家可以根據(jù)需求自行判斷使用什么方法。
4.希望導(dǎo)出多由后端實(shí)現(xiàn)。