<template lang=""> <div class="vueFlow"> <div id="container"></div> <div id="stencil"></div> <div id="graph-container"></div> <div id="minimap"></div> </div> <el-button @click="submitAll" type="primary" style="margin-top:15px">保存</el-button> <el-dialog title="节点信息配置" v-model="dialogVisible" width="800" v-loading="loading" > <byForm :formConfig="formType === 'handle-btn' ? formConfig : branchBtnConfig" :formOption="formOption" v-model="formData.data" :rules="rules" ref="byform" > </byForm> <template #footer> <el-button @click="dialogVisible = false" size="default" >取 消</el-button > <el-button type="danger" @click="deleteFlowDefinitionNodeObj()" size="default" :loading="submitLoading" > 删 除 </el-button> <el-button type="primary" @click="submitForm('byform')" size="default" :loading="submitLoading" > 确 定 </el-button> </template> </el-dialog> <el-dialog title="节点信息配置" v-model="startModalType" width="500" v-loading="loading" > <div> 节点后执行方法 <el-input style="margin-top:10px" v-model="handlingMethod"></el-input> </div> <template #footer> <el-button @click="startModalType = false" size="default">取 消</el-button> <el-button type="primary" @click="startModalType = false" size="default" :loading="submitLoading"> 确 定 </el-button> </template> </el-dialog> </template> <script lang="ts" setup> import { defineComponent, ref, onMounted, onUnmounted, watch, reactive, toRefs, computed, nextTick, getCurrentInstance, onDeactivated, onActivated, } from "vue"; import byForm from "@/components/byForm/index"; import { Graph, Shape } from "@antv/x6"; import { Stencil } from "@antv/x6-plugin-stencil"; import { Transform } from "@antv/x6-plugin-transform"; import { Selection } from "@antv/x6-plugin-selection"; import { Snapline } from "@antv/x6-plugin-snapline"; import { Keyboard } from "@antv/x6-plugin-keyboard"; import { Clipboard } from "@antv/x6-plugin-clipboard"; import { register } from "@antv/x6-vue-shape"; import { History } from "@antv/x6-plugin-history"; import Cookies from "js-cookie"; import { ElMessage, ElMessageBox } from "element-plus"; import startBtn from "./startBtn.vue"; import endBtn from "./endBtn.vue"; import handleBtn from "./handleBtn.vue"; import branchBtn from "./branchBtn.vue"; import { MiniMap } from "@antv/x6-plugin-minimap"; import useTagsViewStore from "@/store/modules/tagsView"; import { rectToBox } from "@vue-flow/core/dist/utils/graph"; defineProps({ title: { type: Object, default: "", }, }); const { proxy } = getCurrentInstance(); const internalInstance = getCurrentInstance(); const dialogVisible = ref(false); const modalType = ref("add"); const loading = ref(false); const submitLoading = ref(false); let formType = ref(1); //1办理 2分支 const formData = reactive({ data: { userName: "", password: "", }, }); const startModalType = ref(false); const handlingMethod = ref(""); const byform = ref(null); const flowDefinitionNodeObj = ref({}); const rules = reactive({ nodeName: [ { required: true, message: "请输入节点名称", trigger: "blur", }, ], handleObjectType: [ { required: true, message: "办理人类型不能为空", trigger: "blur", }, ], handleObjectId: [ { required: true, message: "办理人不能为空", trigger: "blur", }, ], }); const branchBtnConfig = computed(() => { return [ { type: "input", prop: "nodeName", label: "节点名称", required: true, itemType: "text", }, ]; }); const formConfig = computed(() => { return [ { type: "input", prop: "nodeName", label: "节点名称", required: true, itemType: "text", }, { type: "select", prop: "handleObjectType", label: "办理人", placeholder: "请选择办理人类型", required: true, itemWidth: 50, fn: (e) => { gethandleObjectList(e, true); }, //1用户 2部门负责人 3部门总监 4岗位 5角色 data: [ { label: "用户", value: 1, }, { label: "部门负责人", value: 2, }, { label: "部门总监", value: 3, }, { label: "岗位", value: 4, }, { label: "角色", value: 5, }, ], }, // { // type: "treeSelect", // prop: "handleObjectId", // label: "请选择办理人", // itemWidth: 30, // data: [], // }, { type: "select", label: "", itemWidth: 50, prop: "handleObjectId", placeholder: "请选择办理人", data: [], }, { type: "input", prop: "handlingMethod", label: "节点后置执行方法", required: true, itemType: "text", }, { type: "input", prop: "jumpCondition", label: "条件表达式", required: true, itemType: "text", }, { type: "checkbox", prop: "nodeButtonSet", label: "节点按钮", //1通过 2驳回 3返回上一步 4退回到发起人 data: [ { label: "通过", value: 1, disabled: true, }, { label: "驳回", value: 2, }, { label: "返回上一步", value: 3, }, { label: "退回到发起人", value: 4, }, ], }, { type: "radio", prop: "jobNumber11", label: "审批意见必填", data: [ { label: "是", value: 1, }, { label: "否", value: 0, }, ], }, ]; }); const formOption = reactive({ inline: true, labelWidth: 130, itemWidth: 100, }); let graph; const submitForm = () => { byform.value.handleSubmit((valid) => { flowDefinitionNodeObj.value[formData.data.id] = formData.data; dialogVisible.value = false; formData.data.cell.setData({ title: formData.data.nodeName, }); }); }; const submitFormData = { flowInfoId: null, titleTemplate: null, tenantId: Cookies.get("tenantId"), nodeObject: "", lineObject: "", flowDefinitionNodeList: [], }; const submitAll = () => { if (proxy.title == "") { ElMessage({ message: "请输入流程标题", type: "warning", }); return; } submitFormData.titleTemplate = proxy.title; const nodeList = graph.toJSON().cells; submitFormData.nodeObject = JSON.stringify(nodeList); submitFormData.lineObject = JSON.stringify(flowDefinitionNodeObj.value); console.log(nodeList); const isStart = false; for (let i = 0; i < nodeList.length; i++) { const element = nodeList[i]; //是办理节点 if ( element.shape != "start-btn" && element.shape != "edge" && element.shape != "end-btn" ) { if (!flowDefinitionNodeObj.value[element.id]) { ElMessage({ message: "有节点未配置,请检查节点", type: "warning", }); return; } submitFormData.flowDefinitionNodeList.push({ ...flowDefinitionNodeObj.value[element.id], nodeType: element.shape == "branch-btn" ? 3 : 2, }); } if (element.shape == "end-btn") { submitFormData.flowDefinitionNodeList.push({ ...flowDefinitionNodeObj.value[element.id], }); } if (element.shape == "start-btn") { submitFormData.flowDefinitionNodeList.push({ nodeName: "开始", nodeType: 1, id: 1, parentId: 0, nodeButtonSet: [1], handlingMethod: handlingMethod.value, }); } //说明是线 if (element.shape == "edge") { if (!flowDefinitionNodeObj.value[element.target.cell]) { ElMessage({ message: "有节点未配置,请检查节点", type: "warning", }); return; } flowDefinitionNodeObj.value[element.target.cell].id = element.target.cell; flowDefinitionNodeObj.value[element.target.cell].parentId = element.source.cell; submitFormData.flowDefinitionNodeList = []; } } addVersion(); }; //选取一个随机不重复的正整数id const randomId = () => { const id = Math.floor(Math.random() * 100000000000000000); if (flowDefinitionNodeObj.value[id]) { randomId(); } else { return id; } }; const addVersion = () => { const idObg = {}; for (let i = 0; i < submitFormData.flowDefinitionNodeList.length; i++) { const element = submitFormData.flowDefinitionNodeList[i]; if (element.parentId == null && element.nodeName == "结束") { ElMessage({ message: "有结束节点未连线,请配置", type: "warning", }); return; } if (isNaN(element.id)) { if (idObg[element.id]) { element.id = idObg[element.id]; } else { const id = randomId(); idObg[element.id] = id; element.id = id; } } if (isNaN(element.parentId) && element.nodeName != "开始") { if (idObg[element.parentId]) { element.parentId = idObg[element.parentId]; } else { const id = randomId(); idObg[element.parentId] = id; element.parentId = id; } } //nodeButtonSet转成字符串类型,用逗号隔开 if (element.nodeButtonSet) { element.nodeButtonSet = element.nodeButtonSet.join(","); } } proxy.post("/flowDefinition/addVersion", submitFormData).then((res) => { ElMessage({ message: "保存成功", type: "success", }); useTagsViewStore().delView(router.currentRoute.value); history.go(-1); }); }; //将组数里的id和parentId转换成整正整数类型 const changeId = (arr) => { for (let i = 0; i < arr.length; i++) { const element = arr[i]; element.id = parseInt(element.id); element.parentId = parseInt(element.parentId); } }; const deleteFlowDefinitionNodeObj = (id) => { graph.removeNode(formData.data.id); delete flowDefinitionNodeObj.value[id]; dialogVisible.value = false; }; const gethandleObjectList = (e, flag) => { if (flag) { formData.data.handleObjectId = ""; } if (e === 1) { proxy .get( "/tenantUser/list?pageNum=1&pageSize=1000&tenantId=" + submitFormData.tenantId, {} ) .then((res) => { formConfig.value[2].data = res.rows.map((item) => { return { label: item.nickName, value: item.userId, }; }); }); } if (e === 3 || e === 2) { proxy .get( "/tenantDept/list?pageNum=1&pageSize=1000&tenantId=" + submitFormData.tenantId, {} ) .then((res) => { formConfig.value[2].data = res.data.map((item) => { return { label: item.deptName, value: item.deptId, }; }); }); } if (e === 4) { } if (e === 5) { proxy .get( "/tenantRole/list?pageNum=1&pageSize=1000&tenantId=" + submitFormData.tenantId, {} ) .then((res) => { formConfig.value[2].data = res.rows.map((item) => { return { label: item.roleName, value: item.roleId, }; }); }); } }; const getTenantDept = () => {}; getTenantDept(); const recursive = (data) => { data.map((item) => { item.label = item.deptName; item.id = item.deptId; if (item.children) { recursive(item.children); } else { item.children = []; } }); }; const pushRoom = (port: any) => { if (port.node.shape == "end-btn") { flowDefinitionNodeObj.value[port.node.id] = { nodeName: "结束", nodeType: 99, id: port.id, nodeButtonSet: "", parentId: null, }; } }; //用于存储流程定义节点数据 const antvInit = (data) => { graph = new Graph({ height: 600, container: document.getElementById("graph-container")!, grid: true, onPortRendered: pushRoom, mousewheel: { enabled: true, zoomAtMousePosition: true, modifiers: "ctrl", minScale: 0.5, maxScale: 3, }, connecting: { allowLoop: false, // router: 'manhattan', connector: { name: "rounded", args: { radius: 8, }, }, anchor: "center", connectionPoint: "anchor", allowBlank: false, snap: { radius: 20, }, createEdge() { return new Shape.Edge({ attrs: { line: { stroke: "#A2B1C3", strokeWidth: 2, targetMarker: { name: "block", width: 12, height: 8, }, }, }, zIndex: 0, }); }, validateConnection({ targetMagnet }) { return !!targetMagnet; }, }, highlighting: { magnetAdsorbed: { name: "stroke", args: { attrs: { fill: "#5F95FF", stroke: "#5F95FF", }, }, }, }, }); graph.use( new MiniMap({ container: document.getElementById("minimap"), }) ); const stencil = new Stencil({ title: "流程图", target: graph, stencilGraphWidth: 360, stencilGraphHeight: 280, collapsable: true, groups: [ { title: "基础流程图", name: "group1", }, ], layoutOptions: { columns: 1, columnWidth: 170, rowHeight: 100, }, }); document.getElementById("stencil")!.appendChild(stencil.container); // #region 使用插件 graph .use( new Transform({ resizing: true, rotating: true, }) ) .use( new Selection({ enabled: true, rubberband: true, showNodeSelectionBox: true, }) ) .use( new Snapline({ enabled: true, }) ) .use( new Keyboard({ enabled: true, }) ) .use( new Clipboard({ enabled: true, }) ) .use( new History({ enabled: true, }) ); // 控制连接桩显示/隐藏 const showPorts = (ports: NodeListOf<SVGElement>, show: boolean) => { for (let i = 0, len = ports.length; i < len; i += 1) { ports[i].style.visibility = show ? "visible" : "hidden"; } }; graph.on("node:mouseenter", () => { const container = document.getElementById("graph-container")!; const ports = container.querySelectorAll( ".x6-port-body" ) as NodeListOf<SVGElement>; showPorts(ports, true); }); graph.on("node:mouseleave", () => { const container = document.getElementById("graph-container")!; const ports = container.querySelectorAll( ".x6-port-body" ) as NodeListOf<SVGElement>; showPorts(ports, false); }); // #endregion graph.on("cell:click", ({ e, x, y, cell, view }) => { console.log(flowDefinitionNodeObj.value); console.log(cell); if (cell.shape === "start-btn") { startModalType.value = true; return; } if (cell.shape === "end-btn" || cell.shape === "edge") { ElMessageBox.confirm("是否删除", "提示", { confirmButtonText: "确定", cancelButtonText: "取消", type: "warning", }).then(() => { graph.removeNode(cell.id); // delete flowDefinitionNodeObj.value[id] }); return; } formType.value = cell.shape; if (flowDefinitionNodeObj.value[cell.id]) { // 默认去调对应的处理人接口 gethandleObjectList( flowDefinitionNodeObj.value[cell.id].handleObjectType ); setTimeout(() => { formData.data = flowDefinitionNodeObj.value[cell.id]; formData.data.cell = cell; }, 300); } else { formData.data = { id: cell.id, cell: cell, nodeButtonSet: [1], }; } dialogVisible.value = true; }); // #region 初始化图形 const ports = { groups: { top: { position: "top", attrs: { circle: { r: 4, magnet: true, stroke: "#5F95FF", strokeWidth: 1, fill: "#fff", style: { visibility: "hidden", }, }, }, }, right: { position: "right", attrs: { circle: { r: 4, magnet: true, stroke: "#5F95FF", strokeWidth: 1, fill: "#fff", style: { visibility: "hidden", }, }, }, }, bottom: { position: "bottom", attrs: { circle: { r: 4, magnet: true, stroke: "#5F95FF", strokeWidth: 1, fill: "#fff", style: { visibility: "hidden", }, }, }, }, left: { position: "left", attrs: { circle: { r: 4, magnet: true, stroke: "#5F95FF", strokeWidth: 1, fill: "#fff", style: { visibility: "hidden", }, }, }, }, }, items: [ { group: "top", }, { group: "right", }, { group: "bottom", }, { group: "left", }, ], }; Graph.registerNode( "custom-rect", { inherit: "rect", width: 66, height: 36, attrs: { body: { strokeWidth: 1, stroke: "#5F95FF", fill: "#EFF4FF", }, text: { fontSize: 12, fill: "#262626", }, }, ports: { ...ports }, }, true ); register({ shape: "start-btn", width: 150, height: 90, component: startBtn, effect: ["title"], ports: { ...ports }, data: { title: 80, }, }); register({ shape: "handle-btn", width: 150, height: 90, effect: ["title"], component: handleBtn, ports: { ...ports }, }); register({ shape: "branch-btn", width: 150, height: 90, effect: ["title"], component: branchBtn, ports: { ...ports }, }); register({ shape: "end-btn", width: 150, height: 90, effect: ["title"], component: endBtn, ports: { ...ports }, }); // const r1 = graph.createNode({ // shape: 'start-btn', // label: '开始', // zIndex: 100, // attrs: { // body: { // rx: 20, // ry: 26, // }, // }, // data: { // title: 80, // }, // }) const r2 = graph.createNode({ shape: "handle-btn", label: "办理", zIndex: 100, attrs: { body: { rx: 40, ry: 46, }, }, }); const r3 = graph.createNode({ shape: "branch-btn", label: "分支", zIndex: 100, attrs: { body: { rx: 40, ry: 46, }, }, }); const r4 = graph.createNode({ shape: "end-btn", label: "结束", zIndex: 100, attrs: { body: { rx: 20, ry: 26, }, }, }); stencil.load([r2, r3, r4], "group1"); // const startNode = graph.addNode({ // shape: 'custom-rect', // label: '开始', // id: 1, // x: 500, // y: 100, // }) if (data) { graph.fromJSON(data); } else { graph.addNode({ shape: "start-btn", x: 500, y: 20, label: "开始", id: 1, attrs: {}, }); } }; const getFlowInfo = () => { proxy .post("/flowDefinition/getDetails", { id: submitFormData.id }) .then((res) => { if (res.lineObject) { flowDefinitionNodeObj.value = JSON.parse(res.lineObject); for (const key in flowDefinitionNodeObj.value) { if (flowDefinitionNodeObj.value[key].nodeButtonSet) { flowDefinitionNodeObj.value[key].nodeButtonSet = flowDefinitionNodeObj.value[key].nodeButtonSet.map((item) => { return item * 1; }); } } } if (res.nodeObject) { antvInit(JSON.parse(res.nodeObject)); } else { antvInit(); } for (const key in flowDefinitionNodeObj.value) { //延迟等待dom渲染完成 setTimeout(() => { if ( flowDefinitionNodeObj.value[key].nodeName != "结束" && flowDefinitionNodeObj.value[key].cell != "开始" ) { let htmlNode = document.querySelector( "g[data-cell-id='" + key + "']" ); //获取htmlNode节点下的title,修改title的内容 htmlNode.getElementsByClassName("title")[0].innerHTML = flowDefinitionNodeObj.value[key].nodeName; } }, 2000); } dialogVisible.value = false; }); }; const router = useRouter(); onActivated(() => {}); onDeactivated(() => { console.log(window.document.getElementById("minimap").children); if (window.document.getElementById("minimap").children.length > 1) { window.document.getElementById("minimap").children[0].remove(); } }); onMounted(() => { //获取url router参数 submitFormData.flowInfoId = router.currentRoute.value.query.flowInfoId; submitFormData.id = router.currentRoute.value.query.id; submitFormData.tenantId = router.currentRoute.value.query.tenantId; if (submitFormData.flowInfoId) { getFlowInfo(); // antvInit() // setTimeout(() => { // for (let i = 0; i < dataJson.flowDefinitionNodeList.length; i++) { // const element = dataJson.flowDefinitionNodeList[i]; // console.log(element) // if(!element.cell) { // continue // } // if( element.cell.nodeName != '开始' && element.cell.nodeName != '结束'){ // flowDefinitionNodeObj.value[element.cell.id] = element // flowDefinitionNodeObj.value[element.cell.id].nodeButtonSet = element.nodeButtonSet.split(',') // graph.addNode({ // shape: 'handle-btn', // x: element.cell.position.x, // y: element.cell.position.y, // label: '办理', // id:element.cell.id, // attrs: { // }, // }) // } // } // console.log(flowDefinitionNodeObj.value) // }, 1000); } setTimeout(() => { if (window.document.getElementById("minimap").children.length > 1) { window.document.getElementById("minimap").children[0].remove(); } }, 500); }); </script> <style lang="scss" scope> #minimap .x6-widget-minimap { border: 1px solid #dcdcdc; } .x6-widget-stencil-group-title { display: none !important; } .x6-widget-stencil-title { display: none; } .x6-widget-stencil-content { top: 0 !important; &::-webkit-scrollbar { width: 2px !important; height: 2px !important; } } .vueFlow { position: relative; display: flex; justify-content: space-between; overflow: hidden; height: 600px; .x6-graph { width: 100% !important; } #stencil { position: fixed; top: 250px; left: 30px; z-index: 100; width: 190px; height: 550px; background: #fff; overflow: hidden; background: #eee; border-radius: 20px; } #container { } #graph-container { width: 100%; position: absolute; right: 0; top: 0; } } #stencil .x6-widget-stencil-content .x6-widget-stencil-group-content .x6-graph { height: 900px !important; } </style>