topo-engine 0.1.4 → 0.1.6

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.
@@ -52,6 +52,13 @@ class TopoApi {
52
52
  _assertReady() {
53
53
  if (!this._graph) throw new Error("[TopoApi] 图实例未就绪,请等待渲染完成");
54
54
  }
55
+ /** 画布中心兼容取值:G6 v5 getCanvasCenter 返回数组 [x,y,z],历史代码按 {x,y} 对象使用 */
56
+ _canvasCenterXY() {
57
+ const c = this._graph.getCanvasCenter();
58
+ if (Array.isArray(c)) return { x: c[0], y: c[1] };
59
+ if (c && typeof c === "object") return { x: c.x || 0, y: c.y || 0 };
60
+ return { x: 0, y: 0 };
61
+ }
55
62
  /** 安全合并 style:保留现有字段,只覆盖传入字段 */
56
63
  _safeNodeStyle(nodeId, style) {
57
64
  try {
@@ -371,7 +378,7 @@ class TopoApi {
371
378
  try {
372
379
  this._graph.focusElement(nodeId);
373
380
  } catch {
374
- const center = this._graph.getCanvasCenter();
381
+ const center = this._canvasCenterXY();
375
382
  this._graph.translateTo({ x: center.x - p.x * (zoom || this._graph.getZoom()), y: center.y - p.y * (zoom || this._graph.getZoom()) });
376
383
  }
377
384
  }
@@ -379,7 +386,7 @@ class TopoApi {
379
386
  getViewport() {
380
387
  this._assertReady();
381
388
  const zoom = this._graph.getZoom();
382
- const center = this._graph.getCanvasCenter();
389
+ const center = this._canvasCenterXY();
383
390
  return { zoom, center };
384
391
  }
385
392
  // ============================================================
@@ -752,11 +759,25 @@ class TopoApi {
752
759
  if (!container) return;
753
760
  const pos = this._layout.pos.get(nodeId);
754
761
  if (!pos) return;
755
- const zoom = this._graph.getZoom();
756
- const canvasCenter = this._graph.getCanvasCenter();
757
- const containerRect = container.getBoundingClientRect();
758
- const screenX = containerRect.left + canvasCenter.x + pos.x * zoom;
759
- const screenY = containerRect.top + canvasCenter.y + pos.y * zoom;
762
+ let client = null;
763
+ try {
764
+ if (typeof this._graph.getClientByCanvas === "function") {
765
+ const r = this._graph.getClientByCanvas([pos.x, pos.y]);
766
+ client = Array.isArray(r) ? { x: r[0], y: r[1] } : r;
767
+ }
768
+ } catch (e2) {
769
+ }
770
+ let screenX, screenY;
771
+ if (client && isFinite(client.x)) {
772
+ screenX = client.x;
773
+ screenY = client.y;
774
+ } else {
775
+ const zoom = this._graph.getZoom();
776
+ const canvasCenter = this._canvasCenterXY();
777
+ const containerRect = container.getBoundingClientRect();
778
+ screenX = containerRect.left + canvasCenter.x + pos.x * zoom;
779
+ screenY = containerRect.top + canvasCenter.y + pos.y * zoom;
780
+ }
760
781
  const el = document.createElement("div");
761
782
  el.className = "topo-card";
762
783
  el.dataset.nodeId = nodeId;
@@ -1214,6 +1235,53 @@ const SYMBOL_SPEC = {
1214
1235
  function getSymbolSpec(cat) {
1215
1236
  return SYMBOL_SPEC[cat] || SYMBOL_SPEC.other;
1216
1237
  }
1238
+ function buildCustomerMap(customerData) {
1239
+ const map = /* @__PURE__ */ new Map();
1240
+ if (!Array.isArray(customerData)) return map;
1241
+ for (const entry of customerData) {
1242
+ if (entry && entry.psrId && Array.isArray(entry.consumerList)) {
1243
+ map.set(String(entry.psrId), entry.consumerList);
1244
+ }
1245
+ }
1246
+ return map;
1247
+ }
1248
+ const METER_CELL_W = 26;
1249
+ const METER_CELL_H = 38;
1250
+ const METER_GAP = 4;
1251
+ const METER_PAD = 6;
1252
+ const MAX_COLS = 5;
1253
+ const SHOW_NAME_LIMIT = 20;
1254
+ function injectCustomers(model, customerMap) {
1255
+ for (const node of model.nodes) {
1256
+ if (node.cat !== "meterBox") continue;
1257
+ const customers = customerMap.get(node.psrId);
1258
+ if (!customers || customers.length === 0) continue;
1259
+ node._customers = customers;
1260
+ if (customers.length === 1) {
1261
+ node._meterBoxMode = "single";
1262
+ node._meterBoxW = 30;
1263
+ node._meterBoxH = 38;
1264
+ } else {
1265
+ const showName = customers.length <= SHOW_NAME_LIMIT;
1266
+ const cols = Math.min(customers.length, MAX_COLS);
1267
+ const rows = Math.ceil(customers.length / cols);
1268
+ const cellW = METER_CELL_W;
1269
+ const cellH = showName ? METER_CELL_H : METER_CELL_W;
1270
+ node._meterBoxMode = "grid";
1271
+ node._meterBoxCols = cols;
1272
+ node._meterBoxRows = rows;
1273
+ node._meterBoxShowName = showName;
1274
+ node._meterBoxW = METER_PAD * 2 + cols * cellW + (cols - 1) * METER_GAP;
1275
+ node._meterBoxH = METER_PAD * 2 + rows * cellH + (rows - 1) * METER_GAP;
1276
+ }
1277
+ }
1278
+ }
1279
+ function getMeterBoxSize(node) {
1280
+ if (node._meterBoxW != null && node._meterBoxH != null) {
1281
+ return { w: node._meterBoxW, h: node._meterBoxH };
1282
+ }
1283
+ return null;
1284
+ }
1217
1285
  function stationPlans(model) {
1218
1286
  const nodesById = new Map(model.nodes.map((n) => [n.id, n]));
1219
1287
  const plans = [];
@@ -1265,6 +1333,10 @@ function stationInner(plan, model, band = 50, nodeMap) {
1265
1333
  const nmap = nodeMap || new Map(model.nodes.map((n) => [n.id, n]));
1266
1334
  const specOf = (id2) => {
1267
1335
  const n = nmap.get(id2);
1336
+ if (n && n.cat === "meterBox") {
1337
+ const dim = getMeterBoxSize(n);
1338
+ if (dim) return dim;
1339
+ }
1268
1340
  const s2 = getSymbolSpec(n ? n.cat : "other");
1269
1341
  return { w: s2.w, h: s2.h };
1270
1342
  };
@@ -1922,6 +1994,10 @@ function computeOrthogonalLayout(model, opts = {}) {
1922
1994
  const sz = (id2) => {
1923
1995
  const n = extNodeMap.get(id2);
1924
1996
  if (n && n._size) return n._size;
1997
+ if (n && n.cat === "meterBox") {
1998
+ const dim = getMeterBoxSize(n);
1999
+ if (dim) return dim;
2000
+ }
1925
2001
  const spec = getSymbolSpec(n ? n.cat : "other");
1926
2002
  return { w: spec.w, h: spec.h };
1927
2003
  };
@@ -2520,22 +2596,30 @@ function buildGraphData(model, layout) {
2520
2596
  const nodes = model.nodes.map((n) => {
2521
2597
  const spec = getSymbolSpec(n.cat);
2522
2598
  const busW = layout.stationBusWidth && layout.stationBusWidth.get(n.id);
2523
- const w = busW || spec.w;
2599
+ const meterBoxDim = n.cat === "meterBox" ? getMeterBoxSize(n) : null;
2600
+ const w = busW || (meterBoxDim ? meterBoxDim.w : spec.w);
2601
+ const h = meterBoxDim ? meterBoxDim.h : spec.h;
2524
2602
  const p = layout.pos.get(n.id) || { x: 0, y: 0 };
2525
2603
  return {
2526
2604
  id: n.id,
2527
- data: { ...n, width: w, height: spec.h },
2605
+ data: { ...n, width: w, height: h },
2528
2606
  style: {
2529
2607
  x: p.x,
2530
2608
  y: p.y,
2531
- size: [w, spec.h],
2609
+ size: [w, h],
2532
2610
  // 自定义字段(进 attributes,供 svg-symbol 节点绘制使用)
2533
2611
  cat: n.cat,
2534
- labelText: n.cat === "bus" || n.cat === "cableTerminal" ? "" : n.shortName || "",
2612
+ labelText: n.cat === "bus" || n.cat === "cableTerminal" || n.cat === "consumer" ? "" : n.shortName || "",
2535
2613
  rotate: layout.stationRotate && layout.stationRotate.get(n.id) || 0,
2536
2614
  highlight: 0,
2537
2615
  cursor: "pointer",
2538
- _nodeId: n.id
2616
+ _nodeId: n.id,
2617
+ // 计量箱客户数据
2618
+ _meterBoxMode: n._meterBoxMode || null,
2619
+ _meterBoxCols: n._meterBoxCols || 0,
2620
+ _meterBoxRows: n._meterBoxRows || 0,
2621
+ _meterBoxShowName: n._meterBoxShowName || false,
2622
+ _customers: n._customers || null
2539
2623
  }
2540
2624
  };
2541
2625
  });
@@ -27787,8 +27871,45 @@ function drawStationTerminal(attributes, group) {
27787
27871
  group.appendChild(new Polygon({ style: { points: [[0, -5], [0, 5], [9, 0]], fill: "none", stroke: C2.red, lineWidth: 1.4 } }));
27788
27872
  }
27789
27873
  function drawMeterBox(attributes, group) {
27790
- group.appendChild(new Rect({ style: { x: -9.5, y: -9.5, width: 19, height: 19, fill: C2.meterFill, stroke: C2.meterText, lineWidth: 0.9 } }));
27791
- text(attributes, group, "JX", 0, 0.5, 7.5, C2.meterText);
27874
+ const [w, h] = attributes.size;
27875
+ if (attributes._meterBoxMode === "single") {
27876
+ group.appendChild(new Circle({ style: { cx: 0, cy: -3, r: 11, fill: "none", stroke: C2.white, lineWidth: 1.6 } }));
27877
+ group.appendChild(new Line({ style: { x1: 0, y1: -14, x2: 0, y2: -7, stroke: C2.white, lineWidth: 1.6 } }));
27878
+ const name = attributes._customers && attributes._customers[0] ? attributes._customers[0].realConsName || attributes._customers[0].consName || "" : "";
27879
+ if (name) {
27880
+ group.appendChild(new Text({ style: { text: name, x: 0, y: 12, fontSize: 8, fill: C2.white, fontFamily: FONT, textAlign: "center", textBaseline: "middle" } }));
27881
+ }
27882
+ } else if (attributes._meterBoxMode === "grid" && attributes._customers) {
27883
+ 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 } }));
27884
+ const cols = attributes._meterBoxCols || 5;
27885
+ const customers = attributes._customers;
27886
+ const showName = attributes._meterBoxShowName;
27887
+ const cellW = METER_CELL_W;
27888
+ const cellH = showName ? METER_CELL_H : METER_CELL_W;
27889
+ const gridW = cols * cellW + (cols - 1) * METER_GAP;
27890
+ const rows = attributes._meterBoxRows || Math.ceil(customers.length / cols);
27891
+ const gridH = rows * cellH + (rows - 1) * METER_GAP;
27892
+ const startX = -gridW / 2;
27893
+ const startY = -gridH / 2;
27894
+ const meterR = 9;
27895
+ for (let i = 0; i < customers.length; i++) {
27896
+ const row2 = Math.floor(i / cols);
27897
+ const col = i % cols;
27898
+ const cx = startX + col * (cellW + METER_GAP) + cellW / 2;
27899
+ const cy = startY + row2 * (cellH + METER_GAP) + cellW / 2;
27900
+ group.appendChild(new Circle({ style: { cx, cy, r: meterR, fill: "none", stroke: C2.meterText, lineWidth: 0.8 } }));
27901
+ group.appendChild(new Line({ style: { x1: cx, y1: cy - meterR, x2: cx, y2: cy - meterR + 4, stroke: C2.meterText, lineWidth: 0.8 } }));
27902
+ if (showName) {
27903
+ const name = customers[i].realConsName || customers[i].consName || "";
27904
+ if (name) {
27905
+ group.appendChild(new Text({ style: { text: name, x: cx, y: cy + meterR + 6, fontSize: 7, fill: C2.meterText, fontFamily: FONT, textAlign: "center", textBaseline: "middle" } }));
27906
+ }
27907
+ }
27908
+ }
27909
+ } else {
27910
+ group.appendChild(new Rect({ style: { x: -9.5, y: -9.5, width: 19, height: 19, fill: C2.meterFill, stroke: C2.meterText, lineWidth: 0.9 } }));
27911
+ text(attributes, group, "JX", 0, 0.5, 7.5, C2.meterText);
27912
+ }
27792
27913
  }
27793
27914
  function drawConsumer(attributes, group) {
27794
27915
  group.appendChild(new Circle({ style: { cx: 0, cy: 0, r: 12.5, fill: "none", stroke: C2.white, lineWidth: 1.8 } }));
@@ -27914,8 +28035,10 @@ const _sfc_main$2 = {
27914
28035
  props: {
27915
28036
  dataUrl: { type: String, default: "" },
27916
28037
  data: { type: Object, default: null },
28038
+ /** 客户数据(计量箱关联的用户列表),直接传入 JSON 数组 */
28039
+ customerData: { type: Array, default: null },
27917
28040
  /** 主题:'dark'(默认,黑底 SCADA 风)| 'light'(白底);支持 v-model:theme 双向绑定 */
27918
- theme: { type: String, default: "dark" }
28041
+ theme: { type: String, default: "light" }
27919
28042
  },
27920
28043
  emits: ["node-select", "loaded", "top-distances", "update:theme"],
27921
28044
  setup(__props, { expose: __expose, emit: __emit }) {
@@ -28031,6 +28154,10 @@ const _sfc_main$2 = {
28031
28154
  lastRaw.value = raw;
28032
28155
  const theme = applyTheme();
28033
28156
  model = parseTopo(raw);
28157
+ if (props.customerData && props.customerData.length) {
28158
+ const customerMap = buildCustomerMap(props.customerData);
28159
+ injectCustomers(model, customerMap);
28160
+ }
28034
28161
  nodeByIdMap = new Map(model.nodes.map((n) => [n.id, n]));
28035
28162
  title.value = String(model.title || "").replace(/\.+$/, "");
28036
28163
  stats.value = model.stats;
@@ -28082,6 +28209,34 @@ const _sfc_main$2 = {
28082
28209
  const id2 = evt.target.id;
28083
28210
  selectNodeWithInfo(id2);
28084
28211
  });
28212
+ function hitTestNodeAtCanvas(evt) {
28213
+ const c = evt && (evt.canvas || evt.viewport) || {};
28214
+ const cx = c.x, cy = c.y;
28215
+ if (cx == null || cy == null) return null;
28216
+ let best = null, bestD = Infinity;
28217
+ let all = [];
28218
+ try {
28219
+ all = graph.getNodeData();
28220
+ } catch {
28221
+ return null;
28222
+ }
28223
+ for (const nd of all || []) {
28224
+ if (nd && nd.data && nd.data.isStationFrame) continue;
28225
+ const st = nd && nd.style;
28226
+ if (!st || st.x == null || st.y == null) continue;
28227
+ const size = st.size || [0, 0];
28228
+ const w = size[0] || 0, h = size[1] || 0;
28229
+ if (w <= 0 || h <= 0) continue;
28230
+ if (cx >= st.x - w / 2 && cx <= st.x + w / 2 && cy >= st.y - h / 2 && cy <= st.y + h / 2) {
28231
+ const d2 = Math.abs(cx - st.x) + Math.abs(cy - st.y);
28232
+ if (d2 < bestD) {
28233
+ bestD = d2;
28234
+ best = nd.id;
28235
+ }
28236
+ }
28237
+ }
28238
+ return best;
28239
+ }
28085
28240
  graph.on("node:pointerenter", (evt) => {
28086
28241
  const id2 = evt.target.id;
28087
28242
  const data3 = graph.getNodeData(id2);
@@ -28104,6 +28259,11 @@ const _sfc_main$2 = {
28104
28259
  api._emit("edge:click", { edgeId: id2, source: src, target: tgt });
28105
28260
  });
28106
28261
  graph.on("canvas:click", (evt) => {
28262
+ const hitId = hitTestNodeAtCanvas(evt);
28263
+ if (hitId) {
28264
+ selectNodeWithInfo(hitId);
28265
+ return;
28266
+ }
28107
28267
  if (selectedId) {
28108
28268
  const data3 = graph.getNodeData(selectedId);
28109
28269
  const existing = data3 && data3.style || {};
@@ -28116,7 +28276,8 @@ const _sfc_main$2 = {
28116
28276
  });
28117
28277
  graph.on("viewportchange", (evt) => {
28118
28278
  const zoom = graph.getZoom();
28119
- const center = graph.getCanvasCenter();
28279
+ const raw2 = graph.getCanvasCenter();
28280
+ const center = Array.isArray(raw2) ? { x: raw2[0], y: raw2[1] } : raw2;
28120
28281
  api._emit("viewport:change", { zoom, center });
28121
28282
  });
28122
28283
  graph.render().then(() => {
@@ -28162,6 +28323,13 @@ const _sfc_main$2 = {
28162
28323
  if (lastRaw.value) render(lastRaw.value);
28163
28324
  }
28164
28325
  );
28326
+ watch(
28327
+ () => props.customerData,
28328
+ () => {
28329
+ if (lastRaw.value) render(lastRaw.value);
28330
+ },
28331
+ { deep: true }
28332
+ );
28165
28333
  onBeforeUnmount(() => {
28166
28334
  if (api) {
28167
28335
  api.destroy();
@@ -28221,7 +28389,7 @@ const _sfc_main$2 = {
28221
28389
  };
28222
28390
  }
28223
28391
  };
28224
- const TopoGraph = /* @__PURE__ */ _export_sfc(_sfc_main$2, [["__scopeId", "data-v-562b2247"]]);
28392
+ const TopoGraph = /* @__PURE__ */ _export_sfc(_sfc_main$2, [["__scopeId", "data-v-b02bff98"]]);
28225
28393
  const _hoisted_1$1 = ["data-theme"];
28226
28394
  const _hoisted_2$1 = { class: "panel-head" };
28227
28395
  const _hoisted_3$1 = {
@@ -28531,7 +28699,7 @@ const _sfc_main = {
28531
28699
  }
28532
28700
  };
28533
28701
  const DistanceTop10 = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-438b6017"]]);
28534
- const VERSION = "0.1.4";
28702
+ const VERSION = "0.1.6";
28535
28703
  const DESCRIPTION = "配电台区单线图拓扑成图引擎";
28536
28704
  export {
28537
28705
  DESCRIPTION,
@@ -28543,11 +28711,14 @@ export {
28543
28711
  TopoGraph,
28544
28712
  VERSION,
28545
28713
  assertOrthogonal,
28714
+ buildCustomerMap,
28546
28715
  buildGraphData,
28547
28716
  calculateTopDistances,
28548
28717
  computeOrthogonalLayout,
28549
28718
  edgeStyle,
28719
+ getMeterBoxSize,
28550
28720
  getSymbolSpec,
28721
+ injectCustomers,
28551
28722
  parseTopo
28552
28723
  };
28553
28724
  //# sourceMappingURL=topo-engine.js.map