antd table 合并相邻相同单元格数据

antd 表格行/列合并

定义及使用

表头只支持列合并,使用 column 里的 colSpan 进行设置。

表格支持行/列合并,使用 onCell 里的单元格属性 colSpan 或者 rowSpan 设置。 设置为 0 时,设置的表格不会渲染(所以在设置的时候前面的行/列合并了后面的行/列时,后面的被合并的行/列渲染时需要设为 0)。

1、表头列合并时,在 table 定义 columns 时指定 colSpan 即可,为了显示正常要在被前面合并列的后面的几个 column 中指定 colSpan: 0
在这里插入图片描述

示例代码:

const columns = [
  {
    title: "Home phone",
    colSpan: 2,
    dataIndex: "tel",
  },
  {
    title: "Phone",
    colSpan: 0,
    dataIndex: "phone",
  },
];

上面 Home Phone 列指定 colSpan:2 合并了两列(当前列和后面的 Phone 列),为了显示正常(表头不会多出一列),在 Phone 列中需要指定 colSpan: 0

2、数据行合并,在 onCell 函数中返回一个包含 rowSpan 参数的对象
在这里插入图片描述

示例代码:

{
  title: 'Home phone',
  colSpan: 2,
  dataIndex: 'tel',
  onCell: (_, index) => {
    if (index === 2) {
      return {
        rowSpan: 2,
      };
    }
    // These two are merged into above cell
    if (index === 3) {
      return {
        rowSpan: 0,
      };
    }
    if (index === 4) {
      return {
        colSpan: 0,
      };
    }
    return {};
  },
}

上面代码表示在 Home phone 这一列,第 3 行跨两行即合并第 3、4 为一个单元格。第 4 行由于是被合并所以设置为 0,第五行由于列被合并所以 colSpan 设为 0

3、数据列合并,在 onCell 函数中返回一个包含 colSpan 参数的对象
在这里插入图片描述

示例代码:

{
  title: 'Name',
  dataIndex: 'name',
  render: (text) => <a>{text}</a>,
  onCell: (_, index) => ({
    colSpan: index < 4 ? 1 : 5,
  }),
}

上面代码代表 Name 这一列,在第 5 行前面不跨列,在不小于第 5 行后跨 5 列(包含自身)

相邻数据行合并

思维衍生,根据这个我们可以做一个相同数据单元格合并的功能,具体为:
1、如果多行相邻的行数据,上下相邻的单元格数据相同,就把上下相邻且相同的单元格合并。
2、可以给上面的单元格合并加一个限制条件,例如前面的第一列 id 相同的情况下,且出现上述 1 的相邻相同数据才合并

图例:
在这里插入图片描述

  • 上述我们可以用以 name 相同作为合并的前提条件,合并后面的 Age 列。
  • 以 Age 相同作为合并的前提条件,合并后面的 Home phone 列。
  • 不设置前置条件合并所有上下相邻的且值相同的列:phone。
  • 以 Home phone 相同作为合并的前提条件,合并后面的 Address列。

原理:
onCell 方法在当前列的每个单元格都会执行一次,首先会判断当前单元格是已经被合并。

  • 如果否,需要合并的列执行的时候记录当前单元格的值,从前往后遍历表格数据(tableData),遇到相同列(上下相邻行),并且相同数据的行,就记录并把合并行的计数+1,直到遇到不同的值就返回需要合并的行总数(如{rowSpan: 2})。
  • 如果是,后面已经被合并的单元格执行 onCell 方法时,会去rowSpanRecord查看自己是否被合并了,合并了就返回不渲染当前单元格(即返回 {rowSpan:0})

示例代码:

/* 
rowSpanRecord 数据结构
rowSpanRecord = {
  name: {
    'John Browne': {
      rowIndexs: [0]
    },
    'Jim Green': {
      rowIndexs: [1,2]
    },
    'Joe Black': {
      rowIndexs: [3]
    },
    'Jim Red': {
      rowIndexs: [4]
    },
    'Jake White': {
      rowIndexs: [5]
    },
  },
  age: {
    32: {
      rowIndexs: [0,1,2]
    },
    42: {
      rowIndexs: [3]
    },
    16: {
      rowIndexs: [4,5]
    },
  },
  ...
}
*/
let rowSpanRecord = {
  age: {},
  tel: {}
};
columns = [
  {
    title: "Age",
    dataIndex: "age",
    // 无前置合并条件的,相同行直接合并
    onCell: (record, rowIndex) => {
      if (rowSpanRecord["age"][record.age]?.rowIndexs.includes(rowIndex)) {
        return { rowSpan: 0 };
      }
      let rowSpan = 0;
      for (let i = rowIndex; i < data.length; i++) {
        if (record.age === data[i].age) {
          rowSpan++;
          if (rowSpanRecord["age"][record.age]) {
            rowSpanRecord["age"][record.age].records.push(data[i]);
            rowSpanRecord["age"][record.age].rowIndexs.push(i);
          } else {
            rowSpanRecord["age"][record.age] = {
              records: [data[i]],
              rowIndexs: [i]
            };
          }
        } else {
          break;
        }
      }
      return { rowSpan };
    }
  },
  {
    title: "Home phone",
    dataIndex: "tel",
    // 以age相同为合并条件
    onCell: (record, rowIndex) => {
      if (rowSpanRecord["tel"][record.tel]?.rowIndexs.includes(rowIndex)) {
        return { rowSpan: 0 };
      }
      let rowSpan = 0;
      for (let i = rowIndex; i < data.length; i++) {
        if (record.age === data[i].age && record.tel === data[i].tel) {
          rowSpan++;
          if (rowSpanRecord["tel"][record.tel]) {
            rowSpanRecord["tel"][record.tel].records.push(data[i]);
            rowSpanRecord["tel"][record.tel].rowIndexs.push(i);
          } else {
            rowSpanRecord["tel"][record.tel] = {
              records: [data[i]],
              rowIndexs: [i]
            };
          }
        } else {
          break;
        }
      }
      return { rowSpan };
    }
  }
]

将 onCell 合并中合并单元格的方法进行抽象封装,核心代码:

/**
 * 合并目标列中相同值的行
 * @param record antd 提供的当前行数据
 * @param rowIndex antd 提供的当前行的序号
 * @param data 当前table的数据列表 tableData
 * @param curProp 当前需要合并行的列的属性值
 * @param sameKey 可选, 当前要合并行的列的前置条件,就是前面必须那个属性值必须相同才判断当前列相同合并
 * @returns {rowSpan: num}, 返回一个包含要合并的行数的对象
 */
const mergeRows = ({
  record = {},
  rowIndex = 0,
  data = [],
  curProp = "",
  sameKey = undefined
} = {}) => {
  /* 前面行已经进行了判断并合并,前面的行合并了后面的行之后,(该判断是后续行的判断)
  后面的行不能再显示了否则table会变形,所以这里需要将后续的列中返回 {rowSpan:0} */
  if (rowSpanRecord[curProp][record[curProp]]?.rowIndexs.includes(rowIndex)) {
    return { rowSpan: 0 };
  }
  let rowSpan = 0;
  for (let i = rowIndex; i < data.length; i++) {
    // 有 sameKey,需要判断前置条件列中的值必须相同
    if (sameKey) {
      if (
        record[sameKey] === data[i][sameKey] &&
        record[curProp] === data[i][curProp]
      ) {
        rowSpan++;
        // 将列中行的值记录
        if (rowSpanRecord[curProp][record[curProp]]) {
          rowSpanRecord[curProp][record[curProp]].rowIndexs.push(i);
        } else {
          rowSpanRecord[curProp][record[curProp]] = {
            rowIndexs: [i]
          };
        }
      } else {
        break;
      }
    } else {
      // 没有前置列,只需要判断当前列相邻列是否相同
      if (record[curProp] === data[i][curProp]) {
        rowSpan++;
        // 将列中行的值记录
        if (rowSpanRecord[curProp][record[curProp]]) {
          rowSpanRecord[curProp][record[curProp]].rowIndexs.push(i);
        } else {
          rowSpanRecord[curProp][record[curProp]] = {
            rowIndexs: [i]
          };
        }
      } else {
        break;
      }
    }
  }
  return { rowSpan };
};

补充

antd4 中使用时发现每次刷新后会有 bug,导致合并行全都不见了,原因:antd 在浏览器刷新的时候或者说在第一次渲染时 onCell 回调函数会调用两次,所以需要对后面每次重新调用进行一个数据重置。

另外在使用的时候 rowSpanRecord 合并行数据记录对象和 mergeRows 方法都可以放到组件外面

// 当前行数据,当前列的值
const recordCurPropValue = record[curProp];
// 判断是否是重复渲染(antd刷新或者第一次渲染会触发两次onCell回调),是 则重置数据
if (
  rowIndex === 0 &&
  rowSpanRecord[curProp][recordCurPropValue]?.rowIndexs?.length > 0
) {
  for (const key of Object.keys(rowSpanRecord)) {
    rowSpanRecord[key] = {};
  }
}

完整代码:

import React from "react";
import "antd/dist/antd.css";
import "./index.css";
import { Table } from "antd";
import type { ColumnsType } from "antd/es/table";

interface DataType {
  key: string;
  name: string;
  age: number;
  tel: string;
  phone: number;
  address: string;
}

const data: DataType[] = [
  {
    key: "1",
    name: "John Browne",
    age: 32,
    tel: "0571-22098909",
    phone: 18889898989,
    address: "New York "
  },
  {
    key: "2",
    name: "Jim Green",
    tel: "0571-22098333",
    phone: 18889898989,
    age: 32,
    address: "London No. "
  },
  {
    key: "20",
    name: "Jim Green",
    tel: "0571-22098333",
    phone: 18889898989,
    age: 32,
    address: "London No. "
  },
  {
    key: "3",
    name: "Joe Black",
    age: 42,
    tel: "0575-22098909",
    phone: 18900010002,
    address: "Sidney No."
  },
  {
    key: "4",
    name: "Jim Red",
    age: 16,
    tel: "0575-220989091",
    phone: 18900010002,
    address: "Sidney No."
  },
  {
    key: "5",
    name: "Jake White",
    age: 16,
    tel: "0575-220989091",
    phone: 18900010002,
    address: "Sidney No."
  }
];

/*  需要进行行合并的列的属性值对象,主要为了记录每次合并时的行, rowIndexs 为记录的相同值的行号
结构: rowSpanRecord: {age: {rowIndexs: [0,1,2,3]}} */
let rowSpanRecord = {
  age: {},
  tel: {},
  phone: {},
  address: {}
};

/**
 * 合并目标列中相同值的行
 * @param record antd 提供的当前行数据
 * @param rowIndex antd 提供的当前行的序号
 * @param data 当前table的数据列表 tableData
 * @param curProp 当前需要合并行的列的属性值
 * @param sameKey 可选, 当前要合并行的列的前置条件,就是前面必须那个属性值必须相同才判断当前列相同合并
 * @returns {rowSpan: num}, 返回一个包含要合并的行数的对象
 */
