项目里遇到个需求,涉及到比较复杂的单元格合并 、嵌套表头、分组合并行、合并列等,并且数据列还是动态的,效果图如下:
可以分组设置【显示列】例如:当前组为【合同约定】,显示列为【合同节点】和【节点金额】
我们按【数据源】进行分组后,把第一组编号为【0001】的单元行合并 ,然后再插入一行【小计】,根据显示的【列数】决定合并几个【单元格】,比如我们的需求可以设置为当【合同节点】与【金额】全部显示时,显示【小计】的信息,再进行单元格合并
我们的数据源如下:
动态列数据:
const mockRowData = [
// {
// name: '合同约定',
// key: 'agree',
// handle: 'lt',
// handleValue: 2312313,
// color: '#FF811A',
// enable: true,
// checked: true,
// // disabled: true,
// checkable: false,
// hidden: true,
// children: [
{
name: '合同节点',
key: 'contractNodes',
handle: 'lt',
handleValue: 2312313,
color: '#FF811A',
enable: true,
checked: true,
type: 1,
},
{
name: '金额',
key: 'Money',
handle: 'lt',
handleValue: 2312313,
color: '#FF811A',
enable: true,
checked: true,
type: 1,
},
{
name: '节点比例',
key: 'NodeRatio',
handle: 'lt',
handleValue: 2312313,
color: '#FF811A',
enable: false,
checked: true,
type: 1,
},
{
name: '付款条件',
key: 'paymentTerms',
handle: 'lt',
handleValue: 2312313,
color: '#FF811A',
enable: true,
checked: true,
type: 1,
},
// ],
// },
// {
// name: '确认收入',
// key: 'confirmIncome',
// handle: 'lt',
// handleValue: 2312313,
// color: '#FF811A',
// enable: false,
// checked: true,
// // disabled: true,
// checkable: false,
// hidden: true,
// children: [
{
name: '成果提交日期',
key: 'submissionDate',
handle: 'lt',
handleValue: 2312313,
color: '#FF811A',
enable: false,
checked: true,
type: 2,
},
{
name: '证明文件日期',
key: 'fileDate',
handle: 'lt',
handleValue: 2312313,
color: '#FF811A',
enable: true,
checked: true,
type: 2,
},
{
name: '确认收入',
key: 'Income',
handle: 'lt',
handleValue: 2312313,
color: '#FF811A',
enable: false,
checked: true,
type: 2,
},
{
name: '确认比例',
key: 'confirmationRatio',
handle: 'lt',
handleValue: 2312313,
color: '#FF811A',
enable: false,
checked: true,
type: 2,
},
// ],
// },
// {
// name: '收款',
// key: 'receivePayment',
// handle: 'lt',
// handleValue: 2312313,
// color: '#FF811A',
// enable: false,
// checked: true,
// // disabled: true,
// checkable: false,
// hidden: true,
// children: [
{
name: '到账日期',
key: 'DateofReceipt',
handle: 'lt',
handleValue: 2312313,
color: '#FF811A',
enable: false,
checked: true,
type: 3,
},
{
name: '金额',
key: 'Money1',
handle: 'lt',
handleValue: 2312313,
color: '#FF811A',
enable: true,
checked: true,
type: 3,
},
{
name: '确认比例',
key: 'confirmationRatio1',
handle: 'lt',
handleValue: 2312313,
color: '#FF811A',
enable: false,
checked: true,
type: 3,
},
// ],
// },
{
name: '应收款',
key: 'receivables',
handle: 'lt',
handleValue: 2312313,
color: '#FF811A',
enable: false,
checked: true,
children: [],
type: 4,
},
];
数据源:
const mockTableData = {
group1: [
{
group: 'group1',
id: 1,
name: '第一建筑设计院',
order: '0001',
type: '外部承包',
isConsortium: '是',
nameSquare:
'XXXXX有限公司公司名称公司名称公司名称公司名称公司名称公司名称公司名称公司名称公司名称公司名称',
cstatus: '执行中',
money: '60,0000,0000',
value1: 12345,
contractNodes: 12345,
Money: 12345,
NodeRatio: 12345,
paymentTerms: 12345,
confirmIncome: 12345,
submissionDate: 12345,
fileDate: 12345,
Income: 12345,
confirmationRatio: 12345,
Money1: 879,
DateofReceipt: '2024-10-25',
confirmationRatio1: '20%',
receivables: '',
agree: '',
receivePayment: '200,0000,0000',
},
{
group: 'group1',
id: 2,
name: '第二建筑设计院',
order: '0001',
type: '外部承包',
isConsortium: '是',
nameSquare:
'XXXXX有限公司公司名称公司名称公司名称公司名称公司名称公司名称公司名称公司名称公司名称公司名称',
cstatus: '执行中',
money: '60,0000,0000',
value1: 1234124,
contractNodes: 1234124,
Money: 1234124,
NodeRatio: 1234124,
paymentTerms: 1234124,
confirmIncome: 1234124,
submissionDate: 1234124,
fileDate: 1234124,
Income: 1234124,
confirmationRatio: 1234124,
Money1: 879,
DateofReceipt: '2024-10-25',
confirmationRatio1: '20%',
receivables: '',
agree: '',
receivePayment: '200,0000,0000',
},
{
group: 'group1',
id: 3,
name: '第三建筑设计院',
order: '0001',
type: '外部承包',
isConsortium: '是',
nameSquare:
'XXXXX有限公司公司名称公司名称公司名称公司名称公司名称公司名称公司名称公司名称公司名称公司名称',
cstatus: '执行中',
money: '60,0000,0000',
value1: 54321,
contractNodes: 54321,
Money: 54321,
NodeRatio: 54321,
paymentTerms: 54321,
confirmIncome: 54321,
submissionDate: 54321,
fileDate: 54321,
Income: 54321,
confirmationRatio: 54321,
Money1: 879,
DateofReceipt: '2024-10-25',
confirmationRatio1: '20%',
receivables: '',
agree: '',
receivePayment: '200,0000,0000',
},
],
group2: [
{
group: 'group2',
id: 4,
name: '第一建筑设计院',
order: '0002',
type: '外部承包',
isConsortium: '是',
nameSquare:
'XXXXX有限公司公司名称公司名称公司名称公司名称公司名称公司名称公司名称公司名称公司名称公司名称',
cstatus: '执行中',
money: '60,0000,0000',
value1: 12345,
contractNodes: 12345,
Money: 12345,
NodeRatio: 12345,
paymentTerms: 12345,
confirmIncome: 12345,
submissionDate: 12345,
fileDate: 12345,
Income: 12345,
confirmationRatio: 12345,
Money1: 879,
DateofReceipt: '2024-10-25',
confirmationRatio1: '20%',
receivables: '',
agree: '',
receivePayment: '200,0000,0000',
},
{
group: 'group2',
id: 5,
name: '第二建筑设计院',
order: '0002',
type: '外部承包',
isConsortium: '是',
nameSquare:
'XXXXX有限公司公司名称公司名称公司名称公司名称公司名称公司名称公司名称公司名称公司名称公司名称',
cstatus: '执行中',
money: '60,0000,0000',
value1: 1234124,
contractNodes: 1234124,
Money: 1234124,
NodeRatio: 1234124,
paymentTerms: 1234124,
confirmIncome: 1234124,
submissionDate: 1234124,
fileDate: 1234124,
Income: 1234124,
confirmationRatio: 1234124,
Money1: 879,
DateofReceipt: '2024-10-25',
confirmationRatio1: '20%',
receivables: '',
agree: '',
receivePayment: '200,0000,0000',
},
{
group: 'group2',
id: 6,
name: '第三建筑设计院',
order: '0002',
type: '外部承包',
isConsortium: '是',
nameSquare:
'XXXXX有限公司公司名称公司名称公司名称公司名称公司名称公司名称公司名称公司名称公司名称公司名称',
cstatus: '执行中',
money: '60,0000,0000',
value1: 54321,
contractNodes: 54321,
Money: 54321,
NodeRatio: 54321,
paymentTerms: 54321,
confirmIncome: 54321,
submissionDate: 54321,
fileDate: 54321,
Income: 54321,
confirmationRatio: 54321,
Money1: 879,
DateofReceipt: '2024-10-25',
confirmationRatio1: '20%',
receivables: '',
agree: '',
receivePayment: '200,0000,0000',
},
],
};
那接下来咱们就先看如何显示【动态列】吧~
1.显示动态列
const columns = useCallback(() => {
if (rowData && rowData.length) {
console.log(rowData);
const list = [
// 先写入非动态列
{
title: '合同编号',
dataIndex: 'order',
width: 50,
render: (text) => {
return (
<div
className="ellipsis-item"
title={text}
style={{ width: '100px' }}
>
{text}
</div>
);
},
onCell: mergeRow,
},
// 循环动态列,
{
title: '收款',
className: 'type3Column',
children: [
...flattenData(rowData, 0)
.filter((item) => item.checked)
.filter((item) => item.type === 3)
.map((item) => {
return {
title: (
<div className="ellipsis-item" title={item.name}>
{item.name}
</div>
),
dataIndex: item.key,
className: 'type3Column',
width: 100,
render: (text) => {
return (
<div
style={{
background: getColumnColor(
item.enable,
text,
item.handle,
item.handleValue,
item.color,
),
}}
className="value-item"
>
{text}
</div>
);
},
};
}),
],
},
];
console.log('list', list);
return list;
}
}, [rowData]);
2.合并列
思路
我们的列是动态变换的,所以合并的列数也是动态合并的,那我们就需要在每次变化的时候再去判断需要合并几列
1.先将【原始数据源】分组处理,计算每组的【小计】金额 ,然后写入【表格数据源】里
2.循环动态列,将列也分别写入三个数组里
3.判断我们【每组】【动态列】中是否符合我们的需求,是否要显示【小计】信息,设置【要合并的列】为1
4.如果存在,判断本组有无其他列显示,有的话【要合并的列数+1】
处理列数&计算要合并的列数
const handleColumnData = (row) => {
console.log('row=======', row);
let type1list = [];// 分组列1
let type2list = []; // 分组列2
let type3list = []; // 分组列3
let haveAgreeTotal = false; // 分组列1是否有【小计】
let haveIncomeTotal = false; // 分组列1是否有【小计】
let haveReciveTotal = false; // 分组列1是否有【小计】
let AgreeColspan = 0; // 分组列1要合并的列数
let IncomeColspan = 0; // 分组列1要合并的列数
let ReciveColspan = 0; // 分组列1要合并的列数
row.forEach((item) => {
if (item.type === 1) {
type1list.push(item.key);
} else if (item.type === 2) {
type2list.push(item.key);
} else if (item.type === 3) {
type3list.push(item.key);
}
});
// console.log('type1', type1list)
// console.log('type2', type2list)
// console.log('type3', type3list)
if (type1list.length > 0) {
// 如果同时存在【合同节点】和【金额】字段,显示小计
if (
type1list.includes('contractNodes') &&
type1list.includes('Money')
) {
haveAgreeTotal = true;
AgreeColspan = 1;
if (type1list.includes('NodeRatio')) {
AgreeColspan += 1;
}
if (type1list.includes('paymentTerms')) {
AgreeColspan += 1;
}
}
}
if (type2list.length > 0) {
// 如果同时存在【成果提交日期】和【确认收入】字段,显示小计
if (
type2list.includes('submissionDate') &&
type2list.includes('Income')
) {
haveIncomeTotal = true;
IncomeColspan = 1;
if (type2list.includes('fileDate')) {
IncomeColspan += 1;
}
if (type2list.includes('confirmationRatio')) {
IncomeColspan += 1;
}
}
}
if (type3list.length > 0) {
// 如果同时存在【到账日期】和【金额】字段,显示小计
if (
type3list.includes('DateofReceipt') &&
type3list.includes('Money1')
) {
haveReciveTotal = true;
ReciveColspan = 1;
if (type3list.includes('confirmationRatio1')) {
ReciveColspan += 1;
}
}
}
// console.log('haveAgreeTotal, haveIncomeTotal, haveReciveTotal', haveAgreeTotal, haveIncomeTotal, haveReciveTotal)
// console.log('AgreeColspan, IncomeColspan, ReciveColspan', AgreeColspan, IncomeColspan, ReciveColspan)
return {
haveAgreeTotal,
haveIncomeTotal,
haveReciveTotal,
AgreeColspan,
IncomeColspan,
ReciveColspan,
};
};
useEffect(() => {
// setTableData(mockTableData)
let source = formatData(mockTableData, rowData);
setDataSource(source);
}, []);
根据需求写入【小计】行
const formatData = (data, rowData) => {
let obj = handleColumnData(rowData);
let resTableData = [];
const {
haveAgreeTotal,
haveIncomeTotal,
haveReciveTotal,
AgreeColspan,
IncomeColspan,
ReciveColspan,
} = obj;
// 是否有任意一组列存在小计,存在则写入新一行
// 循环列表,把数据分组写入
// 写入小计行,根据列标识写入要合并的列数
// 渲染时判断标识行,写入列数,其余为0
if (haveAgreeTotal || haveIncomeTotal || haveReciveTotal) {
Object.keys(data).forEach((key) => {
let group = data[key];
let nodeSum = 0;
let IncomeSum = 0;
let Money1Sum = 0;
group.forEach((item) => {
resTableData.push(item);
nodeSum += item.NodeRatio;
IncomeSum += item.Income;
Money1Sum += item.Money1;
});
let totalRecord = {
group: key,
id: '',
order: '',
type: '',
isConsortium: '',
nameSquare: '',
cstatus: '',
money: '',
value1: '',
confirmIncome: '',
receivables: '',
agree: '',
receivePayment: '',
};
if (haveAgreeTotal) {
totalRecord.contractNodes = '小计';
totalRecord.Money = nodeSum;
totalRecord.NodeRatio = nodeSum;
totalRecord.paymentTerms = nodeSum;
totalRecord.AgreeColspan = AgreeColspan;
}
if (haveIncomeTotal) {
totalRecord.submissionDate = '小计';
totalRecord.Income = IncomeSum;
totalRecord.fileDate = IncomeSum;
totalRecord.confirmationRatio = IncomeSum;
totalRecord.IncomeColspan = IncomeColspan;
}
if (haveReciveTotal) {
totalRecord.DateofReceipt = '小计';
totalRecord.Money1 = Money1Sum;
totalRecord.confirmationRatio1 = Money1Sum;
totalRecord.ReciveColspan = ReciveColspan;
}
// console.log('recordtotal', totalRecord)
resTableData.push(totalRecord);
});
} else {
Object.keys(data).forEach((key) => {
let group = data[key];
group.forEach((item) => {
resTableData.push(item);
});
});
}
return resTableData
};
渲染表格
此处我们要注意的是!当前合并单元格行数、列数设置好后,我们要把【被合并的行列】都设置为0!!并且【其他的行和列】的合并数也要都为0!不然表格会出现错位问题!
我们把cloumns列设置
{
title: '合同约定',
className: 'type1Column',
children: [
...flattenData(rowData, 0)
.filter((item) => item.checked)
.filter((item) => item.type === 1)
.map((item) => {
return {
title: (
<div className="ellipsis-item" title={item.name}>
{item.name}
</div>
),
dataIndex: item.key,
className: 'type1Column',
width: 100,
render: (text, record, index) => {
if (record.AgreeColspan) { // 判断是否是小计行
if (item.key === 'Money') {
return {
children: handleNode(item, text, 'total_back_pink'),
props: { colSpan: record.AgreeColspan }, // 合并"小计"行从第二列开始的所有列
};
} else if (item.key === 'contractNodes') {
return {
children: handleNode(item, text, 'total_back_pink'),
props: { colSpan: 1 }, // "小计"列不合并
};
} else {
return {
children: handleNode(item, text, 'total_back_pink'),
props: { colSpan: 0 }, // 当前组其他列设置为0,不会出现表格错位情况
};
}
} else {
return {
children: handleNode(item, text),
props: { colSpan: 1 }, // 非"小计"行全部不合并
};
}
},
};
}),
],
},
这样就实现了【分组合并列】、【动态渲染列】啦!
2.合并行
思路
循环【数据源】,从第一行开始判断,如果【当前行】与【下一行】是同一组,那【rowSpan】 + 1,【被合并的行】记得要设置为0哦
const formatData = (data, rowData) => {
// 处理要合并的列
...
// 处理要合并的行
let processedData = [...resTableData];
let prevFinishNum;
let currentIndexForSameProperty;
for (let i = 0; i < processedData.length; i++) {
const currentItem = processedData[i];
if (i === 0 || currentItem.group !== prevFinishNum) {
// 新的组开始,重置计数器,并将上一组最后一项的rowSpan设置好
if (
prevFinishNum !== undefined &&
currentIndexForSameProperty !== undefined
) {
processedData[currentIndexForSameProperty].rowSpan =
i - currentIndexForSameProperty;
}
currentIndexForSameProperty = i;
currentItem.rowSpan = 1;
currentItem.opcaty = '0'; // 标识是不是被合并的行
} else {
// 相同组内,累加rowSpan
processedData[currentIndexForSameProperty].rowSpan += 1;
currentItem.opcaty = '1'; // 标识是被合并的行
}
prevFinishNum = currentItem.group;
}
// 处理数组的最后一项
if (
currentIndexForSameProperty !== undefined &&
currentIndexForSameProperty < processedData.length - 1
) {
processedData[currentIndexForSameProperty].rowSpan =
processedData.length - currentIndexForSameProperty;
}
return processedData;
}
这样就实现了【合并行】啦!
3.嵌套表头
这个在 antd官网 都有,就是给columns设置children就行了,咱们在上面【渲染动态列】的代码里也有提到 ~