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 中打开

相关推荐
星空椰23 分钟前
JavaScript 基础入门:从零开始掌握变量与数据类型
开发语言·前端·javascript·ecmascript
千寻简28 分钟前
一个让 Claude Code 顺手很多的状态栏插件:claude-hud
前端·后端
掘金者阿豪30 分钟前
数据库安全第一关:用户密码存储与认证机制的深度拆解
java·前端·后端
MgArcher37 分钟前
Python高级特性:sorted() 排序完全指南
前端·后端
MgArcher37 分钟前
Python高级特性:返回函数与闭包完全指南
前端·后端
HelloReader37 分钟前
QML 最佳实践写出高质量、可维护、高性能的代码(十二)
前端
HelloReader38 分钟前
Qt Quick Controls 全览控件、弹窗、导航与样式定制(十一)
前端
意法半导体STM3242 分钟前
【官方原创】STM32 USBx Host HID standardalone移植示例 LAT1449
开发语言·前端·stm32·单片机·嵌入式硬件
竹林81843 分钟前
用wagmi v2构建DeFi前端:从连接钱包到读取合约数据的完整实战与避坑指南
前端·javascript
over69743 分钟前
面试官视角:TypeScript Pick 工具类型深度解析与手写实现
前端·面试