topo-engine 0.1.7 → 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.
@@ -25024,8 +25024,8 @@ function injectCustomers(model, customerMap) {
25024
25024
  node._customers = customers;
25025
25025
  if (customers.length === 1) {
25026
25026
  node._meterBoxMode = "single";
25027
- node._meterBoxW = 30;
25028
- node._meterBoxH = 38;
25027
+ node._meterBoxW = 22;
25028
+ node._meterBoxH = 30;
25029
25029
  } else {
25030
25030
  const showName = customers.length <= SHOW_NAME_LIMIT;
25031
25031
  const cols = Math.min(customers.length, MAX_COLS);
@@ -25095,32 +25095,48 @@ 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();
25103
+ this._nodeColorOverrides = /* @__PURE__ */ new Map();
25104
+ this._custColorOverrides = /* @__PURE__ */ new Map();
25105
+ this._cellFx = /* @__PURE__ */ new Map();
25101
25106
  this._bindConnGraphEvents();
25102
25107
  }
25103
25108
  /** 数据刷新(图重新渲染后调用) */
25104
25109
  update(graph, model, layout, nodeByIdMap) {
25110
+ const colorSnapshot = {
25111
+ node: new Map(this._nodeColorOverrides),
25112
+ cust: new Map(this._custColorOverrides)
25113
+ };
25105
25114
  this.destroy();
25106
25115
  this._graph = graph;
25107
25116
  this._model = model;
25108
25117
  this._layout = layout;
25109
25118
  this._nodeByIdMap = nodeByIdMap;
25110
25119
  this._selectedId = null;
25120
+ this._nodeColorOverrides = colorSnapshot.node;
25121
+ this._custColorOverrides = colorSnapshot.cust;
25111
25122
  this._bindConnGraphEvents();
25123
+ this._applyColorOverrides();
25112
25124
  }
25113
25125
  /** 销毁,释放所有资源 */
25114
25126
  destroy() {
25115
25127
  this._removeAllConnectionsInternal();
25116
25128
  this._unbindConnGraphEvents();
25117
25129
  this._removeAllAnimationsInternal();
25130
+ this._removeAllGlowsInternal();
25118
25131
  this._removeAllTopoLinesInternal();
25119
25132
  this._hideAllCardsInternal();
25120
25133
  this._listeners.clear();
25121
25134
  this._originalNodeStyles.clear();
25122
25135
  this._originalEdgeStyles.clear();
25123
25136
  this._nodeLabels.clear();
25137
+ this._nodeColorOverrides.clear();
25138
+ this._custColorOverrides.clear();
25139
+ this._cellFx.clear();
25124
25140
  this._graph = null;
25125
25141
  this._model = null;
25126
25142
  this._layout = null;
@@ -25238,11 +25254,163 @@ class TopoApi {
25238
25254
  }
25239
25255
  }
25240
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
+ // ============================================================
25241
25362
  // 一、查询 API
25242
25363
  // ============================================================
25243
- /** 获取单个节点 */
25364
+ /** 获取单个节点(ref = 节点 id/psrId/资产编号 assetNo)
25365
+ * 命中电表户时返回其所在计量箱节点,并附带 customer/customerIndex 等客户信息 */
25244
25366
  getNode(nodeId) {
25245
- 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 };
25246
25414
  }
25247
25415
  /** 获取所有节点(只读副本) */
