Skip to content

更复杂的分组表头 #1115

@Fansaly

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

Fansaly

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.tsxfillRowCells 之前转换 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;
afc163

afc163 commented on May 8, 2024

@afc163
Member

来个 PR?

Fansaly

Fansaly commented on May 8, 2024

@Fansaly
Author

@afc163 #1118 PR 已提交

Fansaly

Fansaly commented on May 13, 2024

@Fansaly
Author

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

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

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

  • table 数据为空时 .rc-table-placeholder .rc-table-cellcolspan 值不正确

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

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

Fansaly

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

        Participants

        @afc163@Fansaly

        Issue actions

          更复杂的分组表头 · Issue #1115 · react-component/table