UNPKG

shinsu-senju

Version:
206 lines (179 loc) 4.78 kB
import { customSort } from './sort.ts'; import type { Tree, TreeNode } from './tree-mapper.ts'; import { getBy } from './utils.ts'; import type { Getter, UnknownObject } from './utils.ts'; // Types // ----- /** * 字段值类型,表示从数据记录中提取的值 * 可以是任何类型,但通常是字符串、数字或布尔值 */ export type FieldValue = unknown; /** * 分组节点接口,表示树中的一个分组 */ export interface GroupNode extends UnknownObject { /** 节点元数据 */ readonly $group: GroupProcessConfig; readonly label: unknown; readonly value: unknown; /** 子节点列表 */ readonly children: Groupeds; } /** * 树形节点类型,可以是数据记录或分组节点 */ export type Grouped = TreeNode | GroupNode; /** * 树形结构数据类型 */ export type Groupeds = Grouped[]; /** * 分组配置接口,定义如何对数据进行分组 */ export interface GroupConfig extends Record<string, unknown> { /** 分组依据的字段名 */ readonly groupBy?: Getter; /** 分组标签的字段名,默认使用 groupBy 的值 */ readonly labelBy?: Getter; readonly extraBy?: Getter; /** 用于排序的字段名 */ readonly sortBy?: Getter; /** 是否跳过单个子项的分组 */ readonly skipSingle?: boolean; } /** * 分组配置类型,可以是单个配置或配置数组 */ export type Groups = GroupConfig | readonly GroupConfig[]; /** * 分组处理配置接口 */ interface GroupProcessConfig { /** 分组依据的字段名 */ readonly groupBy: Getter; /** 分组标签的字段名 */ readonly labelBy: Getter; readonly extraBy: Getter; /** 用于排序的字段名 */ readonly sortBy: Getter; /** 是否跳过单个子项的分组 */ readonly skipSingle: boolean; } // Core Logic // --------- /** * 将数据项添加到分组映射中 * @param groups - 分组映射 * @param key - 分组键 * @param item - 数据项 */ function addToGroup( groups: Map<FieldValue, Tree>, key: FieldValue, item: TreeNode, ): void { if (!groups.has(key)) { groups.set(key, []); } const arr = groups.get(key); if (arr) { arr.push(item); } } /** * 按指定字段对数据进行分组 * @param data - 数据记录数组 * @param groupBy - 分组字段名 * @returns 分组后的 Map 对象和未分组的数据项 */ function groupByField( data: Tree, groupBy: Getter, ): { groups: Map<FieldValue, Tree>; ungrouped: Tree } { const groups = new Map<FieldValue, Tree>(); const ungrouped: Tree = []; for (const item of data) { const key = getBy(item.$original, groupBy); if (key === undefined) { ungrouped.push(item); } else { addToGroup(groups, key, item); } } return { groups, ungrouped }; } function groupRecursive( data: Tree, configs: readonly GroupProcessConfig[], ): Groupeds { if (configs.length === 0) { return data; } const [current, ...rest] = configs; if (!current?.groupBy) { return groupRecursive(data, rest); } const { groupBy, labelBy, extraBy, skipSingle, sortBy } = current; const { groups, ungrouped } = groupByField(data, groupBy); const nodes: Groupeds = []; for (const [value, items] of groups) { if (items.length === 1 && skipSingle && items[0] !== undefined) { nodes.push(items[0]); } else { nodes.push({ $group: current, label: items .map((item) => getBy(item.$original, labelBy)) .find((item) => item !== undefined), ...(extraBy ? { extra: items .map((item) => getBy(item.$original, extraBy)) .find((item) => item !== undefined), } : undefined), value, selectable: false, children: rest.length > 0 ? groupRecursive(items, rest) : items, }); } } const processedUngrouped = rest.length > 0 ? groupRecursive(ungrouped, rest) : ungrouped; return [...nodes, ...processedUngrouped].toSorted((a, b) => customSort(getBy(a, sortBy), getBy(b, sortBy)), ); } /** * 将表格数据转换为树形结构 * @param data - 源数据记录数组 * @param groupConfigs - 分组配置,可以是单个配置或配置数组 * @returns 树形结构数据 */ export function tableGrouping( data: Tree = [], groupConfigs: Groups = [], ): Groupeds { if (data.length === 0) { return data; } const configs = ( Array.isArray(groupConfigs) ? groupConfigs : [groupConfigs] ).map( ({ skipSingle = false, groupBy, labelBy = groupBy, extraBy, sortBy = labelBy, }) => ({ groupBy, labelBy, sortBy, extraBy, skipSingle, }), ); return groupRecursive(data, configs); }