topo-engine 0.1.6 → 0.1.7

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
@@ -6,7 +6,7 @@
6
6
 
7
7
  ## 功能特性
8
8
 
9
- - 🧩 **Vue 3 组件** — `TopoGraph`(画布,含标题栏 / 统计 / 适应画布 / 导出 PNG 按钮)、`SidePanel`(设备详情与上下游)、`DistanceTop10`(父子连线距离 Top10 面板)
9
+ - 🧩 **Vue 3 组件** — `TopoGraph`(画布;可选左上工具条默认隐藏,`:toolbar` 开启)、`SidePanel`(设备详情与上下游)、`DistanceTop10`(父子连线距离 Top10 面板)
10
10
  - 🎨 **深色 / 浅色双主题** — 黑底 SCADA 或白底两种画布风格,图元 / 导线 / 文字随之配套适配,可随时切换(`theme` prop 或工具栏按钮)。默认浅色主题
11
11
  - 📐 **自研正交布局** — 主干沿最长路径横向铺开 + 分支带垂直直走,整图**横平竖直、0 交叉、0 重叠**,边缘路由为直线 / L 形折线
12
12
  - 🔍 **拓扑查询 API** — 节点 / 边 / 邻居 / 子树 / 主干链 / 路径 / 坐标 查询
@@ -17,7 +17,7 @@
17
17
  - 🔗 **连接线 / 拓扑线** — 任意两节点动态连接线、路径拓扑高亮线
18
18
  - 🎧 **事件系统** — 点击 / 悬停 / 画布空白 / 视口变化事件总线
19
19
  - 💾 **导出** — 整图 PNG 下载、DataURL、图数据 JSON
20
- - 📦 **零配置渲染** — 组件自带深色主题与工具栏,`data` 或 `dataUrl` 二选一即可出图
20
+ - 📦 **零配置渲染** — 组件内置主题与解析渲染,`data` 或 `dataUrl` 二选一即可出图
21
21
 
22
22
  ## 安装
23
23
 
@@ -33,6 +33,9 @@ npm install topo-engine
33
33
  import 'topo-engine/style.css';
