shinsu-senju
Version:
Tree mapper utils
206 lines (179 loc) • 4.78 kB
text/typescript
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);
}