【总结】xlsx前端表格导出

617 阅读5分钟

背景

最近项目有一个功能,导出表格,要前端实现导出功能,翻了一圈文档,发现如果要实现前端导出功能,使用最多的包就是xlsx,奈何这个文档是英文,阅读有障碍,翻了很多文档,看了半天才看懂个七七八八,也找到了个中文版本文档。 现在把使用的功能总结一下。

导出表格

项目表格组件是基于element-ui进行的封装。如果前端实现导出有2种实现方式:

  1. 通过table_to_sheet根据表格dom元素导出,把DOM TABLE元素转换为工作表。

  2. 通过数据构造表格数据,有两种数据格式可以转换

    • JS二维数组结构,对应的api是aoa_to_sheet,把转换JS数据数组的数组为工作表。
    • JS对象数组结构,对应的api是json_to_sheet,把JS对象数组转换为工作表
    • 构造完表格数据后,如果想追加数据,则有对应的api
      • sheet_add_aoa 把JS数据数组的数组添加到已存在的工作表中。
      • sheet_add_json 把JS对象数组添加到已存在的工作表中。

1. table_to_sheet

这种方式导出表格,遇到了两个问题:

  1. 后台表格都是分页的,如果使用这个函数,则只能导出第一页内容,又查了下相应的解决方案。有2种:

    • 导出时将每页大小限制改成所有数据的长度,导出完成后,再将分页数据恢复。参考文章1 参考文章2
    • 添加一个隐藏的表格,表格的数据是全部数据,导出传入这个隐藏的表格的dom。
  2. 表格带样式,xlsx这个社区版本是不支持表格设置样式的,如果要设置样式需要pro版本,那pro版本是要花钱的,那肯定不行。所以还需要 xlsx-styles 这个npm包,一引入就有问题,需要修改webpack配置。通过设置表格的样式属性,然后用这个包的XLSX对应来导出excel,就可以导出带有样式的表格了。

其实样式这个问题不光是table_to_sheet方式导出表格的问题,其他用数据直接构造表格导出也是遇到一样的问题,如果要带有样式的表格,都需要引用到这个库。或者换一个库exceljs

image.png

基础导出demo。

官方demo

exportByTable(dom, filename) {
    if (!dom) {
        throw new Error('dom为空')
    }
    const workbook = XLSX.utils.book_new();
    // dom生成sheet表
    const worksheet = XLSX.utils.table_to_sheet(dom);
    // 如果要添加样式,border等可以对worksheet做修改

    // 将工作表对象添加到工作簿对象中
    XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
    // 导出文件
    this.writeFile(workbook, filename);
}

writeFile(workbook, filename) {
    // 导出Excel, 注意这里用到的是XLSXS对象
    let wbout = XLSXS.write(workbook, {
        bookType: 'xlsx',
        bookSST: false,
        type: 'binary',
      })

      FileSaver.saveAs(
        new Blob([s2ab(wbout)], {
          type: 'application/octet-stream'
        }),
        filename // 保存的文件名
      )

      // 将工作簿对象保存为Excel文件, xlsx的导出无法带样式导出
      // XLSX.writeFile(workbook, 'exported_data.xlsx');
}

2. 数据直接构造表格

因为后台接口返回的数据般来说都是数组对象的格式,所以拿到数据之后可以用这个方法来构造工作worksheet。

  1. json_to_sheet 基础导出用法:
        // 插入表格数据
        // const data = [
        //   { name: 'John Doe', age: 30, email: 'john.doe@example.com', gender: '男' },
        //   { name: 'Jane Smith', age: 35, email: 'jane.smith@example.com', gender: '女' }
        // ];
    
        // const columnHeaders = {
        //   name: '姓名',
        //   age: '年龄',
        //   email: '邮箱',
        //   gender: '性别'
        // };
    exportFile(data, header) {
        // 创建一个工作簿对象
        const workbook = XLSX.utils.book_new();
   
        const newData = [headers, ...data];
    
        // 创建一个工作表对象
        const worksheet = XLSX.utils.json_to_sheet(newData, {
          skipHeader: true
        });
    
        // 添加边框
        this.addBorder(worksheet);
    
        // 将工作表对象添加到工作簿对象中
        XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
    
        // 导出文件
        this.writeFile(workbook, filename);
    }
  1. 已有表格后面添加文案 sheet_add_aoa,

