@ticatec/uniface-element 0.3.16 → 0.3.18

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.
@@ -2,52 +2,88 @@
2
2
 
3
3
  ## 概述
4
4
 
5
- `TreeNode` 是用于表示树形结构中节点的核心数据类型。它包含了节点的数据、子节点、展开状态等信息,并提供了便捷的方法来操作节点本身和其子节点。
5
+ `TreeNode` 是一个类,用于表示树形结构中的节点。它封装了节点的数据、子节点、展开状态,并提供了便捷的方法来操作节点本身和其子节点。
6
6
 
7
- 每个从 `TreeNodes` 获取的节点都自动绑定了操作方法,可以直接调用。
7
+ 每个从 `TreeNodes` 或 `CommonTreeNodes` 创建的节点都是 `TreeNode` 类的实例,具有内置的方法。
8
8
 
9
- ## 类型定义
9
+ ## TreeNode 类
10
10
 
11
11
  ```typescript
12
- interface TreeNode<T> {
13
- /** 节点的数据对象 */
14
- item: T;
12
+ class TreeNode<T> {
13
+ /** 节点的数据对象(只读) */
14
+ public readonly item: T;
15
15
 
16
16
  /** 是否展开(显示子节点) */
17
- expand?: boolean;
17
+ public expand: boolean;
18
18
 
19
- /** 子节点数组 */
20
- children?: TreeNode<T>[];
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;
21
45
  }
22
46
  ```
23
47
 
24
- ### TreeNodeWithMethods<T>
48
+ ## NodeViewOptions 类
25
49
 
26
- 这是实际使用的节点类型,继承了 `TreeNode<T>` 并添加了操作方法:
50
+ `NodeViewOptions` 类处理树节点的视图配置,将显示关注点与数据分离:
27
51
 
28
52
  ```typescript
29
- interface TreeNodeWithMethods<T> extends TreeNode<T> {
30
- /** 添加子节点到当前节点 */
31
- append: (childItem: T) => void;
53
+ class NodeViewOptions<T> {
54
+ /** 唯一标识字段名 */
55
+ keyField: keyof T;
32
56
 
33
- /** 删除当前节点(从父节点中移除) */
34
- remove: () => void;
57
+ /** 显示文字的字段名或函数 */
58
+ textField: keyof T | ((data: T) => string);
35
59
 
36
- /** 替换当前节点的数据 */
37
- replace: (newItem: T) => void;
60
+ /** 可选:图标的字段名或函数 */
61
+ iconField?: keyof T | ((data: T) => string);
38
62
 
39
- /** 将当前节点移动到另一个父节点下 */
40
- moveTo: (newParentId: string) => void;
63
+ /** 可选:CSS 类名的字段名或函数 */
64
+ cssClassField?: keyof T | ((data: T) => string);
41
65
 
42
- /** 删除指定的子节点 */
43
- removeChild: (childId: any) => void;
66
+ /** 可选:内联样式的字段名或函数 */
67
+ styleField?: keyof T | ((data: T) => Record<string, string>);
44
68
 
45
- /** 删除所有子节点 */
46
- removeChildren: () => void;
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;
47
83
  }
48
84
  ```
49
85
 
50
- ## 节点方法详解
86
+ ## TreeNode 方法详解
51
87
 
52
88
  ### 1. append(childItem)
53
89
 
