element实现需同时满足多行合并和展开的表格
需求描述:
以下面这张图为例,此表格的“一级表格”这一行可能存在多行数据,这种情况下需要将“一级指标”,“一级指标扣分xxx”,“一级指标关联xxx”这三列数据的行展示根据后面数据(“二级指标”,…等)进行合并。同时,对于含有多条“二级指标”的单行数据,需要支持可展开可合并。
技术选择:
当前项目的技术栈是vue3,虽然公司有自己组件库,但是私有库有不完美的地方,而且暴露出来的api不够详细,所以选择了element-plus。
element有案例提供“合并行或列”和“展开行的表格”,但是没有提供两者混合使用的表格,本文章以此为基础进行开发。
技术实现:
1. span-method
这是el-table中提供的合并行或列的计算方法以本段代码中提供的objectSpanMethod
为例:
-
判断是否为子行:
const isChildRow = row.first === "";
:通过检查row.first
是否为空字符串来判断当前行是否为子行。
-
处理列索引小于等于2的情况:
-
如果当前行有子行 (
row.children
),进一步检查当前行是否展开 (
row.expanded
):
- 如果展开,返回
rowspan
为子行数加1,colspan
为1。 - 如果未展开,返回
rowspan
和colspan
都为1。
- 如果展开,返回
-
如果当前行是子行,返回
rowspan
和colspan
都为0,表示不显示该单元格。 -
如果当前行既不是子行也没有子行,返回
rowspan
和colspan
都为1。
-
-
处理列索引大于2的情况:
- 默认返回
rowspan
和colspan
都为1。
const objectSpanMethod = ({ row, column, rowIndex, columnIndex }) => {// 判断当前行是否为子行const isChildRow = row.first === "";// 如果列索引小于等于2 这里因为是前三列数据需要合并if (columnIndex <= 2) {// 如果当前行有子行if (row.children) {// 如果当前行是展开状态if (row.expanded) {return {rowspan: row.children.length + 1, // 合并行数为子行数加1colspan: 1, // 合并列数为1};} else {return {rowspan: 1, // 合并行数为1colspan: 1, // 合并列数为1};}} else if (isChildRow) {// 如果当前行是子行return {rowspan: 0, // 不显示该单元格colspan: 0, // 不显示该单元格};} else {// 如果当前行既不是子行也没有子行return {rowspan: 1, // 合并行数为1colspan: 1, // 合并列数为1};}}// 对于列索引大于2的情况,默认返回合并行数和列数都为1return {rowspan: 1,colspan: 1,}; };
- 默认返回
2. expand-change
当用户对某一行展开或者关闭的时候会触发该事件(展开行时,回调的第二个参数为 expandedRows;树形表格时第二参数为 expanded)
- 保证数据状态同步:在行展开或折叠时,更新
row.expanded
属性可以让合并单元格的逻辑(如objectSpanMethod
)根据当前状态返回正确的rowspan
和colspan
值。 - 刷新布局保证显示正确:由于 Vue 更新 DOM 是异步的,使用
nextTick
确保在数据更新后再调用表格组件的doLayout
方法,避免因布局未刷新而出现错位问题。 - 应对树形结构合并单元格:对于存在 children 的行,其展开/折叠状态直接影响整个表格的显示效果,因此立即刷新布局可以确保所有合并单元格重新计算后显示正常。
const handleExpandChange = (row, expanded) => {if (row.children) {row.expanded = expanded;
// 使用 nextTick 包裹一个回调函数,确保在 Vue 完成 DOM 更新之后再刷新表格布局。nextTick(() => {// 如果 el-table 提供 doLayout 方法则调用刷新布局tableRef.value?.doLayout && tableRef.value.doLayout();});}
};
全部代码:
<template><el-tableref="tableRef":data="originalData"style="width: 100%; margin-bottom: 20px"row-key="id"border:span-method="objectSpanMethod":tree-props="{ children: 'children' }"@expand-change="handleExpandChange"><el-table-column prop="first" label="一级指标" /><el-table-column prop="directDeductPoint" label="一级指标扣分范围" /><el-table-column prop="relatedProcessVOS" label="一级指标关联流程" /><el-table-column prop="second" label="二级指标" /><el-table-column prop="third" label="三级指标" /><el-table-column prop="evalDesc" label="考核说明" /><el-table-column prop="deductType" label="考核扣分范围" /><el-table-column prop="enableYn" label="是否启用" /></el-table>
</template><script lang="ts" setup>
import { ref, nextTick } from "vue";// 获取表格组件 ref
const tableRef = ref(null);const originalData = ref([{id: 1,first: "一级指标1111",directDeductPoint: "一级指标扣分范围1111",relatedProcessVOS: "一级指标关联流程1111",second: " 二级指标1111",third: "三级指标1111",evalDesc: "考核说明1111",deductType: "考核扣分范围1111",enableYn: "是",},{id: 2,first: "一级指标2222",directDeductPoint: "一级指标扣分范围2222",relatedProcessVOS: "一级指标关联流程2222",second: "",third: " ",evalDesc: "",deductType: "",enableYn: "",children: [{id: 31,first: "",directDeductPoint: "",relatedProcessVOS: "",second: "二级指标212121",third: "三级指标212121",evalDesc: "考核说明212121",deductType: "考核扣分范围212121",enableYn: "是",},{id: 32,first: "",directDeductPoint: "",relatedProcessVOS: "",second: "二级指标222222",third: " 三级指标222222",evalDesc: "考核说明222222",deductType: "考核扣分范围222222",enableYn: "是",},],},{id: 3,first: "一级指标3333",directDeductPoint: "一级指标扣分范围3333",relatedProcessVOS: "一级指标关联流程3333",second: "二级指标3333",third: "三级指标3333",evalDesc: "考核说明3333",deductType: "考核扣分范围3333",enableYn: "是",},{id: 4,first: "一级指标4444",directDeductPoint: "一级指标扣分范围4444",relatedProcessVOS: "一级指标关联流程4444",second: "二级指标4444",third: "三级指标4444",evalDesc: "考核说明4444",deductType: "考核扣分范围4444",enableYn: "是",},{id: 5,first: "一级指标5555",directDeductPoint: "一级指标扣分范围5555",relatedProcessVOS: "一级指标关联流程5555",second: "二级指标5555",third: "三级指标5555",evalDesc: "考核说明5555",deductType: "考核扣分范围5555",enableYn: "是",},
]);const objectSpanMethod = ({ row, column, rowIndex, columnIndex }) => {const isChildRow = row.first === "";if (columnIndex <= 2) {if (row.children) {if (row.expanded) {return {rowspan: row.children.length + 1,colspan: 1,};} else {return {rowspan: 1,colspan: 1,};}} else if (isChildRow) {return {rowspan: 0,colspan: 0,};} else {return {rowspan: 1,colspan: 1,};}}return {rowspan: 1,colspan: 1,};
};const handleExpandChange = (row, expanded) => {console.log("handleExpandChange", row, expanded);if (row.children) {row.expanded = expanded;nextTick(() => {// 如果 el-table 提供 doLayout 方法则调用刷新布局tableRef.value?.doLayout && tableRef.value.doLayout();});}
};
</script><style lang="scss" scoped>
.cus-table {:deep(.cell) {color: #002f59;}.table-column-btn {:deep(.kui-link--primary) {color: #005bac;}}
}
</style>