Vue3 + Element Plus 实现多列合并单元格功能详解
在使用 Element Plus 的 el-table 组件进行数据展示时,我们常常需要根据某些字段的值来对表格中的单元格进行合并操作。本文将详细讲解如何通过 Vue 3(组合式 API)和 Element Plus 实现 多列合并单元格 的功能,并结合代码示例说明其实现原理。
🎯 功能目标
实现一个可配置的组件,支持以下特性:
- 表格数据动态传入;
- 支持自定义表头列(columns);
- 支持指定多个字段进行合并(mergeProps);
- 合并逻辑遵循“按字段顺序依次合并”原则;
- 自动排序以保证相同字段连续排列;
- 使用
span-method实现跨行合并。
🧱 技术栈
- Vue 3(Composition API)
- Element Plus
- JavaScript 数组、对象、字符串处理
💡 实现思路
1. 数据准备与排序
为确保相同的字段值连续排列,以便后续计算 rowspan,我们需要对数据进行排序。排序规则是按照 mergeProps 中字段顺序进行多重比较。
const sortedTableData = computed(() => {
if (!props.mergeProps.length) return props.tableData;
return [...props.tableData].sort((a, b) => {
for (let key of props.mergeProps) {
let av = a[key] ?? "";
let bv = b[key] ?? "";
let res = String(av).localeCompare(String(bv));
if (res != 0) return res;
}
return 0;
});
});
✅ 使用
localeCompare确保数字、字符串都能正确排序。
2. 计算 rowspan 矩阵
为了记录每个字段在每一行的合并信息,我们构建一个二维数组 rowSpanMatrix:
const rowSpanMatrix = ref([]);
核心函数 calcRowSpan() 遍历排序后的数据,逐层判断是否可以合并:
const calcRowSpan = () => {
let matrix = [];
let data = sortedTableData.value;
let propsArr = props.mergeProps;
if (!propsArr.length) {
rowSpanMatrix.value = [];
return;
}
const markSpan = (rows, mergeIndex, baseIndex) => {
const key = propsArr[mergeIndex];
let i = 0;
while (i < rows.length) {
let count = 1;
let j = i + 1;
while (
j < rows.length &&
rows[j][key] === rows[i][key] &&
propsArr.slice(0, mergeIndex).every((k) => rows[j][k] === rows[i][k])
) {
count++;
j++;
}
matrix[baseIndex + i] = matrix[baseIndex + i] || Array(propsArr.length).fill(1);
matrix[baseIndex + i][mergeIndex] = count;
for (let k = i + 1; k < j; k++) {
matrix[baseIndex + k] = matrix[baseIndex + k] || Array(propsArr.length).fill(1);
matrix[baseIndex + k][mergeIndex] = 0;
}
if (mergeIndex + 1 < propsArr.length) {
markSpan(rows.slice(i, j), mergeIndex + 1, baseIndex + i);
}
i = j;
}
};
markSpan(data, 0, 0);
rowSpanMatrix.value = matrix;
};
🔍 这里采用了递归的方式:先判断第一个字段是否相同,再在子集中判断第二个字段,依此类推。
3. 表格渲染与 spanMethod
在 el-table 中使用 span-method 来控制单元格的合并行为:
<el-table
:data="sortedTableData"
border
:span-method="mergeProps.length ? spanMethod : undefined"
>
<template v-for="column in columns">
<el-table-column :prop="column.prop" :label="column.label" :width="column.width" />
</template>
</el-table>
对应的 spanMethod 方法如下:
function spanMethod({ row, column, rowIndex, columnIndex }) {
const mergeProps = props.mergeProps;
if (!mergeProps.length) return;
const prop = props.columns[columnIndex]?.prop;
const mergeIndex = mergeProps.indexOf(prop);
if (mergeIndex === -1) return;
if (rowSpanMatrix.value[rowIndex]?.[mergeIndex] > 0) {
return [rowSpanMatrix.value[rowIndex][mergeIndex], 1];
} else {
return [0, 0];
}
}
📌 注意:
- 只有被
mergeProps指定的列才会合并;- 返回
[rowspan, colspan],其中colspan=1表示只合并行;- 若
rowspan=0表示该单元格不显示。
🧪 示例数据
你可以这样使用组件:
<template>
<MergeTable
:table-data="tableData"
:columns="columns"
:merge-props="['name', 'age']"
/>
</template>
<script setup>
import MergeTable from "./components/MergeTable.vue";
const tableData = [
{ name: "张三", age: 25, city: "北京" },
{ name: "张三", age: 25, city: "上海" },
{ name: "李四", age: 30, city: "广州" },
{ name: "李四", age: 30, city: "深圳" },
];
const columns = [
{ prop: "name", label: "姓名" },
{ prop: "age", label: "年龄" },
{ prop: "city", label: "城市" },
];
</script>
🧩 效果图示意
| 姓名 | 年龄 | 城市 |
|---|---|---|
| 张三 | 25 | 北京 |
| 上海 | ||
| 李四 | 30 | 广州 |
| 深圳 |
✅ 第一列和第二列都进行了合并,第三列保持独立。
🧾 总结
本组件实现了基于 Vue 3 和 Element Plus 的多列单元格合并功能,具有以下优势:
- 高度可配置:通过
mergeProps控制合并字段; - 自动排序:保证相同字段值连续,便于合并;
- 递归算法:精确控制多级合并逻辑;
- 兼容性好:适用于大多数场景下的表格展示需求。
📚 扩展建议
- 支持横向合并(colspan);
- 添加样式高亮或边框优化;
- 支持分页加载后仍能正确合并;
- 提供默认插槽支持复杂内容渲染。
📚 完整代码
<template>
<el-table
:data="sortedTableData"
border
:span-method="mergeProps.length ? spanMethod : undefined"
>
<template v-for="column in columns">
<el-table-column :prop="column.prop" :label="column.label" :width="column.width" />
</template>
</el-table>
</template>
<script setup>
import { computed, watch, ref } from "vue";
const props = defineProps({
tableData: { type: Array, required: true },
columns: { type: Array, required: true },
mergeProps: { type: Array, default: () => [] },
});
const sortedTableData = computed(() => {
if (!props.mergeProps.length) return props.tableData;
return [...props.tableData].sort((a, b) => {
for (let key of props.mergeProps) {
let av = a[key] ?? "";
let bv = b[key] ?? "";
let res = String(av).localeCompare(String(bv));
if (res != 0) return res;
}
});
});
const rowSpanMatrix = ref([]);
const calcRowSpan = () => {
let matrix = [];
let data = sortedTableData.value;
let propsArr = props.mergeProps;
if (!propsArr.length) {
rowSpanMatrix.value = [];
return;
}
const markSpan = (rows, mergeIndex, baseIndex) => {
const key = propsArr[mergeIndex];
let i = 0;
while (i < rows.length) {
let count = 1;
let j = i + 1;
while (
j < rows.length &&
row[j][key] === rows[i][key] &&
propsArr.slice(0, mergeIndex).evert((k) => rows[j][k] === rows[i][k])
) {
count++;
j++;
}
matrix[baseIndex + i] = matrix[baseIndex + i] || Array(propsArr.length).fill(1);
matrix[baseIndex + i][mergeIndex] = count;
for (let k = i + 1; k < j; k++) {
matrix[baseIndex + k] = matrix[baseIndex + k] || Array(propsArr.length).fill(1);
matrix[baseIndex + k][mergeIndex] = 0;
}
if (mergeIndex + 1 < propsArr.length) {
markSpan(rows.slice(i, j), mergeIndex + 1, baseIndex + i);
}
i = j;
}
};
markSpan(data, 0, 0);
rowSpanMatrix.value = matrix;
};
watch(() => [sortedTableData.value, props.mergeProps], calcRowSpan, {
deep: true,
immediate: true,
});
function spanMethod({ row, column, rowIndex, columnIndex }) {
const mergeProps = props.mergeProps;
if (!mergeProps.length) return;
const prop = props.columns[columnIndex]?.prop;
const mergeIndex = mergeProps.indexOf(prop);
if (mergeIndex === -1) return;
if (rowSpanMatrix.value[rowIndex]?.[mergeIndex] > 0) {
return [rowSpanMatrix.value[rowIndex][mergeIndex], 1];
} else {
return [0, 0];
}
}
</script>
补充,如果想要有数字排序的话
const sortedTableData = computed(() => {
if (!props.mergeProps.length) return props.tableData;
return [...props.tableData].sort((a, b) => {
for (let key of props.mergeProps) {
let av = a[key] ?? "";
let bv = b[key] ?? "";
let an = Number(av),
bn = Number(bv);
let aIsNum = !isNaN(an);
let bIsNum = !isNaN(bn);
if (aIsNum && bIsNum) {
if (an !== bn) return an - bn;
} else {
let res = String(av).localeCompare(String(bv));
if (res != 0) return res;
}
}
return 0
});
});
如果你觉得这篇文章对你有帮助,欢迎点赞、收藏或者分享给需要的朋友!如需进一步封装成 npm 插件或增加更多高级功能,也可以留言交流 😊