vue2.x 写一个支持多行表头的表格

456 阅读3分钟

为什么写

  1. 所在公司 hybri个d 的 h5 中,多行表头表格是个常见的开发需求;
  2. UI 对表格的样式有严格的要求,表格得是圆角,头部得吸顶,首列得固定,样式要易于调整;
  3. 移动端表格交互较少,并不需要展开行或者选中多行这类开源组件库常提供的功能;
  4. kpi 中有对性能的要求,这就有了舍弃其他功能,支持多行表头的,体积更小的表格组件。

实现什么功能

  1. 表格边框是圆角;
  2. 头部吸顶,首列固定,有语意明确的 css 类名和大量的 css 变量以便样式变更;
  3. 用与 el 相同结构的 header 嵌套数组渲染出多行表头,难点在于处理单元格的跨行和跨列。结构是这样的:➡️github.com/FHSWar/usef…

具体实现

表格边框的圆角

html 的 table 标签的 border 一定是直角的,因此需要用 div 来做 table 的边框,实现视觉上的圆角效果。

头部吸顶和首列固定

分拆出 thead,通过相对定位将头部该在原表格上,实现视觉上的吸顶; 用 sticky 布局,计算出 left 的值,赋值给对应的列。

实现多行表头

根据输入的 header 数组,将一位数组分解为对应表头行数的二维数组,该二维数组的长为对应表头行数。

  1. 首先处理行,根据数组计算每个单元所在行数和跨度,数组的第一层的对象们是第一行。若某单元格前面嵌套了 n 个 children,那这个单元格就在 n+1 行。一个表格有 m 行表头,而某单元格在第 n 行就没 chilren 了,那这个单元的 rowspan 就是 m-n+1,也就是跨了 m-n+1 行;比如一个两行的表头,一个第一行的单元格就没了 children,那这个单元格就跨两行。
// 计算表头行数,每个单元格深度,由深度及是否有 children 算出单元格 rowspan
handleRows(arr) {
        let outerDepth = 1
        const copy = JSON.parse(JSON.stringify(arr))

        // 计算出 item 所在层级,由上至下,单向,不出错
        function tagLayer(copy, depth) {
                depth++
                for (const item of copy) {
                        item.layer = depth - 1
                        if (item.children.length !== 0) {
                                if (Math.max(depth, outerDepth) !== outerDepth)
                                        outerDepth = depth
                                tagLayer(item.children, depth)
                        }
                }
        }
        // 根据 outerDepth 和所在 layer 计算具体 item 应有的 rowspan
        function calcRowspans(copy) {
                for (const item of copy) {
                        if (item.children.length === 0) {
                                item.rowspan = outerDepth - item.layer + 1
                        } else {
                                item.rowspan = 1
                                calcRowspans(item.children)
                        }
                }
        }
        tagLayer(copy, 1)
        calcRowspans(copy)

        return copy
}
  1. 接着处理列,一个单元格 chilren 数组的长度就是这个单元格跨的列数,也就是 colspan。
// 计算表头行数,每个单元格深度,由深度及是否有 children 算出单元格 rowspan
// 給表头各列加 colspan 属性并赋予正确的值
handleColumns(arr) {
        const copy = JSON.parse(JSON.stringify(arr))

        // 数树形结构第一层每个节点各有多少子结点
        function countLeaves(obj) {
                let res = 0
                function toLeafLayer(obj) {
                        if (obj.children.length !== 0) {
                                for (const childObj of obj.children) {
                                        countLeaves(childObj)
                                        toLeafLayer(childObj)
                                }
                        } else {
                                res++
                        }
                }
                toLeafLayer(obj)
                obj.colspan = res
                return obj
        }
        // 给树形结构第一层第一个节点的所有子节点打标记
        function tagFistColumn(arr, init, isFirstColumn) {
                for (const [index, item] of arr.entries()) {
                        // 只有第一大列才会有标记,才会往下
                        if ((init && index === 0) || isFirstColumn) item.isFirstColumn = true
                        // 有子结点且属于第一大列,就递归的打标记
                        if (item.children.length !== 0 && item.isFirstColumn === true) {
                                tagFistColumn(item.children, null, true)
                        }
                }
        }
        copy.map(obj => countLeaves(obj))
        tagFistColumn(copy, true)

        return copy
}
  1. 最后是将这个多叉树变形成二位数组
// 把树结构摊平
flatTree(root) {
        const result = [], queue = []
        queue.push(root)

        while (queue.length !== 0) {
                const item = queue.shift()
                if (item.children.length > 0) {
                        item.children.map(childItem => queue.push(childItem))
                }
                result.push(item)
        }
        result.shift() // 以 {children: Array} 入参传进来的在第一个,要去掉

        return result
                .reduce((acc, cur) => {
                        Array.isArray(acc[cur.layer - 1])
                                ? acc[cur.layer - 1].push(cur)
                                : (acc[cur.layer - 1] = [cur])
                        return acc
                }, [])
}

4.经过这三步处理的数组就能交给 vue template 渲染了。

// 初始化 headerConfig 供模版渲染用
initHeader(columns) {
        // dirty 是相对于模版渲染需要而言的
        this.tree = this.handleColumns(this.handleRows(columns))
        this.headerConfig = this.flatTree({ children: this.tree })
}
<template>
<table ref="fhsTable" class="fhs-table__body">
    <!-- 表头的动态渲染 -->
    <thead ref="tableHead">
      <tr v-for="(rowData, row) in headerConfig" :key="`head-${row}`">
        <th
          v-for="({ align, colspan, isFirstColumn, rowspan, title, width }, column) in rowData"
          :key="`${row}-${column}`"
          :colspan="colspan"
          :rowspan="rowspan"
          :style="customCell({ align, column, isFirstColumn, isTh: true, width })"
          :class="thClasses({ column, row })"
        >
          {{ title }}
        </th>
      </tr>
    </thead>
    <!-- 数据的动态渲染 -->
    <tbody ref="tableBody">
      <tr v-for="(rowData, row) in content" :key="`data-${row}`">
        <td
          v-for="({ align, isFirstColumn, prop, width }, index) in headerContentBoundary"
          :key="`${prop}-${index}`"
          :style="customCell({ align, column: index, isFirstColumn, width })"
          :class="tdClasses({ column: index, content, row })"
        >
          {{ rowData[prop] || '-' }}
        </td>
      </tr>
    </tbody>
</table>
</template>

完整的代码

在这里➡️:github.com/FHSWar/usef…

效果图

image.png