Vue3 Element Plus 实现多列层级合并单元格

471 阅读4分钟

Vue3 + Element Plus 实现多列合并单元格功能详解

在使用 Element Plusel-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 插件或增加更多高级功能,也可以留言交流 😊