@ticatec/uniface-element 0.3.15 → 0.3.17

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,577 @@
1
+ # TreeNode - 树节点数据结构
2
+
3
+ ## 概述
4
+
5
+ `TreeNode` 是一个类,用于表示树形结构中的节点。它封装了节点的数据、子节点、展开状态,并提供了便捷的方法来操作节点本身和其子节点。
6
+
7
+ 每个从 `TreeNodes` 或 `CommonTreeNodes` 创建的节点都是 `TreeNode` 类的实例,具有内置的方法。
8
+
9
+ ## TreeNode 类
10
+
11
+ ```typescript
12
+ class TreeNode<T> {
13
+ /** 节点的数据对象(只读) */
14
+ public readonly item: T;
15
+
16
+ /** 是否展开(显示子节点) */
17
+ public expand: boolean;
18
+
19
+ /** 是否正在加载(用于懒加载) */
20
+ public loading: boolean;
21
+
22
+ /** 父节点(根节点为 null) */
23
+ public parent: TreeNode<T> | null;
24
+
25
+ /** 子节点(只读,使用方法修改) */
26
+ get children(): ReadonlyArray<TreeNode<T>>;
27
+
28
+ /** 添加子节点到当前节点 */
29
+ append(childItem: T): void;
30
+
31
+ /** 从父节点中分离(删除当前节点) */
32
+ detach(): void;
33
+
34
+ /** 将当前节点移动到另一个父节点下 */
35
+ moveTo(newParent: TreeNode<T>): void;
36
+
37
+ /** 替换当前节点的数据 */
38
+ replace(newItem: T): void;
39
+
40
+ /** 根据 ID 删除指定的子节点 */
41
+ removeChild(childId: any): void;
42
+
43
+ /** 删除所有子节点 */
44
+ removeChildren(): void;
45
+ }
46
+ ```
47
+
48
+ ## NodeViewOptions 类
49
+
50
+ `NodeViewOptions` 类处理树节点的视图配置,将显示关注点与数据分离:
51
+
52
+ ```typescript
53
+ class NodeViewOptions<T> {
54
+ /** 唯一标识字段名 */
55
+ keyField: keyof T;
56
+
57
+ /** 显示文字的字段名或函数 */
58
+ textField: keyof T | ((data: T) => string);
59
+
60
+ /** 可选:图标的字段名或函数 */
61
+ iconField?: keyof T | ((data: T) => string);
62
+
63
+ /** 可选:CSS 类名的字段名或函数 */
64
+ cssClassField?: keyof T | ((data: T) => string);
65
+
66
+ /** 可选:内联样式的字段名或函数 */
67
+ styleField?: keyof T | ((data: T) => Record<string, string>);
68
+
69
+ /** 从数据获取显示文字 */
70
+ getText(data: T): string;
71
+
72
+ /** 从数据获取图标 */
73
+ getIcon(data: T): string | undefined;
74
+
75
+ /** 从数据获取 CSS 类名 */
76
+ getCssClass(data: T): string | undefined;
77
+
78
+ /** 从数据获取样式 */
79
+ getStyle(data: T): Record<string, string> | undefined;
80
+
81
+ /** 从数据获取唯一键 */
82
+ getKey(data: T): any;
83
+ }
84
+ ```
85
+
86
+ ## TreeNode 方法详解
87
+
88
+ ### 1. append(childItem)
89
+
90
+ 添加一个子节点到当前节点。
91
+
92
+ **参数**:
93
+ - `childItem: T` - 子节点的数据对象
94
+
95
+ **示例**:
96
+ ```typescript
97
+ // 直接传入数据对象
98
+ parentNode.append({
99
+ id: 123,
100
+ name: "新子节点",
101
+ parentId: parentNode.item.id // 会自动设置
102
+ });
103
+ ```
104
+
105
+ **注意事项**:
106
+ - 自动初始化 children 数组(如果需要)
107
+ - 如果配置了排序函数,会自动排序
108
+ - 父节点的 `expand` 会自动设置为 `true`
109
+ - 触发版本更新以实现响应式更新
110
+
111
+ ---
112
+
113
+ ### 2. detach()
114
+
115
+ 从父节点中分离当前节点。
116
+
117
+ **参数**: 无
118
+
119
+ **示例**:
120
+ ```typescript
121
+ // 从父节点中删除该节点
122
+ node.detach();
123
+
124
+ // 删除后清空引用
125
+ activeNode.detach();
126
+ activeNode = null;
127
+ ```
128
+
129
+ **注意事项**:
130
+ - 从父节点的 `children` 数组中移除
131
+ - 从内部映射表中删除
132
+ - 如果是根节点,从根节点列表中移除
133
+ - 触发版本更新以实现响应式更新
134
+
135
+ ---
136
+
137
+ ### 3. moveTo(newParent)
138
+
139
+ 将当前节点移动到另一个父节点下。
140
+
141
+ **参数**:
142
+ - `newParent: TreeNode<T>` - 新的父节点(不是 ID)
143
+
144
+ **示例**:
145
+ ```typescript
146
+ // 移动到另一个父节点
147
+ node.moveTo(newParentNode);
148
+
149
+ // 先获取父节点
150
+ const parentNode = treeNodes.nodeMap.get(parentId);
151
+ if (parentNode) {
152
+ node.moveTo(parentNode);
153
+ }
154
+ ```
155
+
156
+ **注意事项**:
157
+ - 接收 TreeNode 实例作为参数,不是 ID
158
+ - 会自动更新节点数据中的 `parentKeyField`
159
+ - 从原父节点的 `children` 中移除
160
+ - 添加到新父节点的 `children`
161
+ - 如果配置了排序函数,会自动重新排序
162
+ - 触发版本更新以实现响应式更新
163
+
164
+ ---
165
+
166
+ ### 4. replace(newItem)
167
+
168
+ 替换当前节点的数据。
169
+
170
+ **参数**:
171
+ - `newItem: T` - 新的数据对象
172
+
173
+ **示例**:
174
+ ```typescript
175
+ // 更新节点的部分字段
176
+ node.replace({
177
+ ...node.item, // 保持其他字段
178
+ name: "新的名称", // 更新名称
179
+ status: "active" // 更新状态
180
+ });
181
+
182
+ // 或完全替换
183
+ node.replace({
184
+ id: node.item.id, // 必须保持 ID
185
+ name: "完全新的数据",
186
+ newField: "值"
187
+ });
188
+ ```
189
+
190
+ **注意事项**:
191
+ - 通常需要保持 `id` 字段不变
192
+ - 如果配置了排序函数且排序字段改变,会自动重新排序
193
+ - 会自动触发视图更新
194
+ - 保留子节点、展开状态和加载状态
195
+
196
+ ---
197
+
198
+ ### 5. removeChild(childId)
199
+
200
+ 删除指定的子节点。
201
+
202
+ **参数**:
203
+ - `childId: any` - 要删除的子节点的 ID
204
+
205
+ **示例**:
206
+ ```typescript
207
+ // 删除指定 ID 的子节点
208
+ parentNode.removeChild(123);
209
+
210
+ // 使用变量
211
+ const childId = childNode.item.id;
212
+ parentNode.removeChild(childId);
213
+ ```
214
+
215
+ **注意事项**:
216
+ - 只删除直接子节点,不会递归删除孙子节点
217
+ - 如果子节点不存在,会静默忽略
218
+ - 子节点的所有后代也会被删除
219
+ - 触发版本更新以实现响应式更新
220
+
221
+ ---
222
+
223
+ ### 6. removeChildren()
224
+
225
+ 删除所有子节点。
226
+
227
+ **参数**: 无
228
+
229
+ **示例**:
230
+ ```typescript
231
+ // 清空所有子节点
232
+ parentNode.removeChildren();
233
+
234
+ // 删除后添加新节点
235
+ parentNode.removeChildren();
236
+ parentNode.append(newChildData);
237
+ ```
238
+
239
+ **注意事项**:
240
+ - 会删除所有子节点及其后代
241
+ - 节点本身的 `expand` 状态保持不变
242
+ - 内部映射表会同步更新
243
+ - 触发版本更新以实现响应式更新
244
+
245
+ ## CommonTreeNodes 配置选项
246
+
247
+ 创建树时,可以使用以下选项进行配置:
248
+
249
+ ```typescript
250
+ interface TreeNodeOptions<T> {
251
+ /** 唯一标识字段名(默认:'id') */
252
+ keyField?: keyof T;
253
+
254
+ /** 显示文字的字段名或函数(默认:'text') */
255
+ textField?: keyof T | ((data: T) => string);
256
+
257
+ /** 父节点引用字段名(默认:'parentId') */
258
+ parentKeyField?: keyof T;
259
+
260
+ /** 判断数据是否为根节点的函数 */
261
+ checkIsRoot: (data: T) => boolean;
262
+
263
+ /** 可选:判断节点是否为分支(有子节点)的函数 */
264
+ checkIsDirectory?: (node: TreeNode<T>) => boolean;
265
+
266
+ /** 可选:排序比较函数 */
267
+ compareFun?: (o1: T, o2: T) => number | undefined;
268
+
269
+ /** 可选:展开深度(默认:1) */
270
+ expendDepth?: number;
271
+ }
272
+ ```
273
+
274
+ ## 完整使用示例
275
+
276
+ ```svelte
277
+ <script>
278
+ import TreeView from "@ticatec/uniface-element/TreeView";
279
+ import { CommonTreeNodes, type TreeNode } from "@ticatec/uniface-element/lib/TreeNodes";
280
+
281
+ // 数据类型定义
282
+ interface MyData {
283
+ id: number;
284
+ name: string;
285
+ parentId: number | null;
286
+ }
287
+
288
+ let activeNode: TreeNode<MyData> | null = null;
289
+
290
+ // 创建树结构
291
+ const treeNodes = new CommonTreeNodes<MyData>({
292
+ keyField: 'id',
293
+ textField: 'name',
294
+ parentKeyField: 'parentId',
295
+ checkIsRoot: (item) => item.parentId === null,
296
+ checkIsDirectory: (node) => node.children.length > 0,
297
+ expendDepth: 2
298
+ });
299
+
300
+ // 初始化数据
301
+ treeNodes.setData([
302
+ { id: 1, name: "根节点", parentId: null },
303
+ { id: 2, name: "子节点1", parentId: 1 },
304
+ { id: 3, name: "子节点2", parentId: 1 }
305
+ ]);
306
+
307
+ // 添加子节点
308
+ function addChild() {
309
+ if (!activeNode) return;
310
+
311
+ activeNode.append({
312
+ id: Date.now(),
313
+ name: "新子节点",
314
+ parentId: activeNode.item.id
315
+ });
316
+ }
317
+
318
+ // 删除当前节点
319
+ function deleteCurrentNode() {
320
+ if (!activeNode) return;
321
+ activeNode.detach();
322
+ activeNode = null;
323
+ }
324
+
325
+ // 删除指定子节点
326
+ function deleteChild(childId: number) {
327
+ if (!activeNode) return;
328
+ activeNode.removeChild(childId);
329
+ }
330
+
331
+ // 清空所有子节点
332
+ function clearChildren() {
333
+ if (!activeNode) return;
334
+ activeNode.removeChildren();
335
+ }
336
+
337
+ // 重命名节点
338
+ function renameNode(newName: string) {
339
+ if (!activeNode) return;
340
+ activeNode.replace({
341
+ ...activeNode.item,
342
+ name: newName
343
+ });
344
+ }
345
+
346
+ // 移动节点
347
+ function moveNodeTo(newParentId: number) {
348
+ if (!activeNode) return;
349
+ const newParent = treeNodes.nodeMap.get(newParentId);
350
+ if (newParent) {
351
+ activeNode.moveTo(newParent);
352
+ }
353
+ }
354
+ </script>
355
+
356
+ <div>
357
+ <TreeView
358
+ nodes={treeNodes.nodes}
359
+ version={treeNodes.version}
360
+ textField="name"
361
+ bind:activeNode
362
+ checkIsDirectory={(node) => node.children.length > 0}
363
+ />
364
+
365
+ {#if activeNode}
366
+ <div class="node-actions">
367
+ <h3>当前选中: {activeNode.item.name}</h3>
368
+ <button on:click={addChild}>添加子节点</button>
369
+ <button on:click={deleteCurrentNode}>删除节点</button>
370
+ <button on:click={() => renameNode("新名称")}>重命名</button>
371
+ <button on:click={() => moveNodeTo(1)}>移动到根节点</button>
372
+ <button on:click={clearChildren}>清空子节点</button>
373
+ </div>
374
+ {/if}
375
+ </div>
376
+ ```
377
+
378
+ ## 与 TreeNodes 类方法的区别
379
+
380
+ `TreeNode` 实例方法和 `CommonTreeNodes`/`TreeNodes` 类方法有明确的职责分工:
381
+
382
+ ### TreeNode 实例方法(用于操作特定节点实例)
383
+
384
+ ```typescript
385
+ // 直接在节点实例上操作
386
+ node.append(childItem); // 添加子节点到该节点
387
+ node.detach(); // 从父节点中分离该节点
388
+ node.replace(newItem); // 替换该节点的数据
389
+ node.moveTo(newParentNode); // 将该节点移动到另一个父节点
390
+ node.removeChild(childId); // 删除该节点的指定子节点
391
+ node.removeChildren(); // 删除该节点的所有子节点
392
+ ```
393
+
394
+ ### CommonTreeNodes/TreeNodes 类方法(用于树级别的操作)
395
+
396
+ ```typescript
397
+ // 树级别操作
398
+ treeNodes.setData(data); // 初始化树结构
399
+ treeNodes.append(item); // 添加新节点(自动找父节点)
400
+ treeNodes.nodes; // 获取根节点列表
401
+ treeNodes.version; // 获取版本号
402
+ treeNodes.getHierarchyList(); // 获取展开的节点列表(仅 TreeNodes)
403
+ treeNodes.extractDirectories(item); // 提取目录结构(仅 TreeNodes)
404
+ ```
405
+
406
+ ## 主要属性
407
+
408
+ ### item(只读)
409
+ 节点中存储的数据对象。不能直接修改。使用 `replace()` 来更新数据。
410
+
411
+ ### children(只读)
412
+ 返回子节点的只读数组。要修改子节点,使用以下方法:
413
+ - `append()` - 添加子节点
414
+ - `removeChild()` - 删除特定子节点
415
+ - `removeChildren()` - 删除所有子节点
416
+
417
+ ### expand
418
+ 控制节点是否展开(显示其子节点)。可以直接设置:
419
+ ```typescript
420
+ node.expand = true; // 展开节点
421
+ node.expand = false; // 折叠节点
422
+ ```
423
+
424
+ ### loading
425
+ 指示节点是否正在加载数据(用于懒加载)。可以直接设置:
426
+ ```typescript
427
+ node.loading = true; // 显示加载状态
428
+ node.loading = false; // 隐藏加载状态
429
+ ```
430
+
431
+ ### parent
432
+ 父节点的引用。根节点为 `null`。
433
+
434
+ ```typescript
435
+ if (node.parent) {
436
+ console.log("父节点名称:", node.parent.item.name);
437
+ }
438
+ ```
439
+
440
+ ## 响应式更新
441
+
442
+ 所有的节点方法操作都会自动触发 `version` 更新,确保 Svelte 组件能够响应数据变化:
443
+
444
+ ```svelte
445
+ <TreeView
446
+ nodes={treeNodes.nodes}
447
+ version={treeNodes.version} ← 自动更新
448
+ ...
449
+ />
450
+ ```
451
+
452
+ 你不需要手动触发更新,节点方法会自动处理。
453
+
454
+ ## 常见问题
455
+
456
+ ### Q: 如何获取节点实例?
457
+
458
+ ```typescript
459
+ // 方式1: 通过 activeNode(选中节点)
460
+ let activeNode: TreeNode | null = null;
461
+ <TreeView bind:activeNode />
462
+
463
+ // 方式2: 通过 nodeMap(需要保留 TreeNodes 引用)
464
+ const node = treeNodes.nodeMap.get(nodeId);
465
+ ```
466
+
467
+ ### Q: 如何遍历子节点?
468
+
469
+ ```typescript
470
+ // children 是只读的,但可以迭代
471
+ node.children.forEach(child => {
472
+ console.log(child.item.name);
473
+ });
474
+
475
+ // 或使用 for...of
476
+ for (const child of node.children) {
477
+ console.log(child.item.name);
478
+ }
479
+ ```
480
+
481
+ ### Q: 如何查找特定子节点?
482
+
483
+ ```typescript
484
+ function findChild(node: TreeNode, childId: number) {
485
+ return node.children.find(child => child.item.id === childId) || null;
486
+ }
487
+ ```
488
+
489
+ ### Q: 可以直接修改 children 数组吗?
490
+
491
+ 不可以。`children` 属性是只读的。请使用提供的方法:
492
+ - `append()` 添加
493
+ - `removeChild()` 删除特定子节点
494
+ - `removeChildren()` 删除所有子节点
495
+
496
+ ### Q: 如何递归操作所有子孙节点?
497
+
498
+ ```typescript
499
+ function traverse(node: TreeNode, callback: (node: TreeNode) => void) {
500
+ callback(node);
501
+ for (const child of node.children) {
502
+ traverse(child, callback);
503
+ }
504
+ }
505
+
506
+ // 使用
507
+ traverse(rootNode, (node) => {
508
+ console.log(node.item.name);
509
+ node.expand = true; // 展开所有节点
510
+ });
511
+ ```
512
+
513
+ ### Q: TreeNode 和 NodeViewOptions 有什么区别?
514
+
515
+ - **TreeNode**:表示数据结构。每个节点都是具有操作方法的实例。
516
+ - **NodeViewOptions**:配置类,用于定义节点如何显示(文字字段、图标、CSS 等)。
517
+
518
+ 这种分离使得 TreeNode 可以在不同上下文中使用(TreeView、TreeDataGrid 等),而不会与视图特定关注点耦合。
519
+
520
+ ## 最佳实践
521
+
522
+ 1. **优先使用节点方法**:当你有节点实例时,直接调用节点方法更直观
523
+
524
+ 2. **保持 ID 不变**:使用 `replace()` 时,确保 `id` 字段保持不变
525
+
526
+ 3. **正确使用 moveTo**:`moveTo()` 方法接收 TreeNode 实例,不是 ID:
527
+ ```typescript
528
+ // ❌ 错误
529
+ node.moveTo(123);
530
+
531
+ // ✅ 正确
532
+ const parentNode = treeNodes.nodeMap.get(123);
533
+ node.moveTo(parentNode);
534
+ ```
535
+
536
+ 4. **版本管理**:不需要手动管理版本,节点方法会自动更新
537
+
538
+ 5. **类型安全**:使用 TypeScript 泛型确保类型安全
539
+
540
+ ```typescript
541
+ // ✅ 推荐:使用泛型
542
+ const treeNodes = new CommonTreeNodes<MyData>({ ... });
543
+
544
+ // 节点类型会自动推断
545
+ node.append({ id: 1, name: "..." }); // 类型检查
546
+ ```
547
+
548
+ 6. **children 是只读的**:不要尝试直接修改 children 数组。始终使用提供的方法。
549
+
550
+ ## 性能考虑
551
+
552
+ - 所有节点方法的时间复杂度都是 O(1) 或 O(n),其中 n 是子节点数量
553
+ - 删除操作会同时从内部映射表中移除,保持一致性
554
+ - 大批量操作时,考虑直接使用 `setData()` 重建树结构
555
+
556
+ ## 架构设计
557
+
558
+ ### 与视图解耦
559
+
560
+ TreeNode 设计为独立于任何特定视图组件:
561
+ - TreeNode 中没有视图特定的逻辑
562
+ - NodeViewOptions 单独处理视图配置
563
+ - 可用于 TreeView、TreeDataGrid 或任何其他基于树的组件
564
+
565
+ ### 数据不可变性
566
+
567
+ `item` 属性是只读的,以确保数据完整性:
568
+ - 防止意外修改
569
+ - 通过 `replace()` 方法使状态更改显式化
570
+ - 有助于调试和状态跟踪
571
+
572
+ ## 相关组件
573
+
574
+ - `CommonTreeNodes` - 基础树管理类
575
+ - `TreeNodes` - 扩展的树管理类,具有层次结构功能
576
+ - `TreeView` - 树视图组件
577
+ - `NodeViewOptions` - 视图配置类