Просмотр исходного кода

仿钉钉设计流程:增加并行分支

jason 1 год назад
Родитель
Сommit
32d2eb11f0

+ 139 - 55
src/components/SimpleProcessDesigner/src/addNode.vue

@@ -6,7 +6,7 @@
         <div class="add-node-popover-body">
           <a class="add-node-popover-item approver" @click="addType(1)">
             <div class="item-wrapper">
-              <span class="iconfont"></span>
+              <span class="iconfont icon-approve"></span>
             </div>
             <p>审批人</p>
           </a>
@@ -26,20 +26,26 @@
           -->
           <a class="add-node-popover-item notifier" @click="addType(2)">
             <div class="item-wrapper">
-              <span class="iconfont"></span>
+              <span class="iconfont icon-copy"></span>
             </div>
             <p>抄送人</p>
           </a>
           <a class="add-node-popover-item condition" @click="addType(4)">
             <div class="item-wrapper">
-              <span class="iconfont"></span>
+              <span class="iconfont icon-exclusive"></span>
             </div>
             <p>条件分支</p>
           </a>
+          <a class="add-node-popover-item condition" @click="addType(5)">
+            <div class="item-wrapper">
+              <span class="iconfont icon-parallel"></span>
+            </div>
+            <p>并行分支</p>
+          </a>
         </div>
         <template #reference>
-          <button class="btn" type="button">
-            <span class="iconfont"></span>
+          <button class="btn" type="button" v-if="showAddButton">
+            <span><Icon icon="ep:plus" class="addIcon" :size="14" /></span>
           </button>
         </template>
       </el-popover>
@@ -47,66 +53,60 @@
   </div>
 </template>
 <script lang="ts" setup>
+import { NodeType } from './consts'
 import { ref } from 'vue'
 import { generateUUID } from '@/utils'
 let props = defineProps({
   childNodeP: {
     type: Object,
     default: () => ({})
+  },
+  showAddButton:{
+    type:Boolean,
+    default:true
   }
 })
 let emits = defineEmits(['update:childNodeP'])
 let visible = ref(false)
