Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

更复杂的分组表头 #1115

Open
Fansaly opened this issue May 7, 2024 · 5 comments
Open

更复杂的分组表头 #1115

Fansaly opened this issue May 7, 2024 · 5 comments

Comments

@Fansaly
Copy link

Fansaly commented May 7, 2024

更复杂分组表头的渲染

期望渲染结果:
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

@Fansaly
Copy link
Author

Fansaly commented May 8, 2024

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

已有单元测试测试通过

新增 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
Copy link
Member

afc163 commented May 8, 2024

来个 PR?

@Fansaly
Copy link
Author

Fansaly commented May 8, 2024

@afc163 #1118 PR 已提交

@Fansaly
Copy link
Author

Fansaly commented May 13, 2024

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

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

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

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

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

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

@Fansaly
Copy link
Author

Fansaly commented May 14, 2024

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

(匿了

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants