发布时间:2023-04-22 文章分类:WEB开发, 电脑百科 投稿人:王小丽 字号: 默认 | | 超大 打印

超详细纯前端导出excel并完成各种样式的修改(xlsx-style)

一杠正在上传…重新上传取消

2020年12月08日 17:53 ·  阅读 6247

一、前言

最近做的项目涉及到了excel的导出,在这块真的花了很多的时间,起初需求是不需要样式层面的修改的,所以选择了XLSX.JS,没有过多的去考虑样式的修改。但是随着项目进行,客户又提出了需要按照格式修改样式的需求,故而只能去查找相关修改excel样式的资料,本想直接用XLSX.js,但显然不行XLSX.js的基础版本只有宽高、合并单元格等比较基础的修改,要更加复杂样式的修改得升级pro版本,怎么说,在这上面花钱并不是咱的风格,在查找众多资料后,发现能导出xlsx文件且能满足需求中的样式修改的插件只有XLSX-STYLE了(或许还有合适的工具但我不知道的吧),但是XLSX-STYLE貌似已经许久不维护了,所以在上手使用的过程中还是有许多的问题出现。

二、梳理需求

在我的项目中,我需要导出页面中antd的表格,同时还需要对数据进行处理以达到统计的效果,导出的excel内容要根据需求来定,样式上需要修改的有宽度、高度、背景色、字体、字体大小、合并单元格、单元格边框、字体颜色、字体加粗居中等。同时需要兼容到ie11,根据需求的优先级,我们来排排序

三、前期准备

// 本部分代码来源CSDN
// xlsx-style版本0.8.13
// xlsx版本0.14.1 
//这是xlsx-style文件中的xlsx.js的需要修改的代码,是从xlsx文件夹中的xlsx.js中复制出来的
// write_ws_xml_data找到这个方法名字,全部替换
// 把xlsx中能修改高度的代码复制到xlsx-style中
var DEF_PPI = 96, PPI = DEF_PPI;
function px2pt(px) { return px * 96 / PPI; }
function pt2px(pt) { return pt * PPI / 96; }
function write_ws_xml_data(ws, opts, idx, wb) {
	var o = [], r = [], range = safe_decode_range(ws['!ref']), cell="", ref, rr = "", cols = [], R=0, C=0, rows = ws['!rows'];
	var dense = Array.isArray(ws);
	var params = ({r:rr}), row, height = -1;
	for(C = range.s.c; C <= range.e.c; ++C) cols[C] = encode_col(C);
	for(R = range.s.r; R <= range.e.r; ++R) {
		r = [];
		rr = encode_row(R);
		for(C = range.s.c; C <= range.e.c; ++C) {
			ref = cols[C] + rr;
			var _cell = dense ? (ws[R]||[])[C]: ws[ref];
			if(_cell === undefined) continue;
			if((cell = write_ws_xml_cell(_cell, ref, ws, opts, idx, wb)) != null) r.push(cell);
		}
		if(r.length > 0 || (rows && rows[R])) {
			params = ({r:rr});
			if(rows && rows[R]) {
				row = rows[R];
				if(row.hidden) params.hidden = 1;
				height = -1;
				if (row.hpx) height = px2pt(row.hpx);
				else if (row.hpt) height = row.hpt;
				if (height > -1) { params.ht = height; params.customHeight = 1; }
				if (row.level) { params.outlineLevel = row.level; }
			}
			o[o.length] = (writextag('row', r.join(""), params));
		}
	}
	if(rows) for(; R < rows.length; ++R) {
		if(rows && rows[R]) {
			params = ({r:R+1});
			row = rows[R];
			if(row.hidden) params.hidden = 1;
			height = -1;
			if (row.hpx) height = px2pt(row.hpx);
			else if (row.hpt) height = row.hpt;
			if (height > -1) { params.ht = height; params.customHeight = 1; }
			if (row.level) { params.outlineLevel = row.level; }
			o[o.length] = (writextag('row', "", params));
		}
	}
	return o.join("");
}
复制代码
// 从json转化为sheet,xslx中没有aoaToSheet的方法,该方法摘自官方test
function aoa_to_sheet(data){
	var defaultCellStyle = {
		font: { name: "Meiryo UI", sz: 11, color: { auto: 1 } },
		  border: {
			top: {
			style:'thin',
			color: { 
				  auto: 1 
				}
		   },
		   left: {
			style:'thin',
			color: { 
				  auto: 1 
				}
		   },
		   right: {
			style:'thin',
			color: { 
				  auto: 1 
				}
		   },
		   bottom: {
			style:'thin',
			color: { 
				  auto: 1 
				}
		   }
		  },
		  alignment: {
			 /// 自动换行
			wrapText: 1,
			  // 居中
			horizontal: "center",
			vertical: "center",
			indent: 0
	   }
	  };
	function dateNum(date){
		let year = date.getFullYear();
		let month = (date.getMonth()+1)>9?date.getMonth()+1:'0'+(date.getMonth()+1);
		let day = date.getDate()>9?date.getDate():'0'+date.getDate();
		return year+'/'+month+'/'+day;
	};
	const ws = {};
	const range = {s: {c:10000000, r:10000000}, e: {c:0, r:0 }};
	  for(let R = 0; R !== data.length; ++R) {
	  for(let C = 0; C !== data[R].length; ++C) {
		if(range.s.r > R) range.s.r = R;
		if(range.s.c > C) range.s.c = C;
		if(range.e.r < R) range.e.r = R;
		if(range.e.c < C) range.e.c = C;
		/// 这里生成cell的时候,使用上面定义的默认样式
		const cell = {v: data[R][C], s: defaultCellStyle};
		const cell_ref = XLSX.utils.encode_cell({c:C,r:R});
		/* TEST: proper cell types and value handling */
		if(typeof cell.v === 'number') cell.t = 'n';
		else if(typeof cell.v === 'boolean') cell.t = 'b';
		else if(cell.v instanceof Date) {
		cell.t = 'n'; cell.z = XLSX.SSF._table[14];
		cell.v = dateNum(cell.v);
		}
		else cell.t = 's';
		ws[cell_ref] = cell;
	  }
	}
	/* TEST: proper range */
	if(range.s.c < 10000000) ws['!ref'] = XLSX.utils.encode_range(range);
	return ws;
  };