34
34
  ```
35
35
 
36
+ > 📖 **完整 API 参考手册**随包发布:安装后可打开 `node_modules/topo-engine/docs/API.md`,
37
+ > 或在线查看仓库中的 [docs/API.md](docs/API.md)(事件、卡片、连接线、布局参数、TS 类型等一应俱全)。
38
+
36
39
  ## 快速开始
37
40
 
38
41
  ### 1. 最小示例 —— 只画一张图
@@ -186,20 +189,24 @@ const g6Data = buildGraphData(model, layout); // → G6 可直接消费的 { n
186
189
  | ---- | ---- | ---- | ---- |
187
190
  | `dataUrl` | `String` | `''` | 拓扑 JSON 的 URL(内部 `fetch` 加载)。变化后自动重新加载 |
188
191
  | `data` | `Object` | `null` | 直接传入 JSON 对象;优先级高于 `dataUrl`;深度 watch,变化自动重绘 |
192
+ | `customerData` | `Array` | `null` | 客户数据(`[{ psrId, consumerList }]`),注入计量箱并按户渲染 / 可点 |
189
193
  | `theme` | `'dark' \| 'light'` | `'light'` | 画布主题:`dark`(黑底 SCADA)/ `light`(白底,图元配套加深,默认)。支持 `v-model:theme`,也可点内置工具栏 ☀️/🌙 按钮切换 |
194
+ | `toolbar` | `Boolean` | `false` | 是否显示左上工具条(标题 / 统计 / 主题 / 适应画布 / 导出 PNG),默认隐藏 |
190
195
 
191
196
  ### Emits
192
197
 
193
198
  | 事件 | 载荷 | 触发时机 |
194
199
  | ---- | ---- | ---- |
195
200
  | `loaded` | `{ title, stats }`,`stats = { total, depth, leaves, byCat }` | 每次数据渲染完成 |
196
- | `node-select` | `{ node, upstream, downstream }`(均为解析后模型节点) | 点击节点(含金框选中与详情联动) |
201
+ | `node-select` | `{ node, upstream, downstream }`(均为解析后模型节点) | 点击节点(含金框选中与详情联动);空白清除时 `node=null` |
202
+ | `customer-select` | `{ node, customer, index, count, upstream, downstream }` | 点中计量箱内某个用户(展示客户档案) |
197
203
  | `top-distances` | `Array<{ parent: {id,name,catLabel}, child: {id,name,catLabel}, distance }>`(≤10) | 渲染完成时,父-子连线折线长度 Top10(过滤站内边与主干鱼骨边) |
198
204
 
199
205
  ### 内置交互与 UI
200
206
 
201
207
  - 滚轮缩放、拖拽平移、悬停高亮(上下游与连线),点击选中(金色虚框,再次点空白取消)。
202
- - 左上角深色工具栏:标题 / `节点 n · m · 深度 d · 叶子 l` 统计 / 「适应画布」/「导出 PNG」按钮。
208
+ - **计量箱用户点击**:单户箱整箱即该户;多户箱点电表格 / 户名 = 该户客户档案(悬停青圈、选中金圈),点箱体空白 = 选中整箱(事件 `consumer:click` / `customer-select`)。
209
+ - 左上角工具条**默认隐藏**:传 `:toolbar="true"` 后显示标题 / `节点 n · 边 m · 深度 d · 叶子 l` 统计 / 「适应画布」/「导出 PNG」按钮。
203
210
  - 加载中 / 出错(如 dataUrl 404)有内置遮罩提示。
204
211
  - 卸载时自动 `destroy()` 图实例与事件监听,无内存泄漏。
205
212
 
@@ -211,6 +218,8 @@ const g6Data = buildGraphData(model, layout); // → G6 可直接消费的 { n
211
218
  | ---- | ---- |
212
219
  | `loadRaw(jsonText)` | 用一段 JSON 文本(如本地文件上传内容)重新成图 |
213
220
  | `selectNode(id)` | 选中某节点并联动 `node-select` 事件 |
221
+ | `selectCustomer(nodeId, index)` | 编程式选中计量箱内第 `index` 户(联动 `customer-select`) |
222
+ | `getSelectedCustomer()` | 当前选中的户:`{ nodeId, index, customer, node } \| null` |
214
223
  | `resetView()` | 适应画布 |
215
224
  | `exportPng()` | 导出整图 PNG(按画布标题命名下载) |
216
225
  | `getLayoutData()` | 获取当前正交布局数据(坐标 / 折线 / stats) |
@@ -220,7 +229,7 @@ const g6Data = buildGraphData(model, layout); // → G6 可直接消费的 { n
220
229
 
221
230
  引擎内置两套主题,**浅色为默认**(白底,图元配套深色,适合打印或嵌入浅色页面);深色为**黑底 SCADA 风格**。
222
231
 
223
- - `TopoGraph`:`:theme` 取值 `'dark'`(默认)或 `'light'`,推荐 `v-model:theme` 双向绑定;组件左上工具栏内置 ☀️ / 🌙 切换按钮(点击 emit `update:theme`)。
232
+ - `TopoGraph`:`:theme` 取值 `'light'`(默认)或 `'dark'`,推荐 `v-model:theme` 双向绑定;开启 `:toolbar="true"` 后工具条内置 ☀️ / 🌙 切换按钮(点击 emit `update:theme`)。
224
233
  - `SidePanel` / `DistanceTop10`:同样接收 `theme` prop,与画布保持一致观感。
225
234
  - 切换主题会**按当前数据自动重建画布**:G6 画布背景、图元、边、标注文字全部随主题更新,布局不变。
226
235
 
@@ -336,6 +345,7 @@ topo.removeAllTopoLines();
336
345
  | `canvas:click` | `{ event }` | 画布空白处点击 |
337
346
  | `viewport:change` | `{ zoom, center }` | 视口缩放 / 平移变化 |
338
347
  | `card:button` | `{ nodeId, buttonIndex }` | 卡片按钮点击 |
348
+ | `consumer:click` | `{ nodeId, index, customer, node }` | 点中计量箱内用户(多户箱点格子;单户箱整箱即该户) |
339
349
 
340
350
  ```javascript