25248
25416
  getAllNodes() {
@@ -25274,15 +25442,17 @@ class TopoApi {
25274
25442
  getSelectedId() {
25275
25443
  return this._selectedId;
25276
25444
  }
25277
- /** 获取上下游邻居 */
25445
+ /** 获取上下游邻居(ref 可传 节点id/psrId/资产编号;电表户按所在计量箱参与拓扑) */
25278
25446
  getNeighbors(nodeId) {
25447
+ const id2 = this._targetNodeId(nodeId);
25448
+ if (id2 == null) return { upstream: [], downstream: [] };
25279
25449
  const upstream = [];
25280
25450
  const downstream = [];
25281
25451
  for (const e2 of this._model.edges) {
25282
- if (e2.target === nodeId) {
25452
+ if (e2.target === id2) {
25283
25453
  const p = this._nodeByIdMap.get(e2.source);
25284
25454
  if (p) upstream.push(p);
25285
- } else if (e2.source === nodeId) {
25455
+ } else if (e2.source === id2) {
25286
25456
  const c = this._nodeByIdMap.get(e2.target);
25287
25457
  if (c) downstream.push(c);
25288
25458
  }
@@ -25291,11 +25461,13 @@ class TopoApi {
25291
25461
  }
25292
25462
  /**
25293
25463
  * 沿拓扑流向取节点链
25294
- * @param {string} nodeId - 起始节点
25464
+ * @param {string} ref - 节点 id / psrId / 资产编号(电表户按所在箱)
25295
25465
  * @param {'upstream'|'downstream'} direction
25296
25466
  */
25297
- getStreamNodes(nodeId, direction2) {
25298
- 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;
25299
25471
  }
25300
25472
  /** 获取主干链节点 */
25301
25473
  getTrunkNodes() {
@@ -25333,11 +25505,13 @@ class TopoApi {
25333
25505
  for (const k of kids) w += this._subtreeWeight(k, childrenMap, visited);
25334
25506
  return w;
25335
25507
  }
25336
- /** 获取子树所有节点 */
25337
- getSubtreeNodes(nodeId) {
25508
+ /** 获取子树所有节点(ref 可传 节点id/psrId/资产编号) */
25509
+ getSubtreeNodes(ref2) {
25510
+ const id2 = this._targetNodeId(ref2);
25511
+ if (id2 == null) return [];
25338
25512
  const result = [];
25339
25513
  const visited = /* @__PURE__ */ new Set();
25340
- const queue = [nodeId];
25514
+ const queue = [id2];
25341
25515
  while (queue.length) {
25342
25516
  const cur = queue.shift();
25343
25517
  if (visited.has(cur)) continue;
@@ -25350,8 +25524,11 @@ class TopoApi {
25350
25524
  }
25351
25525
  return result;
25352
25526
  }
25353
- /** 获取两节点间路径(BFS */
25354
- 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;
25355
25532
  const parent = /* @__PURE__ */ new Map([[fromId, null]]);
25356
25533
  const queue = [fromId];
25357
25534
  while (queue.length) {
@@ -25378,17 +25555,19 @@ class TopoApi {
25378
25555
  }
25379
25556
  return path;
25380
25557
  }
25381
- /** 获取指定边 */
25382
- getEdge(srcId, tgtId) {
25383
- return this._findEdgeData(srcId, tgtId);
25558
+ /** 获取指定边(端点可传 id/psrId/assetNo,电表户按所在箱) */
25559
+ getEdge(srcRef, tgtRef) {
25560
+ return this._findEdgeData(this._targetNodeId(srcRef), this._targetNodeId(tgtRef));
25384
25561
  }
25385
25562
  /** 获取所有边 */
25386
25563
  getEdges() {
25387
25564
  return [...this._model.edges];
25388
25565
  }
25389
- /** 沿流向取边 */
25390
- getStreamEdges(nodeId, direction2) {
25391
- 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;
25392
25571
  }
25393
25572
  /** 获取主干边 */
25394
25573
  getMainEdge() {
@@ -25402,14 +25581,15 @@ class TopoApi {
25402
25581
  return true;
25403
25582
  });
25404
25583
  }
25405
- /** 获取节点坐标 */
25406
- getNodePosition(nodeId) {
25407
- const p = this._layout.pos.get(nodeId);
25408
- 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;
25409
25588
  }
25410
- /** 获取边折线点序列 */
25411
- getEdgePath(srcId, tgtId) {
25412
- 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);
25413
25593
  return cps ? cps.map(([x, y]) => [x, y]) : null;
25414
25594
  }
25415
25595
  /** 获取图整体边界 */
@@ -25449,17 +25629,29 @@ class TopoApi {
25449
25629
  const current = this._graph.getZoom();
25450
25630
  this._graph.zoomTo(current / (1 + step2));
25451
25631
  }
25452
- /** 定位节点到视口中央 */
25453
- locateNode(nodeId, zoom) {
25632
+ /** 定位节点到视口中央(ref 可传 id/psrId/资产编号;电表户聚焦其表位) */
25633
+ locateNode(ref2, zoom) {
25454
25634
  this._assertReady();
25455
- const p = this._layout.pos.get(nodeId);
25456
- if (!p) return;
25635
+ const t = this._resolveTarget(ref2);
25636
+ if (!t) return;
25637
+ const world = this._targetWorld(ref2);
25638
+ if (!world) return;
25457
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
+ }
25458
25647
  try {
25459
- this._graph.focusElement(nodeId);
25460
- } catch {
25461
- const center = this._canvasCenterXY();
25462
- 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);
25463
25655
  }
25464
25656
  }
25465
25657
  /** 获取当前视口状态 */
@@ -25472,25 +25664,34 @@ class TopoApi {
25472
25664
  // ============================================================
25473
25665
  // 三、节点样式
25474
25666
  // ============================================================
25475
- /** 设置单个节点样式 */
25476
- setNodeStyle(nodeId, style) {
25667
+ /** 设置单个节点样式(ref = 节点id/psrId;传 assetNo 时作用于其所在计量箱节点) */
25668
+ setNodeStyle(ref2, style) {
25477
25669
  this._assertReady();
25478
- this._snapshotNodeStyle(nodeId);
25479
- 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 }]);
25480
25677
  }
25481
- /** 批量设置节点样式 */
25482
- batchSetNodeStyle(nodeIds, style) {
25678
+ /** 批量设置节点样式(每一项可为 id/psrId/assetNo) */
25679
+ batchSetNodeStyle(refs, style) {
25483
25680
  this._assertReady();
25484
- for (const id2 of nodeIds) this._snapshotNodeStyle(id2);
25485
- 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 })));
25486
25685
  }
25487
- /** 重置单个节点样式 */
25488
- resetNodeStyle(nodeId) {
25686
+ /** 重置单个节点样式(ref = 节点id/psrId/assetNo) */
25687
+ resetNodeStyle(ref2) {
25489
25688
  this._assertReady();
25490
- const orig = this._originalNodeStyles.get(nodeId);
25689
+ const id2 = this._targetNodeId(ref2);
25690
+ if (id2 == null) return;
25691
+ const orig = this._originalNodeStyles.get(id2);
25491
25692
  if (orig) {
25492
- this._updateNodes([{ id: nodeId, style: orig }]);
25493
- this._originalNodeStyles.delete(nodeId);
25693
+ this._updateNodes([{ id: id2, style: orig }]);
25694
+ this._originalNodeStyles.delete(id2);
25494
25695
  }
25495
25696
  }
25496
25697
  /** 重置所有节点样式 */
@@ -25504,30 +25705,269 @@ class TopoApi {
25504
25705
  this._originalNodeStyles.clear();
25505
25706
  }
25506
25707
  // ============================================================
25708
+ // 三·五、节点 / 箱内电表户染色(支持 psrId / 资产编号 / 节点 id)
25709
+ //
25710
+ // 普通节点(变压器/开关/熔丝/导线点/用户接入点…)→ 图元“整图单色换装”,
25711
+ // 主体/引线/描边换成指定色,白字与浅色细节保留可读;
25712
+ // 箱内电表户(计量箱 grid/single 里的每只电表)→ 该户电表图标与户名染色。
25713
+ // 引用规则与 addConnection 完全一致:
25714
+ // 节点:节点内部 id 或 psrId(设备编码);电表户:客户档案 assetNo(资产编号,
25715
+ // 兼容 consNo / consId)。染色记录随数据重渲染(主题切换/换数据)自动保留重放。
25716
+ // ============================================================
25717
+ /**
25718
+ * 把当前记录的染色覆盖(节点 / 电表户)重新写进图 style(update 后调用)。
25719
+ * 仅做 updateNodeData 合并,不主动 render —— 由随后的一次 render 统一生效。
25720
+ * 顺带清理指向已不存在节点 / 表位的过期记录(换数据后自动丢弃)。
25721
+ */
25722
+ _applyColorOverrides() {
25723
+ if (!this._graph) return;
25724
+ const updates = [];
25725
+ if (this._nodeColorOverrides.size) {
25726
+ const stale = [];
25727
+ for (const [id2, color2] of this._nodeColorOverrides) {
25728
+ if (!this._graph.getNodeData(id2)) {
25729
+ stale.push(id2);
25730
+ continue;
25731
+ }
25732
+ updates.push({ id: id2, style: { color: color2 || "" } });
25733
+ }
25734
+ for (const id2 of stale) this._nodeColorOverrides.delete(id2);
25735
+ }
25736
+ if (this._custColorOverrides.size) {
25737
+ const byBox = /* @__PURE__ */ new Map();
25738
+ const stale = [];
25739
+ for (const [key, color2] of this._custColorOverrides) {
25740
+ const sep = key.indexOf("#cust");
25741
+ if (sep < 0) {
25742
+ stale.push(key);
25743
+ continue;
25744
+ }
25745
+ const boxId = key.slice(0, sep);
25746
+ if (!this._graph.getNodeData(boxId)) {
25747
+ stale.push(key);
25748
+ continue;
25749
+ }
25750
+ if (!byBox.has(boxId)) byBox.set(boxId, /* @__PURE__ */ new Map());
25751
+ byBox.get(boxId).set(Number(key.slice(sep + 5)), color2);
25752
+ }
25753
+ for (const [boxId, cellMap] of byBox) {
25754
+ const cur = this._safeNodeStyle(boxId, {});
25755
+ const customers = cur._customers;
25756
+ if (!Array.isArray(customers) || !customers.length) continue;
25757
+ const arr = new Array(customers.length).fill(null);
25758
+ let any = false;
25759
+ for (const [idx, color2] of cellMap) {
25760
+ if (idx >= 0 && idx < arr.length && color2) {
25761
+ arr[idx] = color2;
25762
+ any = true;
25763
+ }
25764
+ }
25765
+ updates.push({ id: boxId, style: { _cellColors: any ? arr : null } });
25766
+ }
25767
+ for (const key of stale) this._custColorOverrides.delete(key);
25768
+ }
25769
+ if (updates.length) {
25770
+ try {
25771
+ this._graph.updateNodeData(updates);
25772
+ } catch (e2) {
25773
+ console.error("[TopoApi] _applyColorOverrides error:", e2);
25774
+ }
25775
+ }
25776
+ }
25777
+ /**
25778
+ * 写入某个解析端点的染色(setNodeColor / batchSetNodeColor 共用)。
25779
+ * @param {{kind:'node',id:string}|{kind:'customer',boxId:string,index:number,node:object}} e
25780
+ * @param {string|null} color 颜色;null/''/undefined = 清除该处染色
25781
+ */
25782
+ _writeColorEndpoint(e2, color2) {
25783
+ if (!this._graph) return;
25784
+ color2 = color2 == null ? null : String(color2).trim() || null;
25785
+ if (e2.kind === "node") {
25786
+ const id2 = e2.id;
25787
+ if (color2) this._nodeColorOverrides.set(id2, color2);
25788
+ else this._nodeColorOverrides.delete(id2);
25789
+ this._updateNodes([{ id: id2, style: { color: color2 || "" } }]);
25790
+ return;
25791
+ }
25792
+ const key = `${e2.boxId}#cust${e2.index}`;
25793
+ if (color2) this._custColorOverrides.set(key, color2);
25794
+ else this._custColorOverrides.delete(key);
25795
+ const cur = this._safeNodeStyle(e2.boxId, {});
25796
+ const customers = cur._customers;
25797
+ if (!Array.isArray(customers) || !customers.length) return;
25798
+ const arr = new Array(customers.length).fill(null);
25799
+ let any = false;
25800
+ for (const [k, c] of this._custColorOverrides) {
25801
+ if (!k.startsWith(`${e2.boxId}#cust`)) continue;
25802
+ const idx = Number(k.slice(k.indexOf("#cust") + 5));
25803
+ if (idx >= 0 && idx < arr.length && c) {
25804
+ arr[idx] = c;
25805
+ any = true;
25806
+ }
25807
+ }
25808
+ this._updateNodes([{ id: e2.boxId, style: { _cellColors: any ? arr : null } }]);
25809
+ }
25810
+ /**
25811
+ * 给节点 / 箱内电表户设置颜色。
25812
+ * @param {string} ref 节点内部 id / 节点 psrId(普通节点、计量箱本体)
25813
+ * 或 客户资产编号 assetNo(兼容 consNo/consId,箱内电表户)
25814
+ * @param {string} [color] 颜色('#RRGGBB' / 'red' 等);传 null/''/undefined 清除该处染色
25815
+ * @returns {boolean} 是否命中并设置成功
25816
+ */
25817
+ setNodeColor(ref2, color2) {
25818
+ this._assertReady();
25819
+ const e2 = this._parseEndpoint(ref2);
25820
+ if (!e2) {
25821
+ console.warn(`[TopoApi] setNodeColor: 无法解析目标 ${ref2}(普通节点请用 psrId/节点id,电表户请用 assetNo/consNo/consId)`);
25822
+ return false;
25823
+ }
25824
+ this._writeColorEndpoint(e2, color2);
25825
+ return true;
25826
+ }
25827
+ /**
25828
+ * 批量设置颜色。三种入参形式:
25829
+ * 1) 数组:[ ['8ff4…', '#FF0000'], ['4230…资产编号', '#00FF00'], … ]
25830
+ * 2) 数组:[{ ref / psrId / assetNo / nodeId, color? }](color 缺省用第 2 参)
25831
+ * 3) 对象:{ 'psrId或assetNo': '#FF0000', … }
25832
+ * @param {*} entries
25833
+ * @param {string} [color] 未在单项里给色时的兜底颜色
25834
+ * @returns {{total:number, ok:number, failed:Array<{ref:string, reason:string}>}}
25835
+ */
25836
+ batchSetNodeColor(entries, color2) {
25837
+ this._assertReady();
25838
+ const failed = [];
25839
+ let total = 0;
25840
+ const applyRef = (ref2, c) => {
25841
+ total++;
25842
+ const e2 = this._parseEndpoint(ref2);
25843
+ if (!e2) {
25844
+ failed.push({ ref: String(ref2), reason: "未命中任何节点/电表户" });
25845
+ return;
25846
+ }
25847
+ this._writeColorEndpoint(e2, c);
25848
+ };
25849
+ if (Array.isArray(entries)) {
25850
+ for (const item of entries) {
25851
+ if (Array.isArray(item)) {
25852
+ applyRef(item[0], item[1] != null ? item[1] : color2);
25853
+ } else if (item && typeof item === "object") {
25854
+ const ref2 = item.ref != null ? item.ref : item.psrId != null ? item.psrId : item.assetNo != null ? item.assetNo : item.nodeId;
25855
+ if (ref2 == null) {
25856
+ total++;
25857
+ failed.push({ ref: JSON.stringify(item), reason: "缺少 ref/psrId/assetNo/nodeId" });
25858
+ continue;
25859
+ }
25860
+ applyRef(ref2, item.color != null ? item.color : color2);
25861
+ }
25862
+ }
25863
+ } else if (entries && typeof entries === "object") {
25864
+ for (const [ref2, c] of Object.entries(entries)) applyRef(ref2, c);
25865
+ }
25866
+ return { total, ok: total - failed.length, failed };
25867
+ }
25868
+ /**
25869
+ * 清除某处染色(返回节点 / 电表户到主题默认配色)。
25870
+ * @param {string} ref 同 setNodeColor 的引用
25871
+ */
25872
+ resetNodeColor(ref2) {
25873
+ this._assertReady();
25874
+ const e2 = this._parseEndpoint(ref2);
25875
+ if (!e2) {
25876
+ console.warn(`[TopoApi] resetNodeColor: 无法解析目标 ${ref2}`);
25877
+ return false;
25878
+ }
25879
+ this._writeColorEndpoint(e2, null);
25880
+ return true;
25881
+ }
25882
+ /** 清除全部染色(所有节点与电表户回到主题默认配色) */
25883
+ resetAllNodeColors() {
25884
+ this._assertReady();
25885
+ const nodeUpdates = [];
25886
+ for (const [id2] of this._nodeColorOverrides) {
25887
+ if (this._graph.getNodeData(id2)) nodeUpdates.push({ id: id2, style: { color: "" } });
25888
+ }
25889
+ const boxes = /* @__PURE__ */ new Set();
25890
+ for (const [key] of this._custColorOverrides) {
25891
+ const sep = key.indexOf("#cust");
25892
+ if (sep >= 0) boxes.add(key.slice(0, sep));
25893
+ }
25894
+ for (const boxId of boxes) {
25895
+ if (this._graph.getNodeData(boxId)) {
25896
+ nodeUpdates.push({ id: boxId, style: { _cellColors: null } });
25897
+ }
25898
+ }
25899
+ if (nodeUpdates.length) this._updateNodes(nodeUpdates);
25900
+ this._nodeColorOverrides.clear();
25901
+ this._custColorOverrides.clear();
25902
+ }
25903
+ /**
25904
+ * 查询某处当前生效的染色。
25905
+ * @param {string} ref 同 setNodeColor 的引用
25906
+ * @returns {{ kind: 'node'|'customer', id: string, index?: number, color: string|null } | null}
25907
+ */
25908
+ getNodeColor(ref2) {
25909
+ this._assertReady();
25910
+ const e2 = this._parseEndpoint(ref2);
25911
+ if (!e2) return null;
25912
+ if (e2.kind === "node") {
25913
+ const color3 = this._nodeColorOverrides.get(e2.id);
25914
+ if (color3) return { kind: "node", id: e2.id, color: color3 };
25915
+ const cur2 = this._safeNodeStyle(e2.id, {});
25916
+ return { kind: "node", id: e2.id, color: cur2.color || null };
25917
+ }
25918
+ const key = `${e2.boxId}#cust${e2.index}`;
25919
+ const color2 = this._custColorOverrides.get(key);
25920
+ if (color2) return { kind: "customer", id: key, boxId: e2.boxId, index: e2.index, color: color2 };
25921
+ const cur = this._safeNodeStyle(e2.boxId, {});
25922
+ const arr = cur._cellColors;
25923
+ return {
25924
+ kind: "customer",
25925
+ id: key,
25926
+ boxId: e2.boxId,
25927
+ index: e2.index,
25928
+ color: Array.isArray(arr) && arr[e2.index] || null
25929
+ };
25930
+ }
25931
+ // ============================================================
25507
25932
  // 四、边样式
25508
25933
  // ============================================================
25509
- /** 设置边样式 */
25510
- 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) {
25511
25942
  this._assertReady();
25512
- 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]);
25513
25949
  this._snapshotEdgeStyle(edgeId);
25514
25950
  this._updateEdges([{ id: edgeId, style }]);
25515
25951
  }