-const addType = (type) => {
+const addType = (type: number) => {
   visible.value = false
-  if (type != 4) {
-    var data
-    if (type == 1) {
-      // data = {
-      //   name: '审核人',
-      //   error: true,
-      //   type: 1,
-      //   settype: 1,
-      //   selectMode: 0,
-      //   selectRange: 0,
-      //   directorLevel: 1,
-      //   examineMode: 1,
-      //   noHanderAction: 1,
-      //   examineEndDirectorLevel: 0,
-      //   childNode: props.childNodeP,
-      //   nodeUserList: []
-      // }
-      data = {
-        name: '审核人',
-        error: true,
-        type: 1,
-        // 审批节点配置
-        attributes : {
-          approveMethod : undefined,
-          candidateStrategy: undefined,
-          candidateParam: undefined
-        },
-        childNode: props.childNodeP
-      }
-    } else if (type == 2) {
-      data = {
-        name: '抄送人',
-        type: 2,
-        error: true,
-         // 抄送节点配置
-        attributes : {
-          candidateStrategy: undefined,
-          candidateParam: undefined
-        },
-        childNode: props.childNodeP
-      }
+  // 审核节点
+  if (type === NodeType.APPROVE_USER_NODE) {
+    const data = {
+      name: '审核人',
+      error: true,
+      type: 1,
+      // 审批节点配置
+      attributes: {
+        approveMethod: undefined,
+        candidateStrategy: undefined,
+        candidateParam: undefined
+      },
+      childNode: props.childNodeP
+    }
+    emits('update:childNodeP', data)
+  }
+  // 抄送节点
+  if (type === NodeType.CC_USER_NODE) {
+    const data = {
+      name: '抄送人',
+      type: 2,
+      error: true,
+      // 抄送节点配置
+      attributes: {
+        candidateStrategy: undefined,
+        candidateParam: undefined
+      },
+      childNode: props.childNodeP
     }
     emits('update:childNodeP', data)
-  } else {
-    emits('update:childNodeP', {
-      name: '路由',
+  }
+  // 条件分支
+  if (type === NodeType.EXCLUSIVE_NODE) {
+    const data = {
+      name: '条件分支',
       type: 4,
-      id : 'GateWay_'+ generateUUID(),
+      id: 'GateWay_' + generateUUID(),
       childNode: props.childNodeP,
       conditionNodes: [
         {
@@ -125,8 +125,92 @@ const addType = (type) => {
           childNode: null
         }
       ]
-    })
+    }
+    emits('update:childNodeP', data)
   }
+  // 并行分支 fork
+  if (type === NodeType.PARALLEL_NODE_FORK) {
+    const data = {
+      name: '并行分支_FORK',
+      type: 5,
+      id: 'GateWay_' + generateUUID(),
+      conditionNodes: [
+        {
+          name: '并行1',
+          error: true,
+          type: 3,
+          childNode: null
+        },
+        {
+          name: '并行2',
+          type: 3,
+          childNode: null
+        }
+      ],
+      childNode: {
+        id: 'GateWay_' + generateUUID(),
+        name: '并行分支_JOIN',
+        type: 6,
+        error: true,
+        childNode: props.childNodeP,
+      }
+    }
+    emits('update:childNodeP', data)
+  }
+  // if (type != 4) {
+  //   var data
+  //   if (type == 1) {
+  //     data = {
+  //       name: '审核人',
+  //       error: true,
+  //       type: 1,
+  //       // 审批节点配置
+  //       attributes : {
+  //         approveMethod : undefined,
+  //         candidateStrategy: undefined,
+  //         candidateParam: undefined
+  //       },
+  //       childNode: props.childNodeP
+  //     }
+  //   } else if (type == 2) {
+  //     data = {
+  //       name: '抄送人',
+  //       type: 2,
+  //       error: true,
+  //        // 抄送节点配置
+  //       attributes : {
+  //         candidateStrategy: undefined,
+  //         candidateParam: undefined
+  //       },
+  //       childNode: props.childNodeP
+  //     }
+  //   }
+  //   emits('update:childNodeP', data)
+  // } else {
+  //   emits('update:childNodeP', {
+  //     name: '路由',
+  //     type: 4,
+  //     id : 'GateWay_'+ generateUUID(),
+  //     childNode: props.childNodeP,
+  //     conditionNodes: [
+  //       {
+  //         name: '条件1',
+  //         error: true,
+  //         type: 3,
+  //         priorityLevel: 1,
+  //         conditionList: [],
+  //         childNode: null
+  //       },
+  //       {
+  //         name: '其它情况',
+  //         type: 3,
+  //         priorityLevel: 2,
+  //         conditionList: [],
+  //         childNode: null
+  //       }
+  //     ]
+  //   })
+  // }
 }
 </script>
 <style scoped lang="scss">
@@ -173,9 +257,9 @@ const addType = (type) => {
       box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
       -webkit-transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
       transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
-  
-      .iconfont {
-        font-size: 16px;
+
+      .addIcon {
+        line-height: 30px;
         color: #fff;
       }
 

+ 10 - 2
src/components/SimpleProcessDesigner/src/consts.ts

@@ -18,9 +18,17 @@ export enum NodeType {
    */
   CONDITION_NODE = 3,
   /**
-   * 路由节点
+   * 条件分支节点
    */
-  ROUTER_NODE = 4
+  EXCLUSIVE_NODE = 4,
+  /**
+   * 并行分支分叉节点
+   */
+  PARALLEL_NODE_FORK = 5,
+  /**
+   * 并行分支聚合
+   */
+  PARALLEL_NODE_JOIN = 6
 }
 
 export const NODE_BG_COLOR = new Map()

+ 109 - 35
src/components/SimpleProcessDesigner/src/nodeWrap.vue

@@ -7,17 +7,15 @@
 -->
 <template>
   <div class="node-wrap" v-if="nodeConfig.type < 3">
-    <div
-      class="node-wrap-box"
-      :class="
-        (nodeConfig.type == 0 ? 'start-node ' : '') +
-        (isTried && nodeConfig.error ? 'active error' : '')
-      "
-    >
+    <div class="node-wrap-box" :class="isTried && nodeConfig.error ? 'active error' : ''">
       <div class="title" :style="`background: rgb(${bgColors[nodeConfig.type]});`">
         <span v-if="nodeConfig.type == 0">发起人</span>
         <template v-else>
-          <span class="iconfont">{{ nodeConfig.type == 1 ? '' : '' }}</span>
+          <span
+            class="iconfont"
+            :class="nodeConfig.type === NodeType.APPROVE_USER_NODE ? 'icon-approve' : 'icon-copy'"
+          >
+          </span>
           <input
             v-if="isInput"
             type="text"
@@ -37,9 +35,9 @@
           <span class="placeholder" v-if="!showText">请选择{{ defaultText }}</span>
           <span v-html="showText" class="ellipsis-text" v-else></span>
         </div>
-        <div class="icon-box">
+        <!-- <div class="icon-box">
           <i class="anticon anticon-edit" @click="editNode"></i>
-        </div>
+        </div> -->
         <i class="anticon anticon-right arrow"></i>
       </div>
       <div class="error_tip" v-if="isTried && nodeConfig.error">
@@ -51,7 +49,7 @@
   <div class="branch-wrap" v-if="nodeConfig.type == 4">
     <div class="branch-box-wrap">
       <div class="branch-box">
-        <button class="add-branch" @click="addTerm">添加条件</button>
+        <button class="add-branch" @click="addTerm(nodeConfig.type)">添加条件</button>
         <div class="col-box" v-for="(item, index) in nodeConfig.conditionNodes" :key="index">
           <div class="condition-node">
             <div class="condition-node-box">
@@ -89,7 +87,7 @@
                 <!-- <div class="content" @click="setPerson(item.priorityLevel)">{{
                   conditionStr(nodeConfig, index)
                 }}</div> -->
-                <div class="content">{{conditionStr(nodeConfig, index) }}</div>
+                <div class="content">{{ conditionStr(nodeConfig, index) }}</div>
                 <div class="error_tip" v-if="isTried && item.error">
                   <i class="anticon anticon-exclamation-circle"></i>
                 </div>
@@ -111,6 +109,59 @@
       <addNode v-model:childNodeP="nodeConfig.childNode" />
     </div>
   </div>
+  <div class="branch-wrap" v-if="nodeConfig.type == 5">
+    <div class="branch-box-wrap">
+      <div class="branch-box">
+        <button class="add-branch" @click="addTerm(nodeConfig.type)">添加分支</button>
+        <div class="col-box" v-for="(item, index) in nodeConfig.conditionNodes" :key="index">
+          <div class="condition-node">
+            <div class="condition-node-box">
+              <div class="title-wrapper" :style="`background: rgb(${bgColors[nodeConfig.type]});`">
+                <input
+                  v-if="isInputList[index]"
+                  type="text"
+                  class="ant-input editable-title-input"
+                  @blur="blurEvent(index)"
+                  @focus="$event.currentTarget?.select()"
+                  v-mountedFoucs
+                  v-model="item.name"
+                />
+                <span v-else class="editable-title" @click="clickEvent(index)">{{
+                  item.name
+                }}</span>
+                <i class="anticon anticon-close close" @click="delTerm(nodeConfig.type, index)"></i>
+              </div>
+              <div class="auto-judge" :class="isTried && item.error ? 'error active' : ''">
+                <div class="content">并行执行</div>
+                <div class="error_tip" v-if="isTried && item.error">
+                  <i class="anticon anticon-exclamation-circle"></i>
+                </div>
+              </div>
+              <addNode v-model:childNodeP="item.childNode" />
+            </div>
+          </div>
+          <nodeWrap v-if="item.childNode" v-model:nodeConfig="item.childNode" />
+          <template v-if="index == 0">
+            <div class="top-left-cover-line"></div>
+            <div class="bottom-left-cover-line"></div>
+          </template>
+          <template v-if="index == nodeConfig.conditionNodes.length - 1">
+            <div class="top-right-cover-line"></div>
+            <div class="bottom-right-cover-line"></div>
+          </template>
+        </div>
+      </div>
+      <addNode v-model:childNodeP="nodeConfig.childNode" :show-add-button="false" />
+    </div>
+  </div>
+  <div class="node-wrap" v-if="nodeConfig.type === 6">
+    <div class="node-wrap-box">
+      <div class="content">
+        <div class="text">聚合</div>
+      </div>
+    </div>
+    <addNode v-model:childNodeP="nodeConfig.childNode" />
+  </div>
   <nodeWrap v-if="nodeConfig.childNode" v-model:nodeConfig="nodeConfig.childNode" />
 </template>
 <script lang="ts" setup>
@@ -124,7 +175,7 @@ import {
   placeholderList,
   getApproverShowText
 } from './util'
-import { WorkFlowNode } from './consts'
+import { WorkFlowNode, NodeType } from './consts'
 import { useWorkFlowStoreWithOut } from '@/store/modules/simpleWorkflow'
 let _uid = getCurrentInstance().uid
 
@@ -170,7 +221,7 @@ let {
   setUserTaskConfig
 } = store
 // ???
-const  isTried = computed(() => store.isTried) 
+const isTried = computed(() => store.isTried)
 // 审批节点的配置
 const userTaskConfig = computed(() => store.userTaskConfig)
 // 抄送节点的配置
@@ -188,13 +239,12 @@ const showText = computed(() => {
       return ''
     }
   }
-  if(props.nodeConfig.type === 2) {
-    if(props.nodeConfig.attributes) {
-      return copyerStr( props.nodeConfig.attributes.candidateStrategy)
+  if (props.nodeConfig.type === 2) {
+    if (props.nodeConfig.attributes) {
+      return copyerStr(props.nodeConfig.attributes.candidateStrategy)
     } else {
       return ''
     }
-    
   }
   return ''
 })
@@ -204,9 +254,9 @@ watch(userTaskConfig, (approver) => {
   }
 })
 watch(copyerConfig, (copyer) => {
-  console.log('copyer',copyer)
+  console.log('copyer', copyer)
   if (copyer.flag && copyer.id === _uid) {
-    console.log('copyer id is equal',copyer)
+    console.log('copyer id is equal', copyer)
     emits('update:nodeConfig', copyer.value)
   }
 })
@@ -230,7 +280,7 @@ const blurEvent = (index) => {
     isInputList.value[index] = false
     // eslint-disable-next-line vue/no-mutating-props
     props.nodeConfig.conditionNodes[index].name =
-    props.nodeConfig.conditionNodes[index].name || '条件'
+      props.nodeConfig.conditionNodes[index].name || '条件'
   } else {
     isInput.value = false
     // eslint-disable-next-line vue/no-mutating-props
@@ -240,42 +290,66 @@ const blurEvent = (index) => {
 const delNode = () => {
   emits('update:nodeConfig', props.nodeConfig.childNode)
 }
-const addTerm = () => {
+const addTerm = (type:number) => {
   const len = props.nodeConfig.conditionNodes.length
   let lastIndex = props.nodeConfig.conditionNodes.length - 1
+  let nodeName = '条件' + len
+  if(type === NodeType.PARALLEL_NODE_FORK) {
+    nodeName = '并行' + (len+1);
+    lastIndex = props.nodeConfig.conditionNodes.length;
+  }
   // eslint-disable-next-line vue/no-mutating-props
   props.nodeConfig.conditionNodes.splice(lastIndex, 0, {
-    name: '条件' + len,
+    name: nodeName,
     type: 3,
-    priorityLevel: len,
     conditionList: [],
     childNode: null
   })
   resetConditionNodesErr()
   emits('update:nodeConfig', props.nodeConfig)
 }
-const delTerm = (index) => {
+const delTerm = (nodeType: number, index: number) => {
   if (props.nodeConfig.conditionNodes) {
     // eslint-disable-next-line vue/no-mutating-props
     props.nodeConfig.conditionNodes.splice(index, 1)
-    props.nodeConfig.conditionNodes.map((item, index) => {
+    if (nodeType === NodeType.PARALLEL_NODE_FORK) {
+      props.nodeConfig.conditionNodes.map((item, index) => {
+          item.name = `并行${index + 1}`
+      })
+    } else {
+      props.nodeConfig.conditionNodes.map((item, index) => {
       // item.priorityLevel = index + 1
       if (index !== props.nodeConfig.conditionNodes.length - 1) {
         item.name = `条件${index + 1}`
       }
     })
+    }
+   
     resetConditionNodesErr()
     emits('update:nodeConfig', props.nodeConfig)
     if (props.nodeConfig.conditionNodes.length == 1) {
-      if (props.nodeConfig.childNode) {
-        if (props.nodeConfig.conditionNodes[0].childNode) {
-          reData(props.nodeConfig.conditionNodes[0].childNode, props.nodeConfig.childNode)
-        } else {
-          // eslint-disable-next-line vue/no-mutating-props
-          props.nodeConfig.conditionNodes[0].childNode = props.nodeConfig.childNode
+      if (nodeType === NodeType.PARALLEL_NODE_FORK) {
+        const joinNode = props.nodeConfig.childNode;
+        if (joinNode?.childNode) {
+          if (props.nodeConfig.conditionNodes[0].childNode) {
+            reData(props.nodeConfig.conditionNodes[0].childNode, joinNode?.childNode)
+          } else {
+            // eslint-disable-next-line vue/no-mutating-props
+            props.nodeConfig.conditionNodes[0].childNode = joinNode?.childNode
+          }
+        }
+        emits('update:nodeConfig', props.nodeConfig.conditionNodes[0].childNode)
+      } else {
+        if (props.nodeConfig.childNode) {
+          if (props.nodeConfig.conditionNodes[0].childNode) {
+            reData(props.nodeConfig.conditionNodes[0].childNode, props.nodeConfig.childNode)
+          } else {
+            // eslint-disable-next-line vue/no-mutating-props
+            props.nodeConfig.conditionNodes[0].childNode = props.nodeConfig.childNode
+          }
         }
+        emits('update:nodeConfig', props.nodeConfig.conditionNodes[0].childNode)
       }
-      emits('update:nodeConfig', props.nodeConfig.conditionNodes[0].childNode)
     }
   }
 }
@@ -287,9 +361,9 @@ const reData = (data, addData) => {
   }
 }
 const setPerson = (priorityLevel) => {
-  console.log('priorityLevel',priorityLevel)
+  console.log('priorityLevel', priorityLevel)
   const { type } = props.nodeConfig
-  console.log('type',type)
+  console.log('type', type)
   if (type == 0) {
     // setPromoter(true)
   } else if (type == 1) {

+ 1 - 1
src/components/SimpleProcessDesigner/src/util.ts

@@ -187,7 +187,7 @@ export const removeEle = (arr, elem, key = 'id') => {
   arr.splice(includesIndex, 1)
 }
 
-export const bgColors = ['87, 106, 149', '255, 148, 62', '50, 150, 250','50, 150, 250','248, 107, 248']
+export const bgColors = ['87, 106, 149', '255, 148, 62', '50, 150, 250','50, 150, 250','248, 107, 248','244, 118, 118']
 export const placeholderList = ['发起人', '审核人', '抄送人']
 export const setTypes = [
   { value: 1, label: '指定成员' },

BIN
src/components/SimpleProcessDesigner/theme/iconfont.ttf


BIN
src/components/SimpleProcessDesigner/theme/iconfont.woff


BIN
src/components/SimpleProcessDesigner/theme/iconfont.woff2


+ 41 - 2
src/components/SimpleProcessDesigner/theme/workflow.css

@@ -37,7 +37,7 @@
     font-family: anticon!important
 }
 .anticon-close:before {
-  content: "\E633"
+    content: "\E633"
 }
 .anticon-right:before {
     content: "\E61F"
@@ -486,7 +486,7 @@ html {
     height: 100%
 }
 
-@font-face {
+/* @font-face {
     font-family: IconFont;
     src: url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.eot");
     src: url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.eot?#iefix") format("embedded-opentype"), url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.woff") format("woff"), url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.ttf") format("truetype"), url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.svg#IconFont") format("svg")
@@ -499,6 +499,45 @@ html {
     -webkit-font-smoothing: antialiased;
     -webkit-text-stroke-width: .2px;
     -moz-osx-font-smoothing: grayscale
+} */
+
+@font-face {
+    font-family: "iconfont"; /* Project id 4495938 */
+    src: url('iconfont.woff2?t=1712392083512') format('woff2'),
+        url('iconfont.woff?t=1712392083512') format('woff'),
+        url('iconfont.ttf?t=1712392083512') format('truetype');
+}
+  
+.iconfont {
+    font-family: "iconfont" !important;
+    font-size: 16px;
+    font-style: normal;
+    -webkit-font-smoothing: antialiased;
+    -moz-osx-font-smoothing: grayscale;
+}
+  
+.icon-Inclusive:before {
+    content: "\e602";
+}
+  
+.icon-copy:before {
+    content: "\e7eb";
+}
+  
+.icon-handle:before {
+    content: "\e61c";
+}
+  
+.icon-exclusive:before {
+    content: "\e717";
+}
+  
+.icon-approve:before {
+    content: "\e715";
+}
+  
+.icon-parallel:before {
+    content: "\e688";
 }
 
 .fd-nav {