From e50e09bcaf9dfded839a300924bbece401bee15b Mon Sep 17 00:00:00 2001 From: caoqianming Date: Wed, 22 Jul 2026 13:56:20 +0800 Subject: [PATCH 1/7] =?UTF-8?q?feat:=E5=B7=A5=E8=89=BA=E8=B7=AF=E7=BA=BF?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=8F=AF=E8=A7=86=E5=8C=96=E6=B5=81=E7=A8=8B?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E5=99=A8scFlowEditor(Vue=20Flow+dagre?= =?UTF-8?q?=E5=BC=82=E6=AD=A5=E5=88=86=E5=8C=85);=20=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E5=B7=A5=E8=89=BA=E6=8A=BD=E5=B1=89=E5=8E=8B=E7=BC=A9=E5=B8=83?= =?UTF-8?q?=E5=B1=80=E7=95=99=E7=99=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- package.json | 4 + src/api/model/mtm.js | 6 + src/components/scFlowEditor.vue | 749 +++++++++++++++++++++++++++++++ src/views/mtm/routepack_form.vue | 45 +- 4 files changed, 783 insertions(+), 21 deletions(-) create mode 100644 src/components/scFlowEditor.vue diff --git a/package.json b/package.json index dada463e..bbb6e66b 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,9 @@ "@element-plus/icons-vue": "^2.3.2", "@kjgl77/datav-vue3": "^1.7.1", "@tinymce/tinymce-vue": "5.0.0", + "@vue-flow/background": "^1.3.2", + "@vue-flow/controls": "^1.1.3", + "@vue-flow/core": "^1.48.2", "animate.css": "^4.1.1", "axios": "^1.18.1", "babylonjs": "^6.46.0", @@ -24,6 +27,7 @@ "crypto-browserify": "^3.12.0", "crypto-js": "^4.2.0", "d3": "^7.6.1", + "dagre": "^0.8.5", "dagre-d3": "^0.6.4", "dhtmlx-gantt": "^8.0.6", "echarts": "^5.5.1", diff --git a/src/api/model/mtm.js b/src/api/model/mtm.js index 9cabf4f2..376aa841 100644 --- a/src/api/model/mtm.js +++ b/src/api/model/mtm.js @@ -516,6 +516,12 @@ export default { return await http.get(`${config.API_URL}/mtm/routepack/${id}/final_materials/`); }, }, + validate: { + name: "校验工艺图", + req: async function (id) { + return await http.get(`${config.API_URL}/mtm/routepack/${id}/validate/`); + }, + }, update: { name: "更新", req: async function (id, data) { diff --git a/src/components/scFlowEditor.vue b/src/components/scFlowEditor.vue new file mode 100644 index 00000000..668467ef --- /dev/null +++ b/src/components/scFlowEditor.vue @@ -0,0 +1,749 @@ + + + + + + diff --git a/src/views/mtm/routepack_form.vue b/src/views/mtm/routepack_form.vue index ffce585a..8c9d9936 100644 --- a/src/views/mtm/routepack_form.vue +++ b/src/views/mtm/routepack_form.vue @@ -7,7 +7,7 @@ @closed="$emit('closed')" > - + - -
工艺路线流程图
- - + + - + - -
+ +
- + - + 上一步 @@ -194,11 +191,14 @@ + + + + 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" > - + @@ -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 @@ - - - - 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, From d3ed63639302dafe7fe3c62f8b61d2c4d76197b3 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Thu, 23 Jul 2026 09:21:19 +0800 Subject: [PATCH 7/7] release: 3.1.2026072309 Co-Authored-By: Claude Fable 5 --- changelog.md | 20 ++++++++++++++++++++ src/config/index.js | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 81fa05e5..c34a2db3 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,23 @@ +## 3.1.2026072309 + +- feat: 新增功能 + - 工艺画布scFlow更名xtFlow;工艺包产品常驻画布为终点节点可直接连线;节点按物料真实类型着色;fix:拖线新增setData覆盖params_json致排一次棒表单报错;perf:vue-flow分包prefetch预取+查看器合并重复布局;style:route_show查看页卡片化紧凑布局 [caoqianming] + - 物料表单新增入库检验方式,入库明细未检免检物料显示免检标识;fix:其他配置页提交未实现导致保存无效 [caoqianming] + - 工艺路线新增可视化流程编辑器scFlowEditor(Vue Flow+dagre异步分包); 编辑工艺抽屉压缩布局留白 [caoqianming] + - 禅道444扭转工序,快速报工,扫码框后面加数字识别功能 [shijing] + - 禅道446深加工统计里加工装tab页面 [shijing] + - 销售发货批次选择隐藏可发数量为0的批次 [caoqianming] + - 合格B类字段文案改为记为合格更准确表达 [caoqianming] +- fix: 问题修复 + - 抽屉内打开的el-dialog统一加append-to-body(24文件28处),避免被pages.scss抽屉工作流表单样式误伤成卡片/灰底/底部留白 [caoqianming] + - 抽屉内el-main/el-aside恢复nopadding约定优先级; route_form弹窗加append-to-body避免被抽屉工作流表单样式误伤成卡片 [caoqianming] + - 分检加人员筛选不生效修改 [shijing] + - table下的tr补上tbody包裹,消除vue3.5模板编译的HTML嵌套规范警告 [caoqianming] + - scScanner内置扫码枪输入框改为opt-in(默认隐藏),消除与调用页自有输入框重复 [caoqianming] + - 禅道452综合查询界面棒数据未显示发货数 [shijing] + - 禅道452综合查询界面棒数据未显示发货数 [shijing] + - 禅道452综合查询界面棒数据未显示发货数 [shijing] + - 出入库记录及明细表单新增时清空,避免上次保存数据残留导致"物料找不到" [caoqianming] ## 3.1.2026071608 - feat: 新增功能 diff --git a/src/config/index.js b/src/config/index.js index cc04592b..c4b3e1cc 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -6,7 +6,7 @@ const DEFAULT_CONFIG = { DASHBOARD_URL: "/dashboard", //版本号 - APP_VER: "3.1.2026071608", + APP_VER: "3.1.2026072309", //内核版本号 CORE_VER: "1.6.9",