@@ -62,33 +98,31 @@ interface TreeNodeWithMethods<T> extends TreeNode<T> {
62
98
  parentNode.append({
63
99
  id: 123,
64
100
  name: "新子节点",
65
- parent: parentNode.item.id // 可选,会自动设置
101
+ parentId: parentNode.item.id // 会自动设置
66
102
  });
67
103
  ```
68
104
 
69
105
  **注意事项**:
70
- - **自动加载 lazy 节点**:当节点被选中(点击或外部设置 `activeNode`)时,如果是 lazy 节点且未加载(`children === null`),TreeView 会自动触发 `lazyLoader.load()` 加载子节点
71
- - **手动添加子节点**:可以手动调用 `append()` 添加子节点
72
- - 如果节点还未加载,会自动初始化 `children` 数组
73
- - 节点会从 leaf 转为 branch,UI 会显示 branch 图标
106
+ - 自动初始化 children 数组(如果需要)
74
107
  - 如果配置了排序函数,会自动排序
75
108
  - 父节点的 `expand` 会自动设置为 `true`
109
+ - 触发版本更新以实现响应式更新
76
110
 
77
111
  ---
78
112
 
79
- ### 2. remove()
113
+ ### 2. detach()
80
114
 
81
- 删除当前节点(从其父节点中移除)。
115
+ 从父节点中分离当前节点。
82
116
 
83
117
  **参数**: 无
84
118
 
85
119
  **示例**:
86
120
  ```typescript
87
- // 删除节点
88
- node.remove();
121
+ // 从父节点中删除该节点
122
+ node.detach();
89
123
 
90
124
  // 删除后清空引用
91
- activeNode.remove();
125
+ activeNode.detach();
92
126
  activeNode = null;
93
127
  ```
94
128
 
@@ -96,10 +130,40 @@ activeNode = null;
96
130
  - 从父节点的 `children` 数组中移除
97
131
  - 从内部映射表中删除
98
132
  - 如果是根节点,从根节点列表中移除
133
+ - 触发版本更新以实现响应式更新
99
134
 
100
135
  ---
101
136
 
102
- ### 3. replace(newItem)
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)
103
167
 
104
168
  替换当前节点的数据。
105
169
 
@@ -127,32 +191,7 @@ node.replace({
127
191
  - 通常需要保持 `id` 字段不变
128
192
  - 如果配置了排序函数且排序字段改变,会自动重新排序
129
193
  - 会自动触发视图更新
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 加载的子节点会与移动过来的节点合并
194
+ - 保留子节点、展开状态和加载状态
156
195
 
157
196
  ---
158
197
 
@@ -177,6 +216,7 @@ parentNode.removeChild(childId);
177
216
  - 只删除直接子节点,不会递归删除孙子节点
178
217
  - 如果子节点不存在,会静默忽略
179
218
  - 子节点的所有后代也会被删除
219
+ - 触发版本更新以实现响应式更新
180
220
 
181
221
  ---
182
222
 
@@ -200,37 +240,68 @@ parentNode.append(newChildData);
200
240
  - 会删除所有子节点及其后代
201
241
  - 节点本身的 `expand` 状态保持不变
202
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
+ ```
203
273
 
204
274
  ## 完整使用示例
205
275
 
206
276
  ```svelte
207
277
  <script>
208
278
  import TreeView from "@ticatec/uniface-element/TreeView";
209
- import TreeNodes, { type TreeNode } from "@ticatec/uniface-element/TreeNodes";
279
+ import { CommonTreeNodes, type TreeNode } from "@ticatec/uniface-element/lib/TreeNodes";
210
280
 
211
281
  // 数据类型定义
212
282
  interface MyData {
213
283
  id: number;
214
284
  name: string;
215
- parent: number | null;
285
+ parentId: number | null;
216
286
  }
217
287
 
218
- let activeNode: TreeNodeWithMethods<MyData> | null = null;
288
+ let activeNode: TreeNode<MyData> | null = null;
219
289
 
220
290
  // 创建树结构
221
- const treeNodes = new TreeNodes<MyData>({
291
+ const treeNodes = new CommonTreeNodes<MyData>({
222
292
  keyField: 'id',
223
293
  textField: 'name',
224
- parentKeyField: 'parent',
225
- checkIsRoot: (item) => item.parent === null,
226
- checkIsDirectory: (node) => node.children != null
294
+ parentKeyField: 'parentId',
295
+ checkIsRoot: (item) => item.parentId === null,
296
+ checkIsDirectory: (node) => node.children.length > 0,
297
+ expendDepth: 2
227
298
  });
228
299
 
229
300
  // 初始化数据
230
301
  treeNodes.setData([
231
- { id: 1, name: "根节点", parent: null },
232
- { id: 2, name: "子节点1", parent: 1 },
233
- { id: 3, name: "子节点2", parent: 1 }
302
+ { id: 1, name: "根节点", parentId: null },
303
+ { id: 2, name: "子节点1", parentId: 1 },
304
+ { id: 3, name: "子节点2", parentId: 1 }
234
305
  ]);
235
306
 
236
307
  // 添加子节点
@@ -240,14 +311,14 @@ parentNode.append(newChildData);
240
311
  activeNode.append({
241
312
  id: Date.now(),
242
313
  name: "新子节点",
243
- parent: activeNode.item.id
314
+ parentId: activeNode.item.id
244
315
  });
245
316
  }
246
317
 
247
318
  // 删除当前节点
248
319
  function deleteCurrentNode() {
249
320
  if (!activeNode) return;
250
- activeNode.remove();
321
+ activeNode.detach();
251
322
  activeNode = null;
252
323
  }
253
324
 
@@ -275,7 +346,10 @@ parentNode.append(newChildData);
275
346
  // 移动节点
276
347
  function moveNodeTo(newParentId: number) {
277
348
  if (!activeNode) return;
278
- activeNode.moveTo(newParentId);
349
+ const newParent = treeNodes.nodeMap.get(newParentId);
350
+ if (newParent) {
351
+ activeNode.moveTo(newParent);
352
+ }
279
353
  }
280
354
  </script>
281
355
 
@@ -285,7 +359,7 @@ parentNode.append(newChildData);
285
359
  version={treeNodes.version}
286
360
  textField="name"
287
361
  bind:activeNode
288
- checkIsDirectory={(node) => node.children != null}
362
+ checkIsDirectory={(node) => node.children.length > 0}
289
363
  />
290
364
 
291
365
  {#if activeNode}
@@ -303,30 +377,64 @@ parentNode.append(newChildData);
303
377
 
304
378
  ## 与 TreeNodes 类方法的区别
305
379
 
306
- `TreeNode` 的方法和 `TreeNodes` 类的方法有明确的职责分工:
380
+ `TreeNode` 实例方法和 `CommonTreeNodes`/`TreeNodes` 类方法有明确的职责分工:
307
381
 
308
- ### TreeNode 节点方法(推荐用于操作特定节点)
382
+ ### TreeNode 实例方法(用于操作特定节点实例)
309
383
 
310
384
  ```typescript
311
- // 直接在节点上操作 - 推荐用于已知节点实例的场景
385
+ // 直接在节点实例上操作
312
386
  node.append(childItem); // 添加子节点到该节点
313
- node.remove(); // 删除该节点
387
+ node.detach(); // 从父节点中分离该节点
314
388
  node.replace(newItem); // 替换该节点的数据
315
- node.moveTo(newParentId); // 移动该节点
389
+ node.moveTo(newParentNode); // 将该节点移动到另一个父节点
316
390
  node.removeChild(childId); // 删除该节点的指定子节点
317
391
  node.removeChildren(); // 删除该节点的所有子节点
318
392
  ```
319
393
 
320
- ### TreeNodes 类方法(用于批量操作或初始化)
394
+ ### CommonTreeNodes/TreeNodes 类方法(用于树级别的操作)
321
395
 
322
396
  ```typescript
323
- // 类级别操作 - 适合只知道数据的场景
397
+ // 树级别操作
324
398
  treeNodes.setData(data); // 初始化树结构
325
399
  treeNodes.append(item); // 添加新节点(自动找父节点)
326
400
  treeNodes.nodes; // 获取根节点列表
327
401
  treeNodes.version; // 获取版本号
328
- treeNodes.getHierarchyList(); // 获取展开的节点列表
329
- treeNodes.extractDirectories(item); // 提取目录结构
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
+ }
330
438
  ```
331
439
 
332
440
  ## 响应式更新
@@ -359,10 +467,14 @@ const node = treeNodes.nodeMap.get(nodeId);
359
467
  ### Q: 如何遍历子节点?
360
468
 
361
469
  ```typescript
