topo-engine 0.1.0 → 0.1.2
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 +314 -111
- package/dist/index.d.ts +115 -0
- package/dist/topo-engine.cjs +1 -1
- package/dist/topo-engine.cjs.map +1 -1
- package/dist/topo-engine.js +1 -1
- package/dist/topo-engine.js.map +1 -1
- package/dist/topo-engine.umd.cjs +1 -1
- package/dist/topo-engine.umd.cjs.map +1 -1
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,163 +1,366 @@
|
|
|
1
|
-
#
|
|
1
|
+
# topo-engine
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
配电台区(低压配电网)**单线图拓扑成图引擎**,基于 **Vue 3 + AntV G6 v5**。
|
|
4
|
+
|
|
5
|
+
输入一份台区拓扑 JSON,自动完成「解析 → 建树找根 → **横平竖直、0 交叉** 的正交布局 → 深色 SCADA 风格渲染」,并提供开箱即用的 Vue 组件与一整套拓扑查询 / 样式 / 动画 / 标注控制 API。
|
|
5
6
|
|
|
6
7
|
## 功能特性
|
|
7
8
|
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
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` 二选一即可出图
|
|
16
20
|
|
|
17
21
|
## 安装
|
|
18
22
|
|
|
19
23
|
```bash
|
|
20
|
-
# npm
|
|
21
24
|
npm install topo-engine
|
|
25
|
+
# 或 yarn add topo-engine / pnpm add topo-engine
|
|
26
|
+
```
|
|
22
27
|
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
- 环境要求:**Vue 3.4+**(peerDependency);`@antv/g6` v5 已作为依赖随包安装,无需手动配置。
|
|
29
|
+
- 组件深色样式不自动注入,使用时需引入样式文件:
|
|
25
30
|
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
```js
|
|
32
|
+
import 'topo-engine/style.css';
|
|
28
33
|
```
|
|
29
34
|
|
|
30
35
|
## 快速开始
|
|
31
36
|
|
|
32
|
-
### 1.
|
|
37
|
+
### 1. 最小示例 —— 只画一张图
|
|
33
38
|
|
|
34
39
|
```vue
|
|
35
|
-
<template>
|
|
36
|
-
<TopoGraph
|
|
37
|
-
:data-url="dataUrl"
|
|
38
|
-
@node-select="onNodeSelect"
|
|
39
|
-
@loaded="onLoaded"
|
|
40
|
-
/>
|
|
41
|
-
</template>
|
|
42
|
-
|
|
43
40
|
<script setup>
|
|
44
41
|
import { ref } from 'vue';
|
|
45
42
|
import { TopoGraph } from 'topo-engine';
|
|
43
|
+
import 'topo-engine/style.css';
|
|
46
44
|
|
|
45
|
+
// 两种数据来源任选其一:
|
|
46
|
+
// - dataUrl:内部用 fetch 加载(注意放 public/ 下或跨域可达)
|
|
47
|
+
// - data :直接传入解析好的 JSON 对象(优先级高于 dataUrl)
|
|
47
48
|
const dataUrl = ref('/data/topology.json');
|
|
48
49
|
|
|
49
|
-
const
|
|
50
|
-
console.log('
|
|
50
|
+
const onLoaded = ({ title, stats }) => {
|
|
51
|
+
console.log('加载完成', title, stats); // { total, depth, leaves, byCat }
|
|
51
52
|
};
|
|
53
|
+
</script>
|
|
52
54
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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); }
|
|
56
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>
|
|
57
126
|
```
|
|
58
127
|
|
|
59
|
-
###
|
|
128
|
+
### 3. 只用布局 / 解析工具函数(不依赖组件)
|
|
60
129
|
|
|
61
130
|
```javascript
|
|
62
|
-
import {
|
|
63
|
-
|
|
64
|
-
// 假设你已经有一个 G6 Graph 实例
|
|
65
|
-
const graph = new Graph({ /* ... */ });
|
|
131
|
+
import { parseTopo, computeOrthogonalLayout, buildGraphData } from 'topo-engine';
|
|
66
132
|
|
|
67
|
-
//
|
|
68
|
-
const
|
|
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
|
+
```
|
|
69
137
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
+
}
|
|
75
157
|
```
|
|
76
158
|
|
|
77
|
-
|
|
159
|
+
| 字段 | 说明 |
|
|
160
|
+
| ---- | ---- |
|
|
161
|
+
| `nodes[]` | 设备/线路节点:`id`、`name`(可带结尾 `.`,渲染时会去掉)、`psrType`(图元类型码)、`symbolId`、`psrId`、`status`(`"0"` 视为正常) |
|
|
162
|
+
| `relationships[]` | 有向边 `{ from, to }`,方向为 电源 → 负荷;自动防环(BFS),入度为 0 者作为根,优先选变压器/开关 |
|
|
163
|
+
| `stations[]` | 台区/站房:`name` 作为画布标题,`contain` 内为箱内设备的 `psrId` 列表 |
|
|
78
164
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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();
|
|
85
258
|
```
|
|
86
259
|
|
|
87
|
-
|
|
260
|
+
### 动画
|
|
88
261
|
|
|
89
|
-
|
|
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
|
+
```
|
|
90
268
|
|
|
91
|
-
|
|
269
|
+
### 文字标注
|
|
92
270
|
|
|
93
|
-
|
|
94
|
-
- `data.relationships[]` — 有向边:`{ from, to }`(电源 → 负荷,103 节点 102 边构成树)
|
|
95
|
-
- `data.stations[]` — 台区根(低压配电箱),`contain` 列出箱内设备 psrId
|
|
271
|
+
`setText(id, text, { color, fontSize })`、`removeText(id)`、`removeAllTexts()`、`setDeviceText(cat, fontSize)`、`changeUserName('full' | 'short' | 'hidden')`
|
|
96
272
|
|
|
97
|
-
|
|
273
|
+
### 卡片盒
|
|
98
274
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
+
### 连接线 / 拓扑线
|
|
109
290
|
|
|
110
|
-
|
|
291
|
+
```javascript
|
|
292
|
+
topo.addConnection('1', '8', { color: '#FF6600', width: 2, style: 'dashed' }); // 任意两节点连线
|
|
293
|
+
topo.removeAllConnections();
|
|
111
294
|
|
|
295
|
+
const lineId = topo.addTopoLine(['1', '3', '8'], { color: '#FF0000', width: 3 }); // 沿线路径画高亮线
|
|
296
|
+
topo.removeTopoLine(lineId);
|
|
297
|
+
topo.removeAllTopoLines();
|
|
112
298
|
```
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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 则移除该事件全部监听
|
|
127
315
|
```
|
|
128
316
|
|
|
129
|
-
|
|
317
|
+
### 导出 / 底层
|
|
318
|
+
|
|
319
|
+
`exportPng(filename?)`(整图下载)、`toDataURL(opts?)`、`getGraphData()`、`getGraphInstance()`(原始 G6 Graph)、`getModel()`、`getLayoutData()`、`getNodeByIdMap()`、`update(graph, model, layout, map)`、`destroy()`
|
|
130
320
|
|
|
131
|
-
|
|
132
|
-
- 导线双色:干线白色、支线灰色(对照 SVG 的 `rgb(255,255,255)` / `rgb(128,128,128)`)
|
|
133
|
-
- 图元配色与几何逐项对照 sample SVG 的 `<symbol>` 定义
|
|
134
|
-
- **正交布局(0 交叉,见 `src/engine/layout.js`)—— 单向直走式**:
|
|
135
|
-
- 主干 = 从根沿**最长路径**前进(每步选到最深叶子距离最大的子节点),主干横向铺开
|
|
136
|
-
- 主干节点的其余子节点为侧分支,交替挂下方/上方,带内按内容盒 x 排布
|
|
137
|
-
- 侧分支 = **垂直列式(选定方向后不拐弯)**:分支内每步同样选最长路径子节点**同列垂直直走**,
|
|
138
|
-
其余子节点独占新列(同样垂直直走),列内递归同规则——整张图只有横向主干 + 垂直列,无方向交替拐弯
|
|
139
|
-
- 边路由:同向链 = 直线;跨列/跨带 = L 形(垂直段 + 水平带线,带线统一高度)
|
|
140
|
-
- 数学验证:`assertOrthogonal`(全线段横平竖直)+ `checkZeroCrossing`(0 交叉、0 压线、0 节点重叠)
|
|
141
|
-
- 实测:102 条边端点全部落在节点边界、0 非正交段;分支链节点同列直走(如 K02 链 5 节点 x 完全一致)
|
|
142
|
-
- 交互:滚轮缩放、拖拽平移、悬停 tooltip(设备/名称/编码/状态)、点击选中(金色虚框 + 右侧详情)
|
|
321
|
+
## 布局与渲染参数
|
|
143
322
|
|
|
144
|
-
|
|
323
|
+
`computeOrthogonalLayout(model, options)`:
|
|
145
324
|
|
|
146
325
|
| 参数 | 默认 | 含义 |
|
|
147
326
|
| ---- | ---- | ---- |
|
|
148
|
-
| nodeGap | 26 |
|
|
149
|
-
| band | 50 |
|
|
150
|
-
| packGap | 22 |
|
|
151
|
-
| sideGap | 40 |
|
|
152
|
-
| margin | 40 | 画布外框留白 |
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 拓扑引擎类型声明文件
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// 核心 API
|
|
6
|
+
export class TopoApi {
|
|
7
|
+
constructor(graph: any, model: any, layout: any, nodeByIdMap: Map<string, any>);
|
|
8
|
+
|
|
9
|
+
// 查询 API
|
|
10
|
+
getNode(nodeId: string): any;
|
|
11
|
+
getAllNodes(): any[];
|
|
12
|
+
findNodes(filter: object): any[];
|
|
13
|
+
getSelectedId(): string | null;
|
|
14
|
+
getNeighbors(nodeId: string): { upstream: any[], downstream: any[] };
|
|
15
|
+
getStreamNodes(nodeId: string, direction: 'upstream' | 'downstream'): any[];
|
|
16
|
+
getTrunkNodes(): any[];
|
|
17
|
+
getSubtreeNodes(nodeId: string): any[];
|
|
18
|
+
getPath(fromId: string, toId: string): any[] | null;
|
|
19
|
+
getEdge(srcId: string, tgtId: string): any;
|
|
20
|
+
getEdges(): any[];
|
|
21
|
+
getStreamEdges(nodeId: string, direction: 'upstream' | 'downstream'): any[];
|
|
22
|
+
getMainEdge(): any[];
|
|
23
|
+
getNodePosition(nodeId: string): { x: number, y: number } | null;
|
|
24
|
+
getEdgePath(srcId: string, tgtId: string): number[][] | null;
|
|
25
|
+
getGraphBounds(): { minX: number, minY: number, maxX: number, maxY: number };
|
|
26
|
+
getStats(): any;
|
|
27
|
+
|
|
28
|
+
// 视图控制
|
|
29
|
+
fitView(padding?: number): void;
|
|
30
|
+
zoomTo(ratio: number, center?: { x: number, y: number }): void;
|
|
31
|
+
zoomIn(step?: number): void;
|
|
32
|
+
zoomOut(step?: number): void;
|
|
33
|
+
locateNode(nodeId: string, zoom?: number): void;
|
|
34
|
+
getViewport(): { zoom: number, center: { x: number, y: number } };
|
|
35
|
+
|
|
36
|
+
// 节点样式
|
|
37
|
+
setNodeStyle(nodeId: string, style: object): void;
|
|
38
|
+
batchSetNodeStyle(nodeIds: string[], style: object): void;
|
|
39
|
+
resetNodeStyle(nodeId: string): void;
|
|
40
|
+
resetAllNodeStyles(): void;
|
|
41
|
+
|
|
42
|
+
// 边样式
|
|
43
|
+
setEdgeStyle(srcId: string, tgtId: string, style: object): void;
|
|
44
|
+
batchSetEdgeStyle(edgeIds: [string, string][], style: object): void;
|
|
45
|
+
resetEdgeStyle(srcId: string, tgtId: string): void;
|
|
46
|
+
|
|
47
|
+
// 高亮与标注
|
|
48
|
+
highlightNode(nodeId: string, opts?: object): void;
|
|
49
|
+
unhighlightNode(nodeId: string): void;
|
|
50
|
+
highlightPath(nodeIds: string[], opts?: object): void;
|
|
51
|
+
unhighlightAll(): void;
|
|
52
|
+
dimOthers(keepIds: string[], opacity?: number): void;
|
|
53
|
+
|
|
54
|
+
// 动画
|
|
55
|
+
setNodePulse(nodeId: string, opts?: object): void;
|
|
56
|
+
setEdgeFlow(srcId: string, tgtId: string, opts?: object): void;
|
|
57
|
+
removeAnimation(targetId: string): void;
|
|
58
|
+
removeAllAnimations(): void;
|
|
59
|
+
|
|
60
|
+
// 文字标注
|
|
61
|
+
setText(nodeId: string, text: string, opts?: object): void;
|
|
62
|
+
removeText(nodeId: string): void;
|
|
63
|
+
removeAllTexts(): void;
|
|
64
|
+
setDeviceText(cat: string, fontSize: number): void;
|
|
65
|
+
changeUserName(mode: 'full' | 'short' | 'hidden'): void;
|
|
66
|
+
|
|
67
|
+
// 卡片盒
|
|
68
|
+
showCard(nodeId: string, content: object): void;
|
|
69
|
+
hideCard(nodeId: string): void;
|
|
70
|
+
hideAllCards(): void;
|
|
71
|
+
|
|
72
|
+
// 连接线与拓扑线
|
|
73
|
+
addConnection(fromId: string, toId: string, opts?: object): void;
|
|
74
|
+
removeConnection(fromId: string, toId: string): void;
|
|
75
|
+
removeAllConnections(): void;
|
|
76
|
+
addTopoLine(nodeIds: string[], opts?: object): string;
|
|
77
|
+
removeTopoLine(lineId: string): void;
|
|
78
|
+
removeAllTopoLines(): void;
|
|
79
|
+
|
|
80
|
+
// 事件系统
|
|
81
|
+
on(event: string, handler: Function): void;
|
|
82
|
+
off(event: string, handler?: Function): void;
|
|
83
|
+
|
|
84
|
+
// 导出
|
|
85
|
+
exportPng(filename?: string): Promise<void>;
|
|
86
|
+
toDataURL(opts?: object): Promise<string>;
|
|
87
|
+
getGraphData(): object;
|
|
88
|
+
|
|
89
|
+
// 底层访问
|
|
90
|
+
getGraphInstance(): any;
|
|
91
|
+
getModel(): any;
|
|
92
|
+
getLayoutData(): any;
|
|
93
|
+
getNodeByIdMap(): Map<string, any>;
|
|
94
|
+
|
|
95
|
+
// 数据刷新
|
|
96
|
+
update(graph: any, model: any, layout: any, nodeByIdMap: Map<string, any>): void;
|
|
97
|
+
|
|
98
|
+
// 销毁
|
|
99
|
+
destroy(): void;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 工具函数
|
|
103
|
+
export function parseTopo(raw: object): any;
|
|
104
|
+
export function computeOrthogonalLayout(model: any, options?: object): any;
|
|
105
|
+
export function assertOrthogonal(model: any, edgeCP: Map<string, number[][]>): string[];
|
|
106
|
+
export function buildGraphData(model: any, layout: any): any;
|
|
107
|
+
export function edgeStyle(datum: any): any;
|
|
108
|
+
export function calculateTopDistances(edges: any[], nodes: any[], edgeCP: Map<string, number[][]>, fishboneRibs: Set<string>, topN?: number): any[];
|
|
109
|
+
export function getSymbolSpec(cat: string): { w: number, h: number };
|
|
110
|
+
|
|
111
|
+
// 常量
|
|
112
|
+
export const SYMBOL_SPEC: Record<string, { w: number, h: number }>;
|
|
113
|
+
export const FONT: string;
|
|
114
|
+
export const VERSION: string;
|
|
115
|
+
export const DESCRIPTION: string;
|
package/dist/topo-engine.cjs
CHANGED
|
@@ -28396,7 +28396,7 @@ const _sfc_main = {
|
|
|
28396
28396
|
}
|
|
28397
28397
|
};
|
|
28398
28398
|
const DistanceTop10 = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-936b875b"]]);
|
|
28399
|
-
const VERSION = "0.1.
|
|
28399
|
+
const VERSION = "0.1.2";
|
|
28400
28400
|
const DESCRIPTION = "配电台区单线图拓扑成图引擎";
|
|
28401
28401
|
exports.DESCRIPTION = DESCRIPTION;
|
|
28402
28402
|
exports.DistanceTop10 = DistanceTop10;
|