topo-engine 0.1.6 → 0.1.8

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.
package/docs/API.md ADDED
@@ -0,0 +1,1358 @@
1
+ # 拓扑引擎 API 参考手册
2
+
3
+ > `topo-engine` v1.0 · 基于 Vue3 + G6 v5 + 自研正交布局引擎
4
+
5
+ ---
6
+
7
+ ## 目录
8
+
9
+ - [快速开始](#快速开始)
10
+ - [类型定义](#类型定义)
11
+ - [一、查询 API](#一查询-api)
12
+ - [二、视图控制](#二视图控制)
13
+ - [三、节点样式](#三节点样式)
14
+ - [四、边样式](#四边样式)
15
+ - [五、高亮与标注](#五高亮与标注)
16
+ - [六、动画](#六动画)
17
+ - [七、文字标注](#七文字标注)
18
+ - [八、卡片盒](#八卡片盒)
19
+ - [九、连接线与拓扑线](#九连接线与拓扑线)
20
+ - [十、事件系统](#十事件系统)
21
+ - [十一、导出](#十一导出)
22
+ - [十二、生命周期](#十二生命周期)
23
+ - [十三、TopoGraph 组件 Props / Events 与计量箱客户交互](#十三topograph-组件-props--events-与计量箱客户交互)
24
+
25
+ ---
26
+
27
+ ## 快速开始
28
+
29
+ ```vue
30
+ <template>
31
+ <TopoGraph ref="topo" data-url="/data/sample.json" />
32
+ </template>
33
+
34
+ <script setup>
35
+ import { ref, onMounted } from 'vue';
36
+ import TopoGraph from './components/TopoGraph.vue';
37
+
38
+ const topo = ref(null);
39
+
40
+ onMounted(async () => {
41
+ // 等待图渲染完成
42
+ await new Promise(r => setTimeout(r, 500));
43
+
44
+ // 查询节点
45
+ const nodes = topo.value.findNodes({ cat: 'transformer' });
46
+
47
+ // 定位到第一个变压器
48
+ if (nodes.length) {
49
+ topo.value.locateNode(nodes[0].id, 1.5);
50
+ }
51
+
52
+ // 高亮 + 标注
53
+ topo.value.highlightNode(nodes[0].id, {
54
+ color: '#FF0000',
55
+ label: '异常',
56
+ labelColor: '#FF0000',
57
+ });
58
+
59
+ // 监听事件
60
+ topo.value.on('node:click', ({ nodeId, node }) => {
61
+ console.log('点击:', node.name);
62
+ });
63
+ });
64
+ </script>
65
+ ```
66
+
67
+ ---
68
+
69
+ ## 类型定义
70
+
71
+ ### NodeData
72
+
73
+ ```ts
74
+ interface NodeData {
75
+ id: string; // 唯一标识(String)
76
+ psrId: string; // PMS PSR 编号
77
+ psrType: string; // PMS 类型码(如 '0302', '3301')
78
+ symbolId: string; // PMS 设备编码
79
+ name: string; // 完整名称
80
+ shortName: string; // 简称
81
+ cat: string; // 设备类别(见下表)
82
+ catLabel: string; // 类别中文名
83
+ status: string; // 状态码('0'=正常)
84
+ insideStation: boolean;// 是否在站房内部
85
+ isLine: boolean; // 是否为导线
86
+ isFuse: boolean; // 是否为熔丝
87
+ }
88
+ ```
89
+
90
+ ### 设备类别(cat)
91
+
92
+ | cat | 中文 | 尺寸 (w×h) |
93
+ |---|---|---|
94
+ | `transformer` | 配电变压器 | 56×26 |
95
+ | `fuse` | 熔丝 | 56×20 |
96
+ | `switch` | 开关 | 60×24 |
97
+ | `bus` | 低压母线 | 40×12 |
98
+ | `cableTerminal` | 电缆终端头 | 18×22 |
99
+ | `stationTerminal` | 站内终端头 | 24×14 |
100
+ | `meterBox` | 计量箱 | 21×21 |
101
+ | `consumer` | 用户接入点 | 30×30 |
102
+ | `line` | 导线/连接点 | 12×12 |
103
+ | `junction` | 密母分接点 | 12×12 |
104
+ | `station` | 低压配电箱(站房框) | 动态 |
105
+ | `other` | 其他 | 20×20 |
106
+
107
+ ### EdgeData
108
+
109
+ ```ts
110
+ interface EdgeData {
111
+ source: string; // 起点节点 ID
112
+ target: string; // 终点节点 ID
113
+ }
114
+ ```
115
+
116
+ ### HighlightOptions
117
+
118
+ ```ts
119
+ interface HighlightOptions {
120
+ color?: string; // 高亮色,默认 '#FFD700'(金色)
121
+ label?: string; // 可选标注文字
122
+ labelColor?: string; // 标注颜色,默认 '#FFFFFF'
123
+ labelSize?: number; // 标注字号,默认 12
124
+ borderColor?: string; // 边框色(覆盖 color)
125
+ borderStyle?: 'solid' | 'dashed'; // 边框样式,默认 'dashed'
126
+ borderWidth?: number; // 边框粗细,默认 2
127
+ }
128
+ ```
129
+
130
+ ### NodeStyle
131
+
132
+ ```ts
133
+ interface NodeStyle {
134
+ stroke?: string; // 描边颜色
135
+ strokeWidth?: number; // 描边粗细
136
+ fill?: string; // 背景填充
137
+ opacity?: number; // 透明度 (0-1)
138
+ visible?: boolean; // 是否可见
139
+ cursor?: string; // 鼠标样式
140
+ }
141
+ ```
142
+
143
+ ### EdgeStyle
144
+
145
+ ```ts
146
+ interface EdgeStyle {
147
+ stroke?: string; // 颜色
148
+ lineWidth?: number; // 线宽
149
+ opacity?: number; // 透明度
150
+ lineDash?: number[]; // 虚线样式
151
+ }
152
+ ```
153
+
154
+ ### PulseOptions
155
+
156
+ ```ts
157
+ interface PulseOptions {
158
+ color?: string; // 脉动颜色,默认 '#00C8FF'
159
+ duration?: number; // 周期 (ms),默认 1500
160
+ maxScale?: number; // 最大缩放,默认 1.3
161
+ minScale?: number; // 最小缩放,默认 1.0
162
+ }
163
+ ```
164
+
165
+ ### FlowOptions
166
+
167
+ ```ts
168
+ interface FlowOptions {
169
+ color?: string; // 流动颜色,默认 '#00C8FF'
170
+ duration?: number; // 周期 (ms),默认 2000
171
+ width?: number; // 线宽,默认 3
172
+ }
173
+ ```
174
+
175
+ ### CardContent
176
+
177
+ ```ts
178
+ interface CardContent {
179
+ title?: string;
180
+ fields?: Array<{ label: string; value: string }>;
181
+ buttons?: Array<{ text: string; onClick: () => void; type?: 'primary' | 'default' }>;
182
+ closable?: boolean; // 是否可关闭,默认 true
183
+ width?: number; // 卡片宽度,默认 240
184
+ }
185
+ ```
186
+
187
+ ### ConnectionOptions
188
+
189
+ ```ts
190
+ interface ConnectionOptions {
191
+ color?: string; // 颜色,默认 '#00C8FF'
192
+ width?: number; // 线宽,默认 2
193
+ style?: 'solid' | 'dashed'; // 样式,默认 'solid'
194
+ midArrow?: boolean; // 沿线简洁方向标记(开口 ›),默认 true
195
+ arrowSize?: number; // 方向标记尺寸,默认 7
196
+ arrowGap?: number; // 标记间距,默认 60(更小=更多标记)
197
+ startArrow?: boolean; // 起点贴边箭头(需显式 true)
198
+ endArrow?: boolean; // 终点贴边箭头(需显式 true)
199
+ label?: string; // 可选标签
200
+ animated?: boolean; // 是否流动动画(场景覆盖线暂未实现)
201
+ }
202
+ ```
203
+
204
+ ---
205
+
206
+ ## 一、查询 API
207
+
208
+ ### `getNode(nodeId)`
209
+
210
+ 获取单个节点完整数据。
211
+
212
+ ```ts
213
+ getNode(nodeId: string): NodeData | null
214
+ ```
215
+
216
+ **参数:**
217
+ - `nodeId` — 节点 ID
218
+
219
+ **返回:** 节点数据对象,未找到返回 `null`
220
+
221
+ **示例:**
222
+ ```js
223
+ const node = topo.getNode('123');
224
+ console.log(node.name, node.catLabel); // "上屋胡2#台区柱上变" "配电变压器"
225
+ ```
226
+
227
+ ---
228
+
229
+ ### `getAllNodes()`
230
+
231
+ 获取所有节点。
232
+
233
+ ```ts
234
+ getAllNodes(): NodeData[]
235
+ ```
236
+
237
+ **返回:** 节点数组(只读副本)
238
+
239
+ ---
240
+
241
+ ### `findNodes(filter)`
242
+
243
+ 按条件查找节点,多条件 AND 组合。
244
+
245
+ ```ts
246
+ findNodes(filter: NodeFilter): NodeData[]
247
+ ```
248
+
249
+ **filter 支持的字段:**
250
+
251
+ | 字段 | 类型 | 匹配方式 |
252
+ |---|---|---|
253
+ | `psrId` | `string` | 精确匹配 |
254
+ | `symbolId` | `string` | 精确匹配 |
255
+ | `cat` | `string` | 精确匹配 |
256
+ | `psrType` | `string` | 精确匹配 |
257
+ | `name` | `string \| RegExp` | 包含 / 正则匹配 |
258
+ | `shortName` | `string \| RegExp` | 包含 / 正则匹配 |
259
+ | `status` | `string` | 精确匹配 |
260
+ | `insideStation` | `boolean` | 精确匹配 |
261
+
262
+ **示例:**
263
+ ```js
264
+ // 查找所有变压器
265
+ const transformers = topo.findNodes({ cat: 'transformer' });
266
+
267
+ // 查找名称含"开关"的设备
268
+ const switches = topo.findNodes({ name: /开关/ });
269
+
270
+ // 组合条件:站房内的正常设备
271
+ const inner = topo.findNodes({ insideStation: true, status: '0' });
272
+ ```
273
+
274
+ ---
275
+
276
+ ### `getSelectedId()`
277
+
278
+ 获取当前选中节点 ID。
279
+
280
+ ```ts
281
+ getSelectedId(): string | null
282
+ ```
283
+
284
+ ---
285
+
286
+ ### `getNeighbors(nodeId)`
287
+
288
+ 获取指定节点的上下游邻居。
289
+
290
+ ```ts
291
+ getNeighbors(nodeId: string): { upstream: NodeData[]; downstream: NodeData[] }
292
+ ```
293
+
294
+ **示例:**
295
+ ```js
296
+ const { upstream, downstream } = topo.getNeighbors('123');
297
+ console.log(`上游 ${upstream.length} 个,下游 ${downstream.length} 个`);
298
+ ```
299
+
300
+ ---
301
+
302
+ ### `getStreamNodes(nodeId, direction)`
303
+
304
+ 沿拓扑流向获取节点链(BFS 遍历)。
305
+
306
+ ```ts
307
+ getStreamNodes(nodeId: string, direction: 'upstream' | 'downstream'): NodeData[]
308
+ ```
309
+
310
+ **参数:**
311
+ - `nodeId` — 起始节点 ID
312
+ - `direction` — `'upstream'`(向上游/电源侧)或 `'downstream'`(向下游/负荷侧)
313
+
314
+ **示例:**
315
+ ```js
316
+ // 获取某节点的所有下游节点
317
+ const downstream = topo.getStreamNodes('123', 'downstream');
318
+ ```
319
+
320
+ ---
321
+
322
+ ### `getTrunkNodes()`
323
+
324
+ 获取主干链节点(从根节点沿叶子数最多路径一直走的链)。
325
+
326
+ ```ts
327
+ getTrunkNodes(): NodeData[]
328
+ ```
329
+
330
+ ---
331
+
332
+ ### `getSubtreeNodes(nodeId)`
333
+
334
+ 获取以指定节点为根的子树中所有节点。
335
+
336
+ ```ts
337
+ getSubtreeNodes(nodeId: string): NodeData[]
338
+ ```
339
+
340
+ ---
341
+
342
+ ### `getPath(fromId, toId)`
343
+
344
+ 获取两个节点之间的拓扑路径。
345
+
346
+ ```ts
347
+ getPath(fromId: string, toId: string): NodeData[] | null
348
+ ```
349
+
350
+ **返回:** 路径节点数组(含首尾),不可达返回 `null`
351
+
352
+ ---
353
+
354
+ ### `getEdge(srcId, tgtId)`
355
+
356
+ 获取指定边。
357
+
358
+ ```ts
359
+ getEdge(srcId: string, tgtId: string): EdgeData | null
360
+ ```
361
+
362
+ ---
363
+
364
+ ### `getEdges()`
365
+
366
+ 获取所有边。
367
+
368
+ ```ts
369
+ getEdges(): EdgeData[]
370
+ ```
371
+
372
+ ---
373
+
374
+ ### `getStreamEdges(nodeId, direction)`
375
+
376
+ 沿流向获取边集合(分路导线着色用)。
377
+
378
+ ```ts
379
+ getStreamEdges(nodeId: string, direction: 'upstream' | 'downstream'): EdgeData[]
380
+ ```
381
+
382
+ ---
383
+
384
+ ### `getMainEdge()`
385
+
386
+ 获取主干边(非鱼骨肋骨、非站房内部、非末端分支)。
387
+
388
+ ```ts
389
+ getMainEdge(): EdgeData[]
390
+ ```
391
+
392
+ ---
393
+
394
+ ### `getNodePosition(nodeId)`
395
+
396
+ 获取节点在画布中的坐标。
397
+
398
+ ```ts
399
+ getNodePosition(nodeId: string): { x: number; y: number } | null
400
+ ```
401
+
402
+ ---
403
+
404
+ ### `getEdgePath(srcId, tgtId)`
405
+
406
+ 获取边的折线控制点序列(含端点)。
407
+
408
+ ```ts
409
+ getEdgePath(srcId: string, tgtId: string): [number, number][] | null
410
+ ```
411
+
412
+ ---
413
+
414
+ ### `getGraphBounds()`
415
+
416
+ 获取图整体边界。
417
+
418
+ ```ts
419
+ getGraphBounds(): { minX: number; minY: number; maxX: number; maxY: number }
420
+ ```
421
+
422
+ ---
423
+
424
+ ### `getStats()`
425
+
426
+ 获取拓扑统计信息。
427
+
428
+ ```ts
429
+ getStats(): {
430
+ total: number; // 节点总数
431
+ depth: number; // 最大深度
432
+ leaves: number; // 叶子节点数
433
+ byCat: Record<string, number>; // 按类别统计
434
+ }
435
+ ```
436
+
437
+ ---
438
+
439
+ ## 二、视图控制
440
+
441
+ ### `fitView(padding?)`
442
+
443
+ 适配画布到视口。
444
+
445
+ ```ts
446
+ fitView(padding?: number): void
447
+ ```
448
+
449
+ **参数:**
450
+ - `padding` — 内边距(px),默认 `30`
451
+
452
+ ---
453
+
454
+ ### `zoomTo(ratio, center?)`
455
+
456
+ 缩放到指定比例。
457
+
458
+ ```ts
459
+ zoomTo(ratio: number, center?: { x: number; y: number }): void
460
+ ```
461
+
462
+ **参数:**
463
+ - `ratio` — 缩放比例(`1` = 100%,`2` = 200%)
464
+ - `center` — 缩放中心(画布坐标),默认视口中心
465
+
466
+ ---
467
+
468
+ ### `zoomIn(step?)`
469
+
470
+ 放大一级。
471
+
472
+ ```ts
473
+ zoomIn(step?: number): void
474
+ ```
475
+
476
+ **参数:**
477
+ - `step` — 放大倍率增量,默认 `0.2`(即放大 20%)
478
+
479
+ ---
480
+
481
+ ### `zoomOut(step?)`
482
+
483
+ 缩小一级。
484
+
485
+ ```ts
486
+ zoomOut(step?: number): void
487
+ ```
488
+
489
+ **参数:**
490
+ - `step` — 缩小倍率减量,默认 `0.2`
491
+
492
+ ---
493
+
494
+ ### `locateNode(nodeId, zoom?)`
495
+
496
+ 定位节点到视口中央,可选缩放。
497
+
498
+ ```ts
499
+ locateNode(nodeId: string, zoom?: number): void
500
+ ```
501
+
502
+ **参数:**
503
+ - `nodeId` — 要定位的节点 ID
504
+ - `zoom` — 可选的目标缩放比例
505
+
506
+ **示例:**
507
+ ```js
508
+ // 定位到节点,保持当前缩放
509
+ topo.locateNode('123');
510
+
511
+ // 定位到节点,放大到 2 倍
512
+ topo.locateNode('123', 2);
513
+ ```
514
+
515
+ ---
516
+
517
+ ### `getViewport()`
518
+
519
+ 获取当前视口状态。
520
+
521
+ ```ts
522
+ getViewport(): { zoom: number; center: { x: number; y: number } }
523
+ ```
524
+
525
+ ---
526
+
527
+ ## 三、节点样式
528
+
529
+ ### `setNodeStyle(nodeId, style)`
530
+
531
+ 设置单个节点的视觉样式。
532
+
533
+ ```ts
534
+ setNodeStyle(nodeId: string, style: NodeStyle): void
535
+ ```
536
+
537
+ **示例:**
538
+ ```js
539
+ // 给节点加红色描边
540
+ topo.setNodeStyle('123', { stroke: '#FF0000', strokeWidth: 2 });
541
+
542
+ // 降低透明度
543
+ topo.setNodeStyle('123', { opacity: 0.3 });
544
+ ```
545
+
546
+ ---
547
+
548
+ ### `batchSetNodeStyle(nodeIds, style)`
549
+
550
+ 批量设置节点样式。
551
+
552
+ ```ts
553
+ batchSetNodeStyle(nodeIds: string[], style: NodeStyle): void
554
+ ```
555
+
556
+ ---
557
+
558
+ ### `resetNodeStyle(nodeId)`
559
+
560
+ 重置单个节点为默认样式。
561
+
562
+ ```ts
563
+ resetNodeStyle(nodeId: string): void
564
+ ```
565
+
566
+ ---
567
+
568
+ ### `resetAllNodeStyles()`
569
+
570
+ 重置所有节点为默认样式。
571
+
572
+ ```ts
573
+ resetAllNodeStyles(): void
574
+ ```
575
+
576
+ ---
577
+
578
+ ### 节点 / 箱内电表户染色(psrId / 资产编号)
579
+
580
+ > 给图元“换颜色”。引用规则与 `addConnection` 完全一致:
581
+ > - **普通节点** —— 节点内部 id 或 **`psrId`(设备编码)**:变压器 / 开关 / 熔丝 /
582
+ > 导线连接点 / 母线 / 用户接入点 / 计量箱本体均可;
583
+ > - **箱内电表户** —— 客户档案里的 **`assetNo`(资产编号)**(兼容 `consNo`/`consId`):
584
+ > 命中计量箱内该户的**电表图标 + 户名**(单户箱即整箱那一只表;多户箱只染该格)。
585
+ >
586
+ > 渲染效果:
587
+ > - 普通节点为**整图单色换装**——主体 / 引线 / 描边全部换成指定色,
588
+ > 白字与浅色细节(开关 Kxx 标签、内高光、装饰条)保留以保证可读;
589
+ > - 箱内电表户染该户表体 / 显示窗 / 脉冲点与户名颜色;悬停(青)与选中(金)优先级更高,
590
+ > 取消悬停 / 选中后自动回到所染颜色。
591
+ >
592
+ > 染色记录保存在 API 内部,**主题切换 / 数据重渲染后自动保留并重放**(同一次会话内
593
+ > 无需重新调用;`resetNodeColor` / `resetAllNodeColors` 可清除)。
594
+
595
+ #### `setNodeColor(ref, color)`
596
+
597
+ 按 **节点 psrId / id** 或 **用户 assetNo(资产编号)** 染色。
598
+
599
+ ```ts
600
+ setNodeColor(ref: string, color: string | null): boolean
601
+ // color 传 null / '' / undefined 等价于清除该处染色(回到主题默认配色)
602
+ ```
603
+
604
+ **示例:**
605
+ ```js
606
+ // 1) 普通节点:按 psrId(设备编码)把变压器染红
607
+ topo.setNodeColor('8ff4786d548a7073a2517297e801518ff456732f4d', '#FF0000');
608
+
609
+ // 2) 电表户:按资产编号(assetNo)把某户电表染橙
610
+ topo.setNodeColor('4230010007008084768', '#FF6600');
611
+
612
+ // 3) 也支持节点内部 id
613
+ topo.setNodeColor('5', '#00FF00');
614
+
615
+ // 4) 清除染色(回到主题默认色)
616
+ topo.setNodeColor('4230010007008084768', null);
617
+ ```
618
+
619
+ ---
620
+
621
+ #### `batchSetNodeColor(entries, color?)`
622
+
623
+ 批量染色(如后端返回「psrId → 状态色」「assetNo → 状态色」列表时一次性下发)。
624
+ 三种入参形式:
625
+
626
+ ```ts
627
+ batchSetNodeColor(entries: any, color?: string): {
628
+ total: number;
629
+ ok: number;
630
+ failed: Array<{ ref: string, reason: string }>;
631
+ }
632
+ ```
633
+
634
+ **示例:**
635
+ ```js
636
+ // 形式 1:[ref, color] 数组
637
+ topo.batchSetNodeColor([
638
+ ['8ff4786d548a7073a2517297e801518ff456732f4d', '#FF0000'],
639
+ ['4230010007008084768', '#FF6600'],
640
+ ]);
641
+
642
+ // 形式 2:对象数组(color 缺省时用第 2 参)
643
+ topo.batchSetNodeColor([
644
+ { psrId: '8ff4…', color: '#FF0000' },
645
+ { assetNo: '4230…' },
646
+ ], '#FF6600');
647
+
648
+ // 形式 3:ref → color 对象
649
+ topo.batchSetNodeColor({
650
+ '8ff4786d548a7073a2517297e801518ff456732f4d': '#FF0000',
651
+ '4230010007008084768': '#FF6600',
652
+ });
653
+ ```
654
+
655
+ ---
656
+
657
+ #### `resetNodeColor(ref)`
658
+
659
+ 清除指定节点 / 电表户的染色。
660
+
661
+ ```ts
662
+ resetNodeColor(ref: string): boolean
663
+ ```
664
+
665
+ ---
666
+
667
+ #### `resetAllNodeColors()`
668
+
669
+ 清除全部染色(所有节点与电表户回到主题默认配色)。
670
+
671
+ ```ts
672
+ resetAllNodeColors(): void
673
+ ```
674
+
675
+ ---
676
+
677
+ #### `getNodeColor(ref)`
678
+
679
+ 查询某处当前生效的染色(未命中返回 `null`)。
680
+
681
+ ```ts
682
+ getNodeColor(ref: string):
683
+ | { kind: 'node', id: string, color: string | null }
684
+ | { kind: 'customer', id: string, boxId: string, index: number, color: string | null }
685
+ | null
686
+ ```
687
+
688
+ ---
689
+
690
+ ## 四、边样式
691
+
692
+ ### `setEdgeStyle(srcId, tgtId, style)`
693
+
694
+ 设置边的视觉样式。
695
+
696
+ ```ts
697
+ setEdgeStyle(srcId: string, tgtId: string, style: EdgeStyle): void
698
+ ```
699
+
700
+ **示例:**
701
+ ```js
702
+ // 导线变红加粗
703
+ topo.setEdgeStyle('A', 'B', { stroke: '#FF0000', lineWidth: 3 });
704
+
705
+ // 虚线
706
+ topo.setEdgeStyle('A', 'B', { lineDash: [5, 3] });
707
+ ```
708
+
709
+ ---
710
+
711
+ ### `batchSetEdgeStyle(edgeIds, style)`
712
+
713
+ 批量设置边样式。
714
+
715
+ ```ts
716
+ batchSetEdgeStyle(edgeIds: Array<[string, string]>, style: EdgeStyle): void
717
+ ```
718
+
719
+ ---
720
+
721
+ ### `resetEdgeStyle(srcId, tgtId)`
722
+
723
+ 重置边为默认样式。
724
+
725
+ ```ts
726
+ resetEdgeStyle(srcId: string, tgtId: string): void
727
+ ```
728
+
729
+ ---
730
+
731
+ ## 五、高亮与标注
732
+
733
+ ### `highlightNode(nodeId, options?)`
734
+
735
+ 高亮节点(选中效果 + 可选文字标注)。
736
+
737
+ ```ts
738
+ highlightNode(nodeId: string, options?: HighlightOptions): void
739
+ ```
740
+
741
+ **示例:**
742
+ ```js
743
+ // 金色高亮
744
+ topo.highlightNode('123');
745
+
746
+ // 红色高亮 + "停电" 标注
747
+ topo.highlightNode('123', {
748
+ color: '#FF0000',
749
+ label: '停电',
750
+ labelColor: '#FF0000',
751
+ });
752
+
753
+ // 绿色虚线高亮 + "正常" 标注
754
+ topo.highlightNode('123', {
755
+ color: '#00FF00',
756
+ label: '正常',
757
+ borderStyle: 'dashed',
758
+ });
759
+ ```
760
+
761
+ ---
762
+
763
+ ### `unhighlightNode(nodeId)`
764
+
765
+ 取消节点高亮并移除标注。
766
+
767
+ ```ts
768
+ unhighlightNode(nodeId: string): void
769
+ ```
770
+
771
+ ---
772
+
773
+ ### `highlightPath(nodeIds, options?)`
774
+
775
+ 高亮一组节点和它们之间的边(路径高亮)。
776
+
777
+ ```ts
778
+ highlightPath(nodeIds: string[], options?: HighlightOptions & { edgeColor?: string }): void
779
+ ```
780
+
781
+ **示例:**
782
+ ```js
783
+ // 高亮从电源到某用户的路径
784
+ const path = topo.getPath('root', 'user123');
785
+ if (path) {
786
+ topo.highlightPath(path.map(n => n.id), { color: '#00FF00', label: '供电路径' });
787
+ }
788
+ ```
789
+
790
+ ---
791
+
792
+ ### `unhighlightAll()`
793
+
794
+ 取消所有高亮和标注。
795
+
796
+ ```ts
797
+ unhighlightAll(): void
798
+ ```
799
+
800
+ ---
801
+
802
+ ### `dimOthers(keepIds, opacity?)`
803
+
804
+ 将非指定节点变暗(聚焦效果)。
805
+
806
+ ```ts
807
+ dimOthers(keepIds: string[], opacity?: number): void
808
+ ```
809
+
810
+ **参数:**
811
+ - `keepIds` — 保持正常的节点 ID 列表
812
+ - `opacity` — 变暗透明度,默认 `0.15`
813
+
814
+ **示例:**
815
+ ```js
816
+ // 聚焦某节点及其上下游
817
+ const { upstream, downstream } = topo.getNeighbors('123');
818
+ const keep = ['123', ...upstream.map(n => n.id), ...downstream.map(n => n.id)];
819
+ topo.dimOthers(keep, 0.15);
820
+ ```
821
+
822
+ ---
823
+
824
+ ## 六、动画
825
+
826
+ ### `setNodePulse(nodeId, options?)`
827
+
828
+ 节点脉动/光晕动画。
829
+
830
+ ```ts
831
+ setNodePulse(nodeId: string, options?: PulseOptions): void
832
+ ```
833
+
834
+ **示例:**
835
+ ```js
836
+ topo.setNodePulse('123', { color: '#FF0000', duration: 1500 });
837
+ ```
838
+
839
+ ---
840
+
841
+ ### `setNodeGlow(nodeId, options?)`
842
+
843
+ 节点发光效果。
844
+
845
+ ```ts
846
+ setNodeGlow(nodeId: string, options?: { color?: string; radius?: number }): void
847
+ ```
848
+
849
+ ---
850
+
851
+ ### `setEdgeFlow(srcId, tgtId, options?)`
852
+
853
+ 边流动动画(视觉流动效果)。
854
+
855
+ ```ts
856
+ setEdgeFlow(srcId: string, tgtId: string, options?: FlowOptions): void
857
+ ```
858
+
859
+ **示例:**
860
+ ```js
861
+ topo.setEdgeFlow('A', 'B', { color: '#00C8FF', duration: 2000 });
862
+ ```
863
+
864
+ ---
865
+
866
+ ### `setEdgePulse(srcId, tgtId, options?)`
867
+
868
+ 边脉动动画(线宽周期变化)。
869
+
870
+ ```ts
871
+ setEdgePulse(srcId: string, tgtId: string, options?: { color?: string; duration?: number }): void
872
+ ```
873
+
874
+ ---
875
+
876
+ ### `removeAnimation(targetId)`
877
+
878
+ 移除指定元素的动画。
879
+
880
+ ```ts
881
+ removeAnimation(targetId: string): void
882
+ ```
883
+
884
+ ---
885
+
886
+ ### `removeAllAnimations()`
887
+
888
+ 移除所有动画。
889
+
890
+ ```ts
891
+ removeAllAnimations(): void
892
+ ```
893
+
894
+ ---
895
+
896
+ ## 七、文字标注
897
+
898
+ ### `setText(nodeId, text, options?)`
899
+
900
+ 在节点旁设置附加文字。
901
+
902
+ ```ts
903
+ setText(nodeId: string, text: string, options?: {
904
+ fontSize?: number; // 默认 12
905
+ color?: string; // 默认 '#FFFFFF'
906
+ position?: 'top' | 'bottom' | 'left' | 'right'; // 默认 'top'
907
+ offset?: number; // 偏移量 (px),默认 4
908
+ }): void
909
+ ```
910
+
911
+ **示例:**
912
+ ```js
913
+ topo.setText('123', '突变(3次)', { color: '#FF4444', position: 'top' });
914
+ ```
915
+
916
+ ---
917
+
918
+ ### `removeText(nodeId)`
919
+
920
+ 移除指定节点的附加文字。
921
+
922
+ ```ts
923
+ removeText(nodeId: string): void
924
+ ```
925
+
926
+ ---
927
+
928
+ ### `removeAllTexts()`
929
+
930
+ 移除所有附加文字。
931
+
932
+ ```ts
933
+ removeAllTexts(): void
934
+ ```
935
+
936
+ ---
937
+
938
+ ### `setDeviceText(cat, fontSize)`
939
+
940
+ 按设备类型统一设置文字字号。
941
+
942
+ ```ts
943
+ setDeviceText(cat: string, fontSize: number): void
944
+ ```
945
+
946
+ **示例:**
947
+ ```js
948
+ // 所有用户接入点字号设为 15
949
+ topo.setDeviceText('consumer', 15);
950
+ ```
951
+
952
+ ---
953
+
954
+ ### `changeUserName(mode)`
955
+
956
+ 切换所有用户名的显示方式。
957
+
958
+ ```ts
959
+ changeUserName(mode: 'full' | 'short' | 'hidden'): void
960
+ ```
961
+
962
+ **参数:**
963
+ - `'full'` — 显示完整名称
964
+ - `'short'` — 显示简称
965
+ - `'hidden'` — 隐藏名称
966
+
967
+ ---
968
+
969
+ ## 八、卡片盒
970
+
971
+ ### `showCard(nodeId, content)`
972
+
973
+ 在节点旁弹出信息卡片。
974
+
975
+ ```ts
976
+ showCard(nodeId: string, content: CardContent): void
977
+ ```
978
+
979
+ **示例:**
980
+ ```js
981
+ topo.showCard('123', {
982
+ title: '设备信息',
983
+ fields: [
984
+ { label: '设备类型', value: '配电变压器' },
985
+ { label: 'PSR编号', value: 'GDDW000123' },
986
+ { label: '状态', value: '正常' },
987
+ ],
988
+ buttons: [
989
+ { text: '查看运行曲线', type: 'primary', onClick: () => openChart('123') },
990
+ { text: '查看量测曲线', onClick: () => openMeasure('123') },
991
+ ],
992
+ });
993
+ ```
994
+
995
+ ---
996
+
997
+ ### `hideCard(nodeId)`
998
+
999
+ 关闭指定节点的卡片。
1000
+
1001
+ ```ts
1002
+ hideCard(nodeId: string): void
1003
+ ```
1004
+
1005
+ ---
1006
+
1007
+ ### `hideAllCards()`
1008
+
1009
+ 关闭所有卡片。
1010
+
1011
+ ```ts
1012
+ hideAllCards(): void
1013
+ ```
1014
+
1015
+ ---
1016
+
1017
+ ## 九、连接线与拓扑线
1018
+
1019
+ ### `addConnection(fromId, toId, options?)`
1020
+
1021
+ 添加动态连接线(不参与布局/拓扑查询,直接绘制进 **G6 画布场景最上层**,
1022
+ 随画布缩放平移自动对齐)。默认**中心对中心直线 + 沿线若干简洁方向标记**
1023
+ (开口 › 形、数量随线长自动增减、非实心,样式干净)。
1024
+
1025
+ **端点支持两种引用:**
1026
+ 1. **普通设备节点** —— 用该节点的 **`psrId`(设备编码)**(兼容节点内部 id);
1027
+ 变压器/开关/计量箱/用户接入点等均适用;
1028
+ 2. **箱内用户** —— 用客户档案里的 **`assetNo`(资产编号)**(兼容 `consNo`/`consId`),
1029
+ 自动落到该户表位中心 → 支持 **节点↔用户、用户↔用户**(可跨箱、可同箱内两户)。
1030
+
1031
+ ```ts
1032
+ addConnection(fromId: string, toId: string, options?: ConnectionOptions): string | false
1033
+ // options: { color?, width?, style?: 'solid'|'dashed',
1034
+ // midArrow?: boolean, // 沿线方向标记,默认 true
1035
+ // arrowSize?: number, // 标记尺寸,默认 7
1036
+ // arrowGap?: number, // 标记间距,默认 60
1037
+ // startArrow?: boolean, endArrow?: boolean, // 显式 true 才画贴边两端箭头
1038
+ // }
1039
+ ```
1040
+
1041
+ **示例:**
1042
+ ```js
1043
+ // 节点(psrId)→ 某户(assetNo),橙色虚线 + 沿线方向标记(默认)
1044
+ topo.addConnection('4241005000000040735831', '4230000100014444282', {
1045
+ color: '#FF6600', width: 2, style: 'dashed',
1046
+ });
1047
+
1048
+ // 户 ↔ 户(assetNo),方向标记更密更大
1049
+ topo.addConnection('4230000100014444282', '4230000100014410530', {
1050
+ color: '#00C8FF', arrowGap: 40, arrowSize: 9,
1051
+ });
1052
+
1053
+ // 纯直线:midArrow: false
1054
+ topo.addConnection('4241005000000040735831', '4230000100014444282', { midArrow: false });
1055
+
1056
+ // 想要贴边两端实心箭头:startArrow: true / endArrow: true
1057
+ topo.addConnection('4241005000000040735831', '4230000100014444282', {
1058
+ startArrow: true, endArrow: true,
1059
+ });
1060
+
1061
+ // 解析调试:topo.resolveUserRef('4230000100014444282') → `${计量箱节点id}#cust${下标}`(无匹配为 null)
1062
+ ```
1063
+
1064
+ ---
1065
+
1066
+ ### `removeConnection(fromId, toId)`
1067
+
1068
+ 移除动态连接线。
1069
+
1070
+ ```ts
1071
+ removeConnection(fromId: string, toId: string): void
1072
+ ```
1073
+
1074
+ ---
1075
+
1076
+ ### `removeAllConnections()`
1077
+
1078
+ 移除所有动态连接线。
1079
+
1080
+ ```ts
1081
+ removeAllConnections(): void
1082
+ ```
1083
+
1084
+ ---
1085
+
1086
+ ### `addTopoLine(nodeIds, options?)`
1087
+
1088
+ 沿节点路径添加拓扑高亮线(高亮已有边)。
1089
+
1090
+ ```ts
1091
+ addTopoLine(nodeIds: string[], options?: {
1092
+ color?: string; // 高亮色,默认 '#00C8FF'
1093
+ width?: number; // 线宽,默认 3
1094
+ id?: string; // 线 ID(用于后续移除),默认自动生成
1095
+ }): string // 返回线 ID
1096
+ ```
1097
+
1098
+ **示例:**
1099
+ ```js
1100
+ // 高亮从变压器到用户的路径
1101
+ const path = topo.getPath('transformer1', 'user456');
1102
+ const lineId = topo.addTopoLine(path.map(n => n.id), { color: '#FF0000' });
1103
+ ```
1104
+
1105
+ ---
1106
+
1107
+ ### `removeTopoLine(id)`
1108
+
1109
+ 移除拓扑高亮线。
1110
+
1111
+ ```ts
1112
+ removeTopoLine(id: string): void
1113
+ ```
1114
+
1115
+ ---
1116
+
1117
+ ### `removeAllTopoLines()`
1118
+
1119
+ 移除所有拓扑高亮线。
1120
+
1121
+ ```ts
1122
+ removeAllTopoLines(): void
1123
+ ```
1124
+
1125
+ ---
1126
+
1127
+ ## 十、事件系统
1128
+
1129
+ ### `on(event, handler)`
1130
+
1131
+ 监听事件。
1132
+
1133
+ ```ts
1134
+ on(event: string, handler: Function): void
1135
+ ```
1136
+
1137
+ ---
1138
+
1139
+ ### `off(event, handler?)`
1140
+
1141
+ 移除监听。不传 handler 则移除该事件的所有监听。
1142
+
1143
+ ```ts
1144
+ off(event: string, handler?: Function): void
1145
+ ```
1146
+
1147
+ ---
1148
+
1149
+ ### 标准事件列表
1150
+
1151
+ | 事件名 | 触发时机 | Payload |
1152
+ |---|---|---|
1153
+ | `node:click` | 单击节点 | `{ nodeId, node, event }` |
1154
+ | `node:dblclick` | 双击节点 | `{ nodeId, node, event }` |
1155
+ | `node:hover` | 鼠标进入节点 | `{ nodeId, node }` |
1156
+ | `node:unhover` | 鼠标离开节点 | `{ nodeId }` |
1157
+ | `edge:click` | 单击边 | `{ edgeId, source, target }` |
1158
+ | `canvas:click` | 单击空白区域 | `{ event }` |
1159
+ | `viewport:change` | 缩放/平移后 | `{ zoom, center }` |
1160
+ | `card:button` | 点击卡片按钮 | `{ nodeId, buttonIndex }` |
1161
+ | `consumer:click` | 点击计量箱内用户(多户箱点格子;单户箱整箱即该户) | `{ nodeId, index, customer, node }` |
1162
+
1163
+ > `consumer:click`:点中计量箱内的用户时触发——
1164
+ > 多用户箱(`_meterBoxMode='grid'`)点中箱内具体用户电表格;
1165
+ > 单用户箱(`_meterBoxMode='single'`)整箱即“这一个用户”,点击即触发。
1166
+ > `index` 为该用户在箱内客户列表 `consumerList` 中的下标;`customer` 为该客户档案
1167
+ > (含 `consId/consNo/consName/meterId/assetNo/realConsName` 等字段,视数据源而定)。
1168
+ > 多用户箱点击空白/外框仍按普通 `node:click` 选中整只计量箱。
1169
+
1170
+ **示例:**
1171
+ ```js
1172
+ // 节点点击 → 弹卡片
1173
+ topo.on('node:click', ({ nodeId, node }) => {
1174
+ topo.showCard(nodeId, {
1175
+ title: node.name,
1176
+ fields: [
1177
+ { label: '类型', value: node.catLabel },
1178
+ { label: '编号', value: node.psrId },
1179
+ ],
1180
+ });
1181
+ });
1182
+
1183
+ // 鼠标悬停 → 高亮上下游
1184
+ topo.on('node:hover', ({ nodeId }) => {
1185
+ const { upstream, downstream } = topo.getNeighbors(nodeId);
1186
+ const keep = [nodeId, ...upstream.map(n => n.id), ...downstream.map(n => n.id)];
1187
+ topo.dimOthers(keep);
1188
+ });
1189
+
1190
+ topo.on('node:unhover', () => {
1191
+ topo.resetAllNodeStyles();
1192
+ });
1193
+
1194
+ // 点击空白 → 清除所有
1195
+ topo.on('canvas:click', () => {
1196
+ topo.unhighlightAll();
1197
+ topo.hideAllCards();
1198
+ topo.resetAllNodeStyles();
1199
+ });
1200
+
1201
+ // 多用户计量箱:点中箱内单个用户 → 打印客户档案
1202
+ topo.on('consumer:click', ({ nodeId, index, customer, node }) => {
1203
+ console.log(`箱 ${node.name} 内第 ${index + 1} 户`, customer);
1204
+ });
1205
+ // 也可编程式选中:topo.selectCustomer(nodeId, index)
1206
+ // 或查询当前选中用户:topo.getSelectedCustomer() → { nodeId, index, customer, node } | null
1207
+ ```
1208
+
1209
+ ---
1210
+
1211
+ ## 十一、导出
1212
+
1213
+ ### `exportPng(filename?)`
1214
+
1215
+ 导出整图为 PNG 文件并触发下载。
1216
+
1217
+ ```ts
1218
+ exportPng(filename?: string): void
1219
+ ```
1220
+
1221
+ **参数:**
1222
+ - `filename` — 文件名(不含扩展名),默认使用图标题
1223
+
1224
+ ---
1225
+
1226
+ ### `toDataURL(options?)`
1227
+
1228
+ 获取图片 DataURL(不触发下载)。
1229
+
1230
+ ```ts
1231
+ toDataURL(options?: {
1232
+ mode?: 'overall' | 'viewport'; // 默认 'overall'(整图)
1233
+ type?: 'image/png' | 'image/jpeg'; // 默认 'image/png'
1234
+ quality?: number; // JPEG 质量 (0-1)
1235
+ }): Promise<string>
1236
+ ```
1237
+
1238
+ ---
1239
+
1240
+ ### `getGraphData()`
1241
+
1242
+ 获取当前图的完整数据(可序列化为 JSON)。
1243
+
1244
+ ```ts
1245
+ getGraphData(): {
1246
+ model: object; // 解析后的模型数据
1247
+ layout: {
1248
+ pos: Record<string, { x: number; y: number }>;
1249
+ bounds: object;
1250
+ };
1251
+ }
1252
+ ```
1253
+
1254
+ ---
1255
+
1256
+ ## 十二、生命周期
1257
+
1258
+ ### `loadRaw(text)`
1259
+
1260
+ 加载本地 JSON 文本并重新渲染。
1261
+
1262
+ ```ts
1263
+ loadRaw(text: string): void
1264
+ ```
1265
+
1266
+ ---
1267
+
1268
+ ### `destroy()`
1269
+
1270
+ 销毁图实例,释放资源。组件卸载时自动调用。
1271
+
1272
+ ```ts
1273
+ destroy(): void
1274
+ ```
1275
+
1276
+ ---
1277
+
1278
+ ## 十三、TopoGraph 组件 Props / Events 与计量箱客户交互
1279
+
1280
+ > 前面章节的 API 均通过 `TopoGraph` 组件 ref 直接调用(组件把 `TopoApi` 全部方法代理到自身)。
1281
+
1282
+ ### Props
1283
+
1284
+ | prop | 类型 | 默认 | 说明 |
1285
+ |---|---|---|---|
1286
+ | `data` | `Object` | `null` | 直接传入台区 JSON(优先于 `dataUrl`) |
1287
+ | `dataUrl` | `String` | `''` | 数据地址,内部 `fetch` 加载 |
1288
+ | `customerData` | `Array` | `null` | 客户数据(计量箱关联用户),见下 |
1289
+ | `theme` | `'dark' \| 'light'` | `'light'` | 画布/图元/导线主题,支持 `v-model:theme` |
1290
+ | `toolbar` | `Boolean` | `false` | 是否显示左上工具条(标题/统计/主题/适应画布/导出 PNG),默认隐藏 |
1291
+
1292
+ ### Events(Vue 组件事件)
1293
+
1294
+ | 事件 | 触发时机 | payload |
1295
+ |---|---|---|
1296
+ | `node-select` | 点击节点 / 空白清除 | `{ node, upstream, downstream }`(空白处 `node=null`) |
1297
+ | `customer-select` | 点中计量箱内某个用户 | `{ node, customer, index, count, upstream, downstream }` |
1298
+ | `loaded` | 渲染完成 | `{ stats, title }` |
1299
+ | `top-distances` | Top10 距离计算完成 | `Array` |
1300
+ | `update:theme` | 点击主题切换 | 新主题值 |
1301
+
1302
+ ### 组件 ref 上的快捷方法
1303
+
1304
+ `resetView()`、`exportPng()`、`selectNode(id)`、`selectCustomer(nodeId, index)`、
1305
+ `getSelectedCustomer()`(`{ nodeId, index, customer, node } \| null`);
1306
+ 其余 `TopoApi` 方法(查询/样式/高亮/动画/卡片/连接线…)全部直接可用。
1307
+
1308
+ ### 计量箱客户数据 → 渲染与交互
1309
+
1310
+ 通过 `:customer-data` 传入,格式为数组:每项 `{ psrId, consumerList: [...] }`,
1311
+ `psrId` 对应台区 JSON 中计量箱节点的 PSR 编号:
1312
+
1313
+ ```json
1314
+ [{
1315
+ "psrId": "4230000000109100079",
1316
+ "consumerList": [
1317
+ { "assetNo": "4230010007008084768", "consNo": "4219930230186",
1318
+ "consId": "4226040301216283", "meterId": "4225112000503099", "realConsName": "郭勋豪" }
1319
+ ]
1320
+ }]
1321
+ ```
1322
+
1323
+ - **渲染形态**:
1324
+ - 单户(1 户)→ 电表图标 + 户名(整箱即“这一个用户”);
1325
+ - 多户(2 户起)→ 外框内按网格排电表,每格 = 1 户(≤20 户显示户名,超过只显电表);
1326
+ - 无客户数据 → 保持原始 `JX` 图元。
1327
+ - **点击语义**:
1328
+ - 单户箱:点击整箱任意位置 = 点中该户;
1329
+ - 多户箱:点某个电表格/户名 = 点中该户;点箱体空白/外框 = 选中整箱;
1330
+ - 悬停某户青圈提示,选中金圈高亮。
1331
+ - **编程式选中 / 事件**:`selectCustomer(nodeId, index)`、`getSelectedCustomer()`、
1332
+ 事件 `consumer:click`(见第十章)与组件事件 `customer-select`。
1333
+ - **用户级连线**:连接线的端点可用客户档案 `assetNo`(兼容 `consNo`/`consId`)直接引用(见第九章),
1334
+ 连线终点会自动落到该户表位。
1335
+
1336
+ ---
1337
+
1338
+ ## 附录:G6 v5 底层实例访问
1339
+
1340
+ 如需使用 G6 v5 原生 API(上述封装未覆盖的能力),可通过以下方式获取底层实例:
1341
+
1342
+ ```ts
1343
+ getGraphInstance(): Graph | null
1344
+ ```
1345
+
1346
+ > ⚠️ **注意:** 直接操作 G6 实例可能导致与引擎内部状态不一致,建议优先使用上述封装 API。
1347
+
1348
+ ---
1349
+
1350
+ ## 附录:引擎数据访问
1351
+
1352
+ 如需直接访问引擎内部数据结构:
1353
+
1354
+ ```ts
1355
+ getModel(): object // parseTopo 输出的完整模型
1356
+ getLayoutData(): object // computeOrthogonalLayout 输出的完整布局数据
1357
+ getNodeByIdMap(): Map // ID → NodeData 的 O(1) 查找表
1358
+ ```