@ticatec/uniface-element 0.3.15 → 0.3.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,449 @@
1
+ # TreeNode - 树节点数据结构
2
+
3
+ ## 概述
4
+
5
+ `TreeNode` 是用于表示树形结构中节点的核心数据类型。它包含了节点的数据、子节点、展开状态等信息,并提供了便捷的方法来操作节点本身和其子节点。
6
+
7
+ 每个从 `TreeNodes` 获取的节点都自动绑定了操作方法,可以直接调用。
8
+
9
+ ## 类型定义
10
+
11
+ ```typescript
12
+ interface TreeNode<T> {
13
+ /** 节点的数据对象 */
14
+ item: T;
15
+
16
+ /** 是否展开(显示子节点) */
17
+ expand?: boolean;
18
+
19
+ /** 子节点数组 */
20
+ children?: TreeNode<T>[];
21
+ }
22
+ ```
23
+
24
+ ### TreeNodeWithMethods<T>
25
+
26
+ 这是实际使用的节点类型,继承了 `TreeNode<T>` 并添加了操作方法:
27
+
28
+ ```typescript
29
+ interface TreeNodeWithMethods<T> extends TreeNode<T> {
30
+ /** 添加子节点到当前节点 */
31
+ append: (childItem: T) => void;
32
+
33
+ /** 删除当前节点(从父节点中移除) */
34
+ remove: () => void;
35
+
36
+ /** 替换当前节点的数据 */
37
+ replace: (newItem: T) => void;
38
+
39
+ /** 将当前节点移动到另一个父节点下 */
40
+ moveTo: (newParentId: string) => void;
41
+
42
+ /** 删除指定的子节点 */
43
+ removeChild: (childId: any) => void;
44
+
45
+ /** 删除所有子节点 */
46
+ removeChildren: () => void;
47
+ }
48
+ ```
49
+
50
+ ## 节点方法详解
51
+
52
+ ### 1. append(childItem)
53
+
54
+ 添加一个子节点到当前节点。
55
+
56
+ **参数**:
57
+ - `childItem: T` - 子节点的数据对象
58
+
59
+ **示例**:
60
+ ```typescript
61
+ // 直接传入数据对象
62
+ parentNode.append({
63
+ id: 123,
64
+ name: "新子节点",
65
+ parent: parentNode.item.id // 可选,会自动设置
66
+ });
67
+ ```
68
+
69
+ **注意事项**:
70
+ - **自动加载 lazy 节点**:当节点被选中(点击或外部设置 `activeNode`)时,如果是 lazy 节点且未加载(`children === null`),TreeView 会自动触发 `lazyLoader.load()` 加载子节点
71
+ - **手动添加子节点**:可以手动调用 `append()` 添加子节点
72
+ - 如果节点还未加载,会自动初始化 `children` 数组
73
+ - 节点会从 leaf 转为 branch,UI 会显示 branch 图标
74
+ - 如果配置了排序函数,会自动排序
75
+ - 父节点的 `expand` 会自动设置为 `true`
76
+
77
+ ---
78
+
79
+ ### 2. remove()
80
+
81
+ 删除当前节点(从其父节点中移除)。
82
+
83
+ **参数**: 无
84
+
85
+ **示例**:
86
+ ```typescript
87
+ // 删除节点
88
+ node.remove();
89
+
90
+ // 删除后清空引用
91
+ activeNode.remove();
92
+ activeNode = null;
93
+ ```
94
+
95
+ **注意事项**:
96
+ - 从父节点的 `children` 数组中移除
97
+ - 从内部映射表中删除
98
+ - 如果是根节点,从根节点列表中移除
99
+
100
+ ---
101
+
102
+ ### 3. replace(newItem)
103
+
104
+ 替换当前节点的数据。
105
+
106
+ **参数**:
107
+ - `newItem: T` - 新的数据对象
108
+
109
+ **示例**:
110
+ ```typescript
111
+ // 更新节点的部分字段
112
+ node.replace({
113
+ ...node.item, // 保持其他字段
114
+ name: "新的名称", // 更新名称
115
+ status: "active" // 更新状态
116
+ });
117
+
118
+ // 或完全替换
119
+ node.replace({
120
+ id: node.item.id, // 必须保持 ID
121
+ name: "完全新的数据",
122
+ newField: "值"
123
+ });
124
+ ```
125
+
126
+ **注意事项**:
127
+ - 通常需要保持 `id` 字段不变
128
+ - 如果配置了排序函数且排序字段改变,会自动重新排序
129
+ - 会自动触发视图更新
130
+
131
+ ---
132
+
133
+ ### 4. moveTo(newParentId)
134
+
135
+ 将当前节点移动到另一个父节点下。
136
+
137
+ **参数**:
138
+ - `newParentId: string | number` - 新父节点的 ID
139
+
140
+ **示例**:
141
+ ```typescript
142
+ // 移动到指定的父节点
143
+ node.moveTo("parent-123");
144
+
145
+ // 使用另一个节点的 ID
146
+ node.moveTo(anotherNode.item.id);
147
+ ```
148
+
149
+ **注意事项**:
150
+ - 会自动更新节点的 `parent` 字段
151
+ - 从原父节点的 `children` 中移除
152
+ - 添加到新父节点的 `children`
153
+ - **Lazy 节点处理**:如果新父节点是 lazy-loaded 且未加载(`children === null`),会自动初始化 `children` 数组
154
+ - 新父节点会从 leaf 转为 branch,UI 会显示 branch 图标
155
+ - 未来用户展开节点时,lazyLoader 加载的子节点会与移动过来的节点合并
156
+
157
+ ---
158
+
159
+ ### 5. removeChild(childId)
160
+
161
+ 删除指定的子节点。
162
+
163
+ **参数**:
164
+ - `childId: any` - 要删除的子节点的 ID
165
+
166
+ **示例**:
167
+ ```typescript
168
+ // 删除指定 ID 的子节点
169
+ parentNode.removeChild(123);
170
+
171
+ // 使用变量
172
+ const childId = childNode.item.id;
173
+ parentNode.removeChild(childId);
174
+ ```
175
+
176
+ **注意事项**:
177
+ - 只删除直接子节点,不会递归删除孙子节点
178
+ - 如果子节点不存在,会静默忽略
179
+ - 子节点的所有后代也会被删除
180
+
181
+ ---
182
+
183
+ ### 6. removeChildren()
184
+
185
+ 删除所有子节点。
186
+
187
+ **参数**: 无
188
+
189
+ **示例**:
190
+ ```typescript
191
+ // 清空所有子节点
192
+ parentNode.removeChildren();
193
+
194
+ // 删除后添加新节点
195
+ parentNode.removeChildren();
196
+ parentNode.append(newChildData);
197
+ ```
198
+
199
+ **注意事项**:
200
+ - 会删除所有子节点及其后代
201
+ - 节点本身的 `expand` 状态保持不变
202
+ - 内部映射表会同步更新
203
+
204
+ ## 完整使用示例
205
+
206
+ ```svelte
207
+ <script>
208
+ import TreeView from "@ticatec/uniface-element/TreeView";
209
+ import TreeNodes, { type TreeNode } from "@ticatec/uniface-element/TreeNodes";
210
+
211
+ // 数据类型定义
212
+ interface MyData {
213
+ id: number;
214
+ name: string;
215
+ parent: number | null;
216
+ }
217
+
218
+ let activeNode: TreeNodeWithMethods<MyData> | null = null;
219
+
220
+ // 创建树结构
221
+ const treeNodes = new TreeNodes<MyData>({
222
+ keyField: 'id',
223
+ textField: 'name',
224
+ parentKeyField: 'parent',
225
+ checkIsRoot: (item) => item.parent === null,
226
+ checkIsDirectory: (node) => node.children != null
227
+ });
228
+
229
+ // 初始化数据
230
+ treeNodes.setData([
231
+ { id: 1, name: "根节点", parent: null },
232
+ { id: 2, name: "子节点1", parent: 1 },
233
+ { id: 3, name: "子节点2", parent: 1 }
234
+ ]);
235
+
236
+ // 添加子节点
237
+ function addChild() {
238
+ if (!activeNode) return;
239
+
240
+ activeNode.append({
241
+ id: Date.now(),
242
+ name: "新子节点",
243
+ parent: activeNode.item.id
244
+ });
245
+ }
246
+
247
+ // 删除当前节点
248
+ function deleteCurrentNode() {
249
+ if (!activeNode) return;
250
+ activeNode.remove();
251
+ activeNode = null;
252
+ }
253
+
254
+ // 删除指定子节点
255
+ function deleteChild(childId: number) {
256
+ if (!activeNode) return;
257
+ activeNode.removeChild(childId);
258
+ }
259
+
260
+ // 清空所有子节点
261
+ function clearChildren() {
262
+ if (!activeNode) return;
263
+ activeNode.removeChildren();
264
+ }
265
+
266
+ // 重命名节点
267
+ function renameNode(newName: string) {
268
+ if (!activeNode) return;
269
+ activeNode.replace({
270
+ ...activeNode.item,
271
+ name: newName
272
+ });
273
+ }
274
+
275
+ // 移动节点
276
+ function moveNodeTo(newParentId: number) {
277
+ if (!activeNode) return;
278
+ activeNode.moveTo(newParentId);
279
+ }
280
+ </script>
281
+
282
+ <div>
283
+ <TreeView
284
+ nodes={treeNodes.nodes}
285
+ version={treeNodes.version}
286
+ textField="name"
287
+ bind:activeNode
288
+ checkIsDirectory={(node) => node.children != null}
289
+ />
290
+
291
+ {#if activeNode}
292
+ <div class="node-actions">
293
+ <h3>当前选中: {activeNode.item.name}</h3>
294
+ <button on:click={addChild}>添加子节点</button>
295
+ <button on:click={deleteCurrentNode}>删除节点</button>
296
+ <button on:click={() => renameNode("新名称")}>重命名</button>
297
+ <button on:click={() => moveNodeTo(1)}>移动到根节点</button>
298
+ <button on:click={clearChildren}>清空子节点</button>
299
+ </div>
300
+ {/if}
301
+ </div>
302
+ ```
303
+
304
+ ## 与 TreeNodes 类方法的区别
305
+
306
+ `TreeNode` 的方法和 `TreeNodes` 类的方法有明确的职责分工:
307
+
308
+ ### TreeNode 节点方法(推荐用于操作特定节点)
309
+
310
+ ```typescript
311
+ // 直接在节点上操作 - 推荐用于已知节点实例的场景
312
+ node.append(childItem); // 添加子节点到该节点
313
+ node.remove(); // 删除该节点
314
+ node.replace(newItem); // 替换该节点的数据
315
+ node.moveTo(newParentId); // 移动该节点
316
+ node.removeChild(childId); // 删除该节点的指定子节点
317
+ node.removeChildren(); // 删除该节点的所有子节点
318
+ ```
319
+
320
+ ### TreeNodes 类方法(用于批量操作或初始化)
321
+
322
+ ```typescript
323
+ // 类级别操作 - 适合只知道数据的场景
324
+ treeNodes.setData(data); // 初始化树结构
325
+ treeNodes.append(item); // 添加新节点(自动找父节点)
326
+ treeNodes.nodes; // 获取根节点列表
327
+ treeNodes.version; // 获取版本号
328
+ treeNodes.getHierarchyList(); // 获取展开的节点列表
329
+ treeNodes.extractDirectories(item); // 提取目录结构
330
+ ```
331
+
332
+ ## 响应式更新
333
+
334
+ 所有的节点方法操作都会自动触发 `version` 更新,确保 Svelte 组件能够响应数据变化:
335
+
336
+ ```svelte
337
+ <TreeView
338
+ nodes={treeNodes.nodes}
339
+ version={treeNodes.version} ← 自动更新
340
+ ...
341
+ />
342
+ ```
343
+
344
+ 你不需要手动触发更新,节点方法会自动处理。
345
+
346
+ ## 常见问题
347
+
348
+ ### Q: 如何获取节点实例?
349
+
350
+ ```typescript
351
+ // 方式1: 通过 activeNode(选中节点)
352
+ let activeNode: TreeNode | null = null;
353
+ <TreeView bind:activeNode />
354
+
355
+ // 方式2: 通过 nodeMap(需要保留 TreeNodes 引用)
356
+ const node = treeNodes.nodeMap.get(nodeId);
357
+ ```
358
+
359
+ ### Q: 如何遍历子节点?
360
+
361
+ ```typescript
362
+ if (node.children) {
363
+ node.children.forEach(child => {
364
+ console.log(child.item.name);
365
+ });
366
+ }
367
+ ```
368
+
369
+ ### Q: 如何查找特定子节点?
370
+
371
+ ```typescript
372
+ function findChild(node: TreeNode, childId: number) {
373
+ if (!node.children) return null;
374
+ return node.children.find(child => child.item.id === childId) || null;
375
+ }
376
+ ```
377
+
378
+ ### Q: Lazy-loaded 节点如何工作?
379
+
380
+ 对于使用 lazyLoader 的节点(`children === null`):
381
+
382
+ ```typescript
383
+ // ✅ 当节点被选中时,TreeView 会自动触发加载
384
+ // 点击节点或设置 activeNode 时:
385
+ // - 如果是 lazy 节点且未加载
386
+ // - 自动调用 lazyLoader.load()
387
+ // - children 被设置,节点转为 branch
388
+
389
+ // 手动添加子节点
390
+ lazyNode.append(childItem); // 初始化 children 数组并添加子节点
391
+ // 节点转为 branch,显示 branch 图标
392
+
393
+ lazyNode.moveTo(parentId); // 移动到该节点会自动初始化 children
394
+
395
+ // removeChild() 和 removeChildren() 在加载前无效果
396
+ lazyNode.removeChild(id); // children 为 null 时无效果
397
+ lazyNode.removeChildren(); // children 为 null 时无效果
398
+ ```
399
+
400
+ ### Q: 如何递归操作所有子孙节点?
401
+
402
+ ```typescript
403
+ function traverse(node: TreeNode, callback: (node: TreeNode) => void) {
404
+ callback(node);
405
+ if (node.children) {
406
+ node.children.forEach(child => traverse(child, callback));
407
+ }
408
+ }
409
+
410
+ // 使用
411
+ traverse(rootNode, (node) => {
412
+ console.log(node.item.name);
413
+ node.expand = true; // 展开所有节点
414
+ });
415
+ ```
416
+
417
+ ## 最佳实践
418
+
419
+ 1. **优先使用节点方法**:当你有节点实例时,直接调用节点方法更直观
420
+
421
+ 2. **保持 ID 不变**:使用 `replace()` 时,确保 `id` 字段保持不变
422
+
423
+ 3. **检查 children 存在性**:操作子节点前,先检查 `node.children` 是否存在
424
+
425
+ 4. **处理 lazy 节点**:对于 lazy-loaded 节点,先等待加载完成再操作
426
+
427
+ 5. **版本管理**:不需要手动管理版本,节点方法会自动更新
428
+
429
+ 6. **类型安全**:使用 TypeScript 泛型确保类型安全
430
+
431
+ ```typescript
432
+ // ✅ 推荐:使用泛型
433
+ const treeNodes = new TreeNodes<MyData>({ ... });
434
+
435
+ // 节点会自动推断类型
436
+ node.append({ id: 1, name: "..." }); // 类型检查
437
+ ```
438
+
439
+ ## 性能考虑
440
+
441
+ - 所有节点方法的时间复杂度都是 O(1) 或 O(n),其中 n 是子节点数量
442
+ - 删除操作会同时从内部映射表中移除,保持一致性
443
+ - 大批量操作时,考虑直接使用 `setData()` 重建树结构
444
+
445
+ ## 相关组件
446
+
447
+ - `TreeNodes` - 树管理类
448
+ - `TreeView` - 树视图组件
449
+ - `LazyLoader` - 延迟加载接口
@@ -17,22 +17,105 @@ export interface TreeNodeOptions<T> {
17
17
  compareFun?: CompareFun<T>;
18
18
  expendDepth?: number;
19
19
  }
