前端导出Excel【支持样式配置,多sheet,多级表头】

您所在的位置:网站首页 excel导入xlsx 前端导出Excel【支持样式配置,多sheet,多级表头】

前端导出Excel【支持样式配置,多sheet,多级表头】

2023-05-07 19:42| 来源: 网络整理| 查看: 265

在我们的管理系统项目中,将表格数据导出为Excel文件是很常见的需求,一线业务人员经常会利用生成的excel文件做汇总和报告。根据功能的实现方式可以分为前端导出和后端导出,作为前端开发人员,后端导出对我们来说比较简单,本文主要讨论前端导出。

在正常的项目中,后端导出的情况会更多一下,因为前端导出受限于浏览器和用户体验。但是前端导出的需求也是存在的,比如后端工作量较大忙不过来或者本模块的业务数据量较小,在我们的项目中更多的是考虑该业务模块数据量的大小,项目经理会对这方面进行决策。当然也可以前期使用前端导出,后面业务数据量上来后再进行调整。

前端导出Excel一般在几万条数据内都还可以进行正常的使用,当然这并不绝对,实际上和导出的数据内容多少以及表头有非常大的关系,特别表头的多少【即Excel的列数】。在我们的项目中,常见的一二十来个表头,有部分复杂的业务表有一百多个表头,即使只有万条数据前端导出也已经变得比较缓慢了,所以采用前端导出和后端导出并非是固定的,完全取决于你的实际业务场景。

一,后端导出

前端调用下载接口downloadAPI,传递请求参数params。

后端直接返回加工好的Excel文件流数据,前端进行下载,保存为本地文件。