362
- if (node.children) {
363
- node.children.forEach(child => {
364
- console.log(child.item.name);
365
- });
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);
366
478
  }
367
479
  ```
368
480
 
@@ -370,40 +482,24 @@ if (node.children) {
370
482
 
371
483
  ```typescript
372
484
  function findChild(node: TreeNode, childId: number) {
373
- if (!node.children) return null;
374
485
  return node.children.find(child => child.item.id === childId) || null;
375
486
  }
376
487
  ```
377
488
 
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
489
+ ### Q: 可以直接修改 children 数组吗?
394
490
 
395
- // removeChild() 和 removeChildren() 在加载前无效果
396
- lazyNode.removeChild(id); // children 为 null 时无效果
397
- lazyNode.removeChildren(); // children 为 null 时无效果
398
- ```
491
+ 不可以。`children` 属性是只读的。请使用提供的方法:
492
+ - `append()` 添加
493
+ - `removeChild()` 删除特定子节点
494
+ - `removeChildren()` 删除所有子节点
399
495
 
400
496
  ### Q: 如何递归操作所有子孙节点?
401
497
 
402
498
  ```typescript
403
499
  function traverse(node: TreeNode, callback: (node: TreeNode) => void) {
404
500
  callback(node);
405
- if (node.children) {
406
- node.children.forEach(child => traverse(child, callback));
501
+ for (const child of node.children) {
502
+ traverse(child, callback);
407
503
  }
408
504
  }
409
505
 
@@ -414,36 +510,68 @@ traverse(rootNode, (node) => {
414
510
  });
415
511
  ```
