Ant Design Vue 实现表格合并单元格并且可分页

1. 问题背景

在许多管理类系统中,表格是展示数据的常见方式。而在一些具有层级结构的数据展示场景中,表格可能包含多个数据层级,如 "大条数据" 和 "小条数据"(每大条数据包含若干小条,数量不一定)。例如,在企业项目管理系统中,每一条大条数据可能代表一项主要的项目,而该大条数据下可能包含多条相关的小条数据,比如该项目下的子任务。

这种数据展示通常可以采用多种交互形式,比如:

  • 树形表格:通过树形结构展示父子层级关系,支持展开/收起操作,方便查看层级数据。
  • 嵌套表格:每行展示大条数据,并在大条数据下嵌套子表格显示相关的小条数据。
  • 表格单元格合并(行合并) :通过合并单元格的方式,把大条数据的内容合并到多行数据中。

在 Vue 3 + Antd Design 的项目中,Table 组件可以支持上述三种交互形式,但当采用单元格合并 的方式展示这种层级数据,同时还要进行分页展示的话(假设需求为每页显示 10 大条数据),会遇到一个问题:

Antd Design Vue Table 组件默认的分页逻辑是基于数据的总行数来计算的,pageSize 会直接影响表格显示的条数。也就是说,在使用单元格行合并的时候,其分页逻辑是按小条数据计算(如果一条大条数据包含 5 条小条数据,那么该条大条数据实际上会占用分页的 5 行),而不是按大条数据为单位分页显示。

假设当前表格pageSize=10,表格则会在每一页上展示 10 条小条数据,而不是 10 条大条数据。如果每个大条数据包含多个数量不固定的小条数据,就会造成分页显示的混乱:每一页的大条数据数量不固定,且无法控制每页展示的大条数据的数量,每页可能显示的大条数据数量会大大少于 10 条,无法满足每页显示 10 大条数据的需求。

2. 解决方案

不直接使用 Antd Design Vue Table 组件自带的分页逻辑,而是通过自定义分页逻辑来实现。

3. 代码实现 demo

vue 复制代码
<template>
  <a-table
    :columns="columns"
    :data-source="formatedData"
    :pagination="false"
    bordered
  >
    <template #bodyCell="{ column, text }">
      <template v-if="column.dataIndex === 'name'">
        <a href="javascript:;">{{ text }}</a>
      </template>
    </template>
  </a-table>
  <a-pagination class="custom-pagination" size="small" v-bind="pagination" />
</template>
<script lang="ts" setup>
import { ref, computed } from "vue";
import type { TableColumnType } from "ant-design-vue";

const sharedOnCell = (record) => ({
  rowSpan: record.rowSpan,
});

const data = [
  {
    key: "1",
    name: "John Brown",
    age: 32,
    tel: "0571-22098909",
    phone: 18889898989,
    address: "New York No. 1 Lake Park",
    childList: [
      { name: "John Brown's Child 1", age: 5 },
      { name: "John Brown's Child 2", age: 3 },
    ],
  },
  {
    key: "2",
    name: "Jim Green",
    tel: "0571-22098333",
    phone: 18889898888,
    age: 42,
    address: "London No. 1 Lake Park",
    childList: [
      { name: "Jim Green's Child 1", age: 7 },
      { name: "Jim Green's Child 2", age: 4 },
    ],
  },
  {
    key: "3",
    name: "Joe Black",
    age: 32,
    tel: "0575-22098909",
    phone: 18900010002,
    address: "Sidney No. 1 Lake Park",
    childList: [{ name: "Joe Black's Child 1", age: 6 }],
  },
  {
    key: "4",
    name: "Jim Red",
    age: 18,
    tel: "0575-22098909",
    phone: 18900010002,
    address: "London No. 2 Lake Park",
    childList: [{ name: "Jim Red's Child 1", age: 4 }],
  },
  {
    key: "5",
    name: "Jake White",
    age: 18,
    tel: "0575-22098909",
    phone: 18900010002,
    address: "Dublin No. 2 Lake Park",
    childList: [
      { name: "Jake White's Child 1", age: 2 },
      { name: "Jake White's Child 2", age: 2 },
    ],
  },
];

const columns: TableColumnType[] = [
  {
    title: "No",
    dataIndex: "index",
    customCell: sharedOnCell,
  },
  {
    title: "Name",
    dataIndex: "name",
    customCell: sharedOnCell,
  },
  {
    title: "Age",
    dataIndex: "age",
    customCell: sharedOnCell,
  },
  {
    title: "Home phone",
    colSpan: 2,
    dataIndex: "tel",
    customCell: sharedOnCell,
  },
  {
    title: "Phone",
    colSpan: 0,
    dataIndex: "phone",
    customCell: sharedOnCell,
  },
  {
    title: "Children",
    children: [
      {
        title: "Name",
        dataIndex: "childrenName",
      },
      {
        title: "Age",
        dataIndex: "childrenAge",
      },
    ],
  },
  {
    title: "Address",
    dataIndex: "address",
    customCell: sharedOnCell,
  },
];

const pageSize = ref(3);
const currentPage = ref(1);

const formatedData = computed(() => {
  const start = (currentPage.value - 1) * pageSize.value;
  const end = start + pageSize.value;
  const lastData = data.slice(start, end).flatMap((item, index) => {
    // 每条大数据展开成小数据
    return item.childList.map((subItem, subIndex) => ({
      ...item,
      childrenName: subItem.name,
      childrenAge: subItem.age,
      key: `${item.key}-${subIndex}`,
      rowSpan: subIndex === 0 ? item.childList.length : 0,
      index: start + index + 1,
    }));
  });
  return lastData;
});

const pagination = computed(() => {
  return {
    current: currentPage.value,
    pageSize: pageSize.value,
    total: data.length,
    showTotal: (total) => `共 ${total} 条数据`,
    showSizeChanger: true,
    showQuickJumper: true,
    pageSizeOptions: ["3", "10", "15", "20", "50"],
    onChange: (page, newPageSize) => {
      currentPage.value = page;
      pageSize.value = newPageSize;
    },
  };
});
</script>

<style scoped>
.custom-pagination {
  display: flex;
  padding: 12px 0;
  justify-content: flex-end;
}
</style>

4. 实现效果

预览:在 CodeSandbox 中打开

相关推荐
成长ing1213840 分钟前
cocos creator 放大镜效果
前端·cocos creator
noravinsc1 小时前
vue2 definecomponent is not defined
前端·javascript·vue.js
禁默1 小时前
【学术投稿-2025年计算机视觉研究进展与应用国际学术会议 (ACVRA 2025)】CSS样式解析:行内、内部与外部样式的区别与优先级分析
前端·css
姚永强2 小时前
web前端第三次作业
前端·javascript·css
编程星空2 小时前
diff算法简析
前端·javascript·html
邢行行2 小时前
全面解析 CSS 常见简写语法及其使用规则
前端
田本初2 小时前
【React】如何画一个箭头
前端·react.js·前端框架
落榜美术生2 小时前
vite中的依赖预构建 到底是什么 做了什么
前端
优雅永不过时·2 小时前
原生Three.js 和 Cesium.js 案例 。 智慧城市 数字孪生常用功能列表
前端·javascript·低代码·编辑器·智慧城市·webgl·three.js
Gazer_S2 小时前
【CodePen实战:撤销重做功能全记录】
前端·javascript·vue.js