复制代码

四、导出excel以及兼容ie11


export default function downLoadExcel(data, type, config, ele) {
    console.log(config, 'excel配置参数');
    var blob = IEsheet2blob(ws);
    if (IEVersion() !== 11) {
        openDownloadXLSXDialog(blob, `${type}.xlsx`);
    } else {
        window.navigator.msSaveOrOpenBlob(blob, `${type}.xlsx`);
    }
}
function dislodgeLetter(str) {
    var result;
    var reg = /[a-zA-Z]+/; //[a-zA-Z]表示bai匹配字母,dug表示全局匹配
    while (result = str.match(reg)) { //判断str.match(reg)是否没有字母了
        str = str.replace(result[0], ''); //替换掉字母  result[0] 是 str.match(reg)匹配到的字母
    }
    return str;
}
function IEsheet2blob(sheet, sheetName) {
    try {
        new Uint8Array([1, 2]).slice(0, 2);
    } catch (e) {
        //IE或有些浏览器不支持Uint8Array.slice()方法。改成使用Array.slice()方法
        Uint8Array.prototype.slice = Array.prototype.slice;
    }
    sheetName = sheetName || 'sheet1';
    var workbook = {
        SheetNames: [sheetName],
        Sheets: {}
    };
    workbook.Sheets[sheetName] = sheet;
    // 生成excel的配置项
    var wopts = {
        bookType: 'xlsx', // 要生成的文件类型
        bookSST: false, // 是否生成Shared String Table,官方解释是,如果开启生成速度会下降,但在低版本IOS设备上有更好的兼容性
        type: 'binary'
    };
    var wbout = XLSX.write(workbook, wopts);
    var blob = new Blob([s2ab(wbout)], {
        type: "application/octet-stream"
    });
    // 字符串转ArrayBuffer
    function s2ab(s) {
        var buf = new ArrayBuffer(s.length);
        var view = new Uint8Array(buf);
        for (var i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF;
        return buf;
    }
    return blob;
}
function getDefaultStyle() {
    let defaultStyle = {
        fill: {
            fgColor: {
                rgb: ''
            }
        },
        font: {
            name: "Meiryo UI",
            sz: 11,
            color: {
                rgb: ''
            },
            bold: true
        },
        border: {
            top: {
                style: 'thin',
                color: {
                    auto: 1
                }
            },
            left: {
                style: 'thin',
                color: {
                    auto: 1
                }
            },
            right: {
                style: 'thin',
                color: {
                    auto: 1
                }
            },
            bottom: {
                style: 'thin',
                color: {
                    auto: 1
                }
            }
        },
        alignment: {
            /// 自动换行
            wrapText: 1,
            // 居中
            horizontal: "center",
            vertical: "center",
            indent: 0
        }
    };
    return defaultStyle;
}
function IEVersion() {
    var userAgent = navigator.userAgent; //取得浏览器的userAgent字符串  
    var isIE = userAgent.indexOf("compatible") > -1 && userAgent.indexOf("MSIE") > -1; //判断是否IE<11浏览器  
    var isEdge = userAgent.indexOf("Edge") > -1 && !isIE; //判断是否IE的Edge浏览器  
    var isIE11 = userAgent.indexOf('Trident') > -1 && userAgent.indexOf("rv:11.0") > -1;
    if (isIE) {
        var reIE = new RegExp("MSIE (\\d+\\.\\d+);");
        reIE.test(userAgent);
        var fIEVersion = parseFloat(RegExp["$1"]);
        if (fIEVersion == 7) {
            return 7;
        } else if (fIEVersion == 8) {
            return 8;
        } else if (fIEVersion == 9) {
            return 9;
        } else if (fIEVersion == 10) {
            return 10;
        } else {
            return 6; //IE版本<=7
        }
    } else if (isEdge) {
        return 'edge'; //edge
    } else if (isIE11) {
        return 11; //IE11  
    } else {
        return -1; //不是ie浏览器
    }
}
function openDownloadXLSXDialog(url, saveName) {
    if (typeof url == 'object' && url instanceof Blob) {
        url = URL.createObjectURL(url); // 创建blob地址
    }
    var aLink = document.createElement('a');
    aLink.href = url;
    aLink.download = saveName || ''; // HTML5新增的属性,指定保存文件名,可以不要后缀,注意,file:///模式下不会生效
    var event;
    if (window.MouseEvent) event = new MouseEvent('click');
    else {
        event = document.createEvent('MouseEvents');
        event.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
    }
    aLink.dispatchEvent(event);
}
复制代码

五、xlsx的认识和使用

    //编码行号
    XLSX.utils.encode_row(2);  //"3"
    //解码行号
    XLSX.utils.decode_row("2"); //1
    //编码列标
    XLSX.utils.encode_col(2);  //"C"
    //解码列标 
    XLSX.utils.decode_col("A"); //0
    //编码单元格
    XLSX.utils.encode_cell({ c: 1, r: 1 });  //"B2"
    //解码单元格
    XLSX.utils.decode_cell("B1"); //{c: 1, r: 0}
    //编码单元格范围
    XLSX.utils.encode_range({ s: { c: 1, r: 0 }, e: { c: 2, r: 8 } });  //"B1:C9"
    //解码单元格范围
    XLSX.utils.decode_range("B1:C9"); //{s:{c: 1, r: 0},e: {c: 2, r: 8}}
复制代码
s:{
    font: { //字体相关样式
        name: '宋体', //字体类型
        sz: 11, //字体大小
        color: { rgb: '' }, //字体颜色
        bold: true, //是否加粗
    },
    fill: { //背景相关样式
        bgColor: { rgb: '' }, //背景色,填充图案背景
        fgColor: { rgb: '' }, //前背景色,项目中修改单元格颜色用这项
    },
    border: [{   //边框相关样式
    },{
    },{
    },{
    }],
    alignment: {  // 对齐方式相关样式
        vertical: 'center', //垂直对齐方式
        horizontal: 'center', //水平对齐方式
        wrapText: true,  //自动换行
    }
}
//更详细的配置参数如下图,图片来源知乎
复制代码

六、样式修改及相关封装

  if (config.merge.length != 0) {
        ws['!merges'] = config.merge;
    }
    ws['!cols'] = config.size.cols;
    if (config.myStyle.all) { //作用在所有单元格的样式,必须在最顶层,然后某些特殊样式在后面的操作中覆盖基本样式
        Object.keys(ws).forEach((item, index) => {
            if (ws[item].t) {
                ws[item].s = config.myStyle.all;
            }
        });
    }
    if (config.myStyle.headerColor) {
        if (config.myStyle.headerLine) {
            let line = config.myStyle.headerLine;
            let p = /^[A-Z]{1}[A-Z]$/;
            Object.keys(ws).forEach((item, index) => {
                for (let i = 1; i <= line; i++) {
                    if (item.replace(i, '').length == 1 || (p.test(item.replace(i, '')))) {
                        let myStyle = getDefaultStyle();
                        myStyle.fill.fgColor.rgb = config.myStyle.headerColor;
                        myStyle.font.color.rgb = config.myStyle.headerFontColor;
                        ws[item].s = myStyle;
                    }
                }
            });
        }
    }
    if (config.myStyle.specialCol) {
        config.myStyle.specialCol.forEach((item, index) => {
            item.col.forEach((item1, index1) => {
                Object.keys(ws).forEach((item2, index2) => {
                    if (item.expect && item.s) {
                        if (item2.includes(item1) && !item.expect.includes(item2)) {
                            ws[item2].s = item.s;
                        }
                    }
                    if (item.t) {
                        if (item2.includes(item1) && item2.t) {
                            ws[item2].t = item.t;
                        }
                    }
                });
            });
        });
    }
    if (config.myStyle.bottomColor) {
        if (config.myStyle.rowCount) {
            Object.keys(ws).forEach((item, index) => {
                if (item.indexOf(config.myStyle.rowCount) != -1) {
                    let myStyle1 = getDefaultStyle();
                    myStyle1.fill.fgColor.rgb = config.myStyle.bottomColor;
                    ws[item].s = myStyle1;
                }
            })
        }
    }
    if (config.myStyle.colCells) {
        Object.keys(ws).forEach((item, index) => {
            if (item.split('')[0] === config.myStyle.colCells.col && item !== 'C1' && item !== 'C2') {
                ws[item].s = config.myStyle.colCells.s;
            }
        })
    }
    if (config.myStyle.mergeBorder) { //对导出合并单元格无边框的处理
        let arr = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"]
        let range = config.myStyle.mergeBorder;
        range.forEach((item, index) => {
            if (item.s.c == item.e.c) { //行相等,横向合并
                let star = item.s.r;
                let end = item.e.r;
                for (let i = star + 1; i <= end; i++) {
                    ws[arr[i] + (Number(item.s.c) + 1)] = {
                        s: ws[arr[star] + (Number(item.s.c) + 1)].s
                    }
                }
            } else { //列相等,纵向合并
                let star = item.s.c;
                let end = item.e.c;
                for (let i = star + 1; i <= end; i++) {
                    ws[arr[item.s.r] + (i + 1)] = {
                        s: ws[arr[item.s.r] + (star + 1)].s
                    }
                }
            }
        });
    }
    if (config.myStyle.specialHeader) {
        config.myStyle.specialHeader.forEach((item, index) => {
            Object.keys(ws).forEach((item1, index1) => {
                if (item.cells.includes(item1)) {
                    ws[item1].s.fill = {
                        fgColor: {
                            rgb: item.rgb
                        }
                    };
                    if (item.color) {
                        ws[item1].s.font.color = {
                            rgb: item.color
                        };
                    }
                }
            });
        });
    }
    if (config.myStyle.heightLightColor) {
        Object.keys(ws).forEach((item, index) => {
            if (ws[item].t === 's' && ws[item].v && ws[item].v.includes('%') && !item.includes(config.myStyle.rowCount)) {
                if (Number(ws[item].v.replace('%', '')) < 100) {
                    ws[item].s = {
                        fill: {
                            fgColor: {
                                rgb: config.myStyle.heightLightColor
                            }
                        },
                        font: {
                            name: "Meiryo UI",
                            sz: 11,
                            color: {
                                auto: 1
                            }
                        },
                        border: {
                            top: {
                                style: 'thin',
                                color: {
                                    auto: 1
                                }
                            },
                            left: {
                                style: 'thin',
                                color: {
                                    auto: 1
                                }
                            },
                            right: {
                                style: 'thin',
                                color: {
                                    auto: 1
                                }
                            },
                            bottom: {
                                style: 'thin',
                                color: {
                                    auto: 1
                                }
                            }
                        },
                        alignment: {
                            /// 自动换行
                            wrapText: 1,
                            // 居中
                            horizontal: "center",
                            vertical: "center",
                            indent: 0
                        }
                    }
                }
            }
        });
    }
    if (config.myStyle.rowCells) {
        config.myStyle.rowCells.row.forEach((item, index) => {
            Object.keys(ws).forEach((item1, index1) => {
                let num = Number(dislodgeLetter(item1));
                if (num === item) {
                    ws[item1].s = config.myStyle.rowCells.s;
                }
            });
        });
    }
    Object.keys(ws).forEach((item, index) => {  //对所有的空数据进行处理,不让其显示null
        if (ws[item].t === 's' && !ws[item].v) {
            ws[item].v = '-';
        }
    });
复制代码

七、导出数据处理难点

    const countFormatedData = (formatedData, text) => {
    //formatedData是整个部门的数据,text是新添加的汇总行的名称在当前项目是小计或者合计
        if (formatedData.length != 0) {
            let result1 = {
                '1月': 0,
                '2月': 0,
                '3月': 0,
                '4月': 0,
                '5月': 0,
                '6月': 0,
                '7月': 0,
                '8月': 0,
                '9月': 0,
                '10月': 0,
                '11月': 0,
                '12月': 0,
                [T('上期实绩')]: 0,
                [T('下期实绩')]: 0,
                [T('年度实绩')]: 0,
                [T('上期目标')]: 0,
                [T('下期目标')]: 0,
                [T('年度目标')]: 0,
                ['共同1月']: 0,
                ['共同2月']: 0,
                ['共同3月']: 0,
                ['共同4月']: 0,
                ['共同5月']: 0,
                ['共同6月']: 0,
                ['共同7月']: 0,
                ['共同8月']: 0,
                ['共同9月']: 0,
                ['共同10月']: 0,
                ['共同11月']: 0,
                ['共同12月']: 0,
            }
            let result = formatedData.reduce((init, next) => {
                if (next['部署'] === T('小计')) {
                    return init;
                } else {
                    return {
                        '1月': init['1月'] + next['1月'],
                        '2月': init['2月'] + next['2月'],
                        '3月': init['3月'] + next['3月'],
                        '4月': init['4月'] + next['4月'],
                        '5月': init['5月'] + next['5月'],
                        '6月': init['6月'] + next['6月'],
                        '7月': init['7月'] + next['7月'],
                        '8月': init['8月'] + next['8月'],
                        '9月': init['9月'] + next['9月'],
                        '10月': init['10月'] + next['10月'],
                        '11月': init['11月'] + next['11月'],
                        '12月': init['12月'] + next['12月'],
                        [T('上期实绩')]: init[T('上期实绩')] + next[T('上期实绩')],
                        [T('下期实绩')]: init[T('下期实绩')] + next[T('下期实绩')],
                        [T('年度实绩')]: init[T('年度实绩')] + next[T('年度实绩')],
                        [T('上期目标')]: init[T('上期目标')] + next[T('上期目标')],
                        [T('下期目标')]: init[T('下期目标')] + next[T('下期目标')],
                        [T('年度目标')]: init[T('年度目标')] + next[T('年度目标')],
                        ['共同1月']: init['共同1月'] + next['共同1月'],
                        ['共同2月']: init['共同2月'] + next['共同2月'],
                        ['共同3月']: init['共同3月'] + next['共同3月'],
                        ['共同4月']: init['共同4月'] + next['共同4月'],
                        ['共同5月']: init['共同5月'] + next['共同5月'],
                        ['共同6月']: init['共同6月'] + next['共同6月'],
                        ['共同7月']: init['共同7月'] + next['共同7月'],
                        ['共同8月']: init['共同8月'] + next['共同8月'],
                        ['共同9月']: init['共同9月'] + next['共同9月'],
                        ['共同10月']: init['共同10月'] + next['共同10月'],
                        ['共同11月']: init['共同11月'] + next['共同11月'],
                        ['共同12月']: init['共同12月'] + next['共同12月'],
                    }
                }
            }, result1);
            result[T('上期达成率')] = result[T('上期目标')] ? `${(result[T('上期实绩')] / result[T('上期目标')] * 100).toFixed(1)}%` : '0%';
            result[T('下期达成率')] = result[T('下期目标')] ? `${(result[T('下期实绩')] / result[T('下期目标')] * 100).toFixed(1)}%` : '0%';
            result[T('年度达成率')] = result[T('年度目标')] ? `${(result[T('年度实绩')] / result[T('年度目标')] * 100).toFixed(1)}%` : '0%';
            result['NO.'] = text;
            result['部署'] = text;
            result[T("姓名")] = text;
            formatedData.push(result)
        }
        return formatedData;
    }
复制代码

利用reduce计算累加是我能想到最好的办法了,还是老样子,欢迎大家探讨更优的写法。

以下是我合并单元格配置的计算逻辑code:

exportData.forEach((item, index) => { //遍历找出合并需要的下标
//小计和合计需要合并,原因请看封面图
        if (item[1] === T('总计') || item[1] === T('小计')) {
            merge.push({
                s: {
                    r: index,
                    c: 0
                },
                e: {
                    r: index,
                    c: 2
                }
            });
            if (item[1] === T('小计')) {
            //config是我们之前已经介绍了的配置信息
                config.myStyle.rowCells.row.push(index + 1);
            }
        }
    });
    deptCount.forEach((item, index) => { //这里是重头戏,不进行一定的考量很容易计算错误
        if (index == 0) {
            merge.push({
                s: {
                    r: 2,
                    c: 1
                },
                e: {
                    r: item + 1,
                    c: 1
                }
            });
        } else {
            let count = 0;
            for (let i = 0; i < index; i++) {
                count = count + deptCount[i];
            }
            if (item > 1) {
                merge.push({
                    s: {
                        r: count + index + 2,
                        c: 1
                    },
                    e: {
                        r: count + item + index + 1,
                        c: 1
                    }
                });
            }
        }
    })
复制代码

项目中还有一个表格是不需要小计的,那自然比需要小计的简单些,这里就不再介绍了。

export function removeDuplicate(data,field) {
    let newData = data //去重
    let shouldDelete = [];  //将所有需要删除的节点放到数组,根据title判断删除节点
    for (let i = 0; i < newData.length; i++) {
        for (let j = i + 1; j < newData.length; j++) {
            if (newData[i][field] == newData[j][field]) {
                shouldDelete.push({[field]:newData[i][field],index:i});
            }
        }
    }
    console.log(shouldDelete);
    if (shouldDelete.length != 0) {
        newData = newData.filter((item,index) => {
            let result = true;
            shouldDelete.forEach((item1) => {
                if (item[field] === item1[field]&&index==item1.index) {
                    result = false;
                }
            });
            return result;
        });
    }
    return newData;
}
复制代码

第二种,和第一种不一样的地方在于,它要判断元素的userName字段是否包含,包含的话去掉两个元素之间的元素和前面那个元素,具体看代码:

export function duplicateRemove(flowData) {
    let newFlowInfo = flowData //去重
    let shouldDelete = [];  //将所有需要删除的节点放到数组,根据title判断删除节点
    for (let i = 0; i < newFlowInfo.length; i++) {
        for (let j = i + 1; j < newFlowInfo.length; j++) {
            let name = newFlowInfo[i].userName.split('、').concat(newFlowInfo[j].userName.split('、'));
            let newName = Array.from(new Set(name));
            if (name.length != newName.length //有重复项
                &&
                newFlowInfo[i].code && newFlowInfo[i].code !== 'Committee' &&
                newFlowInfo[j].code && newFlowInfo[j].code !== 'Committee') {
                for (let k = i; k < j; k++) {
                    shouldDelete.push(newFlowInfo[k]);
                }
            }
        }
    }
    console.log(shouldDelete);
    if (shouldDelete.length != 0) {
        newFlowInfo = newFlowInfo.filter((item) => {
            let result = true;
            shouldDelete.forEach((item1) => {
                if (item.title === item1.title) {
                    result = false;
                }
            });
            return result;
        });
    }
    return newFlowInfo;
}
复制代码

第三种,这种去除UPN相同的项,最简洁:

               //结果去重
                let data = {};
                result.data.data = result.data.data.reduce((cur, next) => {
                    data[next.UPN] ? "" : data[next.UPN] = true && cur.push(next);
                    return cur;
                }, []);
复制代码

以上是我在项目中用到的去重方式,当然还有其它方式,这里单独拿几个出来展示一下。

八、总结

excel导出的功能很多前端项目需要用到,奈何市面上很少能符合大部分需求的工具,没办法我们只能自己折腾,所幸折腾来折腾去最终还是满足要求了,当前的处理虽然还很粗糙,不过继续完善相信这次项目用到的东西在以后的需求也可以再次使用,我还得花多点时间处理封装一个更用起来更加便捷的excel导出功能模块,大家有什么建议或者疑问记得评论区见,晚点我会将我书写的excel导出代码和处理好的xlsx-style放到github上供大家使用和完善,今天就到这里啦(^_^)

项目资料:github.com/wannigebang…

更新: 改进后的xlsx-style发布到npm上啦!安装命令npm install xlsx-style-medalsoft

九、更新

最近对功能模块代码做了整理,添加了类型判断以及多表导出,不过因为改动较多 ,代码变得有点复杂了,逻辑肯定还有优化空间,以后有机会再做一次整理吧,先将最新的放上来先

import XLSX from 'xlsx-style';
// npm install xlsx-style-medalsoft
interface IStyle {
    font?: any;
    fill?: any;
    border?: any;
    alignment?: any;
}
interface ICell {
    r: number;
    c: number;
}
interface IMerge {
    s: ICell;
    e: ICell
}
interface ICol {
    wpx?: number;
    wch?: number;
}
interface ISize {
    cols: ICol[];
    rows: any[];
}
interface ISpecialHeader {
    cells: string[];
    rgb: string;
    color?: string;
}
interface ISpecialCol {
    col: string[];
    rgb: string;
    expect: string[];
    s: IStyle;
    t?: string;
}
interface IRowCells {
    row: string[];
    s: IStyle;
}
interface IMyStyle {
    all?: IStyle;
    headerColor?: string;
    headerFontColor?: string;
    headerLine?: number;
    bottomColor?: string;
    rowCount?: number;
    heightLightColor?: string;
    rowCells?: IRowCells;
    specialHeader?: ISpecialHeader[];
    specialCol?: ISpecialCol[];
    mergeBorder?: any[];
}
interface IConfig {
    merge?: IMerge[];
    size?: ISize;
    myStyle?: IMyStyle;
}
/*
 * @function 导出excel的方法
 * @param data table节点||二维数组 
 * @param type 导出excel文件名
 * @param config 样式配置参数 { all:{all样式的基本格式请参考xlsx-style内xlsx.js文件内的defaultCellStyle}
 *  merge:[s:{r:开始单元格行坐标,c:开始单元格列坐标},e:{r:结束单元格行坐标,c:结束单元格列坐标}]
 *  size:{col:[{wpx:800}],row:[{hpx:800}]},
 *  headerColor: 八位表格头部背景颜色, headerFontColor: 八位表格头部字体颜色,bottomColor:八位表格底部背景色,
 *  rowCount:表格底部行数, specialHeader: [{cells:[特殊单元格坐标],rgb:特殊单元格背景色}],
 *  sepcialCol:[{col:[特殊列的列坐标],rgb:特殊列的单元格背景色,expect:[特殊列中不需要修改的单元格坐标],s:{特殊列的单元格样式}}
 *   }]
 *
 *   导出流程  table节点|二维数组->worksheet工作表对象->workboo工作簿对象->bolb对象->uri资源链接->a标签下载或者调用navigator API下载
 *   每个worksheet都是由!ref、!merge、!col以及属性名为单元格坐标(A1,B2等)值为{v:单元格值,s:单元格样式,t:单元格值类型}的属性组成
 */
export function downLoadExcel(exportElement: [][] | any, fileName: string, config: IConfig|IConfig[], multiSheet?: boolean, sheetNames?: string[]) {
    let ws;
    let wb = [];
    if (multiSheet) {
        exportElement.forEach((item, index) => {
            wb.push(getSheetWithMyStyle(item, config[index]));
        });
    } else {
        if(!Array.isArray(config)){
            ws = getSheetWithMyStyle(exportElement, config);
        }
    }
    console.log(ws, 'worksheet数据');
    if (ws) {
        downLoad([ws], fileName,sheetNames);
    } else {
        downLoad(wb, fileName, sheetNames);
    }
}
export function downLoad(ws, fileName: string, sheetNames?: string[]) {
    var blob = IEsheet2blob(ws, sheetNames);
    if (IEVersion() !== 11) { //判断ie版本
        openDownloadXLSXDialog(blob, `${fileName}.xlsx`);
    } else {
        window.navigator.msSaveOrOpenBlob(blob, `${fileName}.xlsx`);
    }
}
export function getWorkSheetElement(exportElement: [][] | any) {
    let ifIsArray = Array.isArray(exportElement);
    let ws = ifIsArray ? XLSX.utils.aoa_to_sheet(exportElement) : XLSX.utils.table_to_sheet(exportElement);
    return ws;
}
export function getSheetWithMyStyle(exportElement: [][] | any, config: IConfig, callback?: Function) {
    //样式处理函数,返回ws对象,如果要对ws对象进行自定义的修改,可以单独调用此函数获得ws对象进行修改
    try {
        let ws = getWorkSheetElement(exportElement);
        console.log(config, 'excel配置参数');
        //根据data类型选择worksheet对象生成方式
        if (config.merge) {
            ws['!merges'] = config.merge;
        }
        ws['!cols'] = config.size.cols;
        //all样式的基本格式请参考xlsx-style内xlsx.js文件内的defaultCellStyle
        if (config.myStyle) {
            if (config.myStyle.all) { //作用在所有单元格的样式,必须在最顶层,然后某些特殊样式在后面的操作中覆盖基本样式
                Object.keys(ws).forEach((item, index) => {
                    if (ws[item].t) {
                        ws[item].s = config.myStyle.all;
                    }
                });
            }
            if (config.myStyle.headerColor) {
                if (config.myStyle.headerLine) {
                    let line = config.myStyle.headerLine;
                    let p = /^[A-Z]{1}[A-Z]$/;
                    Object.keys(ws).forEach((item, index) => {
                        for (let i = 1; i <= line; i++) {
                            if (item.replace(i.toString(), '').length == 1 || (p.test(item.replace(i.toString(), '')))) {
                                let headerStyle = getDefaultStyle();
                                headerStyle.fill.fgColor.rgb = config.myStyle.headerColor;
                                headerStyle.font.color.rgb = config.myStyle.headerFontColor;
                                ws[item].s = headerStyle;
                            }
                        }
                    });
                }
            }
            if (config.myStyle.specialCol) {
                config.myStyle.specialCol.forEach((item, index) => {
                    item.col.forEach((item1, index1) => {
                        Object.keys(ws).forEach((item2, index2) => {
                            if (item.expect && item.s) {
                                if (item2.includes(item1) && !item.expect.includes(item2)) {
                                    ws[item2].s = item.s;
                                }
                            }
                            if (item.t) {
                                if (item2.includes(item1) && ws[item2].t) {
                                    ws[item2].t = item.t;
                                }
                            }
                        });
                    });
                });
            }
            if (config.myStyle.bottomColor) {
                if (config.myStyle.rowCount) {
                    Object.keys(ws).forEach((item, index) => {
                        if (item.indexOf((config.myStyle.rowCount).toString()) != -1) {
                            let bottomStyle = getDefaultStyle();
                            bottomStyle.fill.fgColor.rgb = config.myStyle.bottomColor;
                            ws[item].s = bottomStyle;
                        }
                    })
                }
            }
            config.myStyle?.specialHeader?.forEach((item, index) => {
                Object.keys(ws).forEach((item1, index1) => {
                    if (item.cells.includes(item1)) {
                        ws[item1].s.fill = {
                            fgColor: {
                                rgb: item.rgb
                            }
                        };
                        if (item.color) {
                            ws[item1].s.font.color = {
                                rgb: item.color
                            };
                        }
                    }
                });
            });
            if (config.myStyle.heightLightColor) {
                Object.keys(ws).forEach((item, index) => {
                    if (ws[item].t === 's' && ws[item].v && ws[item].v.includes('%') && !item.includes((config.myStyle.rowCount).toString())) {
                        if (Number(ws[item].v.replace('%', '')) < 100) {
                            ws[item].s = {
                                fill: {
                                    fgColor: {
                                        rgb: config.myStyle.heightLightColor
                                    }
                                },
                                font: {
                                    name: "Meiryo UI",
                                    sz: 11,
                                    color: {
                                        auto: 1
                                    }
                                },
                                border: {
                                    top: {
                                        style: 'thin',
                                        color: {
                                            auto: 1
                                        }
                                    },
                                    left: {
                                        style: 'thin',
                                        color: {
                                            auto: 1
                                        }
                                    },
                                    right: {
                                        style: 'thin',
                                        color: {
                                            auto: 1
                                        }
                                    },
                                    bottom: {
                                        style: 'thin',
                                        color: {
                                            auto: 1
                                        }
                                    }
                                },
                                alignment: {
                                    /// 自动换行
                                    wrapText: 1,
                                    // 居中
                                    horizontal: "center",
                                    vertical: "center",
                                    indent: 0
                                }
                            }
                        }
                    }
                });
            }
            config.myStyle?.rowCells?.row.forEach((item, index) => {
                Object.keys(ws).forEach((item1, index1) => {
                    let num = Number(dislodgeLetter(item1));
                    if (num == Number(item)) {
                        ws[item1].s = config.myStyle.rowCells.s;
                    }
                });
            });
            if (!Array.isArray(exportElement) && config.myStyle.mergeBorder) { //对导出合并单元格无边框的处理,只针对dom导出,因为只有dom导出会出现合并无边框的情况
                let arr = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"]
                let range = config.myStyle.mergeBorder;
                range.forEach((item, index) => {
                    if (item.s.c == item.e.c) { //行相等,横向合并
                        let star = item.s.r;
                        let end = item.e.r;
                        for (let i = star + 1; i <= end; i++) {
                            ws[arr[i] + (Number(item.s.c) + 1)] = {
                                s: ws[arr[star] + (Number(item.s.c) + 1)].s
                            }
                        }
                    } else { //列相等,纵向合并
                        let star = item.s.c;
                        let end = item.e.c;
                        for (let i = star + 1; i <= end; i++) {
                            ws[arr[item.s.r] + (i + 1)] = {
                                s: ws[arr[item.s.r] + (star + 1)].s
                            }
                        }
                    }
                });
            }
        }
        callback && callback();
        Object.keys(ws).forEach((item, index) => { //空数据处理,单元格值为空时不显示null
            if (ws[item].t === 's' && !ws[item].v) {
                ws[item].v = '-';
            }
        });
        Object.keys(ws).forEach((item, index) => { //空数据处理,单元格值为空时不显示null
            if (ws[item].t === 's' && ws[item].v.includes('%')) {
                ws[item].v = ws[item].v.includes('.') ? (ws[item].v.replace('%', '').split('.')[1] === '0' ? `${ws[item].v.replace('%', '').split('.')[0]}%` : ws[item].v) : ws[item].v;
            }
        });
        return ws;
    } catch (e) {
        throw (e);
    }
}
function transIndexToLetter(num) { //数字转字母坐标,25->Z ,26-> AA
    if (num < 26) {
        return String.fromCharCode(num + 65);
    } else {
        return transIndexToLetter(Math.floor(num / 26) - 1) + transIndexToLetter(num % 26);
    }
}
function dislodgeLetter(str) {  //去掉字符串中的字母
    var result;
    var reg = /[a-zA-Z]+/; //[a-zA-Z]表示bai匹配字母,dug表示全局匹配
    while (result = str.match(reg)) { //判断str.match(reg)是否没有字母了
        str = str.replace(result[0], ''); //替换掉字母  result[0] 是 str.match(reg)匹配到的字母
    }
    return str;
}
export function sheetToJSON(wb,option?){
    return XLSX.utils.sheet_to_json(wb,option);
}
function IEsheet2blob(sheet, sheetName?: string | string[]) {
    console.log(sheet,'sheet');
    console.log(sheetName,'sheetName');
    try {
        new Uint8Array([1, 2]).slice(0, 2);
    } catch (e) {
        //IE或有些浏览器不支持Uint8Array.slice()方法。改成使用Array.slice()方法
        Uint8Array.prototype.slice = Array.prototype.slice;
    }
    sheetName = Array.isArray(sheetName)?(sheetName.length?sheetName:['sheet1']):'sheet1';
    var workbook = {
        SheetNames: Array.isArray(sheetName) ? sheetName : [sheetName],
        Sheets: {}
    };
    if (Array.isArray(sheetName)) {
        sheetName.forEach((item, index) => {
            workbook.Sheets[item] = sheet[index];
        });
    } else { workbook.Sheets[sheetName] = sheet; }
    console.log(workbook,'workbook');
    // 生成excel的配置项
    var wopts = {
        bookType: 'xlsx', // 要生成的文件类型
        bookSST: false, // 是否生成Shared String Table,官方解释是,如果开启生成速度会下降,但在低版本IOS设备上有更好的兼容性
        type: 'binary'
    };
    var wbout = XLSX.write(workbook, wopts);
    var blob = new Blob([s2ab(wbout)], {
        type: "application/octet-stream"
    });
    // 字符串转ArrayBuffer
    function s2ab(s) {
        var buf = new ArrayBuffer(s.length);
        var view = new Uint8Array(buf);
        for (var i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF;
        return buf;
    }
    return blob;
}
export function sheetToWorkBook(sheet, sheetName?: string | string[]){
    console.log(sheet,'sheet');
    console.log(sheetName,'sheetName');
    try {
        new Uint8Array([1, 2]).slice(0, 2);
    } catch (e) {
        //IE或有些浏览器不支持Uint8Array.slice()方法。改成使用Array.slice()方法
        Uint8Array.prototype.slice = Array.prototype.slice;
    }
    sheetName = sheetName || 'sheet1';
    var workbook = {
        SheetNames: Array.isArray(sheetName) ? sheetName : [sheetName],
        Sheets: {}
    };
    if (Array.isArray(sheetName)) {
        sheetName.forEach((item, index) => {
            workbook.Sheets[item] = sheet[index];
        });
    } else { workbook.Sheets[sheetName] = sheet; }
    console.log(workbook,'workbook');
    // 生成excel的配置项
    var wopts = {
        bookType: 'xlsx', // 要生成的文件类型
        bookSST: false, // 是否生成Shared String Table,官方解释是,如果开启生成速度会下降,但在低版本IOS设备上有更好的兼容性
        type: 'binary'
    };
    var wbout = XLSX.write(workbook, wopts);
    return wbout;
}
function getDefaultStyle() {
    let defaultStyle = {
        fill: {
            fgColor: {
                rgb: ''
            }
        },
        font: {
            name: "Meiryo UI",
            sz: 11,
            color: {
                rgb: ''
            },
            bold: true
        },
        border: {
            top: {
                style: 'thin',
                color: {
                    auto: 1
                }
            },
            left: {
                style: 'thin',
                color: {
                    auto: 1
                }
            },
            right: {
                style: 'thin',
                color: {
                    auto: 1
                }
            },
            bottom: {
                style: 'thin',
                color: {
                    auto: 1
                }
            }
        },
        alignment: {
            /// 自动换行
            wrapText: 1,
            // 居中
            horizontal: "center",
            vertical: "center",
            indent: 0
        }
    };
    return defaultStyle;
}
function IEVersion() {
    var userAgent = navigator.userAgent; //取得浏览器的userAgent字符串  
    var isIE = userAgent.indexOf("compatible") > -1 && userAgent.indexOf("MSIE") > -1; //判断是否IE<11浏览器  
    var isEdge = userAgent.indexOf("Edge") > -1 && !isIE; //判断是否IE的Edge浏览器  
    var isIE11 = userAgent.indexOf('Trident') > -1 && userAgent.indexOf("rv:11.0") > -1;
    if (isIE) {
        var reIE = new RegExp("MSIE (\\d+\\.\\d+);");
        reIE.test(userAgent);
        var fIEVersion = parseFloat(RegExp["$1"]);
        if (fIEVersion == 7) {
            return 7;
        } else if (fIEVersion == 8) {
            return 8;
        } else if (fIEVersion == 9) {
            return 9;
        } else if (fIEVersion == 10) {
            return 10;
        } else {
            return 6; //IE版本<=7
        }
    } else if (isEdge) {
        return 'edge'; //edge
    } else if (isIE11) {
        return 11; //IE11  
    } else {
        return -1; //不是ie浏览器
    }
}
function openDownloadXLSXDialog(url, saveName: string) {
    try {
        if (typeof url == 'object' && url instanceof Blob) {
            url = URL.createObjectURL(url); // 创建blob地址
        }
        var aLink = document.createElement('a');
        aLink.href = url;
        aLink.download = saveName || ''; // HTML5新增的属性,指定保存文件名,可以不要后缀,注意,file:///模式下不会生效
        var event;
        if (window.MouseEvent) event = new MouseEvent('click');
        else {
            event = document.createEvent('MouseEvents');
            event.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
        }
        aLink.dispatchEvent(event);
    } catch (e) {
        throw (e);
    }
}
export function uploadExcel(exportElement: [][] | any, fileName: string, config: IConfig) {
    let ws = getSheetWithMyStyle(exportElement, config);
    console.log(ws, 'worksheet数据');
    downLoad(ws, fileName);
}
export default {
    downLoad,
    downLoadExcel,
    getWorkSheetElement,
    getSheetWithMyStyle,
    transIndexToLetter,
    getDefaultStyle,
    sheetToWorkBook,
    sheetToJSON,
};