为什么写
- 所在公司 hybri个d 的 h5 中,多行表头表格是个常见的开发需求;
- UI 对表格的样式有严格的要求,表格得是圆角,头部得吸顶,首列得固定,样式要易于调整;
- 移动端表格交互较少,并不需要展开行或者选中多行这类开源组件库常提供的功能;
- kpi 中有对性能的要求,这就有了舍弃其他功能,支持多行表头的,体积更小的表格组件。
实现什么功能
- 表格边框是圆角;
- 头部吸顶,首列固定,有语意明确的 css 类名和大量的 css 变量以便样式变更;
- 用与 el 相同结构的 header 嵌套数组渲染出多行表头,难点在于处理单元格的跨行和跨列。结构是这样的:➡️github.com/FHSWar/usef…
具体实现
表格边框的圆角
html 的 table 标签的 border 一定是直角的,因此需要用 div 来做 table 的边框,实现视觉上的圆角效果。
头部吸顶和首列固定
分拆出 thead,通过相对定位将头部该在原表格上,实现视觉上的吸顶; 用 sticky 布局,计算出 left 的值,赋值给对应的列。
实现多行表头
根据输入的 header 数组,将一位数组分解为对应表头行数的二维数组,该二维数组的长为对应表头行数。
- 首先处理行,根据数组计算每个单元所在行数和跨度,数组的第一层的对象们是第一行。若某单元格前面嵌套了 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
}
- 接着处理列,一个单元格 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
}
- 最后是将这个多叉树变形成二位数组
// 把树结构摊平
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…
效果图