Skip to content

更复杂的分组表头 #1115

Open
Open

Description

@Fansaly

更复杂分组表头的渲染

期望渲染结果:
0-2

实际渲染结果:
0-1

示例复现的代码
import React from 'react';
import type { TableProps } from 'rc-table';
import Table from 'rc-table';
import '../../assets/index.less';

const columns: TableProps['columns'] = [
  {
    title: '姓名',
    rowSpan: 4,
    dataIndex: 'name',
    key: 'name',
  },
  {
    title: '出勤',
    colSpan: 3,
    rowSpan: 3,
    children: [
      {
        title: '出勤',
        dataIndex: 'attendance',
        key: 'attendance',
      },
      {
        title: '迟到',
        dataIndex: 'late',
        key: 'late',
      },
      {
        title: '请假',
        dataIndex: 'leave',
        key: 'leave',
      },
    ],
  },
  {
    title: '其它',
    colSpan: 4,
    children: [
      {
        title: '年龄',
        dataIndex: 'age',
        key: 'age',
        rowSpan: 3,
      },
      {
        title: '住址',
        colSpan: 3,
        children: [
          {
            title: '街道',
            dataIndex: 'street',
            key: 'street',
            rowSpan: 2,
          },
          {
            title: '小区',
            colSpan: 2,
            children: [
              {
                title: '单元',
                dataIndex: 'building',
                key: 'building',
              },
              {
                title: '门牌',
                dataIndex: 'number',
                key: 'number',
              },
            ],
          },
        ],
      },
    ],
  },
  {
    title: '技能',
    colSpan: 2,
    rowSpan: 2,
    children: [
      {
        title: '前端',
        dataIndex: 'frontend',
        key: 'frontend',
        rowSpan: 2,
      },
      {
        title: '后端',
        dataIndex: 'backend',
        key: 'backend',
        rowSpan: 2,
      },
    ],
  },
  {
    title: '公司',
    colSpan: 2,
    children: [
      {
        title: '地址',
        dataIndex: 'companyAddress',
        key: 'companyAddress',
        rowSpan: 3,
      },
      {
        title: '名称',
        dataIndex: 'companyName',
        key: 'companyName',
        rowSpan: 3,
      },
    ],
  },
  {
    title: '性别',
    dataIndex: 'gender',
    key: 'gender',
    rowSpan: 4,
  },
];

const data = [
  {
    key: '1',
    name: '胡彦斌',
    attendance: 20,
    late: 0,
    leave: 1,
    age: 32,
    street: '拱墅区和睦街道',
    building: 1,
    number: 2033,
    frontend: 'S',
    backend: 'S',
    companyAddress: '西湖区湖底公园',
    companyName: '湖底有限公司',
    gender: '男',
  },
  {
    key: '2',
    name: '胡彦祖',
    attendance: 20,
    late: 0,
    leave: 1,
    age: 42,
    street: '拱墅区和睦街道',
    building: 3,
    number: 2035,
    frontend: 'S',
    backend: 'S',
    companyAddress: '西湖区湖底公园',
    companyName: '湖底有限公司',
    gender: '男',
  },
];

const Demo = () => (
  <div>
    <h2>grouping columns specified colSpan & rowSpan</h2>
    <Table columns={columns} data={data} className="bordered" />
  </div>
);

export default Demo;

不知是否有支持计划,如有可以提交相关 PR