downloadAPI(params, { responseType: 'arraybuffer' }) .then((res)=> {    const blob = new Blob([res], { type: 'application/octet-stream' }); const download_url = URL.createObjectURL(blob);    const a = document.createElement('a');    a.href = download_url; // 下载地址 a.download = 'fileName'; // 文件名称 a.style.display = 'none'; // 隐藏    document.body.appendChild(a);    a.click();    // 下载完成后    setTimeout(()=> {   document.body.removeChild(a);   URL.revokeObjectURL(download_url);   }, 200) }) 复制代码 二,前端导出

将表格中的json数据,导出为Excel文件。

我们需要使用两个插件:

exceljs file-saver

exceljs是一个非常强大的插件,在github上有11k的Star。支持读取/导出Excel文件,并且对导出的Excel文件有丰富的样式配置。

本节主要展示基于exceljs插件的前端导出方法封装和一些常见的配置案例,完整的使用方法可以阅读Exceljs官方文档。

1,封装导出Excel方法

我们在utils/index.js中暴露出一个exportDataToExcel方法:

// 封装exceljs const ExcelJS = require('exceljs'); const FileSaver = require('file-saver'); ​ /** * 导出数据到Excel方法 * @param {Array[Object]} config.data 表格数据 * @param {Array[String]} config.fields 字段列表 * @param {Array[String]} config.headers excel表头列表[[]],可以是多级表头[['A1','B1'],['A2','B2']] * @param {Array[Object]} config.merges 需要合并的单元格,需要考虑表头的行数[{row:1, col:1, rowspan: 1, colspan: 2}] * @param {Array[Object]} config.attrs 单元格样式配置 * @param {Array[Object]} config.views 工作表视图配置 * @param {Array[Number]} columnsWidth 每个字段列对应的宽度 * @param {Object} config.protect 工作表保护【此配置会保护全表,一般推荐只针对单元格进行保护配置】 * @param {String} sheetName 工作表名称,默认从sheet1开始 * @param {String} fileName excel文件名称 */ export function exportDataToExcel(config, fileName) {  if (!config) return;  const options = {    fileName: fileName || `导出excel文件【${Date.now()}】.xlsx`,    worksheets: [] }  if (!Array.isArray(config)) {    config = [config] }  config.forEach((item) => {    // 深拷贝data【JSON.stringify有缺陷,可自行换成_.cloneDeep】    const data = JSON.parse(JSON.stringify(item.data));    const results = data.map(obj => {      return item.fields.map(key => {        return obj[key]     })   })    // 生成完整excel数据    let excelData = [];    excelData = excelData.concat(item.headers).concat(results);    // 单元格合并处理【excel数据的第一行/列是从1开始】    let excelMerges = [];    excelMerges = item.merges.map(m => {      return [m.row + 1, m.col + 1, m.row + m.rowspan, m.col + m.colspan]   })    // 单元格配置处理【excel数据的第一行/列是从1开始】    let excelAttrs = [];    excelAttrs = item.attrs.map(attr => {      attr.rowStart += 1;      attr.rowEnd += 1;      attr.colStart += 1;      attr.colEnd += 1;      return attr   })    options.worksheets.push({      data: excelData,      merges: excelMerges,      attrs: excelAttrs,      views: item.views,      columnsWidth: item.columnsWidth,      protect: item.protect,      sheetName: item.sheetName   }) })  createExcel(options) } ​ // 创建Excel文件方法 async function createExcel(options) {  if (!options.worksheets.length) return;  // 创建工作簿  const workbook = new ExcelJS.Workbook();  for (let i = 0; i < options.worksheets.length; i++) {    const sheetOption = options.worksheets[i];    // 创建工作表    const sheet = workbook.addWorksheet(sheetOption.sheetName || 'sheet' + (i + 1));    // 添加数据行    sheet.addRows(sheetOption.data);    // 配置视图    sheet.views = sheetOption.views;    // 单元格合并处理【开始行,开始列,结束行,结束列】    if (sheetOption.merges) {      sheetOption.merges.forEach((item) => {        sheergeCells(item);     });   }    // 工作表保护    if (sheetOption.protect) {      const res = await sheet.protect(sheetOption.protect.password, sheetOption.protect.options);   }    // 单元格样式处理    if (sheetOption.attrs.length) {      sheetOption.attrs.forEach((item) => {        const attr = item.attr || {};        // 获取开始行-结束行; 开始列-结束列        const rowStart = item.rowStart;        const rowEnd = item.rowEnd;        const colStart = item.colStart;        const colEnd = item.colEnd;        if (rowStart) { // 设置行          for (let r = rowStart; r {                row[key] = attr[key];             });           }         }       } else if (colStart) { // 未设置行,只设置了列          for (let c = colStart; c {              column[key] = attr[key];           });         }       } else {          // 没有设置具体的行列,则为整表设置          Object.keys(attr).forEach((key) => {            sheet[key] = attr[key];         });       }     })   }    // 列宽设置    if (sheetOption.columnsWidth) {      for (let i = 0; i < sheet.columns.length; i++) {        sheet.columns[i].width = sheetOption.columnsWidth[i]     }   } } ​  // 生成excel文件  workbook.xlsx.writeBuffer().then(buffer => {    // application/octet-stream 二进制数据    FileSaver.saveAs(new Blob([buffer], { type: 'application/octet-stream' }), options.fileName) }) } 复制代码 2,项目中的使用

然后在需要的地方引入exportDataToExcel方法:

导出excel数据 ​ import { exportDataToExcel } from "../utils/index.js"; export default { data() { return { tableList: [ { name: "张三", age: 20, address: "重庆市江北区福泉路123号" }, { name: "张三", age: 20, address: "重庆市江北区福泉路123号" }, { name: "张三", age: 20, address: "重庆市江北区福泉路123号" } ] }; }, methods: { onExport() { const config = this.exportConfig(); exportDataToExcel(config, "张三个人信息表.xlsx"); },        # 导出配置【根据自己的需求自由配置】 exportConfig() { const header = ["姓名", "年龄", "地址"];   // 如果导出前要处理数据,需要深克隆一份表格数据,然后进行处理 const config = { data: this.tableList, fields: ["name", "age", "address"], headers: [header], merges: [], attrs: [],                view: [], columnsWidth: [20, 20, 30], // protect: {}, sheetName: "个人信息" }; // 设置全表单元格边框,居中布局 config.attrs.push({ rowStart: 0, rowEnd: config.data.length, colStart: 0, colEnd: config.fields.length - 1, attr: { alignment: { vertical: "middle", horizontal: "center" }, border: { top: { style: "thin" }, left: { style: "thin" }, bottom: { style: "thin" }, right: { style: "thin" } } } }); // 设置表头填充颜色,字体加粗 config.attrs.push({ rowStart: 0, rowEnd: 0, colStart: 0, colEnd: config.fields.length - 1, attr: { fill: { type: "pattern", pattern: "solid", fgColor: { argb: "99CCFF" } }, font: { bold: true } } }); return config; } } }; ​ 复制代码

1.png

3,常见的导出配置 全表保护

可以在config中定义protect选项,即可开启全表保护:

exportConfig() { const config = { ...        # 全表保护        protect: {            password: '123',            options: {}       }, } } 复制代码

如果想要修改默认的保护选项options,可以查看官方文档工作表保护选项。

2.png

单元格保护

在更多的情况下,我们应该针对某个字段的列进行保护,而不是整个工作表。注意单元格的保护是在全表保护的前提下设置的,解锁某些列,剩下的列就是需要保护的。

比如我们要保护【姓名】不允许修改,可以修改年龄和地址。

# 第一种设置方式 exportConfig() { const config = { ...        # 全表保护        protect: {            password: '123',            options: {}       }, }    // 解锁年龄,地址列 config.attrs.push({ rowStart: 1, rowEnd: config.data.length, colStart: 1, colEnd: 2, attr: { protection: { // 解锁 locked: false } } }); } 复制代码 # 第二种设置方式 exportConfig() { const config = { ...        # 全表保护        protect: {            password: '123',            options: {}       }, }    // 解锁全表 config.attrs.push({ rowStart: 1, rowEnd: config.data.length, colStart: 0, colEnd: config.fields.length - 1, attr: { protection: { // 解锁 locked: false } } }); // 给姓名列加锁 config.fields.forEach((item, index) => {        if (item === 'name') {            config.attrs.push({                rowStart: 1,                rowEnd: config.data.length,                colStart: index,                colEnd: index,                attr: {                    protection: {                        // 加锁                        locked: true                   }               }           });       }   }) } 复制代码 第一种方式:解锁可以编辑列,剩下的就是保护的列。 第二种方式:先解锁全列,再加锁需要保护的列。 单元格对齐

一般给全表设置一个对齐方式即可:

exportConfig() { const config = { ... attrs: [] }    // 设置全表对齐 config.attrs.push({ rowStart: 0, rowEnd: config.data.length, colStart: 0, colEnd: config.fields.length - 1,        attr: { // 对齐方式 alignment: { vertical: "middle", // 垂直居中 horizontal: "center", // 水平居中 wrapText: true // 自动换行 } }   } } 复制代码

也可以给某一列设置不同的对齐:

exportConfig() { const config = { ... attrs: [] }    // 给age列独立设置水平靠右 config.fields.forEach((item, index) => { if (item === "age") { config.attrs.push({ rowStart: 1, rowEnd: config.data.length, colStart: index, colEnd: index, attr: { alignment: { horizontal: "right", // 水平靠右 } } }); } }); } 复制代码

3.png

单元格边框

一般直接设置全表边框即可:

exportConfig() { const config = { ... attrs: [] }    // 设置全表边框 config.attrs.push({ rowStart: 0, rowEnd: config.data.length, colStart: 0, colEnd: config.fields.length - 1,        attr: { // 边框 border: { top: { style: "thin" }, left: { style: "thin" }, bottom: { style: "thin" }, right: { style: "thin" } } }   } } 复制代码 单元格字体

设置单元格的字体类型,大小以及加粗等等:

exportConfig() { const config = { ... attrs: [] }    // 给姓名列设置字体样式 config.fields.forEach((item, index) => { if (item === "name") { config.attrs.push({ rowStart: 1, rowEnd: config.data.length, colStart: index, colEnd: index, attr: { font: { name: "Arial", // 字体名称                        size: 10, // 字体大小                        color: { argb: 'eeeeee' }, // 字体颜色                        bold: true, // 是否加粗                        italic: true, // 是否倾斜                        ... } } }); } }); } 复制代码

4.png

单元格填充 exportConfig() { const config = { ... attrs: [] }    // 给姓名列设置填充 config.fields.forEach((item, index) => { if (item === "name") { config.attrs.push({ rowStart: 1, rowEnd: config.data.length, colStart: index, colEnd: index, attr: { fill: { type: "pattern", pattern: "solid",                        // 一般只需要配置填充前景色即可 fgColor: { argb: "eeeeee" } } } }); } }); } 复制代码

5.png

单元格数字格式

一般金额字段和日期字段的显示需要进行配置:

我们给表格新加两列:【出生日期】【存款】

tableList: [ { name: "张三", birthday: '2003-01-01', age: 20, deposit: 20000, address: "重庆市江北区福泉路123号" }, { name: "张三", birthday: '2003-01-01', age: 20, deposit: 20000, address: "重庆市江北区福泉路123号" }, { name: "张三", birthday: '2003-01-01', age: 20, deposit: 20000, address: "重庆市江北区福泉路123号" } ] 复制代码 exportConfig() { const config = { ... attrs: [] } config.fields.forEach((item, index) => {        // 设置日期显示        if (item === "birthday") { config.attrs.push({ rowStart: 1, rowEnd: config.data.length, colStart: index, colEnd: index, attr: { numFmt: 'yyyy"年"m"月"d"日"' } }); }        // 设置金额显示 if (item === "deposit") { config.attrs.push({ rowStart: 1, rowEnd: config.data.length, colStart: index, colEnd: index, attr: { numFmt: "¥#,##0.00;¥-#,##0.00" } }); } }); } 复制代码

6.png

常见的配置格式:

numFmt: "0.00%"  // 百分比 0.015 => 1.5% numFmt: "#,##0.00"  // 普通数值格式,保留两位小数 numFmt: "#,##0.000"  // 普通数值格式,保留三位小数 numFmt: "¥#,##0.00;¥-#,##0.00"  // ¥金额格式,保留两位小数 numFmt: "$#,##0.00;$-#,##0.00"  // $金额格式,保留两位小数 numFmt: "yyyy-mm-dd"  // 2003-01-01 # 更多的配置格式参数Excel单元格数字格式... 复制代码 数据验证

数字格式numFmt字段更多的情况下是配合dataValidation进行数据验证,控制用户的编辑:

exportConfig() { const config = { ... attrs: [] } config.fields.forEach((item, index) => {        // 数据验证,只允许输入日期格式        if (item === "birthday") { config.attrs.push({ rowStart: 1, rowEnd: config.data.length, colStart: index, colEnd: index, attr: { numFmt: 'yyyy-mm-dd',                    dataValidation: { type: "date", showErrorMessage: true, error: "必须为日期格式[yyyy-MM-dd]", allowBlank: true } } }); }        // 限制输入的范围        if (item === "salary") { config.attrs.push({ rowStart: 1, rowEnd: config.data.length, colStart: index, colEnd: index, attr: { numFmt: "0", dataValidation: { type: "decimal", operator: "between", showErrorMessage: true, error: "工资只允许在[0, 3000]范围内", allowBlank: true, formulae: [0, 3000] } } }); } }); } 复制代码

7.png

8.png

多级表头

多级表头是我们项目也比较常见的需求,它的实现也并不复杂。

tableList: [ { name: "张三", birthday: "2003-01-01", province: '重庆', city: '江北区', address: "重庆市江北区福泉路123号" }, { name: "张三", birthday: "2003-01-01", province: '重庆', city: '江北区', address: "重庆市江北区福泉路123号" }, { name: "张三", birthday: "2003-01-01", province: '重庆', city: '江北区', address: "重庆市江北区福泉路123号" } ] 复制代码 exportConfig() {    // 有几行表头就需要定义几个表头 const header1 = ["人员", "详细信息", "", "", ""]; const header2 = ["", "出生日期", "居住地址", "", ""]; const header3 = ["", "", "省份", "城市", "地址"];    const merges = [       {row: 0, col: 0, rowspan: 3, colspan: 1},       {row: 0, col: 1, rowspan: 1, colspan: 4},       {row: 1, col: 1, rowspan: 2, colspan: 1},       {row: 1, col: 2, rowspan: 1, colspan: 3},   ] const config = { data: this.tableList, fields: ["name", "birthday", "province", "city", "address"], headers: [header1, header2, header3], merges: [], attrs: [], columnsWidth: [20, 20, 30, 30, 30], sheetName: "个人信息" };    // 设置单元格样式 config.attrs.push({ rowStart: 0, rowEnd: config.data.length + 2, colStart: 0, colEnd: config.fields.length - 1, attr: { alignment: { vertical: "middle", horizontal: "center" }, border: { top: { style: "thin" }, left: { style: "thin" }, bottom: { style: "thin" }, right: { style: "thin" } } } });    // 设置表头填充颜色,字体加粗 config.attrs.push({ rowStart: 0, rowEnd: 2, colStart: 0, colEnd: config.fields.length - 1, attr: { fill: { type: "pattern", pattern: "solid", fgColor: { argb: "99CCFF" } }, font: { bold: true } } }); return config; } 复制代码

9.png

merges的参数说明:【开始行,开始列,合并单元格的行数,合并单元格的列数】

# 人员-表头 第一行,第一列, 合并三行,合并一列 {row: 0, col: 0, rowspan: 3, colspan: 1}, # 详细信息-表头 第一行,第二列, 合并一行,合并四列 {row: 0, col: 1, rowspan: 1, colspan: 4}, 复制代码

merges以及attrs的配置都会根据表头的变化而变化,所以先确定表头headers的配置是关键。

扩展: merges的参数与你的封装方式息息相关。

export function exportDataToExcel(config, fileName) { let excelMerges = [];    // 修改封装方式    excelMerges = item.merges.map(m => {      return [m.rowStart + 1, m.colStart + 1, m.rowEnd + 1, m.colEnd + 1]   }) } 复制代码

那么merges的参数就应该使用Exceljs默认的的【开始行,开始列,结束行,结束列】

# 人员-表头 第一行,第一列, 到第三行结束,到第一列结束 {rowStart: 0, colStart: 0, rowEnd: 2, colEnd: 0}, # 详细信息-表头 第一行,第二列, 到第一行结束,到第四列结束 {rowStart: 0, colStart: 1, rowEnd: 0, colEnd: 3}, 复制代码

以上两种配置在处理后都是一样的效果,随便选择哪种方式都可以。

三,结语

本节主要讨论了基于Exceljs插件的前端导出,封装了一个导出数据到excel方法,以及展示了一些常见的导出配置。觉得对你有帮助的小伙伴可以点赞收藏,封装的exportDataToExcel方法可以直接复制到项目进行使用。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3