topo-engine 0.1.2 → 0.1.4

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/README.md CHANGED
@@ -1,366 +1,402 @@
1
- # topo-engine
2
-
3
- 配电台区(低压配电网)**单线图拓扑成图引擎**,基于 **Vue 3 + AntV G6 v5**。
4
-
5
- 输入一份台区拓扑 JSON,自动完成「解析 → 建树找根 → **横平竖直、0 交叉** 的正交布局 → 深色 SCADA 风格渲染」,并提供开箱即用的 Vue 组件与一整套拓扑查询 / 样式 / 动画 / 标注控制 API。
6
-
7
- ## 功能特性
8
-
9
- - 🧩 **Vue 3 组件** — `TopoGraph`(画布,含标题栏 / 统计 / 适应画布 / 导出 PNG 按钮)、`SidePanel`(设备详情与上下游)、`DistanceTop10`(父子连线距离 Top10 面板)
10
- - 📐 **自研正交布局**主干沿最长路径横向铺开 + 分支带垂直直走,整图**横平竖直、0 交叉、0 重叠**,边缘路由为直线 / L 形折线
11
- - 🔍 **拓扑查询 API** 节点 / / 邻居 / 子树 / 主干链 / 路径 / 坐标 查询
12
- - 🎨 **样式控制**节点、边样式覆盖与批量重置
13
- - **动画与高亮**节点脉动、边流动、路径高亮、其余变暗、异常标注
14
- - 📝 **文字标注**附加文字、用户名 full / short / hidden 切换
15
- - 🃏 **卡片盒**节点信息卡片(字段 + 按钮),适配"运行曲线/工单跳转"类场景
16
- - 🔗 **连接线 / 拓扑线** 任意两节点动态连接线、路径拓扑高亮线
17
- - 🎧 **事件系统** — 点击 / 悬停 / 画布空白 / 视口变化事件总线
18
- - 💾 **导出**整图 PNG 下载、DataURL、图数据 JSON
19
- - 📦 **零配置渲染**组件自带深色主题与工具栏,`data` `dataUrl` 二选一即可出图
20
-
21
- ## 安装
22
-
23
- ```bash
24
- npm install topo-engine
25
- # yarn add topo-engine / pnpm add topo-engine
26
- ```
27
-
28
- - 环境要求:**Vue 3.4+**(peerDependency);`@antv/g6` v5 已作为依赖随包安装,无需手动配置。
29
- - 组件深色样式不自动注入,使用时需引入样式文件:
30
-
31
- ```js
32
- import 'topo-engine/style.css';
33
- ```
34
-
35
- ## 快速开始
36
-
37
- ### 1. 最小示例 —— 只画一张图
38
-
39
- ```vue
40
- <script setup>
41
- import { ref } from 'vue';
42
- import { TopoGraph } from 'topo-engine';
43
- import 'topo-engine/style.css';
44
-
45
- // 两种数据来源任选其一:
46
- // - dataUrl:内部用 fetch 加载(注意放 public/ 下或跨域可达)
47
- // - data :直接传入解析好的 JSON 对象(优先级高于 dataUrl)
48
- const dataUrl = ref('/data/topology.json');
49
-
50
- const onLoaded = ({ title, stats }) => {
51
- console.log('加载完成', title, stats); // { total, depth, leaves, byCat }
52
- };
53
- </script>
54
-
55
- <template>
56
- <!-- 组件撑满父容器,父容器必须有明确高度 -->
57
- <div class="page">
58
- <TopoGraph :data-url="dataUrl" @loaded="onLoaded" />
59
- </div>
60
- </template>
61
-
62
- <style scoped>
63
- .page { height: 640px; }
64
- </style>
65
- ```
66
-
67
- ### 2. 组合示例 —— 图 + 详情面板 + Top10 距离面板
68
-
69
- ```vue
70
- <script setup>
71
- import { ref } from 'vue';
72
- import { TopoGraph, SidePanel, DistanceTop10 } from 'topo-engine';
73
- import 'topo-engine/style.css';
74
-
75
- const topo = ref(null); // ref → 调用完整控制 API
76
- const graphData = ref(null); // 直接传数据对象
77
- const panelOpen = ref(false);
78
- const selected = ref(null);
79
- const upstream = ref([]);
80
- const downstream = ref([]);
81
- const stats = ref({});
82
- const distances = ref([]);
83
-
84
- function onSelect({ node, upstream: up, downstream: down }) {
85
- selected.value = node;
86
- upstream.value = up;
87
- downstream.value = down;
88
- panelOpen.value = true;
89
- }
90
- function onTopDistances(list) { distances.value = list; }
91
- function jumpTo(id) { topo.value?.selectNode(id); }
92
- </script>
93
-
94
- <template>
95
- <div class="app">
96
- <div class="graph-area">
97
- <TopoGraph
98
- ref="topo"
99
- :data="graphData"
100
- @node-select="onSelect"
101
- @top-distances="onTopDistances"
102
- />
103
- <SidePanel
104
- v-if="panelOpen"
105
- :node="selected"
106
- :upstream="upstream"
107
- :downstream="downstream"
108
- :stats="stats"
109
- @close="panelOpen = false"
110
- @jump="jumpTo"
111
- />
112
- </div>
113
- <DistanceTop10
114
- v-if="distances.length"
115
- :distances="distances"
116
- @close="distances = []"
117
- @jump="jumpTo"
118
- />
119
- </div>
120
- </template>
121
-
122
- <style scoped>
123
- .app { display: flex; height: 720px; }
124
- .graph-area { flex: 1; display: flex; min-width: 0; position: relative; }
125
- </style>
126
- ```
127
-
128
- ### 3. 只用布局 / 解析工具函数(不依赖组件)
129
-
130
- ```javascript
131
- import { parseTopo, computeOrthogonalLayout, buildGraphData } from 'topo-engine';
132
-
133
- const model = parseTopo(rawJson); // 原始 JSON → 图模型(含 nodes/edges/rootId/stats)
134
- const layout = computeOrthogonalLayout(model); // 正交布局结果(节点坐标 + 每边折线点序列)
135
- const g6Data = buildGraphData(model, layout); // G6 可直接消费的 { nodes, edges }
136
- ```
137
-
138
- > 说明:包内渲染使用的自定义图元节点(`svg-symbol`)与正交折线边(`orth-polyline`)目前仅在 `TopoGraph` 内部注册使用,未作为公开 API 导出。直接用 G6 手工拼图时建议以 `TopoGraph` 组件(或它的 render 流程)为基准,保证节点与边风格一致。
139
-
140
- ## 输入数据格式
141
-
142
- 顶层可以是 `{ code, msg, data: { nodes, relationships, stations } }`(常见接口包装格式),也可以是直接 `{ nodes, relationships, stations }` —— 引擎两种都接受。
143
-
144
- ```json
145
- {
146
- "data": {
147
- "nodes": [
148
- { "id": 1, "name": "1#台区.", "psrType": "0302", "symbolId": "1664029", "status": "0" },
149
- { "id": 2, "name": "1#台区低压熔丝.", "psrType": "3301", "symbolId": "68688", "status": "0" }
150
- ],
151
- "relationships": [ { "from": 1, "to": 2 } ],
152
- "stations": [
153
- { "id": 1, "name": "1#台区.", "contain": ["8ff4786d548a7073a2517297e801518ff456732f4d"] }
154
- ]
155
- }
156
- }
157
- ```
158
-
159
- | 字段 | 说明 |
160
- | ---- | ---- |
161
- | `nodes[]` | 设备/线路节点:`id`、`name`(可带结尾 `.`,渲染时会去掉)、`psrType`(图元类型码)、`symbolId`、`psrId`、`status`(`"0"` 视为正常) |
162
- | `relationships[]` | 有向边 `{ from, to }`,方向为 电源 → 负荷;自动防环(BFS),入度为 0 者作为根,优先选变压器/开关 |
163
- | `stations[]` | 台区/站房:`name` 作为画布标题,`contain` 内为箱内设备的 `psrId` 列表 |
164
-
165
- `parseTopo` 后模型节点会附带派生抽像字段:`cat`、`catLabel`、`shortName`、`isLine`、`insideStation`、`isFuse` 等(完整类型见 `dist/index.d.ts`)。
166
-
167
- 主要 `psrType` → 图元类别:
168
-
169
- | psrType | 类别 | 渲染 |
170
- | ------- | ---- | ---- |
171
- | `0302` | 配电变压器 | 橙色圆 + 红圈 |
172
- | `3301` | 开关 / 熔丝 | 红色矩形(开关名自动缩写为 K 编号) |
173
- | `3303` | 低压母线 | 白色短粗线 |
174
- | `3202` / `320300000` | 电缆终端头 / 分接点 | 三角 / 红色米字 |
175
- | `310100000` | 低压导线 | 青色连接点(名称缩写为杆号区间) |
176
- | `3112` | 计量箱 | 黑底白框 "JX" |
177
- | `3218000` | 用户接入点 | 白圈 "J"(名称缩写为户号) |
178
- | `32500000` | 低压配电箱(台区) | 站房 |
179
-
180
- ## TopoGraph 组件参考
181
-
182
- ### Props
183
-
184
- | Prop | 类型 | 默认 | 说明 |
185
- | ---- | ---- | ---- | ---- |
186
- | `dataUrl` | `String` | `''` | 拓扑 JSON 的 URL(内部 `fetch` 加载)。变化后自动重新加载 |
187
- | `data` | `Object` | `null` | 直接传入 JSON 对象;优先级高于 `dataUrl`;深度 watch,变化自动重绘 |
188
-
189
- ### Emits
190
-
191
- | 事件 | 载荷 | 触发时机 |
192
- | ---- | ---- | ---- |
193
- | `loaded` | `{ title, stats }`,`stats = { total, depth, leaves, byCat }` | 每次数据渲染完成 |
194
- | `node-select` | `{ node, upstream, downstream }`(均为解析后模型节点) | 点击节点(含金框选中与详情联动) |
195
- | `top-distances` | `Array<{ parent: {id,name,catLabel}, child: {id,name,catLabel}, distance }>`(≤10) | 渲染完成时,父-子连线折线长度 Top10(过滤站内边与主干鱼骨边) |
196
-
197
- ### 内置交互与 UI
198
-
199
- - 滚轮缩放、拖拽平移、悬停高亮(上下游与连线),点击选中(金色虚框,再次点空白取消)。
200
- - 左上角深色工具栏:标题 / `节点 n · 边 m · 深度 d · 叶子 l` 统计 / 「适应画布」/「导出 PNG」按钮。
201
- - 加载中 / 出错(如 dataUrl 404)有内置遮罩提示。
202
- - 卸载时自动 `destroy()` 图实例与事件监听,无内存泄漏。
203
-
204
- ### 通过 ref 调用的方法
205
-
206
- `<TopoGraph ref="topo">` 后,`topo` 暴露 **`TopoApi` 的全部公开方法**(下节 API 表),外加:
207
-
208
- | 方法 | 说明 |
209
- | ---- | ---- |
210
- | `loadRaw(jsonText)` | 用一段 JSON 文本(如本地文件上传内容)重新成图 |
211
- | `selectNode(id)` | 选中某节点并联动 `node-select` 事件 |
212
- | `resetView()` | 适应画布 |
213
- | `exportPng()` | 导出整图 PNG(按画布标题命名下载) |
214
- | `getLayoutData()` | 获取当前正交布局数据(坐标 / 折线 / stats) |
215
- | `getTopDistances()` | 获取最近一次 Top10 距离结果 |
216
-
217
- ## 控制 API(TopoApi 方法,经组件 ref 直接调用)
218
-
219
- > 也可自行 `import { TopoApi }`,按 `new TopoApi(graph, model, layout, nodeByIdMap)` 构造后调用同一套方法;事件与查询见下。使用组件时无需关心内部构造。
220
-
221
- ### 查询
222
-
223
- | 方法 | 说明 |
224
- | ---- | ---- |
225
- | `getNode(nodeId)` | 按 ID 取节点(解析后模型节点) |
226
- | `getAllNodes()` | 全部节点 |
227
- | `findNodes(filter)` | 多条件 AND 过滤,`filter` 支持 `{ cat, psrType, name, shortName, status, insideStation, … }`,值可为字符串 / 布尔 / `RegExp` |
228
- | `getSelectedId()` | 当前选中节点 ID |
229
- | `getNeighbors(id)` | `{ upstream: [], downstream: [] }` |
230
- | `getStreamNodes(id, 'upstream'\|'downstream')` | 沿流向 BFS 取节点链 |
231
- | `getTrunkNodes()` / `getSubtreeNodes(id)` | 主干链节点 / 子树节点 |
232
- | `getPath(a, b)` | 两节点间路径(不可达返回 `null`) |
233
- | `getEdge(src, tgt)` / `getEdges()` | 按端点取边 / 全部边 |
234
- | `getStreamEdges(id, dir)` / `getMainEdge()` | 沿流边的链 / 主干边 |
235
- | `getNodePosition(id)` / `getEdgePath(src, tgt)` / `getGraphBounds()` | 坐标 / 边折线点序列 / 全图包围盒 |
236
- | `getStats()` | `{ total, depth, leaves, byCat }` |
237
-
238
- ### 视图
239
-
240
- `fitView(padding?)`、`zoomTo(ratio, center?)`、`zoomIn(step?)`、`zoomOut(step?)`、`locateNode(id, zoom?)`、`getViewport()`
241
-
242
- ### 样式
243
-
244
- `setNodeStyle(id, style)`、`batchSetNodeStyle(ids, style)`、`resetNodeStyle(id)`、`resetAllNodeStyles()`
245
- `setEdgeStyle(src, tgt, style)`、`batchSetEdgeStyle([[src,tgt],…], style)`、`resetEdgeStyle(src, tgt)`
246
-
247
- ### 高亮 / 变暗
248
-
249
- ```javascript
250
- topo.highlightNode('5', { color: '#FF0000', label: '异常', labelColor: '#FF0000' });
251
- topo.unhighlightNode('5');
252
-
253
- const path = topo.getPath('1', '5'); // 高亮一条供电路径
254
- if (path) topo.highlightPath(path.map(n => n.id), { color: '#00FF00' });
255
-
256
- topo.dimOthers(['1', '2'], 0.15); // 除指定节点外整体变暗
257
- topo.unhighlightAll();
258
- ```
259
-
260
- ### 动画
261
-
262
- ```javascript
263
- topo.setNodePulse('2', { color: '#00C8FF', duration: 1500 }); // 节点脉动
264
- topo.setEdgeFlow('1', '2', { color: '#00C8FF', duration: 2000 }); // 边流动
265
- topo.removeAnimation('2');
266
- topo.removeAllAnimations();
267
- ```
268
-
269
- ### 文字标注
270
-
271
- `setText(id, text, { color, fontSize })`、`removeText(id)`、`removeAllTexts()`、`setDeviceText(cat, fontSize)`、`changeUserName('full' | 'short' | 'hidden')`
272
-
273
- ### 卡片盒
274
-
275
- ```javascript
276
- topo.showCard('5', {
277
- title: '设备名称',
278
- fields: [
279
- { label: '类型', value: '配电变压器' },
280
- { label: 'PSR编号', value: 'xxx' },
281
- ],
282
- buttons: [{ text: '查看运行曲线', type: 'primary', onClick: () => console.log('click') }],
283
- closable: true,
284
- });
285
- topo.hideCard('5');
286
- topo.hideAllCards();
287
- ```
288
-
289
- ### 连接线 / 拓扑线
290
-
291
- ```javascript
292
- topo.addConnection('1', '8', { color: '#FF6600', width: 2, style: 'dashed' }); // 任意两节点连线
293
- topo.removeAllConnections();
294
-
295
- const lineId = topo.addTopoLine(['1', '3', '8'], { color: '#FF0000', width: 3 }); // 沿线路径画高亮线
296
- topo.removeTopoLine(lineId);
297
- topo.removeAllTopoLines();
298
- ```
299
-
300
- ### 事件总线(`on` / `off`)
301
-
302
- | 事件 | 载荷 | 说明 |
303
- | ---- | ---- | ---- |
304
- | `node:click` | `{ nodeId, node }` | 节点点击 |
305
- | `node:hover` / `node:unhover` | `{ nodeId, node }` / `{ nodeId }` | 悬停进入 / 离开 |
306
- | `edge:click` | `{ edgeId, source, target }` | 边点击 |
307
- | `canvas:click` | `{ event }` | 画布空白处点击 |
308
- | `viewport:change` | `{ zoom, center }` | 视口缩放 / 平移变化 |
309
- | `card:button` | `{ nodeId, buttonIndex }` | 卡片按钮点击 |
310
-
311
- ```javascript
312
- const handler = ({ nodeId }) => console.log('点击了', nodeId);
313
- topo.on('node:click', handler);
314
- topo.off('node:click', handler); // 不传 handler 则移除该事件全部监听
315
- ```
316
-
317
- ### 导出 / 底层
318
-
319
- `exportPng(filename?)`(整图下载)、`toDataURL(opts?)`、`getGraphData()`、`getGraphInstance()`(原始 G6 Graph)、`getModel()`、`getLayoutData()`、`getNodeByIdMap()`、`update(graph, model, layout, map)`、`destroy()`
320
-
321
- ## 布局与渲染参数
322
-
323
- `computeOrthogonalLayout(model, options)`:
324
-
325
- | 参数 | 默认 | 含义 |
326
- | ---- | ---- | ---- |
327
- | `nodeGap` | `26` | 链上相邻节点边界间距 |
328
- | `band` | `50` | 侧分支根到父节点中心线的距离(分支带宽) |
329
- | `packGap` | `22` | 分支带内并排子分支间距 |
330
- | `sideGap` | `40` | 相邻主干节点的分支带隔离间距 |
331
- | `margin` | `40` | 画布外框留白 |
332
-
333
- `TopoGraph` 组件内部即用上述默认参数,返回布局数据含每节点坐标、`edgeCP`(每条边的完整折线点序列)与 `fishboneRibs`,可配 `assertOrthogonal(model, edgeCP)` 0 交叉 / 正交性校验。
334
-
335
- ## TypeScript
336
-
337
- 类型声明随包发布(`dist/index.d.ts`),支持按需导入:
338
-
339
- ```typescript
340
- import { TopoApi, TopoGraph } from 'topo-engine';
341
- import type { TopoApi } from 'topo-engine'; // 需要类型时
342
- ```
343
-
344
- ## 常见问题
345
-
346
- - **图不显示 / 高度为 0**:`TopoGraph` 撑满父容器(内部 `width/height: 100%`),请给外层元素设置明确高度(如 `height: 600px`)。
347
- - **样式错乱 / 没有深色背景**:确认已 `import 'topo-engine/style.css'`。
348
- - **`dataUrl` 加载失败**:确认文件可被 `fetch` 访问(Vite `public/`,不要用相对路径);失败会触发 `error` 态显示并 `console.error`。
349
- - **切换数据源**:响应式修改 `:data` / `:data-url` 会自动重新加载;本地文件可用 `ref.loadRaw(text)`。
350
- - **节点 ID 传字符串**:内部统一按字符串存储(`String(id)`),查询 API 请传字符串,如 `getNode('1')`。
351
- - **和 Vue 2 / 其它 G6 版本混用**:仅支持 Vue 3.4+,图内部依赖 `@antv/g6` v5,请勿重复安装不同版本。
352
-
353
- ## 本地开发
354
-
355
- ```bash
356
- npm install # 安装依赖
357
- npm run dev # 开发演示(内置 API Playground,可试所有控制 API)→ http://127.0.0.1:5173/
358
- npm run build # 生产构建(应用 + 库)
359
- npm run build:lib # 仅构建库产物到 dist/
360
- npm run preview # 预览生产构建
361
- npm pack --dry-run # 查看发布包内容清单
362
- ```
363
-
364
- ## License
365
-
366
- MIT
1
+ # topo-engine
2
+
3
+ 配电台区(低压配电网)**单线图拓扑成图引擎**,基于 **Vue 3 + AntV G6 v5**。
4
+
5
+ 输入一份台区拓扑 JSON,自动完成「解析 → 建树找根 → **横平竖直、0 交叉** 的正交布局 → 深色 SCADA 风格渲染」,并提供开箱即用的 Vue 组件与一整套拓扑查询 / 样式 / 动画 / 标注控制 API。
6
+
7
+ ## 功能特性
8
+
9
+ - 🧩 **Vue 3 组件** — `TopoGraph`(画布,含标题栏 / 统计 / 适应画布 / 导出 PNG 按钮)、`SidePanel`(设备详情与上下游)、`DistanceTop10`(父子连线距离 Top10 面板)
10
+ - 🎨 **深色 / 浅色双主题** 黑底 SCADA 或白底两种画布风格,图元 / 导线 / 文字随之配套适配,可随时切换(`theme` prop 或工具栏按钮)。默认浅色主题
11
+ - 📐 **自研正交布局**主干沿最长路径横向铺开 + 分支带垂直直走,整图**横平竖直、0 交叉、0 重叠**,边缘路由为直线 / L 形折线
12
+ - 🔍 **拓扑查询 API** 节点 / 边 / 邻居 / 子树 / 主干链 / 路径 / 坐标 查询
13
+ - 🎨 **样式控制**节点、边样式覆盖与批量重置
14
+ - **动画与高亮**节点脉动、边流动、路径高亮、其余变暗、异常标注
15
+ - 📝 **文字标注**附加文字、用户名 full / short / hidden 切换
16
+ - 🃏 **卡片盒** 节点信息卡片(字段 + 按钮),适配"运行曲线/工单跳转"类场景
17
+ - 🔗 **连接线 / 拓扑线** 任意两节点动态连接线、路径拓扑高亮线
18
+ - 🎧 **事件系统**点击 / 悬停 / 画布空白 / 视口变化事件总线
19
+ - 💾 **导出**整图 PNG 下载、DataURL、图数据 JSON
20
+ - 📦 **零配置渲染** — 组件自带深色主题与工具栏,`data` 或 `dataUrl` 二选一即可出图
21
+
22
+ ## 安装
23
+
24
+ ```bash
25
+ npm install topo-engine
26
+ # 或 yarn add topo-engine / pnpm add topo-engine
27
+ ```
28
+
29
+ - 环境要求:**Vue 3.4+**(peerDependency);`@antv/g6` v5 已作为依赖随包安装,无需手动配置。
30
+ - 组件深色样式不自动注入,使用时需引入样式文件:
31
+
32
+ ```js
33
+ import 'topo-engine/style.css';
34
+ ```
35
+
36
+ ## 快速开始
37
+
38
+ ### 1. 最小示例 —— 只画一张图
39
+
40
+ ```vue
41
+ <script setup>
42
+ import { ref } from 'vue';
43
+ import { TopoGraph } from 'topo-engine';
44
+ import 'topo-engine/style.css';
45
+
46
+ // 两种数据来源任选其一:
47
+ // - dataUrl:内部用 fetch 加载(注意放 public/ 下或跨域可达)
48
+ // - data :直接传入解析好的 JSON 对象(优先级高于 dataUrl)
49
+ const dataUrl = ref('/data/topology.json');
50
+
51
+ const onLoaded = ({ title, stats }) => {
52
+ console.log('加载完成', title, stats); // { total, depth, leaves, byCat }
53
+ };
54
+ </script>
55
+
56
+ <template>
57
+ <!-- 组件撑满父容器,父容器必须有明确高度 -->
58
+ <div class="page">
59
+ <TopoGraph :data-url="dataUrl" @loaded="onLoaded" />
60
+ </div>
61
+ </template>
62
+
63
+ <style scoped>
64
+ .page { height: 640px; }
65
+ </style>
66
+ ```
67
+
68
+ ### 2. 组合示例 —— 图 + 详情面板 + Top10 距离面板
69
+
70
+ ```vue
71
+ <script setup>
72
+ import { ref } from 'vue';
73
+ import { TopoGraph, SidePanel, DistanceTop10 } from 'topo-engine';
74
+ import 'topo-engine/style.css';
75
+
76
+ const topo = ref(null); // ref → 调用完整控制 API
77
+ const graphData = ref(null); // 直接传数据对象
78
+ const panelOpen = ref(false);
79
+ const selected = ref(null);
80
+ const upstream = ref([]);
81
+ const downstream = ref([]);
82
+ const stats = ref({});
83
+ const distances = ref([]);
84
+
85
+ function onSelect({ node, upstream: up, downstream: down }) {
86
+ selected.value = node;
87
+ upstream.value = up;
88
+ downstream.value = down;
89
+ panelOpen.value = true;
90
+ }
91
+ function onTopDistances(list) { distances.value = list; }
92
+ function jumpTo(id) { topo.value?.selectNode(id); }
93
+ </script>
94
+
95
+ <template>
96
+ <div class="app">
97
+ <div class="graph-area">
98
+ <TopoGraph
99
+ ref="topo"
100
+ :data="graphData"
101
+ @node-select="onSelect"
102
+ @top-distances="onTopDistances"
103
+ />
104
+ <SidePanel
105
+ v-if="panelOpen"
106
+ :node="selected"
107
+ :upstream="upstream"
108
+ :downstream="downstream"
109
+ :stats="stats"
110
+ @close="panelOpen = false"
111
+ @jump="jumpTo"
112
+ />
113
+ </div>
114
+ <DistanceTop10
115
+ v-if="distances.length"
116
+ :distances="distances"
117
+ @close="distances = []"
118
+ @jump="jumpTo"
119
+ />
120
+ </div>
121
+ </template>
122
+
123
+ <style scoped>
124
+ .app { display: flex; height: 720px; }
125
+ .graph-area { flex: 1; display: flex; min-width: 0; position: relative; }
126
+ </style>
127
+ ```
128
+
129
+ ### 3. 只用布局 / 解析工具函数(不依赖组件)
130
+
131
+ ```javascript
132
+ import { parseTopo, computeOrthogonalLayout, buildGraphData } from 'topo-engine';
133
+
134
+ const model = parseTopo(rawJson); // 原始 JSON → 图模型(含 nodes/edges/rootId/stats)
135
+ const layout = computeOrthogonalLayout(model); // 正交布局结果(节点坐标 + 每边折线点序列)
136
+ const g6Data = buildGraphData(model, layout); // → G6 可直接消费的 { nodes, edges }
137
+ ```
138
+
139
+ > 说明:包内渲染使用的自定义图元节点(`svg-symbol`)与正交折线边(`orth-polyline`)目前仅在 `TopoGraph` 内部注册使用,未作为公开 API 导出。直接用 G6 手工拼图时建议以 `TopoGraph` 组件(或它的 render 流程)为基准,保证节点与边风格一致。
140
+
141
+ ## 输入数据格式
142
+
143
+ 顶层可以是 `{ code, msg, data: { nodes, relationships, stations } }`(常见接口包装格式),也可以是直接 `{ nodes, relationships, stations }` —— 引擎两种都接受。
144
+
145
+ ```json
146
+ {
147
+ "data": {
148
+ "nodes": [
149
+ { "id": 1, "name": "1#台区.", "psrType": "0302", "symbolId": "1664029", "status": "0" },
150
+ { "id": 2, "name": "1#台区低压熔丝.", "psrType": "3301", "symbolId": "68688", "status": "0" }
151
+ ],
152
+ "relationships": [ { "from": 1, "to": 2 } ],
153
+ "stations": [
154
+ { "id": 1, "name": "1#台区.", "contain": ["8ff4786d548a7073a2517297e801518ff456732f4d"] }
155
+ ]
156
+ }
157
+ }
158
+ ```
159
+
160
+ | 字段 | 说明 |
161
+ | ---- | ---- |
162
+ | `nodes[]` | 设备/线路节点:`id`、`name`(可带结尾 `.`,渲染时会去掉)、`psrType`(图元类型码)、`symbolId`、`psrId`、`status`(`"0"` 视为正常) |
163
+ | `relationships[]` | 有向边 `{ from, to }`,方向为 电源 → 负荷;自动防环(BFS),入度为 0 者作为根,优先选变压器/开关 |
164
+ | `stations[]` | 台区/站房:`name` 作为画布标题,`contain` 内为箱内设备的 `psrId` 列表 |
165
+
166
+ `parseTopo` 后模型节点会附带派生抽像字段:`cat`、`catLabel`、`shortName`、`isLine`、`insideStation`、`isFuse` 等(完整类型见 `dist/index.d.ts`)。
167
+
168
+ 主要 `psrType` → 图元类别:
169
+
170
+ | psrType | 类别 | 渲染 |
171
+ | ------- | ---- | ---- |
172
+ | `0302` | 配电变压器 | 橙色圆 + 红圈 |
173
+ | `3301` | 开关 / 熔丝 | 红色矩形(开关名自动缩写为 K 编号) |
174
+ | `3303` | 低压母线 | 白色短粗线(无文字标签) |
175
+ | `3202` / `320300000` | 电缆终端头 / 分接点 | 三角 / 红色米字(无文字标签) |
176
+ | `310100000` | 低压导线 | 青色连接点(名称缩写为杆号区间) |
177
+ | `3112` | 计量箱 | 黑底白框 "JX" |
178
+ | `3218000` | 用户接入点 | 白圈 "J"(名称缩写为户号) |
179
+ | `32500000` | 低压配电箱(台区) | 站房 |
180
+
181
+ ## TopoGraph 组件参考
182
+
183
+ ### Props
184
+
185
+ | Prop | 类型 | 默认 | 说明 |
186
+ | ---- | ---- | ---- | ---- |
187
+ | `dataUrl` | `String` | `''` | 拓扑 JSON URL(内部 `fetch` 加载)。变化后自动重新加载 |
188
+ | `data` | `Object` | `null` | 直接传入 JSON 对象;优先级高于 `dataUrl`;深度 watch,变化自动重绘 |
189
+ | `theme` | `'dark' \| 'light'` | `'light'` | 画布主题:`dark`(黑底 SCADA)/ `light`(白底,图元配套加深,默认)。支持 `v-model:theme`,也可点内置工具栏 ☀️/🌙 按钮切换 |
190
+
191
+ ### Emits
192
+
193
+ | 事件 | 载荷 | 触发时机 |
194
+ | ---- | ---- | ---- |
195
+ | `loaded` | `{ title, stats }`,`stats = { total, depth, leaves, byCat }` | 每次数据渲染完成 |
196
+ | `node-select` | `{ node, upstream, downstream }`(均为解析后模型节点) | 点击节点(含金框选中与详情联动) |
197
+ | `top-distances` | `Array<{ parent: {id,name,catLabel}, child: {id,name,catLabel}, distance }>`(≤10) | 渲染完成时,父-子连线折线长度 Top10(过滤站内边与主干鱼骨边) |
198
+
199
+ ### 内置交互与 UI
200
+
201
+ - 滚轮缩放、拖拽平移、悬停高亮(上下游与连线),点击选中(金色虚框,再次点空白取消)。
202
+ - 左上角深色工具栏:标题 / `节点 n · 边 m · 深度 d · 叶子 l` 统计 / 「适应画布」/「导出 PNG」按钮。
203
+ - 加载中 / 出错(如 dataUrl 404)有内置遮罩提示。
204
+ - 卸载时自动 `destroy()` 图实例与事件监听,无内存泄漏。
205
+
206
+ ### 通过 ref 调用的方法
207
+
208
+ `<TopoGraph ref="topo">` 后,`topo` 暴露 **`TopoApi` 的全部公开方法**(下节 API 表),外加:
209
+
210
+ | 方法 | 说明 |
211
+ | ---- | ---- |
212
+ | `loadRaw(jsonText)` | 用一段 JSON 文本(如本地文件上传内容)重新成图 |
213
+ | `selectNode(id)` | 选中某节点并联动 `node-select` 事件 |
214
+ | `resetView()` | 适应画布 |
215
+ | `exportPng()` | 导出整图 PNG(按画布标题命名下载) |
216
+ | `getLayoutData()` | 获取当前正交布局数据(坐标 / 折线 / stats) |
217
+ | `getTopDistances()` | 获取最近一次 Top10 距离结果 |
218
+
219
+ ### 主题切换(深色 / 浅色)
220
+
221
+ 引擎内置两套主题,**浅色为默认**(白底,图元配套深色,适合打印或嵌入浅色页面);深色为**黑底 SCADA 风格**。
222
+
223
+ - `TopoGraph`:`:theme` 取值 `'dark'`(默认)或 `'light'`,推荐 `v-model:theme` 双向绑定;组件左上工具栏内置 ☀️ / 🌙 切换按钮(点击 emit `update:theme`)。
224
+ - `SidePanel` / `DistanceTop10`:同样接收 `theme` prop,与画布保持一致观感。
225
+ - 切换主题会**按当前数据自动重建画布**:G6 画布背景、图元、边、标注文字全部随主题更新,布局不变。
226
+
227
+ ```vue
228
+ <script setup>
229
+ import { ref } from 'vue';
230
+ import { TopoGraph } from 'topo-engine';
231
+ import 'topo-engine/style.css';
232
+
233
+ const theme = ref('light'); // 初始浅色(默认),可在运行时切换
234
+ const dataUrl = ref('/data/topology.json');
235
+ </script>
236
+
237
+ <template>
238
+ <div class="page">
239
+ <TopoGraph v-model:theme="theme" :data-url="dataUrl" />
240
+ </div>
241
+ </template>
242
+ ```
243
+
244
+ > 说明:主题色板定义在包内 `theme.js`(`THEMES.dark` / `THEMES.light`,已随库打包,未作为公开导出);若需要按自有品牌定制,可在外层自行覆盖 `dist/style.css` 中各组件的 `--tp-*` / `--sp-*` / `--dt-*` CSS 变量。
245
+
246
+ ## 控制 API(TopoApi 方法,经组件 ref 直接调用)
247
+
248
+ > 也可自行 `import { TopoApi }`,按 `new TopoApi(graph, model, layout, nodeByIdMap)` 构造后调用同一套方法;事件与查询见下。使用组件时无需关心内部构造。
249
+
250
+ ### 查询
251
+
252
+ | 方法 | 说明 |
253
+ | ---- | ---- |
254
+ | `getNode(nodeId)` | ID 取节点(解析后模型节点) |
255
+ | `getAllNodes()` | 全部节点 |
256
+ | `findNodes(filter)` | 多条件 AND 过滤,`filter` 支持 `{ cat, psrType, name, shortName, status, insideStation, … }`,值可为字符串 / 布尔 / `RegExp` |
257
+ | `getSelectedId()` | 当前选中节点 ID |
258
+ | `getNeighbors(id)` | `{ upstream: [], downstream: [] }` |
259
+ | `getStreamNodes(id, 'upstream'\|'downstream')` | 沿流向 BFS 取节点链 |
260
+ | `getTrunkNodes()` / `getSubtreeNodes(id)` | 主干链节点 / 子树节点 |
261
+ | `getPath(a, b)` | 两节点间路径(不可达返回 `null`) |
262
+ | `getEdge(src, tgt)` / `getEdges()` | 按端点取边 / 全部边 |
263
+ | `getStreamEdges(id, dir)` / `getMainEdge()` | 沿流边的链 / 主干边 |
264
+ | `getNodePosition(id)` / `getEdgePath(src, tgt)` / `getGraphBounds()` | 坐标 / 边折线点序列 / 全图包围盒 |
265
+ | `getStats()` | `{ total, depth, leaves, byCat }` |
266
+
267
+ ### 视图
268
+
269
+ `fitView(padding?)`、`zoomTo(ratio, center?)`、`zoomIn(step?)`、`zoomOut(step?)`、`locateNode(id, zoom?)`、`getViewport()`
270
+
271
+ ### 样式
272
+
273
+ `setNodeStyle(id, style)`、`batchSetNodeStyle(ids, style)`、`resetNodeStyle(id)`、`resetAllNodeStyles()`
274
+ `setEdgeStyle(src, tgt, style)`、`batchSetEdgeStyle([[src,tgt],…], style)`、`resetEdgeStyle(src, tgt)`
275
+
276
+ ### 高亮 / 变暗
277
+
278
+ ```javascript
279
+ topo.highlightNode('5', { color: '#FF0000', label: '异常', labelColor: '#FF0000' });
280
+ topo.unhighlightNode('5');
281
+
282
+ const path = topo.getPath('1', '5'); // 高亮一条供电路径
283
+ if (path) topo.highlightPath(path.map(n => n.id), { color: '#00FF00' });
284
+
285
+ topo.dimOthers(['1', '2'], 0.15); // 除指定节点外整体变暗
286
+ topo.unhighlightAll();
287
+ ```
288
+
289
+ ### 动画
290
+
291
+ ```javascript
292
+ topo.setNodePulse('2', { color: '#00C8FF', duration: 1500 }); // 节点脉动
293
+ topo.setEdgeFlow('1', '2', { color: '#00C8FF', duration: 2000 }); // 边流动
294
+ topo.removeAnimation('2');
295
+ topo.removeAllAnimations();
296
+ ```
297
+
298
+ ### 文字标注
299
+
300
+ `setText(id, text, { color, fontSize })`、`removeText(id)`、`removeAllTexts()`、`setDeviceText(cat, fontSize)`、`changeUserName('full' | 'short' | 'hidden')`
301
+
302
+ ### 卡片盒
303
+
304
+ ```javascript
305
+ topo.showCard('5', {
306
+ title: '设备名称',
307
+ fields: [
308
+ { label: '类型', value: '配电变压器' },
309
+ { label: 'PSR编号', value: 'xxx' },
310
+ ],
311
+ buttons: [{ text: '查看运行曲线', type: 'primary', onClick: () => console.log('click') }],
312
+ closable: true,
313
+ });
314
+ topo.hideCard('5');
315
+ topo.hideAllCards();
316
+ ```
317
+
318
+ ### 连接线 / 拓扑线
319
+
320
+ ```javascript
321
+ topo.addConnection('1', '8', { color: '#FF6600', width: 2, style: 'dashed' }); // 任意两节点连线
322
+ topo.removeAllConnections();
323
+
324
+ const lineId = topo.addTopoLine(['1', '3', '8'], { color: '#FF0000', width: 3 }); // 沿线路径画高亮线
325
+ topo.removeTopoLine(lineId);
326
+ topo.removeAllTopoLines();
327
+ ```
328
+
329
+ ### 事件总线(`on` / `off`)
330
+
331
+ | 事件 | 载荷 | 说明 |
332
+ | ---- | ---- | ---- |
333
+ | `node:click` | `{ nodeId, node }` | 节点点击 |
334
+ | `node:hover` / `node:unhover` | `{ nodeId, node }` / `{ nodeId }` | 悬停进入 / 离开 |
335
+ | `edge:click` | `{ edgeId, source, target }` | 边点击 |
336
+ | `canvas:click` | `{ event }` | 画布空白处点击 |
337
+ | `viewport:change` | `{ zoom, center }` | 视口缩放 / 平移变化 |
338
+ | `card:button` | `{ nodeId, buttonIndex }` | 卡片按钮点击 |
339
+
340
+ ```javascript
341
+ const handler = ({ nodeId }) => console.log('点击了', nodeId);
342
+ topo.on('node:click', handler);
343
+ topo.off('node:click', handler); // 不传 handler 则移除该事件全部监听
344
+ ```
345
+
346
+ ### 导出 / 底层
347
+
348
+ `exportPng(filename?)`(整图下载)、`toDataURL(opts?)`、`getGraphData()`、`getGraphInstance()`(原始 G6 Graph)、`getModel()`、`getLayoutData()`、`getNodeByIdMap()`、`update(graph, model, layout, map)`、`destroy()`
349
+
350
+ ## 文字渲染
351
+
352
+ - **设备标签自动换行**:开关标签、站房标题、下方名称标签等文字根据设备宽度自动换行,避免文字重叠
353
+ - **站房标题自适应高度**:站房框高度会根据标题换行行数自动扩展,确保多行标题完整显示在框内
354
+ - **母线 / 电缆终端头**:不渲染额外文字标签(母线不显示"母线",电缆终端头不显示名称)
355
+ - **站房标题**:使用独立属性 `_stationTitle` 渲染,避免 G6 默认 label 重复绘制
356
+
357
+ ## 布局与渲染参数
358
+
359
+ `computeOrthogonalLayout(model, options)`:
360
+
361
+ | 参数 | 默认 | 含义 |
362
+ | ---- | ---- | ---- |
363
+ | `nodeGap` | `26` | 链上相邻节点边界间距 |
364
+ | `band` | `50` | 侧分支根到父节点中心线的距离(分支带宽) |
365
+ | `packGap` | `22` | 分支带内并排子分支间距 |
366
+ | `sideGap` | `40` | 相邻主干节点的分支带隔离间距 |
367
+ | `margin` | `40` | 画布外框留白 |
368
+
369
+ `TopoGraph` 组件内部即用上述默认参数,返回布局数据含每节点坐标、`edgeCP`(每条边的完整折线点序列)与 `fishboneRibs`,可配 `assertOrthogonal(model, edgeCP)` 做 0 交叉 / 正交性校验。
370
+
371
+ ## TypeScript
372
+
373
+ 类型声明随包发布(`dist/index.d.ts`),支持按需导入:
374
+
375
+ ```typescript
376
+ import { TopoApi, TopoGraph } from 'topo-engine';
377
+ import type { TopoApi } from 'topo-engine'; // 需要类型时
378
+ ```
379
+
380
+ ## 常见问题
381
+
382
+ - **图不显示 / 高度为 0**:`TopoGraph` 撑满父容器(内部 `width/height: 100%`),请给外层元素设置明确高度(如 `height: 600px`)。
383
+ - **样式错乱 / 没有深色背景**:确认已 `import 'topo-engine/style.css'`。
384
+ - **`dataUrl` 加载失败**:确认文件可被 `fetch` 访问(Vite 放 `public/`,不要用相对路径);失败会触发 `error` 态显示并 `console.error`。
385
+ - **切换数据源**:响应式修改 `:data` / `:data-url` 会自动重新加载;本地文件可用 `ref.loadRaw(text)`。
386
+ - **节点 ID 传字符串**:内部统一按字符串存储(`String(id)`),查询 API 请传字符串,如 `getNode('1')`。
387
+ - **和 Vue 2 / 其它 G6 版本混用**:仅支持 Vue 3.4+,图内部依赖 `@antv/g6` v5,请勿重复安装不同版本。
388
+
389
+ ## 本地开发
390
+
391
+ ```bash
392
+ npm install # 安装依赖
393
+ npm run dev # 开发演示(内置 API Playground,可试所有控制 API)→ http://127.0.0.1:5173/
394
+ npm run build # 生产构建(应用 + 库)
395
+ npm run build:lib # 仅构建库产物到 dist/
396
+ npm run preview # 预览生产构建
397
+ npm pack --dry-run # 查看发布包内容清单
398
+ ```
399
+
400
+ ## License
401
+
402
+ MIT