更新: 略显尴尬,在测试进行了各种数据测试之后,发现处理数据还是有些问题,有问题才能进步嘛,哈哈哈,还好发现及时,今天下午又进行了修改,对合并数据的地方修改了很多,详细内容见新的dealData方法:

dealBody(data) {let depth = 1const resultList = []const tmp = {}const { companyList = [] } = data// 用来记录每个父节点下面有多少个子节点,得到需要合并的rowspanconst levelWidth = {}const func = (dataList, fn, level, parentSysno) => {dataList.forEach((item) => {const title = item.value || ''if (item.isLeaf) {levelWidth[parentSysno] = dataList.lengthconst 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] = 0dataList.forEach((row) => {levelWidth[parentSysno] += levelWidth[row.sysno]})}if (fn) {fn(list)}}, level + 1, item.sysno)}})}func(data.clauseList, null, 1, null)this.dataList = resultListreturn depth}

一、实现背景

最近的一个项目需要将很多表格打印出来,而这些表格基本上是树形的数据,需要用到合并行和合并列,数据的组合有些复杂。最开始已经用vxe-table实现了一版,用在了网页详情展示里,但是如果要打印出来的话,vxe-table的样式就无法正常显示,比如边框不显示等问题。所以在打印预览页我采用了原生的table来显示。

二、技术难点

拿到的数据是树形的,并且由多个数据组成,这就需要我们自己去实现合并行和列。对于数据的组合,这里我用的是递归+回调的方式。

三、具体实现

先来看看效果图。

图3-1

图中,树形内容是动态的,且是树形层级的,可能存在多级,右侧动态项部分也是动态的,根据接口返回的项目有多少展示多少。所以数据处理的难点在于动态树形内容的合并。

先来想想如果要显示这样的效果图,那我们想要的数据是怎样的?先看html部分,这里我用vue写的,表头和表体我都是用的双重for循环渲染的,:

<table class="print-table"><thead><!-- 这一部分tr是为了控制表格的最低宽度,避免文字过多,挤压为一个字一行,按实际需求有的最低是4个字,有的是5个字,当然这个宽度也是根据当前页面的字号来估计的。这一行实际上从界面上来看是隐藏了的 --><tr><tdv-for="(item, index) in maxCols":key="index":style="{minWidth: item.minWidth + 'px'}"/></tr><!-- 这一部分tr是真的表头显示部分 --><trv-for="(column, index) in columns":key="index + 'header'"class="header"><!-- 在获取表头时就已经计算了rowspan和colspan了 --><thv-for="(item, ind) in column":key="item.field + index + ind":rowspan="item.rowspan || 1":colspan="item.colspan || 1">{{ item.name }}</th></tr></thead><tbody><!-- 这部分是表体 --><trv-for="(column, index) in dataList":key="index"><tdv-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

通过截图也可以看出来每一行的数据长度是不一致的,因为有的合并了列

[[{"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

图3-4

我们要想通过上面的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})`,})})// 处理数据,得到要显示的表体,并且通过处理数据的时候,拿到树形深度,得到树形内容所需要的colspanconst contentColspan = this.dealBody(data) || 1const 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 = maxColsthis.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 : 0fn(size + newSize, list)}}, level + 1)}})}// 第一级调用,此时没有回调函数,且level是1func(data.clauseList, null, 1)this.dataList = resultListreturn depth},

footer部分看需要显示,我这里需要显示,就简单写了一个方法:

getFooter(colspan) {const { companyList = [], conclusionList = [] } = this.infoconst result = [{value: '结论', colspan, field: guid(), class: 'center',},]companyList.forEach((item) => {const { sysno } = itemlet flag = trueconclusionList.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

这里我给tbody的td和thead的th单独设置了border,然后不给table标签上添加border,并且我在给第一行用来占位的tr下的td没有设置border,这样从界面上来看,是看不出来那一行的。

2、打印页带输入框,打印内容的显示

我所写的打印页,为了方便用户打印时临时改一些数据,则提供了一些输入框,输入框的样式可以通过打印样式查询(@media print)去设置去掉边框,这样打印的时候就不会显示边框了,但是仍然有个问题,那就是输入框很短,但是输入的内容很长的话,直接打印就会被截掉一些数据,所以此时我直接在打印的时候隐藏了输入框,然后用pre标签来显示输入的内容,这样打印的时候就能完全显示,并且能保留输入的格式,比如输入时换行的情况。

3、表格过长,合并行太多时,打印分页不显示边框的情况

因表格太长,导致打印时一个表格被拆分为了两页,但是如果分开的部分是合并了很多行,则打印时,表格会出现边框不全的情况,如图

图4-2

实际网页显示的是:

图4-3

此时可以在打印的界面选择缩放,缩放到合并行在一页的时候,就能正常显示了,当然这只是个临时办法,对于表格特别特别长的情况,还是只有另想办法,比如使用相关的pdf打印插件,如jspdf-Autotable等,具体使用方法可以去了解一下。

纯手写table展示树形数据,实现浏览器打印预览功能相关推荐

  1. 纯手写原生PHP网站管理后台系统 网站管理系统

    一.源码简介 一套纯手写原生的PHP网站管理后台,前端利用LayUI实现,实现PHP初学者专研学习使用,对于PHP学习的人,只有熟悉了原生的PHP开发,才适合利用其它框架搭建自己的网站平台.封城期间, ...

  2. SQL纯手写创建数据库到表内内容

    建表啥的只点点鼠标,太外行了,不如来看看我的纯手写,让表从无到有一系列:还有存储过程临时表,不间断的重排序: 一:建数据库 1create Database Show 2 on 3 primary 4 ...

  3. vue+js纯手写日历(包含农历,节假日)

    vue+js纯手写日历(包含农历,节假日) 使用的js 地址 dataChange.js 插件使用了elementui //完整代码 <template><div><di ...

  4. vue纯手写思维导图,拒绝插件(cv即用)

    vue纯手写思维导图,拒绝插件(cv即用) 已完成功能点:折叠.放大.缩小.移动 后续增加功能点:添加.删除 先看结果: 有这么个需求,按照层级关系,把表格放在思维导图上,我第一时间想到用插件,但是找 ...

  5. 【手写系列】纯手写实现一个高可用的RPC

    前言 在实际后台服务开发中,比如订单服务(开发者A负责)需要调用商品服务(开发者B负责),那么开发者B会和A约定调用API,以接口的形式提供给A.通常都是B把API上传到Maven私服,然后B开始写A ...

  6. 超级简单的jQuery纯手写五星评分效果

    超级简单的评分功能,分为四个步骤轻松搞定: 第一步: 引入jquery文件:这里我用百度CDN的jquery: <script src="http://apps.bdimg.com/l ...

  7. 帝君级别 纯手写 原创 jQuery入门笔记

    帝君级别 纯手写 原创 jQuery入门笔记 广治君今天下午整理了一下jQuery的入门学习思路,以及学习内容 一.学习jQuery的目的 为什么要学习jQuery,低程度的一定是你在学前端或者后端, ...

  8. iOS--MVC、自定义大小可变的view(纯手写)

    采用MVC自定义一个view,效果: 思路: 采用MVC,第一步确定model.很明显这个view的model应该由image.string.string组成.model如下: dataModel: ...

  9. 纯手写SpringFramework-完结版(原创)

    个人简介 作者是一个来自河源的大三在校生,以下笔记都是作者自学之路的一些浅薄经验,如有错误请指正,将来会不断的完善笔记,帮助更多的Java爱好者入门. 文章目录 个人简介 纯手写SpringFrame ...

最新文章

  1. 三同轴连接器_罗森伯格射频同轴连接器之板间连接器三
  2. char *a 和char a[] 的区别(指针和数组的区别)
  3. 不可不知的Python模块: collections
  4. 去了新公司,物理通过
  5. 您有新的订单提示音_《胡闹厨房:全都好吃》PS5新手柄专属功能细节揭露
  6. 二维碰撞检测matlab,二维平面内的碰撞检测【二】
  7. 常见 Java 字节码 指令 助记符
  8. Android 基础—— 对Context的理解与使用技巧
  9. Tensorflow深度学习应用(进阶篇)-回归(函数拟合训练)-可视化
  10. 电脑上mysql数据库无法登录_无法远程登入MySQL数据库的几种解决办法MySQL综合 -电脑资料...
  11. mysql服务2013错误_错误2013(HY000):在“读取授权数据包”时丢失与MySQL服务器的连接,系统错误:0...
  12. 【数据库】期末考试、考研复试、工作面试总结
  13. VC MFC 换肤 SkinSharp
  14. 温度控制pid c语言程序,51单片机温度PID算法(C程序)
  15. EyouCms1.0前台GetShell漏洞复现
  16. Python——集合运算
  17. 显示前半内容后半内容用省略号_2015年广东中考满分作文赏析:特别的一朵花_1500字...
  18. windows xp系统账号密码忘记解决办法
  19. 日本超人气洛比(Robi)声控机器人
  20. nessus在kali中的安装与详细使用

热门文章

  1. LyricEase 永久停服!可惜了!
  2. 来瞧瞧金砖大会的“护花使者”吧!
  3. pyqt5样式表设计
  4. oracle通过sqlplus 创建用户和密码
  5. HTML入门 — 网页内容的撰写
  6. Tomcat介绍,jdk安装,Tomcat安装
  7. Eclipse 设置字体大小
  8. STM32入门(十四)----EXTI
  9. windows10环境下QtCreator中出现skipping incompatible xxx when searching for xxx 问题解决办法
  10. 深入理解计算机系统——知识总结