25516
- /** 批量设置边样式 */
25517
- batchSetEdgeStyle(edgeIds, style) {
25952
+ /** 批量设置边样式(每个端对可为 id/psrId/assetNo) */
25953
+ batchSetEdgeStyle(edgeRefs, style) {
25518
25954
  this._assertReady();
25519
25955
  const updates = [];
25520
- for (const [src, tgt] of edgeIds) {
25521
- 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]);
25522
25960
  this._snapshotEdgeStyle(eid);
25523
25961
  updates.push({ id: eid, style });
25524
25962
  }
25525
25963
  if (updates.length) this._updateEdges(updates);
25526
25964
  }
25527
- /** 重置边样式 */
25528
- resetEdgeStyle(srcId, tgtId) {
25965
+ /** 重置边样式(端点可为节点 id/psrId/资产编号 assetNo) */
25966
+ resetEdgeStyle(srcRef, tgtRef) {
25529
25967
  this._assertReady();
25530
- 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]);
25531
25971
  const orig = this._originalEdgeStyles.get(edgeId);
25532
25972
  if (orig) {
25533
25973
  this._updateEdges([{ id: edgeId, style: orig }]);
@@ -25538,54 +25978,92 @@ class TopoApi {
25538
25978
  // 五、高亮与标注
25539
25979
  // ============================================================
25540
25980
  /**
25541
- * 高亮节点
25542
- * @param {string} nodeId
25981
+ * 高亮节点 / 箱内电表户
25982
+ * @param {string} ref - 节点 id / psrId / 资产编号(电表户 → 该户表位高亮环 + 可选文字)
25543
25983
  * @param {object} [opts] - { color, label, labelColor, labelSize, borderColor, borderStyle, borderWidth }
25544
25984
  */
25545
- highlightNode(nodeId, opts = {}) {
25985
+ highlightNode(ref2, opts = {}) {
25546
25986
  this._assertReady();
25547
- this._snapshotNodeStyle(nodeId);
25548
- const style = {
25549
- 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]
25550
26013
  };
25551
- if (opts.borderColor) style.highlightColor = opts.borderColor;
25552
- if (opts.borderWidth) style.highlightWidth = opts.borderWidth;
25553
- if (opts.borderStyle === "solid") style.highlightDash = [];
25554
26014
  if (opts.label) {
25555
- style.annotationText = opts.label;
25556
- style.annotationColor = opts.labelColor || "#FFFFFF";
25557
- 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
+ };
25558
26022
  }
25559
- this._updateNodes([{ id: nodeId, style }]);
26023
+ this._flushCellFx(t.boxId);
25560
26024
  }
25561
- /** 取消节点高亮 */
25562
- unhighlightNode(nodeId) {
26025
+ /** 取消节点 / 箱内电表户高亮(ref 可传 id/psrId/assetNo) */
26026
+ unhighlightNode(ref2) {
25563
26027
  this._assertReady();
25564
- this._updateNodes([{
25565
- id: nodeId,
25566
- style: {
25567
- highlight: 0,
25568
- annotationText: "",
25569
- annotationColor: "",
25570
- annotationSize: 0
25571
- }
25572
- }]);
25573
- 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);
25574
26049
  }
25575
- /** 高亮路径(节点+边) */
25576
- highlightPath(nodeIds, opts = {}) {
26050
+ /** 高亮路径(节点与边;节点可为 id/psrId,箱内户可用 assetNo) */
26051
+ highlightPath(refs, opts = {}) {
25577
26052
  this._assertReady();
25578
- for (const id2 of nodeIds) {
26053
+ for (const id2 of refs) {
25579
26054
  this.highlightNode(id2, opts);
25580
26055
  }
25581
26056
  const edgeColor = opts.edgeColor || opts.color || "#00C8FF";
25582
- for (let i = 0; i + 1 < nodeIds.length; i++) {
25583
- 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);
25584
26062
  this._snapshotEdgeStyle(eid);
25585
26063
  this._updateEdges([{ id: eid, style: { overrideStroke: edgeColor, overrideLineWidth: 2.5 } }]);
25586
26064
  }
25587
26065
  }
25588
- /** 取消所有高亮 */
26066
+ /** 取消所有高亮(含箱内电表户高亮环) */
25589
26067
  unhighlightAll() {
25590
26068
  this._assertReady();
25591
26069
  const nodeUpdates = [];
@@ -25603,11 +26081,26 @@ class TopoApi {
25603
26081
  }
25604
26082
  if (edgeUpdates.length) this._updateEdges(edgeUpdates);
25605
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
+ }
25606
26099
  }
25607
- /** 非指定节点变暗 */
25608
- dimOthers(keepIds, opacity2 = 0.15) {
26100
+ /** 非指定节点变暗(keepIds 内每一项可为 节点id/psrId/assetNo) */
26101
+ dimOthers(keepRefs, opacity2 = 0.15) {
25609
26102
  this._assertReady();
25610
- const keepSet = new Set(keepIds);
26103
+ const keepSet = new Set(keepRefs.map((r) => this._targetNodeId(r)).filter((v) => v != null));
25611
26104
  const updates = [];
25612
26105
  for (const n of this._model.nodes) {
25613
26106
  if (!keepSet.has(n.id)) {
@@ -25621,13 +26114,34 @@ class TopoApi {
25621
26114
  // 六、动画
25622
26115
  // ============================================================
25623
26116
  /**
25624
- * 节点脉动动画(G6 v5: updateNodeData + draw 方式)
26117
+ * 节点脉动动画(G6 v5: updateNodeData + draw 方式)。
26118
+ * ref = 节点 id/psrId → 图元透明度呼吸;assetNo → 该户电表格淡入淡出。
26119
+ * opts: { duration, min, max }(min/max 为透明度下限/上限,默认 0.3/1)
25625
26120
  */
25626
- setNodePulse(nodeId, opts = {}) {
26121
+ setNodePulse(ref2, opts = {}) {
25627
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;
25628
26143
  this.removeAnimation(nodeId);
25629
26144
  const duration2 = opts.duration || 1500;
25630
- this._safeNodeStyle(nodeId, {});
25631
26145
  let start = null;
25632
26146
  const animate = (ts) => {
25633
26147
  if (!this._graph || !this._animations.has(nodeId)) return;
@@ -25711,8 +26225,19 @@ class TopoApi {
25711
26225
  const raf2 = requestAnimationFrame(animate);
25712
26226
  this._animations.set(edgeId, raf2);
25713
26227
  }
25714
- /** 移除指定元素动画 */
26228
+ /** 移除指定元素动画(ref = 节点 id/psrId → 脉动/边流动;assetNo → 该户脉动+光晕) */
25715
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
+ }
25716
26241
  const raf2 = this._animations.get(targetId);
25717
26242
  if (raf2) {
25718
26243
  cancelAnimationFrame(raf2);
@@ -25735,9 +26260,11 @@ class TopoApi {
25735
26260
  console.error("[TopoApi] removeAnimation update error:", e2);
25736
26261
  }
25737
26262
  }
25738
- /** 移除所有动画 */
26263
+ /** 移除所有动画(脉动/流动/光晕/户脉动户光晕) */
25739
26264
  removeAllAnimations() {
25740
26265
  this._removeAllAnimationsInternal();
26266
+ this._removeAllGlowsInternal();
26267
+ this._removeAllCellPulses();
25741
26268
  }
25742
26269
  _removeAllAnimationsInternal() {
25743
26270
  for (const [id2, raf2] of this._animations) {
@@ -25761,12 +26288,270 @@ class TopoApi {
25761
26288
  }
25762
26289
  this._animations.clear();
25763
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
+ }
25764
26528
  // ============================================================
25765
26529
  // 七、文字标注
25766
26530
  // ============================================================
25767
- /** 设置附加文字 */
25768
- setText(nodeId, text2, opts = {}) {
26531
+ /**
26532
+ * 设置附加文字(ref = 节点 id/psrId → 节点旁标注;assetNo → 该户表位上方小标注)
26533
+ * opts: { color, fontSize, position: 'top'|'bottom' }
26534
+ */
26535
+ setText(ref2, text2, opts = {}) {
25769
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;
25770
26555
  this._snapshotNodeStyle(nodeId);
25771
26556
  this._nodeLabels.set(nodeId, { text: text2, opts });
25772
26557
  this._updateNodes([{
@@ -25780,16 +26565,28 @@ class TopoApi {
25780
26565
  }
25781
26566
  }]);
25782
26567
  }
25783
- /** 移除附加文字 */
25784
- removeText(nodeId) {
26568
+ /** 移除附加文字(ref = 节点 id/psrId/资产编号) */
26569
+ removeText(ref2) {
25785
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;
25786
26583
  this._nodeLabels.delete(nodeId);
25787
26584
  this._updateNodes([{
25788
26585
  id: nodeId,
25789
26586
  style: { annotationText: "", annotationColor: "", annotationSize: 0 }
25790
26587
  }]);
25791
26588
  }
25792
- /** 移除所有附加文字 */
26589
+ /** 移除所有附加文字(含箱内电表户标注) */
25793
26590
  removeAllTexts() {
25794
26591
  this._assertReady();
25795
26592
  const updates = [];
@@ -25798,6 +26595,18 @@ class TopoApi {
25798
26595
  }
25799
26596
  if (updates.length) this._updateNodes(updates);
25800
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);
25801
26610
  }
25802
26611
  /** 按设备类型设字号 */
25803
26612
  setDeviceText(cat, fontSize2) {
@@ -25827,22 +26636,30 @@ class TopoApi {
25827
26636
  // 八、卡片盒
25828
26637
  // ============================================================
25829
26638
  /**
25830
- * 在节点旁弹卡片
25831
- * @param {string} nodeId
25832
- * @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
+ * 缺省自动按「设备 / 客户档案」生成字段
25833
26643
  */
25834
- showCard(nodeId, content) {
26644
+ showCard(ref2, content) {
25835
26645
  var _a, _b, _c, _d, _e, _f;
25836
26646
  this._assertReady();
25837
- 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);
25838
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));
25839
26656
  if (!container) return;
25840
- const pos = this._layout.pos.get(nodeId);
25841
- if (!pos) return;
26657
+ const world = this._targetWorld(ref2);
26658
+ if (!world) return;
25842
26659
  let client = null;
25843
26660
  try {
25844
26661
  if (typeof this._graph.getClientByCanvas === "function") {
25845
- const r = this._graph.getClientByCanvas([pos.x, pos.y]);
26662
+ const r = this._graph.getClientByCanvas([world.x, world.y]);
25846
26663
  client = Array.isArray(r) ? { x: r[0], y: r[1] } : r;
25847
26664
  }
25848
26665
  } catch (e2) {
@@ -25855,12 +26672,37 @@ class TopoApi {
25855
26672
  const zoom = this._graph.getZoom();
25856
26673
  const canvasCenter = this._canvasCenterXY();
25857
26674
  const containerRect = container.getBoundingClientRect();
25858
- screenX = containerRect.left + canvasCenter.x + pos.x * zoom;
25859
- 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
+ }
25860
26702
  }
25861
26703
  const el = document.createElement("div");
25862
26704
  el.className = "topo-card";
25863
- el.dataset.nodeId = nodeId;
26705
+ el.dataset.nodeId = cardKey;
25864
26706
  el.style.cssText = `
25865
26707
  position: fixed; left: ${screenX + 30}px; top: ${screenY - 20}px;
25866
26708
  width: ${content.width || 240}px; z-index: 1000;
@@ -25870,22 +26712,22 @@ class TopoApi {
25870
26712
  `;
25871
26713
  const head = document.createElement("div");
25872
26714
  head.style.cssText = "display:flex;align-items:center;justify-content:space-between;padding:8px 12px;border-bottom:1px solid #2c2c30;";
25873
- 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>`;
25874
26716
  if (content.closable !== false) {
25875
26717
  const closeBtn = document.createElement("button");
25876
26718
  closeBtn.textContent = "×";
25877
26719
  closeBtn.style.cssText = "border:none;background:none;color:#888;font-size:16px;cursor:pointer;";
25878
- closeBtn.onclick = () => this.hideCard(nodeId);
26720
+ closeBtn.onclick = () => this.hideCard(cardKey);
25879
26721
  head.appendChild(closeBtn);
25880
26722
  }
25881
26723
  el.appendChild(head);
25882
- if (content.fields && content.fields.length) {
26724
+ if (cFields && cFields.length) {
25883
26725
  const body = document.createElement("div");
25884
26726
  body.style.cssText = "padding:8px 12px;";
25885
- for (const f2 of content.fields) {
26727
+ for (const f2 of cFields) {
25886
26728
  const row2 = document.createElement("div");
25887
26729
  row2.style.cssText = "display:flex;gap:8px;margin-bottom:4px;";
25888
- 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>`;
25889
26731
  body.appendChild(row2);
25890
26732
  }
25891
26733
  el.appendChild(body);
@@ -25899,21 +26741,34 @@ class TopoApi {
25899
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;";
25900
26742
  b.onclick = () => {
25901
26743
  if (btn.onClick) btn.onClick();
25902
- 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
+ }
25903
26756
  };
25904
26757
  foot.appendChild(b);
25905
26758
  });
25906
26759
  el.appendChild(foot);
25907
26760
  }
25908
26761
  document.body.appendChild(el);
25909
- this._cards.set(nodeId, el);
26762
+ this._cards.set(cardKey, el);
25910
26763
  }
25911
- /** 关闭指定卡片 */
25912
- hideCard(nodeId) {
25913
- 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);
25914
26769
  if (el) {
25915
26770
  el.remove();
25916
- this._cards.delete(nodeId);
26771
+ this._cards.delete(key);
25917
26772
  }
25918
26773
  }
25919
26774
  /** 关闭所有卡片 */
@@ -26247,12 +27102,17 @@ class TopoApi {
26247
27102
  }
26248
27103
  /**
26249
27104
  * 添加拓扑高亮线
26250
- * @param {string[]} nodeIds - 节点路径
27105
+ * @param {string[]} refs - 节点路径(每点可为 id/psrId;assetNo 落到所在计量箱)
26251
27106
  * @param {object} [opts] - { color, width, id }
26252
27107
  * @returns {string} 线 ID
26253
27108
  */
26254
- addTopoLine(nodeIds, opts = {}) {
27109
+ addTopoLine(refs, opts = {}) {
26255
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
+ }
26256
27116
  const lineId = opts.id || `topo-${Date.now()}`;
26257
27117
  const color2 = opts.color || "#00C8FF";
26258
27118
  const width = opts.width || 3;
@@ -27899,6 +28759,9 @@ function buildGraphData(model, layout) {
27899
28759
  size: [w, h],
27900
28760
  // 自定义字段(进 attributes,供 svg-symbol 节点绘制使用)
27901
28761
  cat: n.cat,
28762
+ // labelText:节点标签文字(canvasNode 用固定每行 N 字换行绘制)。
28763
+ // 禁用 G6 内置 label(label:false),避免其单行画出长名不换行。
28764
+ label: false,
27902
28765
  labelText: n.cat === "bus" || n.cat === "cableTerminal" || n.cat === "consumer" ? "" : n.shortName || "",
27903
28766
  rotate: layout.stationRotate && layout.stationRotate.get(n.id) || 0,
27904
28767
  highlight: 0,
@@ -28035,21 +28898,56 @@ function setNodePalette(themeName) {
28035
28898
  if (key in C2) C2[key] = t[key];
28036
28899
  }
28037
28900
  }
28901
+ function nodeColor(attributes) {
28902
+ return attributes && attributes.color || null;
28903
+ }
28904
+ function cellColorAt(attributes, i) {
28905
+ const arr = attributes && attributes._cellColors;
28906
+ return Array.isArray(arr) ? arr[i] || null : null;
28907
+ }
28908
+ function localPalette(attributes) {
28909
+ const c = nodeColor(attributes);
28910
+ if (!c) return C2;
28911
+ return {
28912
+ ...C2,
28913
+ red: c,
28914
+ orange: c,
28915
+ cyan: c,
28916
+ gold: c,
28917
+ trunkLine: c,
28918
+ dotFill: withAlpha(c, 0.18)
28919
+ };
28920
+ }
28921
+ function withAlpha(color2, a2) {
28922
+ if (typeof color2 !== "string") return color2;
28923
+ const m = color2.match(/^#([0-9a-f]{6}|[0-9a-f]{3})$/i);
28924
+ if (!m) return color2;
28925
+ let hex2 = m[1];
28926
+ if (hex2.length === 3) hex2 = hex2.split("").map((h) => h + h).join("");
28927
+ const n = parseInt(hex2, 16);
28928
+ const r = n >> 16 & 255, g = n >> 8 & 255, b = n & 255;
28929
+ return `rgba(${r},${g},${b},${a2})`;
28930
+ }
28931
+ const LW = 1.8;
28932
+ const LW_HI = 2.2;
28038
28933
  function drawHighlight(attributes, group) {
28039
28934
  if (!attributes.highlight) return;
28040
28935
  const [w, h] = attributes.size;
28936
+ const color2 = attributes.highlightColor || C2.gold;
28937
+ const lw = attributes.highlightWidth || 2;
28938
+ const dash = attributes.highlightDash || [5, 4];
28041
28939
  group.appendChild(
28042
28940
  new Rect({
28043
28941
  style: {
28044
- x: -w / 2 + 1,
28045
- y: -h / 2 + 1,
28046
- width: w - 2,
28047
- height: h - 2,
28048
- radius: 4,
28942
+ x: -w / 2 - 3,
28943
+ y: -h / 2 - 3,
28944
+ width: w + 6,
28945
+ height: h + 6,
28946
+ radius: 6,
28049
28947
  fill: "none",
28050
- stroke: C2.gold,
28051
- lineWidth: 1.6,
28052
- lineDash: [4, 3]
28948
+ stroke: color2,
28949
+ lineWidth: lw,
28950
+ lineDash: dash
28053
28951
  }
28054
28952
  })
28055
28953
  );
@@ -28061,15 +28959,16 @@ function drawAnnotation(attributes, group) {
28061
28959
  const pos = attributes.annotationPos || "top";
28062
28960
  const offset = attributes.annotationOffset || 4;
28063
28961
  const y = pos === "top" ? -h / 2 - offset - 6 : h / 2 + offset + 6;
28064
- _drawWrappedText(group, label, 0, y, attributes.annotationSize || 12, attributes.annotationColor || C2.white, w + 10, 1.25);
28962
+ _drawWrappedText(group, label, 0, y, attributes.annotationSize || 12, attributes.annotationColor || C2.white, w + 10, 1.25, 600);
28065
28963
  }
28066
28964
  function drawLabelUnder(attributes, group) {
28067
28965
  const label = attributes.labelText || "";
28068
28966
  if (!label) return;
28069
28967
  const [w, h] = attributes.size;
28070
- _drawWrappedText(group, label, 0, h / 2 + 8, 11, C2.white, w + 4, 1.3);
28968
+ const x0 = w / 2 + 6;
28969
+ _drawWrappedTextFixed(group, label, x0, -h / 2, 8, C2.white, 4, 1.3, 500, "left");
28071
28970
  }
28072
- function text(attributes, group, str2, x, y, size, fill) {
28971
+ function text(attributes, group, str2, x, y, size, fill, weight = 400) {
28073
28972
  if (!str2) return;
28074
28973
  group.appendChild(
28075
28974
  new Text({
@@ -28079,6 +28978,7 @@ function text(attributes, group, str2, x, y, size, fill) {
28079
28978
  y,
28080
28979
  fontSize: size,
28081
28980
  fill,
28981
+ fontWeight: weight,
28082
28982
  fontFamily: FONT,
28083
28983
  textAlign: "center",
28084
28984
  textBaseline: "middle"
@@ -28109,7 +29009,44 @@ function _wrapLines(str2, fontSize2, maxWidth) {
28109
29009
  if (line) lines.push(line);
28110
29010
  return lines;
28111
29011
  }
28112
- function _drawWrappedText(group, str2, x, y, fontSize2, fill, maxWidth, lineGap = 1.3) {
29012
+ function _wrapLinesFixed(str2, chars2) {
29013
+ if (!str2) return [];
29014
+ const lines = [];
29015
+ let line = "";
29016
+ for (const ch of str2) {
29017
+ line += ch;
29018
+ if (line.length >= chars2) {
29019
+ lines.push(line);
29020
+ line = "";
29021
+ }
29022
+ }
29023
+ if (line) lines.push(line);
29024
+ return lines;
29025
+ }
29026
+ function _drawWrappedTextFixed(group, str2, x, y, fontSize2, fill, chars2, lineGap = 1.3, weight = 400, align = "center") {
29027
+ if (!str2) return;
29028
+ const lines = _wrapLinesFixed(str2, chars2);
29029
+ if (!lines.length) return;
29030
+ const lineHeight2 = fontSize2 * lineGap;
29031
+ for (let i = 0; i < lines.length; i++) {
29032
+ group.appendChild(
29033
+ new Text({
29034
+ style: {
29035
+ text: lines[i],
29036
+ x,
29037
+ y: y + lineHeight2 * i,
29038
+ fontSize: fontSize2,
29039
+ fill,
29040
+ fontWeight: weight,
29041
+ fontFamily: FONT,
29042
+ textAlign: align === "left" ? "left" : "center",
29043
+ textBaseline: "middle"
29044
+ }
29045
+ })
29046
+ );
29047
+ }
29048
+ }
29049
+ function _drawWrappedText(group, str2, x, y, fontSize2, fill, maxWidth, lineGap = 1.3, weight = 400) {
28113
29050
  if (!str2) return;
28114
29051
  const lines = _wrapLines(str2, fontSize2, maxWidth);
28115
29052
  if (!lines.length) return;
@@ -28124,6 +29061,7 @@ function _drawWrappedText(group, str2, x, y, fontSize2, fill, maxWidth, lineGap
28124
29061
  y: y - totalH / 2 + lineHeight2 * (i + 0.5),
28125
29062
  fontSize: fontSize2,
28126
29063
  fill,
29064
+ fontWeight: weight,
28127
29065
  fontFamily: FONT,
28128
29066
  textAlign: "center",
28129
29067
  textBaseline: "middle"
@@ -28133,77 +29071,172 @@ function _drawWrappedText(group, str2, x, y, fontSize2, fill, maxWidth, lineGap
28133
29071
  }
28134
29072
  }
28135
29073
  function drawTransformer(attributes, group) {
28136
- const lw = 1.4;
28137
- group.appendChild(new Line({ style: { x1: -24, y1: 0, x2: -15, y2: 0, stroke: C2.trunkLine, lineWidth: lw } }));
28138
- group.appendChild(new Circle({ style: { cx: -5, cy: 0, r: 10, fill: C2.orange, stroke: C2.trunkLine, lineWidth: 1.2 } }));
28139
- group.appendChild(new Circle({ style: { cx: 10, cy: 0, r: 10, fill: "none", stroke: C2.red, lineWidth: 1.2 } }));
28140
- group.appendChild(new Line({ style: { x1: 20, y1: 0, x2: 29, y2: 0, stroke: C2.red, lineWidth: lw } }));
29074
+ const P = localPalette(attributes);
29075
+ const lw = LW;
29076
+ group.appendChild(new Line({ style: { x1: -26, y1: 0, x2: -14, y2: 0, stroke: P.trunkLine, lineWidth: lw, lineCap: "round" } }));
29077
+ group.appendChild(new Circle({ style: { cx: -4.5, cy: 0, r: 10, fill: P.orange, stroke: P.trunkLine, lineWidth: 1.4 } }));
29078
+ group.appendChild(new Circle({ style: { cx: -7.2, cy: -3.2, r: 3.2, fill: withAlpha(C2.white, 0.25), stroke: "none" } }));
29079
+ group.appendChild(new Circle({ style: { cx: 10, cy: 0, r: 10, fill: withAlpha(P.red, 0.06), stroke: P.red, lineWidth: LW_HI } }));
29080
+ group.appendChild(new Line({ style: { x1: 20, y1: 0, x2: 30, y2: 0, stroke: P.red, lineWidth: lw, lineCap: "round" } }));
28141
29081
  }
28142
29082
  function drawFuse(attributes, group) {
28143
- const lw = 1.4;
28144
- group.appendChild(new Line({ style: { x1: -26, y1: 0, x2: -19, y2: 0, stroke: C2.red, lineWidth: lw } }));
28145
- group.appendChild(new Rect({ style: { x: -19, y: -7, width: 38, height: 14, radius: 1, fill: "none", stroke: C2.red, lineWidth: lw } }));
28146
- group.appendChild(new Circle({ style: { cx: -13, cy: 0, r: 3.6, fill: C2.red } }));
28147
- group.appendChild(new Rect({ style: { x: -8, y: -2.4, width: 5, height: 4.8, fill: C2.red } }));
28148
- group.appendChild(new Rect({ style: { x: -1, y: -5.4, width: 13, height: 10.8, fill: C2.red } }));
28149
- group.appendChild(new Line({ style: { x1: 19, y1: 0, x2: 26, y2: 0, stroke: C2.red, lineWidth: lw } }));
29083
+ const P = localPalette(attributes);
29084
+ const lw = LW;
29085
+ group.appendChild(new Line({ style: { x1: -28, y1: 0, x2: -19, y2: 0, stroke: P.red, lineWidth: lw, lineCap: "round" } }));
29086
+ group.appendChild(new Line({ style: { x1: 19, y1: 0, x2: 28, y2: 0, stroke: P.red, lineWidth: lw, lineCap: "round" } }));
29087
+ group.appendChild(new Rect({ style: { x: -19, y: -7.5, width: 38, height: 15, radius: 2, fill: withAlpha(P.red, 0.05), stroke: P.red, lineWidth: lw } }));
29088
+ group.appendChild(new Circle({ style: { cx: -13, cy: 0, r: 3.4, fill: P.red, stroke: "none" } }));
29089
+ group.appendChild(new Rect({ style: { x: -8.2, y: -2.4, width: 5, height: 4.8, radius: 0.6, fill: P.red } }));
29090
+ group.appendChild(new Rect({ style: { x: -1.4, y: -5.4, width: 13, height: 10.8, radius: 0.8, fill: P.red } }));
29091
+ group.appendChild(new Rect({ style: { x: 5, y: -1.2, width: 8, height: 2.4, fill: C2.white, opacity: 0.55 } }));
28150
29092
  }
28151
29093
  function drawBreaker(attributes, group) {
28152
- const lw = 1.4;
28153
- group.appendChild(new Line({ style: { x1: -28, y1: 0, x2: -21, y2: 0, stroke: C2.red, lineWidth: lw } }));
28154
- group.appendChild(new Rect({ style: { x: -21, y: -9, width: 42, height: 18, radius: 2, fill: "none", stroke: C2.red, lineWidth: lw } }));
28155
- group.appendChild(new Rect({ style: { x: -21, y: -9, width: 42, height: 7, fill: "rgba(255,0,0,0.25)" } }));
28156
- _drawWrappedText(group, attributes.labelText, 0, 0.5, 10, C2.white, 36, 1.2);
28157
- group.appendChild(new Line({ style: { x1: 21, y1: 0, x2: 28, y2: 0, stroke: C2.red, lineWidth: lw } }));
29094
+ const P = localPalette(attributes);
29095
+ const lw = LW;
29096
+ const [nw] = attributes.size;
29097
+ const bw = 42;
29098
+ const bh = 20, r = 4;
29099
+ const bx = -bw / 2, by = -bh / 2;
29100
+ const xEdge = (nw || 60) / 2;
29101
+ const xBox = bw / 2;
29102
+ if (xEdge > xBox) {
29103
+ group.appendChild(new Line({ style: { x1: -xEdge, y1: 0, x2: -xBox, y2: 0, stroke: P.red, lineWidth: lw, lineCap: "round" } }));
29104
+ group.appendChild(new Line({ style: { x1: xBox, y1: 0, x2: xEdge, y2: 0, stroke: P.red, lineWidth: lw, lineCap: "round" } }));
29105
+ }
29106
+ group.appendChild(new Rect({ style: { x: bx, y: by, width: bw, height: bh, radius: r, fill: P.red, stroke: withAlpha(C2.white, 0.22), lineWidth: 1 } }));
29107
+ if (attributes.labelText) {
29108
+ _drawWrappedText(group, attributes.labelText, 0, 0.6, 11.5, "#FFFFFF", bw - 8, 1.15, 700);
29109
+ }
28158
29110
  }
28159
29111
  function drawCableTerminal(attributes, group) {
28160
- group.appendChild(new Polygon({ style: { points: [[1, -8], [-7, 8], [9, 8]], fill: C2.red, stroke: C2.red, lineWidth: 1 } }));
29112
+ const P = localPalette(attributes);
29113
+ const lw = 1.6;
29114
+ group.appendChild(new Line({ style: { x1: -11, y1: 0, x2: -5.5, y2: 0, stroke: P.red, lineWidth: lw, lineCap: "round" } }));
29115
+ group.appendChild(new Polygon({
29116
+ style: {
29117
+ points: [[-5, -9.5], [11, 0], [-5, 9.5]],
29118
+ fill: P.red,
29119
+ stroke: withAlpha(C2.white, 0.3),
29120
+ lineWidth: 1
29121
+ }
29122
+ }));
28161
29123
  }
28162
29124
  function drawStationTerminal(attributes, group) {
28163
- group.appendChild(new Line({ style: { x1: -9, y1: 0, x2: -1, y2: 0, stroke: C2.red, lineWidth: 1.4 } }));
28164
- group.appendChild(new Polygon({ style: { points: [[0, -5], [0, 5], [9, 0]], fill: "none", stroke: C2.red, lineWidth: 1.4 } }));
29125
+ const P = localPalette(attributes);
29126
+ const lw = LW;
29127
+ group.appendChild(new Line({ style: { x1: -12, y1: 0, x2: -4, y2: 0, stroke: P.red, lineWidth: lw, lineCap: "round" } }));
29128
+ group.appendChild(new Polygon({
29129
+ style: {
29130
+ points: [[0, -6], [0, 6], [10, 0]],
29131
+ fill: withAlpha(P.red, 0.08),
29132
+ stroke: P.red,
29133
+ lineWidth: lw
29134
+ }
29135
+ }));
29136
+ }
29137
+ function drawMeterFace(group, cx, cy, meterW, meterH, lineColor, markCell, cellIdx) {
29138
+ const r = Math.max(2, meterW * 0.16);
29139
+ const body = new Rect({ style: { x: cx - meterW / 2, y: cy - meterH / 2, width: meterW, height: meterH, radius: r, fill: withAlpha(lineColor, 0.06), stroke: lineColor, lineWidth: 1.2, cursor: "pointer" } });
29140
+ if (markCell) markCell(body, cellIdx);
29141
+ group.appendChild(body);
29142
+ const winW = meterW * 0.66, winH = Math.max(2.4, meterH * 0.17);
29143
+ const win = new Rect({ style: { x: cx - winW / 2, y: cy - meterH * 0.31, width: winW, height: winH, radius: winH / 2, fill: lineColor, opacity: 0.5, stroke: "none" } });
29144
+ if (markCell) markCell(win, cellIdx);
29145
+ group.appendChild(win);
29146
+ const dot2 = new Circle({ style: { cx, cy: cy + meterH * 0.12, r: 1.6, fill: lineColor, opacity: 0.85, stroke: "none" } });
29147
+ if (markCell) markCell(dot2, cellIdx);
29148
+ group.appendChild(dot2);
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");
28165
29185
  }
28166
29186
  function drawMeterBox(attributes, group) {
28167
29187
  const [w, h] = attributes.size;
29188
+ const boxColor = nodeColor(attributes);
28168
29189
  if (attributes._meterBoxMode === "single") {
28169
29190
  const nodeId = attributes._nodeId || "";
28170
29191
  const sel = attributes._custSel === 0;
28171
29192
  const hover = attributes._custHover === 0;
28172
- const lineColor = sel ? C2.gold : hover ? C2.cyan : C2.white;
29193
+ const baseColor = cellColorAt(attributes, 0) || boxColor || C2.white;
29194
+ const lineColor = sel ? C2.gold : hover ? C2.cyan : baseColor;
29195
+ const fx = cellFxAt(attributes, 0);
29196
+ const cellGroup = new Group({});
28173
29197
  const markCell = (shape) => {
28174
29198
  if (!nodeId) return;
28175
29199
  shape.__custBox = nodeId;
28176
29200
  shape.__custIndex = 0;
28177
29201
  };
29202
+ const meterW = 20;
29203
+ const meterH = 26;
29204
+ const cx = 0;
29205
+ const cy = -1;
28178
29206
  if (sel || hover) {
28179
- const ring = new Circle({
29207
+ const ring = new Rect({
28180
29208
  style: {
28181
- cx: 0,
28182
- cy: -3,
28183
- r: 13,
28184
- fill: sel ? C2.gold : C2.cyan,
28185
- fillOpacity: sel ? 0.3 : 0.18,
29209
+ x: cx - meterW / 2 - 4,
29210
+ y: cy - meterH / 2 - 4,
29211
+ width: meterW + 8,
29212
+ height: meterH + 8,
29213
+ radius: 5,
29214
+ fill: "none",
28186
29215
  stroke: sel ? C2.gold : C2.cyan,
28187
- lineWidth: sel ? 1.6 : 1.1
29216
+ lineWidth: sel ? 2 : 1.3
28188
29217
  }
28189
29218
  });
28190
29219
  markCell(ring);
28191
- group.appendChild(ring);
28192
- }
28193
- const meterCircle = new Circle({ style: { cx: 0, cy: -3, r: 11, fill: "none", stroke: lineColor, lineWidth: sel ? 1.8 : 1.6, cursor: "pointer" } });
28194
- markCell(meterCircle);
28195
- group.appendChild(meterCircle);
28196
- const stub = new Line({ style: { x1: 0, y1: -14, x2: 0, y2: -7, stroke: lineColor, lineWidth: 1.6, cursor: "pointer" } });
28197
- markCell(stub);
28198
- group.appendChild(stub);
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);
29228
+ }
28199
29229
  const name = attributes._customers && attributes._customers[0] ? attributes._customers[0].realConsName || attributes._customers[0].consName || "" : "";
28200
29230
  if (name) {
28201
- const nameText = new Text({ style: { text: name, x: 0, y: 12, fontSize: 8, fill: sel ? C2.gold : hover ? C2.cyan : C2.white, fontFamily: FONT, textAlign: "center", textBaseline: "middle", cursor: "pointer" } });
28202
- markCell(nameText);
28203
- group.appendChild(nameText);
29231
+ _drawWrappedTextFixed(cellGroup, name, cx, meterH / 2 + 7, 7, lineColor, 4, 1.15, 500);
29232
+ }
29233
+ if (fx && fx.pulse && fx.pulse.opacity != null && fx.pulse.opacity < 1) {
29234
+ cellGroup.style.opacity = fx.pulse.opacity;
28204
29235
  }
29236
+ group.appendChild(cellGroup);
28205
29237
  } else if (attributes._meterBoxMode === "grid" && attributes._customers) {
28206
- group.appendChild(new Rect({ style: { x: -w / 2, y: -h / 2, width: w, height: h, fill: C2.meterFill, stroke: C2.meterText, lineWidth: 0.9, radius: 2 } }));
29238
+ const frameColor = boxColor || C2.meterText;
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 } }));
28207
29240
  const cols = attributes._meterBoxCols || 5;
28208
29241
  const customers = attributes._customers;
28209
29242
  const showName = attributes._meterBoxShowName;
@@ -28214,11 +29247,12 @@ function drawMeterBox(attributes, group) {
28214
29247
  const gridH = rows * cellH + (rows - 1) * METER_GAP;
28215
29248
  const startX = -gridW / 2;
28216
29249
  const startY = -gridH / 2;
28217
- const meterR = 9;
29250
+ const meterW = Math.min(13, cellW * 0.5);
29251
+ const meterH = meterW * 1.5;
29252
+ const hitR = Math.max(9, meterH * 0.4);
28218
29253
  const custSel = attributes._custSel == null ? -1 : attributes._custSel;
28219
29254
  const custHover = attributes._custHover == null ? -1 : attributes._custHover;
28220
29255
  const nodeId = attributes._nodeId || "";
28221
- const hitR = meterR + 2.5;
28222
29256
  const markCell = (shape, i) => {
28223
29257
  if (!nodeId) return;
28224
29258
  shape.__custBox = nodeId;
@@ -28231,9 +29265,12 @@ function drawMeterBox(attributes, group) {
28231
29265
  const cy = startY + row2 * (cellH + METER_GAP) + cellW / 2;
28232
29266
  const sel = i === custSel;
28233
29267
  const hover = i === custHover;
28234
- const lineColor = sel ? C2.gold : hover ? C2.cyan : C2.meterText;
29268
+ const baseColor = cellColorAt(attributes, i) || C2.meterText;
29269
+ const lineColor = sel ? C2.gold : hover ? C2.cyan : baseColor;
29270
+ const fx = cellFxAt(attributes, i);
29271
+ const cellGroup = new Group({});
28235
29272
  if (sel || hover) {
28236
- group.appendChild(new Circle({
29273
+ cellGroup.appendChild(new Circle({
28237
29274
  style: {
28238
29275
  cx,
28239
29276
  cy,
@@ -28241,49 +29278,83 @@ function drawMeterBox(attributes, group) {
28241
29278
  fill: sel ? C2.gold : C2.cyan,
28242
29279
  fillOpacity: sel ? 0.3 : 0.18,
28243
29280
  stroke: sel ? C2.gold : C2.cyan,
28244
- lineWidth: sel ? 1.6 : 1.1
29281
+ lineWidth: sel ? 1.7 : 1.1
28245
29282
  }
28246
29283
  }));
28247
29284
  }
28248
- group.appendChild(new Circle({ style: { cx, cy, r: meterR, fill: "none", stroke: lineColor, lineWidth: sel ? 1.2 : 0.8 } }));
28249
- group.appendChild(new Line({ style: { x1: cx, y1: cy - meterR, x2: cx, y2: cy - meterR + 4, stroke: lineColor, lineWidth: 0.8 } }));
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
+ }
28250
29297
  const hit = new Circle({ style: { cx, cy, r: hitR, fill: "rgba(255,255,255,0.01)", stroke: "none", cursor: "pointer" } });
28251
29298
  markCell(hit, i);
28252
- group.appendChild(hit);
29299
+ cellGroup.appendChild(hit);
28253
29300
  if (showName) {
28254
29301
  const name = customers[i].realConsName || customers[i].consName || "";
28255
29302
  if (name) {
28256
- const nameText = new Text({ style: { text: name, x: cx, y: cy + meterR + 6, fontSize: 7, fill: sel ? C2.gold : hover ? C2.cyan : C2.meterText, fontFamily: FONT, textAlign: "center", textBaseline: "middle", cursor: "pointer" } });
28257
- markCell(nameText, i);
28258
- group.appendChild(nameText);
29303
+ const hitName = new Circle({
29304
+ style: {
29305
+ cx,
29306
+ cy: cy + meterH / 2 + 6.5 + 4,
29307
+ r: Math.max(8, Math.min(14, Math.ceil(name.length / 4) * 4 + 4)),
29308
+ fill: "rgba(255,255,255,0.01)",
29309
+ stroke: "none",
29310
+ cursor: "pointer"
29311
+ }
29312
+ });
29313
+ markCell(hitName, i);
29314
+ cellGroup.appendChild(hitName);
28259
29315
  }
28260
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);
28261
29321
  }
28262
29322
  } else {
28263
- group.appendChild(new Rect({ style: { x: -9.5, y: -9.5, width: 19, height: 19, fill: C2.meterFill, stroke: C2.meterText, lineWidth: 0.9 } }));
28264
- text(attributes, group, "JX", 0, 0.5, 7.5, C2.meterText);
29323
+ const s2 = Math.min(w, h) || 21;
29324
+ const ink = boxColor || C2.meterText;
29325
+ group.appendChild(new Rect({ style: { x: -s2 / 2, y: -s2 / 2, width: s2, height: s2, radius: Math.max(2, s2 * 0.16), fill: C2.meterFill, stroke: ink, lineWidth: 1.4 } }));
29326
+ group.appendChild(new Rect({ style: { x: -s2 / 2 + 2, y: -s2 / 2 + 2, width: s2 - 4, height: 2, radius: 1, fill: withAlpha(ink, 0.35), stroke: "none" } }));
29327
+ text(attributes, group, "JX", 0, s2 * 0.06, Math.max(8, s2 * 0.4), ink, 700);
28265
29328
  }
28266
29329
  }
28267
29330
  function drawConsumer(attributes, group) {
28268
- group.appendChild(new Circle({ style: { cx: 0, cy: 0, r: 12.5, fill: "none", stroke: C2.white, lineWidth: 1.8 } }));
28269
- text(attributes, group, "J", 0, 0.5, 13, C2.white);
29331
+ const ink = nodeColor(attributes) || C2.white;
29332
+ group.appendChild(new Circle({ style: { cx: 0, cy: 0, r: 13, fill: "none", stroke: ink, lineWidth: LW_HI } }));
29333
+ text(attributes, group, "J", 0, 0.8, 14.5, ink, 700);
28270
29334
  }
28271
29335
  function drawBus(attributes, group) {
28272
29336
  const [w] = attributes.size;
28273
- group.appendChild(new Rect({ style: { x: -w / 2, y: -3, width: w, height: 6, radius: 2, fill: C2.busFill, stroke: C2.white, lineWidth: 1.4 } }));
29337
+ const ink = nodeColor(attributes) || C2.white;
29338
+ const hh = Math.max(4.5, Math.min(6, w / 10));
29339
+ group.appendChild(new Rect({ style: { x: -w / 2, y: -hh / 2, width: w, height: hh, radius: hh / 2, fill: withAlpha(ink, 0.5), stroke: ink, lineWidth: 1.2 } }));
29340
+ group.appendChild(new Rect({ style: { x: -w / 2 + 1, y: -0.7, width: w - 2, height: 1.4, fill: withAlpha(C2.canvasBg || "#000", 0.18), stroke: "none" } }));
28274
29341
  }
28275
29342
  function drawLineDot(attributes, group) {
28276
- group.appendChild(new Circle({ style: { cx: 0, cy: 0, r: 4, fill: C2.dotFill, stroke: C2.cyan, lineWidth: 1.4 } }));
29343
+ const P = localPalette(attributes);
29344
+ group.appendChild(new Circle({ style: { cx: 0, cy: 0, r: 4.6, fill: P.dotFill, stroke: P.cyan, lineWidth: 1.6 } }));
29345
+ group.appendChild(new Circle({ style: { cx: 0, cy: 0, r: 1.5, fill: P.cyan, stroke: "none" } }));
28277
29346
  }
28278
29347
  function drawJunction(attributes, group) {
28279
- const lw = 1.4;
28280
- group.appendChild(new Line({ style: { x1: -5, y1: 0, x2: 5, y2: 0, stroke: C2.red, lineWidth: lw } }));
28281
- group.appendChild(new Line({ style: { x1: 0, y1: -5, x2: 0, y2: 5, stroke: C2.red, lineWidth: lw } }));
28282
- group.appendChild(new Line({ style: { x1: -3.5, y1: -3.5, x2: 3.5, y2: 3.5, stroke: C2.red, lineWidth: lw } }));
28283
- group.appendChild(new Line({ style: { x1: 3.5, y1: -3.5, x2: -3.5, y2: 3.5, stroke: C2.red, lineWidth: lw } }));
29348
+ const P = localPalette(attributes);
29349
+ const lw = LW;
29350
+ group.appendChild(new Line({ style: { x1: -5.5, y1: 0, x2: 5.5, y2: 0, stroke: P.red, lineWidth: lw, lineCap: "round" } }));
29351
+ group.appendChild(new Line({ style: { x1: 0, y1: -5.5, x2: 0, y2: 5.5, stroke: P.red, lineWidth: lw, lineCap: "round" } }));
29352
+ group.appendChild(new Line({ style: { x1: -3.9, y1: -3.9, x2: 3.9, y2: 3.9, stroke: P.red, lineWidth: lw, lineCap: "round" } }));
29353
+ group.appendChild(new Line({ style: { x1: 3.9, y1: -3.9, x2: -3.9, y2: 3.9, stroke: P.red, lineWidth: lw, lineCap: "round" } }));
28284
29354
  }
28285
29355
  function drawUnknown(attributes, group) {
28286
- group.appendChild(new Circle({ style: { cx: 0, cy: 0, r: 8, fill: C2.unknownFill, stroke: C2.unknownStroke, lineWidth: 1.2 } }));
29356
+ group.appendChild(new Circle({ style: { cx: 0, cy: 0, r: 9, fill: C2.unknownFill, stroke: C2.unknownStroke, lineWidth: 1.4 } }));
29357
+ text(attributes, group, "?", 0, 0.8, 13, C2.unknownStroke, 700);
28287
29358
  }
28288
29359
  function drawStation(attributes, group) {
28289
29360
  const [w, h] = attributes.size;
@@ -28294,16 +29365,30 @@ function drawStation(attributes, group) {
28294
29365
  y: -h / 2,
28295
29366
  width: w,
28296
29367
  height: h,
28297
- radius: 2,
29368
+ radius: 5,
28298
29369
  fill: C2.stationFill,
28299
29370
  stroke: C2.white,
28300
- lineWidth: 1.2,
29371
+ lineWidth: 1.5,
29372
+ opacity: 0.95
29373
+ }
29374
+ })
29375
+ );
29376
+ group.appendChild(
29377
+ new Rect({
29378
+ style: {
29379
+ x: -w / 2 + 2,
29380
+ y: -h / 2 + 2,
29381
+ width: w - 4,
29382
+ height: 2.4,
29383
+ radius: 1.2,
29384
+ fill: withAlpha(C2.white, 0.28),
29385
+ stroke: "none",
28301
29386
  opacity: 0.9
28302
29387
  }
28303
29388
  })
28304
29389
  );
28305
29390
  if (attributes._stationTitle) {
28306
- _drawWrappedText(group, attributes._stationTitle, 0, -h / 2 + 13, 11, C2.stationText, w - 12, 1.35);
29391
+ _drawWrappedText(group, attributes._stationTitle, 0, -h / 2 + 15, 12, C2.stationText, w - 12, 1.35, 600);
28307
29392
  }
28308
29393
  }
28309
29394
  const DRAWERS = {
@@ -28320,8 +29405,36 @@ const DRAWERS = {
28320
29405
  station: drawStation,
28321
29406
  other: drawUnknown
28322
29407
  };
28323
- const LABEL_UNDER = /* @__PURE__ */ new Set(["transformer", "fuse"]);
28324
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
+ }
28325
29438
  class SvgSymbolNode extends BaseNode {
28326
29439
  drawKeyShape(attributes, container) {
28327
29440
  const [w, h] = attributes.size;
@@ -28352,9 +29465,12 @@ class SvgSymbolNode extends BaseNode {
28352
29465
  if (rot) key.appendChild(target);
28353
29466
  const draw = DRAWERS[attributes.cat] || drawUnknown;
28354
29467
  draw(attributes, target);
29468
+ drawGlow(attributes, target);
28355
29469
  drawHighlight(attributes, target);
28356
29470
  drawAnnotation(attributes, target);
28357
- if (LABEL_UNDER.has(attributes.cat)) drawLabelUnder(attributes, target);
29471
+ if (attributes.labelText && !attributes._stationTitle && attributes.cat !== "bus" && attributes.cat !== "cableTerminal" && attributes.cat !== "consumer" && attributes.cat !== "switch" && attributes.cat !== "meterBox") {
29472
+ drawLabelUnder(attributes, target);
29473
+ }
28358
29474
  const pulse = _pulseState.get(attributes._nodeId || "");
28359
29475
  if (pulse) {
28360
29476
  const items = rot ? [target] : key.children || [];
@@ -28541,6 +29657,32 @@ const _sfc_main$2 = {
28541
29657
  emit("node-select", { node, upstream, downstream });
28542
29658
  if (api) api._emit("node:click", { nodeId: id2, node });
28543
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
+ }
28544
29686
  function routeNodeTap(id2) {
28545
29687
  if (!graph || id2 == null) return;
28546
29688
  const customers = customersOf(id2);
@@ -28853,9 +29995,9 @@ const _sfc_main$2 = {
28853
29995
  }, {}),
28854
29996
  // 保留原有快捷方法
28855
29997
  resetView,
28856
- selectNode: selectNodeWithInfo,
28857
- /** 选中计量箱内第 index 个用户(多用户箱,等价于画布点击该用户) */
28858
- selectCustomer: (nodeId, index) => selectCustomerWithInfo(nodeId, index),
29998
+ selectNode: selectNodeRef,
29999
+ /** 选中计量箱内第 index 个用户(nodeRef=箱节点id/psrId;传 assetNo 时自动定位该户) */
30000
+ selectCustomer: selectCustomerRef,
28859
30001
  /** 当前选中的“箱内用户”信息({ nodeId, index, customer, node } | null) */
28860
30002
  getSelectedCustomer,
28861
30003
  exportPng,
@@ -28894,7 +30036,7 @@ const _sfc_main$2 = {
28894
30036
  };
28895
30037
  }
28896
30038
  };
28897
- 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"]]);
28898
30040
  const _hoisted_1$1 = ["data-theme"];
28899
30041
  const _hoisted_2$1 = { class: "panel-head" };
28900
30042
  const _hoisted_3$1 = {
@@ -29267,7 +30409,7 @@ const _sfc_main = {
29267
30409
  }
29268
30410
  };
29269
30411
  const DistanceTop10 = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-438b6017"]]);
29270
- const VERSION = "0.1.7";
30412
+ const VERSION = "0.1.9";
29271
30413
  const DESCRIPTION = "配电台区单线图拓扑成图引擎";
29272
30414
  export {
29273
30415
  DESCRIPTION,