20
+ export type TreeNodeWithMethods<T> = TreeNode<T> & {
21
+ /**
22
+ * 添加子节点到当前节点
23
+ * @param childItem 子节点的数据(直接传入数据对象,不需要包装成 TreeNode)
24
+ *
25
+ * @example
26
+ * // 直接传入数据对象
27
+ * parentNode.append({ id: 1, name: "子节点" });
28
+ */
29
+ append: (childItem: T) => void;
30
+ /**
31
+ * 删除当前节点(从父节点中移除)
32
+ *
33
+ * @example
34
+ * node.remove();
35
+ */
36
+ remove: () => void;
37
+ /**
38
+ * 替换当前节点的数据
39
+ * @param newItem 新的数据对象(直接传入数据对象,不需要包装成 TreeNode)
40
+ *
41
+ * @example
42
+ * node.replace({ id: 1, name: "新名称", ...otherFields });
43
+ */
44
+ replace: (newItem: T) => void;
45
+ /**
46
+ * 将当前节点移动到另一个父节点下
47
+ * @param newParentId 新父节点的 ID(根据 keyField 指定的字段)
48
+ *
49
+ * @example
50
+ * node.moveTo("parent-123");
51
+ */
52
+ moveTo: (newParentId: string) => void;
53
+ /**
54
+ * 删除指定的子节点
55
+ * @param childId 子节点的 ID(根据 keyField 指定的字段)
56
+ *
57
+ * @example
58
+ * node.removeChild(123);
59
+ */
60
+ removeChild: (childId: any) => void;
61
+ /**
62
+ * 删除所有子节点
63
+ *
64
+ * @example
65
+ * node.removeChildren();
66
+ */
67
+ removeChildren: () => void;
68
+ };
20
69
  export declare class CommonTreeNodes<T> {
21
70
  protected readonly checkIsRoot: CheckIsRoot<T>;
22
71
  protected readonly keyField: keyof T;
23
72
  protected readonly parentKeyField: keyof T;
24
73
  protected readonly textField: keyof T | GetText<T>;
25
74
  protected readonly checkIsDirectory: CheckIsDirectory<T> | undefined;
26
- protected nodeMap: Map<any, TreeNode<T>>;
75
+ protected nodeMap: Map<any, TreeNodeWithMethods<T>>;
27
76
  protected compareFun: any | undefined;
28
77
  protected expendDepth: number;
29
- protected _nodes: Array<TreeNode<T>>;
78
+ protected _nodes: Array<TreeNodeWithMethods<T>>;
79
+ protected _version: number;
30
80
  constructor(options: TreeNodeOptions<T>);
81
+ protected touch(): void;
82
+ /**
83
+ * 创建带有方法的节点
84
+ */
85
+ protected createNodeWithMethods(item: T): TreeNodeWithMethods<T>;
86
+ /**
87
+ * 添加子节点到指定节点
88
+ */
89
+ protected addNodeToNode(parentNode: TreeNodeWithMethods<T>, childItem: T): void;
90
+ /**
91
+ * 删除指定节点
92
+ */
93
+ protected removeNode(node: TreeNodeWithMethods<T>): void;
94
+ /**
95
+ * 替换节点的数据
96
+ */
97
+ protected replaceNodeItem(node: TreeNodeWithMethods<T>, newItem: T): void;
98
+ /**
99
+ * 将节点移动到新的父节点下
100
+ */
101
+ protected moveNodeTo(node: TreeNodeWithMethods<T>, newParentId: string): void;
102
+ /**
103
+ * 删除指定节点的某个子节点
104
+ */
105
+ protected removeChildNode(parentNode: TreeNodeWithMethods<T>, childId: any): void;
106
+ /**
107
+ * 删除指定节点的所有子节点
108
+ */
109
+ protected removeChildrenNodes(parentNode: TreeNodeWithMethods<T>): void;
31
110
  setData(list: Array<T>): void;
32
111
  /**
33
112
  * 获取节点
34
113
  */
35
114
  get nodes(): Array<TreeNode<T>>;
115
+ /**
116
+ * 获取版本号,用于触发 Svelte 响应式更新
117
+ */
118
+ get version(): number;
36
119
  /**
37
120
  * 设置展开层级
38
121
  * @param nodes
@@ -46,7 +129,7 @@ export declare class CommonTreeNodes<T> {
46
129
  * @param doSort
47
130
  * @protected
48
131
  */
49
- protected appendNode(node: TreeNode<T>, doSort?: boolean): void;
132
+ protected appendNode(node: TreeNodeWithMethods<T>, doSort?: boolean): void;
50
133
  }
51
134
  export default class TreeNodes<T> extends CommonTreeNodes<T> {
52
135
  /**
@@ -54,26 +137,10 @@ export default class TreeNodes<T> extends CommonTreeNodes<T> {
54
137
  */
55
138
  getHierarchyList(): Array<TreeNode<T>>;
56
139
  /**
57
- * 给treeview设定数据,根据数据构建树结构
58
- * @param list
59
- */
60
- /**
61
- * 增加一个新节点
62
- * @param item
140
+ * 增加一个新节点(便捷方法,会自动根据 parentKeyField 找到父节点)
141
+ * @param item 要添加的节点数据
63
142
  */
64
143
  append(item: T): void;
65
- /**
66
- * 替换一个节点的数据
67
- * @param item
68
- */
69
- replace(item: T): void;
70
- remove(item: T): void;
71
- /**
72
- * 将一个节点移动到另外一个节点下
73
- * @param item
74
- * @param parentId
75
- */
76
- moveTo(item: T, parentId: string): void;
77
144
  /**
78
145
  * 获取除指定节点外的其他节点
79
146
  * @param exclusiveData