Activity

  1. Fansaly commented on May 8, 2024

    @Fansaly
    Author

    实现复杂分组表头最小侵入方案:

    已有单元测试测试通过

    新增 src/utils/convertUtil.ts
    interface Column {
      [key: string | symbol]: any;
    };
    
    interface Options {
      children: string;
      colSpan: string;
      rowSpan: string;
      hidden: string;
    }
    
    export function convertColumns<Columns extends readonly any[] = Column[]>(
      columns: Columns,
      options: Partial<Options> = {},
    ) {
      if (!Array.isArray(columns) || columns.length === 0) {
        return [] as unknown as Columns;
      }
    
      const defaultOptions = {
        children: 'children',
        colSpan: 'colSpan',
        rowSpan: 'rowSpan',
        hidden: 'hidden',
      };
      const {
        children: childrenProp,
        colSpan: colSpanProp,
        rowSpan: rowSpanProp,
        hidden: hiddenProp,
      } = Object.assign({}, defaultOptions, options);
    
      let specified = false;
      let tree = columns.map((item) => ({ ...item } as Column));
    
      let depthCurr = 0;
      let depthNext = 0;
      const nodePos: {
        index: number;
        total: number;
      } = [{
        index: tree.length,
        total: tree.length,
      }];
      const rowSpans: number[] = [];
      const columnsMap = new Map<number, Column[]>();
      const treeMap = new Map<Column, Column[]>();
      const branchLastSet = new Set<Column>();
    
      while (tree.length > 0) {
        depthCurr = depthNext;
    
        nodePos.splice(depthCurr + 1);
        rowSpans.splice(depthCurr);
    
        nodePos[depthCurr].index--;
    
        if (nodePos[depthCurr].index <= 0) {
          depthNext = 0;
    
          for (let i = nodePos.length - 1; i >= 0; i--) {
            if (nodePos[i].index > 0) {
              depthNext = i;
              break;
            }
          }
        }
    
        const node = tree.shift();
    
        if (!node || typeof node !== 'object' || node[hiddenProp]) {
          continue;
        }
    
        // const pathKey = nodePos.reduce((acc, { index, total }) => {
        //   return `${acc}-${total - 1 - index}`;
        // }, 'key');
    
        const colSpanSpecified = node[colSpanProp];
        const rowSpanSpecified = node[rowSpanProp];
        const colSpan = node[colSpanProp] ?? 1;
        const rowSpan = node[rowSpanProp] ?? 1;
        node[colSpanProp] = colSpan;
        node[rowSpanProp] = rowSpan;
    
        if (!specified && (colSpan > 1 || rowSpan > 1)) {
          specified = true;
        }
    
        const parentsRowCount = rowSpans.reduce((acc, num) => acc + num, 0);
        if (!columnsMap.has(parentsRowCount)) {
          columnsMap.set(parentsRowCount, []);
        }
        columnsMap.get(parentsRowCount).push(node);
    
        let leaf = node[childrenProp];
        delete node[childrenProp];
    
        if (Array.isArray(leaf) && leaf.length > 0) {
          depthNext = depthCurr + 1;
          nodePos[depthNext] = { index: leaf.length, total: leaf.length };
          rowSpans[depthCurr] = rowSpan;
    
          leaf = leaf.map((item) => ({ ...item } as Column));
          node.colSpanSpecified = colSpanSpecified;
          if (!treeMap.has(node)) {
            treeMap.set(node, []);
          }
          treeMap.get(node).push(...leaf);
          tree = [...leaf, ...tree];
        } else {
          node.rowSpanSpecified = rowSpanSpecified;
          node.parentsRowCount = parentsRowCount;
          branchLastSet.add(node);
        }
      }
    
      if (!specified) {
        return columns;
      }
    
      // correct colSpan of parent column in default state
      [...treeMap.keys()].reverse().forEach((column) => {
        const { colSpanSpecified } = column;
        delete column.colSpanSpecified;
    
        if (column[hiddenProp] || Number.isInteger(colSpanSpecified)) {
          return;
        }
    
        const children = treeMap.get(column);
        column[colSpanProp] = children.reduce((acc, item) => {
          return item[hiddenProp] ? acc : acc + item[colSpanProp];
        }, 0);
      });
    
      let rowCountMax = 0;
      branchLastSet.forEach((column) => {
        const rowCount = column[rowSpanProp] + column.parentsRowCount;
        if (rowCount > rowCountMax) {
          rowCountMax = rowCount;
        }
      });
    
      // correct rowSpan of column in default state
      branchLastSet.forEach((column) => {
        const { rowSpanSpecified, parentsRowCount } = column;
    
        if (!Number.isInteger(rowSpanSpecified)) {
          column[rowSpanProp] = rowCountMax - parentsRowCount;
        }
    
        delete column.rowSpanSpecified;
        delete column.parentsRowCount;
      });
    
      const keys = [...columnsMap.keys()].sort();
      for (let i = keys.length - 1; i >= 1; i--) {
        const parent = columnsMap.get(keys[i - 1]);
        parent[0][childrenProp] = columnsMap.get(keys[i]);
      }
    
      return columnsMap.get(0) as unknown as Columns;
    }

    在 src/Header/Header.tsx 的 fillRowCells 之前转换 rootColumns :

    diff --git a/src/Header/Header.tsx b/src/Header/Header.tsx
    index f21b817b..07162562 100644
    --- a/src/Header/Header.tsx
    +++ b/src/Header/Header.tsx
    @@ -2,6 +2,7 @@ import { useContext } from '@rc-component/context';
     import * as React from 'react';
     import TableContext, { responseImmutable } from '../context/TableContext';
     import devRenderTimes from '../hooks/useRenderTimes';
    +import { convertColumns } from '../utils/convertUtil';
     import type {
       CellType,
       ColumnGroupType,
    @@ -67,7 +68,7 @@ function parseHeaderRows<RecordType>(
       }
    
       // Generate `rows` cell data
    -  fillRowCells(rootColumns, 0);
    +  fillRowCells(convertColumns<ColumnsType<RecordType>>(rootColumns), 0);
    
       // Handle `rowSpan`
       const rowCount = rows.length;
  2. afc163 commented on May 8, 2024

    @afc163
    Member

    来个 PR?

  3. Fansaly commented on May 8, 2024

    @Fansaly
    Author

    @afc163 #1118 PR 已提交

  4. Fansaly commented on May 13, 2024

    @Fansaly
    Author

    新增复杂分组表头后会造成以下影响

    • 复杂分组表头下的 colStart colEnd 计算不正确

      在显式指定的 colSpan 或 rowSpan 大于或者等于 2 时

    • table 数据为空时 .rc-table-placeholder .rc-table-cell 的 colspan 值不正确

      在 flattenColumns.length 与 实际表头列数 不一致时

    改动的文件可能较多,继续在 #1118 追加完善,还是等待 merge 后再提交新的 PR 呢

  5. Fansaly commented on May 14, 2024

    @Fansaly
    Author
    • 复杂分组表头
    • 复杂分组表头下 colStart 与 colEnd 不正确的问题
    • 复杂分组表头下,且数据为空时 body colspan 不正确的问题
    • 测试

    (匿了

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions