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" });
}
}