const mergeRows = ({
  record = {},
  rowIndex = 0,
  data = [],
  curProp = "",
  sameKey = undefined,
} = {}) => {
  // 当前行数据,当前列的值
  const recordCurPropValue = record[curProp];
  // 判断是否是重复渲染(antd刷新或者第一次渲染会触发两次onCell回调)
  if (
    rowIndex === 0 &&
    rowSpanRecord[curProp][recordCurPropValue]?.rowIndexs?.length > 0
  ) {
    for (const key of Object.keys(rowSpanRecord)) {
      rowSpanRecord[key] = {};
    }
  }
  // 记录中,当前要合并的列的prop,下面值的集合
  const rowSpanRecordCurPorp = rowSpanRecord[curProp];

  /**
       * 前面行已经进行了判断并合并,前面的行合并了后面的行之后,(该判断是后续行的判断)
      后面的行不能再显示了否则table会变形,所以这里需要将后续的列中返回 {rowSpan:0}
       */
  const curPropIndexs = rowSpanRecordCurPorp[recordCurPropValue]?.rowIndexs;
  if (curPropIndexs?.includes(rowIndex)) {
    return { rowSpan: 0 };
  }
  let rowSpan = 0;
  for (let i = rowIndex; i < data.length; i++) {
    // 有 sameKey,需要判断前置条件列中的值必须相同
    if (sameKey) {
      if (
        record[sameKey] === data[i][sameKey] &&
        recordCurPropValue === data[i][curProp]
      ) {
        rowSpan += 1;
        // 将列中行的值记录
        if (rowSpanRecordCurPorp[recordCurPropValue]) {
          rowSpanRecordCurPorp[recordCurPropValue].rowIndexs.push(i);
        } else {
          rowSpanRecordCurPorp[recordCurPropValue] = {
            rowIndexs: [i],
          };
        }
      } else {
        break;
      }
    } else {
      // 没有前置列,只需要判断当前列相邻列是否相同
      if (recordCurPropValue === data[i][curProp]) {
        rowSpan += 1;
        // 将列中行的值记录
        if (rowSpanRecordCurPorp[recordCurPropValue]) {
          rowSpanRecordCurPorp[recordCurPropValue].rowIndexs.push(i);
        } else {
          rowSpanRecordCurPorp[recordCurPropValue] = {
            rowIndexs: [i],
          };
        }
      } else {
        break;
      }
    }
  }
  return { rowSpan };
};

const columns: ColumnsType<DataType> = [
  {
    title: "Name",
    dataIndex: "name",
    render: (text) => <a>{text}</a>
  },
  {
    title: "Age",
    dataIndex: "age",
    onCell: (record, rowIndex) => {
      // 这是第一个指明相同的合并的列,前面的列可以没有合并,比如name
      return mergeRows({
        record,
        rowIndex,
        data,
        curProp: "age",
        sameKey: "name"
      });
    }
  },
  {
    title: "Home phone",
    dataIndex: "tel",
    onCell: (record, rowIndex) => {
      // 指明前置条件 age 必须相同,只有当age相同时,当前列有相邻相同行值才会合并
      return mergeRows({
        record,
        rowIndex,
        data,
        curProp: "tel",
        sameKey: "age"
      });
    }
  },
  {
    title: "Phone",
    dataIndex: "phone",
    onCell: (record, rowIndex) => {
      // 没有加第四个参数 age,没有前置条件所以会合并当前列相同项
      return mergeRows({ record, rowIndex, data, curProp: "phone" });
    }
  },
  {
    title: "Address",
    dataIndex: "address",
    onCell: (record, rowIndex) => {
      // 不管前面的age,现在以Home Phone必须相同,也能生效
      return mergeRows({
        record,
        rowIndex,
        data,
        curProp: "address",
        sameKey: "tel"
      });
    }
  }
];

const App: React.FC = () => (
  <Table columns={columns} dataSource={data} bordered />
);

export default App;

如果想对第一列的 Name 相同值也做合并,需要在并且在 name 列加上 onCell,并且 rowSpanRecord 对象中需要加上 name 属性

{
  title: "Name",
  dataIndex: "name",
  onCell: (record, rowIndex) => {
    // 没有前置条件所以会合并当前列相同项
    return mergeRows({ record, rowIndex, data, curProp: "name" });
  }
}

