feat:工艺路线新增可视化流程编辑器scFlowEditor(Vue Flow+dagre异步分包); 编辑工艺抽屉压缩布局留白

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
caoqianming 2026-07-22 13:56:20 +08:00
parent 7fccfc9869
commit e50e09bcaf
4 changed files with 783 additions and 21 deletions

View File

@ -10,6 +10,9 @@
"@element-plus/icons-vue": "^2.3.2", "@element-plus/icons-vue": "^2.3.2",
"@kjgl77/datav-vue3": "^1.7.1", "@kjgl77/datav-vue3": "^1.7.1",
"@tinymce/tinymce-vue": "5.0.0", "@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", "animate.css": "^4.1.1",
"axios": "^1.18.1", "axios": "^1.18.1",
"babylonjs": "^6.46.0", "babylonjs": "^6.46.0",
@ -24,6 +27,7 @@
"crypto-browserify": "^3.12.0", "crypto-browserify": "^3.12.0",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"d3": "^7.6.1", "d3": "^7.6.1",
"dagre": "^0.8.5",
"dagre-d3": "^0.6.4", "dagre-d3": "^0.6.4",
"dhtmlx-gantt": "^8.0.6", "dhtmlx-gantt": "^8.0.6",
"echarts": "^5.5.1", "echarts": "^5.5.1",

View File

