本文结合react+antd Pro组件库,提供了一种自定义行分组的表格表单的实现方案
先看效果
- 实现一个自定义表格行标题的表格,支持行分组合并
- 基于position:sticky设置吸附和滚动定位,
- 支持自定义单元格渲染,意味着你可以嵌入ProForm,自定义表格表单组件
- 基于proForm,表单的常见操作都是支持的,表单校验,赋值,取值,单元格表单自定义等等。
表头数据
js
/** 列配置 */
let colList = [
{
title: 'A',
key: 'A',
},
{
title: 'B',
key: 'B',
},
{
title: 'C',
key: 'C',
},
{
title: 'D',
key: 'D',
},
{
title: 'E',
key: 'E',
},
{
title: 'F',
key: 'F',
},
{
title: 'G',
key: 'G',
},
];
行配置数据,代码逻辑支持无限嵌套
js
/** 行配置 */
let rowList = [
{
title: '中国',
key: '中国',
children: [
{
title: '北方',
key: '北方',
},
{
title: '南方',
key: '南方',
// children: [
// {
// title: 'A-2-1',
// key: 'A-2-1',
// children: [
// {
// title: 'uu-1',
// key: 'uu-1',
// },
// {
// title: 'dd-2',
// key: 'dd-2',
// },
// ],
// },
// {
// title: 'A-2-2',
// key: 'A-2-2',
// },
// ],
},
],
},
{
title: '俄罗斯',
key: '俄罗斯',
children: [
{
title: '俄罗斯-1',
key: '俄罗斯-1',
},
{
title: '俄罗斯-2',
key: '俄罗斯-2',
},
],
},
{
title: '北美',
key: '北美',
},
{
title: '汇总',
key: '汇总',
},
];
样式文件
css
.bcTableRenderClass {
overflow: auto;
width: 60vw;
height: 60vh; /* 固定高度 */
border: 1px solid rgba(5, 5, 5, 8%);
border-bottom: 0;
border-right: 0;
table {
border-collapse: separate;
table-layout: fixed;
width: 100%; /* 固定寬度 */
}
td,
th {
border-right: 1px solid rgba(5, 5, 5, 8%);
border-bottom: 1px solid rgba(5, 5, 5, 8%);
box-sizing: border-box;
/* 单元格宽高 */
height: 30px;
width: 120px;
}
th {
background-color: gray;
}
td {
text-align: center;
}
/* 控制表头固定的核心代码 */
thead tr th {
position: sticky;
top: 0; /* 第一列最上 */
z-index: 9;
}
}
核心组件tsx代码
js
/* eslint-disable react/no-unknown-property */
import './index.less';
type typeRow = {
isHit?: number; // 在行中出现的次数,用于行合并的标识
title: string;
key: string;
children?: typeRow[];
parent?: typeRow;
rowSpan?: number;
};
type typeCol = {
title: string;
key: string;
};
// /** 列配置 */
// let colList = [
// {
// title: 'RM',
// key: 'RM',
// },
// {
// title: 'DM',
// key: 'DM',
// },
// {
// title: 'MICS',
// key: 'MICS',
// },
// {
// title: 'KA',
// key: 'KA',
// },
// {
// title: 'Test',
// key: 'Test',
// },
// {
// title: 'Test1',
// key: 'Test1',
// },
// {
// title: 'Test2',
// key: 'Test2',
// },
// ];
// /** 行配置 */
// let rowList = [
// {
// title: 'A',
// key: 'A',
// children: [
// {
// title: 'A-1',
// key: 'A-1',
// },
// {
// title: 'A-2',
// key: 'A-2',
// // children: [
// // {
// // title: 'A-2-1',
// // key: 'A-2-1',
// // children: [
// // {
// // title: 'uu-1',
// // key: 'uu-1',
// // },
// // {
// // title: 'dd-2',
// // key: 'dd-2',
// // },
// // ],
// // },
// // {
// // title: 'A-2-2',
// // key: 'A-2-2',
// // },
// // ],
// },
// ],
// },
// {
// title: 'B过程类',
// key: 'B过程类',
// children: [
// {
// title: '111B过程类',
// key: '111B过程类',
// },
// {
// title: '222B过程类',
// key: '222B过程类',
// },
// ],
// },
// {
// title: 'C过程类',
// key: 'C过程类',
// },
// {
// title: '汇总',
// key: '汇总',
// },
// ];
/**
* 获取渲染的行配置数据
* @param list
* @returns
*/
const getList = (list: typeRow[]) => {
// 行配置的层级深度,children多一层,就加一,绘制前面的几个自定义表头
let rowDeepLen = 0;
// 根据children查找,统计层级的递归函数
const cacadeList = (listOpts: typeRow[], deepLen = 1) => {
let rowDeepLen = deepLen;
listOpts.forEach((element) => {
if (element.children?.length) {
rowDeepLen = Math.max(
cacadeList(element.children, deepLen + 1),
rowDeepLen,
);
}
});
return rowDeepLen;
};
rowDeepLen = cacadeList(list);
// 计算每一行需要向下合并的行数
const updateSpan = (list: typeRow[]) => {
// 行数合并计算逻辑
const getRowSpan = (item: typeRow) => {
let temp = 0;
// 没有子节点,直接就是1
if (!item.children?.length) {
return 1;
} else {
// 有子节点,更新每一个子节点的合并行数,同时当前的合并行数要加上子行的全部合并行数,因为要展示嵌套关系
item.children?.forEach((child) => {
child.rowSpan = getRowSpan(child);
temp += getRowSpan(child);
});
}
return temp;
};
const newrowList = list.map((item) => {
return {
...item,
rowSpan: getRowSpan(item),
};
});
return newrowList;
};
// 将嵌套的行配置数据进行排平
const cacadeListToFlatList = (list: typeRow[], parent: typeRow | null) => {
let res: typeRow[] = [];
for (let i = 0; i < list.length; i++) {
let cur = list[i];
// @ts-ignore
cur.parent = parent;
if (!cur.children?.length) {
res.push(cur);
} else {
res = res.concat(cacadeListToFlatList(cur.children, cur));
}
}
return res;
};
let addSpanList = updateSpan(list);
let resList = cacadeListToFlatList(addSpanList, null);
return {
newRowList: resList,
rowDeepLen,
};
};
const getStyle = (index = 0, rowWidth = 120) => {
const styleCfg = {
position: 'sticky',
left: rowWidth * index + 'px',
background: 'gray',
zIndex: 1,
} as const;
return styleCfg;
};
/**
* 根据嵌套关系获取一行需要选人的td数据列表 从子节点:a-1-1, 不断查找parent或得【a, a-1, a-1-1】这个渲染数组
* @param cellItem
* @returns
*/
const renderCell = (cellItem: typeRow) => {
let tempCellItem = {
...cellItem,
} as typeRow | undefined;
let renderList = [];
while (tempCellItem) {
if (!tempCellItem.isHit) {
tempCellItem.isHit = 1; // 计算重复引用次数,合并行的时候,会有多行相同的,第一行是1,后续自增,渲染的时候,不是1的td表格元素直接渲染成null,以便实现行列合并
} else {
tempCellItem.isHit = tempCellItem.isHit + 1;
}
renderList.unshift({
...tempCellItem,
});
tempCellItem = tempCellItem.parent;
}
return renderList;
};
type typeProps = {
colList: typeRow[];
rowList: typeCol[];
cellRender: (params: {
rowItem: typeRow;
colItem: typeCol;
rowIndex: number;
colIndex: number;
}) => React.ReactNode;
};
const TableRender = (props: typeProps) => {
const { colList = [], rowList = [], cellRender } = props;
let { newRowList, rowDeepLen } = getList(rowList);
return (
<div className="bcTableRenderClass">
{/* @ts-ignore */}
<table cellspacing="0" border="0" cellpadding="0">
<thead>
<tr
style={{
background: 'lightblue',
}}
>
{/* 这部分是处理展示自定义合并表格行需要的列渲染 */}
{Array(rowDeepLen)
.fill(0)
.map((obj, oIndex) => {
return (
<th
key={oIndex}
style={{
...getStyle(oIndex), // position:sticky场景下,第一列,第二列的left吸附值不一样
top: 0,
zIndex: 999,
}}
>
自定义表头{oIndex + 1}
</th>
);
})}
{/* 这部分是传入的动态数据的列渲染 */}
{colList.map((colItem) => {
return <th key={colItem.key}>{colItem.title}</th>;
})}
</tr>
</thead>
<tbody>
{newRowList.map((rowItem, rowIndex) => {
let renderList = renderCell(rowItem);
return (
<tr key={rowItem.key}>
{/* 自定义合并行的渲染区域 */}
{Array(rowDeepLen)
.fill(0)
.map((_, oIndex) => {
const tdItem = renderList[oIndex];
if (!tdItem) {
return (
<td
style={{
...getStyle(oIndex),
background: '#eee',
}}
key={oIndex}
>
{/* {rowIndex}-{oIndex} */}
{/* 无组件 */}
</td>
);
}
if (tdItem.isHit !== 1) {
return null;
}
return (
<td
key={oIndex}
rowSpan={tdItem.rowSpan}
style={{
...getStyle(oIndex),
background: '#eee',
}}
>
{tdItem.title}
</td>
);
})}
{/* 表格单元格渲染 */}
{colList.map((colItem, colIndex) => {
return (
<td key={colItem.key}>
{cellRender?.({
rowItem,
colItem,
rowIndex,
colIndex: colIndex,
})}
</td>
);
})}
</tr>
);
})}
</tbody>
</table>
</div>
);
};
export default TableRender;
组件测试代码
js
import {
ProForm,
ProFormInstance,
ProFormText,
} from '@ant-design/pro-components';
import { Button } from 'antd';
import { useRef } from 'react';
import './index.less';
import TableRender from './table-render';
/** 列配置 */
let colList = [
{
title: 'A',
key: 'A',
},
{
title: 'B',
key: 'B',
},
{
title: 'C',
key: 'C',
},
{
title: 'D',
key: 'D',
},
{
title: 'E',
key: 'E',
},
{
title: 'F',
key: 'F',
},
{
title: 'G',
key: 'G',
},
];
/** 行配置 */
let rowList = [
{
title: '中国',
key: '中国',
children: [
{
title: '北方',
key: '北方',
},
{
title: '南方',
key: '南方',
},
],
},
{
title: '俄罗斯',
key: '俄罗斯',
children: [
{
title: '俄罗斯-1',
key: '俄罗斯-1',
},
{
title: '俄罗斯-2',
key: '俄罗斯-2',
},
],
},
{
title: '北美',
key: '北美',
},
{
title: '汇总',
key: '汇总',
},
];
const Welcome: React.FC = () => {
const formRef = useRef<ProFormInstance>();
const getData = () => {
let res = formRef.current?.getFieldsValue();
console.log(res);
};
const setData = () => {
formRef.current?.setFieldsValue({
'A-1': {
RM: '112',
},
});
};
const cellRender = ({ colIndex, rowItem, colItem, rowIndex }) => {
return (
<div style={{ padding: '8px' }}>
<ProFormText
required
label={
<div style={{ fontSize: '10px' }}>
数量({rowItem.title}
{colItem.title})
</div>
}
layout="horizontal"
rules={[
{
required: true,
},
]}
name={[rowItem.key, colItem.key, 'name']}
/>
</div>
);
};
return (
<div>
<ProForm formRef={formRef}>
<div style={{ padding: '8px' }}>
<TableRender
colList={colList}
rowList={rowList}
cellRender={cellRender}
/>
</div>
</ProForm>
<div style={{ marginTop: 20, display: 'flex', gap: '4px' }}>
<Button type="primary" onClick={() => getData()}>
获取数据
</Button>
<Button
type="primary"
onClick={() => {
formRef.current?.resetFields();
}}
>
清空数据
</Button>
<Button type="primary" onClick={() => setData()}>
设置数据
</Button>
</div>
</div>
);
};
export default Welcome;
效果如下:
最后
感谢你看到这里,有帮助记得收藏点赞加关注,持续更新干货!