瀏覽代碼

流程设置

l1069030731 2 年之前
父節點
當前提交
f5fba0679a

+ 18 - 0
src/api/process/processNode.js

@@ -53,3 +53,21 @@ export function processEditVersion(data = {}) {
     data: data,
   })
 }
+
+// 部门下拉框
+export function deptTree(data = {}) {
+  return request({
+    url: '/api/blade-system/dept/tree',
+    method: 'get',
+    params: data,
+  })
+}
+
+// 角色下拉框
+export function roleTree(data = {}) {
+  return request({
+    url: '/api/blade-system/role/tree',
+    method: 'get',
+    params: data,
+  })
+}

+ 29 - 27
src/main.js

@@ -21,6 +21,7 @@ import website from '@/config/website'
 import crudCommon from '@/mixins/crud'
 import Example from './components/example'
 import Pagination from './components/Pagination'
+import directive from './util/directives'
 
 //cdn迁移进src目录
 import './assets/cdn/element-ui/2.15.1/theme-chalk/index.css'
@@ -29,7 +30,7 @@ import './assets/cdn/iconfont/avue/iconfont.css'
 import './assets/cdn/iconfont/saber/iconfont.css'
 import './assets/cdn/avue/2.8.18/index.css'
 import { set } from 'nprogress'
-import { setToken } from '@/util/auth';
+import { setToken } from '@/util/auth'
 // 注册全局crud驱动
 window.$crudCommon = crudCommon
 // 加载Vue拓展