图例:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
我们调用这个方法还得写一个全局变量,不能直接通过引用的方式,将上面的 rowSpanRecord 封装进 mergeRows 方法里面,就可以让他以工具方法的形式进行引用调用了,封装代码:

/**
 * 合并目标列中相同值的行
 * @param record antd 提供的当前行数据
 * @param rowIndex antd 提供的当前行的序号
 * @param data 当前table的数据列表 tableData
 * @param curProp 当前需要合并行的列的属性值
 * @param sameKey 可选, 当前要合并行的列的前置条件,就是前面必须那个属性值必须相同才判断当前列相同合并
 * @returns {rowSpan: num}, 返回一个包含要合并的行数的对象
 */
const mergeRows = () => {
  // 合并行的数据记录
  let rowSpanRecord = {};
  return ({
    record = {},
    rowIndex = 0,
    data = [],
    curProp = "",
    sameKey = undefined,
  } = {}) => {
    // 往rowSpanRecord中添加数据
    if (!rowSpanRecord[sameKey] && sameKey) {
      rowSpanRecord[sameKey] = {};
    }
    // 往rowSpanRecord中添加数据
    if (!rowSpanRecord[curProp]) {
      rowSpanRecord[curProp] = {};
    }
    // console.log(rowSpanRecord, 'sdf', curProp, !!sameKey)
    // 当前行数据,当前列的值
    const recordCurPropValue = record[curProp];
    // 判断是否是重复渲染(antd刷新或者第一次渲染会触发两次onCell回调)
    if (
      rowIndex === 0 &&
      rowSpanRecord[curProp][recordCurPropValue]?.rowIndexs?.length > 0
    ) {
      for (const key of Object.keys(rowSpanRecord)) {
        rowSpanRecord[key] = {};
      }
    }
    // 记录中,当前要合并的列的prop,下面值的集合
    const rowSpanRecordCurPorp = rowSpanRecord[curProp];

    /**
       * 前面行已经进行了判断并合并,前面的行合并了后面的行之后,(该判断是后续行的判断)
      后面的行不能再显示了否则table会变形,所以这里需要将后续的列中返回 {rowSpan:0}
       */
    const curPropIndexs = rowSpanRecordCurPorp[recordCurPropValue]?.rowIndexs;
    if (curPropIndexs?.includes(rowIndex)) {
      return { rowSpan: 0 };
    }
    let rowSpan = 0;
    for (let i = rowIndex; i < data.length; i++) {
      // 有 sameKey,需要判断前置条件列中的值必须相同
      if (sameKey) {
        if (
          record[sameKey] === data[i][sameKey] &&
          recordCurPropValue === data[i][curProp]
        ) {
          rowSpan += 1;
          // 将列中行的值记录
          if (rowSpanRecordCurPorp[recordCurPropValue]) {
            rowSpanRecordCurPorp[recordCurPropValue].rowIndexs.push(i);
          } else {
            rowSpanRecordCurPorp[recordCurPropValue] = {
              rowIndexs: [i],
            };
          }
        } else {
          break;
        }
      } else {
        // 没有前置列,只需要判断当前列相邻列是否相同
        if (recordCurPropValue === data[i][curProp]) {
          rowSpan += 1;
          // 将列中行的值记录
          if (rowSpanRecordCurPorp[recordCurPropValue]) {
            rowSpanRecordCurPorp[recordCurPropValue].rowIndexs.push(i);
          } else {
            rowSpanRecordCurPorp[recordCurPropValue] = {
              rowIndexs: [i],
            };
          }
        } else {
          break;
        }
      }
    }
    return { rowSpan };
  };
};

调用方式:

// 合并行函数,在组件外面调用就行
const mergeTableRow = mergeRows()

// 组件内部的columns选项对象
{
  title: "Name",
  dataIndex: "name",
  onCell: (record, rowIndex) => {
    // 没有前置条件所以会合并当前列相同项
    return mergeTableRow({ record, rowIndex, data, curProp: "name" });
  }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值