diff --git a/src/components/flow/MaterialNode.vue b/src/components/flow/MaterialNode.vue
index ca8d3cbf..1edf847d 100644
--- a/src/components/flow/MaterialNode.vue
+++ b/src/components/flow/MaterialNode.vue
@@ -10,7 +10,7 @@
-
-
-
-
diff --git a/src/components/scFlowEditor.vue b/src/components/xtFlowEditor.vue
similarity index 90%
rename from src/components/scFlowEditor.vue
rename to src/components/xtFlowEditor.vue
index 2c9a8afc..4c871628 100644
--- a/src/components/scFlowEditor.vue
+++ b/src/components/xtFlowEditor.vue
@@ -15,7 +15,7 @@
- 从左侧拖入原料作为起点 → 从节点圆点拉线到空白处生成下一道工序;单击线条编辑,右键线条删工序 / 右键节点移除物料
+ 从左侧拖入原料作为起点 → 节点间拉线新建工序,拉到空白处自动生成半成品,连到产品节点收尾;单击线条编辑,右键线条删工序 / 右键节点移除物料
当前状态不可编辑(仅创建中的工艺可编辑)
@@ -72,7 +72,7 @@
@edge-context-menu="onEdgeContextMenu"
@node-context-menu="onNodeContextMenu"
>
-
+
@@ -114,11 +114,14 @@ import "@vue-flow/controls/dist/style.css";
let flowSeq = 0;
export default {
- name: "scFlowEditor",
+ name: "xtFlowEditor",
components: { VueFlow, Background, Controls, routeForm, MaterialNode },
props: {
// 工艺包 id
routepack: { type: String, default: "" },
+ // 工艺包产品:常驻画布作为终点节点,可从其他节点拉线连入
+ productId: { type: [String, Number], default: "" },
+ productName: { type: String, default: "" },
// 是否只读(非“创建中”状态)
readonly: { type: Boolean, default: false },
},
@@ -163,6 +166,9 @@ export default {
routepack() {
this.reload();
},
+ productId() {
+ if (this.routepack) this.reload();
+ },
},
mounted() {
this.loadMaterials();
@@ -198,7 +204,15 @@ export default {
});
},
- /** 节点类型:起点原料 / 终点成品 / 中间半成品 */
+ /** 物料真实类型 → 节点配色:10成品 30原料 其余半成品 */
+ kindOfType(t) {
+ const s = String(t);
+ if (s === "10") return "final";
+ if (s === "30") return "raw";
+ return "mid";
+ },
+
+ /** 拓扑推断兜底(后端未返回物料类型时用):起点原料 / 终点成品 / 中间半成品 */
kindOf(id, targetIds) {
const isSrc = this.routes.some((r) => String(r.material_in) === id);
const isTgt = targetIds.has(id);
@@ -210,6 +224,7 @@ export default {
/** 由 routes 构建 物料节点 + 工序边,并用 dagre 自动布局;合并未落库的种子节点 */
buildGraph() {
const matMap = {};
+ const matKind = {};
const targetIds = new Set();
const edges = [];
@@ -219,6 +234,8 @@ export default {
const tid = String(r.material_out);
if (!matMap[sid]) matMap[sid] = r.material_in_name || sid;
if (!matMap[tid]) matMap[tid] = r.material_out_name || tid;
+ if (r.material_in_type != null) matKind[sid] = this.kindOfType(r.material_in_type);
+ if (r.material_out_type != null) matKind[tid] = this.kindOfType(r.material_out_type);
targetIds.add(tid);
const locked = !!r.from_route;
@@ -245,12 +262,24 @@ export default {
const routeNodes = Object.keys(matMap).map((id) => ({
id,
type: "material",
- data: { label: matMap[id], kind: this.kindOf(id, targetIds) },
+ data: { label: matMap[id], kind: matKind[id] || this.kindOf(id, targetIds) },
position: { x: 0, y: 0 },
}));
+ // 工艺包产品常驻画布作为终点节点(尚未被任何工序引用时也显示)
+ const pid = this.productId ? String(this.productId) : "";
+ if (pid && !matMap[pid]) {
+ routeNodes.push({
+ id: pid,
+ type: "material",
+ data: { label: this.productName || "产品", kind: "final" },
+ position: { x: 0, y: 0 },
+ });
+ }
+
// 已落库的物料从种子集合中剔除,其余种子保留在画布上
const routeNodeIds = new Set(Object.keys(matMap));
+ if (pid) routeNodeIds.add(pid);
this.seedNodes = this.seedNodes.filter((s) => !routeNodeIds.has(s.id));
this.edges = edges;
@@ -293,14 +322,14 @@ export default {
/* ============ 左侧面板拖拽入画布 ============ */
onDragStart(evt, mat) {
evt.dataTransfer.setData(
- "application/scflow",
- JSON.stringify({ id: mat.id, name: mat.full_name })
+ "application/xtflow",
+ JSON.stringify({ id: mat.id, name: mat.full_name, type: this.matType })
);
evt.dataTransfer.effectAllowed = "move";
},
onDrop(evt) {
if (this.readonly) return;
- const raw = evt.dataTransfer.getData("application/scflow");
+ const raw = evt.dataTransfer.getData("application/xtflow");
if (!raw) return;
let mat;
try {
@@ -332,7 +361,7 @@ export default {
const node = {
id,
type: "material",
- data: { label: mat.name, kind: "raw" },
+ data: { label: mat.name, kind: this.kindOfType(mat.type) },
position,
};
this.seedNodes.push(node);
@@ -423,6 +452,10 @@ export default {
if (this.readonly) return;
const node = evt.node || evt;
const nid = node.id;
+ if (this.productId && nid === String(this.productId)) {
+ this.$message.warning("产品节点为工艺终点,不可移除");
+ return;
+ }
const inUse = this.routes.some(
(r) => String(r.material_in) === nid || String(r.material_out) === nid
);
diff --git a/src/components/xtFlowViewer.vue b/src/components/xtFlowViewer.vue
new file mode 100644
index 00000000..15119b9a
--- /dev/null
+++ b/src/components/xtFlowViewer.vue
@@ -0,0 +1,261 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/views/mtm/route.vue b/src/views/mtm/route.vue
index f733e2bd..fdb2f33d 100644
--- a/src/views/mtm/route.vue
+++ b/src/views/mtm/route.vue
@@ -17,7 +17,7 @@
-
+
@@ -125,7 +125,7 @@
diff --git a/src/views/mtm/routepack_form.vue b/src/views/mtm/routepack_form.vue
index 8c9d9936..0ea87702 100644
--- a/src/views/mtm/routepack_form.vue
+++ b/src/views/mtm/routepack_form.vue
@@ -94,12 +94,14 @@
-
+ >
@@ -195,10 +197,10 @@ import { defineAsyncComponent } from "vue";
import saveDialog from "./route_form.vue";
import ticketd_b from "@/views/wf/ticketd_b.vue";
// 异步加载:Vue Flow + dagre 单独分包,仅打开工艺编辑器时才下载,首屏零影响
-const scFlowEditor = defineAsyncComponent(() => import("@/components/scFlowEditor.vue"));
+const xtFlowEditor = defineAsyncComponent(() => import(/* webpackPrefetch: true */ "@/components/xtFlowEditor.vue"));
export default {
name: "routepack_form",
- components: { saveDialog, ticketd_b, scFlowEditor },
+ components: { saveDialog, ticketd_b, xtFlowEditor },
data() {
return {
active: 0,