topo-engine 0.1.10 → 0.1.12

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.
@@ -27321,12 +27321,24 @@ class TopoApi {
27321
27321
  }
27322
27322
  const TYPE_MAP = {
27323
27323
  "0302": { cat: "transformer", label: "配电变压器", order: 1 },
27324
+ "0110": { cat: "transformer", label: "配电变压器", order: 1 },
27325
+ // 柱上变压器(PMS 另一套类型码,与 0302 共用图元)
27324
27326
  "3301": { cat: "switch", label: "开关", order: 2 },
27327
+ "321500000": { cat: "switch", label: "开关", order: 2 },
27328
+ // 开关/断路器(另一套类型码,与 3301 共用图元)
27325
27329
  "3303": { cat: "bus", label: "低压母线", order: 3 },
27326
27330
  "3202": { cat: "cableTerminal", label: "电缆终端头", order: 4 },
27327
27331
  "310100000": { cat: "line", label: "低压导线", order: 5 },
27332
+ "3201": { cat: "line", label: "低压导线", order: 5 },
27333
+ // 低压导线(另一套类型码,与 310100000 共用图元)
27334
+ "3102": { cat: "line", label: "低压导线", order: 5 },
27335
+ // 低压电杆/导线点(同上,共用低压导线图元)
27336
+ "3101": { cat: "line", label: "低压导线", order: 5 },
27337
+ // 低压导线分段(同上,共用低压导线图元)
27328
27338
  "320300000": { cat: "junction", label: "密母分接点", order: 4 },
27329
27339
  "3112": { cat: "meterBox", label: "计量箱", order: 6 },
27340
+ "31120003": { cat: "meterBox", label: "计量箱", order: 6 },
27341
+ // 计量箱(另一套类型码,与 3112 共用图元)
27330
27342
  "3218000": { cat: "consumer", label: "用户接入点", order: 7 },
27331
27343
  "32500000": { cat: "station", label: "低压配电箱", order: 0 }
27332
27344
  };
@@ -27359,7 +27371,7 @@ function parseTopo(raw) {
27359
27371
  }
27360
27372
  }
27361
27373
  let roots = [...nodes].filter((n) => (indeg.get(n.id) || 0) === 0);
27362
- const prio = (n) => n.psrType === "0302" ? 0 : n.psrType === "3301" ? 1 : 2;
27374
+ const prio = (n) => n.psrType === "0302" || n.psrType === "0110" ? 0 : n.psrType === "3301" || n.psrType === "321500000" ? 1 : 2;
27363
27375
  roots.sort((a2, b) => prio(a2) - prio(b) || a2.id - b.id);
27364
27376
  const rootId = roots.length ? roots[0].id : nodes[0].id;
27365
27377
  const children = /* @__PURE__ */ new Map();
@@ -27378,19 +27390,20 @@ function parseTopo(raw) {
27378
27390
  }
27379
27391
  }