content为而为数组,每一项是每一格的数据,空行的话直接添加空数组,第三个参数 origin:-1代表在最后一行添加。

    insertAfterTable(worksheet, content) {
    // contentDemo
    // const arr = [
    //     [],
    //     [],
    //     ['说明:'],				
    //     ['1、',	'管理费不含期限管理费'],			
    //     ['2、',	'管理费'],			
    //     ['3、',	'管理费管理费']
    // ]
        // 表格后面插入说明
        XLSX.utils.sheet_add_aoa(worksheet, content, { origin: -1 });
    }

  1. 已有表格前面添加标题sheet_add_aoa

这样直接插入会把前面行的内容直接替换掉,如何不替换掉?

    // 已有表格前面插入title等
    insertBeforeTable(worksheet, content) {
    // 表头插入
    // const contentDemo = [
    //     [],
    //     ['管理费率数据', '', '', ''], 
    //     ['', '', '', '单位:税前数,bp'],
    //     ['主投x部门', '', '资产管理x部门', '']
    // ];
        // 表格后面插入说明, 需要留出插入内容的空白行
        XLSX.utils.sheet_add_aoa(worksheet, content, { origin: 0 });
    }
  1. 合并单元格

数据格式是excel文档单元格的号,merges为数组,每一项代表要合并大单元格的起始和终止位置。例如A2:D2代表合并第一行的A到D列。

这里思考如何简单的实现复杂嵌套表单的标题?

    mergeCell(ws, merges) {
    // const merges = [
    //     "A2:D2", // 合并一级表头的单元格
    //     "A4:B4", // 合并二级表头的单元格
    //     "C4:D4"
    // ];
    if (merges) {
        if (!ws['!merges']) {
        ws['!merges'] = [];
        }
        merges.forEach(item => {
        ws['!merges'].push(XLSX.utils.decode_range(item));
        })
    }
    }
  1. 添加表格样式和边框

变态的是需要一个一个单元格的进行遍历,然后设置每一个单元格的边框。真是变态啊,这api用法真难。单元格的样式也是通过设置cell.s中的属性。

如何简单的设置样式?

    addBorder(worksheet, borderRange) {
        // 设置表格边框样式
        const borderStyle = {
            top: { style: 'thin', color: { rgb: '000' } },
            bottom: { style: 'thin', color: { rgb: '000' } },
            left: { style: 'thin', color: { rgb: '000' } },
            right: { style: 'thin', color: { rgb: '000' } }
        };
        borderRange = borderRange || worksheet[!ref];
        var range = XLSX.utils.decode_range(borderRange);
        for (let R = range.s.r; R <= range.e.r; ++R) {
            for (let C = range.s.c; C <= range.e.c; ++C) {
                const cellAddress = XLSX.utils.encode_cell({ r: R, c: C });
                const cell = worksheet[cellAddress];
                
                if (!cell) continue; // 跳过非字符串单元格
            
                cell.s = {
                    border: borderStyle
                };
                if (R === 3 || R === 4) {
                    cell.s = {
                    ...cell.s,
                    fill: {
                        fgColor: {
                            rgb: "eeeeee"
                        }
                    },
                    font: {
                        name: "黑体",
                        sz: "15",
                        bold: true
                    }
                    }
                }
            }
        }
    }

Todo

    1. 表头插入如何不覆盖?

    函数加参数options,options beforeHeader:在表格前插入的数据

    tableTitle: {
        value = [
            [], // 空行
            ['管理费率数据', '', '', ''], 
            ['', '', '', '单位:税前数,bp'],
            ['主投x部门', '', '资产管理x部门', '']
        ],
        merge: ['A2:H2']
    },
    afterTable: {
        value = [
            [],
            [],
            ['说明:'],				
            ['1、',	'管理费不含期限管理费'],			
            ['2、',	'管理费'],			
            ['3、',	'管理费管理费']
        ],
    }
    
    1. 复杂表头如何设计传入结构?

