更新: 略显尴尬,在测试进行了各种数据测试之后,发现处理数据还是有些问题,有问题才能进步嘛,哈哈哈,还好发现及时,今天下午又进行了修改,对合并数据的地方修改了很多,详细内容见新的dealData方法:
dealBody(data) {
let depth = 1
const resultList = []
const tmp = {}
const { companyList = [] } = data
// 用来记录每个父节点下面有多少个子节点,得到需要合并的rowspan
const levelWidth = {}
const func = (dataList, fn, level, parentSysno) => {
dataList.forEach((item) => {
const title = item.value || ''
if (item.isLeaf) {
levelWidth[parentSysno] = dataList.length
const list = []
if (level > depth) {
depth = level
}
list.push({
field: guid(),
value: title,
})
const fields = companyList.map((field) => field.sysno)
fields.forEach((field) => {
const info = this.getConclusionList(field, item.sysno)
list.push({
field: guid(),
...info,
class: 'center',
})
})
fn(list)
list.unshift({
field: `seq${guid()}`,
value: resultList.length + 1,
class: 'center',
})
resultList.push(list)
} else {
// 层层递归,通过传递回调函数,将前面每一级需要装入的第一个数据装进去
func(item.childList, (list) => {
// 通过判断每一级是否有重复的,如果有,则不装进去
if (tmp[level]) {
if (!tmp[level][item.sysno]) {
const key = item.sysno + guid()
list.unshift({
field: key,
value: title,
rowspan: levelWidth[item.sysno] || 1,
})
tmp[level][item.sysno] = key
} else {
// 如果有重复的,则判断是不是levelWidth对应的数据更新了,因为在数组长度不为1时,第一次拿到的不是准确的长度
// 所以每次遍历更新出来的长度
const key = tmp[level][item.sysno]
resultList.forEach((arr) => {
const ind = arr.findIndex((e) => e.field === key)
if (ind !== -1) {
if (arr[ind].rowspan < levelWidth[item.sysno]) {
arr[ind].rowspan = levelWidth[item.sysno]
}
}
})
}
} else {
const key = item.sysno + guid()
list.unshift({
field: key,
value: title,
rowspan: levelWidth[item.sysno] || 1,
})
tmp[level] = {
[item.sysno]: key,
}
}
if (parentSysno) {
levelWidth[parentSysno] = 0
dataList.forEach((row) => {
levelWidth[parentSysno] += levelWidth[row.sysno]
})
}
if (fn) {
fn(list)
}
}, level + 1, item.sysno)
}
})
}
func(data.clauseList, null, 1, null)
this.dataList = resultList
return depth
}
一、实现背景
最近的一个项目需要将很多表格打印出来,而这些表格基本上是树形的数据,需要用到合并行和合并列,数据的组合有些复杂。最开始已经用vxe-table实现了一版,用在了网页详情展示里,但是如果要打印出来的话,vxe-table的样式就无法正常显示,比如边框不显示等问题。所以在打印预览页我采用了原生的table来显示。
二、技术难点
拿到的数据是树形的,并且由多个数据组成,这就需要我们自己去实现合并行和列。对于数据的组合,这里我用的是递归+回调的方式。
三、具体实现
先来看看效果图。
图3-1
![](https://img-blog.csdnimg.cn/img_convert/903a53ef642f549373f2d4ca491e22d7.png)
图中,树形内容是动态的,且是树形层级的,可能存在多级,右侧动态项部分也是动态的,根据接口返回的项目有多少展示多少。所以数据处理的难点在于动态树形内容的合并。
先来想想如果要显示这样的效果图,那我们想要的数据是怎样的?先看html部分,这里我用vue写的,表头和表体我都是用的双重for循环渲染的,:
<table class="print-table">
<thead>
<!-- 这一部分tr是为了控制表格的最低宽度,避免文字过多,挤压为一个字一行,按实际需求有的最低是4个字,有的是5个字,当然这个宽度也是根据当前页面的字号来估计的。这一行实际上从界面上来看是隐藏了的 -->
<tr>
<td
v-for="(item, index) in maxCols"
:key="index"
:style="{minWidth: item.minWidth + 'px'}"
/>
</tr>
<!-- 这一部分tr是真的表头显示部分 -->
<tr
v-for="(column, index) in columns"
:key="index + 'header'"
class="header"
>
<!-- 在获取表头时就已经计算了rowspan和colspan了 -->
<th
v-for="(item, ind) in column"
:key="item.field + index + ind"
:rowspan="item.rowspan || 1"
:colspan="item.colspan || 1"
>{{ item.name }}</th>
</tr>
</thead>
<tbody>
<!-- 这部分是表体 -->
<tr
v-for="(column, index) in dataList"
:key="index"
>
<td
v-for="(item, ind) in column"
:key="item.field + index + ind"
:rowspan="item.rowspan || 1"
:colspan="item.colspan || 1"
:class="item.class"
>{{ item.value }}</td>
</tr>
</tbody>
</table>
通过上图的html部分我们可以知道,每一个td都是数组里的一个对象,一行tr就是一个数组,所以表头和表体的数据都是二维数组。接下来我们看看这个数据长什么样:
表头数据:
[
[
{
"field":"seq",
"name":"序号",
"rowspan":2
},
{
"field":"content",
"name":"树形内容",
"rowspan":2,
"colspan":3
},
{
"field":"companys",
"name":"动态项",
"rowspan":1,
"colspan":5
}
],
[
{
"field":"2f0d7ff3-1c61-4f3b-5a28-521be8cdf3ca",
"name":"B(1)"
},
{
"field":"7b76de6b-a612-0786-358a-1419afb66fa8",
"name":"A(2)"
},
{
"field":"89b3eb6a-0773-1763-0b9c-684944c47168",
"name":"C(3)"
},
{
"field":"23975423-8cd1-cca2-f52f-7b8687ff2008",
"name":"D(4)"
},
{
"field":"6da87f38-6c56-30a6-8218-b4b76bd058ea",
"name":"E(5)"
}
]
]
可以看到,序号和树形内容这两项的rowspan都是2,动态项的colspan是5,rowspan是1,且第二行数据只有实际的动态项数据(B、A、C、D、E)。因为前面的数据被rowspan给合并了,所以不需要传入,传入则会有多的列出来,后面的动态项则合并了列,只占了一行。类似的,我们表体的数据也是这样:
图3-2
![](https://img-blog.csdnimg.cn/img_convert/c1f3c207560545abf1a8fef0a206479a.png)
通过截图也可以看出来每一行的数据长度是不一致的,因为有的合并了列
[
[
{
"field":"seqef644d70-e23f-a6ee-a796-a2b10047e4f3",
"value":1,
"class":"center"
},
{
"field":"600000079050ef921c-dd0f-a257-4e88-17c7b001ddbe",
"value":"1",
"rowspan":2
},
{
"field":"60000007919db33f61-b865-0ee3-feae-8b75d4b95ce9",
"value":"1.1",
"rowspan":0
},
{
"field":"e357a367-f951-aaf4-5e7e-aea6df2170ad",
"value":"1.1.1"
},
{
"field":"2e697dc1-14fa-1d49-5805-9df13e7b3bb7",
"value":"√",
"class":"center"
},
{
"field":"17ab7f73-fa88-4eaf-32b4-a404013cbd85",
"value":"√",
"class":"center"
},
{
"field":"b68c2337-a4c4-e93d-4d96-75720b1fe127",
"value":"√",
"class":"center"
},
{
"field":"f1c8e1cc-93d7-921d-0000-5707128a00d1",
"value":"√",
"class":"center"
},
{
"field":"4f87b39b-5ffe-97f7-338e-c9b11aeca796",
"value":"√",
"class":"center"
}
],
[
{
"field":"seq83465ad3-bd9a-0d98-911a-739be01ccedb",
"value":2,
"class":"center"
},
{
"field":"60000007936780df03-eecb-a39c-ae20-334c1993f985",
"value":"1.2",
"rowspan":0
},
{
"field":"c31b95ad-b67d-0f0c-30f7-38b654ae011e",
"value":"1.2.1"
},
{
"field":"254f3ca4-6f5e-1382-882d-5117d794e71b",
"value":"√",
"class":"center"
},
{
"field":"29c6f288-f969-26d9-5fd7-3ccaf04174f9",
"bidOpenSysno":6000000098,
"order":2,
"bidComplianceClauseSysno":6000000794,
"bidProjectPackageSysno":6000000104,
"conclusionShow":-1,
"conclusion":-1,
"remark":"不符合条款内容,初审结果不合格",
"value":"X×1",
"class":"center"
},
{
"field":"56790c61-bc69-cc98-963c-3599b0efd2d0",
"value":"√",
"class":"center"
},
{
"field":"90eabe7a-5d29-554e-d466-cef55fc4eb8f",
"value":"√",
"class":"center"
},
{
"field":"86f1a799-0f9c-9ed3-5b6f-4b2af22283a3",
"value":"√",
"class":"center"
}
],
[
{
"field":"seq3e5b41f9-9e75-3112-624c-f9c07b91e5f7",
"value":3,
"class":"center"
},
{
"field":"6000000795854d90ad-5afc-ffb2-9151-8149864d2ef5",
"value":"2",
"rowspan":0
},
{
"field":"60000007961e20784b-3f36-2d39-49dd-52263cd691ff",
"value":"2.1",
"rowspan":0
},
{
"field":"68a425da-a8c9-af31-f5a1-3b1e3d7db8e7",
"value":"2.1.1"
},
{
"field":"26ef50fa-fe66-a3f3-fe7f-aadb82a56f8d",
"value":"√",
"class":"center"
},
{
"field":"e70d2ab5-c570-3bb5-7a37-46f4d74d4717",
"value":"√",
"class":"center"
},
{
"field":"e7ab0f3a-7a9c-342c-23e8-6cd6104d73a3",
"bidOpenSysno":6000000100,
"order":3,
"bidComplianceClauseSysno":6000000797,
"bidProjectPackageSysno":6000000104,
"conclusionShow":-1,
"conclusion":-1,
"remark":"第二天不符合条款,初审结果不合格",
"value":"X×2",
"class":"center"
},
{
"field":"c5274724-d7d4-fec9-5286-791317a8b619",
"value":"√",
"class":"center"
},
{
"field":"8e7cb4c6-6d5a-66ff-3ee6-3f793fba76a9",
"value":"√",
"class":"center"
}
],
[
{
"field":"seq41bd201d-55a4-b700-3f7c-a2e8a42e0b8a",
"value":4,
"class":"center"
},
{
"field":"6000000798d2188137-f379-e348-ff20-d822440bf098",
"value":"3",
"rowspan":0
},
{
"field":"600000079999bcd09d-6dfa-f4e6-258e-b90acf71d1ad",
"value":"3.1",
"rowspan":0
},
{
"field":"959959db-c1a6-aa2b-2afa-5fb978aaf81d",
"value":"3.1.1"
},
{
"field":"98eaf015-6bfc-3b7f-c7f8-4c6e3f3697a4",
"value":"√",
"class":"center"
},
{
"field":"19b21b9e-96a9-be70-4c09-699dbcb82c71",
"value":"√",
"class":"center"
},
{
"field":"9ac30a85-b66c-356c-0fbd-86b8c96874d3",
"value":"√",
"class":"center"
},
{
"field":"af856b36-9f3f-def3-9f05-fb64f14d1389",
"value":"√",
"class":"center"
},
{
"field":"683a01d2-5ff6-42c6-b6cf-947acc2c815d",
"value":"√",
"class":"center"
}
],
[
{
"field":"seqc2bd9ba7-473a-9ac9-6e21-e8641564665f",
"value":5,
"class":"center"
},
{
"field":"600000080175058443-c1e3-669f-96e0-c9fc31711a2f",
"value":"4",
"rowspan":0
},
{
"field":"6000000802c9520fb0-f011-4552-770c-767607890743",
"value":"4.1",
"rowspan":0
},
{
"field":"f0fe1835-fcc9-a21d-6834-54adcbe772d4",
"value":"4.1.1"
},
{
"field":"5f973bac-3135-0bdb-37a0-2036a2597f3c",
"value":"√",
"class":"center"
},
{
"field":"063ac2c9-6ffb-96ca-ea12-4636854b5db5",
"value":"√",
"class":"center"
},
{
"field":"eeb74602-5f4a-18af-b527-38b801e17235",
"value":"√",
"class":"center"
},
{
"field":"5488219b-0a09-d1e7-cea4-0c51bda4ed04",
"value":"√",
"class":"center"
},
{
"field":"387742dd-d524-6b2e-a5ee-a4bdc9966e46",
"value":"√",
"class":"center"
}
],
[
{
"value":"结论",
"colspan":4,
"field":"18037736-9985-43f9-f559-9cbff02fe682",
"class":"center"
},
{
"field":"6000000099total",
"value":"合格",
"class":"center"
},
{
"field":"6000000098total",
"value":"不合格",
"class":"center"
},
{
"field":"6000000100total",
"value":"不合格",
"class":"center"
},
{
"field":"6000000101total",
"value":"合格",
"class":"center"
},
{
"field":"6000000102total",
"value":"合格",
"class":"center"
}
]
]
数据较多,可能不好观察,可通过下方的截图来理解:
将如下图的树形数据,用表格来显示
图3-3
![](https://img-blog.csdnimg.cn/img_convert/d33032ebed8592f09df06abd3e73d373.png)
图3-4
![](https://img-blog.csdnimg.cn/img_convert/cc1538dd1a8ba50597ec4ee7aea1dc8e.png)
我们要想通过上面的html渲染得到这样的结果,数据则是这样:
[
[{a}, {b}, {d}],
[{e}]
[{c, f}]
]
所以处理数据时,需要判断,如果a是重复的,则只在第一次遇到的时候装入,同理b也是这样,并且在装入的时候需要给它设置rowspan。
代码如下图:
dealData(data) {
// 处理表头,表头第二行是树形内容和动态项的内容
const row2 = []
const { companyList = [] } = data
// companyList即是动态项,先遍历这个动态项,拿到表头数据
companyList.forEach((item) => {
row2.push({
field: guid(),
name: `${item.sealedBidCode}(${item.order})`,
})
})
// 处理数据,得到要显示的表体,并且通过处理数据的时候,拿到树形深度,得到树形内容所需要的colspan
const contentColspan = this.dealBody(data) || 1
const row1 = [
{ field: 'seq', name: '序号', rowspan: 2 },
{
field: 'content', name: '动态树形内容', rowspan: 2, colspan: contentColspan,
},
...(row2.length ? [{
field: 'companys', name: '动态内容', rowspan: 1, colspan: row2.length,
}] : []),
]
// 为不同的部分设置不同的最小宽度
const maxCols = [
{ minWidth: 60 },
]
for (let i = 0; i < contentColspan; i++) {
maxCols.push({ minWidth: 60 })
}
for (let i = 0; i < row2.length; i++) {
maxCols.push({ minWidth: 80 })
}
this.maxCols = maxCols
this.columns = [
row1,
row2,
]
// 获取尾部的合并项
this.getFooter(contentColspan + 1)
},
dealBody是比较复杂难理解的部分,主要是层层遍历和回调(注意:这个版本是有bug的,最新的内容我更新到页面顶部了)
dealBody(data) {
let depth = 1 // 记录树的深度
const resultList = []
const tmp = {} // 用来记录是否在当前层级,当前列有重复的,如果是重复的,则不装入,因为第一次装入的时候已经合并了行了
const { companyList = [] } = data
// 在这个函数内部我定义了一个递归函数,传入的参数是数据、回调方法、层级
const func = (dataList, fn, level) => {
dataList.forEach((item) => {
const title = item.title || ''
// 这里判断是否已经深入到子节点了,如果是最后一级了 ,则可以将当前的数据装入数据了
if (item.isLeaf) {
const list = []
if (level > depth) {
// 树的深度取最大
depth = level
}
// 将最后一级的数据装入数组,
list.push({
field: guid(),
value: title,
})
//这里还装入了动态项的数据部分
const fields = companyList.map((field) => field.sysno)
fields.forEach((field) => {
// 这个getConclusionList方法是我这边需要从第三个数据对象里匹配到符合的数据,显示√和X。
const info = this.getConclusionList(field, item.sysno)
list.push({
field: guid(),
...info,
class: 'center',
})
})
// 上面的代码只是将最后一级和其他动态项的数据装了,对于前面几级数据,通过回调的fn方法,倒序装入
fn(dataList.length > 1 ? dataList.length : 0, list)
// 装完了第一级的数据,装入固定的数据“序号”这一列,因为序号是在第一列,所以记得用unshift方法。
list.unshift({
field: `seq${guid()}`,
value: resultList.length + 1,
class: 'center',
})
// 这个list就是一行,即tr的内容,多个tr凑成一个表格,resutList是最终的表格数据。
resultList.push(list)
} else {
// 层层递归,通过传递回调函数,将前面每一级需要装入的第一个数据装进去
func(item.childList, (size, list) => {
// 这个回调方法里,我们传入的参数有两个,size和list,size表示子级有多少个,比如上面图3-3里,对于b元素,size就是2,对于a元素,size就是3,list是一个引用,引用了当前行的对象
// 通过判断每一级是否有重复的,如果有,则不装进去
if (tmp[level]) {
if (!tmp[level][item.sysno]) {
list.unshift({
field: item.sysno + guid(),
value: title,
rowspan: size,
})
tmp[level][item.sysno] = true
}
} else {
// 记得倒着装入
list.unshift({
field: item.sysno + guid(),
value: title,
rowspan: size,
})
tmp[level] = {
[item.sysno]: true,
}
}
if (fn) {
// 通过回调将子元素的长度一层层返回上去,这样第一级就能拿到整个树的广度
// 如果子元素的个数为1,则只需要加0,否则加入子元素的个数,这样上一级才能拿到这一级的整个宽度。
const newSize = dataList.length > 1 ? dataList.length : 0
fn(size + newSize, list)
}
}, level + 1)
}
})
}
// 第一级调用,此时没有回调函数,且level是1
func(data.clauseList, null, 1)
this.dataList = resultList
return depth
},
footer部分看需要显示,我这里需要显示,就简单写了一个方法:
getFooter(colspan) {
const { companyList = [], conclusionList = [] } = this.info
const result = [
{
value: '结论', colspan, field: guid(), class: 'center',
},
]
companyList.forEach((item) => {
const { sysno } = item
let flag = true
conclusionList.forEach((row) => {
if (row.bidOpenSysno === sysno && row.conclusion === -1) {
flag = false
}
})
result.push({
field: `${sysno}total`,
value: flag ? '合格' : '不合格',
class: 'center',
})
})
this.dataList.push(result)
},
整个代码就已经完全展示了,对于有类似需求的童靴可以参考我的代码改编,或许我这个写法比较复杂,但是确实是很基础了,也满足了所在项目的需求,如果有更好的方式,欢迎大家指点呀!
四、打印相关的知识补充
1、表格的打印
既然说到这个表格是用于打印的,这里也提一些打印过程遇到的问题,首先我运气比较好,表格打印的样式完全按照我写的来了,此时遇到同事看到我在写这个,就问我为什么用原生的table不用div去手写,因为他之前用原生table打印出来的样式不对劲,比如表格边框的颜色始终改不了,打印出来灰扑扑的,这里就是我说我运气比较好的地方了,因为我不是直接给table标签加上border=“1”,而是通过css样式去实现的边框,这样表格边框的颜色就是可控的了。不用担心我这样写表格边框会重复,表格属性可以设置去重复的(border-collapse: collapse;),默认不写这个属性也不会重复。
图4-1
![](https://img-blog.csdnimg.cn/img_convert/eaa936a31bab8d4cf0900be7bc101bd3.png)
这里我给tbody的td和thead的th单独设置了border,然后不给table标签上添加border,并且我在给第一行用来占位的tr下的td没有设置border,这样从界面上来看,是看不出来那一行的。
2、打印页带输入框,打印内容的显示
我所写的打印页,为了方便用户打印时临时改一些数据,则提供了一些输入框,输入框的样式可以通过打印样式查询(@media print)去设置去掉边框,这样打印的时候就不会显示边框了,但是仍然有个问题,那就是输入框很短,但是输入的内容很长的话,直接打印就会被截掉一些数据,所以此时我直接在打印的时候隐藏了输入框,然后用pre标签来显示输入的内容,这样打印的时候就能完全显示,并且能保留输入的格式,比如输入时换行的情况。
3、表格过长,合并行太多时,打印分页不显示边框的情况
因表格太长,导致打印时一个表格被拆分为了两页,但是如果分开的部分是合并了很多行,则打印时,表格会出现边框不全的情况,如图
图4-2
![](https://img-blog.csdnimg.cn/img_convert/4a26c7b465cf800c196e9df76e225e7a.png)
实际网页显示的是:
图4-3
![](https://img-blog.csdnimg.cn/img_convert/3e070316af87cd40088a6a83ce368ab9.png)
此时可以在打印的界面选择缩放,缩放到合并行在一页的时候,就能正常显示了,当然这只是个临时办法,对于表格特别特别长的情况,还是只有另想办法,比如使用相关的pdf打印插件,如jspdf-Autotable等,具体使用方法可以去了解一下。
![](https://img-blog.csdnimg.cn/img_convert/fe316176c4e0778a33a52fba967bb741.png)