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

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

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

先来想想如果要显示这样的效果图,那我们想要的数据是怎样的?先看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

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

[
    [
        {
            "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})`,
        })
      })
      // 处理数据,得到要显示的表体,并且通过处理数据的时候,拿到树形深度,得到树形内容所需要的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

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

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

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

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

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

图4-2

实际网页显示的是:

图4-3

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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
实现手写体识别,可以使用Python中的决策树分类器。这个分类器可以根据输入的特征向量,将输入的样本分类到不同的类别中。 在手写体识别中,每个手写数字都可以被表示为一个28x28像素的图像,也就是说每个手写数字都可以被表示为一个784维的特征向量。因此,我们可以使用这些特征向量来训练我们的决策树分类器,并使用它来对新的手写数字进行分类。 在Python中,有很多库可以帮助我们实现决策树分类器,比如scikit-learn。下面是一个简单的代码,使用scikit-learn库实现手写体识别: ```python from sklearn.tree import DecisionTreeClassifier from sklearn.datasets import load_digits from sklearn.model_selection import train_test_split # 加载手写数字数据集 digits = load_digits() # 分割数据集 X_train, X_test, y_train, y_test = train_test_split(digits.data, digits.target, test_size=0.3) # 创建决策树分类器 clf = DecisionTreeClassifier() # 训练分类器 clf.fit(X_train, y_train) # 对测试集进行预测 y_pred = clf.predict(X_test) # 输出预测结果 print("Accuracy:", clf.score(X_test, y_test)) ``` 在上面的代码中,我们首先加载了scikit-learn库中的DecisionTreeClassifier类,它可以帮助我们实现决策树分类器。然后,我们使用load_digits()函数加载手写数字数据集,并使用train_test_split()函数将数据集分割为训练集和测试集。 接下来,我们创建了一个DecisionTreeClassifier对象,并使用fit()函数训练了分类器。最后,我们使用predict()函数对测试集进行预测,并输出预测结果和准确率。 需要注意的是,上面的代码并没有提供数据集,您可以通过搜索“手写数字数据集”来找到适合您的数据集,并将它加载到代码中。 希望这个回答能够帮助您。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值