table icon indicating copy to clipboard operation
table copied to clipboard

更复杂的分组表头

Open Fansaly opened this issue 1 year ago • 5 comments

更复杂分组表头的渲染

期望渲染结果: 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 avatar May 07 '24 02:05 Fansaly

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

已有单元测试测试通过

新增 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;

Fansaly avatar May 08 '24 08:05 Fansaly

来个 PR?

afc163 avatar May 08 '24 10:05 afc163

@afc163 #1118 PR 已提交

Fansaly avatar May 08 '24 13:05 Fansaly

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

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

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

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

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

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

Fansaly avatar May 13 '24 02:05 Fansaly

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

(匿了

Fansaly avatar May 14 '24 04:05 Fansaly