@@ -47,6 +48,7 @@ Vue.use(window.AVUE, {
 Vue.use(Example)
 Vue.use(Pagination)
 Vue.use(set)
+Vue.use(directive)
 
 Element.Select.props.filterable = {
   type: Boolean,
@@ -116,13 +118,13 @@ function render({ props = {} } = {}) {
     router,
     store,
     i18n,
-    data(){},
+    data() {},
     render: (h) => h(App),
   }).$mount(container ? container.querySelector('#fjhxCloudVue') : '#fjhxCloudVue')
   console.log(instance)
 }
 // 独立运行时
-(function () {
+;(function () {
   console.log(window.__POWERED_BY_QIANKUN__)
   if (window.__POWERED_BY_QIANKUN__) {
     __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
@@ -135,31 +137,31 @@ export async function bootstrap() {
   console.log('vue app bootstraped')
 }
 export async function mount(props) {
-	//设置token
-	console.log(props.data.token)
-	//挂载父路由
-	Vue.prototype.$parentRouter = props.data.router
-	if(props.data.token != null){
-		//注入父应用传来的登录信息
-		setToken(props.data.token.access_token)
-		store.commit('SET_TOKEN',props.data.token.access_token)
-		store.commit('SET_REFRESH_TOKEN', props.data.token.refresh_token);
-		store.commit('SET_TENANT_ID', props.data.token.tenant_id);
-		store.commit('SET_USER_INFO', props.data.token);
-		//设置监听事件触发方法,全局共享
-		props.onGlobalStateChange((state, prev) => {
-			// state: 变更后的状态; prev 变更前的状态
-			console.log(state, prev, '子组件监听');
-		});
-	}
-	//发送数据给全局
-	// props.setGlobalState({ token: 21312312 }) 
-	render(props)
+  //设置token
+  console.log(props.data.token)
+  //挂载父路由
+  Vue.prototype.$parentRouter = props.data.router
+  if (props.data.token != null) {
+    //注入父应用传来的登录信息
+    setToken(props.data.token.access_token)
+    store.commit('SET_TOKEN', props.data.token.access_token)
+    store.commit('SET_REFRESH_TOKEN', props.data.token.refresh_token)
+    store.commit('SET_TENANT_ID', props.data.token.tenant_id)
+    store.commit('SET_USER_INFO', props.data.token)
+    //设置监听事件触发方法,全局共享
+    props.onGlobalStateChange((state, prev) => {
+      // state: 变更后的状态; prev 变更前的状态
+      console.log(state, prev, '子组件监听')
+    })
+  }
+  //发送数据给全局
+  // props.setGlobalState({ token: 21312312 })
+  render(props)
 }
 export async function unmount() {
-	// instance.$destroy();
-	instance.$el.innerHTML = "";
-	instance = null;
-	// router = null;
+  // instance.$destroy();
+  instance.$el.innerHTML = ''
+  instance = null
+  // router = null;
 }
 Vue.config.productionTip = false

+ 30 - 0
src/util/directives.js

@@ -0,0 +1,30 @@
+import Vue from 'vue'
+
+const clickDown = Vue.directive('clickDown', {
+  inserted(el, binding, vnode) {
+    let clickTimer = null
+
+    // 单击
+    el.addEventListener('click', () => {
+      if (clickTimer) {
+        window.clearTimeout(clickTimer)
+        clickTimer = null
+      }
+      clickTimer = setTimeout(() => {
+        vnode.context[binding.value.clickFun](binding.value.clickFunType)
+      }, 300)
+    })
+
+    // 双击
+    el.addEventListener('dblclick', () => {
+      if (clickTimer) {
+        window.clearTimeout(clickTimer)
+        clickTimer = null
+      }
+
+      vnode.context[binding.value.dblclickFu](binding.value.dblclickFuType)
+    })
+  },
+})
+
+export default { clickDown }

+ 169 - 160
src/util/util.js

@@ -1,14 +1,14 @@
-import {validatenull} from './validate'
+import { validatenull } from './validate'
 //表单序列化
-export const serialize = data => {
-  let list = [];
-  Object.keys(data).forEach(ele => {
+export const serialize = (data) => {
+  let list = []
+  Object.keys(data).forEach((ele) => {
     list.push(`${ele}=${data[ele]}`)
   })
-  return list.join('&');
-};
-export const getObjType = obj => {
-  var toString = Object.prototype.toString;
+  return list.join('&')
+}
+export const getObjType = (obj) => {
+  var toString = Object.prototype.toString
   var map = {
     '[object Boolean]': 'boolean',
     '[object Number]': 'number',
@@ -19,93 +19,86 @@ export const getObjType = obj => {
     '[object RegExp]': 'regExp',
     '[object Undefined]': 'undefined',
     '[object Null]': 'null',
-    '[object Object]': 'object'
-  };
+    '[object Object]': 'object',
+  }
   if (obj instanceof Element) {
-    return 'element';
+    return 'element'
   }
-  return map[toString.call(obj)];
-};
+  return map[toString.call(obj)]
+}
 export const getViewDom = () => {
   return window.document.getElementById('avue-view').getElementsByClassName('el-scrollbar__wrap')[0]
 }
 /**
  * 对象深拷贝
  */
-export const deepClone = data => {
-  var type = getObjType(data);
-  var obj;
+export const deepClone = (data) => {
+  var type = getObjType(data)
+  var obj
   if (type === 'array') {
-    obj = [];
+    obj = []
   } else if (type === 'object') {
-    obj = {};
+    obj = {}
   } else {
     //不再具有下一层次
-    return data;
+    return data
   }
   if (type === 'array') {
     for (var i = 0, len = data.length; i < len; i++) {
-      obj.push(deepClone(data[i]));
+      obj.push(deepClone(data[i]))
     }
   } else if (type === 'object') {
     for (var key in data) {
-      obj[key] = deepClone(data[key]);
+      obj[key] = deepClone(data[key])
     }
   }
-  return obj;
-};
+  return obj
+}
 /**
  * 设置灰度模式
  */
 export const toggleGrayMode = (status) => {
   if (status) {
-    document.body.className = document.body.className + ' grayMode';
+    document.body.className = document.body.className + ' grayMode'
   } else {
-    document.body.className = document.body.className.replace(' grayMode', '');
+    document.body.className = document.body.className.replace(' grayMode', '')
   }
-};
+}
 /**
  * 设置主题
  */
 export const setTheme = (name) => {
-  document.body.className = name;
+  document.body.className = name
 }
 
 /**
  * 加密处理
  */
 export const encryption = (params) => {
-  let {
-    data,
-    type,
-    param,
-    key
-  } = params;
-  let result = JSON.parse(JSON.stringify(data));
+  let { data, type, param, key } = params
+  let result = JSON.parse(JSON.stringify(data))
   if (type == 'Base64') {
-    param.forEach(ele => {
-      result[ele] = btoa(result[ele]);
+    param.forEach((ele) => {
+      result[ele] = btoa(result[ele])
     })
   } else if (type == 'Aes') {
-    param.forEach(ele => {
-      result[ele] = window.CryptoJS.AES.encrypt(result[ele], key).toString();
+    param.forEach((ele) => {
+      result[ele] = window.CryptoJS.AES.encrypt(result[ele], key).toString()
     })
-
   }
-  return result;
-};
-
+  return result
+}
 
 /**
  * 浏览器判断是否全屏
  */
 export const fullscreenToggel = () => {
   if (fullscreenEnable()) {
-    exitFullScreen();
+    exitFullScreen()
   } else {
-    reqFullScreen();
+    reqFullScreen()
   }
-};
+}
 /**
  * esc监听全屏
  */
@@ -114,25 +107,25 @@ export const listenfullscreen = (callback) => {
     callback()
   }
 
-  document.addEventListener("fullscreenchange", function () {
-    listen();
-  });
-  document.addEventListener("mozfullscreenchange", function () {
-    listen();
-  });
-  document.addEventListener("webkitfullscreenchange", function () {
-    listen();
-  });
-  document.addEventListener("msfullscreenchange", function () {
-    listen();
-  });
-};
+  document.addEventListener('fullscreenchange', function () {
+    listen()
+  })
+  document.addEventListener('mozfullscreenchange', function () {
+    listen()
+  })
+  document.addEventListener('webkitfullscreenchange', function () {
+    listen()
+  })
+  document.addEventListener('msfullscreenchange', function () {
+    listen()
+  })
+}
 /**
  * 浏览器判断是否全屏
  */
 export const fullscreenEnable = () => {
   var isFullscreen = document.isFullScreen || document.mozIsFullScreen || document.webkitIsFullScreen
-  return isFullscreen;
+  return isFullscreen
 }
 
 /**
@@ -140,25 +133,25 @@ export const fullscreenEnable = () => {
  */
 export const reqFullScreen = () => {
   if (document.documentElement.requestFullScreen) {
-    document.documentElement.requestFullScreen();
+    document.documentElement.requestFullScreen()
   } else if (document.documentElement.webkitRequestFullScreen) {
-    document.documentElement.webkitRequestFullScreen();
+    document.documentElement.webkitRequestFullScreen()
   } else if (document.documentElement.mozRequestFullScreen) {
-    document.documentElement.mozRequestFullScreen();
+    document.documentElement.mozRequestFullScreen()
   }
-};
+}
 /**
  * 浏览器退出全屏
  */
 export const exitFullScreen = () => {
   if (document.documentElement.requestFullScreen) {
-    document.exitFullScreen();
+    document.exitFullScreen()
   } else if (document.documentElement.webkitRequestFullScreen) {
-    document.webkitCancelFullScreen();
+    document.webkitCancelFullScreen()
   } else if (document.documentElement.mozRequestFullScreen) {
-    document.mozCancelFullScreen();
+    document.mozCancelFullScreen()
   }
-};
+}
 /**
  * 递归寻找子类的父类
  */
@@ -168,16 +161,16 @@ export const findParent = (menu, id) => {
     if (menu[i].children.length != 0) {
       for (let j = 0; j < menu[i].children.length; j++) {
         if (menu[i].children[j].id == id) {
-          return menu[i];
+          return menu[i]
         } else {
           if (menu[i].children[j].children.length != 0) {
-            return findParent(menu[i].children[j].children, id);
+            return findParent(menu[i].children[j].children, id)
           }
         }
       }
     }
   }
-};
+}
 /**
  * 判断2个对象属性和值是否相等
  */
@@ -186,90 +179,93 @@ export const findParent = (menu, id) => {
  * 动态插入css
  */
 
-export const loadStyle = url => {
-  const link = document.createElement('link');
-  link.type = 'text/css';
-  link.rel = 'stylesheet';
-  link.href = url;
-  const head = document.getElementsByTagName('head')[0];
-  head.appendChild(link);
-};
+export const loadStyle = (url) => {
+  const link = document.createElement('link')
+  link.type = 'text/css'
+  link.rel = 'stylesheet'
+  link.href = url
+  const head = document.getElementsByTagName('head')[0]
+  head.appendChild(link)
+}
 /**
  * 判断路由是否相等
  */
 export const diff = (obj1, obj2) => {
-  delete obj1.close;
-  var o1 = obj1 instanceof Object;
-  var o2 = obj2 instanceof Object;
-  if (!o1 || !o2) { /*  判断不是对象  */
-    return obj1 === obj2;
+  delete obj1.close
+  var o1 = obj1 instanceof Object
+  var o2 = obj2 instanceof Object
+  if (!o1 || !o2) {
+    /*  判断不是对象  */
+    return obj1 === obj2
   }
 
   if (Object.keys(obj1).length !== Object.keys(obj2).length) {
-    return false;
+    return false
     //Object.keys() 返回一个由对象的自身可枚举属性(key值)组成的数组,例如:数组返回下表:let arr = ["a", "b", "c"];console.log(Object.keys(arr))->0,1,2;
   }
 
   for (var attr in obj1) {
-    var t1 = obj1[attr] instanceof Object;
-    var t2 = obj2[attr] instanceof Object;
+    var t1 = obj1[attr] instanceof Object
+    var t2 = obj2[attr] instanceof Object
     if (t1 && t2) {
-      return diff(obj1[attr], obj2[attr]);
+      return diff(obj1[attr], obj2[attr])
     } else if (obj1[attr] !== obj2[attr]) {
-      return false;
+      return false
     }
   }
-  return true;
+  return true
 }
 /**
  * 根据字典的value显示label
  */
 export const findByvalue = (dic, value) => {
-  let result = '';
-  if (validatenull(dic)) return value;
-  if (typeof (value) == 'string' || typeof (value) == 'number' || typeof (value) == 'boolean') {
-    let index = 0;
-    index = findArray(dic, value);
+  let result = ''
+  if (validatenull(dic)) return value
+  if (typeof value == 'string' || typeof value == 'number' || typeof value == 'boolean') {
+    let index = 0
+    index = findArray(dic, value)
     if (index != -1) {
-      result = dic[index].label;
+      result = dic[index].label
     } else {
-      result = value;
+      result = value
     }
   } else if (value instanceof Array) {
-    result = [];
-    let index = 0;
-    value.forEach(ele => {
-      index = findArray(dic, ele);
+    result = []
+    let index = 0
+    value.forEach((ele) => {
+      index = findArray(dic, ele)
       if (index != -1) {
-        result.push(dic[index].label);
+        result.push(dic[index].label)
       } else {
-        result.push(value);
+        result.push(value)
       }
-    });
-    result = result.toString();
+    })
+    result = result.toString()
   }
-  return result;
-};
+  return result
+}
 /**
  * 根据字典的value查找对应的index
  */
 export const findArray = (dic, value) => {
   for (let i = 0; i < dic.length; i++) {
     if (dic[i].value == value) {
-      return i;
+      return i
     }
   }
-  return -1;
-};
+  return -1
+}
 /**
  * 生成随机len位数字
  */
 export const randomLenNum = (len, date) => {
-  let random = '';
-  random = Math.ceil(Math.random() * 100000000000000).toString().substr(0, len ? len : 4);
-  if (date) random = random + Date.now();
-  return random;
-};
+  let random = ''
+  random = Math.ceil(Math.random() * 100000000000000)
+    .toString()
+    .substr(0, len ? len : 4)
+  if (date) random = random + Date.now()
+  return random
+}
 /**
  * 打开小窗口
  */
@@ -281,9 +277,20 @@ export const openWindow = (url, title, w, h) => {
   const width = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width
   const height = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height
 
-  const left = ((width / 2) - (w / 2)) + dualScreenLeft
-  const top = ((height / 2) - (h / 2)) + dualScreenTop
-  const newWindow = window.open(url, title, 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=yes, copyhistory=no, width=' + w + ', height=' + h + ', top=' + top + ', left=' + left)
+  const left = width / 2 - w / 2 + dualScreenLeft
+  const top = height / 2 - h / 2 + dualScreenTop
+  const newWindow = window.open(
+    url,
+    title,
+    'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=yes, copyhistory=no, width=' +
+      w +
+      ', height=' +
+      h +
+      ', top=' +
+      top +
+      ', left=' +
+      left,
+  )
 
   // Puts focus on the newWindow
   if (window.focus) {
@@ -295,7 +302,7 @@ export const openWindow = (url, title, w, h) => {
  * 获取顶部地址栏地址
  */
 export const getTopUrl = () => {
-  return window.location.href.split("/#/")[0];
+  return window.location.href.split('/#/')[0]
 }
 
 /**
@@ -303,10 +310,10 @@ export const getTopUrl = () => {
  * @param name 参数名
  */
 export const getQueryString = (name) => {
-  let reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)", "i");
-  let r = window.location.search.substr(1).match(reg);
-  if (r != null) return unescape(decodeURI(r[2]));
-  return null;
+  let reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i')
+  let r = window.location.search.substr(1).match(reg)
+  if (r != null) return unescape(decodeURI(r[2]))
+  return null
 }
 
 /**
@@ -315,28 +322,28 @@ export const getQueryString = (name) => {
  * @param {String} name - 文件名,eg: test.png
  */
 export const downloadFileBlob = (path, name) => {
-  const xhr = new XMLHttpRequest();
-  xhr.open('get', path);
-  xhr.responseType = 'blob';
-  xhr.send();
+  const xhr = new XMLHttpRequest()
+  xhr.open('get', path)
+  xhr.responseType = 'blob'
+  xhr.send()
   xhr.onload = function () {
     if (this.status === 200 || this.status === 304) {
       // 如果是IE10及以上,不支持download属性,采用msSaveOrOpenBlob方法,但是IE10以下也不支持msSaveOrOpenBlob
       if ('msSaveOrOpenBlob' in navigator) {
-        navigator.msSaveOrOpenBlob(this.response, name);
-        return;
+        navigator.msSaveOrOpenBlob(this.response, name)
+        return
       }
-      const url = URL.createObjectURL(this.response);
-      const a = document.createElement('a');
-      a.style.display = 'none';
-      a.href = url;
-      a.download = name;
-      document.body.appendChild(a);
-      a.click();
-      document.body.removeChild(a);
-      URL.revokeObjectURL(url);
+      const url = URL.createObjectURL(this.response)
+      const a = document.createElement('a')
+      a.style.display = 'none'
+      a.href = url
+      a.download = name
+      document.body.appendChild(a)
+      a.click()
+      document.body.removeChild(a)
+      URL.revokeObjectURL(url)
     }
-  };
+  }
 }
 
 /**
@@ -345,32 +352,34 @@ export const downloadFileBlob = (path, name) => {
  * @param {String} name - 文件名,eg: test.png
  */
 export const downloadFileBase64 = (path, name) => {
-  const xhr = new XMLHttpRequest();
-  xhr.open('get', path);
-  xhr.responseType = 'blob';
-  xhr.send();
+  const xhr = new XMLHttpRequest()
+  xhr.open('get', path)
+  xhr.responseType = 'blob'
+  xhr.send()
   xhr.onload = function () {
     if (this.status === 200 || this.status === 304) {
-      const fileReader = new FileReader();
-      fileReader.readAsDataURL(this.response);
+      const fileReader = new FileReader()
+      fileReader.readAsDataURL(this.response)
       fileReader.onload = function () {
-        const a = document.createElement('a');
-        a.style.display = 'none';
-        a.href = this.result;
-        a.download = name;
-        document.body.appendChild(a);
-        a.click();
-        document.body.removeChild(a);
-      };
+        const a = document.createElement('a')
+        a.style.display = 'none'
+        a.href = this.result
+        a.download = name
+        document.body.appendChild(a)
+        a.click()
+        document.body.removeChild(a)
+      }
     }
-  };
+  }
 }
 
 //生成指定长度的唯一ID
-export function GenNonDuplicateID(randomLength) {
-  return Number(
-      Math.random()
-      .toString()
-      .substr(3, randomLength) + Date.now()
-  ).toString(36);
-}
+export function GenNonDuplicateID(data) {
+  // return Number(Math.random().toString().substr(3, randomLength) + Date.now()).toString(36)
+  let num = String(Math.floor(Math.random() * (999999999999999999 - 100000000000000000 + 1) + 100000000000000000))
+  let list = data.lineList.filter((item) => item.id === num).concat(data.nodeList.filter((item) => item.id === num))
+  if (list && list.length > 0) {
+    num = this.GenNonDuplicateID(data)
+  }
+  return num
+}

+ 76 - 78
src/views/process/processNode/components/node-item.vue

@@ -1,34 +1,37 @@
 <template>
-  <div class="node-item" ref="node"
-    :class="[(isActive || isSelected) ? 'active' : '']"
+  <div
+    class="node-item"
+    ref="node"
+    :class="[isActive || isSelected ? 'active' : '']"
     :style="flowNodeContainer"
     v-click-outside="setNotActive"
-    @click="setActive"
     @mouseenter="showAnchor"
     @mouseleave="hideAnchor"
-    @dblclick.prevent="editNode"
-    @contextmenu.prevent="onContextmenu">
+    @contextmenu.prevent="onContextmenu"
+    v-clickDown="{ clickFun: 'setActive', dblclickFu: 'editNode' }"
+  >
     <div class="log-wrap">
-      <img :src="node.logImg" alt="">
+      <img :src="node.logImg" alt="" />
     </div>
-    <div class="nodeName">{{node.nodeName}}</div>
-      <!--连线用--//触发连线的区域-->
-      <div class="node-anchor anchor-top" v-show="mouseEnter"></div>
-      <div class="node-anchor anchor-right" v-show="mouseEnter"></div>
-      <div class="node-anchor anchor-bottom" v-show="mouseEnter"></div>
-      <div class="node-anchor anchor-left" v-show="mouseEnter"></div>
+    <div class="nodeName">{{ node.nodeName }}</div>
+    <!--连线用--//触发连线的区域-->
+    <div class="node-anchor anchor-top" v-show="mouseEnter"></div>
+    <div class="node-anchor anchor-right" v-show="mouseEnter"></div>
+    <div class="node-anchor anchor-bottom" v-show="mouseEnter"></div>
+    <div class="node-anchor anchor-left" v-show="mouseEnter"></div>
   </div>
 </template>
 
 <script>
 import ClickOutside from 'vue-click-outside'
+
 export default {
-  name: "nodeItem",
+  name: 'nodeItem',
   props: {
-      node: Object
+    node: Object,
   },
   directives: {
-    ClickOutside
+    ClickOutside,
   },
   computed: {
     // 节点容器样式
@@ -36,17 +39,17 @@ export default {
       get() {
         return {
           top: this.node.top,
-          left: this.node.left
-        };
-      }
-    }
+          left: this.node.left,
+        }
+      },
+    },
   },
   data() {
     return {
       mouseEnter: false,
       isActive: false,
-      isSelected: false
-    };
+      isSelected: false,
+    }
   },
   methods: {
     showAnchor() {
@@ -57,63 +60,58 @@ export default {
     },
     onContextmenu() {
       this.$confirm(this.$t('askDeleteData'), {
-				confirmButtonText: this.$t('submitText'),
-				cancelButtonText: this.$t('cancelText'),
-				type: 'warning',
-			}).then(() => {
-				this.deleteNode()
-			})
+        confirmButtonText: this.$t('submitText'),
+        cancelButtonText: this.$t('cancelText'),
+        type: 'warning',
+      }).then(() => {
+        this.deleteNode()
+      })
     },
     setActive() {
-      alert('拿详情,展示')
-      if(window.event.ctrlKey){
-        this.isSelected = !this.isSelected
-        return false
-      }
+      this.isSelected = !this.isSelected
       this.isActive = true
-      this.isSelected = false
       setTimeout(() => {
-        this.$emit("changeLineState", this.node.id, true)
-      },0)
+        this.$emit('changeLineState', this.node.id, true)
+        this.$emit('clickNode', this.node)
+      }, 0)
     },
     setNotActive() {
-      if(!window.event.ctrlKey){
+      if (!window.event.ctrlKey) {
         this.isSelected = false
       }
-      if(!this.isActive) {
+      if (!this.isActive) {
         return
       }
-      this.$emit("changeLineState", this.node.id, false)
+      this.$emit('changeLineState', this.node.id, false)
       this.isActive = false
     },
     editNode() {
       this.newNodeName = this.node.nodeName
-      this.$prompt(
-        '请输入流程名称:', 
-        '提示',
-        {
-            confirmButtonText: '确定',
-            cancelButtonText: '取消',
-            type:"warning",            // 图标样式 "warning"|"error"...
-            inputValue: this.node.nodeName,
-            inputErrorMessage: '输入不能为空',
-            inputValidator: (value) => {       // 点击按钮时,对文本框里面的值进行验证
-                if(!value) {
-                    return '输入不能为空';
-                }
-            },
-        }).then(({value}) => {
+      this.$prompt('请输入流程名称:', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning', // 图标样式 "warning"|"error"...
+        inputValue: this.node.nodeName,
+        inputErrorMessage: '输入不能为空',
+        inputValidator: (value) => {
+          // 点击按钮时,对文本框里面的值进行验证
+          if (!value) {
+            return '输入不能为空'
+          }
+        },
+      })
+        .then(({ value }) => {
           this.$emit('setNodeName', this.node.id, value)
-            // TO DO DO ...
-        }).catch((err) => {
-            console.log(err);
-        }); 
+        })
+        .catch((err) => {
+          console.log(err)
+        })
     },
     deleteNode() {
-      this.$emit("deleteNode", this.node)
-    }
-  }
-};
+      this.$emit('deleteNode', this.node)
+    },
+  },
+}
 </script>
 
 <style lang="scss" scoped>
