From dea337b632a223f7f6272b21db85873a3d0209cd Mon Sep 17 00:00:00 2001 From: caoqianming Date: Thu, 23 Jul 2026 09:18:59 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E5=B7=A5=E8=89=BA=E7=94=BB=E5=B8=83scFlow?= =?UTF-8?q?=E6=9B=B4=E5=90=8DxtFlow;=E5=B7=A5=E8=89=BA=E5=8C=85=E4=BA=A7?= =?UTF-8?q?=E5=93=81=E5=B8=B8=E9=A9=BB=E7=94=BB=E5=B8=83=E4=B8=BA=E7=BB=88?= =?UTF-8?q?=E7=82=B9=E8=8A=82=E7=82=B9=E5=8F=AF=E7=9B=B4=E6=8E=A5=E8=BF=9E?= =?UTF-8?q?=E7=BA=BF;=E8=8A=82=E7=82=B9=E6=8C=89=E7=89=A9=E6=96=99?= =?UTF-8?q?=E7=9C=9F=E5=AE=9E=E7=B1=BB=E5=9E=8B=E7=9D=80=E8=89=B2;fix:?= =?UTF-8?q?=E6=8B=96=E7=BA=BF=E6=96=B0=E5=A2=9EsetData=E8=A6=86=E7=9B=96pa?= =?UTF-8?q?rams=5Fjson=E8=87=B4=E6=8E=92=E4=B8=80=E6=AC=A1=E6=A3=92?= =?UTF-8?q?=E8=A1=A8=E5=8D=95=E6=8A=A5=E9=94=99;perf:vue-flow=E5=88=86?= =?UTF-8?q?=E5=8C=85prefetch=E9=A2=84=E5=8F=96+=E6=9F=A5=E7=9C=8B=E5=99=A8?= =?UTF-8?q?=E5=90=88=E5=B9=B6=E9=87=8D=E5=A4=8D=E5=B8=83=E5=B1=80;style:ro?= =?UTF-8?q?ute=5Fshow=E6=9F=A5=E7=9C=8B=E9=A1=B5=E5=8D=A1=E7=89=87?= =?UTF-8?q?=E5=8C=96=E7=B4=A7=E5=87=91=E5=B8=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- src/components/flow/MaterialNode.vue | 2 +- src/components/scFlowViewer.vue | 147 ---------- .../{scFlowEditor.vue => xtFlowEditor.vue} | 51 +++- src/components/xtFlowViewer.vue | 261 ++++++++++++++++++ src/views/mtm/route.vue | 6 +- src/views/mtm/route_form.vue | 10 +- src/views/mtm/route_show.vue | 225 ++++++++++----- src/views/mtm/routepack_form.vue | 10 +- 8 files changed, 477 insertions(+), 235 deletions(-) delete mode 100644 src/components/scFlowViewer.vue rename src/components/{scFlowEditor.vue => xtFlowEditor.vue} (90%) create mode 100644 src/components/xtFlowViewer.vue 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,