341
351
  const handler = ({ nodeId }) => console.log('点击了', nodeId);
@@ -361,10 +371,13 @@ topo.off('node:click', handler); // 不传 handler 则移除该事件全部监
361
371
  | 模式 | 条件 | 渲染效果 |
362
372
  | ---- | ---- | ---- |
363
373
  | 无客户数据 | 未传入 customerData 或无匹配 | 原始 JX 黑底白字方框 |
364
- | 单用户 | customerList 有 1 个用户 | 电能表图标(圆圈 + 竖线)+ 用户名 |
374
+ | 单用户 | customerList 有 1 个用户 | 电能表图标(圆圈 + 竖线)+ 用户名;**整箱即该户,点击即客户档案** |
365
375
  | 多用户(显示名称) | customerList 2~20 个用户 | 方框内电能表网格,每个格子含图标 + 用户名(每行最多 5 个) |
366
376
  | 多用户(仅图标) | customerList >20 个用户 | 方框内电能表网格,仅显示图标(格子更紧凑) |
367
377
 
378
+ > 交互:多用户箱内**点击某个电表格 / 户名 = 选中该户**(金圈高亮 + `customer-select` / `consumer:click`),
379
+ > 点箱体空白 / 外框 = 选中整箱;悬停该户有青圈提示。用户端点(连接线)用客户档案 `assetNo` 引用即可。
380
+
368
381
  客户数据格式(与拓扑 JSON 配套的 customer JSON):
369
382
 