@@ -134,14 +132,14 @@ $viewSize: 10px;
   z-index: 9995;
   &:hover {
     z-index: 9998;
-    .delete-btn{
+    .delete-btn {
       display: block;
     }
   }
-  .log-wrap{
+  .log-wrap {
     width: 40px;
     height: 40px;
-    border-right: 1px solid  #b7b6b6;
+    border-right: 1px solid #b7b6b6;
   }
   .nodeName {
     flex-grow: 1;
@@ -162,29 +160,29 @@ $viewSize: 10px;
     z-index: 9999;
     background: -webkit-radial-gradient(sandybrown 10%, white 30%, #9a54ff 60%);
   }
-  .anchor-top{
-    top: calc((20px / 2)*-1);
+  .anchor-top {
+    top: calc((20px / 2) * -1);
     left: 50%;
-    margin-left: calc((20px/2)*-1);
+    margin-left: calc((20px / 2) * -1);
   }
-  .anchor-right{
+  .anchor-right {
     top: 50%;
-    right: calc((20px / 2)*-1);
-    margin-top: calc((20px / 2)*-1);
+    right: calc((20px / 2) * -1);
+    margin-top: calc((20px / 2) * -1);
   }
-  .anchor-bottom{
-    bottom: calc((20px / 2)*-1);
+  .anchor-bottom {
+    bottom: calc((20px / 2) * -1);
     left: 50%;
-    margin-left: calc((20px / 2)*-1);
+    margin-left: calc((20px / 2) * -1);
   }
-  .anchor-left{
+  .anchor-left {
     top: 50%;
-    left: calc((20px / 2)*-1);
-    margin-top: calc((20px / 2)*-1);
+    left: calc((20px / 2) * -1);
+    margin-top: calc((20px / 2) * -1);
   }
 }
-.active{
+.active {
   border: 1px dashed $labelColor;
-  box-shadow: 0px 5px 9px 0px rgba(0,0,0,0.5);
+  box-shadow: 0px 5px 9px 0px rgba(0, 0, 0, 0.5);
 }
 </style>

+ 17 - 3
src/views/process/processNode/config/data.json

@@ -1,4 +1,18 @@
 {
-  "nodeList": [{"type":"start","typeName":"开始","nodeName":"开始","id":"34v56ha2l9c000","top":"160px","left":"100px"},{"type":"dataSet","typeName":"文件","nodeName":"文件","id":"5sdjugrcqhc000","top":"160px","left":"315px"},{"type":"encode","typeName":"加密","nodeName":"加密","id":"3atqi5p6oa4000","top":"80px","left":"600px"},{"type":"personService","typeName":"个人服务","nodeName":"个人服务","id":"49vcu89p5q0000","top":"245px","left":"600px"},{"type":"arrange","typeName":"清洗","nodeName":"清洗","id":"1jhiilb0t2tc00","top":"180px","left":"880px"},{"type":"end","typeName":"结束","nodeName":"结束","id":"1ogr3wzy6zhc00","top":"180px","left":"1160px"}],
-  "lineList": [{"from":"34v56ha2l9c000","to":"5sdjugrcqhc000","label":"连线名称","id":"5n6pp5xqd6s000","Remark":""},{"from":"5sdjugrcqhc000","to":"3atqi5p6oa4000","label":"连线名称","id":"2a0ya9j1kev400","Remark":""},{"from":"5sdjugrcqhc000","to":"49vcu89p5q0000","label":"连线名称","id":"zoisvo5gpvk00","Remark":""},{"from":"3atqi5p6oa4000","to":"1jhiilb0t2tc00","label":"连线名称","id":"4xkb3dju1g0000","Remark":""},{"from":"49vcu89p5q0000","to":"1jhiilb0t2tc00","label":"连线名称","id":"ldc917l47w000","Remark":""},{"from":"1jhiilb0t2tc00","to":"1ogr3wzy6zhc00","label":"连线名称","id":"478galw3u34000","Remark":""}]
-}
+  "nodeList": [
+    { "type": "start", "typeName": "开始", "nodeName": "开始", "id": "34v56ha2l9c000", "top": "160px", "left": "100px" },
+    { "type": "dataSet", "typeName": "文件", "nodeName": "文件", "id": "5sdjugrcqhc000", "top": "160px", "left": "315px" },
+    { "type": "encode", "typeName": "加密", "nodeName": "加密", "id": "3atqi5p6oa4000", "top": "80px", "left": "600px" },
+    { "type": "personService", "typeName": "个人服务", "nodeName": "个人服务", "id": "49vcu89p5q0000", "top": "245px", "left": "600px" },
+    { "type": "arrange", "typeName": "清洗", "nodeName": "清洗", "id": "1jhiilb0t2tc00", "top": "180px", "left": "880px" },
+    { "type": "end", "typeName": "结束", "nodeName": "结束", "id": "1ogr3wzy6zhc00", "top": "180px", "left": "1160px" }
+  ],
+  "lineList": [
+    { "from": "34v56ha2l9c000", "to": "5sdjugrcqhc000", "label": "连线名称", "id": "5n6pp5xqd6s000", "Remark": "" },
+    { "from": "5sdjugrcqhc000", "to": "3atqi5p6oa4000", "label": "连线名称", "id": "2a0ya9j1kev400", "Remark": "" },
+    { "from": "5sdjugrcqhc000", "to": "49vcu89p5q0000", "label": "连线名称", "id": "zoisvo5gpvk00", "Remark": "" },
+    { "from": "3atqi5p6oa4000", "to": "1jhiilb0t2tc00", "label": "连线名称", "id": "4xkb3dju1g0000", "Remark": "" },
+    { "from": "49vcu89p5q0000", "to": "1jhiilb0t2tc00", "label": "连线名称", "id": "ldc917l47w000", "Remark": "" },
+    { "from": "1jhiilb0t2tc00", "to": "1ogr3wzy6zhc00", "label": "连线名称", "id": "478galw3u34000", "Remark": "" }
+  ]
+}

+ 33 - 41
src/views/process/processNode/config/init.js

@@ -1,41 +1,33 @@
-const nodeTypeList = [{
-  type: 'start',
-  typeName: '开始',
-  nodeName: '开始',
-  
-  log_bg_color: 'rgba(0, 128, 0, 0.2)'
-},{
-  type: 'end',
-  typeName: '结束',
-  nodeName: '结束',
-
-  log_bg_color: 'rgba(255, 0, 0, 0.2)'
-},{
-  type: 'dataSet',
-  typeName: '文件',
-  nodeName: '文件',
-
-  log_bg_color: 'rgba(0, 128, 0, 0.2)'
-},{
-  type: 'encode',
-  typeName: '加密',
-  nodeName: '加密',
-
-  log_bg_color: 'rgba(163, 117, 233, 0.2)'
-},{
-  type: 'personService',
-  typeName: '个人服务',
-  nodeName: '个人服务',
-
-  log_bg_color: 'rgba(132, 166, 251, 0.2)'
-},{
-  type: 'arrange',
-  typeName: '清洗',
-  nodeName: '清洗',
-
-  log_bg_color: 'rgba(250, 205, 81, 0.2)'
-}]
-
-console.log(nodeTypeList)
-
-export {nodeTypeList};
+const nodeTypeList = [
+  {
+    type: 1,
+    typeName: '开始',
+    nodeName: '开始',
+  },
+  {
+    type: 2,
+    typeName: '审核',
+    nodeName: '审核',
+  },
+  {
+    type: 3,
+    typeName: '审批',
+    nodeName: '审批',
+  },
+  {
+    type: 4,
+    typeName: '办理',
+    nodeName: '办理',
+  },
+  {
+    type: 5,
+    typeName: '分支',
+    nodeName: '分支',
+  },
+  {
+    type: 99,
+    typeName: '结束',
+    nodeName: '结束',
+  },
+]
+export { nodeTypeList }

+ 150 - 136
src/views/process/processNode/config/methods.js

@@ -1,70 +1,70 @@
-import panzoom from "panzoom";
-import { GenNonDuplicateID } from "@/util/util";
+import panzoom from 'panzoom'
+import { GenNonDuplicateID } from '@/util/util'
 
 const methods = {
   init() {
     this.jsPlumb.ready(() => {
       // 导入默认配置
-      this.jsPlumb.importDefaults(this.jsplumbSetting);
+      this.jsPlumb.importDefaults(this.jsplumbSetting)
       //完成连线前的校验
-      this.jsPlumb.bind("beforeDrop", evt => {
-        let res = () => { } //此处可以添加是否创建连接的校验, 返回 false 则不添加; 
+      this.jsPlumb.bind('beforeDrop', (evt) => {
+        let res = () => {} //此处可以添加是否创建连接的校验, 返回 false 则不添加;
         return res
       })
       // 连线创建成功后,维护本地数据
-      this.jsPlumb.bind("connection", evt => {
+      this.jsPlumb.bind('connection', (evt) => {
         this.addLine(evt)
-      });
+      })
       //连线双击删除事件
-      this.jsPlumb.bind("dblclick",(conn, originalEvent) => {
+      this.jsPlumb.bind('dblclick', (conn, originalEvent) => {
         this.confirmDelLine(conn)
       })
       //断开连线后,维护本地数据
-      this.jsPlumb.bind("connectionDetached", evt => {
+      this.jsPlumb.bind('connectionDetached', (evt) => {
         this.deleLine(evt)
       })
-      this.loadEasyFlow();
+      this.loadEasyFlow()
       // 会使整个jsPlumb立即重绘。
-      this.jsPlumb.setSuspendDrawing(false, true);
-    });
-    this.initPanZoom();
+      this.jsPlumb.setSuspendDrawing(false, true)
+    })
+    this.initPanZoom()
   },
   // 加载流程图
   loadEasyFlow() {
     // 初始化节点
     for (let i = 0; i < this.data.nodeList.length; i++) {
-      let node = this.data.nodeList[i];
+      let node = this.data.nodeList[i]
       // 设置源点,可以拖出线连接其他节点
-      this.jsPlumb.makeSource(node.id, this.jsplumbSourceOptions);
+      this.jsPlumb.makeSource(node.id, this.jsplumbSourceOptions)
       // // 设置目标点,其他源点拖出的线可以连接该节点
-      this.jsPlumb.makeTarget(node.id, this.jsplumbTargetOptions);
+      this.jsPlumb.makeTarget(node.id, this.jsplumbTargetOptions)
       // this.jsPlumb.draggable(node.id);
       this.draggableNode(node.id)
     }
 
     // 初始化连线
-    this.jsPlumb.unbind("connection"); //取消连接事件
+    this.jsPlumb.unbind('connection') //取消连接事件
     for (let i = 0; i < this.data.lineList.length; i++) {
-      let line = this.data.lineList[i];
+      let line = this.data.lineList[i]
       this.jsPlumb.connect(
         {
           source: line.from,
-          target: line.to
+          target: line.to,
         },
-        this.jsplumbConnectOptions
-      );
+        this.jsplumbConnectOptions,
+      )
     }
-    this.jsPlumb.bind("connection", evt => {
-      let from = evt.source.id;
-      let to = evt.target.id;
+    this.jsPlumb.bind('connection', (evt) => {
+      let from = evt.source.id
+      let to = evt.target.id
       this.data.lineList.push({
         from: from,
         to: to,
-        label: "连线名称",
-        id: GenNonDuplicateID(8),
-        Remark: ""
-      });
-    });
+        label: '连线名称',
+        id: GenNonDuplicateID(this.data),
+        Remark: '',
+      })
+    })
   },
   draggableNode(nodeId) {
     this.jsPlumb.draggable(nodeId, {
@@ -72,26 +72,25 @@ const methods = {
       drag: (params) => {
         this.alignForLine(nodeId, params.pos)
       },
-      start: () => {
-
-      },
+      start: () => {},
       stop: (params) => {
         this.auxiliaryLine.isShowXLine = false
         this.auxiliaryLine.isShowYLine = false
         this.changeNodePosition(nodeId, params.pos)
-      }
+      },
     })
   },
   //移动节点时,动态显示对齐线
   alignForLine(nodeId, position) {
-    let showXLine = false, showYLine = false
-    this.data.nodeList.some(el => {
-      if(el.id !== nodeId && el.left == position[0]+'px') {
-        this.auxiliaryLinePos.x = position[0] + 60;
+    let showXLine = false,
+      showYLine = false
+    this.data.nodeList.some((el) => {
+      if (el.id !== nodeId && el.left == position[0] + 'px') {
+        this.auxiliaryLinePos.x = position[0] + 60
         showYLine = true
       }
-      if(el.id !== nodeId && el.top == position[1]+'px') {
-        this.auxiliaryLinePos.y = position[1] + 20;
+      if (el.id !== nodeId && el.top == position[1] + 'px') {
+        this.auxiliaryLinePos.y = position[1] + 20
         showXLine = true
       }
     })
@@ -99,90 +98,88 @@ const methods = {
     this.auxiliaryLine.isShowXLine = showXLine
   },
   changeNodePosition(nodeId, pos) {
-    this.data.nodeList.some(v => {
-      if(nodeId == v.id) {
-        v.left = pos[0] +'px'
+    this.data.nodeList.some((v) => {
+      if (nodeId == v.id) {
+        v.left = pos[0] + 'px'
         v.top = pos[1] + 'px'
         return true
-      }else {
+      } else {
         return false
       }
     })
   },
   drag(ele, item) {
-    this.currentItem = item;
+    this.currentItem = item
   },
   drop(event) {
-    const containerRect = this.jsPlumb.getContainer().getBoundingClientRect();
-    const scale = this.getScale();
-    let left = (event.pageX - containerRect.left -60) / scale;
-    let top = (event.pageY - containerRect.top -20) / scale;
-
+    const containerRect = this.jsPlumb.getContainer().getBoundingClientRect()
+    const scale = this.getScale()
+    let left = (event.pageX - containerRect.left - 60) / scale
+    let top = (event.pageY - containerRect.top - 20) / scale
     var temp = {
       ...this.currentItem,
-      id: GenNonDuplicateID(8),
-      top: (Math.round(top/20))*20 + "px",
-      left:  (Math.round(left/20))*20 + "px"
-    };
-    this.addNode(temp);
+      id: GenNonDuplicateID(this.data),
+      top: Math.round(top / 20) * 20 + 'px',
+      left: Math.round(left / 20) * 20 + 'px',
+    }
+    this.addNode(temp)
   },
   addLine(line) {
-    let from = line.source.id;
-    let to = line.target.id;
+    let from = line.source.id
+    let to = line.target.id
     this.data.lineList.push({
       from: from,
       to: to,
-      label: "连线名称",
-      id: GenNonDuplicateID(8),
-      Remark: ""
-    });
+      label: '连线名称',
+      id: GenNonDuplicateID(this.data),
+      Remark: '',
+    })
   },
   confirmDelLine(line) {
     this.$confirm(this.$t('askDeleteData'), {
-      confirmButtonText: '确认删除该连线?',
+      confirmButtonText: '确认',
       cancelButtonText: '取消',
       type: 'warning',
     }).then(() => {
       this.jsPlumb.deleteConnection(line)
     })
-    
   },
   deleLine(line) {
     this.data.lineList.forEach((item, index) => {
-      if(item.from === line.sourceId && item.to === line.targetId) {
+      if (item.from === line.sourceId && item.to === line.targetId) {
         this.data.lineList.splice(index, 1)
       }
     })
   },
   // dragover默认事件就是不触发drag事件,取消默认事件后,才会触发drag事件
   allowDrop(event) {
-    event.preventDefault();
+    event.preventDefault()
   },
   getScale() {
-    let scale1;
+    let scale1
     if (this.jsPlumb.pan) {
-      const { scale } = this.jsPlumb.pan.getTransform();
-      scale1 = scale;
+      const { scale } = this.jsPlumb.pan.getTransform()
+      scale1 = scale
     } else {
-      const matrix = window.getComputedStyle(this.jsPlumb.getContainer()).transform;
-      scale1 = matrix.split(", ")[3] * 1;
+      const matrix = window.getComputedStyle(this.jsPlumb.getContainer()).transform
+      scale1 = matrix.split(', ')[3] * 1
     }
-    this.jsPlumb.setZoom(scale1);
-    return scale1;
+    this.jsPlumb.setZoom(scale1)
+    return scale1
   },
   // 添加新的节点
   addNode(temp) {
-    this.data.nodeList.push(temp);
+    this.data.nodeList.push(temp)
     this.$nextTick(() => {
-      this.jsPlumb.makeSource(temp.id, this.jsplumbSourceOptions);
-      this.jsPlumb.makeTarget(temp.id, this.jsplumbTargetOptions);
+      this.jsPlumb.makeSource(temp.id, this.jsplumbSourceOptions)
+      this.jsPlumb.makeTarget(temp.id, this.jsplumbTargetOptions)
       this.draggableNode(temp.id)
-    });
+    })
   },
 
   initPanZoom() {
-    const mainContainer = this.jsPlumb.getContainer();
-    const mainContainerWrap = mainContainer.parentNode;
+    const mainContainer = this.jsPlumb.getContainer()
+    const mainContainerWrap = mainContainer.parentNode
     const pan = panzoom(mainContainer, {
       smoothScroll: false,
       bounds: true,
@@ -196,51 +193,51 @@ const methods = {
         // let shouldIgnore = !e.ctrlKey
         // return shouldIgnore
       },
-      beforeMouseDown: function(e) {
+      beforeMouseDown: function (e) {
         // allow mouse-down panning only if altKey is down. Otherwise - ignore
-        var shouldIgnore = e.ctrlKey;
-        return shouldIgnore;
-      }
-    });
-    this.jsPlumb.mainContainerWrap = mainContainerWrap;
-    this.jsPlumb.pan = pan;
+        var shouldIgnore = e.ctrlKey
+        return shouldIgnore
+      },
+    })
+    this.jsPlumb.mainContainerWrap = mainContainerWrap
+    this.jsPlumb.pan = pan
     // 缩放时设置jsPlumb的缩放比率
-    pan.on("zoom", e => {
-      const { x, y, scale } = e.getTransform();
-      this.jsPlumb.setZoom(scale);
+    pan.on('zoom', (e) => {
+      const { x, y, scale } = e.getTransform()
+      this.jsPlumb.setZoom(scale)
       //根据缩放比例,缩放对齐辅助线长度和位置
-      this.auxiliaryLinePos.width = (1/scale) * 100 + '%'
-      this.auxiliaryLinePos.height = (1/scale) * 100 + '%'
-      this.auxiliaryLinePos.offsetX = -(x/scale)
-      this.auxiliaryLinePos.offsetY = -(y/scale)
-    });
-    pan.on("panend", (e) => {
-      const {x, y, scale} = e.getTransform();
-      this.auxiliaryLinePos.width = (1/scale) * 100 + '%'
-      this.auxiliaryLinePos.height = (1/scale) * 100 + '%'
-      this.auxiliaryLinePos.offsetX = -(x/scale)
-      this.auxiliaryLinePos.offsetY = -(y/scale)
+      this.auxiliaryLinePos.width = (1 / scale) * 100 + '%'
+      this.auxiliaryLinePos.height = (1 / scale) * 100 + '%'
+      this.auxiliaryLinePos.offsetX = -(x / scale)
+      this.auxiliaryLinePos.offsetY = -(y / scale)
+    })
+    pan.on('panend', (e) => {
+      const { x, y, scale } = e.getTransform()
+      this.auxiliaryLinePos.width = (1 / scale) * 100 + '%'
+      this.auxiliaryLinePos.height = (1 / scale) * 100 + '%'
+      this.auxiliaryLinePos.offsetX = -(x / scale)
+      this.auxiliaryLinePos.offsetY = -(y / scale)
     })
 
     // 平移时设置鼠标样式
-    mainContainerWrap.style.cursor = "grab";
-    mainContainerWrap.addEventListener("mousedown", function wrapMousedown() {
-      this.style.cursor = "grabbing";
-      mainContainerWrap.addEventListener("mouseout", function wrapMouseout() {
-        this.style.cursor = "grab";
-      });
-    });
-    mainContainerWrap.addEventListener("mouseup", function wrapMouseup() {
-      this.style.cursor = "grab";
-    });
-  }, 
+    mainContainerWrap.style.cursor = 'grab'
+    mainContainerWrap.addEventListener('mousedown', function wrapMousedown() {
+      this.style.cursor = 'grabbing'
+      mainContainerWrap.addEventListener('mouseout', function wrapMouseout() {
+        this.style.cursor = 'grab'
+      })
+    })
+    mainContainerWrap.addEventListener('mouseup', function wrapMouseup() {
+      this.style.cursor = 'grab'
+    })
+  },
 
   setNodeName(nodeId, name) {
     this.data.nodeList.some((v) => {
-      if(v.id === nodeId) {
+      if (v.id === nodeId) {
         v.nodeName = name
         return true
-      }else {
+      } else {
         return false
       }
     })
@@ -248,12 +245,26 @@ const methods = {
 
   //删除节点
   deleteNode(node) {
-    this.data.nodeList.some((v,index) => {
-      if(v.id === node.id) {
+    if (node.type === 99) {
+      let list = this.data.nodeList.filter((item) => item.type === 99)
+      if (list && list.length === 1) {
+        this.msgInfo(this.$t('cannotDeleteNode'))
+        return false
+      }
+    }
+    if (node.type === 1) {
+      let list = this.data.nodeList.filter((item) => item.type === 1)
+      if (list && list.length === 1) {
+        this.msgInfo(this.$t('cannotDeleteNode'))
+        return false
+      }
+    }
+    this.data.nodeList.some((v, index) => {
+      if (v.id === node.id) {
         this.data.nodeList.splice(index, 1)
         this.jsPlumb.remove(v.id)
         return true
-      }else {
+      } else {
         return false
       }
     })
@@ -261,13 +272,12 @@ const methods = {
 
   //更改连线状态
   changeLineState(nodeId, val) {
-    console.log(val)
     let lines = this.jsPlumb.getAllConnections()
-    lines.forEach(line => {
-      if(line.targetId === nodeId || line.sourceId === nodeId) {
-        if(val) {
+    lines.forEach((line) => {
+      if (line.targetId === nodeId || line.sourceId === nodeId) {
+        if (val) {
           line.canvas.classList.add('active')
-        }else {
+        } else {
           line.canvas.classList.remove('active')
         }
       }
@@ -276,21 +286,25 @@ const methods = {
 
   //初始化节点位置  (以便对齐,居中)
   fixNodesPosition() {
-    if(this.data.nodeList && this.$refs.flowWrap) {
+    if (this.data.nodeList && this.$refs.flowWrap) {
       const nodeWidth = 120
       const nodeHeight = 40
       let wrapInfo = this.$refs.flowWrap.getBoundingClientRect()
-      let maxLeft = 0, minLeft = wrapInfo.width, maxTop = 0, minTop = wrapInfo.height;
+      let maxLeft = 0,
+        minLeft = wrapInfo.width,
+        maxTop = 0,
+        minTop = wrapInfo.height
       let nodePoint = {
         left: 0,
         right: 0,
         top: 0,
-        bottom: 0
+        bottom: 0,
       }
-      let fixTop = 0, fixLeft = 0;
-      this.data.nodeList.forEach(el => {
-        let top = Number(el.top.substring(0, el.top.length -2))
-        let left = Number(el.left.substring(0, el.left.length -2))
+      let fixTop = 0,
+        fixLeft = 0
+      this.data.nodeList.forEach((el) => {
+        let top = Number(el.top.substring(0, el.top.length - 2))
+        let left = Number(el.left.substring(0, el.left.length - 2))
         maxLeft = left > maxLeft ? left : maxLeft
         minLeft = left < minLeft ? left : minLeft
         maxTop = top > maxTop ? top : maxTop
@@ -299,19 +313,19 @@ const methods = {
       nodePoint.left = minLeft
       nodePoint.right = wrapInfo.width - maxLeft - nodeWidth
       nodePoint.top = minTop
-      nodePoint.bottom = wrapInfo.height - maxTop - nodeHeight;
+      nodePoint.bottom = wrapInfo.height - maxTop - nodeHeight
 
-      fixTop = nodePoint.top !== nodePoint.bottom ? (nodePoint.bottom - nodePoint.top) / 2 : 0;
-      fixLeft = nodePoint.left !== nodePoint.right ? (nodePoint.right - nodePoint.left) / 2 : 0;
+      fixTop = nodePoint.top !== nodePoint.bottom ? (nodePoint.bottom - nodePoint.top) / 2 : 0
+      fixLeft = nodePoint.left !== nodePoint.right ? (nodePoint.right - nodePoint.left) / 2 : 0
 
-      this.data.nodeList.map(el => {
-        let top = Number(el.top.substring(0, el.top.length - 2)) + fixTop;
-        let left = Number(el.left.substring(0, el.left.length - 2)) + fixLeft;
-        el.top = (Math.round(top/20))* 20 + 'px'
-        el.left = (Math.round(left/20))*20 + 'px'
+      this.data.nodeList.map((el) => {
+        let top = Number(el.top.substring(0, el.top.length - 2)) + fixTop
+        let left = Number(el.left.substring(0, el.left.length - 2)) + fixLeft
+        el.top = Math.round(top / 20) * 20 + 'px'
+        el.left = Math.round(left / 20) * 20 + 'px'
       })
     }
-  }, 
+  },
 }
 
-export default methods;
+export default methods

+ 59 - 6
src/views/process/processNode/index.vue

@@ -14,13 +14,12 @@
             <el-table-column :label="$t('process.generalProcess.codeName')" prop="name" />
             <el-table-column :label="$t('process.generalProcess.version')" align="center" width="120">
               <template slot-scope="scope">
-                <span style="color: #409eff; cursor: pointer">v{{ scope.row.versionNumber }}</span>
+                <span style="color: #409eff; cursor: pointer" @click="handleVersion(scope.row)">v{{ scope.row.versionNumber }}</span>
               </template>
             </el-table-column>
-            <el-table-column :label="$t('operation')" align="center" width="140">
+            <el-table-column :label="$t('operation')" align="center" width="100">
               <template slot-scope="scope">
                 <el-button type="text" @click="handleNewVersion(scope.row)">{{ $t('process.generalProcess.newVersion') }}</el-button>
-                <el-button type="text" @click="handleModify(scope.row)">{{ $t('process.generalProcess.modify') }}</el-button>
               </template>
             </el-table-column>
           </el-table>
@@ -29,6 +28,31 @@
           <el-dialog :title="$t('process.generalProcess.add')" :visible.sync="open" width="30%">
             <from-render ref="form" v-model="dialogParams" :form-config="dialogForm" :insideRules="dialogRules"></from-render>
           </el-dialog>
+
+          <el-dialog :title="$t('process.generalProcess.change')" v-if="openChange" :visible.sync="openChange" width="30%">
+            <div style="padding: 0 2vw">
+              <el-form ref="rowData" :model="rowData" label-width="auto">
+                <el-form-item label="业务服务">
+                  <span>{{ rowData.serviceName }}</span>
+                </el-form-item>
+                <el-form-item label="模块名称">
+                  <span>{{ rowData.moduleName }}</span>
+                </el-form-item>
+                <el-form-item label="流程名称">
+                  <span>{{ rowData.name }}</span>
+                </el-form-item>
+                <el-form-item label="切换版本">
+                  <el-select v-model="rowData.id" placeholder="请选择" style="width: 100%" size="small">
+                    <el-option v-for="item in versionList" :key="item.id" :label="item.name" :value="item.id" />
+                  </el-select>
+                </el-form-item>
+              </el-form>
+              <div style="padding-top: 8px; text-align: center">
+                <el-button size="mini" @click="openChange = false">取 消</el-button>
+                <el-button type="primary" size="mini" @click="handleSubmit">确定</el-button>
+              </div>
+            </div>
+          </el-dialog>
         </el-card>
       </el-col>
     </el-row>
@@ -164,6 +188,9 @@ export default {
         processInfoId: [{ required: true, message: this.$t('process.generalProcess.moduleNameRules'), trigger: 'blur' }],
         name: [{ required: true, message: this.$t('process.generalProcess.flowNameRules'), trigger: 'blur' }],
       },
+      rowData: {},
+      openChange: false,
+      versionList: [],
     }
   },
   created() {
@@ -228,17 +255,38 @@ export default {
         }
       })
     },
-    handleNewVersion(row) {
-      console.log(row)
+    handleVersion(row) {
+      this.rowData = JSON.parse(JSON.stringify(row))
+      this.openChange = true
+      API.processGetVersionByProcessInfoId({ bindingTenantId: '000000', processInfoId: row.processInfoId }).then((res) => {
+        console.log(res)
+        this.versionList = res.data.data.map((item) => {
+          return {
+            id: item.id,
+            name: 'v' + item.versionNumber,
+          }
+        })
+      })
     },
-    handleModify(row) {
+    handleNewVersion(row) {
       this.$router.push({
         path: '/nodeConfig',
         query: {
           id: row.id,
+          processInfoId: row.processInfoId,
         },
       })
     },
+    handleSubmit() {
+      if (!this.rowData.id) {
+        return this.msgInfo(this.$t('process.node.selectVersion'))
+      }
+      API.processEditVersion({ processInfoId: this.rowData.processInfoId, bindingTenantId: '000000', id: this.rowData.id }).then(() => {
+        this.openChange = false
+        this.msgSuccess(this.$t('process.node.switchSuccess'))
+        this.getList()
+      })
+    },
   },
 }
 </script>
@@ -248,4 +296,9 @@ export default {
   height: calc(100vh - 60px);
   overflow-y: auto;
 }
+::v-deep {
+  .el-form-item {
+    margin-bottom: 8px;
+  }
+}
 </style>

+ 470 - 218
src/views/process/processNode/nodeConfig.vue

@@ -1,245 +1,497 @@
 
 
 <template>
-	<div class="video-setting flow_region">
-		<el-row :gutter="20">
-			<el-col :span="3">
-				<el-card shadow="always" :body-style="{ padding: '20px' }">
-					<div slot="header">
-						<span>节点列表</span>
-					</div>
-					<div class="nodes-wrap">
-						<div
-							v-for="item in nodeTypeList"
-							:key="item.type"
-							class="node"
-							draggable="true"
-							@dragstart="drag($event, item)"
-						>
-							<div class="name">{{ item.typeName }}</div>
-						</div>
-					</div>
-				</el-card>
-			</el-col>
+  <div class="video-setting flow_region">
+    <el-row :gutter="20">
+      <el-col :span="3">
+        <el-card class="box-card" shadow="always" :body-style="{ padding: '20px' }">
+          <div slot="header">
+            <span>{{ $t('process.nodeList') }}</span>
+          </div>
+          <div class="nodes-wrap">
+            <div v-for="item in nodeTypeList" :key="item.type" class="node" draggable="true" @dragstart="drag($event, item)">
+              <div class="name">{{ item.typeName }}</div>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
 
-			<el-col :span="16">
-				<el-card class="box-card">
-					<div slot="header">
-						<span>{{ $t('process.processList') }}</span>
-					</div>
-					<div
-						id="flowWrap"
-						ref="flowWrap"
-						class="flow-wrap"
-						@drop="drop($event)"
-						@dragover="allowDrop($event)"
-					>
-						<div id="flow">
-							<div
-								v-show="auxiliaryLine.isShowXLine"
-								class="auxiliary-line-x"
-								:style="{
-									width: auxiliaryLinePos.width,
-									top: auxiliaryLinePos.y + 'px',
-									left: auxiliaryLinePos.offsetX + 'px',
-								}"
-							></div>
-							<div
-								v-show="auxiliaryLine.isShowYLine"
-								class="auxiliary-line-y"
-								:style="{
-									height: auxiliaryLinePos.height,
-									left: auxiliaryLinePos.x + 'px',
-									top: auxiliaryLinePos.offsetY + 'px',
-								}"
-							></div>
-							<flowNode
-								v-for="item in data.nodeList"
-								:id="item.id"
-								:key="item.id"
-								:node="item"
-								@setNodeName="setNodeName"
-								@deleteNode="deleteNode"
-								@changeLineState="changeLineState"
-							></flowNode>
-						</div>
-					</div>
-				</el-card>
-			</el-col>
-            <el-col :span="5">
-				<el-card shadow="always" :body-style="{ padding: '20px' }">
-					<div slot="header">
-						<span>节点设置</span>
-					</div>
-				</el-card>
-			</el-col>
-		</el-row>
-	</div>
+      <el-col :span="16">
+        <el-card class="box-card">
+          <div slot="header" style="position: relative;">
+            <span>{{ $t('process.processList') }}</span>
+            <el-button type="primary" size="small" style="position: absolute; top: -6px; right: 16px" @click="clickSubmit">{{ $t('submit') }}</el-button>
+          </div>
+          <div id="flowWrap" ref="flowWrap" class="flow-wrap" @drop="drop($event)" @dragover="allowDrop($event)">
+            <div id="flow">
+              <div
+                v-show="auxiliaryLine.isShowXLine"
+                class="auxiliary-line-x"
+                :style="{
+                  width: auxiliaryLinePos.width,
+                  top: auxiliaryLinePos.y + 'px',
+                  left: auxiliaryLinePos.offsetX + 'px',
+                }"
+              ></div>
+              <div
+                v-show="auxiliaryLine.isShowYLine"
+                class="auxiliary-line-y"
+                :style="{
+                  height: auxiliaryLinePos.height,
+                  left: auxiliaryLinePos.x + 'px',
+                  top: auxiliaryLinePos.offsetY + 'px',
+                }"
+              ></div>
+              <flowNode
+                v-for="item in data.nodeList"
+                :id="item.id"
+                :key="item.id"
+                :node="item"
+                @setNodeName="setNodeName"
+                @deleteNode="deleteNode"
+                @changeLineState="changeLineState"
+                @clickNode="clickNode"
+              ></flowNode>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="5">
+        <el-card class="box-card" shadow="always" :body-style="{ padding: '20px' }">
+          <div slot="header">
+            <span>{{ $t('process.nodeProcess') }}</span>
+          </div>
+          <from-render ref="form" v-model="form" :form-config="formConfig" :insideRules="formRules"></from-render>
+        </el-card>
+      </el-col>
+    </el-row>
+  </div>
 </template>
   <script>
-import * as API from '@/api/process/generalProcess.js'
+import * as API from '@/api/process/processNode.js'
 import fromRender from '@/components/form-test/index.vue'
 import { jsPlumb } from 'jsplumb'
 import { nodeTypeList } from './config/init'
-import {
-	jsplumbSetting,
-	jsplumbConnectOptions,
-	jsplumbSourceOptions,
-	jsplumbTargetOptions,
-} from './config/commonConfig'
+import { jsplumbSetting, jsplumbConnectOptions, jsplumbSourceOptions, jsplumbTargetOptions } from './config/commonConfig'
 import methods from './config/methods'
-import data from './config/data.json'
 import flowNode from './components/node-item'
+
 export default {
-	name: 'equipment-management',
-	components: { fromRender, flowNode },
-	data() {
-		return {
-			jsPlumb: null,
-			currentItem: null,
-			nodeTypeList: nodeTypeList,
-			nodeTypeObj: {},
-			data: {
-				nodeList: [],
-				lineList: [],
-			},
-			selectedList: [],
-			jsplumbSetting: jsplumbSetting,
-			jsplumbConnectOptions: jsplumbConnectOptions,
-			jsplumbSourceOptions: jsplumbSourceOptions,
-			jsplumbTargetOptions: jsplumbTargetOptions,
-			auxiliaryLine: { isShowXLine: false, isShowYLine: false }, //对齐辅助线是否显示
-			auxiliaryLinePos: {
-				width: '100%',
-				height: '100%',
-				offsetX: 0,
-				offsetY: 0,
-				x: 20,
-				y: 20,
-			},
-			commonGrid: [5, 5], //节点移动最小距离
-			selectModuleFlag: false, //多选标识
-			rectAngle: {
-				px: '', //多选框绘制时的起始点横坐标
-				py: '', //多选框绘制时的起始点纵坐标
-				left: 0,
-				top: 0,
-				height: 0,
-				width: 0,
-			},
-		}
-	},
-	created() {
-		//this.getSelectList()
-	},
-	mounted() {
-		this.jsPlumb = jsPlumb.getInstance()
-		this.initNodeTypeObj()
-		this.initNode()
-		this.fixNodesPosition()
-		this.$nextTick(() => {
-			this.init()
-		})
-	},
-	watch: {},
-	methods: {
-		...methods,
-		initNodeTypeObj() {
-			nodeTypeList.map((v) => {
-				this.nodeTypeObj[v.type] = v
-			})
-		},
-		initNode() {
-			this.data.lineList = data.lineList
-			data.nodeList.map((v) => {
-				v.logImg = this.nodeTypeObj[v.type].logImg
-				v.log_bg_color = this.nodeTypeObj[v.type].log_bg_color
-				this.data.nodeList.push(v)
-			})
-		},
-	},
+  name: 'equipment-management',
+  components: { fromRender, flowNode },
+  data() {
+    return {
+      jsPlumb: null,
+      currentItem: null,
+      nodeTypeList: nodeTypeList,
+      nodeTypeObj: {},
+      data: {
+        nodeList: [],
+        lineList: [],
+      },
+      selectedList: [],
+      jsplumbSetting: jsplumbSetting,
+      jsplumbConnectOptions: jsplumbConnectOptions,
+      jsplumbSourceOptions: jsplumbSourceOptions,
+      jsplumbTargetOptions: jsplumbTargetOptions,
+      auxiliaryLine: { isShowXLine: false, isShowYLine: false }, //对齐辅助线是否显示
+      auxiliaryLinePos: {
+        width: '100%',
+        height: '100%',
+        offsetX: 0,
+        offsetY: 0,
+        x: 20,
+        y: 20,
+      },
+      commonGrid: [5, 5], //节点移动最小距离
+      selectModuleFlag: false, //多选标识
+      rectAngle: {
+        px: '', //多选框绘制时的起始点横坐标
+        py: '', //多选框绘制时的起始点纵坐标
+        left: 0,
+        top: 0,
+        height: 0,
+        width: 0,
+      },
+      transferParams: {
+        id: '',
+        processInfoId: '',
+      },
+      form: {
+        id: '',
+        name: '',
+        handleObjectType: '',
+        handleObjectIdSet: [],
+      },
+      formConfig: {
+        name: {
+          label: this.$t('process.node.nodeName'),
+          type: 'input',
+        },
+        handleObjectType: {
+          label: this.$t('process.node.type'),
+          type: 'select',
+          data: [
+            {
+              value: 1,
+              label: '全部',
+            },
+            {
+              value: 2,
+              label: '指定用户',
+            },
+            {
+              value: 3,
+              label: '指定角色',
+            },
+            {
+              value: 4,
+              label: '指定部门',
+            },
+          ],
+          keyName: 'value',
+          labelName: 'label',
+          change: (val) => {
+            this.form.handleObjectIdSet = ''
+            if (val === 1) {
+              this.formConfig.handleObjectIdSet.disabled = true
+              this.formRules.handleObjectIdSet[0].required = false
+            } else {
+              this.formConfig.handleObjectIdSet.disabled = false
+              this.formRules.handleObjectIdSet[0].required = true
+              if (val === 2) {
+              } else if (val === 3) {
+                API.roleTree().then((res) => {
+                  this.formConfig.handleObjectIdSet.data = this.treeTurnList([], res.data.data)
+                })
+              } else if (val === 4) {
+                API.deptTree().then((res) => {
+                  this.formConfig.handleObjectIdSet.data = this.treeTurnList([], res.data.data)
+                })
+              } else {
+                this.formConfig.handleObjectIdSet.data = []
+              }
+            }
+          },
+        },
+        handleObjectIdSet: {
+          label: this.$t('process.node.operationType'),
+          type: 'select',
+          data: [],
+          keyName: 'id',
+          labelName: 'name',
+          multiple: true,
+          disabled: false,
+        },
+        otherButton: {
+          align: 'center',
+          list: [
+            {
+              name: this.$t('submitText'),
+              methodsText: 'submit',
+              type: 'primary',
+              submit: () => {
+                if (!this.form.id) {
+                  return this.msgInfo(this.$t('process.node.notId'))
+                }
+                this.$refs.form.$refs['form'].validate((valid) => {
+                  if (valid) {
+                    this.data.nodeList = this.data.nodeList.map((item) => {
+                      if (item.id === this.form.id) {
+                        item.nodeName = this.form.name
+                        item.handleObjectType = this.form.handleObjectType
+                        item.handleObjectIdSet = this.form.handleObjectIdSet
+                      }
+                      return {
+                        ...item,
+                      }
+                    })
+                    this.msgSuccess(this.$t('changeSuccess'))
+                  }
+                })
+              },
+            },
+          ],
+        },
+      },
+      formRules: {
+        name: [{ required: true, message: this.$t('process.node.nodeNameRules'), trigger: 'blur' }],
+        handleObjectType: [{ required: true, message: this.$t('process.node.typeRules'), trigger: 'change' }],
+        handleObjectIdSet: [{ required: true, message: this.$t('process.node.operationTypeRules'), trigger: 'change' }],
+      },
+    }
+  },
+  created() {
+    this.jsPlumb = jsPlumb.getInstance()
+    this.initNodeTypeObj()
+    this.fixNodesPosition()
+    this.transferParams = this.$route.query
+  },
+  mounted() {
+    if (this.transferParams.id) {
+      this.getDetails()
+    }
+  },
+  watch: {},
+  methods: {
+    ...methods,
+    initNodeTypeObj() {
+      nodeTypeList.map((v) => {
+        this.nodeTypeObj[v.type] = v
+      })
+    },
+    getDetails() {
+      API.processDetails({ id: this.transferParams.id }).then((res) => {
+        if (!res.data.data.nodeObject) {
+          let data = {
+            nodeList: [
+              {
+                type: 1,
+                typeName: '开始',
+                nodeName: '开始',
+                id: '',
+                top: '320px',
+                left: '240px',
+                handleObjectType: '',
+                handleObjectIdSet: [],
+              },
+              {
+                type: 99,
+                typeName: '结束',
+                nodeName: '结束',
+                id: '',
+                top: '320px',
+                left: '700px',
+                handleObjectType: '',
+                handleObjectIdSet: [],
+              },
+            ],
+            lineList: [
+              {
+                from: '',
+                to: '',
+                label: '连线名称',
+                id: '413363014331985340',
+                Remark: '',
+              },
+            ],
+          }
+          res.data.data.list.map((item) => {
+            if (item.type === 1) {
+              data.nodeList[0].id = item.id
+              data.lineList[0].from = item.id
+            }
+            if (item.type === 99) {
+              data.nodeList[1].id = item.id
+              data.lineList[0].to = item.id
+            }
+          })
+          this.data = data
+        } else {
+          this.data.nodeList = JSON.parse(res.data.data.nodeObject)
+          this.data.lineList = JSON.parse(res.data.data.lineObject)
+        }
+        this.$nextTick(() => {
+          this.init()
+        })
+      })
+    },
+    clickSubmit() {
+      let startList = Array.from(
+        new Set(
+          this.data.nodeList
+            .filter((item) => {
+              return item.type === 1
+            })
+            .map((item) => {
+              return item.id
+            }),
+        ),
+      )
+      if (startList && startList.length !== 1) {
+        return this.msgInfo(this.$t('process.startNotOnly'))
+      }
+      let nodeList = Array.from(
+        new Set(
+          this.data.nodeList.map((item) => {
+            return item.id
+          }),
+        ),
+      )
+      let lineNodeList = Array.from(
+        new Set(
+          this.data.lineList
+            .map((item) => {
+              return item.from
+            })
+            .concat(
+              this.data.lineList.map((item) => {
+                return item.to
+              }),
+            ),
+        ),
+      )
+      if (nodeList.length !== lineNodeList.length) {
+        return this.msgInfo(this.$t('process.AbnormalFlowConfiguration'))
+      }
+      let toList = Array.from(
+        new Set(
+          this.data.lineList.map((item) => {
+            return item.to
+          }),
+        ),
+      )
+      if (toList.length !== this.data.lineList.length) {
+        return this.msgInfo(this.$t('process.cannotMultipleNodes'))
+      }
+      let parameter = {
+        processInfoId: this.transferParams.processInfoId,
+        bindingTenantId: '000000',
+        nodeObject: JSON.stringify(this.data.nodeList),
+        lineObject: JSON.stringify(this.data.lineList),
+        processNodeList: [],
+      }
+      this.data.nodeList.map((item) => {
+        let parentId = ''
+        let parentList = this.data.lineList.filter((itemLine) => itemLine.to === item.id)
+        if (parentList && parentList.length > 0) {
+          parentId = parentList[0].from
+        }
+        let handleObjectIdSet = ''
+        if (item.handleObjectType === 1) {
+          handleObjectIdSet = item.handleObjectIdSet
+        } else {
+          handleObjectIdSet = item.handleObjectIdSet.join(',')
+        }
+        parameter.processNodeList.push({
+          id: item.id,
+          type: item.type,
+          parentId: parentId || '0',
+          name: item.nodeName,
+          handleObjectType: item.handleObjectType,
+          handleObjectIdSet: handleObjectIdSet,
+        })
+      })
+      API.processAddVersion(parameter).then((res) => {
+        this.msgSuccess(this.$t('saveSuccess'))
+        this.$store.dispatch('delView', this.$route)
+        this.$router.replace({
+          path: '/processNode',
+        })
+      })
+    },
+    clickNode(item) {
+      if (item.handleObjectType === 2) {
+      } else if (item.handleObjectType === 3) {
+        API.roleTree().then((res) => {
+          this.formConfig.handleObjectIdSet.data = this.treeTurnList([], res.data.data)
+        })
+      } else if (item.handleObjectType === 4) {
+        API.deptTree().then((res) => {
+          this.formConfig.handleObjectIdSet.data = this.treeTurnList([], res.data.data)
+        })
+      } else {
+        this.formConfig.handleObjectIdSet.data = []
+      }
+      this.form.id = item.id
+      this.form.name = item.nodeName
+      this.form.handleObjectType = item.handleObjectType
+      this.form.handleObjectIdSet = item.handleObjectIdSet
+      this.$nextTick(() => {
+        this.$refs.form.$refs['form'].clearValidate()
+      })
+    },
+    treeTurnList(data, list) {
+      list.map((item) => {
+        data.push({ id: item.id, name: item.title })
+        if (item.children && item.children.length > 0) {
+          data = this.treeTurnList(data, item.children)
+        }
+      })
+      return data
+    },
+  },
 }
 </script>
-  
-<style lang="scss" scoped>
 
+<style lang="scss" scoped>
 .box-card {
-	height: calc(100vh - 110px);
-	overflow-y: auto;
+  height: calc(100vh - 60px);
+  overflow-y: auto;
 }
 .jtk-connector.active {
-	z-index: 9999;
-	path {
-		stroke: #150042;
-		stroke-width: 1.5;
-		animation: ring;
-		animation-duration: 3s;
-		animation-timing-function: linear;
-		animation-iteration-count: infinite;
-		stroke-dasharray: 5;
-	}
+  z-index: 9999;
+  path {
+    stroke: #150042;
+    stroke-width: 1.5;
+    animation: ring;
+    animation-duration: 3s;
+    animation-timing-function: linear;
+    animation-iteration-count: infinite;
+    stroke-dasharray: 5;
+  }
 }
 @keyframes ring {
-	from {
-		stroke-dashoffset: 50;
-	}
-	to {
-		stroke-dashoffset: 0;
-	}
+  from {
+    stroke-dashoffset: 50;
+  }
+  to {
+    stroke-dashoffset: 0;
+  }
 }
 </style>
 <style lang="scss" scoped>
 .flow_region {
-	.nodes-wrap {
-		width: 100%;
-		height: 100%;
-        text-align: center;
-		.node {
-			display: flex;
-			height: 40px;
-			width: 100%;
-			margin: 5px auto;
-			border: 1px solid #ccc;
-			line-height: 40px;
-			&:hover {
-				cursor: grab;
-			}
-			&:active {
-				cursor: grabbing;
-			}
-			.log {
-				width: 40px;
-				height: 40px;
-			}
-			.name {
-				width: 0;
-				flex-grow: 1;
-			}
-		}
-	}
-	.flow-wrap {
-		height: calc(100vh - 300px);
-		position: relative;
-		overflow: hidden;
-		outline: none !important;
-		flex-grow: 1;
-		// background-image: url('../assets/point.png');
-		#flow {
-			position: relative;
-			width: 100%;
-			height: 100%;
-			.auxiliary-line-x {
-				position: absolute;
-				border: 0.5px dashed #2ab1e8;
-				z-index: 9999;
-			}
-			.auxiliary-line-y {
-				position: absolute;
-				border: 0.5px dashed #2ab1e8;
-				z-index: 9999;
-			}
-		}
-	}
+  .nodes-wrap {
+    width: 100%;
+    height: 100%;
+    text-align: center;
+    .node {
+      display: flex;
+      height: 40px;
+      width: 100%;
+      margin: 5px auto;
+      border: 1px solid #ccc;
+      line-height: 40px;
+      &:hover {
+        cursor: grab;
+      }
+      &:active {
+        cursor: grabbing;
+      }
+      .log {
+        width: 40px;
+        height: 40px;
+      }
+      .name {
+        width: 0;
+        flex-grow: 1;
+      }
+    }
+  }
+  .flow-wrap {
+    height: calc(100vh - 160px);
+    position: relative;
+    overflow: hidden;
+    outline: none !important;
+    flex-grow: 1;
+    // background-image: url('../assets/point.png');
+    #flow {
+      position: relative;
+      width: 100%;
+      height: 100%;
+      .auxiliary-line-x {
+        position: absolute;
+        border: 0.5px dashed #2ab1e8;
+        z-index: 9999;
+      }
+      .auxiliary-line-y {
+        position: absolute;
+        border: 0.5px dashed #2ab1e8;
+        z-index: 9999;
+      }
+    }
+  }
 }
 </style>