import type { BaseTableProps } from '../../base-table' import type { ArtColumn, SpanRect } from '../../interfaces' import { getTreeDepth, isLeafNode } from '../../utils' import { always } from '../../utils/others' import { ROW_KEY } from './constants' import type { CrossTableProps } from './cross-table' import type { CrossTableLeftMetaColumn, LeftCrossTreeNode, TopCrossTreeNode } from './interfaces' import { CrossTableDataColumn, CrossTableDataParentColumn, CrossTableLeftColumn, CrossTableRenderColumn, } from './internals' interface CrossTableRenderRow { [ROW_KEY]: string /** 行上所有左侧单元格 的合并信息 */ rects: SpanRect[] /** 行上所有左侧单元格 上的树节点 */ nodes: LeftCrossTreeNode[] } export interface BuildCrossTableOptions { leftTree: LeftCrossTreeNode[] topTree: TopCrossTreeNode[] leftTotalNode?: LeftCrossTreeNode topTotalNode?: TopCrossTreeNode leftMetaColumns?: CrossTableLeftMetaColumn[] getValue?: CrossTableProps['getValue'] render?: CrossTableProps['render'] getCellProps?: CrossTableProps['getCellProps'] } export default function buildCrossTable( options: BuildCrossTableOptions & { columnOffset?: number rowOffset?: number }, ): Pick { const { leftTotalNode, topTotalNode } = options const columnOffset = options.columnOffset ?? 0 const rowOffset = options.rowOffset ?? 0 const hasOffset = columnOffset !== 0 || rowOffset !== 0 // 有的时候 leftTree/topTree 是通过 node.children 传入的 // 此时 leftTree/topTree 等于 null 和等于空数组是等价的 // 故在这里兼容 leftTree/topTree 为空的情况 const leftTree = options.leftTree ?? [] const topTree = options.topTree ?? [] const getValue = options.getValue ?? always(null) const leftMetaColumns = options.leftMetaColumns ?? [] const leftHeaderWidth = Math.max(leftMetaColumns.length, getTreeDepth(leftTree) + 1) return { columns: getColumns(), dataSource: getDataSource(), } /** 获取表格的列配置 */ function getColumns(): CrossTableRenderColumn[] { return [...getLeftPartColumns(), ...getDataPartColumns()] function getLeftPartColumns(): CrossTableRenderColumn[] { const leftPartColumns: CrossTableLeftColumn[] = [] for (let index = 0; index < leftHeaderWidth; index++) { const metaCol = leftMetaColumns[index] ?? ({} as CrossTableLeftMetaColumn) const { getCellProps, render, ...staticMetaColConfig } = metaCol leftPartColumns.push({ columnType: 'left', lock: true, ...staticMetaColConfig, getCellProps: leftHeaderGetCellPropsFactory(metaCol, index), getSpanRect: leftHeaderGetSpanRectFactory(metaCol, index), getValue: leftHeaderGetValueFactory(metaCol, index), render: leftHeaderRenderFactory(metaCol, index), }) } return leftPartColumns function leftHeaderGetCellPropsFactory( metaCol: CrossTableLeftMetaColumn, colIndex: number, ): ArtColumn['getCellProps'] { return (_value: any, row: CrossTableRenderRow, rowIndex: number) => { const node = row.nodes[colIndex] return metaCol.getCellProps?.(node, colIndex) } } function leftHeaderGetSpanRectFactory( metaCol: CrossTableLeftMetaColumn, colIndex: number, ): ArtColumn['getSpanRect'] { return (_value: any, row: CrossTableRenderRow) => { const rect = row.rects[colIndex] if (hasOffset) { return { left: rect.left + columnOffset, right: rect.right + columnOffset, top: rect.top + rowOffset, bottom: rect.bottom + rowOffset, } } return rect } } function leftHeaderGetValueFactory(metaCol: CrossTableLeftMetaColumn, colIndex: number) { return (row: CrossTableRenderRow, rowIndex: number) => { const node = row.nodes[colIndex] return node.value } } function leftHeaderRenderFactory(metaCol: CrossTableLeftMetaColumn, colIndex: number) { return (v: any, row: CrossTableRenderRow, rowIndex: number) => { const node = row.nodes[colIndex] if (metaCol.render) { return metaCol.render(node, colIndex) } return node.title ?? node.value } } } /** 获取表格数据部分的列配置 */ function getDataPartColumns() { type Ctx = { valuePath: string[]; depth: number } if (topTree.length > 0) { return dfs(topTree, { valuePath: [], depth: 0 }) } else if (topTotalNode) { return dfs([topTotalNode], { valuePath: [], depth: 0 }) } else { return [] } function dfs(nodes: TopCrossTreeNode[], ctx: Ctx): (CrossTableDataColumn | CrossTableDataParentColumn)[] { const result: (CrossTableDataColumn | CrossTableDataParentColumn)[] = [] for (const node of nodes) { ctx.valuePath.push(node.value) if (isLeafNode(node)) { // 叶子节点 result.push(getDataColumn(node, ctx.depth)) } else { const { key, value, children, ...others } = node // 强制展开的节点 result.push({ columnType: 'data-parent', ...others, name: value, children: dfs(children, { valuePath: ctx.valuePath, depth: ctx.depth + 1 }), }) } ctx.valuePath.pop() } return result } } function getDataColumn(topNode: TopCrossTreeNode, topDepth: number): CrossTableDataColumn { const columnGetValue = (row: CrossTableRenderRow) => { const leftDepth = row.nodes.length - 1 const leftNode = row.nodes[leftDepth] return getValue(leftNode, topNode, leftDepth, topDepth) } const { key, value, children, ...others } = topNode return { columnType: 'data', ...others, getValue: columnGetValue, name: value, children: null, render(value: any, row: CrossTableRenderRow) { if (options.render) { const leftDepth = row.nodes.length - 1 const leftNode = row.nodes[leftDepth] return options.render(value, leftNode, topNode, leftDepth, topDepth) } return value }, getCellProps(value, row: CrossTableRenderRow) { if (options.getCellProps) { const leftDepth = row.nodes.length - 1 const leftNode = row.nodes[leftDepth] return options.getCellProps(value, leftNode, topNode, leftDepth, topDepth) } }, } } } function getDataSource(): CrossTableRenderRow[] { type DfsCtx = { depth: number rects: SpanRect[] nodes: LeftCrossTreeNode[] rowIndex: number } const flatRows: CrossTableRenderRow[] = [] const ctx: DfsCtx = { depth: 0, nodes: [], rects: [], rowIndex: 0 } if (leftTree.length > 0) { dfs(leftTree, ctx) } else if (leftTotalNode) { dfs([leftTotalNode], ctx) } // else 表格没有行,展示空表格 return flatRows function dfs(nodes: LeftCrossTreeNode[], ctx: DfsCtx): { count: number } { let count = 0 for (const node of nodes) { if (node.hidden) { // 跳过被隐藏的节点 continue } const rect: SpanRect = { top: ctx.rowIndex + count, bottom: -1, // 会在 dfs 之后算出结果 left: ctx.depth, right: -1, // 会在 dfs 之后算出结果 } const row: CrossTableRenderRow = { [ROW_KEY]: node.key, rects: [...ctx.rects, rect], nodes: [...ctx.nodes, node], } if (isLeafNode(node)) { rect.right = leftHeaderWidth rect.bottom = rect.top + 1 flatRows.push(row) count += 1 } else { ctx.rects.push(rect) ctx.nodes.push(node) const ret = dfs(node.children, { nodes: ctx.nodes, rects: ctx.rects, depth: ctx.depth + 1, rowIndex: ctx.rowIndex + count, }) ctx.rects.pop() ctx.nodes.pop() count += ret.count rect.right = rect.left + 1 rect.bottom = rect.top + ret.count } } return { count } } } }