【前端-组件】定义行分组的表格表单实现-bysking

本文结合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;

效果如下:

最后

感谢你看到这里,有帮助记得收藏点赞加关注,持续更新干货!

相关推荐
Kika写代码14 分钟前
【微信小程序】2|轮播图 | 我的咖啡店-综合实训
前端·微信小程序·小程序
red润21 分钟前
使用 HTML5 Canvas 实现动态蜈蚣动画
前端·html·html5
sg_knight28 分钟前
VSCode如何修改默认扩展路径和用户文件夹目录到D盘
前端·ide·vscode·编辑器·web
一个处女座的程序猿O(∩_∩)O38 分钟前
完成第一个 Vue3.2 项目后,这是我的技术总结
前端·vue.js
mubeibeinv38 分钟前
项目搭建+图片(添加+图片)
java·服务器·前端
逆旅行天涯1 小时前
【Threejs】从零开始(六)--GUI调试开发3D效果
前端·javascript·3d
m0_748255261 小时前
easyExcel导出大数据量EXCEL文件,前端实现进度条或者遮罩层
前端·excel
web147862107232 小时前
C# .Net Web 路由相关配置
前端·c#·.net
m0_748247802 小时前
Flutter Intl包使用指南:实现国际化和本地化
前端·javascript·flutter
飞的肖2 小时前
前端使用 Element Plus架构vue3.0实现图片拖拉拽,后等比压缩,上传到Spring Boot后端
前端·spring boot·架构