children表格嵌套,根据以下格式转换为merges数组。

 const columns = [
    {
    title: 'Name',
    dataIndex: 'name',
    key: 'name',
    width: 100,
    },
    {
      title: 'Other',
      children: [
        {
          title: 'Age',
          dataIndex: 'age',
          key: 'age',
          width: 150,
        },
        {
          title: 'Address',
          children: [
            {
              title: 'Street',
              dataIndex: 'street',
              key: 'street',
              width: 150,
            },
            {
              title: 'Block',
              children: [
                {
                  title: 'Building',
                  dataIndex: 'building',
                  key: 'building',
                  width: 100,
                },
                {
                  title: 'Door No.',
                  dataIndex: 'number',
                  key: 'number',
                  width: 100,
                },
              ],
            },
          ],
        },
      ],
    },
    1. 表格样式修改格式如何传入?

表格整体样式,只针对表格部分增加样式,不包含表格前后插入的数据,取出最后表格的范围,设置以下样式。

cellStyle: {
    borderColorRgb: '333'
},
headerCellStyle: {
    fontColorRgb: 'FF8040'
},
bodyCellStyle: {
    fillFgColorRgb: 'EEEEE0'
},

针对单个表头单元格也可以设置样式

cellStyle: {
    borderColorRgb: '333'
},

后续

今天找到一个库,觉得也的不错的,pengchen96.github.io/table-xlsx/… 表格导出的功能,基本满足了复杂表头、样式设置的功能,但是没有在表头插入和表后面插入内容的功能,拉下来带后,一顿操作猛如虎,加了这2个基本操作。好了,完成度90%了,还差表格头部的样式设置了,节后在搞。😄

新增两个参数:

  • tableTitle: 表格前面添加标题,demo如下
{
    value: [
        [],
        ['投资部门', '', '', ''], 
        ['', '', '', '单位:税前数,bp']
    ],
    merges: [
        'B1:D4'
    ]
=}
  • afterTable:表格后面插入说明语句
const arr = [
    [],
    [],
    ['说明:'],				
    ['1、',	'管理费包含'],			
    ['2、',	'管理费金额'],			
    ['3、',	'管理费管理费']
 ]

逻辑是在之前计算完表格的数据后,再进行表头和表后面的数据插入,表后的插入比较简单,表头的插入复杂,目前简单处理是,如果有表头:

  • 修改数据结果,sheet数据全部下移,修改过程中要注意从下往上复制,否则会出错。
  • 修改merges数组结构,将合并单元格的row信息都加上title占用的行
if (tableTitle) {
    offset = tableTitle?.value?.length || 0;
    // sheet整体偏移计算
    const row = sheet['!rows'].length;
    const col = sheet['!cols'].length;
    for (let i = row; i > 0; i--) {
        for (let j = 65; j <= 65 + col; j++) {
            const char = String.fromCharCode(j);
            sheet[`${char}${i + 3}`] =  sheet[`${char}${i}`];
            if (i <= offset) {
                sheet[`${char}${i}`] = {v: '', t: 's'};
            }
        }
    }
    // 设置merges
    sheet['!merges'].forEach(item => {
        item.s.r += offset;
        item.e.r += offset;
    })
    const [start, end] = sheet['!ref'].split(':');
    const newEnd = end.slice(0, 1) + (row + offset);
    sheet['!ref'] = `A1:${newEnd}`;
    insertBeforeTable(sheet, tableTitle);
}
if (afterTable.length) {
    insertAfterTable(sheet, afterTable);
}

最后结果:

image.png

总结

一个简单的表格导出功能,竟然也耗费了很长时间,最开始用的最基础的导出功能,以为完事了,结果一看导出模版,竟然这么多样式,好吧,那咱查查怎么弄这样的表格,可是看了半天sheetjs的文档,硬是没看出眉目,最后发现这api简直反人类,我以为我只要调用一个api就可以添加border,结果没有,需要一个一个单元格设置===,而我想要的效果就是像通常调用组件一样传入好多options,然后就导出来对应的样式了,结果没有,幸好找到了一个库,发现写法还是不错的,那直接拿来用吧,再改改,就差不多了。😂