diff --git a/src/components/flow/MaterialNode.vue b/src/components/flow/MaterialNode.vue
new file mode 100644
index 00000000..ca8d3cbf
--- /dev/null
+++ b/src/components/flow/MaterialNode.vue
@@ -0,0 +1,106 @@
+
+
+
+
{{ kindLabel }}
+
{{ data.label }}
+
+
+
+
+
+
+
+
+
diff --git a/src/components/flow/dagreLayout.js b/src/components/flow/dagreLayout.js
new file mode 100644
index 00000000..a7275036
--- /dev/null
+++ b/src/components/flow/dagreLayout.js
@@ -0,0 +1,24 @@
+import dagre from "dagre";
+
+export const NODE_W = 150;
+export const NODE_H = 54;
+
+/**
+ * dagre 自动布局,返回带 position 的 nodes
+ * @param {Array} nodes Vue Flow 节点(id 必须为字符串)
+ * @param {Array} edges Vue Flow 边
+ * @param {Object} opts { rankdir: TB|BT|LR|RL, nodeW, nodeH }
+ */
+export function layoutGraph(nodes, edges, opts = {}) {
+ const { rankdir = "TB", nodeW = NODE_W, nodeH = NODE_H } = opts;
+ const g = new dagre.graphlib.Graph();
+ g.setGraph({ rankdir, nodesep: 30, ranksep: 56, marginx: 20, marginy: 20 });
+ g.setDefaultEdgeLabel(() => ({}));
+ nodes.forEach((n) => g.setNode(n.id, { width: nodeW, height: nodeH }));
+ edges.forEach((e) => g.setEdge(e.source, e.target));
+ dagre.layout(g);
+ return nodes.map((n) => {
+ const gn = g.node(n.id);
+ return { ...n, position: { x: gn.x - nodeW / 2, y: gn.y - nodeH / 2 } };
+ });
+}
diff --git a/src/components/scFlowEditor.vue b/src/components/scFlowEditor.vue
index 668467ef..2c9a8afc 100644
--- a/src/components/scFlowEditor.vue
+++ b/src/components/scFlowEditor.vue
@@ -72,14 +72,9 @@
@edge-context-menu="onEdgeContextMenu"
@node-context-menu="onNodeContextMenu"
>
-
+
-
-
-
{{ kindLabel(data.kind) }}
-
{{ data.label }}
-
-
+
@@ -105,24 +100,22 @@
+
+
+
+
diff --git a/src/views/mtm/route.vue b/src/views/mtm/route.vue
index b6fe9275..f733e2bd 100644
--- a/src/views/mtm/route.vue
+++ b/src/views/mtm/route.vue
@@ -17,14 +17,7 @@
-
-
+
@@ -130,6 +123,9 @@