370
383
  ```json
package/dist/index.d.ts CHANGED
@@ -2,6 +2,11 @@
2
2
  * 拓扑引擎类型声明文件
3
3
  */
4
4
 
5
+ // 让本类型声明在消费方 tsconfig 未开启 ES2015+ lib 时也能编译:
6
+ // 声明里用到了 Map/Promise(TS2583: Cannot find name 'Map'),此处自动引入对应 lib 定义。
7
+ /// <reference lib="es2015.collection" />
8
+ /// <reference lib="es2015.promise" />
9
+
5
10
  // 核心 API
6
11
  export class TopoApi {
7
12
  constructor(graph: any, model: any, layout: any, nodeByIdMap: Map<string, any>);
@@ -69,8 +74,9 @@ export class TopoApi {
69
74
  hideCard(nodeId: string): void;
70
75
  hideAllCards(): void;
71
76
 
72
- // 连接线与拓扑线
73
- addConnection(fromId: string, toId: string, opts?: object): void;
77
+ // 连接线与拓扑线(端点支持节点 id 或箱内用户的客户档案编号 consNo/consId)
78
+ resolveUserRef(ref: string): string | null;
79
+ addConnection(fromId: string, toId: string, opts?: object): string | false;
74
80
  removeConnection(fromId: string, toId: string): void;
75
81
  removeAllConnections(): void;
76
82
  addTopoLine(nodeIds: string[], opts?: object): string;
@@ -112,6 +118,7 @@ export function getSymbolSpec(cat: string): { w: number, h: number };
112
118
  export function buildCustomerMap(customerData: Array<any>): Map<string, Array<any>>;
113
119
  export function injectCustomers(model: any, customerMap: Map<string, Array<any>>): void;
114
120
  export function getMeterBoxSize(node: any): { w: number, h: number } | null;
121
+ export function getCustomerLocalPositions(node: any): Array<{ x: number, y: number }>;
115
122
 
116
123
  // 常量
117
124
  export const SYMBOL_SPEC: Record<string, { w: number, h: number }>;
package/dist/style.css CHANGED
@@ -1,6 +1,6 @@
1
1
 
2
2
  /* —— 主题变量:dark 为默认值,light 由 [data-theme='light'] 覆盖 —— */
3
- .topo-wrap[data-v-b02bff98] {
3
+ .topo-wrap[data-v-c2cd8b2c] {
4
4
  --tp-bg: #050505;
5
5
  --tp-toolbar-bg: rgba(20, 20, 20, 0.88);
6
6
  --tp-toolbar-border: #333;
@@ -19,7 +19,7 @@
19
19
  overflow: hidden;
20
20
  background: var(--tp-bg);
21
21
  }
22
- .topo-wrap[data-theme='light'][data-v-b02bff98] {
22
+ .topo-wrap[data-theme='light'][data-v-c2cd8b2c] {
23
23
  --tp-bg: #ffffff;
24
24
  --tp-toolbar-bg: rgba(255, 255, 255, 0.92);
25
25
  --tp-toolbar-border: #d9d9d9;
@@ -33,11 +33,11 @@
33
33
  --tp-text: #555;
34
34
  --tp-error: #d32f2f;
35
35
  }
36
- .topo-container[data-v-b02bff98] {
36
+ .topo-container[data-v-c2cd8b2c] {
37
37
  width: 100%;
38
38
  height: 100%;
39
39
  }
40
- .topo-toolbar[data-v-b02bff98] {
40
+ .topo-toolbar[data-v-c2cd8b2c] {
41
41
  position: absolute;
42
42
  top: 10px;
43
43
  left: 12px;
@@ -52,14 +52,14 @@
52
52
  color: var(--tp-text);
53
53
  z-index: 10;
54
54
  }
55
- .topo-title[data-v-b02bff98] {
55
+ .topo-title[data-v-c2cd8b2c] {
56
56
  color: var(--tp-title);
57
57
  font-weight: 600;
58
58
  }
59
- .topo-meta[data-v-b02bff98] {
59
+ .topo-meta[data-v-c2cd8b2c] {
60
60
  color: var(--tp-meta);
61
61
  }
62
- .btn[data-v-b02bff98] {
62
+ .btn[data-v-c2cd8b2c] {
63
63
  margin-left: 4px;
64
64
  padding: 3px 10px;
65
65
  border: 1px solid var(--tp-btn-border);
@@ -69,12 +69,12 @@
69
69
  cursor: pointer;
70
70
  font-size: 12px;
71
71
  }
72
- .btn[data-v-b02bff98]:hover {
72
+ .btn[data-v-c2cd8b2c]:hover {
73
73
  border-color: var(--tp-accent);
74
74
  color: var(--tp-accent);
75
75
  }
76
- .topo-loading[data-v-b02bff98],
77
- .topo-error[data-v-b02bff98] {
76
+ .topo-loading[data-v-c2cd8b2c],
77
+ .topo-error[data-v-c2cd8b2c] {
78
78
  position: absolute;
79
79
  inset: 0;
80
80
  display: flex;
@@ -85,12 +85,12 @@
85
85
  z-index: 5;
86
86
  background: var(--tp-overlay-bg);
87
87
  }
88
- .topo-error[data-v-b02bff98] {
88
+ .topo-error[data-v-c2cd8b2c] {
89
89
  color: var(--tp-error);
90
90
  }
91
91
 
92
92
  /* —— 主题变量:dark 默认,light 由 [data-theme='light'] 覆盖 —— */
93
- .panel[data-v-547b7d57] {
93
+ .panel[data-v-f5a2adf8] {
94
94
  --sp-bg: rgba(16, 16, 18, 0.96);
95
95
  --sp-border: #2c2c30;
96
96
  --sp-text: #ddd;
@@ -116,7 +116,7 @@
116
116
  flex-direction: column;
117
117
  color: var(--sp-text);
118
118
  }
119
- .panel[data-theme='light'][data-v-547b7d57] {
119
+ .panel[data-theme='light'][data-v-f5a2adf8] {
120
120
  --sp-bg: rgba(255, 255, 255, 0.97);
121
121
  --sp-border: #e4e4ea;
122
122
  --sp-text: #333;
@@ -135,7 +135,7 @@
135
135
  --sp-hint: #8a8a8a;
136
136
  --sp-label: #666;
137
137
  }
138
- .panel-head[data-v-547b7d57] {
138
+ .panel-head[data-v-f5a2adf8] {
139
139
  display: flex;
140
140
  align-items: center;
141
141
  justify-content: space-between;
@@ -145,7 +145,7 @@
145
145
  color: var(--sp-title);
146
146
  border-bottom: 1px solid var(--sp-border);
147
147
  }
148
- .panel-close[data-v-547b7d57] {
148
+ .panel-close[data-v-f5a2adf8] {
149
149
  border: none;
150
150
  background: none;
151
151
  color: var(--sp-close);
@@ -153,57 +153,86 @@
153
153
  cursor: pointer;
154
154
  line-height: 1;
155
155
  }
156
- .panel-close[data-v-547b7d57]:hover {
156
+ .panel-close[data-v-f5a2adf8]:hover {
157
157
  color: var(--sp-close-hover);
158
158
  }
159
- .panel-body[data-v-547b7d57] {
159
+ .panel-body[data-v-f5a2adf8] {
160
160
  padding: 14px;
161
161
  overflow-y: auto;
162
162
  flex: 1;
163
163
  }
164
- .kv[data-v-547b7d57] {
164
+ .kv[data-v-f5a2adf8] {
165
165
  display: flex;
166
166
  gap: 10px;
167
167
  margin-bottom: 10px;
168
168
  font-size: 13px;
169
169
  word-break: break-all;
170
170
  }
171
- .kv .k[data-v-547b7d57] {
171
+ .kv .k[data-v-f5a2adf8] {
172
172
  color: var(--sp-sub);
173
173
  flex: none;
174
- width: 64px;
174
+ width: 74px;
175
175
  }
176
- .kv .v[data-v-547b7d57] {
176
+ .kv .v[data-v-f5a2adf8] {
177
177
  color: var(--sp-text);
178
178
  }
179
- .mono[data-v-547b7d57] {
179
+ .mono[data-v-f5a2adf8] {
180
180
  font-family: Consolas, monospace;
181
181
  font-size: 12px;
182
182
  color: var(--sp-mono);
183
183
  }
184
- .ok[data-v-547b7d57] {
184
+ .ok[data-v-f5a2adf8] {
185
185
  color: var(--sp-ok);
186
186
  }
187
- .warn[data-v-547b7d57] {
187
+ .warn[data-v-f5a2adf8] {
188
188
  color: var(--sp-warn);
189
+ }
190
+ /* 客户档案:所属计量箱信息卡 + 返回按钮 */
191
+ .cust-box[data-v-f5a2adf8] {
192
+ display: flex;
193
+ flex-direction: column;
194
+ gap: 8px;
195
+ padding: 8px 10px;
196
+ margin-bottom: 12px;
197
+ background: var(--sp-item);
198
+ border: 1px solid var(--sp-item-border);
199
+ border-radius: 5px;
200
+ }
201
+ .cust-box .kv[data-v-f5a2adf8] {
202
+ margin-bottom: 0;
203
+ }
204
+ .panel-back[data-v-f5a2adf8] {
205
+ align-self: flex-start;
206
+ border: 1px solid var(--sp-item-border);
207
+ border-radius: 4px;
208
+ background: var(--sp-item);
209
+ color: var(--sp-accent);
210
+ font-size: 12px;
211
+ padding: 3px 10px;
212
+ cursor: pointer;
213
+ }
214
+ .panel-back[data-v-f5a2adf8]:hover {
215
+ border-color: var(--sp-hover-border);
216
+ background: var(--sp-hover-bg);
217
+ color: var(--sp-title);
189
218
  }
190
- .neighbor-section[data-v-547b7d57] {
219
+ .neighbor-section[data-v-f5a2adf8] {
191
220
  margin-top: 16px;
192
221
  border-top: 1px solid var(--sp-border);
193
222
  padding-top: 12px;
194
223
  }
195
- .neighbor-title[data-v-547b7d57] {
224
+ .neighbor-title[data-v-f5a2adf8] {
196
225
  font-size: 13px;
197
226
  font-weight: 600;
198
227
  color: var(--sp-title);
199
228
  margin-bottom: 6px;
200
229
  }
201
- .neighbor-list[data-v-547b7d57] {
230
+ .neighbor-list[data-v-f5a2adf8] {
202
231
  display: flex;
203
232
  flex-direction: column;
204
233
  gap: 4px;
205
234
  }
206
- .neighbor-item[data-v-547b7d57] {
235
+ .neighbor-item[data-v-f5a2adf8] {
207
236
  display: flex;
208
237
  flex-direction: column;
209
238
  gap: 3px;
@@ -214,34 +243,34 @@
214
243
  cursor: pointer;
215
244
  font-size: 12px;
216
245
  }
217
- .neighbor-item[data-v-547b7d57]:hover {
246
+ .neighbor-item[data-v-f5a2adf8]:hover {
218
247
  border-color: var(--sp-hover-border);
219
248
  background: var(--sp-hover-bg);
220
249
  }
221
- .n-main[data-v-547b7d57] {
250
+ .n-main[data-v-f5a2adf8] {
222
251
  display: flex;
223
252
  align-items: center;
224
253
  gap: 8px;
225
254
  min-width: 0;
226
255
  }
227
- .n-type[data-v-547b7d57] {
256
+ .n-type[data-v-f5a2adf8] {
228
257
  flex: none;
229
258
  color: var(--sp-accent);
230
259
  font-size: 11px;
231
260
  }
232
- .n-name[data-v-547b7d57] {
261
+ .n-name[data-v-f5a2adf8] {
233
262
  color: var(--sp-text);
234
263
  overflow: hidden;
235
264
  text-overflow: ellipsis;
236
265
  white-space: nowrap;
237
266
  }
238
- .n-psr[data-v-547b7d57] {
267
+ .n-psr[data-v-f5a2adf8] {
239
268
  display: flex;
240
269
  align-items: center;
241
270
  gap: 6px;
242
271
  min-width: 0;
243
272
  }
244
- .n-psr-id[data-v-547b7d57] {
273
+ .n-psr-id[data-v-f5a2adf8] {
245
274
  font-family: Consolas, monospace;
246
275
  font-size: 11px;
247
276
  color: var(--sp-mono);
@@ -252,7 +281,7 @@
252
281
  user-select: text;
253
282
  -webkit-user-select: text;
254
283
  }
255
- .copy-btn[data-v-547b7d57] {
284
+ .copy-btn[data-v-f5a2adf8] {
256
285
  flex: none;
257
286
  border: 1px solid var(--sp-item-border);
258
287
  border-radius: 3px;
@@ -263,40 +292,40 @@
263
292
  padding: 2px 5px;
264
293
  cursor: pointer;
265
294
  }
266
- .copy-btn[data-v-547b7d57]:hover {
295
+ .copy-btn[data-v-f5a2adf8]:hover {
267
296
  border-color: var(--sp-hover-border);
268
297
  color: var(--sp-title);
269
298
  background: var(--sp-hover-bg);
270
299
  }
271
- .neighbor-empty[data-v-547b7d57] {
300
+ .neighbor-empty[data-v-f5a2adf8] {
272
301
  font-size: 12px;
273
302
  color: var(--sp-hint);
274
303
  padding: 4px 0;
275
304
  }
276
- .stat-grid[data-v-547b7d57] {
305
+ .stat-grid[data-v-f5a2adf8] {
277
306
  display: grid;
278
307
  grid-template-columns: 1fr 1fr;
279
308
  gap: 10px;
280
309
  margin-bottom: 16px;
281
310
  }
282
- .stat[data-v-547b7d57] {
311
+ .stat[data-v-f5a2adf8] {
283
312
  background: var(--sp-item);
284
313
  border: 1px solid var(--sp-item-border);
285
314
  border-radius: 6px;
286
315
  padding: 10px;
287
316
  text-align: center;
288
317
  }
289
- .stat-num[data-v-547b7d57] {
318
+ .stat-num[data-v-f5a2adf8] {
290
319
  font-size: 20px;
291
320
  font-weight: 700;
292
321
  color: var(--sp-accent);
293
322
  }
294
- .stat-label[data-v-547b7d57] {
323
+ .stat-label[data-v-f5a2adf8] {
295
324
  font-size: 12px;
296
325
  color: var(--sp-label);
297
326
  margin-top: 2px;
298
327
  }
299
- .hint[data-v-547b7d57] {
328
+ .hint[data-v-f5a2adf8] {
300
329
  font-size: 12px;
301
330
  color: var(--sp-hint);
302
331
  line-height: 1.7;