topo-engine 0.1.8 → 0.1.9

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.
@@ -25095,11 +25095,14 @@ class TopoApi {
25095
25095
  this._topoLines = /* @__PURE__ */ new Map();
25096
25096
  this._cards = /* @__PURE__ */ new Map();
25097
25097
  this._animations = /* @__PURE__ */ new Map();
25098
+ this._glowNodes = /* @__PURE__ */ new Map();
25099
+ this._glowRaf = null;
25098
25100
  this._nodeLabels = /* @__PURE__ */ new Map();
25099
25101
  this._originalNodeStyles = /* @__PURE__ */ new Map();
25100
25102
  this._originalEdgeStyles = /* @__PURE__ */ new Map();
25101
25103
  this._nodeColorOverrides = /* @__PURE__ */ new Map();
25102
25104
  this._custColorOverrides = /* @__PURE__ */ new Map();
25105
+ this._cellFx = /* @__PURE__ */ new Map();
25103
25106
  this._bindConnGraphEvents();
25104
25107
  }
25105
25108
  /** 数据刷新(图重新渲染后调用) */
@@ -25124,6 +25127,7 @@ class TopoApi {
25124
25127
  this._removeAllConnectionsInternal();
25125
25128
  this._unbindConnGraphEvents();
25126
25129
  this._removeAllAnimationsInternal();
25130
+ this._removeAllGlowsInternal();
25127
25131
  this._removeAllTopoLinesInternal();
25128
25132
  this._hideAllCardsInternal();
25129
25133
  this._listeners.clear();
@@ -25132,6 +25136,7 @@ class TopoApi {
25132
25136
  this._nodeLabels.clear();
25133
25137
  this._nodeColorOverrides.clear();
25134
25138
  this._custColorOverrides.clear();
25139
+ this._cellFx.clear();
25135
25140
  this._graph = null;
25136
25141
  this._model = null;
25137
25142
  this._layout = null;
@@ -25249,11 +25254,163 @@ class TopoApi {
25249
25254
  }
25250
25255
  }
25251
25256
  // ============================================================
25257
+ // 统一目标解析(id / psrId / assetNo)
25258
+ //
25259
+ // 全库约定:凡入参语义为「节点/设备/用户」的方法,一律接受三种引用:
25260
+ // - 节点内部 id 或 psrId(设备编码) → 普通节点(变压器/开关/导线点/计量箱…)
25261
+ // - 客户 assetNo(资产编号,兼容 consNo/consId)→ 计量箱内该户「电表户」
25262
+ // 解析结果:
25263
+ // { kind:'node', id, node } 普通节点
25264
+ // { kind:'customer', boxId, index, boxNode, customer } 箱内电表户
25265
+ // ============================================================
25266
+ /** 统一解析 ref(内部实现;对外用 resolveRef) */
25267
+ _resolveTarget(ref2) {
25268
+ if (ref2 == null) return null;
25269
+ const e2 = this._parseEndpoint(ref2);
25270
+ if (!e2) return null;
25271
+ if (e2.kind === "node") {
25272
+ return { kind: "node", id: e2.id, node: this._nodeByIdMap.get(e2.id) || null };
25273
+ }
25274
+ const boxNode = this._nodeByIdMap.get(e2.boxId) || null;
25275
+ const customers = boxNode && Array.isArray(boxNode._customers) ? boxNode._customers : [];
25276
+ return {
25277
+ kind: "customer",
25278
+ boxId: e2.boxId,
25279
+ index: e2.index,
25280
+ boxNode,
25281
+ customer: customers[e2.index] || null,
25282
+ customers
25283
+ };
25284
+ }
25285
+ /** 解析到“节点 id”层:customer 统一落到其所在计量箱节点 */
25286
+ _targetNodeId(ref2) {
25287
+ const t = this._resolveTarget(ref2);
25288
+ return t ? t.kind === "node" ? t.id : t.boxId : null;
25289
+ }
25290
+ /** 解析到“图 id”层:普通节点=节点 id;电表户=`${boxId}#cust${index}`(唯一键) */
25291
+ _targetGraphKey(ref2) {
25292
+ const t = this._resolveTarget(ref2);
25293
+ if (!t) return null;
25294
+ return t.kind === "node" ? t.id : `${t.boxId}#cust${t.index}`;
25295
+ }
25296
+ /** 目标中心的世界坐标(节点中心 / 户表位中心) */
25297
+ _targetWorld(ref2) {
25298
+ const e2 = this._parseEndpoint(ref2);
25299
+ if (!e2) return null;
25300
+ if (e2.kind === "customer") return this._endpointWorld(e2);
25301
+ const p = this._layout.pos.get(e2.id);
25302
+ return p ? { x: p.x, y: p.y } : null;
25303
+ }
25304
+ // ---- 箱内电表户特效(_cellFx)读写 ----
25305
+ _cellFxKey(boxId, index) {
25306
+ return `${boxId}#cust${index}`;
25307
+ }
25308
+ /** 取某户特效 descriptor(无则新建空对象,不落库) */
25309
+ _cellFxGet(boxId, index) {
25310
+ const key = this._cellFxKey(boxId, index);
25311
+ let d2 = this._cellFx.get(key);
25312
+ if (!d2) {
25313
+ d2 = {};
25314
+ this._cellFx.set(key, d2);
25315
+ }
25316
+ return d2;
25317
+ }
25318
+ /** 删除某户特效键(若因此无任何特效则一并清除该箱 style 上的 _cellFx) */
25319
+ _cellFxDrop(boxId, index) {
25320
+ const key = this._cellFxKey(boxId, index);
25321
+ this._cellFx.delete(key);
25322
+ }
25323
+ /** 把某只计量箱的 _cellFx 映射刷成 style(无特效则移除 style 字段) */
25324
+ _flushCellFx(boxId) {
25325
+ if (!this._graph) return;
25326
+ const out = {};
25327
+ let any = false;
25328
+ for (const [key, d2] of this._cellFx) {
25329
+ if (!key.startsWith(`${boxId}#cust`)) continue;
25330
+ const idx = Number(key.slice(key.indexOf("#cust") + 5));
25331
+ if (!d2 || !d2.hl && !d2.note && !d2.glow && !d2.pulse) {
25332
+ this._cellFx.delete(key);
25333
+ continue;
25334
+ }
25335
+ out[idx] = d2;
25336
+ any = true;
25337
+ }
25338
+ const cur = this._safeNodeStyle(boxId, {});
25339
+ this._graph.updateNodeData([{ id: boxId, style: { ...cur, _cellFx: any ? out : void 0 } }]);
25340
+ this._graph.render();
25341
+ }
25342
+ /** 清理失效的户特效记录(箱/户不存在时自动丢弃),返回受影响箱列表 */
25343
+ _pruneCellFx() {
25344
+ const touched = /* @__PURE__ */ new Set();
25345
+ for (const [key] of this._cellFx) {
25346
+ const sep = key.indexOf("#cust");
25347
+ if (sep < 0) {
25348
+ this._cellFx.delete(key);
25349
+ continue;
25350
+ }
25351
+ const boxId = key.slice(0, sep);
25352
+ const idx = Number(key.slice(sep + 5));
25353
+ const box2 = this._nodeByIdMap.get(boxId);
25354
+ if (!box2 || !Array.isArray(box2._customers) || idx < 0 || idx >= box2._customers.length) {
25355
+ this._cellFx.delete(key);
25356
+ touched.add(boxId);
25357
+ }
25358
+ }
25359
+ return touched;
25360
+ }
25361
+ // ============================================================
25252
25362
  // 一、查询 API
25253
25363
  // ============================================================
25254
- /** 获取单个节点 */
25364
+ /** 获取单个节点(ref = 节点 id/psrId/资产编号 assetNo)
25365
+ * 命中电表户时返回其所在计量箱节点,并附带 customer/customerIndex 等客户信息 */
25255
25366
  getNode(nodeId) {
25256
- return this._nodeByIdMap.get(nodeId) || null;
25367
+ const t = this._resolveTarget(nodeId);
25368
+ if (!t) return null;
25369
+ if (t.kind === "node") return t.node || null;
25370
+ if (!t.boxNode) return null;
25371
+ const box2 = t.boxNode;
25372
+ return {
25373
+ ...box2,
25374
+ targetKind: "customer",
25375
+ meterBoxId: t.boxId,
25376
+ customer: t.customer,
25377
+ customerIndex: t.index,
25378
+ customerCount: t.customers.length
25379
+ };
25380
+ }
25381
+ /**
25382
+ * 解析任意引用 → 标准目标描述(调试 / 业务分支判断用)。
25383
+ * @param {string} ref 节点 id / psrId / 资产编号(assetNo,兼容 consNo/consId)
25384
+ * @returns {object|null}
25385
+ * kind='node' → { kind, id, node }
25386
+ * kind='customer' → { kind, boxId, boxNode, index, customer, key, assetNo, name }
25387
+ */
25388
+ resolveRef(ref2) {
25389
+ const t = this._resolveTarget(ref2);
25390
+ if (!t) return null;
25391
+ if (t.kind === "node") {
25392
+ return { kind: "node", id: t.id, node: t.node };
25393
+ }
25394
+ const c = t.customer || {};
25395
+ return {
25396
+ kind: "customer",
25397
+ boxId: t.boxId,
25398
+ boxNode: t.boxNode,
25399
+ index: t.index,
25400
+ customer: t.customer,
25401
+ key: this._cellFxKey(t.boxId, t.index),
25402
+ assetNo: c.assetNo != null ? c.assetNo : c.consNo != null ? c.consNo : c.consId,
25403
+ name: c.realConsName || c.consName || ""
25404
+ };
25405
+ }
25406
+ /**
25407
+ * 按资产编号 / 客户编号取客户档案包装(也可传所在箱的 psrId 时返回 null)。
25408
+ * @returns {{ node, customer, index, count, boxId } | null}
25409
+ */
25410
+ getCustomer(ref2) {
25411
+ const t = this._resolveTarget(ref2);
25412
+ if (!t || t.kind !== "customer") return null;
25413
+ return { node: t.boxNode, customer: t.customer, index: t.index, count: t.customers.length, boxId: t.boxId };
25257
25414
  }
25258
25415
  /** 获取所有节点(只读副本) */
25259
25416
  getAllNodes() {
@@ -25285,15 +25442,17 @@ class TopoApi {
25285
25442
  getSelectedId() {
25286
25443
  return this._selectedId;
25287
25444
  }
25288
- /** 获取上下游邻居 */
25445
+ /** 获取上下游邻居(ref 可传 节点id/psrId/资产编号;电表户按所在计量箱参与拓扑) */
25289
25446
  getNeighbors(nodeId) {
25447
+ const id2 = this._targetNodeId(nodeId);
25448
+ if (id2 == null) return { upstream: [], downstream: [] };
25290
25449
  const upstream = [];
25291
25450
  const downstream = [];
25292
25451
  for (const e2 of this._model.edges) {
25293
- if (e2.target === nodeId) {
25452
+ if (e2.target === id2) {
25294
25453
  const p = this._nodeByIdMap.get(e2.source);
25295
25454
  if (p) upstream.push(p);
25296
- } else if (e2.source === nodeId) {
25455
+ } else if (e2.source === id2) {
25297
25456
  const c = this._nodeByIdMap.get(e2.target);
25298
25457
  if (c) downstream.push(c);
25299
25458
  }
@@ -25302,11 +25461,13 @@ class TopoApi {
25302
25461
  }
25303
25462
  /**
25304
25463
  * 沿拓扑流向取节点链
25305
- * @param {string} nodeId - 起始节点
25464
+ * @param {string} ref - 节点 id / psrId / 资产编号(电表户按所在箱)
25306
25465
  * @param {'upstream'|'downstream'} direction
25307
25466
  */
25308
- getStreamNodes(nodeId, direction2) {
25309
- return this._bfs(nodeId, direction2).nodes;
25467
+ getStreamNodes(ref2, direction2) {
25468
+ const id2 = this._targetNodeId(ref2);
25469
+ if (id2 == null) return [];
25470
+ return this._bfs(id2, direction2).nodes;
25310
25471
  }
25311
25472
  /** 获取主干链节点 */
25312
25473
  getTrunkNodes() {
@@ -25344,11 +25505,13 @@ class TopoApi {
25344
25505
  for (const k of kids) w += this._subtreeWeight(k, childrenMap, visited);
25345
25506
  return w;
25346
25507
  }
25347
- /** 获取子树所有节点 */
25348
- getSubtreeNodes(nodeId) {
25508
+ /** 获取子树所有节点(ref 可传 节点id/psrId/资产编号) */
25509
+ getSubtreeNodes(ref2) {
25510
+ const id2 = this._targetNodeId(ref2);
25511
+ if (id2 == null) return [];
25349
25512
  const result = [];
25350
25513
  const visited = /* @__PURE__ */ new Set();
25351
- const queue = [nodeId];
25514
+ const queue = [id2];
25352
25515
  while (queue.length) {
25353
25516
  const cur = queue.shift();
25354
25517
  if (visited.has(cur)) continue;
@@ -25361,8 +25524,11 @@ class TopoApi {
25361
25524
  }
25362
25525
  return result;
25363
25526
  }
25364
- /** 获取两节点间路径(BFS */
25365
- getPath(fromId, toId) {
25527
+ /** 获取两节点间路径(BFS;端点可传 id/psrId/assetNo,电表户按所在箱) */
25528
+ getPath(fromRef, toRef) {
25529
+ const fromId = this._targetNodeId(fromRef);
25530
+ const toId = this._targetNodeId(toRef);
25531
+ if (fromId == null || toId == null) return null;
25366
25532
  const parent = /* @__PURE__ */ new Map([[fromId, null]]);
25367
25533
  const queue = [fromId];
25368
25534
  while (queue.length) {
@@ -25389,17 +25555,19 @@ class TopoApi {
25389
25555
  }
25390
25556
  return path;
25391
25557
  }
25392
- /** 获取指定边 */
25393
- getEdge(srcId, tgtId) {
25394
- return this._findEdgeData(srcId, tgtId);
25558
+ /** 获取指定边(端点可传 id/psrId/assetNo,电表户按所在箱) */
25559
+ getEdge(srcRef, tgtRef) {
25560
+ return this._findEdgeData(this._targetNodeId(srcRef), this._targetNodeId(tgtRef));
25395
25561
  }
25396
25562
  /** 获取所有边 */
25397
25563
  getEdges() {
25398
25564
  return [...this._model.edges];
25399
25565
  }
25400
- /** 沿流向取边 */
25401
- getStreamEdges(nodeId, direction2) {
25402
- return this._bfs(nodeId, direction2).edges;
25566
+ /** 沿流向取边(ref 可传 节点id/psrId/资产编号) */
25567
+ getStreamEdges(ref2, direction2) {
25568
+ const id2 = this._targetNodeId(ref2);
25569
+ if (id2 == null) return [];
25570
+ return this._bfs(id2, direction2).edges;
25403
25571
  }
25404
25572
  /** 获取主干边 */
25405
25573
  getMainEdge() {
@@ -25413,14 +25581,15 @@ class TopoApi {
25413
25581
  return true;
25414
25582
  });
25415
25583
  }
25416
- /** 获取节点坐标 */
25417
- getNodePosition(nodeId) {
25418
- const p = this._layout.pos.get(nodeId);
25419
- return p ? { x: p.x, y: p.y } : null;
25584
+ /** 获取节点 / 户表位中心坐标(assetNo → 该户表位中心;节点 id/psrId → 节点中心) */
25585
+ getNodePosition(ref2) {
25586
+ const world = this._targetWorld(ref2);
25587
+ return world ? { x: world.x, y: world.y } : null;
25420
25588
  }
25421
- /** 获取边折线点序列 */
25422
- getEdgePath(srcId, tgtId) {
25423
- const cps = this._layout.edgeCP.get(this._edgeKey(srcId, tgtId));
25589
+ /** 获取边折线点序列(端点可传 id/psrId/assetNo) */
25590
+ getEdgePath(srcRef, tgtRef) {
25591
+ const key = this._edgeKey(this._targetNodeId(srcRef), this._targetNodeId(tgtRef));
25592
+ const cps = this._layout.edgeCP.get(key);
25424
25593
  return cps ? cps.map(([x, y]) => [x, y]) : null;
25425
25594
  }
25426
25595
  /** 获取图整体边界 */
@@ -25460,17 +25629,29 @@ class TopoApi {
25460
25629
  const current = this._graph.getZoom();
25461
25630
  this._graph.zoomTo(current / (1 + step2));
25462
25631
  }
25463
- /** 定位节点到视口中央 */
25464
- locateNode(nodeId, zoom) {
25632
+ /** 定位节点到视口中央(ref 可传 id/psrId/资产编号;电表户聚焦其表位) */
25633
+ locateNode(ref2, zoom) {
25465
25634
  this._assertReady();
25466
- const p = this._layout.pos.get(nodeId);
25467
- if (!p) return;
25635
+ const t = this._resolveTarget(ref2);
25636
+ if (!t) return;
25637
+ const world = this._targetWorld(ref2);
25638
+ if (!world) return;
25468
25639
  if (zoom) this._graph.zoomTo(zoom);
25640
+ if (t.kind === "node") {
25641
+ try {
25642
+ this._graph.focusElement(t.id);
25643
+ return;
25644
+ } catch {
25645
+ }
25646
+ }
25469
25647
  try {
25470
- this._graph.focusElement(nodeId);
25471
- } catch {
25472
- const center = this._canvasCenterXY();
25473
- this._graph.translateTo({ x: center.x - p.x * (zoom || this._graph.getZoom()), y: center.y - p.y * (zoom || this._graph.getZoom()) });
25648
+ const z2 = zoom || this._graph.getZoom();
25649
+ const [vx, vy] = this._graph.getViewportCenter();
25650
+ const dx = (vx - world.x) * z2;
25651
+ const dy = (vy - world.y) * z2;
25652
+ this._graph.translateBy([dx, dy]);
25653
+ } catch (e2) {
25654
+ console.warn("[TopoApi] locateNode 平移失败:", e2 && e2.message);
25474
25655
  }
25475
25656
  }
25476
25657
  /** 获取当前视口状态 */
@@ -25483,25 +25664,34 @@ class TopoApi {
25483
25664
  // ============================================================
25484
25665
  // 三、节点样式
25485
25666
  // ============================================================
25486
- /** 设置单个节点样式 */
25487
- setNodeStyle(nodeId, style) {
25667
+ /** 设置单个节点样式(ref = 节点id/psrId;传 assetNo 时作用于其所在计量箱节点) */
25668
+ setNodeStyle(ref2, style) {
25488
25669
  this._assertReady();
25489
- this._snapshotNodeStyle(nodeId);
25490
- this._updateNodes([{ id: nodeId, style }]);
25670
+ const id2 = this._targetNodeId(ref2);
25671
+ if (id2 == null) {
25672
+ console.warn(`[TopoApi] setNodeStyle: 无法解析目标 ${ref2}`);
25673
+ return;
25674
+ }
25675
+ this._snapshotNodeStyle(id2);
25676
+ this._updateNodes([{ id: id2, style }]);
25491
25677
  }
25492
- /** 批量设置节点样式 */
25493
- batchSetNodeStyle(nodeIds, style) {
25678
+ /** 批量设置节点样式(每一项可为 id/psrId/assetNo) */
25679
+ batchSetNodeStyle(refs, style) {
25494
25680
  this._assertReady();
25495
- for (const id2 of nodeIds) this._snapshotNodeStyle(id2);
25496
- this._updateNodes(nodeIds.map((id2) => ({ id: id2, style })));
25681
+ const ids = refs.map((r) => this._targetNodeId(r)).filter((v) => v != null);
25682
+ if (!ids.length) return;
25683
+ for (const id2 of ids) this._snapshotNodeStyle(id2);
25684
+ this._updateNodes(ids.map((id2) => ({ id: id2, style })));
25497
25685
  }
25498
- /** 重置单个节点样式 */
25499
- resetNodeStyle(nodeId) {
25686
+ /** 重置单个节点样式(ref = 节点id/psrId/assetNo) */
25687
+ resetNodeStyle(ref2) {
25500
25688
  this._assertReady();
25501
- const orig = this._originalNodeStyles.get(nodeId);
25689
+ const id2 = this._targetNodeId(ref2);
25690
+ if (id2 == null) return;
25691
+ const orig = this._originalNodeStyles.get(id2);
25502
25692
  if (orig) {
25503
- this._updateNodes([{ id: nodeId, style: orig }]);
25504
- this._originalNodeStyles.delete(nodeId);
25693
+ this._updateNodes([{ id: id2, style: orig }]);
25694
+ this._originalNodeStyles.delete(id2);
25505
25695
  }
25506
25696
  }
25507
25697
  /** 重置所有节点样式 */
@@ -25741,28 +25931,43 @@ class TopoApi {
25741
25931
  // ============================================================
25742
25932
  // 四、边样式
25743
25933
  // ============================================================
25744
- /** 设置边样式 */
25745
- setEdgeStyle(srcId, tgtId, style) {
25934
+ /** 解析边端点 → 节点 id(customer → 所在计量箱;失败返回 null) */
25935
+ _edgeEndpoints(srcRef, tgtRef) {
25936
+ const a2 = this._targetNodeId(srcRef);
25937
+ const b = this._targetNodeId(tgtRef);
25938
+ return a2 != null && b != null ? [a2, b] : null;
25939
+ }
25940
+ /** 设置边样式(端点可为节点 id/psrId/资产编号 assetNo) */
25941
+ setEdgeStyle(srcRef, tgtRef, style) {
25746
25942
  this._assertReady();
25747
- const edgeId = this._edgeKey(srcId, tgtId);
25943
+ const ep = this._edgeEndpoints(srcRef, tgtRef);
25944
+ if (!ep) {
25945
+ console.warn(`[TopoApi] setEdgeStyle: 无法解析端点 ${srcRef} / ${tgtRef}`);
25946
+ return;
25947
+ }
25948
+ const edgeId = this._edgeKey(ep[0], ep[1]);
25748
25949
  this._snapshotEdgeStyle(edgeId);
25749
25950
  this._updateEdges([{ id: edgeId, style }]);
25750
25951
  }
25751
- /** 批量设置边样式 */
25752
- batchSetEdgeStyle(edgeIds, style) {
25952
+ /** 批量设置边样式(每个端对可为 id/psrId/assetNo) */
25953
+ batchSetEdgeStyle(edgeRefs, style) {
25753
25954
  this._assertReady();
25754
25955
  const updates = [];
25755
- for (const [src, tgt] of edgeIds) {
25756
- const eid = this._edgeKey(src, tgt);
25956
+ for (const [src, tgt] of edgeRefs) {
25957
+ const ep = this._edgeEndpoints(src, tgt);
25958
+ if (!ep) continue;
25959
+ const eid = this._edgeKey(ep[0], ep[1]);
25757
25960
  this._snapshotEdgeStyle(eid);
25758
25961
  updates.push({ id: eid, style });
25759
25962
  }
25760
25963
  if (updates.length) this._updateEdges(updates);
25761
25964
  }
25762
- /** 重置边样式 */
25763
- resetEdgeStyle(srcId, tgtId) {
25965
+ /** 重置边样式(端点可为节点 id/psrId/资产编号 assetNo) */
25966
+ resetEdgeStyle(srcRef, tgtRef) {
25764
25967
  this._assertReady();
25765
- const edgeId = this._edgeKey(srcId, tgtId);
25968
+ const ep = this._edgeEndpoints(srcRef, tgtRef);
25969
+ if (!ep) return;
25970
+ const edgeId = this._edgeKey(ep[0], ep[1]);
25766
25971
  const orig = this._originalEdgeStyles.get(edgeId);
25767
25972
  if (orig) {
25768
25973
  this._updateEdges([{ id: edgeId, style: orig }]);
@@ -25773,54 +25978,92 @@ class TopoApi {
25773
25978
  // 五、高亮与标注
25774
25979
  // ============================================================
25775
25980
  /**
25776
- * 高亮节点
25777
- * @param {string} nodeId
25981
+ * 高亮节点 / 箱内电表户
25982
+ * @param {string} ref - 节点 id / psrId / 资产编号(电表户 → 该户表位高亮环 + 可选文字)
25778
25983
  * @param {object} [opts] - { color, label, labelColor, labelSize, borderColor, borderStyle, borderWidth }
25779
25984
  */
25780
- highlightNode(nodeId, opts = {}) {
25985
+ highlightNode(ref2, opts = {}) {
25781
25986
  this._assertReady();
25782
- this._snapshotNodeStyle(nodeId);
25783
- const style = {
25784
- highlight: 1
25987
+ const t = this._resolveTarget(ref2);
25988
+ if (!t) {
25989
+ console.warn(`[TopoApi] highlightNode: 无法解析目标 ${ref2}`);
25990
+ return;
25991
+ }
25992
+ if (t.kind === "node") {
25993
+ this._snapshotNodeStyle(t.id);
25994
+ const style = {
25995
+ highlight: 1
25996
+ };
25997
+ if (opts.borderColor) style.highlightColor = opts.borderColor;
25998
+ if (opts.borderWidth) style.highlightWidth = opts.borderWidth;
25999
+ if (opts.borderStyle === "solid") style.highlightDash = [];
26000
+ if (opts.label) {
26001
+ style.annotationText = opts.label;
26002
+ style.annotationColor = opts.labelColor || "#FFFFFF";
26003
+ style.annotationSize = opts.labelSize || 12;
26004
+ }
26005
+ this._updateNodes([{ id: t.id, style }]);
26006
+ return;
26007
+ }
26008
+ const d2 = this._cellFxGet(t.boxId, t.index);
26009
+ d2.hl = {
26010
+ color: opts.borderColor || opts.color || "#FFD700",
26011
+ width: opts.borderWidth || 2,
26012
+ dash: opts.borderStyle === "solid" ? [] : [4, 3]
25785
26013
  };
25786
- if (opts.borderColor) style.highlightColor = opts.borderColor;
25787
- if (opts.borderWidth) style.highlightWidth = opts.borderWidth;
25788
- if (opts.borderStyle === "solid") style.highlightDash = [];
25789
26014
  if (opts.label) {
25790
- style.annotationText = opts.label;
25791
- style.annotationColor = opts.labelColor || "#FFFFFF";
25792
- style.annotationSize = opts.labelSize || 12;
26015
+ d2.note = {
26016
+ text: String(opts.label),
26017
+ color: opts.labelColor || "#FFD700",
26018
+ size: Math.min(opts.labelSize || 8, 9),
26019
+ position: "top",
26020
+ mark: "hl"
26021
+ };
25793
26022
  }
25794
- this._updateNodes([{ id: nodeId, style }]);
26023
+ this._flushCellFx(t.boxId);
25795
26024
  }
25796
- /** 取消节点高亮 */
25797
- unhighlightNode(nodeId) {
26025
+ /** 取消节点 / 箱内电表户高亮(ref 可传 id/psrId/assetNo) */
26026
+ unhighlightNode(ref2) {
25798
26027
  this._assertReady();
25799
- this._updateNodes([{
25800
- id: nodeId,
25801
- style: {
25802
- highlight: 0,
25803
- annotationText: "",
25804
- annotationColor: "",
25805
- annotationSize: 0
25806
- }
25807
- }]);
25808
- this._originalNodeStyles.delete(nodeId);
26028
+ const t = this._resolveTarget(ref2);
26029
+ if (!t) return;
26030
+ if (t.kind === "node") {
26031
+ this._updateNodes([{
26032
+ id: t.id,
26033
+ style: {
26034
+ highlight: 0,
26035
+ annotationText: "",
26036
+ annotationColor: "",
26037
+ annotationSize: 0
26038
+ }
26039
+ }]);
26040
+ this._originalNodeStyles.delete(t.id);
26041
+ return;
26042
+ }
26043
+ const d2 = this._cellFx.get(this._cellFxKey(t.boxId, t.index));
26044
+ if (!d2) return;
26045
+ delete d2.hl;
26046
+ if (d2.note && d2.note.mark === "hl") delete d2.note;
26047
+ if (!d2.hl && !d2.note && !d2.glow && !d2.pulse) this._cellFx.delete(this._cellFxKey(t.boxId, t.index));
26048
+ this._flushCellFx(t.boxId);
25809
26049
  }
25810
- /** 高亮路径(节点+边) */
25811
- highlightPath(nodeIds, opts = {}) {
26050
+ /** 高亮路径(节点与边;节点可为 id/psrId,箱内户可用 assetNo) */
26051
+ highlightPath(refs, opts = {}) {
25812
26052
  this._assertReady();
25813
- for (const id2 of nodeIds) {
26053
+ for (const id2 of refs) {
25814
26054
  this.highlightNode(id2, opts);
25815
26055
  }
25816
26056
  const edgeColor = opts.edgeColor || opts.color || "#00C8FF";
25817
- for (let i = 0; i + 1 < nodeIds.length; i++) {
25818
- const eid = this._edgeKey(nodeIds[i], nodeIds[i + 1]);
26057
+ for (let i = 0; i + 1 < refs.length; i++) {
26058
+ const a2 = this._targetNodeId(refs[i]);
26059
+ const b = this._targetNodeId(refs[i + 1]);
26060
+ if (a2 == null || b == null || a2 === b) continue;
26061
+ const eid = this._edgeKey(a2, b);
25819
26062
  this._snapshotEdgeStyle(eid);
25820
26063
  this._updateEdges([{ id: eid, style: { overrideStroke: edgeColor, overrideLineWidth: 2.5 } }]);
25821
26064
  }
25822
26065
  }
25823
- /** 取消所有高亮 */
26066
+ /** 取消所有高亮(含箱内电表户高亮环) */
25824
26067
  unhighlightAll() {
25825
26068
  this._assertReady();
25826
26069
  const nodeUpdates = [];
@@ -25838,11 +26081,26 @@ class TopoApi {
25838
26081
  }
25839
26082
  if (edgeUpdates.length) this._updateEdges(edgeUpdates);
25840
26083
  this._originalEdgeStyles.clear();
26084
+ const boxes = /* @__PURE__ */ new Set();
26085
+ for (const [key, d2] of this._cellFx) {
26086
+ const hasHl = !!d2.hl;
26087
+ if (d2.note && d2.note.mark === "hl") delete d2.note;
26088
+ delete d2.hl;
26089
+ if (!d2.hl && !d2.note && !d2.glow && !d2.pulse) {
26090
+ this._cellFx.delete(key);
26091
+ } else if (hasHl) {
26092
+ const sep = key.indexOf("#cust");
26093
+ boxes.add(sep > 0 ? key.slice(0, sep) : null);
26094
+ }
26095
+ }
26096
+ for (const boxId of boxes) {
26097
+ if (boxId != null) this._flushCellFx(boxId);
26098
+ }
25841
26099
  }
25842
- /** 非指定节点变暗 */
25843
- dimOthers(keepIds, opacity2 = 0.15) {
26100
+ /** 非指定节点变暗(keepIds 内每一项可为 节点id/psrId/assetNo) */
26101
+ dimOthers(keepRefs, opacity2 = 0.15) {
25844
26102
  this._assertReady();
25845
- const keepSet = new Set(keepIds);
26103
+ const keepSet = new Set(keepRefs.map((r) => this._targetNodeId(r)).filter((v) => v != null));
25846
26104
  const updates = [];
25847
26105
  for (const n of this._model.nodes) {
25848
26106
  if (!keepSet.has(n.id)) {
@@ -25856,13 +26114,34 @@ class TopoApi {
25856
26114
  // 六、动画
25857
26115
  // ============================================================
25858
26116
  /**
25859
- * 节点脉动动画(G6 v5: updateNodeData + draw 方式)
26117
+ * 节点脉动动画(G6 v5: updateNodeData + draw 方式)。
26118
+ * ref = 节点 id/psrId → 图元透明度呼吸;assetNo → 该户电表格淡入淡出。
26119
+ * opts: { duration, min, max }(min/max 为透明度下限/上限,默认 0.3/1)
25860
26120
  */
25861
- setNodePulse(nodeId, opts = {}) {
26121
+ setNodePulse(ref2, opts = {}) {
25862
26122
  this._assertReady();
26123
+ const t = this._resolveTarget(ref2);
26124
+ if (!t) {
26125
+ console.warn(`[TopoApi] setNodePulse: 无法解析目标 ${ref2}(节点用 id/psrId,电表户用 assetNo)`);
26126
+ return;
26127
+ }
26128
+ if (t.kind === "customer") {
26129
+ this.removeAnimation(ref2);
26130
+ const d2 = this._cellFxGet(t.boxId, t.index);
26131
+ d2.pulse = {
26132
+ duration: opts.duration || 1500,
26133
+ min: opts.min != null ? opts.min : 0.3,
26134
+ max: opts.max != null ? opts.max : 1,
26135
+ _start: performance.now(),
26136
+ opacity: 1
26137
+ };
26138
+ this._flushCellFx(t.boxId);
26139
+ this._ensureFxLoop();
26140
+ return;
26141
+ }
26142
+ const nodeId = t.id;
25863
26143
  this.removeAnimation(nodeId);
25864
26144
  const duration2 = opts.duration || 1500;
25865
- this._safeNodeStyle(nodeId, {});
25866
26145
  let start = null;
25867
26146
  const animate = (ts) => {
25868
26147
  if (!this._graph || !this._animations.has(nodeId)) return;
@@ -25946,8 +26225,19 @@ class TopoApi {
25946
26225
  const raf2 = requestAnimationFrame(animate);
25947
26226
  this._animations.set(edgeId, raf2);
25948
26227
  }
25949
- /** 移除指定元素动画 */
26228
+ /** 移除指定元素动画(ref = 节点 id/psrId → 脉动/边流动;assetNo → 该户脉动+光晕) */
25950
26229
  removeAnimation(targetId) {
26230
+ const t = targetId != null ? this._resolveTarget(String(targetId)) : null;
26231
+ if (t && t.kind === "customer") {
26232
+ const key = this._cellFxKey(t.boxId, t.index);
26233
+ const d2 = this._cellFx.get(key);
26234
+ if (!d2) return;
26235
+ delete d2.pulse;
26236
+ delete d2.glow;
26237
+ if (!d2.hl && !d2.note && !d2.glow && !d2.pulse) this._cellFx.delete(key);
26238
+ this._flushCellFx(t.boxId);
26239
+ return;
26240
+ }
25951
26241
  const raf2 = this._animations.get(targetId);
25952
26242
  if (raf2) {
25953
26243
  cancelAnimationFrame(raf2);
@@ -25970,9 +26260,11 @@ class TopoApi {
25970
26260
  console.error("[TopoApi] removeAnimation update error:", e2);
25971
26261
  }
25972
26262
  }
25973
- /** 移除所有动画 */
26263
+ /** 移除所有动画(脉动/流动/光晕/户脉动户光晕) */
25974
26264
  removeAllAnimations() {
25975
26265
  this._removeAllAnimationsInternal();
26266
+ this._removeAllGlowsInternal();
26267
+ this._removeAllCellPulses();
25976
26268
  }
25977
26269
  _removeAllAnimationsInternal() {
25978
26270
  for (const [id2, raf2] of this._animations) {
@@ -25996,12 +26288,270 @@ class TopoApi {
25996
26288
  }
25997
26289
  this._animations.clear();
25998
26290
  }
26291
+ /** 清除全部箱内电表户的脉动(保留光晕/高亮/文字) */
26292
+ _removeAllCellPulses() {
26293
+ if (this._glowRaf) {
26294
+ cancelAnimationFrame(this._glowRaf);
26295
+ this._glowRaf = null;
26296
+ }
26297
+ const dirty = /* @__PURE__ */ new Set();
26298
+ for (const [key, d2] of this._cellFx) {
26299
+ if (!d2.pulse) continue;
26300
+ delete d2.pulse;
26301
+ if (!d2.hl && !d2.note && !d2.glow && !d2.pulse) {
26302
+ this._cellFx.delete(key);
26303
+ } else {
26304
+ const sep = key.indexOf("#cust");
26305
+ if (sep > 0) dirty.add(key.slice(0, sep));
26306
+ }
26307
+ }
26308
+ if (!this._graph) return;
26309
+ for (const boxId of dirty) this._flushCellFx(boxId);
26310
+ if (this._hasFramedFx()) this._ensureFxLoop();
26311
+ }
26312
+ // ------------------------------------------------------------
26313
+ // 光晕 / 脉动 动画 —— 节点 & 箱内电表户
26314
+ // setNodeGlow(ref, { color, spread/radius, duration, opacity })
26315
+ // ref = 节点 id/psrId → 图元光环;assetNo → 该户表位光环
26316
+ // setNodePulse(ref, opts) → 节点透明度呼吸;assetNo → 该户电表格淡入淡出
26317
+ // ------------------------------------------------------------
26318
+ /** 是否有需要逐帧推进的动画(节点光晕 / 户光晕 / 户脉动) */
26319
+ _hasFramedFx() {
26320
+ if (this._glowNodes.size) return true;
26321
+ for (const d2 of this._cellFx.values()) {
26322
+ if (d2.glow && !d2.glow.done || d2.pulse && !d2.pulse.done) return true;
26323
+ }
26324
+ return false;
26325
+ }
26326
+ /** 启动统一动画循环(节点光晕 + 箱内户光晕/脉动,每帧一次批量写入) */
26327
+ _ensureFxLoop() {
26328
+ if (this._glowRaf) return;
26329
+ const loop = (ts) => {
26330
+ if (!this._graph) {
26331
+ this._glowRaf = null;
26332
+ return;
26333
+ }
26334
+ const updates = [];
26335
+ if (this._glowNodes.size) {
26336
+ for (const [id2, s2] of this._glowNodes) {
26337
+ if (!this._graph.getNodeData(id2)) {
26338
+ this._glowNodes.delete(id2);
26339
+ continue;
26340
+ }
26341
+ s2.phase = (ts - s2.start) % s2.duration / s2.duration;
26342
+ updates.push({ id: id2, style: { _glowT: s2.phase } });
26343
+ }
26344
+ }
26345
+ const dirtyBoxes = /* @__PURE__ */ new Set();
26346
+ if (this._cellFx.size) {
26347
+ const stale = [];
26348
+ for (const [key, d2] of this._cellFx) {
26349
+ if (!d2.glow && !d2.pulse) continue;
26350
+ const sep = key.indexOf("#cust");
26351
+ if (sep < 0) {
26352
+ stale.push(key);
26353
+ continue;
26354
+ }
26355
+ const boxId = key.slice(0, sep);
26356
+ const idx = Number(key.slice(sep + 5));
26357
+ const box2 = this._nodeByIdMap.get(boxId);
26358
+ if (!box2 || !Array.isArray(box2._customers) || idx < 0 || idx >= box2._customers.length) {
26359
+ stale.push(key);
26360
+ continue;
26361
+ }
26362
+ if (d2.glow) d2.glow.t = (ts - d2.glow._start) % d2.glow.duration / d2.glow.duration;
26363
+ if (d2.pulse) {
26364
+ d2.pulse.opacity = d2.pulse.min + (d2.pulse.max - d2.pulse.min) * (0.5 + 0.5 * Math.sin((ts - d2.pulse._start) % d2.pulse.duration / d2.pulse.duration * Math.PI * 2));
26365
+ }
26366
+ dirtyBoxes.add(boxId);
26367
+ }
26368
+ for (const k of stale) this._cellFx.delete(k);
26369
+ }
26370
+ if (dirtyBoxes.size) {
26371
+ const perBox = /* @__PURE__ */ new Map();
26372
+ for (const [key, d2] of this._cellFx) {
26373
+ const sep = key.indexOf("#cust");
26374
+ if (sep < 0) continue;
26375
+ const boxId = key.slice(0, sep);
26376
+ if (!dirtyBoxes.has(boxId)) continue;
26377
+ if (!d2.hl && !d2.note && !d2.glow && !d2.pulse) continue;
26378
+ if (!perBox.has(boxId)) perBox.set(boxId, {});
26379
+ perBox.get(boxId)[Number(key.slice(sep + 5))] = d2;
26380
+ }
26381
+ for (const [boxId, obj] of perBox) {
26382
+ const cur = this._safeNodeStyle(boxId, {});
26383
+ updates.push({ id: boxId, style: { ...cur, _cellFx: obj } });
26384
+ }
26385
+ }
26386
+ if (updates.length) {
26387
+ try {
26388
+ this._graph.updateNodeData(updates);
26389
+ this._graph.render();
26390
+ } catch {
26391
+ }
26392
+ }
26393
+ if (!this._hasFramedFx()) {
26394
+ this._glowRaf = null;
26395
+ return;
26396
+ }
26397
+ this._glowRaf = requestAnimationFrame(loop);
26398
+ };
26399
+ this._glowRaf = requestAnimationFrame(loop);
26400
+ }
26401
+ /** 清除某节点 style 上的光晕帧字段(光晕环不再绘制) */
26402
+ _clearGlowStyle(nodeId) {
26403
+ try {
26404
+ const cur = this._safeNodeStyle(nodeId, {});
26405
+ this._graph.updateNodeData([{
26406
+ id: nodeId,
26407
+ style: {
26408
+ ...cur,
26409
+ _glowColor: void 0,
26410
+ _glowSpread: void 0,
26411
+ _glowOpacity: void 0,
26412
+ _glowDuration: void 0,
26413
+ _glowT: void 0
26414
+ }
26415
+ }]);
26416
+ } catch {
26417
+ }
26418
+ }
26419
+ /**
26420
+ * 节点 / 箱内电表户光晕(发光环)动画。
26421
+ * @param {string} ref 节点 id / psrId(图元光环)或资产编号 assetNo(该户表位光环)
26422
+ * @param {object} [opts] - { color, spread/radius, duration, opacity }
26423
+ * @returns {boolean} 是否命中并启动
26424
+ */
26425
+ setNodeGlow(ref2, opts = {}) {
26426
+ this._assertReady();
26427
+ const t = this._resolveTarget(ref2);
26428
+ if (!t) {
26429
+ console.warn(`[TopoApi] setNodeGlow: 无法解析目标 ${ref2}(节点用 id/psrId,电表户用 assetNo)`);
26430
+ return false;
26431
+ }
26432
+ const cfg = {
26433
+ color: opts.color || "#00C8FF",
26434
+ spread: opts.spread != null ? opts.spread : opts.radius != null ? opts.radius : 14,
26435
+ opacity: opts.opacity != null ? opts.opacity : 0.9,
26436
+ duration: opts.duration || 1600,
26437
+ _start: performance.now(),
26438
+ t: 0
26439
+ };
26440
+ if (t.kind === "node") {
26441
+ this._glowNodes.set(t.id, cfg);
26442
+ this._updateNodes([{
26443
+ id: t.id,
26444
+ style: {
26445
+ _glowColor: cfg.color,
26446
+ _glowSpread: cfg.spread,
26447
+ _glowOpacity: cfg.opacity,
26448
+ _glowDuration: cfg.duration,
26449
+ _glowT: 0
26450
+ }
26451
+ }]);
26452
+ } else {
26453
+ const d2 = this._cellFxGet(t.boxId, t.index);
26454
+ d2.glow = cfg;
26455
+ this._flushCellFx(t.boxId);
26456
+ }
26457
+ this._ensureFxLoop();
26458
+ return true;
26459
+ }
26460
+ /**
26461
+ * 停止节点 / 箱内电表户的光晕动画。
26462
+ * @param {string} ref 节点 id / psrId / 资产编号
26463
+ */
26464
+ removeNodeGlow(ref2) {
26465
+ this._assertReady();
26466
+ const t = this._resolveTarget(ref2);
26467
+ if (!t) return false;
26468
+ if (t.kind === "node") {
26469
+ if (this._glowNodes.delete(t.id)) this._clearGlowStyle(t.id);
26470
+ return true;
26471
+ }
26472
+ const d2 = this._cellFx.get(this._cellFxKey(t.boxId, t.index));
26473
+ if (!d2) return false;
26474
+ delete d2.glow;
26475
+ if (!d2.hl && !d2.note && !d2.glow && !d2.pulse) this._cellFx.delete(this._cellFxKey(t.boxId, t.index));
26476
+ this._flushCellFx(t.boxId);
26477
+ return true;
26478
+ }
26479
+ /** 停止全部光晕动画(节点 + 箱内电表户) */
26480
+ removeAllNodeGlows() {
26481
+ this._removeAllGlowsInternal();
26482
+ if (this._hasFramedFx()) this._ensureFxLoop();
26483
+ }
26484
+ _removeAllGlowsInternal() {
26485
+ if (this._glowRaf) {
26486
+ cancelAnimationFrame(this._glowRaf);
26487
+ this._glowRaf = null;
26488
+ }
26489
+ const updates = [];
26490
+ if (this._graph && this._glowNodes.size) {
26491
+ for (const [id2] of this._glowNodes) {
26492
+ if (!this._graph.getNodeData(id2)) continue;
26493
+ updates.push({
26494
+ id: id2,
26495
+ style: {
26496
+ _glowColor: void 0,
26497
+ _glowSpread: void 0,
26498
+ _glowOpacity: void 0,
26499
+ _glowDuration: void 0,
26500
+ _glowT: void 0
26501
+ }
26502
+ });
26503
+ }
26504
+ }
26505
+ this._glowNodes.clear();
26506
+ const dirty = /* @__PURE__ */ new Set();
26507
+ for (const [key, d2] of this._cellFx) {
26508
+ if (!d2.glow) continue;
26509
+ delete d2.glow;
26510
+ if (!d2.hl && !d2.note && !d2.glow && !d2.pulse) {
26511
+ this._cellFx.delete(key);
26512
+ } else {
26513
+ const sep = key.indexOf("#cust");
26514
+ if (sep > 0) dirty.add(key.slice(0, sep));
26515
+ }
26516
+ }
26517
+ if (this._graph) {
26518
+ if (updates.length) {
26519
+ try {
26520
+ this._graph.updateNodeData(updates);
26521
+ this._graph.render();
26522
+ } catch {
26523
+ }
26524
+ }
26525
+ for (const boxId of dirty) this._flushCellFx(boxId);
26526
+ }
26527
+ }
25999
26528
  // ============================================================
26000
26529
  // 七、文字标注
26001
26530
  // ============================================================
26002
- /** 设置附加文字 */
26003
- setText(nodeId, text2, opts = {}) {
26531
+ /**
26532
+ * 设置附加文字(ref = 节点 id/psrId → 节点旁标注;assetNo → 该户表位上方小标注)
26533
+ * opts: { color, fontSize, position: 'top'|'bottom' }
26534
+ */
26535
+ setText(ref2, text2, opts = {}) {
26004
26536
  this._assertReady();
26537
+ const t = this._resolveTarget(ref2);
26538
+ if (!t) {
26539
+ console.warn(`[TopoApi] setText: 无法解析目标 ${ref2}`);
26540
+ return;
26541
+ }
26542
+ if (t.kind === "customer") {
26543
+ const d2 = this._cellFxGet(t.boxId, t.index);
26544
+ d2.note = {
26545
+ text: String(text2),
26546
+ color: opts.color || "#FFD700",
26547
+ size: Math.min(opts.fontSize || 8, 9),
26548
+ position: opts.position === "bottom" ? "bottom" : "top",
26549
+ mark: "text"
26550
+ };
26551
+ this._flushCellFx(t.boxId);
26552
+ return;
26553
+ }
26554
+ const nodeId = t.id;
26005
26555
  this._snapshotNodeStyle(nodeId);
26006
26556
  this._nodeLabels.set(nodeId, { text: text2, opts });
26007
26557
  this._updateNodes([{
@@ -26015,16 +26565,28 @@ class TopoApi {
26015
26565
  }
26016
26566
  }]);
26017
26567
  }
26018
- /** 移除附加文字 */
26019
- removeText(nodeId) {
26568
+ /** 移除附加文字(ref = 节点 id/psrId/资产编号) */
26569
+ removeText(ref2) {
26020
26570
  this._assertReady();
26571
+ const t = this._resolveTarget(ref2);
26572
+ if (!t) return;
26573
+ if (t.kind === "customer") {
26574
+ const key = this._cellFxKey(t.boxId, t.index);
26575
+ const d2 = this._cellFx.get(key);
26576
+ if (!d2) return;
26577
+ delete d2.note;
26578
+ if (!d2.hl && !d2.note && !d2.glow && !d2.pulse) this._cellFx.delete(key);
26579
+ this._flushCellFx(t.boxId);
26580
+ return;
26581
+ }
26582
+ const nodeId = t.id;
26021
26583
  this._nodeLabels.delete(nodeId);
26022
26584
  this._updateNodes([{
26023
26585
  id: nodeId,
26024
26586
  style: { annotationText: "", annotationColor: "", annotationSize: 0 }
26025
26587
  }]);
26026
26588
  }
26027
- /** 移除所有附加文字 */
26589
+ /** 移除所有附加文字(含箱内电表户标注) */
26028
26590
  removeAllTexts() {
26029
26591
  this._assertReady();
26030
26592
  const updates = [];
@@ -26033,6 +26595,18 @@ class TopoApi {
26033
26595
  }
26034
26596
  if (updates.length) this._updateNodes(updates);
26035
26597
  this._nodeLabels.clear();
26598
+ const dirty = /* @__PURE__ */ new Set();
26599
+ for (const [key, d2] of this._cellFx) {
26600
+ if (!d2.note) continue;
26601
+ delete d2.note;
26602
+ if (!d2.hl && !d2.note && !d2.glow && !d2.pulse) {
26603
+ this._cellFx.delete(key);
26604
+ } else {
26605
+ const sep = key.indexOf("#cust");
26606
+ if (sep > 0) dirty.add(key.slice(0, sep));
26607
+ }
26608
+ }
26609
+ for (const boxId of dirty) this._flushCellFx(boxId);
26036
26610
  }
26037
26611
  /** 按设备类型设字号 */
26038
26612
  setDeviceText(cat, fontSize2) {
@@ -26062,22 +26636,30 @@ class TopoApi {
26062
26636
  // 八、卡片盒
26063
26637
  // ============================================================
26064
26638
  /**
26065
- * 在节点旁弹卡片
26066
- * @param {string} nodeId
26067
- * @param {object} content - { title, fields, buttons, closable, width }
26639
+ * 在节点 / 箱内电表户旁弹卡片。
26640
+ * @param {string} ref 节点 id / psrId / 资产编号(assetNo → 卡片定位在该户表位旁)
26641
+ * @param {object} [content] - { title, fields, buttons, closable, width }
26642
+ * 缺省自动按「设备 / 客户档案」生成字段
26068
26643
  */
26069
- showCard(nodeId, content) {
26644
+ showCard(ref2, content) {
26070
26645
  var _a, _b, _c, _d, _e, _f;
26071
26646
  this._assertReady();
26072
- this.hideCard(nodeId);
26647
+ const t = this._resolveTarget(ref2);
26648
+ if (!t) {
26649
+ console.warn(`[TopoApi] showCard: 无法解析目标 ${ref2}(节点用 id/psrId,电表户用 assetNo)`);
26650
+ return;
26651
+ }
26652
+ const isCustomer = t.kind === "customer";
26653
+ const cardKey = isCustomer ? this._cellFxKey(t.boxId, t.index) : t.id;
26654
+ this.hideCard(cardKey);
26073
26655
  const container = ((_b = (_a = this._graph).getContainer) == null ? void 0 : _b.call(_a)) || ((_f = (_e = (_d = (_c = this._graph).getCanvas) == null ? void 0 : _d.call(_c)) == null ? void 0 : _e.getContainer) == null ? void 0 : _f.call(_e));
26074
26656
  if (!container) return;
26075
- const pos = this._layout.pos.get(nodeId);
26076
- if (!pos) return;
26657
+ const world = this._targetWorld(ref2);
26658
+ if (!world) return;
26077
26659
  let client = null;
26078
26660
  try {
26079
26661
  if (typeof this._graph.getClientByCanvas === "function") {
26080
- const r = this._graph.getClientByCanvas([pos.x, pos.y]);
26662
+ const r = this._graph.getClientByCanvas([world.x, world.y]);
26081
26663
  client = Array.isArray(r) ? { x: r[0], y: r[1] } : r;
26082
26664
  }
26083
26665
  } catch (e2) {
@@ -26090,12 +26672,37 @@ class TopoApi {
26090
26672
  const zoom = this._graph.getZoom();
26091
26673
  const canvasCenter = this._canvasCenterXY();
26092
26674
  const containerRect = container.getBoundingClientRect();
26093
- screenX = containerRect.left + canvasCenter.x + pos.x * zoom;
26094
- screenY = containerRect.top + canvasCenter.y + pos.y * zoom;
26675
+ screenX = containerRect.left + canvasCenter.x + world.x * zoom;
26676
+ screenY = containerRect.top + canvasCenter.y + world.y * zoom;
26677
+ }
26678
+ content = content || {};
26679
+ let cTitle = content.title;
26680
+ let cFields = content.fields;
26681
+ if (!cTitle || !Array.isArray(cFields) || !cFields.length) {
26682
+ if (isCustomer) {
26683
+ const c = t.customer || {};
26684
+ cTitle = cTitle || c.realConsName || c.consName || `表位 ${t.index + 1}`;
26685
+ cFields = [
26686
+ ["户名", c.realConsName || c.consName],
26687
+ ["资产编号", c.assetNo],
26688
+ ["客户编号", c.consId],
26689
+ ["户号", c.consNo],
26690
+ ["电表编号", c.meterId]
26691
+ ].filter(([, v]) => v != null && v !== "").map(([label, value]) => ({ label, value }));
26692
+ } else {
26693
+ const node = t.node || {};
26694
+ cTitle = cTitle || node.name || "设备信息";
26695
+ cFields = [
26696
+ ["类型", node.catLabel],
26697
+ ["设备名称", node.name],
26698
+ ["PSR编号", node.psrId],
26699
+ ["状态", node.status === "0" ? "正常" : node.status]
26700
+ ].filter(([, v]) => v != null && v !== "").map(([label, value]) => ({ label, value }));
26701
+ }
26095
26702
  }
26096
26703
  const el = document.createElement("div");
26097
26704
  el.className = "topo-card";
26098
- el.dataset.nodeId = nodeId;
26705
+ el.dataset.nodeId = cardKey;
26099
26706
  el.style.cssText = `
26100
26707
  position: fixed; left: ${screenX + 30}px; top: ${screenY - 20}px;
26101
26708
  width: ${content.width || 240}px; z-index: 1000;
@@ -26105,22 +26712,22 @@ class TopoApi {
26105
26712
  `;
26106
26713
  const head = document.createElement("div");
26107
26714
  head.style.cssText = "display:flex;align-items:center;justify-content:space-between;padding:8px 12px;border-bottom:1px solid #2c2c30;";
26108
- head.innerHTML = `<span style="font-weight:600;color:#fff;">${content.title || ""}</span>`;
26715
+ head.innerHTML = `<span style="font-weight:600;color:#fff;">${String(cTitle || "").replace(/</g, "&lt;")}</span>`;
26109
26716
  if (content.closable !== false) {
26110
26717
  const closeBtn = document.createElement("button");
26111
26718
  closeBtn.textContent = "×";
26112
26719
  closeBtn.style.cssText = "border:none;background:none;color:#888;font-size:16px;cursor:pointer;";
26113
- closeBtn.onclick = () => this.hideCard(nodeId);
26720
+ closeBtn.onclick = () => this.hideCard(cardKey);
26114
26721
  head.appendChild(closeBtn);
26115
26722
  }
26116
26723
  el.appendChild(head);
26117
- if (content.fields && content.fields.length) {
26724
+ if (cFields && cFields.length) {
26118
26725
  const body = document.createElement("div");
26119
26726
  body.style.cssText = "padding:8px 12px;";
26120
- for (const f2 of content.fields) {
26727
+ for (const f2 of cFields) {
26121
26728
  const row2 = document.createElement("div");
26122
26729
  row2.style.cssText = "display:flex;gap:8px;margin-bottom:4px;";
26123
- row2.innerHTML = `<span style="color:#8a8a96;flex:none;width:56px;">${f2.label}</span><span style="color:#eee;">${f2.value}</span>`;
26730
+ row2.innerHTML = `<span style="color:#8a8a96;flex:none;width:56px;">${String(f2.label || "").replace(/</g, "&lt;")}</span><span style="color:#eee;">${String(f2.value != null ? f2.value : "").replace(/</g, "&lt;")}</span>`;
26124
26731
  body.appendChild(row2);
26125
26732
  }
26126
26733
  el.appendChild(body);
@@ -26134,21 +26741,34 @@ class TopoApi {
26134
26741
  b.style.cssText = btn.type === "primary" ? "padding:3px 10px;border:1px solid #00c8ff;border-radius:4px;background:#00c8ff;color:#000;cursor:pointer;font-size:12px;" : "padding:3px 10px;border:1px solid #3a3a42;border-radius:4px;background:#18181c;color:#ddd;cursor:pointer;font-size:12px;";
26135
26742
  b.onclick = () => {
26136
26743
  if (btn.onClick) btn.onClick();
26137
- this._emit("card:button", { nodeId, buttonIndex: idx });
26744
+ if (isCustomer) {
26745
+ this._emit("card:button", {
26746
+ nodeId: t.boxId,
26747
+ boxId: t.boxId,
26748
+ index: t.index,
26749
+ customer: t.customer,
26750
+ cardKey,
26751
+ buttonIndex: idx
26752
+ });
26753
+ } else {
26754
+ this._emit("card:button", { nodeId: t.id, buttonIndex: idx });
26755
+ }
26138
26756
  };
26139
26757
  foot.appendChild(b);
26140
26758
  });
26141
26759
  el.appendChild(foot);
26142
26760
  }
26143
26761
  document.body.appendChild(el);
26144
- this._cards.set(nodeId, el);
26762
+ this._cards.set(cardKey, el);
26145
26763
  }
26146
- /** 关闭指定卡片 */
26147
- hideCard(nodeId) {
26148
- const el = this._cards.get(nodeId);
26764
+ /** 关闭指定节点 / 电表户旁的卡片(ref 可传 id/psrId/assetNo) */
26765
+ hideCard(ref2) {
26766
+ const t = this._resolveTarget(ref2);
26767
+ const key = t ? t.kind === "node" ? t.id : this._cellFxKey(t.boxId, t.index) : String(ref2);
26768
+ const el = this._cards.get(key);
26149
26769
  if (el) {
26150
26770
  el.remove();
26151
- this._cards.delete(nodeId);
26771
+ this._cards.delete(key);
26152
26772
  }
26153
26773
  }
26154
26774
  /** 关闭所有卡片 */
@@ -26482,12 +27102,17 @@ class TopoApi {
26482
27102
  }
26483
27103
  /**
26484
27104
  * 添加拓扑高亮线
26485
- * @param {string[]} nodeIds - 节点路径
27105
+ * @param {string[]} refs - 节点路径(每点可为 id/psrId;assetNo 落到所在计量箱)
26486
27106
  * @param {object} [opts] - { color, width, id }
26487
27107
  * @returns {string} 线 ID
26488
27108
  */
26489
- addTopoLine(nodeIds, opts = {}) {
27109
+ addTopoLine(refs, opts = {}) {
26490
27110
  this._assertReady();
27111
+ const nodeIds = refs.map((n) => this._targetNodeId(n)).filter((v) => v != null);
27112
+ if (nodeIds.length < 2) {
27113
+ console.warn("[TopoApi] addTopoLine: 有效节点不足 2 个,无法画线");
27114
+ return opts.id || `topo-${Date.now()}`;
27115
+ }
26491
27116
  const lineId = opts.id || `topo-${Date.now()}`;
26492
27117
  const color2 = opts.color || "#00C8FF";
26493
27118
  const width = opts.width || 3;
@@ -28522,6 +29147,42 @@ function drawMeterFace(group, cx, cy, meterW, meterH, lineColor, markCell, cellI
28522
29147
  if (markCell) markCell(dot2, cellIdx);
28523
29148
  group.appendChild(dot2);
28524
29149
  }
29150
+ function cellFxAt(attributes, i) {
29151
+ const fx = attributes && attributes._cellFx;
29152
+ return fx && typeof fx === "object" ? fx[i] || null : null;
29153
+ }
29154
+ function drawCellGlowRings(group, cx, cy, baseR, g, t) {
29155
+ const color2 = g.color || "#00C8FF";
29156
+ const spread = g.spread || 10;
29157
+ const maxOp = g.opacity == null ? 0.8 : g.opacity;
29158
+ const ease2 = 1 - (1 - t) * (1 - t);
29159
+ const alpha = maxOp * Math.pow(1 - t, 1.4);
29160
+ if (alpha <= 0.02) return;
29161
+ const r = baseR + spread * ease2;
29162
+ const lw = Math.max(1, 3.2 * (1 - t) + 0.6);
29163
+ group.appendChild(new Circle({ style: { cx, cy, r, stroke: color2, lineWidth: lw, strokeOpacity: alpha, fill: "none", lineCap: "round" } }));
29164
+ group.appendChild(new Circle({ style: { cx, cy, r: r + spread * 0.6, stroke: color2, lineWidth: Math.max(0.6, lw * 0.55), strokeOpacity: alpha * 0.35, fill: "none", lineCap: "round" } }));
29165
+ }
29166
+ function drawCellHighlight(group, cx, cy, baseR, hl) {
29167
+ group.appendChild(new Circle({
29168
+ style: {
29169
+ cx,
29170
+ cy,
29171
+ r: baseR + 3,
29172
+ stroke: hl.color || "#FFD700",
29173
+ lineWidth: hl.width || 2,
29174
+ lineDash: hl.dash || [4, 3],
29175
+ fill: "none"
29176
+ }
29177
+ }));
29178
+ }
29179
+ function drawCellNote(group, cx, cy, baseR, note) {
29180
+ const size = note.size || 7;
29181
+ const color2 = note.color || C2.gold;
29182
+ const top = !note.position || note.position === "top";
29183
+ const y = top ? cy - baseR - 5 - size / 2 : cy + baseR + 6 + size / 2;
29184
+ _drawWrappedTextFixed(group, note.text, cx, y, size, color2, 6, 1.15, 600, "center");
29185
+ }
28525
29186
  function drawMeterBox(attributes, group) {
28526
29187
  const [w, h] = attributes.size;
28527
29188
  const boxColor = nodeColor(attributes);
@@ -28531,6 +29192,8 @@ function drawMeterBox(attributes, group) {
28531
29192
  const hover = attributes._custHover === 0;
28532
29193
  const baseColor = cellColorAt(attributes, 0) || boxColor || C2.white;
28533
29194
  const lineColor = sel ? C2.gold : hover ? C2.cyan : baseColor;
29195
+ const fx = cellFxAt(attributes, 0);
29196
+ const cellGroup = new Group({});
28534
29197
  const markCell = (shape) => {
28535
29198
  if (!nodeId) return;
28536
29199
  shape.__custBox = nodeId;
@@ -28538,12 +29201,13 @@ function drawMeterBox(attributes, group) {
28538
29201
  };
28539
29202
  const meterW = 20;
28540
29203
  const meterH = 26;
28541
- drawMeterFace(group, 0, -1, meterW, meterH, lineColor, markCell, 0);
29204
+ const cx = 0;
29205
+ const cy = -1;
28542
29206
  if (sel || hover) {
28543
29207
  const ring = new Rect({
28544
29208
  style: {
28545
- x: -meterW / 2 - 4,
28546
- y: -meterH / 2 - 4,
29209
+ x: cx - meterW / 2 - 4,
29210
+ y: cy - meterH / 2 - 4,
28547
29211
  width: meterW + 8,
28548
29212
  height: meterH + 8,
28549
29213
  radius: 5,
@@ -28553,12 +29217,23 @@ function drawMeterBox(attributes, group) {
28553
29217
  }
28554
29218
  });
28555
29219
  markCell(ring);
28556
- group.appendChild(ring);
29220
+ cellGroup.appendChild(ring);
29221
+ }
29222
+ drawMeterFace(cellGroup, cx, cy, meterW, meterH, lineColor, markCell, 0);
29223
+ if (fx) {
29224
+ const baseR = Math.max(12, meterH / 2 + 2);
29225
+ if (fx.hl) drawCellHighlight(cellGroup, cx, cy, baseR, fx.hl);
29226
+ if (fx.note && fx.note.text) drawCellNote(cellGroup, cx, cy, baseR, fx.note);
29227
+ if (fx.glow) drawCellGlowRings(cellGroup, cx, cy, baseR, fx.glow, fx.glow.t || 0);
28557
29228
  }
28558
29229
  const name = attributes._customers && attributes._customers[0] ? attributes._customers[0].realConsName || attributes._customers[0].consName || "" : "";
28559
29230
  if (name) {
28560
- _drawWrappedTextFixed(group, name, 0, meterH / 2 + 7, 7, lineColor, 4, 1.15, 500);
29231
+ _drawWrappedTextFixed(cellGroup, name, cx, meterH / 2 + 7, 7, lineColor, 4, 1.15, 500);
28561
29232
  }
29233
+ if (fx && fx.pulse && fx.pulse.opacity != null && fx.pulse.opacity < 1) {
29234
+ cellGroup.style.opacity = fx.pulse.opacity;
29235
+ }
29236
+ group.appendChild(cellGroup);
28562
29237
  } else if (attributes._meterBoxMode === "grid" && attributes._customers) {
28563
29238
  const frameColor = boxColor || C2.meterText;
28564
29239
  group.appendChild(new Rect({ style: { x: -w / 2, y: -h / 2, width: w, height: h, fill: withAlpha(frameColor, 0.04), stroke: frameColor, lineWidth: 1, radius: 4 } }));
@@ -28592,8 +29267,10 @@ function drawMeterBox(attributes, group) {
28592
29267
  const hover = i === custHover;
28593
29268
  const baseColor = cellColorAt(attributes, i) || C2.meterText;
28594
29269
  const lineColor = sel ? C2.gold : hover ? C2.cyan : baseColor;
29270
+ const fx = cellFxAt(attributes, i);
29271
+ const cellGroup = new Group({});
28595
29272
  if (sel || hover) {
28596
- group.appendChild(new Circle({
29273
+ cellGroup.appendChild(new Circle({
28597
29274
  style: {
28598
29275
  cx,
28599
29276
  cy,
@@ -28605,14 +29282,24 @@ function drawMeterBox(attributes, group) {
28605
29282
  }
28606
29283
  }));
28607
29284
  }
28608
- drawMeterFace(group, cx, cy, meterW, meterH, lineColor, markCell, i);
29285
+ drawMeterFace(cellGroup, cx, cy, meterW, meterH, lineColor, markCell, i);
29286
+ if (fx) {
29287
+ if (fx.hl) drawCellHighlight(cellGroup, cx, cy, hitR, fx.hl);
29288
+ if (fx.note && fx.note.text) drawCellNote(cellGroup, cx, cy, hitR, fx.note);
29289
+ if (fx.glow) drawCellGlowRings(cellGroup, cx, cy, hitR, fx.glow, fx.glow.t || 0);
29290
+ }
29291
+ if (showName) {
29292
+ const name = customers[i].realConsName || customers[i].consName || "";
29293
+ if (name) {
29294
+ _drawWrappedTextFixed(cellGroup, name, cx, cy + meterH / 2 + 6.5, 6.5, lineColor, 4, 1.15, 500);
29295
+ }
29296
+ }
28609
29297
  const hit = new Circle({ style: { cx, cy, r: hitR, fill: "rgba(255,255,255,0.01)", stroke: "none", cursor: "pointer" } });
28610
29298
  markCell(hit, i);
28611
- group.appendChild(hit);
29299
+ cellGroup.appendChild(hit);
28612
29300
  if (showName) {
28613
29301
  const name = customers[i].realConsName || customers[i].consName || "";
28614
29302
  if (name) {
28615
- _drawWrappedTextFixed(group, name, cx, cy + meterH / 2 + 6.5, 6.5, lineColor, 4, 1.15, 500);
28616
29303
  const hitName = new Circle({
28617
29304
  style: {
28618
29305
  cx,
@@ -28624,9 +29311,13 @@ function drawMeterBox(attributes, group) {
28624
29311
  }
28625
29312
  });
28626
29313
  markCell(hitName, i);
28627
- group.appendChild(hitName);
29314
+ cellGroup.appendChild(hitName);
28628
29315
  }
28629
29316
  }
29317
+ if (fx && fx.pulse && fx.pulse.opacity != null && fx.pulse.opacity < 1) {
29318
+ cellGroup.style.opacity = fx.pulse.opacity;
29319
+ }
29320
+ group.appendChild(cellGroup);
28630
29321
  }
28631
29322
  } else {
28632
29323
  const s2 = Math.min(w, h) || 21;
@@ -28715,6 +29406,35 @@ const DRAWERS = {
28715
29406
  other: drawUnknown
28716
29407
  };
28717
29408
  const _pulseState = /* @__PURE__ */ new Map();
29409
+ function drawGlow(attributes, group) {
29410
+ const color2 = attributes._glowColor;
29411
+ if (!color2) return;
29412
+ const t = attributes._glowT == null ? 0 : attributes._glowT;
29413
+ const [w, h] = attributes.size || [40, 40];
29414
+ const baseR = Math.max(w, h) / 2 + 3;
29415
+ const spread = attributes._glowSpread || 14;
29416
+ const maxOp = attributes._glowOpacity == null ? 0.9 : attributes._glowOpacity;
29417
+ const ease2 = 1 - (1 - t) * (1 - t);
29418
+ const alpha = maxOp * Math.pow(1 - t, 1.4);
29419
+ if (alpha <= 0.02) return;
29420
+ const r = baseR + spread * ease2;
29421
+ const lw = Math.max(1.2, 4.4 * (1 - t) + 0.8);
29422
+ group.appendChild(new Circle({
29423
+ style: { cx: 0, cy: 0, r, stroke: color2, lineWidth: lw, strokeOpacity: alpha, fill: "none", lineCap: "round" }
29424
+ }));
29425
+ group.appendChild(new Circle({
29426
+ style: {
29427
+ cx: 0,
29428
+ cy: 0,
29429
+ r: r + spread * 0.6,
29430
+ stroke: color2,
29431
+ lineWidth: Math.max(0.8, lw * 0.55),
29432
+ strokeOpacity: alpha * 0.35,
29433
+ fill: "none",
29434
+ lineCap: "round"
29435
+ }
29436
+ }));
29437
+ }
28718
29438
  class SvgSymbolNode extends BaseNode {
28719
29439
  drawKeyShape(attributes, container) {
28720
29440
  const [w, h] = attributes.size;
@@ -28745,6 +29465,7 @@ class SvgSymbolNode extends BaseNode {
28745
29465
  if (rot) key.appendChild(target);
28746
29466
  const draw = DRAWERS[attributes.cat] || drawUnknown;
28747
29467
  draw(attributes, target);
29468
+ drawGlow(attributes, target);
28748
29469
  drawHighlight(attributes, target);
28749
29470
  drawAnnotation(attributes, target);
28750
29471
  if (attributes.labelText && !attributes._stationTitle && attributes.cat !== "bus" && attributes.cat !== "cableTerminal" && attributes.cat !== "consumer" && attributes.cat !== "switch" && attributes.cat !== "meterBox") {
@@ -28936,6 +29657,32 @@ const _sfc_main$2 = {
28936
29657
  emit("node-select", { node, upstream, downstream });
28937
29658
  if (api) api._emit("node:click", { nodeId: id2, node });
28938
29659
  }
29660
+ function selectNodeRef(ref2) {
29661
+ if (!api || !graph || ref2 == null) return;
29662
+ const t = api._resolveTarget(ref2);
29663
+ if (!t) {
29664
+ selectNodeWithInfo(String(ref2));
29665
+ return;
29666
+ }
29667
+ if (t.kind === "customer") {
29668
+ selectCustomerWithInfo(t.boxId, t.index);
29669
+ } else {
29670
+ selectNodeWithInfo(t.id);
29671
+ }
29672
+ }
29673
+ function selectCustomerRef(nodeRef, index) {
29674
+ if (!api || !graph || nodeRef == null) return;
29675
+ const t = api._resolveTarget(nodeRef);
29676
+ if (!t) {
29677
+ selectNodeWithInfo(String(nodeRef));
29678
+ return;
29679
+ }
29680
+ if (t.kind === "customer") {
29681
+ selectCustomerWithInfo(t.boxId, t.index);
29682
+ return;
29683
+ }
29684
+ selectCustomerWithInfo(t.id, index);
29685
+ }
28939
29686
  function routeNodeTap(id2) {
28940
29687
  if (!graph || id2 == null) return;
28941
29688
  const customers = customersOf(id2);
@@ -29248,9 +29995,9 @@ const _sfc_main$2 = {
29248
29995
  }, {}),
29249
29996
  // 保留原有快捷方法
29250
29997
  resetView,
29251
- selectNode: selectNodeWithInfo,
29252
- /** 选中计量箱内第 index 个用户(多用户箱,等价于画布点击该用户) */
29253
- selectCustomer: (nodeId, index) => selectCustomerWithInfo(nodeId, index),
29998
+ selectNode: selectNodeRef,
29999
+ /** 选中计量箱内第 index 个用户(nodeRef=箱节点id/psrId;传 assetNo 时自动定位该户) */
30000
+ selectCustomer: selectCustomerRef,
29254
30001
  /** 当前选中的“箱内用户”信息({ nodeId, index, customer, node } | null) */
29255
30002
  getSelectedCustomer,
29256
30003
  exportPng,
@@ -29289,7 +30036,7 @@ const _sfc_main$2 = {
29289
30036
  };
29290
30037
  }
29291
30038
  };
29292
- const TopoGraph = /* @__PURE__ */ _export_sfc(_sfc_main$2, [["__scopeId", "data-v-c2cd8b2c"]]);
30039
+ const TopoGraph = /* @__PURE__ */ _export_sfc(_sfc_main$2, [["__scopeId", "data-v-d3774ed5"]]);
29293
30040
  const _hoisted_1$1 = ["data-theme"];
29294
30041
  const _hoisted_2$1 = { class: "panel-head" };
29295
30042
  const _hoisted_3$1 = {
@@ -29662,7 +30409,7 @@ const _sfc_main = {
29662
30409
  }
29663
30410
  };
29664
30411
  const DistanceTop10 = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-438b6017"]]);
29665
- const VERSION = "0.1.8";
30412
+ const VERSION = "0.1.9";
29666
30413
  const DESCRIPTION = "配电台区单线图拓扑成图引擎";
29667
30414
  export {
29668
30415
  DESCRIPTION,