27380
27392
  const shortName = (n) => {
27381
- if (n.psrType === "3301") {
27393
+ if (n.psrType === "3301" || n.psrType === "321500000") {
27382
27394
  const no = extractSwitchNo(n.name);
27383
27395
  if (no) return no;
27396
+ if (/母联/.test(n.name || "")) return "母联";
27384
27397
  return /熔丝/.test(n.name || "") ? "熔丝" : "开关";
27385
27398
  }
27386
- if (n.psrType === "310100000") return extractLineShort(n.name);
27399
+ if (n.psrType === "310100000" || n.psrType === "3201" || n.psrType === "3102" || n.psrType === "3101") return extractLineShort(n.name);
27387
27400
  if (n.psrType === "3218000") {
27388
27401
  const m = String(n.name || "").match(/([\dA-Za-z#\-]+用户接入点)/);
27389
27402
  return m ? m[1].replace("用户接入点", "") : "用户";
27390
27403
  }
27391
27404
  if (n.psrType === "3303") return "母线";
27392
- if (n.psrType === "0302") return "柱上变";
27393
- if (n.psrType === "3112") return "";
27405
+ if (n.psrType === "0302" || n.psrType === "0110") return "柱上变";
27406
+ if (n.psrType === "3112" || n.psrType === "31120003") return "";
27394
27407
  if (n.psrType === "320300000") {
27395
27408
  const m = String(n.name || "").match(/(\d+栋\d+单元\d+层)/);
27396
27409
  return m ? m[1] : "分接点";
@@ -27409,9 +27422,9 @@ function parseTopo(raw) {
27409
27422
  cat: type.cat,
27410
27423
  catLabel: type.label,
27411
27424
  shortName: shortName(n),
27412
- isLine: n.psrType === "310100000",
27425
+ isLine: n.psrType === "310100000" || n.psrType === "3201" || n.psrType === "3102" || n.psrType === "3101",
27413
27426
  insideStation: stationPsrIds.has(n.psrId),
27414
- isFuse: n.psrType === "3301" && /熔丝/.test(nm),
27427
+ isFuse: (n.psrType === "3301" || n.psrType === "321500000") && /熔丝/.test(nm),
27415
27428
  status: n.status
27416
27429
  };
27417
27430
  });
@@ -27481,10 +27494,11 @@ function stationPlans(model) {
27481
27494
  const containSet = new Set(st.contain);
27482
27495
  const parents = model.edges.filter((e2) => containSet.has(e2.target) && !containSet.has(e2.source));
27483
27496
  const outEdges = model.edges.filter((e2) => containSet.has(e2.source) && !containSet.has(e2.target));
27484
- const busNode = st.contain.find((id2) => {
27497
+ const busList = st.contain.filter((id2) => {
27485
27498
  const n = nodesById.get(id2);
27486
27499
  return n && n.cat === "bus";
27487
- }) || null;
27500
+ });
27501
+ let busNode = busList.length ? busList[0] : null;
27488
27502
  const hasParent = new Set(model.edges.map((e2) => e2.target));
27489
27503
  const innerRoot = st.contain.map((c) => nodesById.get(c)).filter(Boolean).find((n) => !hasParent.has(n.id) && n.cat !== "bus");
27490
27504
  let inNode = parents.length ? parents[0].target : null;
@@ -27500,9 +27514,78 @@ function stationPlans(model) {
27500
27514
  return n && n.cat !== "bus" && !outNodes.has(k);
27501
27515
  }) || null;
27502
27516
  }
27517
+ const multiBusStation = busList.length > 1;
27518
+ let busNodes = busList;
27519
+ let ties = [];
27520
+ let busOuts = /* @__PURE__ */ new Map();
27521
+ let busLeaves = /* @__PURE__ */ new Map();
27522
+ let primaryBus = busNode;
27523
+ if (inNode) {
27524
+ const feed = model.edges.find(
27525
+ (e2) => e2.source === inNode && containSet.has(e2.target) && (nodesById.get(e2.target) || {}).cat === "bus"
27526
+ );
27527
+ if (feed) primaryBus = feed.target;
27528
+ }
27529
+ if (multiBusStation) busNode = primaryBus;
27530
+ if (multiBusStation) {
27531
+ const busSet = new Set(busList);
27532
+ const catOf = (id2) => (nodesById.get(id2) || {}).cat;
27533
+ for (const id2 of st.contain) {
27534
+ const n = nodesById.get(id2);
27535
+ if (!n || n.cat === "bus") continue;
27536
+ const fromBus = model.edges.find(
27537
+ (e2) => e2.target === id2 && containSet.has(e2.source) && catOf(e2.source) === "bus"
27538
+ );
27539
+ const toBus = model.edges.find(
27540
+ (e2) => e2.source === id2 && containSet.has(e2.target) && catOf(e2.target) === "bus"
27541
+ );
27542
+ if (fromBus && toBus) ties.push({ id: id2, from: fromBus.source, to: toBus.target });
27543
+ }
27544
+ const tieIds = new Set(ties.map((t) => t.id));
27545
+ const busOfNode = (id2) => {
27546
+ const pe = model.edges.find((e2) => e2.target === id2 && busSet.has(e2.source));
27547
+ return pe ? pe.source : busNode;
27548
+ };
27549
+ for (const b of busList) {
27550
+ busOuts.set(b, []);
27551
+ busLeaves.set(b, []);
27552
+ }
27553
+ for (const e2 of outEdges) {
27554
+ const b = busOfNode(e2.source);
27555
+ if (!busOuts.has(b)) busOuts.set(b, []);
27556
+ busOuts.get(b).push({ node: e2.source, child: e2.target });
27557
+ }
27558
+ for (const id2 of st.contain) {
27559
+ const n = nodesById.get(id2);
27560
+ if (!n || n.cat === "bus") continue;
27561
+ if (id2 === inNode || innerRoot && id2 === innerRoot.id || tieIds.has(id2)) continue;
27562
+ if (outEdges.some((e2) => e2.source === id2)) continue;
27563
+ const b = busOfNode(id2);
27564
+ if (!busLeaves.has(b)) busLeaves.set(b, []);
27565
+ busLeaves.get(b).push(id2);
27566
+ }
27567
+ const order = busNode ? [busNode] : [];
27568
+ const seen = new Set(order);
27569
+ for (let i = 0; i < order.length; i++) {
27570
+ for (const t of ties) {
27571
+ if (t.from === order[i] && !seen.has(t.to)) {
27572
+ seen.add(t.to);
27573
+ order.push(t.to);
27574
+ } else if (t.to === order[i] && !seen.has(t.from)) {
27575
+ seen.add(t.from);
27576
+ order.push(t.from);
27577
+ }
27578
+ }
27579
+ }
27580
+ for (const b of busList) if (!seen.has(b)) {
27581
+ seen.add(b);
27582
+ order.push(b);
27583
+ }
27584
+ busNodes = order;
27585
+ }
27503
27586
  const outSources = new Set(outEdges.map((e2) => e2.source));
27504
27587
  const drawn = new Set([busNode, inNode, innerRoot && innerRoot.id, ...outSources].filter(Boolean));
27505
- const leafOuts = st.contain.filter((id2) => !drawn.has(id2)).sort((a2, b) => {
27588
+ const leafOuts = multiBusStation ? [] : st.contain.filter((id2) => !drawn.has(id2)).sort((a2, b) => {
27506
27589
  const sa = String((nodesById.get(a2) || {}).shortName || a2);
27507
27590
  const sb = String((nodesById.get(b) || {}).shortName || b);
27508
27591
  return sa.localeCompare(sb, "zh", { numeric: true });
@@ -27515,6 +27598,11 @@ function stationPlans(model) {
27515
27598
  inNode,
27516
27599
  innerRoot: innerRoot ? innerRoot.id : null,
27517
27600
  busNode,
27601
+ busNodes,
27602
+ multiBus: multiBusStation,
27603
+ ties,
27604
+ busOuts,
27605
+ busLeaves,
27518
27606
  leafOuts,
27519
27607
  outs: outEdges.map((e2) => ({ child: e2.target, node: e2.source }))
27520
27608
  });
@@ -27542,6 +27630,130 @@ function stationInner(plan, model, band = 50, nodeMap) {
27542
27630
  ...plan.outs.map((o) => ({ id: o.node, child: o.child, s: specOf(o.node) })),
27543
27631
  ...(plan.leafOuts || []).map((id2) => ({ id: id2, child: null, s: specOf(id2) }))
27544
27632
  ];
27633
+ if (plan.multiBus && plan.busNodes && plan.busNodes.length > 1) {
27634
+ const TIE_GAP = 22;
27635
+ const busH = bs0.h;
27636
+ const busSwitchIds = /* @__PURE__ */ new Map();
27637
+ for (const b of plan.busNodes) {
27638
+ const outs = (plan.busOuts.get(b) || []).map((o) => o.node);
27639
+ const leaves = plan.busLeaves.get(b) || [];
27640
+ busSwitchIds.set(b, [.../* @__PURE__ */ new Set([...outs, ...leaves])]);
27641
+ }
27642
+ const busWidthOf = (b) => {
27643
+ const ids = busSwitchIds.get(b) || [];
27644
+ let mv = 0;
27645
+ for (const id2 of ids) mv = Math.max(mv, vert(specOf(id2)).w);
27646
+ const st = mv + 72;
27647
+ return Math.max(120, ids.length * st);
27648
+ };
27649
+ const order = plan.busNodes;
27650
+ const gapTies = /* @__PURE__ */ new Map();
27651
+ const placedTies = /* @__PURE__ */ new Set();
27652
+ for (let i = 0; i + 1 < order.length; i++) {
27653
+ const L2 = order[i], R = order[i + 1];
27654
+ for (const t of plan.ties) {
27655
+ if (placedTies.has(t.id)) continue;
27656
+ const link = t.from === L2 && t.to === R || t.from === R && t.to === L2;
27657
+ if (!link) continue;
27658
+ if (!gapTies.has(L2)) gapTies.set(L2, []);
27659
+ gapTies.get(L2).push({ ...t, left: L2, right: R });
27660
+ placedTies.add(t.id);
27661
+ }
27662
+ }
27663
+ for (const t of plan.ties) {
27664
+ if (placedTies.has(t.id)) continue;
27665
+ const L2 = t.from, R = t.to;
27666
+ if (!gapTies.has(L2)) gapTies.set(L2, []);
27667
+ gapTies.get(L2).push({ ...t, left: L2, right: R });
27668
+ placedTies.add(t.id);
27669
+ }
27670
+ const buses = [];
27671
+ const tieDefs = [];
27672
+ let cursor = 0;
27673
+ for (const bid of order) {
27674
+ const w = busWidthOf(bid);
27675
+ buses.push({ id: bid, w, x: cursor + w / 2 });
27676
+ cursor += w;
27677
+ const ts = gapTies.get(bid) || [];
27678
+ if (ts.length) {
27679
+ const TIE_SIDE = 22;
27680
+ const TIE_MIN_GAP = 168;
27681
+ const totalTieW = ts.reduce((s2, t) => s2 + specOf(t.id).w, 0) + (ts.length - 1) * TIE_SIDE;
27682
+ const gapTotal = Math.max(TIE_MIN_GAP, totalTieW + 2 * TIE_SIDE);
27683
+ let tc = cursor + (gapTotal - totalTieW) / 2;
27684
+ for (const t of ts) {
27685
+ const tsp = specOf(t.id);
27686
+ const dy = busH / 2 + 34 + tsp.h / 2;
27687
+ tieDefs.push({
27688
+ id: t.id,
27689
+ from: t.from,
27690
+ to: t.to,
27691
+ left: t.left,
27692
+ right: t.right,
27693
+ w: tsp.w,
27694
+ h: tsp.h,
27695
+ x: tc + tsp.w / 2,
27696
+ dy
27697
+ });
27698
+ tc += tsp.w + TIE_SIDE;
27699
+ }
27700
+ cursor += gapTotal;
27701
+ }
27702
+ }
27703
+ const primary = buses.find((b) => b.id === plan.busNode) || buses[0];
27704
+ const dx = -primary.x;
27705
+ for (const b of buses) b.x += dx;
27706
+ for (const t of tieDefs) t.x += dx;
27707
+ const inPosMb = plan.inNode ? (() => {
27708
+ const s2 = vert(specOf(plan.inNode));
27709
+ return { x: 0, y: -(busH / 2 + VGAP + s2.h / 2), w: s2.w, h: s2.h, rot: 90 };
27710
+ })() : null;
27711
+ let minX2 = Infinity, maxX2 = -Infinity, minY2 = -busH / 2, maxY2 = busH / 2;
27712
+ for (const b of buses) {
27713
+ minX2 = Math.min(minX2, b.x - b.w / 2);
27714
+ maxX2 = Math.max(maxX2, b.x + b.w / 2);
27715
+ }
27716
+ for (const t of tieDefs) {
27717
+ minX2 = Math.min(minX2, t.x - t.w / 2);
27718
+ maxX2 = Math.max(maxX2, t.x + t.w / 2);
27719
+ }
27720
+ if (inPosMb) {
27721
+ minY2 = Math.min(minY2, inPosMb.y - inPosMb.h / 2);
27722
+ maxY2 = Math.max(maxY2, inPosMb.y + inPosMb.h / 2);
27723
+ }
27724
+ if (plan.innerRoot) {
27725
+ const s2 = specOf(plan.innerRoot);
27726
+ minX2 = Math.min(minX2, -s2.w / 2);
27727
+ maxX2 = Math.max(maxX2, s2.w / 2);
27728
+ const inTop = inPosMb ? inPosMb.y - inPosMb.h / 2 : -busH / 2 - VGAP;
27729
+ minY2 = Math.min(minY2, inTop - VGAP - s2.h);
27730
+ }
27731
+ let maxSwH = 0;
27732
+ for (const [, ids] of busSwitchIds) for (const id2 of ids) maxSwH = Math.max(maxSwH, vert(specOf(id2)).h);
27733
+ const switchBottomMaxMb = busH / 2 + VGAP + maxSwH;
27734
+ maxY2 = Math.max(maxY2, switchBottomMaxMb);
27735
+ const needK02Mb = plan.outs.length >= 2 && !plan.outs.every(
27736
+ (o) => (nmap.get(o.child) || {}).cat === "consumer"
27737
+ );
27738
+ const sinkExtraMb = Math.max(0, switchBottomMaxMb + (needK02Mb ? band + 100 : 0) - maxY2);
27739
+ const spanW = maxX2 - minX2;
27740
+ return {
27741
+ inPos: inPosMb,
27742
+ outDefs: plan.outs.map((o) => ({ id: o.node, child: o.child, s: specOf(o.node) })),
27743
+ bs: { w: spanW, h: busH },
27744
+ VGAP,
27745
+ HGAP,
27746
+ HM,
27747
+ VM,
27748
+ vert,
27749
+ specOf,
27750
+ estW: spanW + 2 * HM,
27751
+ estH: maxY2 - minY2 + 2 * VM + sinkExtraMb,
27752
+ fanW: spanW,
27753
+ box: { minX: minX2, maxX: maxX2, minY: minY2, maxY: maxY2 },
27754
+ multiBus: { buses, ties: tieDefs, busH, busSwitchIds, TIE_GAP }
27755
+ };
27756
+ }
27545
27757
  let maxVW = 0;
27546
27758
  for (const o of outDefs) maxVW = Math.max(maxVW, vert(o.s).w);
27547
27759
  const step2 = maxVW + 72;
@@ -27750,6 +27962,7 @@ function fanSubtreeXRange(model, plans, root2, pos, nodeMap) {
27750
27962
  return [minX, maxX];
27751
27963
  }
27752
27964
  function expandStations(model, plans, pos, edgeCP, band, bandClear, vTrunkStations, nodeMap) {
27965
+ var _a;
27753
27966
  const nmap = nodeMap || new Map(model.nodes.map((n) => [n.id, n]));
27754
27967
  for (const plan of plans) {
27755
27968
  const sp = pos.get(plan.sid);
@@ -27819,6 +28032,7 @@ function expandStations(model, plans, pos, edgeCP, band, bandClear, vTrunkStatio
27819
28032
  const frames = [];
27820
28033
  const rotateMap = /* @__PURE__ */ new Map();
27821
28034
  const busWidth = /* @__PURE__ */ new Map();
28035
+ const tieNodes = /* @__PURE__ */ new Set();
27822
28036
  for (const plan of plans) {
27823
28037
  const sp = pos.get(plan.sid);
27824
28038
  if (!sp) continue;
@@ -27835,73 +28049,151 @@ function expandStations(model, plans, pos, edgeCP, band, bandClear, vTrunkStatio
27835
28049
  dirs.push({ ...o, dir: Math.abs(dy) < 0.5 ? "trunk" : dy > 0 ? "V" : "U" });
27836
28050
  }
27837
28051
  const devs = [];
27838
- if (plan.busNode) devs.push({ id: plan.busNode, x: xS, y: yS, w: bs.w, h: bs.h, rot: 0 });
28052
+ const mb = inner.multiBus || null;
28053
+ const outPos = /* @__PURE__ */ new Map();
28054
+ const busOfSwitch = /* @__PURE__ */ new Map();
28055
+ if (mb) for (const [b, ids] of mb.busSwitchIds) for (const id2 of ids) busOfSwitch.set(id2, b);
28056
+ if (mb) {
28057
+ for (const b of mb.buses) devs.push({ id: b.id, x: xS + b.x, y: yS, w: b.w, h: bs.h, rot: 0 });
28058
+ for (const t of mb.ties) {
28059
+ tieNodes.add(t.id);
28060
+ devs.push({ id: t.id, x: xS + t.x, y: yS + (t.dy || 0), w: t.w, h: t.h, rot: 0 });
28061
+ }
28062
+ } else if (plan.busNode) {
28063
+ devs.push({ id: plan.busNode, x: xS, y: yS, w: bs.w, h: bs.h, rot: 0 });
28064
+ }
27839
28065
  if (inner.inPos) {
27840
28066
  const p = inner.inPos;
27841
28067
  devs.push({ id: plan.inNode, x: xS + p.x, y: yS + p.y, w: p.w, h: p.h, rot: p.rot });
27842
28068
  }
27843
- if (plan.innerRoot && plan.inNode) {
28069
+ if (plan.innerRoot) {
27844
28070
  const s2 = specOf(plan.innerRoot);
27845
- const inTop = yS + inner.inPos.y - inner.inPos.h / 2;
27846
- devs.push({ id: plan.innerRoot, x: xS, y: inTop - VGAP - s2.h / 2, w: s2.w, h: s2.h, rot: 0 });
28071
+ if (plan.inNode && inner.inPos) {
28072
+ const inTop = yS + inner.inPos.y - inner.inPos.h / 2;
28073
+ devs.push({ id: plan.innerRoot, x: xS, y: inTop - VGAP - s2.h / 2, w: s2.w, h: s2.h, rot: 0 });
28074
+ } else if (plan.busNode) {
28075
+ const bx = mb ? ((_a = mb.buses.find((b) => b.id === plan.busNode)) == null ? void 0 : _a.x) ?? 0 : 0;
28076
+ devs.push({ id: plan.innerRoot, x: xS + bx, y: yS - (bs.h / 2 + VGAP + s2.h / 2), w: s2.w, h: s2.h, rot: 0 });
28077
+ }
27847
28078
  }
27848
28079
  const vOuts = dirs.filter((o) => o.node !== plan.busNode).sort((a2, b) => pos.get(a2.child).x - pos.get(b.child).x);
27849
28080
  const leafOuts = (plan.leafOuts || []).map((id2) => ({ node: id2, child: null, dir: "V" }));
27850
- const vAll = [...vOuts, ...leafOuts];
27851
28081
  const busHalf = bs.w / 2;
27852
28082
  const uniqNodes = [];
27853
28083
  const seenNodes = /* @__PURE__ */ new Set();
27854
- for (const o of vAll) {
27855
- if (!seenNodes.has(o.node)) {
27856
- seenNodes.add(o.node);
27857
- uniqNodes.push({ node: o.node, firstChild: o.child });
28084
+ if (mb) {
28085
+ for (const b of mb.buses) {
28086
+ const outs = vOuts.filter((o) => busOfSwitch.get(o.node) === b.id);
28087
+ const leaves = (plan.busLeaves.get(b.id) || []).map((id2) => ({ node: id2, child: null, dir: "V" }));
28088
+ const list = [];
28089
+ const seen = /* @__PURE__ */ new Set();
28090
+ for (const o of [...outs, ...leaves]) {
28091
+ if (!seen.has(o.node)) {
28092
+ seen.add(o.node);
28093
+ list.push(o);
28094
+ }
28095
+ }
28096
+ list.forEach((o, k) => {
28097
+ const v = vert(specOf(o.node));
28098
+ const x = list.length === 1 ? 0 : -b.w / 2 + (k + 0.5) * b.w / list.length;
28099
+ const p = { id: o.node, x: xS + b.x + x, y: yS + (bs.h / 2 + VGAP + v.h / 2), w: v.w, h: v.h, rot: 90 };
28100
+ outPos.set(o.node, p);
28101
+ devs.push(p);
28102
+ });
28103
+ }
28104
+ } else {
28105
+ const vAll = [...vOuts, ...leafOuts];
28106
+ for (const o of vAll) {
28107
+ if (!seenNodes.has(o.node)) {
28108
+ seenNodes.add(o.node);
28109
+ uniqNodes.push({ node: o.node, firstChild: o.child });
28110
+ }
28111
+ }
28112
+ const rowPos = (list) => list.map((o, k) => {
28113
+ const s2 = specOf(o.node);
28114
+ const v = vert(s2);
28115
+ const x = list.length === 1 ? 0 : -busHalf + (k + 0.5) * (2 * busHalf) / list.length;
28116
+ return { o, x, v };
28117
+ });
28118
+ for (const { o, x, v } of rowPos(uniqNodes)) {
28119
+ const p = { id: o.node, x: xS + x, y: yS + (bs.h / 2 + VGAP + v.h / 2), w: v.w, h: v.h, rot: 90 };
28120
+ outPos.set(o.node, p);
28121
+ devs.push(p);
27858
28122
  }
27859
- }
27860
- const rowPos = (list) => list.map((o, k) => {
27861
- const s2 = specOf(o.node);
27862
- const v = vert(s2);
27863
- const x = list.length === 1 ? 0 : -busHalf + (k + 0.5) * (2 * busHalf) / list.length;
27864
- return { o, x, v };
27865
- });
27866
- const outPos = /* @__PURE__ */ new Map();
27867
- for (const { o, x, v } of rowPos(uniqNodes)) {
27868
- const p = { id: o.node, x: xS + x, y: yS + (bs.h / 2 + VGAP + v.h / 2), w: v.w, h: v.h, rot: 90 };
27869
- outPos.set(o.node, p);
27870
- devs.push(p);
27871
28123
  }
27872
28124
  for (const d2 of devs) {
27873
28125
  pos.set(d2.id, { x: d2.x, y: d2.y });
27874
28126
  if (d2.rot) rotateMap.set(d2.id, d2.rot);
27875
28127
  }
27876
- if (plan.busNode && inner.inPos) {
27877
- const inBottom = pos.get(plan.inNode).y + inner.inPos.h / 2;
27878
- edgeCP.set(`${plan.inNode}-${plan.busNode}`, [[xS, inBottom], [xS, yS - bs.h / 2]]);
27879
- }
27880
- if (plan.innerRoot && plan.inNode) {
27881
- const ir = pos.get(plan.innerRoot);
27882
- const inTop = pos.get(plan.inNode).y - inner.inPos.h / 2;
27883
- edgeCP.set(`${plan.innerRoot}-${plan.inNode}`, [[xS, ir.y + specOf(plan.innerRoot).h / 2], [xS, inTop]]);
27884
- }
27885
- if (plan.busNode) {
27886
- for (const o of dirs) {
27887
- if (o.node === plan.busNode) continue;
27888
- const s2 = specOf(o.node);
27889
- const v = vert(s2);
27890
- const p = outPos.get(o.node);
27891
- const key = `${plan.busNode}-${o.node}`;
27892
- const top = p.y - v.h / 2;
27893
- edgeCP.set(key, [[p.x, yS + bs.h / 2], [p.x, top]]);
28128
+ if (mb) {
28129
+ if (plan.innerRoot && plan.inNode && inner.inPos) {
28130
+ const ir = pos.get(plan.innerRoot);
28131
+ const inTop = pos.get(plan.inNode).y - inner.inPos.h / 2;
28132
+ edgeCP.set(`${plan.innerRoot}-${plan.inNode}`, [[xS, ir.y + specOf(plan.innerRoot).h / 2], [xS, inTop]]);
28133
+ }
28134
+ if (plan.innerRoot && !plan.inNode && plan.busNode) {
28135
+ const ir = pos.get(plan.innerRoot);
28136
+ const pb = mb.buses.find((b) => b.id === plan.busNode) || mb.buses[0];
28137
+ edgeCP.set(`${plan.innerRoot}-${plan.busNode}`, [[xS + pb.x, ir.y + specOf(plan.innerRoot).h / 2], [xS + pb.x, yS - bs.h / 2]]);
28138
+ }
28139
+ if (plan.inNode && plan.busNode) {
28140
+ const pb = mb.buses.find((b) => b.id === plan.busNode) || mb.buses[0];
28141
+ const inBottom = pos.get(plan.inNode).y + inner.inPos.h / 2;
28142
+ edgeCP.set(`${plan.inNode}-${plan.busNode}`, [[xS + pb.x, inBottom], [xS + pb.x, yS - bs.h / 2]]);
28143
+ }
28144
+ for (const t of mb.ties) {
28145
+ const lb = mb.buses.find((b) => b.id === t.left);
28146
+ const rb = mb.buses.find((b) => b.id === t.right);
28147
+ if (!lb || !rb) continue;
28148
+ const lEnd = xS + lb.x + lb.w / 2;
28149
+ const rEnd = xS + rb.x - rb.w / 2;
28150
+ const tL = xS + t.x - t.w / 2;
28151
+ const tR = xS + t.x + t.w / 2;
28152
+ const yT = yS + (t.dy || 0);
28153
+ if (t.from === t.left) edgeCP.set(`${t.from}-${t.id}`, [[lEnd, yS], [lEnd, yT], [tL, yT]]);
28154
+ else edgeCP.set(`${t.from}-${t.id}`, [[rEnd, yS], [rEnd, yT], [tR, yT]]);
28155
+ if (t.to === t.right) edgeCP.set(`${t.id}-${t.to}`, [[tR, yT], [rEnd, yT], [rEnd, yS]]);
28156
+ else edgeCP.set(`${t.id}-${t.to}`, [[tL, yT], [lEnd, yT], [lEnd, yS]]);
28157
+ }
28158
+ for (const [nodeId, p] of outPos) {
28159
+ const b = busOfSwitch.get(nodeId) || plan.busNode;
28160
+ const v = vert(specOf(nodeId));
28161
+ edgeCP.set(`${b}-${nodeId}`, [[p.x, yS + bs.h / 2], [p.x, p.y - v.h / 2]]);
27894
28162
  }
27895
- }
27896
- const busOrIn = plan.busNode || plan.inNode;
27897
- if (busOrIn) {
27898
- const busP = plan.busNode ? pos.get(plan.busNode) : pos.get(plan.inNode);
27899
- const busY = (busP || { y: yS }).y;
27900
- for (const id2 of plan.leafOuts || []) {
27901
- const p = outPos.get(id2);
27902
- if (!p) continue;
27903
- const v = vert(specOf(id2));
27904
- edgeCP.set(`${busOrIn}-${id2}`, [[p.x, busY + bs.h / 2], [p.x, p.y - v.h / 2]]);
28163
+ } else {
28164
+ if (plan.busNode && inner.inPos) {
28165
+ const inBottom = pos.get(plan.inNode).y + inner.inPos.h / 2;
28166
+ edgeCP.set(`${plan.inNode}-${plan.busNode}`, [[xS, inBottom], [xS, yS - bs.h / 2]]);
28167
+ }
28168
+ if (plan.innerRoot && plan.inNode) {
28169
+ const ir = pos.get(plan.innerRoot);
28170
+ const inTop = pos.get(plan.inNode).y - inner.inPos.h / 2;
28171
+ edgeCP.set(`${plan.innerRoot}-${plan.inNode}`, [[xS, ir.y + specOf(plan.innerRoot).h / 2], [xS, inTop]]);
28172
+ } else if (plan.innerRoot && plan.busNode) {
28173
+ const ir = pos.get(plan.innerRoot);
28174
+ edgeCP.set(`${plan.innerRoot}-${plan.busNode}`, [[xS, ir.y + specOf(plan.innerRoot).h / 2], [xS, yS - bs.h / 2]]);
28175
+ }
28176
+ if (plan.busNode) {
28177
+ for (const o of dirs) {
28178
+ if (o.node === plan.busNode) continue;
28179
+ const s2 = specOf(o.node);
28180
+ const v = vert(s2);
28181
+ const p = outPos.get(o.node);
28182
+ const key = `${plan.busNode}-${o.node}`;
28183
+ const top = p.y - v.h / 2;
28184
+ edgeCP.set(key, [[p.x, yS + bs.h / 2], [p.x, top]]);
28185
+ }
28186
+ }
28187
+ const busOrIn = plan.busNode || plan.inNode;
28188
+ if (busOrIn) {
28189
+ const busP = plan.busNode ? pos.get(plan.busNode) : pos.get(plan.inNode);
28190
+ const busY = (busP || { y: yS }).y;
28191
+ for (const id2 of plan.leafOuts || []) {
28192
+ const p = outPos.get(id2);
28193
+ if (!p) continue;
28194
+ const v = vert(specOf(id2));
28195
+ edgeCP.set(`${busOrIn}-${id2}`, [[p.x, busY + bs.h / 2], [p.x, p.y - v.h / 2]]);
28196
+ }
27905
28197
  }
27906
28198
  }
27907
28199
  const inTarget = plan.inNode ? plan.inNode : plan.busNode;
@@ -28060,18 +28352,21 @@ function expandStations(model, plans, pos, edgeCP, band, bandClear, vTrunkStatio
28060
28352
  const cw = specOf(o.child).w;
28061
28353
  const ch = specOf(o.child).h;
28062
28354
  const level = levelOf.get(o) ?? bandLevel;
28063
- if (o.node === plan.busNode) {
28355
+ if (o.node === plan.busNode || mb && mb.buses.some((b) => b.id === o.node)) {
28356
+ const bb = mb ? mb.buses.find((b) => b.id === o.node) : null;
28357
+ const anchorX = bb ? xS + bb.x : xS;
28358
+ const anchorHalf = bb ? bb.w / 2 : busHalf;
28064
28359
  if (o.dir === "trunk") {
28065
28360
  edgeCP.set(`${o.node}-${o.child}`, [
28066
- [xS + busHalf, yS],
28067
- [xS + busHalf, level],
28361
+ [anchorX + anchorHalf, yS],
28362
+ [anchorX + anchorHalf, level],
28068
28363
  [childPos.x - cw / 2, level],
28069
28364
  [childPos.x - cw / 2, childPos.y - ch / 2]
28070
28365
  ]);
28071
28366
  } else {
28072
28367
  edgeCP.set(`${o.node}-${o.child}`, [
28073
- [xS, yS + bs.h / 2],
28074
- [xS, level],
28368
+ [anchorX, yS + bs.h / 2],
28369
+ [anchorX, level],
28075
28370
  [childPos.x, level],
28076
28371
  [childPos.x, childPos.y - ch / 2]
28077
28372
  ]);
@@ -28098,10 +28393,10 @@ function expandStations(model, plans, pos, edgeCP, band, bandClear, vTrunkStatio
28098
28393
  }
28099
28394
  edgeCP.delete(`${plan.sid}-${o.child}`);
28100
28395
  }
28101
- let bminX = -busHalf;
28102
- let bmaxX = busHalf;
28103
- let bminY = -bs.h / 2;
28104
- let bmaxY = bs.h / 2;
28396
+ let bminX = mb ? Infinity : -busHalf;
28397
+ let bmaxX = mb ? -Infinity : busHalf;
28398
+ let bminY = mb ? Infinity : -bs.h / 2;
28399
+ let bmaxY = mb ? -Infinity : bs.h / 2;
28105
28400
  for (const d2 of devs) {
28106
28401
  bminX = Math.min(bminX, d2.x - xS - d2.w / 2);
28107
28402
  bmaxX = Math.max(bmaxX, d2.x - xS + d2.w / 2);
@@ -28130,9 +28425,22 @@ function expandStations(model, plans, pos, edgeCP, band, bandClear, vTrunkStatio
28130
28425
  }
28131
28426
  pos.set(plan.sid, { x: fx, y: fy });
28132
28427
  frames.push({ id: plan.sid, x: fx, y: fy, w: fw, h: fh, name: plan.name });
28133
- if (plan.busNode) busWidth.set(plan.busNode, bs.w);
28428
+ if (mb) {
28429
+ for (const b of mb.buses) busWidth.set(b.id, b.w);
28430
+ } else if (plan.busNode) {
28431
+ busWidth.set(plan.busNode, bs.w);
28432
+ }
28433
+ for (const e2 of model.edges) {
28434
+ if (!plan.containSet.has(e2.source) || !plan.containSet.has(e2.target)) continue;
28435
+ const key = `${e2.source}-${e2.target}`;
28436
+ if (edgeCP.has(key)) continue;
28437
+ const pa = pos.get(e2.source);
28438
+ const pb = pos.get(e2.target);
28439
+ if (!pa || !pb) continue;
28440
+ edgeCP.set(key, [[pa.x, pa.y], [pa.x, pb.y], [pb.x, pb.y]]);
28441
+ }
28134
28442
  }
28135
- return { frames, rotateMap, busWidth };
28443
+ return { frames, rotateMap, busWidth, tieNodes };
28136
28444
  }
28137
28445
  function computeOrthogonalLayout(model, opts = {}) {
28138
28446
  const {
@@ -28639,6 +28947,7 @@ function computeOrthogonalLayout(model, opts = {}) {
28639
28947
  const stationFrames = [];
28640
28948
  const stationRotate = /* @__PURE__ */ new Map();
28641
28949
  const stationBusWidth = /* @__PURE__ */ new Map();
28950
+ const stationTies = /* @__PURE__ */ new Set();
28642
28951
  if (plans.length) {
28643
28952
  const depthOf = /* @__PURE__ */ new Map([[extModel.rootId, 0]]);
28644
28953
  const vis = /* @__PURE__ */ new Set([extModel.rootId]);
@@ -28654,10 +28963,11 @@ function computeOrthogonalLayout(model, opts = {}) {
28654
28963
  }
28655
28964
  }
28656
28965
  plans.sort((a2, b) => (depthOf.get(b.sid) || 0) - (depthOf.get(a2.sid) || 0));
28657
- const { frames, rotateMap, busWidth } = expandStations(model, plans, pos, edgeCP, band, bandClear, vChainStations, nodeMap);
28966
+ const { frames, rotateMap, busWidth, tieNodes } = expandStations(model, plans, pos, edgeCP, band, bandClear, vChainStations, nodeMap);
28658
28967
  stationFrames.push(...frames);
28659
28968
  for (const [k, v] of rotateMap) stationRotate.set(k, v);
28660
28969
  for (const [k, v] of busWidth) stationBusWidth.set(k, v);
28970
+ if (tieNodes) for (const id2 of tieNodes) stationTies.add(id2);
28661
28971
  }
28662
28972
  const nodeSpec = (id2) => {
28663
28973
  const f2 = stationFrames.find((x) => x.id === id2);
@@ -28705,6 +29015,7 @@ function computeOrthogonalLayout(model, opts = {}) {
28705
29015
  stationFrames,
28706
29016
  stationRotate,
28707
29017
  stationBusWidth,
29018
+ stationTies,
28708
29019
  bounds: { minX: minX + dx, minY: minY + dy, maxX: maxX + dx, maxY: maxY + dy },
28709
29020
  stats: { trunkLen: 0, leaves: 0, nodes: model.nodes.length }
28710
29021
  };
@@ -28792,6 +29103,7 @@ function setEdgeTheme(themeName) {
28792
29103
  function buildGraphData(model, layout) {
28793
29104
  const nodes = model.nodes.map((n) => {
28794
29105
  const spec = getSymbolSpec(n.cat);
29106
+ const isTie = !!(layout.stationTies && layout.stationTies.has(n.id));
28795
29107
  const busW = layout.stationBusWidth && layout.stationBusWidth.get(n.id);
28796
29108
  const meterBoxDim = n.cat === "meterBox" ? getMeterBoxSize(n) : null;
28797
29109
  const w = busW || (meterBoxDim ? meterBoxDim.w : spec.w);
@@ -28810,6 +29122,8 @@ function buildGraphData(model, layout) {
28810
29122
  // 禁用 G6 内置 label(label:false),避免其单行画出长名不换行。
28811
29123
  label: false,
28812
29124
  labelText: n.cat === "bus" || n.cat === "cableTerminal" || n.cat === "consumer" ? "" : n.shortName || "",
29125
+ // 母联标记:canvasNode 据此把(短名)标签居中画在「半矩形」内部
29126
+ _tieLabel: isTie,
28813
29127
  rotate: layout.stationRotate && layout.stationRotate.get(n.id) || 0,
28814
29128
  highlight: 0,
28815
29129
  cursor: "pointer",
@@ -29028,6 +29342,12 @@ function drawLabelUnder(attributes, group) {
29028
29342
  const x0 = w / 2 + 6;
29029
29343
  _drawWrappedTextFixed(group, label, x0, -h / 2, 8, C2.white, 4, 1.3, 500, "left");
29030
29344
  }
29345
+ function drawTieLabel(attributes, group) {
29346
+ const label = attributes.labelText || "";
29347
+ if (!label) return;
29348
+ const [w, h] = attributes.size;
29349
+ _drawWrappedText(group, label, 0, -(h / 2 + 22), 7, C2.white, Math.max(72, w + 34), 1.2, 500);
29350
+ }
29031
29351
  function text(attributes, group, str2, x, y, size, fill, weight = 400) {
29032
29352
  if (!str2) return;
29033
29353
  group.appendChild(
@@ -29165,7 +29485,7 @@ function drawBreaker(attributes, group) {
29165
29485
  group.appendChild(new Line({ style: { x1: xBox, y1: 0, x2: xEdge, y2: 0, stroke: P.red, lineWidth: lw, lineCap: "round" } }));
29166
29486
  }
29167
29487
  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 } }));
29168
- if (attributes.labelText) {
29488
+ if (attributes.labelText && !attributes._tieLabel) {
29169
29489
  _drawWrappedText(group, attributes.labelText, 0, 0.6, 11.5, C2.switchText, bw - 8, 1.15, 700);
29170
29490
  }
29171
29491
  }
@@ -29672,6 +29992,7 @@ class SvgSymbolNode extends g6.BaseNode {
29672
29992
  drawGlowBackdrop(attributes, target);
29673
29993
  const symStart = (target.children || []).length;
29674
29994
  draw(attributes, target);
29995
+ if (attributes._tieLabel && attributes.labelText) drawTieLabel(attributes, target);
29675
29996
  const symEnd = (target.children || []).length;
29676
29997
  drawGlow(attributes, target);
29677
29998
  drawHighlight(attributes, target);
@@ -29720,10 +30041,58 @@ const _hoisted_4$2 = { class: "topo-meta" };
29720
30041
  const _hoisted_5$2 = ["title"];
29721
30042
  const _hoisted_6$2 = {
29722
30043
  key: 1,
30044
+ class: "topo-legend"
30045
+ };
30046
+ const _hoisted_7$2 = { class: "topo-legend-body" };
30047
+ const _hoisted_8$2 = {
30048
+ class: "topo-legend-icon",
30049
+ viewBox: "0 0 32 20",
30050
+ width: "32",
30051
+ height: "20",
30052
+ "aria-hidden": "true"
30053
+ };
30054
+ const _hoisted_9$2 = ["stroke"];
30055
+ const _hoisted_10$2 = ["fill", "stroke"];
30056
+ const _hoisted_11$2 = ["stroke"];
30057
+ const _hoisted_12$2 = ["stroke"];
30058
+ const _hoisted_13$2 = ["stroke"];
30059
+ const _hoisted_14$2 = ["fill"];
30060
+ const _hoisted_15$1 = ["stroke"];
30061
+ const _hoisted_16$1 = ["stroke"];
30062
+ const _hoisted_17$1 = ["stroke"];
30063
+ const _hoisted_18$1 = ["fill"];
30064
+ const _hoisted_19$1 = ["fill"];
30065
+ const _hoisted_20$1 = ["fill", "stroke"];
30066
+ const _hoisted_21$1 = ["stroke"];
30067
+ const _hoisted_22$1 = ["fill"];
30068
+ const _hoisted_23$1 = ["stroke"];
30069
+ const _hoisted_24$1 = ["stroke"];
30070
+ const _hoisted_25$1 = ["stroke"];
30071
+ const _hoisted_26$1 = ["stroke"];
30072
+ const _hoisted_27$1 = ["fill", "stroke"];
30073
+ const _hoisted_28$1 = ["fill"];
30074
+ const _hoisted_29$1 = ["stroke"];
30075
+ const _hoisted_30$1 = ["fill"];
30076
+ const _hoisted_31$1 = ["fill", "stroke"];
30077
+ const _hoisted_32$1 = ["fill"];
30078
+ const _hoisted_33$1 = ["stroke"];
30079
+ const _hoisted_34$1 = ["fill"];
30080
+ const _hoisted_35$1 = ["fill"];
30081
+ const _hoisted_36$1 = ["fill", "stroke"];
30082
+ const _hoisted_37$1 = ["stroke", "stroke-width"];
30083
+ const _hoisted_38$1 = ["fill"];
30084
+ const _hoisted_39 = ["stroke"];
30085
+ const _hoisted_40 = ["fill"];
30086
+ const _hoisted_41 = ["stroke"];
30087
+ const _hoisted_42 = ["fill"];
30088
+ const _hoisted_43 = ["fill"];
30089
+ const _hoisted_44 = { class: "topo-legend-label" };
30090
+ const _hoisted_45 = {
30091
+ key: 2,
29723
30092
  class: "topo-loading"
29724
30093
  };
29725
- const _hoisted_7$2 = {
29726
- key: 2,
30094
+ const _hoisted_46 = {
30095
+ key: 3,
29727
30096
  class: "topo-error"
29728
30097
  };
29729
30098
  const _sfc_main$2 = {
@@ -29735,10 +30104,14 @@ const _sfc_main$2 = {
29735
30104
  customerData: { type: Array, default: null },
29736
30105
  /** 主题:'dark'(默认,黑底 SCADA 风)| 'light'(白底);支持 v-model:theme 双向绑定 */
29737
30106
  theme: { type: String, default: "light" },
29738
- /** 左上角工具条(标题/统计/主题/适应/导出)是否显示,默认不显示 */
29739
- toolbar: { type: Boolean, default: false }
30107
+ /** 左上角工具条(标题/统计/主题/图例/适应/导出)是否显示,默认不显示 */
30108
+ toolbar: { type: Boolean, default: false },
30109
+ /** 是否显示左下角图例面板,默认 false(关闭);支持 v-model:legend */
30110
+ legend: { type: Boolean, default: false },
30111
+ /** 自定义图例项(可选):[{ label, shape, color?, text?, key? }];不传则按当前图内容自动生成 */
30112
+ legendItems: { type: Array, default: null }
29740
30113
  },
29741
- emits: ["node-select", "customer-select", "loaded", "top-distances", "update:theme"],
30114
+ emits: ["node-select", "customer-select", "loaded", "top-distances", "update:theme", "update:legend"],
29742
30115
  setup(__props, { expose: __expose, emit: __emit }) {
29743
30116
  const props = __props;
29744
30117
  const emit = __emit;
@@ -29748,6 +30121,49 @@ const _sfc_main$2 = {
29748
30121
  const title = vue.ref("");
29749
30122
  const stats = vue.ref({ total: 0, depth: 0, leaves: 0, byCat: {} });
29750
30123
  const edgeCount = vue.ref(0);
30124
+ const legendCollapsed = vue.ref(false);
30125
+ const hasCustomerData = vue.ref(false);
30126
+ const hasSwitch = vue.ref(false);
30127
+ const hasFuse = vue.ref(false);
30128
+ const LEGEND_DEFS = [
30129
+ { cat: "transformer", label: "配电变压器", shape: "transformer" },
30130
+ { cat: "switch", label: "开关 / 断路器", shape: "switch" },
30131
+ { cat: "fuse", label: "低压熔丝", shape: "fuse" },
30132
+ { cat: "bus", label: "低压母线", shape: "bus" },
30133
+ { cat: "cableTerminal", label: "电缆终端头", shape: "cableTerminal" },
30134
+ { cat: "junction", label: "密母分接点", shape: "junction" },
30135
+ { cat: "meterBox", label: "计量箱", shape: "meterBox" },
30136
+ { cat: "consumer", label: "用户接入点", shape: "consumer" },
30137
+ { cat: "line", label: "低压导线点", shape: "line" },
30138
+ { cat: "station", label: "低压配电箱", shape: "station" },
30139
+ { cat: "other", label: "其它设备", shape: "other" }
30140
+ ];
30141
+ const legendPalette = vue.computed(() => THEMES[themeName()] || THEMES.dark);
30142
+ function legendColor(item, role) {
30143
+ if (item && item.color) return item.color;
30144
+ return legendPalette.value[role] || "#888888";
30145
+ }
30146
+ const legendEntries = vue.computed(() => {
30147
+ const custom = props.legendItems;
30148
+ if (Array.isArray(custom) && custom.length) {
30149
+ return custom.map((it, i) => ({ key: it.key || `custom-${i}`, ...it }));
30150
+ }
30151
+ const byCat = stats.value.byCat || {};
30152
+ const include = (d2) => {
30153
+ if (d2.cat === "switch") return hasSwitch.value;
30154
+ if (d2.cat === "fuse") return hasFuse.value;
30155
+ return !!byCat[d2.cat];
30156
+ };
30157
+ const list = LEGEND_DEFS.filter(include).map((d2) => ({ ...d2, key: d2.cat }));
30158
+ if (list.length) {
30159
+ list.push({ key: "edge-trunk", label: "主干导线", shape: "edge", role: "trunkEdge" });
30160
+ list.push({ key: "edge-branch", label: "分支导线", shape: "edge", role: "branchEdge" });
30161
+ }
30162
+ if (hasCustomerData.value) {
30163
+ list.push({ key: "meterUser", label: "客户电表", shape: "meterUser" });
30164
+ }
30165
+ return list;
30166
+ });
29751
30167
  let graph = null;
29752
30168
  let model = null;
29753
30169
  let selectedId = null;
@@ -30037,6 +30453,9 @@ const _sfc_main$2 = {
30037
30453
  title.value = String(model.title || "").replace(/\.+$/, "");
30038
30454
  stats.value = model.stats;
30039
30455
  edgeCount.value = model.edges.length;
30456
+ hasCustomerData.value = model.nodes.some((n) => Array.isArray(n._customers) && n._customers.length);
30457
+ hasSwitch.value = model.nodes.some((n) => n.cat === "switch" && !n.isFuse);
30458
+ hasFuse.value = model.nodes.some((n) => n.cat === "switch" && n.isFuse);
30040
30459
  layoutData = computeOrthogonalLayout(model, {
30041
30460
  nodeGap: 26,
30042
30461
  // 主干相邻节点边界间距
@@ -30302,6 +30721,11 @@ const _sfc_main$2 = {
30302
30721
  title: __props.theme === "light" ? "切换到深色主题" : "切换到浅色主题",
30303
30722
  onClick: _cache[0] || (_cache[0] = ($event) => emit("update:theme", __props.theme === "light" ? "dark" : "light"))
30304
30723
  }, vue.toDisplayString(__props.theme === "light" ? "🌙 深色" : "☀️ 浅色"), 9, _hoisted_5$2),
30724
+ vue.createElementVNode("button", {
30725
+ class: "btn",
30726
+ title: "显示 / 隐藏图例",
30727
+ onClick: _cache[1] || (_cache[1] = ($event) => emit("update:legend", !__props.legend))
30728
+ }, vue.toDisplayString(__props.legend ? "🗺 图例 ✓" : "🗺 图例"), 1),
30305
30729
  vue.createElementVNode("button", {
30306
30730
  class: "btn",
30307
30731
  onClick: resetView
@@ -30311,12 +30735,334 @@ const _sfc_main$2 = {
30311
30735
  onClick: exportPng
30312
30736
  }, "导出 PNG")
30313
30737
  ])) : vue.createCommentVNode("", true),
30314
- loading.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_6$2, "正在解析台区拓扑…")) : error.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_7$2, vue.toDisplayString(error.value), 1)) : vue.createCommentVNode("", true)
30738
+ __props.legend && !loading.value && !error.value && legendEntries.value.length ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_6$2, [
30739
+ vue.createElementVNode("div", {
30740
+ class: "topo-legend-title",
30741
+ onClick: _cache[2] || (_cache[2] = ($event) => legendCollapsed.value = !legendCollapsed.value)
30742
+ }, [
30743
+ vue.createElementVNode("span", {
30744
+ class: vue.normalizeClass(["topo-legend-arrow", { open: !legendCollapsed.value }])
30745
+ }, "▸", 2),
30746
+ _cache[3] || (_cache[3] = vue.createTextVNode(" 图例 ", -1))
30747
+ ]),
30748
+ vue.withDirectives(vue.createElementVNode("div", _hoisted_7$2, [
30749
+ (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(legendEntries.value, (it) => {
30750
+ return vue.openBlock(), vue.createElementBlock("div", {
30751
+ key: it.key || it.label,
30752
+ class: "topo-legend-row"
30753
+ }, [
30754
+ (vue.openBlock(), vue.createElementBlock("svg", _hoisted_8$2, [
30755
+ it.shape === "transformer" ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 0 }, [
30756
+ vue.createElementVNode("line", {
30757
+ x1: "2",
30758
+ y1: "10",
30759
+ x2: "8",
30760
+ y2: "10",
30761
+ stroke: legendColor(it, "trunkLine"),
30762
+ "stroke-width": "1.6",
30763
+ "stroke-linecap": "round"
30764
+ }, null, 8, _hoisted_9$2),
30765
+ vue.createElementVNode("circle", {
30766
+ cx: "12",
30767
+ cy: "10",
30768
+ r: "5",
30769
+ fill: legendColor(it, "orange"),
30770
+ stroke: legendColor(it, "trunkLine"),
30771
+ "stroke-width": "1.2"
30772
+ }, null, 8, _hoisted_10$2),
30773
+ vue.createElementVNode("circle", {
30774
+ cx: "22",
30775
+ cy: "10",
30776
+ r: "5",
30777
+ fill: "none",
30778
+ stroke: legendColor(it, "red"),
30779
+ "stroke-width": "2"
30780
+ }, null, 8, _hoisted_11$2),
30781
+ vue.createElementVNode("line", {
30782
+ x1: "27",
30783
+ y1: "10",
30784
+ x2: "31",
30785
+ y2: "10",
30786
+ stroke: legendColor(it, "red"),
30787
+ "stroke-width": "1.6",
30788
+ "stroke-linecap": "round"
30789
+ }, null, 8, _hoisted_12$2)
30790
+ ], 64)) : it.shape === "switch" ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 1 }, [
30791
+ vue.createElementVNode("line", {
30792
+ x1: "2",
30793
+ y1: "10",
30794
+ x2: "7",
30795
+ y2: "10",
30796
+ stroke: legendColor(it, "red"),
30797
+ "stroke-width": "1.6",
30798
+ "stroke-linecap": "round"
30799
+ }, null, 8, _hoisted_13$2),
30800
+ vue.createElementVNode("rect", {
30801
+ x: "7",
30802
+ y: "5",
30803
+ width: "18",
30804
+ height: "10",
30805
+ rx: "2",
30806
+ fill: legendColor(it, "red")
30807
+ }, null, 8, _hoisted_14$2),
30808
+ vue.createElementVNode("line", {
30809
+ x1: "25",
30810
+ y1: "10",
30811
+ x2: "30",
30812
+ y2: "10",
30813
+ stroke: legendColor(it, "red"),
30814
+ "stroke-width": "1.6",
30815
+ "stroke-linecap": "round"
30816
+ }, null, 8, _hoisted_15$1)
30817
+ ], 64)) : it.shape === "fuse" ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 2 }, [
30818
+ vue.createElementVNode("line", {
30819
+ x1: "2",
30820
+ y1: "10",
30821
+ x2: "6",
30822
+ y2: "10",
30823
+ stroke: legendColor(it, "red"),
30824
+ "stroke-width": "1.6",
30825
+ "stroke-linecap": "round"
30826
+ }, null, 8, _hoisted_16$1),
30827
+ vue.createElementVNode("rect", {
30828
+ x: "6",
30829
+ y: "5",
30830
+ width: "20",
30831
+ height: "10",
30832
+ rx: "2",
30833
+ fill: "none",
30834
+ stroke: legendColor(it, "red"),
30835
+ "stroke-width": "1.6"
30836
+ }, null, 8, _hoisted_17$1),
30837
+ vue.createElementVNode("circle", {
30838
+ cx: "12",
30839
+ cy: "10",
30840
+ r: "2.2",
30841
+ fill: legendColor(it, "red")
30842
+ }, null, 8, _hoisted_18$1),
30843
+ vue.createElementVNode("rect", {
30844
+ x: "16",
30845
+ y: "7",
30846
+ width: "7",
30847
+ height: "6",
30848
+ fill: legendColor(it, "red")
30849
+ }, null, 8, _hoisted_19$1)
30850
+ ], 64)) : it.shape === "bus" ? (vue.openBlock(), vue.createElementBlock("rect", {
30851
+ key: 3,
30852
+ x: "5",
30853
+ y: "8",
30854
+ width: "22",
30855
+ height: "4",
30856
+ rx: "2",
30857
+ fill: legendColor(it, "white"),
30858
+ "fill-opacity": "0.5",
30859
+ stroke: legendColor(it, "white"),
30860
+ "stroke-width": "1.2"
30861
+ }, null, 8, _hoisted_20$1)) : it.shape === "cableTerminal" ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 4 }, [
30862
+ vue.createElementVNode("line", {
30863
+ x1: "3",
30864
+ y1: "10",
30865
+ x2: "9",
30866
+ y2: "10",
30867
+ stroke: legendColor(it, "red"),
30868
+ "stroke-width": "1.6",
30869
+ "stroke-linecap": "round"
30870
+ }, null, 8, _hoisted_21$1),
30871
+ vue.createElementVNode("polygon", {
30872
+ points: "9,4 20,10 9,16",
30873
+ fill: legendColor(it, "red")
30874
+ }, null, 8, _hoisted_22$1)
30875
+ ], 64)) : it.shape === "junction" ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 5 }, [
30876
+ vue.createElementVNode("line", {
30877
+ x1: "10",
30878
+ y1: "10",
30879
+ x2: "22",
30880
+ y2: "10",
30881
+ stroke: legendColor(it, "red"),
30882
+ "stroke-width": "1.8",
30883
+ "stroke-linecap": "round"
30884
+ }, null, 8, _hoisted_23$1),
30885
+ vue.createElementVNode("line", {
30886
+ x1: "16",
30887
+ y1: "4",
30888
+ x2: "16",
30889
+ y2: "16",
30890
+ stroke: legendColor(it, "red"),
30891
+ "stroke-width": "1.8",
30892
+ "stroke-linecap": "round"
30893
+ }, null, 8, _hoisted_24$1),
30894
+ vue.createElementVNode("line", {
30895
+ x1: "11",
30896
+ y1: "5",
30897
+ x2: "21",
30898
+ y2: "15",
30899
+ stroke: legendColor(it, "red"),
30900
+ "stroke-width": "1.8",
30901
+ "stroke-linecap": "round"
30902
+ }, null, 8, _hoisted_25$1),
30903
+ vue.createElementVNode("line", {
30904
+ x1: "21",
30905
+ y1: "5",
30906
+ x2: "11",
30907
+ y2: "15",
30908
+ stroke: legendColor(it, "red"),
30909
+ "stroke-width": "1.8",
30910
+ "stroke-linecap": "round"
30911
+ }, null, 8, _hoisted_26$1)
30912
+ ], 64)) : it.shape === "meterBox" ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 6 }, [
30913
+ vue.createElementVNode("rect", {
30914
+ x: "10",
30915
+ y: "2",
30916
+ width: "16",
30917
+ height: "16",
30918
+ rx: "3",
30919
+ fill: legendColor(it, "meterFill"),
30920
+ stroke: legendColor(it, "meterText"),
30921
+ "stroke-width": "1.4"
30922
+ }, null, 8, _hoisted_27$1),
30923
+ vue.createElementVNode("text", {
30924
+ x: "18",
30925
+ y: "13.5",
30926
+ "text-anchor": "middle",
30927
+ "font-size": "8",
30928
+ "font-weight": "700",
30929
+ fill: legendColor(it, "meterText")
30930
+ }, "JX", 8, _hoisted_28$1)
30931
+ ], 64)) : it.shape === "consumer" ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 7 }, [
30932
+ vue.createElementVNode("circle", {
30933
+ cx: "16",
30934
+ cy: "10",
30935
+ r: "7",
30936
+ fill: "none",
30937
+ stroke: legendColor(it, "white"),
30938
+ "stroke-width": "2"
30939
+ }, null, 8, _hoisted_29$1),
30940
+ vue.createElementVNode("text", {
30941
+ x: "16",
30942
+ y: "13",
30943
+ "text-anchor": "middle",
30944
+ "font-size": "9",
30945
+ "font-weight": "700",
30946
+ fill: legendColor(it, "white")
30947
+ }, "J", 8, _hoisted_30$1)
30948
+ ], 64)) : it.shape === "line" ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 8 }, [
30949
+ vue.createElementVNode("circle", {
30950
+ cx: "16",
30951
+ cy: "10",
30952
+ r: "4.6",
30953
+ fill: legendColor(it, "dotFill"),
30954
+ stroke: legendColor(it, "cyan"),
30955
+ "stroke-width": "1.6"
30956
+ }, null, 8, _hoisted_31$1),
30957
+ vue.createElementVNode("circle", {
30958
+ cx: "16",
30959
+ cy: "10",
30960
+ r: "1.5",
30961
+ fill: legendColor(it, "cyan")
30962
+ }, null, 8, _hoisted_32$1)
30963
+ ], 64)) : it.shape === "meterUser" ? (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 9 }, [
30964
+ vue.createElementVNode("rect", {
30965
+ x: "12",
30966
+ y: "2",
30967
+ width: "8",
30968
+ height: "15",
30969
+ rx: "2",
30970
+ fill: "none",
30971
+ stroke: legendColor(it, "meterText"),
30972
+ "stroke-width": "1.2"
30973
+ }, null, 8, _hoisted_33$1),
30974
+ vue.createElementVNode("rect", {
30975
+ x: "13.5",
30976
+ y: "4",
30977
+ width: "5",
30978
+ height: "2.2",
30979
+ rx: "1",
30980
+ fill: legendColor(it, "meterText"),
30981
+ "fill-opacity": "0.6"
30982
+ }, null, 8, _hoisted_34$1),
30983
+ vue.createElementVNode("circle", {
30984
+ cx: "16",
30985
+ cy: "11",
30986
+ r: "1.4",
30987
+ fill: legendColor(it, "meterText")
30988
+ }, null, 8, _hoisted_35$1)
30989
+ ], 64)) : it.shape === "station" ? (vue.openBlock(), vue.createElementBlock("rect", {
30990
+ key: 10,
30991
+ x: "6",
30992
+ y: "4",
30993
+ width: "20",
30994
+ height: "12",
30995
+ rx: "2",
30996
+ fill: legendColor(it, "stationFill"),
30997
+ stroke: legendColor(it, "white"),
30998
+ "stroke-width": "1.4"
30999
+ }, null, 8, _hoisted_36$1)) : it.shape === "edge" ? (vue.openBlock(), vue.createElementBlock("line", {
31000
+ key: 11,
31001
+ x1: "3",
31002
+ y1: "10",
31003
+ x2: "29",
31004
+ y2: "10",
31005
+ stroke: legendColor(it, it.role || "trunkEdge"),
31006
+ "stroke-width": it.role === "branchEdge" ? 1.2 : 2.4,
31007
+ "stroke-linecap": "round"
31008
+ }, null, 8, _hoisted_37$1)) : it.shape === "square" ? (vue.openBlock(), vue.createElementBlock("rect", {
31009
+ key: 12,
31010
+ x: "10",
31011
+ y: "4",
31012
+ width: "12",
31013
+ height: "12",
31014
+ rx: "2",
31015
+ fill: it.color || legendColor(it, "cyan")
31016
+ }, null, 8, _hoisted_38$1)) : it.shape === "ring" ? (vue.openBlock(), vue.createElementBlock("circle", {
31017
+ key: 13,
31018
+ cx: "16",
31019
+ cy: "10",
31020
+ r: "6",
31021
+ fill: "none",
31022
+ stroke: it.color || legendColor(it, "cyan"),
31023
+ "stroke-width": "2.4"
31024
+ }, null, 8, _hoisted_39)) : it.shape === "triangle" ? (vue.openBlock(), vue.createElementBlock("polygon", {
31025
+ key: 14,
31026
+ points: "10,16 22,16 16,4",
31027
+ fill: it.color || legendColor(it, "red")
31028
+ }, null, 8, _hoisted_40)) : it.shape === "hline" ? (vue.openBlock(), vue.createElementBlock("line", {
31029
+ key: 15,
31030
+ x1: "3",
31031
+ y1: "10",
31032
+ x2: "29",
31033
+ y2: "10",
31034
+ stroke: it.color || legendColor(it, "cyan"),
31035
+ "stroke-width": "2.4",
31036
+ "stroke-linecap": "round"
31037
+ }, null, 8, _hoisted_41)) : it.shape === "text" ? (vue.openBlock(), vue.createElementBlock("text", {
31038
+ key: 16,
31039
+ x: "16",
31040
+ y: "14",
31041
+ "text-anchor": "middle",
31042
+ "font-size": "11",
31043
+ "font-weight": "700",
31044
+ fill: it.color || legendColor(it, "white")
31045
+ }, vue.toDisplayString(it.text || "JX"), 9, _hoisted_42)) : (vue.openBlock(), vue.createElementBlock("circle", {
31046
+ key: 17,
31047
+ cx: "16",
31048
+ cy: "10",
31049
+ r: "5",
31050
+ fill: it.color || legendColor(it, "cyan")
31051
+ }, null, 8, _hoisted_43))
31052
+ ])),
31053
+ vue.createElementVNode("span", _hoisted_44, vue.toDisplayString(it.label), 1)
31054
+ ]);
31055
+ }), 128))
31056
+ ], 512), [
31057
+ [vue.vShow, !legendCollapsed.value]
31058
+ ])
31059
+ ])) : vue.createCommentVNode("", true),
31060
+ loading.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_45, "正在解析台区拓扑…")) : error.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_46, vue.toDisplayString(error.value), 1)) : vue.createCommentVNode("", true)
30315
31061
  ], 8, _hoisted_1$2);
30316
31062
  };
30317
31063
  }
30318
31064
  };
30319
- const TopoGraph = /* @__PURE__ */ _export_sfc(_sfc_main$2, [["__scopeId", "data-v-a4a4c398"]]);
31065
+ const TopoGraph = /* @__PURE__ */ _export_sfc(_sfc_main$2, [["__scopeId", "data-v-0736fa17"]]);
30320
31066
  const _hoisted_1$1 = ["data-theme"];
30321
31067
  const _hoisted_2$1 = { class: "panel-head" };
30322
31068
  const _hoisted_3$1 = {
@@ -30689,7 +31435,7 @@ const _sfc_main = {
30689
31435
  }
30690
31436
  };
30691
31437
  const DistanceTop10 = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-438b6017"]]);
30692
- const VERSION = "0.1.10";
31438
+ const VERSION = "0.1.12";
30693
31439
  const DESCRIPTION = "配电台区单线图拓扑成图引擎";
30694
31440
  exports.DESCRIPTION = DESCRIPTION;
30695
31441
  exports.DistanceTop10 = DistanceTop10;