+
{
+ clearTimeout(this.resizeTimer);
+ this.resizeTimer = setTimeout(() => {
+ if (this.hasFitted) this.fitToWidth(false);
+ }, 80);
+ });
+ this.resizeObserver.observe(this.$refs.canvas);
+ }
+ },
+ beforeUnmount() {
+ if (this.resizeObserver) this.resizeObserver.disconnect();
+ clearTimeout(this.resizeTimer);
},
methods: {
/** nodes/edges 通常被先后赋值,用微任务合并成一次 rebuild,避免双次 dagre 布局 */
@@ -102,11 +120,15 @@ export default {
rebuild() {
const sourceIds = new Set();
const targetIds = new Set();
+ const parallelEdgeCount = {};
const rawEdges = (this.edges || [])
.filter((e) => e.source != null && e.target != null)
.map((e) => {
const sid = String(e.source);
const tid = String(e.target);
+ const edgeKey = sid + "->" + tid;
+ const edgeIndex = parallelEdgeCount[edgeKey] || 0;
+ parallelEdgeCount[edgeKey] = edgeIndex + 1;
sourceIds.add(sid);
targetIds.add(tid);
return {
@@ -114,9 +136,11 @@ export default {
source: sid,
target: tid,
label: e.label || "",
- type: "smoothstep",
+ // 贝塞尔线会在分叉后立即分离;同一对节点有多道工序时用不同曲率错开。
+ type: "default",
+ pathOptions: { curvature: 0.25 + edgeIndex * 0.12 },
markerEnd: "arrowclosed",
- style: { stroke: "#409eff", strokeWidth: 2 },
+ style: { stroke: "#409eff", strokeWidth: 2.2 },
labelStyle: { fill: "#5b6370", fontSize: "12px", fontWeight: 500 },
labelBgStyle: { fill: "#ffffff", stroke: "#e4e7ed", strokeWidth: 1 },
labelBgPadding: [7, 4],
@@ -137,19 +161,47 @@ export default {
const nodeIds = new Set(vNodes.map((n) => n.id));
const vEdges = rawEdges.filter((e) => nodeIds.has(e.source) && nodeIds.has(e.target));
+ this.renderedNodes = layoutGraph(vNodes, vEdges, {
+ rankdir: this.rankdir,
+ nodesep: 58,
+ edgesep: 32,
+ ranksep: 96,
+ marginx: 32,
+ marginy: 32,
+ });
this.setEdges([]);
- this.setNodes(layoutGraph(vNodes, vEdges, { rankdir: this.rankdir }));
+ this.setNodes(this.renderedNodes);
this.setEdges(vEdges);
- // 首次挂载交给 nodes-initialized(尺寸测量完成)再 fit;数据更新时尺寸已就绪,直接 fit
- if (this.hasFitted) this.doFit();
+ // 首次挂载交给 nodes-initialized(尺寸测量完成)再定位;数据更新时直接重新适配宽度
+ if (this.hasFitted) this.fitToWidth();
},
onNodesInit() {
- this.doFit();
+ this.fitToWidth();
},
- doFit() {
+ /**
+ * 只按宽度缩放,长流程不再为了塞进高度而缩成缩略图。
+ * 图从画布顶部开始展示,用户可拖动画布继续向下查看。
+ */
+ fitToWidth(animated = true) {
this.$nextTick(() => {
try {
- this.fitView({ padding: 0.2, maxZoom: 1, duration: this.hasFitted ? 200 : 0 });
+ const canvas = this.$refs.canvas;
+ if (!canvas || !this.renderedNodes.length) return;
+ const xs = this.renderedNodes.map((n) => n.position.x);
+ const ys = this.renderedNodes.map((n) => n.position.y);
+ const minX = Math.min(...xs);
+ const maxX = Math.max(...xs) + NODE_W;
+ const minY = Math.min(...ys);
+ const paddingX = 48;
+ const paddingTop = 36;
+ const graphWidth = Math.max(maxX - minX, NODE_W);
+ const zoom = Math.min(1, Math.max(0.2, (canvas.clientWidth - paddingX * 2) / graphWidth));
+ const x = (canvas.clientWidth - graphWidth * zoom) / 2 - minX * zoom;
+ const y = paddingTop - minY * zoom;
+ this.setViewport(
+ { x, y, zoom },
+ { duration: animated && this.hasFitted ? 200 : 0 }
+ );
this.hasFitted = true;
} catch (e) {
/* noop */