@ -516,6 +516,12 @@ export default {
return await http.get(`${config.API_URL}/mtm/routepack/${id}/final_materials/`); 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: { update: {
name: "更新", name: "更新",
req: async function (id, data) { req: async function (id, data) {

View File

@ -0,0 +1,749 @@
<template>
<div class="flow-editor" v-loading="loading">
<!-- 顶部工具栏 -->
<div class="flow-toolbar">
<div class="flow-toolbar__left">
<el-button v-if="!readonly" type="primary" icon="el-icon-plus" size="small" round @click="addStep">新增工序</el-button>
<el-button icon="el-icon-magic-stick" size="small" round @click="relayout">自动布局</el-button>
<el-button icon="el-icon-full-screen" size="small" round @click="doFit">适应画布</el-button>
<el-button icon="el-icon-circle-check" size="small" round @click="doValidate" :loading="validating">校验</el-button>
</div>
<div class="flow-legend">
<span class="lg lg--raw">原料</span>
<span class="lg lg--mid">半成品</span>
<span class="lg lg--final">成品</span>
</div>
</div>
<div class="flow-tip" v-if="!readonly">
从左侧拖入原料作为起点 从节点圆点拉线到空白处生成下一道工序单击线条编辑右键线条删工序 / 右键节点移除物料
</div>
<div class="flow-tip flow-tip--ro" v-else>当前状态不可编辑仅创建中的工艺可编辑</div>
<div class="flow-body">
<!-- 左侧物料面板拖入画布作为节点 -->
<div class="flow-palette" v-if="!readonly">
<div class="palette-head">
<div class="palette-title">物料库</div>
<el-select v-model="matType" size="small" @change="loadMaterials" style="width: 100%">
<el-option label="主要原料" value="30"></el-option>
<el-option label="成品" value="10"></el-option>
<el-option label="半成品" value="20"></el-option>
</el-select>
<el-input
v-model="matKeyword"
size="small"
clearable
prefix-icon="el-icon-search"
placeholder="搜索物料"
style="margin-top: 6px"
></el-input>
</div>
<div class="palette-list" v-loading="matLoading">
<div
v-for="m in filteredMaterials"
:key="m.id"
class="palette-item"
:draggable="true"
@dragstart="onDragStart($event, m)"
:title="m.full_name"
>
<i class="el-icon-rank palette-item__grip"></i>
<span class="palette-item__txt">{{ m.full_name }}</span>
</div>
<div v-if="!matLoading && filteredMaterials.length === 0" class="palette-empty">无匹配物料</div>
</div>
</div>
<!-- 画布 -->
<div class="flow-canvas" @drop="onDrop" @dragover.prevent>
<VueFlow
:id="flowId"
:default-viewport="{ zoom: 0.9 }"
:min-zoom="0.2"
:max-zoom="2"
:nodes-draggable="true"
:nodes-connectable="!readonly"
:elements-selectable="true"
@nodes-initialized="onNodesInit"
@connect-start="onConnectStart"
@connect="onConnect"
@connect-end="onConnectEnd"
@edge-click="onEdgeClick"
@edge-context-menu="onEdgeContextMenu"
@node-context-menu="onNodeContextMenu"
>
<!-- 自定义物料节点 -->
<template #node-material="{ data }">
<div class="mnode" :class="'mnode--' + (data.kind || 'mid')">
<Handle type="target" :position="Position.Top" class="mnode__handle" />
<span class="mnode__tag">{{ kindLabel(data.kind) }}</span>
<div class="mnode__name" :title="data.label">{{ data.label }}</div>
<Handle type="source" :position="Position.Bottom" class="mnode__handle" />
</div>
</template>
<Background :gap="18" pattern-color="#e6e9ef" />
<Controls :show-interactive="false" />
</VueFlow>
<div v-if="!loading && nodes.length === 0" class="flow-empty">
<i class="el-icon-position flow-empty__icon"></i>
<div>从左侧把原料拖到这里开始搭建工艺路线</div>
</div>
</div>
</div>
<!-- 复用现有单步表单新增/编辑一条 Route -->
<route-form
v-if="dialogVisible"
ref="routeForm"
:count="count"
:routepack="routepack"
@success="onFormSuccess"
@closed="dialogVisible = false"
></route-form>
</div>
</template>
<script>
import { VueFlow, useVueFlow, Handle, Position } from "@vue-flow/core";
import { Background } from "@vue-flow/background";
import { Controls } from "@vue-flow/controls";
import dagre from "dagre";
import routeForm from "@/views/mtm/route_form.vue";
import "@vue-flow/core/dist/style.css";
import "@vue-flow/core/dist/theme-default.css";
import "@vue-flow/controls/dist/style.css";
const NODE_W = 150;
const NODE_H = 54;
// flowId Vue Flow store id
let flowSeq = 0;
export default {
name: "scFlowEditor",
components: { VueFlow, Background, Controls, routeForm, Handle },
props: {
// id
routepack: { type: String, default: "" },
//
readonly: { type: Boolean, default: false },
},
emits: ["changed"],
setup() {
const flowId = "route-flow-editor-" + ++flowSeq;
const { fitView, screenToFlowCoordinate, project, setNodes, setEdges, addNodes, removeNodes } =
useVueFlow(flowId);
return { flowId, Position, fitView, screenToFlowCoordinate, project, setNodes, setEdges, addNodes, removeNodes };
},
data() {
return {
loading: false,
validating: false,
routes: [],
nodes: [],
edges: [],
count: 0,
dialogVisible: false,
//
matType: "30",
matKeyword: "",
materials: [],
matLoading: false,
// Route
seedNodes: [],
// 线
connectSource: null,
connectMade: false,
//
hasFitted: false,
};
},
computed: {
filteredMaterials() {
const kw = (this.matKeyword || "").trim().toLowerCase();
if (!kw) return this.materials;
return this.materials.filter((m) => (m.full_name || "").toLowerCase().includes(kw));
},
},
watch: {
routepack() {
this.reload();
},
},
mounted() {
this.loadMaterials();
if (this.routepack) this.reload();
},
methods: {
kindLabel(kind) {
return { raw: "原料", final: "成品", mid: "半成品" }[kind] || "半成品";
},
/** 加载左侧物料面板列表 */
loadMaterials() {
this.matLoading = true;
this.$API.mtm.material.list
.req({ page: 0, type__in: this.matType, is_hidden: false, query: "{full_name,id}" })
.then((res) => {
this.materials = Array.isArray(res) ? res : res.results || [];
})
.finally(() => {
this.matLoading = false;
});
},
/** 重新拉取路线并重建图 */
reload() {
if (!this.routepack) return;
this.loading = true;
this.$API.mtm.route.list
.req({ page: 0, routepack: this.routepack })
.then((res) => {
this.routes = Array.isArray(res) ? res : res.results || [];
this.count = this.routes.length;
this.buildGraph();
})
.finally(() => {
this.loading = false;
});
},
/** 节点类型:起点原料 / 终点成品 / 中间半成品 */
kindOf(id, targetIds) {
const isSrc = this.routes.some((r) => String(r.material_in) === id);
const isTgt = targetIds.has(id);
if (isTgt && !isSrc) return "final";
if (!isTgt && isSrc) return "raw";
return "mid";
},
/** 由 routes 构建 物料节点 + 工序边,并用 dagre 自动布局;合并未落库的种子节点 */
buildGraph() {
const matMap = {};
const targetIds = new Set();
const edges = [];
this.routes.forEach((r) => {
if (r.material_in == null || r.material_out == null) return;
const sid = String(r.material_in);
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;
targetIds.add(tid);
const locked = !!r.from_route;
edges.push({
id: String(r.id),
source: sid,
target: tid,
label: (r.process_name || "") + (locked ? " 🔒" : ""),
data: { route: r, locked },
type: "smoothstep",
markerEnd: "arrowclosed",
style: {
stroke: locked ? "#c0c4cc" : "#409eff",
strokeWidth: locked ? 1.5 : 2,
strokeDasharray: locked ? "5 4" : undefined,
},
labelStyle: { fill: "#5b6370", fontSize: "12px", fontWeight: 500 },
labelBgStyle: { fill: "#ffffff", stroke: "#e4e7ed", strokeWidth: 1 },
labelBgPadding: [7, 4],
labelBgBorderRadius: 6,
});
});
const routeNodes = Object.keys(matMap).map((id) => ({
id,
type: "material",
data: { label: matMap[id], kind: this.kindOf(id, targetIds) },
position: { x: 0, y: 0 },
}));
//
const routeNodeIds = new Set(Object.keys(matMap));
this.seedNodes = this.seedNodes.filter((s) => !routeNodeIds.has(s.id));
this.edges = edges;
this.nodes = this.doLayout(routeNodes.concat(this.seedNodes), edges);
this.syncStore();
},
/** 把本地 nodes/edges 命令式同步到 Vue Flow store先节点后边避免边找不到端点 */
syncStore() {
this.setEdges([]);
this.setNodes(this.nodes);
this.setEdges(this.edges);
},
/** dagre 布局,返回带 position 的 nodes自上而下 TB */
doLayout(nodes, edges, rankdir = "TB") {
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: NODE_W, height: NODE_H }));
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 - NODE_W / 2, y: gn.y - NODE_H / 2 } };
});
},
/** 重新布局并适应画布 */
relayout() {
this.nodes = this.doLayout([...this.nodes], this.edges);
this.setNodes(this.nodes);
this.doFit();
},
doFit() {
this.$nextTick(() => {
try {
this.fitView({ padding: 0.25, maxZoom: 1, duration: 300 });
} catch (e) {
/* noop */
}
});
},
/** VueFlow 节点尺寸测量完成:仅首次自动适配一次 */
onNodesInit() {
if (this.hasFitted) return;
if (this.nodes.length === 0) return;
this.hasFitted = true;
this.doFit();
},
/* ============ 左侧面板拖拽入画布 ============ */
onDragStart(evt, mat) {
evt.dataTransfer.setData(
"application/scflow",
JSON.stringify({ id: mat.id, name: mat.full_name })
);
evt.dataTransfer.effectAllowed = "move";
},
onDrop(evt) {
if (this.readonly) return;
const raw = evt.dataTransfer.getData("application/scflow");
if (!raw) return;
let mat;
try {
mat = JSON.parse(raw);
} catch (e) {
return;
}
const pos = this.toFlowPos(evt.clientX, evt.clientY);
this.addSeed(mat, pos);
},
toFlowPos(clientX, clientY) {
if (typeof this.screenToFlowCoordinate === "function") {
return this.screenToFlowCoordinate({ x: clientX, y: clientY });
}
if (typeof this.project === "function") {
return this.project({ x: clientX, y: clientY });
}
return { x: clientX, y: clientY };
},
addSeed(mat, position) {
const id = String(mat.id);
if (this.nodes.some((n) => n.id === id)) {
const inUse = this.routes.some(
(r) => String(r.material_in) === id || String(r.material_out) === id
);
this.$message.info(inUse ? "该物料已在工序中使用" : "该物料已在画布中(右键节点可移除)");
return;
}
const node = {
id,
type: "material",
data: { label: mat.name, kind: "raw" },
position,
};
this.seedNodes.push(node);
this.nodes = [...this.nodes, node];
// store Handle dimensions
this.addNodes([node]);
},
/* ============ 连线手势 ============ */
onConnectStart(params) {
this.connectSource = (params && (params.nodeId || params.source)) || null;
this.connectMade = false;
},
/** 连到已有节点:新建一条 Route预填输入/输出物料 */
onConnect(conn) {
this.connectMade = true;
if (this.readonly) return;
const src = this.nodes.find((n) => n.id === conn.source);
const tgt = this.nodes.find((n) => n.id === conn.target);
// id Number()
this.openForm("add", {
material_in: conn.source,
material_in_name: src ? src.data.label : "",
material_out: conn.target,
material_out_name: tgt ? tgt.data.label : "",
});
},
/** 拉线到空白处:新建工序,输出物料留空由后端自动生成下一节点 */
onConnectEnd() {
const src = this.connectSource;
const made = this.connectMade;
this.connectSource = null;
this.connectMade = false;
if (this.readonly || made || !src) return;
const node = this.nodes.find((n) => n.id === src);
// id
this.openForm("add", {
material_in: src,
material_in_name: node ? node.data.label : "",
});
},
/** 单击边 => 编辑对应 Route复用 route_form */
onEdgeClick(evt) {
const edge = evt.edge || evt;
const route = edge.data && edge.data.route;
if (!route) return;
if (this.readonly) {
this.$message.info("当前状态不可编辑");
return;
}
if (edge.data.locked) {
this.$message.warning("该工序引用了其他步骤,无法编辑");
return;
}
this.openForm("edit", route);
},
/** 右键边 => 删除对应 Route */
onEdgeContextMenu(evt) {
const oe = evt.event || evt;
if (oe && oe.preventDefault) oe.preventDefault();
if (this.readonly) return;
const edge = evt.edge || evt;
const route = edge.data && edge.data.route;
if (!route) return;
if (edge.data.locked) {
this.$message.warning("该工序被其他步骤引用,无法删除");
return;
}
this.$confirm(`确定删除工序「${route.process_name || ""}」吗?`, "提示", {
type: "warning",
})
.then(() => {
this.$API.mtm.route.delete.req(route.id).then(() => {
this.$message.success("删除成功");
this.reload();
this.$emit("changed");
});
})
.catch(() => {});
},
/** 右键节点 => 移除未落库的种子节点(已用于工序的物料需删对应工序) */
onNodeContextMenu(evt) {
const oe = evt.event || evt;
if (oe && oe.preventDefault) oe.preventDefault();
if (this.readonly) return;
const node = evt.node || evt;
const nid = node.id;
const inUse = this.routes.some(
(r) => String(r.material_in) === nid || String(r.material_out) === nid
);
if (inUse) {
this.$message.warning("该物料已用于工序,请删除对应工序");
return;
}
const label = (node.data && node.data.label) || "";
this.$confirm(`从画布移除「${label}」?`, "提示", { type: "warning" })
.then(() => {
this.seedNodes = this.seedNodes.filter((s) => s.id !== nid);
this.nodes = this.nodes.filter((n) => n.id !== nid);
this.removeNodes([nid]);
})
.catch(() => {});
},
addStep() {
this.openForm("add", null);
},
/** 打开单步表单prefill 为 null 表示纯新增 */
openForm(mode, prefill) {
this.dialogVisible = true;
this.$nextTick(() => {
const inst = this.$refs.routeForm.open(mode);
if (prefill) inst.setData(prefill);
});
},
onFormSuccess() {
this.dialogVisible = false;
this.reload();
this.$emit("changed");
},
/** 调后端 validate 接口,实时反馈 DAG 是否合法 */
doValidate() {
if (!this.routepack) return;
this.validating = true;
this.$API.mtm.routepack.validate
.req(this.routepack)
.then((res) => {
if (res && res.valid) {
this.$message.success("工艺图校验通过");
} else {
this.$message.error("校验未通过:" + (res ? res.error : "未知错误"));
}
})
.finally(() => {
this.validating = false;
});
},
},
};
</script>
<style scoped>
.flow-editor {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
min-height: 480px;
}
.flow-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 4px 8px;
background: #f7f9fc;
border: 1px solid #eef1f6;
border-radius: 8px;
flex-wrap: wrap;
}
.flow-toolbar__left {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.flow-legend {
display: flex;
align-items: center;
gap: 12px;
}
.lg {
position: relative;
padding-left: 16px;
font-size: 12px;
color: #606266;
}
.lg::before {
content: "";
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 10px;
height: 10px;
border-radius: 3px;
border: 1px solid;
}
.lg--raw::before {
background: #fdf6ec;
border-color: #e6a23c;
}
.lg--mid::before {
background: #ecf5ff;
border-color: #409eff;
}
.lg--final::before {
background: #f0f9eb;
border-color: #67c23a;
}
.flow-tip {
color: #909399;
font-size: 12px;
padding: 3px 2px 4px;
}
.flow-tip--ro {
color: #e6a23c;
}
.flow-body {
display: flex;
flex: 1;
min-height: 420px;
gap: 6px;
}
.flow-palette {
width: 190px;
flex: 0 0 190px;
display: flex;
flex-direction: column;
border: 1px solid #eef1f6;
border-radius: 8px;
overflow: hidden;
background: #fff;
}
.palette-head {
padding: 8px;
border-bottom: 1px solid #f0f2f5;
background: #fafbfd;
}
.palette-title {
font-size: 13px;
font-weight: 600;
color: #303133;
margin-bottom: 8px;
}
.palette-list {
flex: 1;
overflow-y: auto;
padding: 8px;
}
.palette-item {
display: flex;
align-items: center;
gap: 6px;
padding: 7px 9px;
margin-bottom: 6px;
border: 1px solid #ebeef5;
border-radius: 6px;
background: #fff;
cursor: grab;
font-size: 12px;
color: #303133;
transition: all 0.15s;
}
.palette-item:hover {
border-color: #409eff;
background: #ecf5ff;
box-shadow: 0 2px 6px rgba(64, 158, 255, 0.15);
transform: translateY(-1px);
}
.palette-item:active {
cursor: grabbing;
}
.palette-item__grip {
color: #c0c4cc;
flex-shrink: 0;
}
.palette-item__txt {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.palette-empty {
color: #909399;
font-size: 12px;
text-align: center;
padding: 16px 0;
}
.flow-canvas {
position: relative;
flex: 1;
min-height: 420px;
border: 1px solid #eef1f6;
border-radius: 8px;
overflow: hidden;
background: #fbfcfe;
}
.flow-empty {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #a8abb2;
font-size: 13px;
text-align: center;
pointer-events: none;
}
.flow-empty__icon {
font-size: 34px;
color: #c8ccd4;
display: block;
margin-bottom: 10px;
}
/* ===== 自定义物料节点 ===== */
.mnode {
position: relative;
width: 150px;
padding: 6px 10px 8px;
border-radius: 8px;
border: 1px solid #dcdfe6;
background: #fff;
box-shadow: 0 2px 8px rgba(31, 45, 61, 0.08);
text-align: center;
transition: box-shadow 0.15s, transform 0.15s;
}
.mnode:hover {
box-shadow: 0 4px 14px rgba(31, 45, 61, 0.16);
transform: translateY(-1px);
}
.mnode__tag {
display: inline-block;
font-size: 11px;
line-height: 1;
padding: 2px 6px;
border-radius: 8px;
margin-bottom: 5px;
color: #fff;
}
.mnode__name {
font-size: 12px;
line-height: 1.3;
color: #303133;
word-break: break-all;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.mnode--raw {
border-color: #f0c78a;
background: #fffaf2;
}
.mnode--raw .mnode__tag {
background: #e6a23c;
}
.mnode--mid {
border-color: #a3d0ff;
background: #f5faff;
}
.mnode--mid .mnode__tag {
background: #409eff;
}
.mnode--final {
border-color: #a4da89;
background: #f6fcf1;
}
.mnode--final .mnode__tag {
background: #67c23a;
}
</style>
<style>
/* Vue Flow 内部元素(非 scoped */
.vue-flow__handle {
width: 9px;
height: 9px;
background: #fff;
border: 2px solid #409eff;
}
.vue-flow__handle:hover {
background: #409eff;
}
.vue-flow__node-material {
background: transparent;
border: none;
padding: 0;
}
.vue-flow__controls {
box-shadow: 0 2px 10px rgba(31, 45, 61, 0.12);
border-radius: 6px;
overflow: hidden;
}
</style>

View File

@ -7,7 +7,7 @@
@closed="$emit('closed')" @closed="$emit('closed')"
> >
<el-container> <el-container>
<el-header> <el-header style="height: auto; padding: 6px 20px 0">
<el-steps <el-steps
:active="active" :active="active"
style="width: 100%" style="width: 100%"
@ -93,30 +93,27 @@
<!--工序!--> <!--工序!-->
<el-main class="nopadding" v-if="active === 1"> <el-main class="nopadding" v-if="active === 1">
<el-container> <el-container>
<el-aside style="width: 50%;overflow: scroll;"> <el-aside class="nopadding" style="width: 55%;overflow: hidden;padding-right: 6px;">
<div style="font-weight: 600;color: #303133;font-size: 16px;padding: 10px 0;position: fixed;width: 30px;">工艺路线流程图</div> <scFlowEditor
<scDegra ref="flowEditor"
style="margin-top: 50px;" :routepack="routepack"
v-if="limitedWatch" :readonly="!!(form.state && form.state !== 10)"
ref="degraDialogs" @changed="handleEditorChanged"
:nodes="nodes" ></scFlowEditor>
:edges="edges"
:rankdir="'DL'"
>
</scDegra>
</el-aside> </el-aside>
<el-main style="width: 50%;overflow: scroll;"> <el-main class="nopadding" style="width: 45%;overflow: auto;">
<el-container> <el-container>
<el-header> <el-header style="height: auto; padding: 4px 0">
<div class="left-panel" style="margin: 10px"> <div class="left-panel">
<el-button <el-button
type="primary" type="primary"
icon="el-icon-plus" icon="el-icon-plus"
size="small"
@click="table_add" @click="table_add"
></el-button> ></el-button>
</div> </div>
</el-header> </el-header>
<el-main> <el-main class="nopadding">
<scTable <scTable
ref="tables" ref="tables"
:apiObj="apiObj" :apiObj="apiObj"
@ -176,7 +173,7 @@
</el-main> </el-main>
</el-container> </el-container>
</el-main> </el-main>
<el-footer v-if="active === 1" style="text-align: center"> <el-footer v-if="active === 1" style="height: 44px; display: flex; align-items: center; justify-content: center; gap: 8px;">
<el-button @click="handleLastStep" style="margin-right: 4px">上一步</el-button> <el-button @click="handleLastStep" style="margin-right: 4px">上一步</el-button>
<ticketd_b :workflow_key="'routepack'" :title="form.name" :t_id="form.id" :ticket_="form.ticket_" <ticketd_b :workflow_key="'routepack'" :title="form.name" :t_id="form.id" :ticket_="form.ticket_"
@success="$emit('closed')"></ticketd_b> @success="$emit('closed')"></ticketd_b>
@ -194,11 +191,14 @@
</el-drawer> </el-drawer>
</template> </template>
<script> <script>
import { defineAsyncComponent } from "vue";
import saveDialog from "./route_form.vue"; import saveDialog from "./route_form.vue";
import ticketd_b from "@/views/wf/ticketd_b.vue"; import ticketd_b from "@/views/wf/ticketd_b.vue";
// Vue Flow + dagre
const scFlowEditor = defineAsyncComponent(() => import("@/components/scFlowEditor.vue"));
export default { export default {
name: "routepack_form", name: "routepack_form",
components: { saveDialog, ticketd_b }, components: { saveDialog, ticketd_b, scFlowEditor },
data() { data() {
return { return {
active: 0, active: 0,
@ -369,7 +369,7 @@ export default {
let that = this; let that = this;
that.$API.mtm.route.delete.req(row.id).then((res) => { that.$API.mtm.route.delete.req(row.id).then((res) => {
that.$message.success("工序步骤删除成功"); that.$message.success("工序步骤删除成功");
that.getDEGdatas(); that.$refs.flowEditor && that.$refs.flowEditor.reload();
that.$refs.tables.refresh(); that.$refs.tables.refresh();
return res; return res;
}).catch((err) => { }).catch((err) => {
@ -378,10 +378,13 @@ export default {
}, },
// //
handleSaveSuccess() { handleSaveSuccess() {
console.log("handleSaveSuccess");
let that = this; let that = this;
that.$refs.tables.refresh(); that.$refs.tables.refresh();
that.getDEGdatas(); that.$refs.flowEditor && that.$refs.flowEditor.reload();
},
// ->
handleEditorChanged() {
this.$refs.tables && this.$refs.tables.refresh();
}, },
}, },
}; };