记录--前端如何优雅导出多表头xlsx
这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助
前言
xlsx导出是比较前后端开发过程中都比较常见的一个功能。但传统的二维表格可能很难能满足我们对业务的需求,因为当数据的维度和层次比较多时,二维表格很难以清晰和压缩的方式展现所有的信息,所以我们也就经常能碰到多级表头开发了。
demo
每当我们新使用一个插件的时候,我们都可以看着官方文档去新建立一个demo,然后去尝试一下效果,这有助于我们分析错误。
npm i xlsx -S
function exportFile() {
const ws = utils.json_to_sheet([])
const wb = utils.book_new()
utils.sheet_add_aoa(ws, [
[1, 2, 3, 4, 5, 6, 7, 8, 9],
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i']
], { origin: 'A1' })
utils.book_append_sheet(wb, ws, 'Data')
writeFileXLSX(wb, 'SheetJSVueAoO.xlsx')
}
exportFile()
demo已经成功了,xlsx已经下载下来了。
需求分析
- 新建一个表格
- 根据表头将表格进行合并
- 对合并后的表头进行内容填充
- 填入数据内容
效果如上图(时间原因就先不写xlsx的样式了)。
需求实现
- 合并单元格: 需要指定开始的行和列以及结束的行和列,如
{ 's': { 'r': 0, 'c': 0 }, 'e': { 'r': 3, 'c': 0 } }
,计算好需要合并的单元格后统一赋值给!merges
属性。 - 合并单元格后填充内容:由多个合并后的单元格填入内容时,应该也按照多个单元格填入,只是第一个有内容,其他按空填入即可。
- 表头结束后我们可以指定在某一行继续填入内容,即可继续填入数据内容。
function exportFile() {
const ws = utils.json_to_sheet([])
ws['!merges'] = [
{ 's': { 'r': 0, 'c': 0 }, 'e': { 'r': 3, 'c': 0 } },
{ 's': { 'r': 0, 'c': 1 }, 'e': { 'r': 3, 'c': 1 } },
{ 's': { 'r': 0, 'c': 2 }, 'e': { 'r': 3, 'c': 2 } },
{ 's': { 'r': 0, 'c': 3 }, 'e': { 'r': 0, 'c': 8 } },
{ 's': { 'r': 1, 'c': 3 }, 'e': { 'r': 3, 'c': 3 } },
{ 's': { 'r': 1, 'c': 4 }, 'e': { 'r': 1, 'c': 7 } },
{ 's': { 'r': 2, 'c': 4 }, 'e': { 'r': 3, 'c': 4 } },
{ 's': { 'r': 2, 'c': 5 }, 'e': { 'r': 3, 'c': 5 } },
{ 's': { 'r': 2, 'c': 6 }, 'e': { 'r': 2, 'c': 7 } },
{ 's': { 'r': 1, 'c': 8 }, 'e': { 'r': 3, 'c': 8 } },
{ 's': { 'r': 0, 'c': 9 }, 'e': { 'r': 3, 'c': 9 } }
] // 合并单元格内容
const wb = utils.book_new()
utils.book_append_sheet(wb, ws, 'Data')
utils.sheet_add_aoa(ws, [
['序号', '姓名', '性别', '公司概况', '', '', '', '', '', '备注'],
['', '', '', '职位', '项目', '', '', '', '公司名称'],
['', '', '', '', '项目时长', '项目描述', '金额', ''],
['', '', '', '', '', '', '总金额', '利润']
], { origin: 'A1' }) // 表头内容
utils.sheet_add_aoa(ws, [
[0, '张三', '男', '区域经理', '3天', '暂无描述', 998, 9.98, '阿里巴巴', '暂无'],
[1, '李四', '女', 'CEO', '30天', '稳了', 998, 9.98, '中石油', '暂无']
], { origin: 'A5' }) // 数据内容
writeFileXLSX(wb, `${+new Date()}.xlsx`)
}
好的,大功告成,今天就先到这里?
这东西也太丑了吧,我是一个开发,我不是来这里数格子的。看看上面的代码,我都不好意思说是我自己写的。要不到同事电脑上提交一下吧?
数据分析
[
{ 's': { 'r': 0, 'c': 0 }, 'e': { 'r': 3, 'c': 0 } },
{ 's': { 'r': 0, 'c': 1 }, 'e': { 'r': 3, 'c': 1 } },
{ 's': { 'r': 0, 'c': 2 }, 'e': { 'r': 3, 'c': 2 } },
{ 's': { 'r': 0, 'c': 3 }, 'e': { 'r': 0, 'c': 8 } },
{ 's': { 'r': 1, 'c': 3 }, 'e': { 'r': 3, 'c': 3 } },
{ 's': { 'r': 1, 'c': 4 }, 'e': { 'r': 1, 'c': 7 } },
{ 's': { 'r': 2, 'c': 4 }, 'e': { 'r': 3, 'c': 4 } },
{ 's': { 'r': 2, 'c': 5 }, 'e': { 'r': 3, 'c': 5 } },
{ 's': { 'r': 2, 'c': 6 }, 'e': { 'r': 2, 'c': 7 } },
{ 's': { 'r': 1, 'c': 8 }, 'e': { 'r': 3, 'c': 8 } },
{ 's': { 'r': 0, 'c': 9 }, 'e': { 'r': 3, 'c': 9 } }
]
我想要转成上面的数据结构,r从0开始,最大值就是它的深度,c从0开始,最大值就是它的广度。因为这是一个多级表头,每一级都会出现比上一级相等或更多子级的情况,我好像已经把答案说到嘴边了。对,就是用树形结构将其转换处理。
我们结合上面已转换好的列表结构和下面准备转换的树形结构,比如现在要合并第一个单元格序号
,我们应该先找到起始位置,也就是0,0
,这个很好确定;我们单单从当前节点并不能判断真正的结束位置,我们应该找到同级节点的最大深度,也就是公司概况
->项目
->金额
->总金额
,深度为3。所以它的结束位置应该为3,0
。
当我们要合并横向单元格的时候,比如公司概况
,它下边有三个子节点分别是职位
,项目
,公司名称
,而子节点下方仍有不同的子节点,此时我们就应该去获取它们的每个子节点的每层子节点的总长度 - 1
,为什么要 - 1,因为当前节点和第一个子节点占用的是同一个col
,因此可以需要减一。也就是说,如果公司概况的起始点为0,3
,那么它的终止位置由此可推:职位+项目+公司名称-1+项目时长+项目描述+金额-1+总金额+利润-1
= 5
。所以终点位置为0,3+5
=> 0,8
const mergedCells = [
{ name: '序号', prop: 'id' },
{ name: '姓名', prop: 'name' },
{ name: '性别', prop: 'sex' },
{
name: '公司概况',
children: [
{ name: '职位', prop: 'jobTitle' },
{
name: '项目', children: [
{ name: '项目时长', prop: 'projectTime' },
{ name: '项目描述', prop: 'projectDesc' },
{
name: '金额',
children: [
{ name: '总金额', prop: 'total' },
{ name: '利润', prop: 'profit' }
]
}
]
},
{ name: '公司名称', prop: 'companyName' }
]
},
{ name: '备注', prop: 'remark' }
]
思路分析
- 找到当前节点的深度和广度
- 根据当前节点深度和广度,生成当前节点单元格开始与结束位置
- 根据当前节点深度和广度,生成表头数据结构
- 根据最大深度位置,生成表单列表数据
代码实现
tips: 如果你对树结构的遍历还不太熟悉,可以看看【前端不求人】树形结构和一维数组,一笑泯恩仇
获取当前节点最大广度和最大深度
- 递归发现当前已无子节点时,就返回0,然后每返回一层就递增1,每次返回时都获取当前节点的最大值,这样就能获得最深层数。
- 递归记录每层每个子节点的长度 - 1,这样就能获取当前列表的最大宽度。
- 我们使用map做记录,下次获取就不需要重新计算了。
const map = new Map()
const getCellsSize = list => {
if (map.has(list)) { return map.get(list) }
if (list?.length) {
let rows = -1, cols = list.length - 1
list.forEach(item => {
if (item.children) {
const size = getCellsSize(item.children)
rows = Math.max(size[0], rows)
cols += size[1]
}
})
map.set(list, [rows + 1, cols])
return [rows + 1, cols]
}
}
合并单元格开始和结束位置
- 获取当前节点的开始和结束位置
- 当前节点无子节点,单元格宽为1,高为整个根节点的最大深度
- 当前节点有子节点,单元格高为1,宽为当前节点的宽,即最大广度
const size = getCellsSize(headers)
const headerMerge = []
const mergeHeadersCell = (headers, row, col) => {
for (let i = 0, len = headers.length;i < len;i++) {
const cell = headers[i]
if (!cell.children?.length) {
if (row === size[0]) { continue }
headerMerge.push({ s: { r: row, c: col + i }, e: { r: size[0], c: col + i } })
} else {
const size = map.get(cell.children)
headerMerge.push({ s: { r: row, c: col + i }, e: { r: row, c: col + size[1] + i }})
mergeHeadersCell(cell.children, row + 1, col + i)
col += size[1]
}
}
}
多表头值填充
- 我们声明一个headerValue的空数组来记录表头内容
- headerValue应该是一个二维数组,headerValue[i][j]代表第i行第j列的内容
- 当发现当前节点有children,直接获取当前节点的宽度,该宽度就是合并后空白单元格的个数。
- 当发现当前节点并没有headerValue,表示前面的节点被纵向合并了,因此应该直接加上这些空白单元格的节点
const headerValue = []
const getHeadersValue = (headers, row, col) => {
if (!headerValue[row]) {
headerValue[row] = new Array(col).fill('')
}
for (let i = 0, len = headers.length; i < len; i++) {
const cell = headers[i]
headerValue[row].push(cell.name)
if (cell.children?.length) {
const len = getCellsSize(cell.children)[1]
const emptyNameList = new Array(len).fill('')
headerValue[row].push(...emptyNameList)
getHeadersValue(cell.children, row + 1, col + i)
}
}
}
获取列表prop
- 继续递归mergedCells
- 收集无叶子节点的prop值
- 将prop值依次放进一个数组中以备后续使用
const bodyMapList = []
const getBodyMapList = list => {
if (list?.length) {
list.forEach(item => {
!item.children ? bodyMapList.push(item.prop) : getBodyMapList(item.children)
})
}
} list.map(item => bodyMapList.map(key => item[key]))
以上就是核心代码展示啦,如果想看完整代码,可以到github观看,欢迎star。
总结
我们通过计算当前树节点的大小,就可以获取该节点的广度和深度,通过广度和深度又可以让我们进一步去演算当前节点是否需要去合并其他单元格,是否需要生成空白单元格的数据内容。生成表格内容则只需要将最子层节点的prop收集,然后对应取值即可。
本文转载于:
https://juejin.cn/post/7243435843145678907
如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。
记录--前端如何优雅导出多表头xlsx的更多相关文章
- 如何使用JavaScript实现前端导入和导出excel文件
一.SpreadJS 简介 SpreadJS 是一款基于 HTML5 的纯 JavaScript 电子表格和网格功能控件,以“高速低耗.纯前端.零依赖”为产品特色,可嵌入任何操作系统,同时满足 .NE ...
- Vue导出模板、使用前端js办法导出表格数据、导入表格前端读取表格数据、导入表格发送后端读取数据
以下是几种用的较多的函数方法,可以参考使用. // 導出1 myExport() { // post請求文件寫法1 const url = 'http://XXXX/XXXX/XXXX/XXXX' c ...
- NPOI导出多表头Execl(通过html表格遍历表头)
关于NPOI的相关信息,我想博客园已经有很多了,而且NPOI导出Execl的文章和例子也很多,但导出多表头缺蛮少的:今天要讲的通过自己画html表格:通过html表格来导出自定义的多表头: 先来看要实 ...
- 利用xlst导出多表头的简便方法
大家都知道在ASP.NET中进行表格导出有很多种办法,aspose,npoi,cvs等等,今天就来介绍xlst,导出多表头.与以往不一样的是我们利用模板,只需要在模板中定义好表格样式,然后绑定数据就可 ...
- Web 前端如何优雅的处理海量数据
Web 前端如何优雅的处理海量数据 Q: 如何在 Web 页面上处理上亿条后端返回的数据,并且保证 UI 展示的流畅性 A: 思路: 时间分片, 批处理,Buffer 缓存,虚拟滚动,Web Work ...
- easypoi导出动态表头excel
easypoi导出动态表头excel 1: springBoot项目maven依赖: <dependency> <groupId>cn.afterturn</groupI ...
- 如何使用JavaScript实现纯前端读取和导出excel文件
js-xlsx 介绍 由SheetJS出品的js-xlsx是一款非常方便的只需要纯JS即可读取和导出excel的工具库,功能强大,支持格式众多,支持xls.xlsx.ods(一种OpenOffice专 ...
- 使用NPOI导入导出Excel(xls/xlsx)数据到DataTable中
using System; using System.Collections.Generic; using System.Text; using System.IO; using NPOI.SS.Us ...
- winfrom 使用NPOI导入导出Excel(xls/xlsx)数据到DataTable中
1.通过NUGET管理器下载nopi,在引入命令空间 using System; using System.Collections.Generic; using System.Text; using ...
- 前端Table数据导出Excel使用HSSFWorkbook(Java)
一.实现原理: 1. 前端查询列表数据并渲染至table(<table>...</table>)表格 2. 表格html代码传输至后台 3. 后台把html转成Excel输出流 ...
随机推荐
- CF1833G Ksyusha and Chinchilla
题目链接 题解 知识点:贪心,树形dp. 当 \(3 \not \mid n\) 时,显然无解. 考虑一种贪心策略,从叶子节点往上只,要以当前节点为根的子树大小能被 \(3\) 整除,就立刻切除这棵子 ...
- P2P通讯方式
概述 实现p2p通讯我们提供两种方式,这两种方式分别是通过客户端直接互通和p2p映射: 无论哪一种,首先设备两端都得部署好fastnat客户端,NAT类型不能是对称类型NAT(Symmetric),否 ...
- CompletableFuture使用自定义线程池实现多任务结果聚合返回
为什么要使用自定义线程池? 默认线程池缺点 1.CompletableFuture默认使用的线程池是 ForkJoinPool.commonPool(),commonPool是当前 JVM(进程) 上 ...
- Java8接口中抽象方法和default和static方法的区别和使用
Java接口说明 传统的理解是接口只能是抽象方法.但是程序员们在使用中,发现很不方便,实现接口必须重写所有方法,很麻烦.所以java设计者妥协了,在java8中,支持default和static方法, ...
- 为什么华为今年疯狂招od?
不知道的大家有没有发现 这两年市场不好公司用人需求紧缩 唯有华子疯狂招人 很多人都听过华为OD 但是具体是什么还是有很多人疑惑 总结以下三个部分: 1.为啥疯狂招od而不是之前的纯"外包&q ...
- lib,dll的位置
在添加第三方库的时候需要注意放置的路径,注意区分x86和x64的文件夹路径以及VS的版本,不要放错了 lib的位置:(需要先将lib放到该路径下,不然会说找不到.lib)C:\Program File ...
- String--cannot convert from 'const char *' to 'LPTSTR'错误
这种错误,很多情况下是类型不匹配 LPTSTR表示为指向常量TCHAR字符串的长指针 TCHAR可以是wchar_t或char,基于项目是多字节还是宽字节版本. 看下面的代码,代码来源:Example ...
- KPTP 汇报模板
1.什么是KPTP 它是由4个单词:Keep.Problem.Try.Plan的首字母组成的. K:keep,今天做了哪些工作: P:problem,遇到了哪些问题: T:try,计划尝试如何解决这些 ...
- 框架和MVC架构
网络框架及MVC架构 网络框架 所谓网络框架是指这样的一组Python包,它能够使开发者专注于网站应用业务逻辑的开发,而无须处理网络应用底层的协议.线程.进程等方面.这样能大大提高开发者的工作效率,同 ...
- Vue3基础知识提炼
1.{{}} data2.""等同{{}},数据单向绑定v-bind: 简化 :3.v-if v-else4.v-for="(i, index) in array&quo ...