416
512
 
513
+ ### Q: TreeNode 和 NodeViewOptions 有什么区别?
514
+
515
+ - **TreeNode**:表示数据结构。每个节点都是具有操作方法的实例。
516
+ - **NodeViewOptions**:配置类,用于定义节点如何显示(文字字段、图标、CSS 等)。
517
+
518
+ 这种分离使得 TreeNode 可以在不同上下文中使用(TreeView、TreeDataGrid 等),而不会与视图特定关注点耦合。
519
+
417
520
  ## 最佳实践
418
521
 
419
522
  1. **优先使用节点方法**:当你有节点实例时,直接调用节点方法更直观
420
523
 
421
524
  2. **保持 ID 不变**:使用 `replace()` 时,确保 `id` 字段保持不变
422
525
 
423
- 3. **检查 children 存在性**:操作子节点前,先检查 `node.children` 是否存在
526
+ 3. **正确使用 moveTo**:`moveTo()` 方法接收 TreeNode 实例,不是 ID:
527
+ ```typescript
528
+ // ❌ 错误
529
+ node.moveTo(123);
424
530
 
425
- 4. **处理 lazy 节点**:对于 lazy-loaded 节点,先等待加载完成再操作
531
+ // 正确
532
+ const parentNode = treeNodes.nodeMap.get(123);
533
+ node.moveTo(parentNode);
534
+ ```
426
535
 
427
- 5. **版本管理**:不需要手动管理版本,节点方法会自动更新
536
+ 4. **版本管理**:不需要手动管理版本,节点方法会自动更新
428
537
 
429
- 6. **类型安全**:使用 TypeScript 泛型确保类型安全
538
+ 5. **类型安全**:使用 TypeScript 泛型确保类型安全
430
539
 
431
540
  ```typescript
432
541
  // ✅ 推荐:使用泛型
433
- const treeNodes = new TreeNodes<MyData>({ ... });
542
+ const treeNodes = new CommonTreeNodes<MyData>({ ... });
434
543
 
435
- // 节点会自动推断类型
544
+ // 节点类型会自动推断
436
545
  node.append({ id: 1, name: "..." }); // 类型检查
437
546
  ```
438
547
 
548
+ 6. **children 是只读的**:不要尝试直接修改 children 数组。始终使用提供的方法。
549
+
439
550
  ## 性能考虑
440
551
 
441
552
  - 所有节点方法的时间复杂度都是 O(1) 或 O(n),其中 n 是子节点数量
442
553
  - 删除操作会同时从内部映射表中移除,保持一致性
443
554
  - 大批量操作时,考虑直接使用 `setData()` 重建树结构
444
555
 
556
+ ## 架构设计
557
+
558
+ ### 与视图解耦
559
+
560
+ TreeNode 设计为独立于任何特定视图组件:
561
+ - TreeNode 中没有视图特定的逻辑
562
+ - NodeViewOptions 单独处理视图配置
563
+ - 可用于 TreeView、TreeDataGrid 或任何其他基于树的组件
564
+
565
+ ### 数据不可变性
566
+
567
+ `item` 属性是只读的,以确保数据完整性:
568
+ - 防止意外修改
569
+ - 通过 `replace()` 方法使状态更改显式化
570
+ - 有助于调试和状态跟踪
571
+
445
572
  ## 相关组件
446
573
 
447
- - `TreeNodes` - 树管理类
574
+ - `CommonTreeNodes` - 基础树管理类
575
+ - `TreeNodes` - 扩展的树管理类,具有层次结构功能
448
576
  - `TreeView` - 树视图组件
449
- - `LazyLoader` - 延迟加载接口
577
+ - `NodeViewOptions` - 视图配置类