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