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, // 合并行数为子行数加1 colspan: 1, // 合并列数为1 }; } else { return { rowspan: 1, // 合并行数为1 colspan: 1, // 合并列数为1 }; } } else if (isChildRow) { // 如果当前行是子行 return { rowspan: 0, // 不显示该单元格 colspan: 0, // 不显示该单元格 }; } else { // 如果当前行既不是子行也没有子行 return { rowspan: 1, // 合并行数为1 colspan: 1, // 合并列数为1 }; } } // 对于列索引大于2的情况,默认返回合并行数和列数都为1 return { 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-table
ref="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>