Browse Source

仿钉钉流程设计器- 新增条件分支节点

jason 1 năm trước cách đây
mục cha
commit
9a2dcf2004

+ 41 - 2
src/components/SimpleProcessDesignerV2/src/NodeHandler.vue

@@ -15,6 +15,12 @@
               </div>
               <div class="handler-item-text">抄送</div>
             </div>
+            <div class="handler-item" @click="addNode(NodeType.EXCLUSIVE_NODE)">
+              <div class="handler-item-icon condition">
+                <span class="iconfont icon-size icon-exclusive"></span>
+              </div>
+              <div class="handler-item-text">条件分支</div>
+            </div>
           </div>
           <template #reference>
             <div class="add-icon"><Icon icon="ep:plus" /></div>
@@ -50,7 +56,7 @@ const addNode = (type: number) => {
   popoverShow.value = false
   if (type === NodeType.USER_TASK_NODE) {
     const data: SimpleFlowNode = {
-      id:  generateUUID(),
+      id:  'Activity_'+ generateUUID(),
       name: NODE_DEFAULT_NAME.get(NodeType.USER_TASK_NODE) as string,
       showText: '',
       type: NodeType.USER_TASK_NODE,
@@ -66,7 +72,7 @@ const addNode = (type: number) => {
   }
   if (type === NodeType.COPY_TASK_NODE) {
     const data: SimpleFlowNode = {
-      id:  generateUUID(),
+      id:  'Activity_'+ generateUUID(),
       name: NODE_DEFAULT_NAME.get(NodeType.COPY_TASK_NODE) as string,
       showText: '',
       type: NodeType.COPY_TASK_NODE,
@@ -79,6 +85,39 @@ const addNode = (type: number) => {
     }
     emits('update:childNode', data)
   }
+  if (type === NodeType.EXCLUSIVE_NODE) {
+    const data : SimpleFlowNode = {
+      name: '条件分支',
+      type: NodeType.EXCLUSIVE_NODE,
+      id: 'GateWay_' + generateUUID(),
+      childNode: props.childNode,
+      conditionNodes: [
+        {
+          id: 'Flow_'+ generateUUID(),
+          name: '条件1',
+          showText: '',
+          type: NodeType.CONDITION_NODE,
+          childNode: undefined,
+          attributes: {
+            conditionType: 1,
+            defaultCondition: false
+          }
+        },
+        {
+          id: 'Flow_'+ generateUUID(),
+          name: '其它情况',
+          showText: '其它情况进入此流程',
+          type: NodeType.CONDITION_NODE,
+          childNode: undefined,
+          attributes: {
+            conditionType: undefined,
+            defaultCondition: true
+          }
+        }
+      ]
+    }
+    emits('update:childNode', data)
+  }
 }
 </script>
 

+ 5 - 0
src/components/SimpleProcessDesignerV2/src/ProcessNodeTree.vue

@@ -9,6 +9,10 @@
   <CopyTaskNode
         v-if="currentNode && currentNode.type === NodeType.COPY_TASK_NODE" 
         :flow-node ="currentNode" @update:model-value="handleModelValueUpdate"/>
+  <!-- 条件节点 -->
+  <ExclusiveNode 
+        v-if="currentNode && currentNode.type === NodeType.EXCLUSIVE_NODE" 
+        :flow-node ="currentNode" @update:model-value="handleModelValueUpdate"/>
   <!-- 递归显示孩子节点  -->
   <ProcessNodeTree v-if="currentNode && currentNode.childNode"  v-model:flow-node="currentNode.childNode"/>
 
@@ -20,6 +24,7 @@ import StartEventNode from './nodes/StartEventNode.vue';
 import EndEventNode from './nodes/EndEventNode.vue';
 import UserTaskNode from './nodes/UserTaskNode.vue';
 import CopyTaskNode from './nodes/CopyTaskNode.vue';
+import ExclusiveNode from './nodes/ExclusiveNode.vue';
 import { SimpleFlowNode, NodeType }  from './consts';
 defineOptions({
   name: 'ProcessNodeTree'

+ 6 - 2
src/components/SimpleProcessDesignerV2/src/consts.ts

@@ -112,5 +112,9 @@ export const APPROVE_METHODS: DictDataVO [] = [
   { label: '多人会签(需所有审批人同意)', value: 2 },
   { label: '多人或签(一名审批人同意即可)', value: 3 },
   { label: '依次审批(按顺序依次审批)', value: 4 }
-  // TODO 更多的类型
-]
+]
+
+export const CONDITION_CONFIG_TYPES: DictDataVO [] = [
+  { label: '条件规则', value: 1 },
+  { label: '条件表达式', value: 2 }
+]

+ 126 - 0
src/components/SimpleProcessDesignerV2/src/nodes-config/ConditionNodeConfig.vue

@@ -0,0 +1,126 @@
+<template>
+  <el-drawer
+    :append-to-body="true"
+    v-model="settingVisible"
+    :show-close="false"
+    :size="550"
+    :before-close="saveConfig"
+   >
+   <template #header>
+      <div class="w-full flex flex-col">
+        <div class="mb-2 text-size-2xl">{{ currentNode.name }}</div>
+        <el-divider />
+      </div>
+   </template>
+    <div> 
+      <div class="mb-3 text-size-sm" v-if="currentNode.attributes.defaultCondition">其它条件不满足进入此分支(该分支不可编辑和删除)</div>
+      <div v-else>
+          <el-form label-position="top">
+            <el-form-item label="配置方式" prop="conditionType">
+              <el-radio-group
+                v-model="currentNode.attributes.conditionType"
+                @change="changeConditionType"
+              >
+                <el-radio
+                  v-for="(dict, index) in CONDITION_CONFIG_TYPES"
+                  :key="index"
+                  :value="dict.value"
+                  :label="dict.value"
+                >
+                  {{ dict.label }}
+                </el-radio>
+              </el-radio-group>
+            </el-form-item>
+              
+            <el-form-item
+              v-if="currentNode.attributes.conditionType === 2"
+              label="条件表达式"
+              prop="conditionExpression"
+            >
+              <el-input
+                type="textarea"
+                v-model="currentNode.attributes.conditionExpression"
+                clearable
+                style="width: 100%"
+              />
+            </el-form-item>
+            <el-form-item
+              v-if="currentNode.attributes.conditionType === 1"
+              label="条件规则"
+              prop="conditionExpression"
+            >
+              <span class="text-red-400">待实现</span>
+            </el-form-item>
+          </el-form>
+      </div>    
+    </div>
+    <template #footer>
+      <el-divider />
+      <div>
+        <el-button type="primary" @click="saveConfig">确 定</el-button>
+        <el-button @click="closeDrawer">取 消</el-button>
+      </div>
+    </template>
+  </el-drawer>
+</template>
+<script setup lang="ts">
+import { SimpleFlowNode, CONDITION_CONFIG_TYPES } from '../consts'
+defineOptions({
+  name: 'ConditionNode'
+})
+const props = defineProps({
+  conditionNode: {
+    type: Object as () => SimpleFlowNode,
+    required: true
+  }
+})
+const settingVisible = ref(false)
+const open = () => {
+  settingVisible.value = true
+}
+
+watch(() => props.conditionNode, (newValue) => {  
+  currentNode.value = newValue;  
+}); 
+
+const currentNode = ref<SimpleFlowNode>(props.conditionNode)
+// TODO nodeInfo 测试
+defineExpose({ open, nodeInfo: currentNode }) // 提供 open 方法,用于打开弹窗
+
+// 关闭
+const closeDrawer = () => {
+  settingVisible.value = false
+}
+// 保存配置
+const saveConfig = () => {
+  if (!currentNode.value.attributes.defaultCondition) {
+    currentNode.value.showText = getShowText();
+  }
+  settingVisible.value = false
+}
+const getShowText = () : string => {
+  let showText ='';
+  if (currentNode.value.attributes.conditionType === 1) {
+    showText = '待实现'
+  } 
+  if (currentNode.value.attributes.conditionType === 2) {
+    if (currentNode.value.attributes.conditionExpression) {
+      showText = `表达式:${currentNode.value.attributes.conditionExpression}`
+    }
+  }
+  return showText
+}
+// 改变条件配置方式
+const changeConditionType = () => {
+
+}
+</script>
+
+<style lang="scss" scoped>
+::v-deep(.el-divider--horizontal) {
+  display: block;
+  height: 1px;
+  margin: 0;
+  border-top: 1px var(--el-border-color) var(--el-border-style);
+}
+</style>

+ 0 - 1
src/components/SimpleProcessDesignerV2/src/nodes-config/CopyTaskNodeConfig.vue

@@ -5,7 +5,6 @@
     :show-close="false"
     :size="550"
     :before-close="saveConfig"
-    class="justify-start"
   >
     <template #header>
       <div class="w-full flex flex-col">

+ 147 - 0
src/components/SimpleProcessDesignerV2/src/nodes/ExclusiveNode.vue

@@ -0,0 +1,147 @@
+<template>
+  <div class="branch-node-wrapper">
+    <div class="branch-node-container">
+      <div class="branch-node-add" @click="addCondition">添加条件</div>
+      <div
+        class="branch-node-item"
+        v-for="(item, index) in currentNode.conditionNodes"
+        :key="index"
+      >
+        <template v-if="index == 0">
+          <div class="branch-line-first-top"></div>
+          <div class="branch-line-first-bottom"></div>
+        </template>
+        <template v-if="index + 1 == currentNode.conditionNodes?.length">
+          <div class="branch-line-last-top"></div>
+          <div class="branch-line-last-bottom"></div>
+        </template>
+        <div class="node-wrapper">
+          <div class="node-container">
+            <div class="node-box" :class="{'node-config-error': !item.showText}">
+              <div class="branch-node-title-container">
+                <div class="branch-title" v-if="showInputs[index]">
+                  <input
+                    type="text"
+                    class="input-max-width editable-title-input"
+                    @blur="blurEvent(index)"
+                    v-mountedFocus
+                    v-model="item.name"
+                  />
+                </div>
+                <div v-else class="branch-title" @click="clickEvent(index)"> {{ item.name }} </div>
+                <div class="branch-priority"> 优先级{{ index + 1 }} </div>
+              </div>
+              <div class="node-content" @click="conditionNodeConfig(item.id)">
+                <div class="node-text" :title="item.showText" v-if ="item.showText">
+                  {{ item.showText }}
+                </div>
+                <div class="node-text" v-else >
+                  {{ NODE_DEFAULT_TEXT.get(NodeType.CONDITION_NODE) }}
+                </div>
+                <Icon icon="ep:arrow-right-bold" />
+              </div>
+              <div class="node-toolbar" v-if="index + 1 !== currentNode.conditionNodes?.length">
+                <div class="toolbar-icon"><Icon  icon="ep:circle-close"  @click="deleteCondition(index)"/></div>
+              </div>
+            </div>
+            <NodeHandler v-model:child-node="item.childNode" />
+          </div>
+        </div>
+        <ConditionNodeConfig :condition-node="item" :ref="item.id" />
+        <!-- 递归显示子节点  -->
+        <ProcessNodeTree  v-if="item && item.childNode" v-model:flow-node="item.childNode" />
+      </div>
+    </div>
+    <NodeHandler v-if="currentNode" v-model:child-node="currentNode.childNode" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import NodeHandler from '../NodeHandler.vue'
+import ProcessNodeTree from '../ProcessNodeTree.vue'
+import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
+import { generateUUID } from '@/utils'
+import ConditionNodeConfig from '../nodes-config/ConditionNodeConfig.vue'
+const { proxy } = getCurrentInstance() as any
+defineOptions({
+  name: 'ExclusiveNode'
+})
+const props = defineProps({
+  flowNode: {
+    type: Object as () => SimpleFlowNode,
+    required: true
+  }
+})
+// 定义事件,更新父组件
+const emits = defineEmits<{
+  'update:modelValue': [node : SimpleFlowNode | undefined]
+}>()
+
+const currentNode = ref<SimpleFlowNode>(props.flowNode)
+// const conditionNodes = computed(() => currentNode.value.conditionNodes);
+
+watch(() => props.flowNode, (newValue) => {  
+  currentNode.value = newValue;  
+});
+// TODO 测试后续去掉
+// watch(() => conditionNodes, (newValue) => {  
+//   console.log('new conditionNodes is ', newValue);
+// },{ deep: true }); 
+
+const showInputs = ref<boolean[]>([])
+// 失去焦点
+const blurEvent = (index:number) => {
+  showInputs.value[index] = false
+  const conditionNode =  currentNode.value.conditionNodes?.at(index) as SimpleFlowNode;
+  conditionNode.name =  conditionNode.name || '条件' + index
+}
+// 点击条件名称
+const clickEvent = (index:number) => {
+  showInputs.value[index] = true
+}
+
+const conditionNodeConfig = (nodeId:string) => {
+  console.log('nodeId', nodeId);
+  console.log("proxy.$refs", proxy.$refs);
+  // TODO 测试后续去掉
+  const conditionNode = proxy.$refs[nodeId][0];
+  console.log("node inf is ", conditionNode.nodeInfo);
+  conditionNode.open()
+}
+
+const addCondition = () => {
+  const conditionNodes =  currentNode.value.conditionNodes;
+  if (conditionNodes) {
+    const len = conditionNodes.length
+    let lastIndex = len - 1
+    const conditionData: SimpleFlowNode = {
+      id:  generateUUID(),
+      name: '条件'+len,
+      showText : '',
+      type: NodeType.CONDITION_NODE,
+      childNode: undefined,
+      conditionNodes: [],
+      attributes: {
+        conditionType: 1,
+        defaultCondition: false
+      }
+    }
+    conditionNodes.splice(lastIndex, 0, conditionData)
+  }
+}
+
+const deleteCondition = (index:number) => {
+  const conditionNodes =  currentNode.value.conditionNodes;
+  if (conditionNodes) {
+    conditionNodes.splice(index, 1)
+    if (conditionNodes.length == 1) {
+      const childNode = currentNode.value.childNode
+      // 更新此节点为后续孩子节点
+      emits('update:modelValue', childNode)
+    }
+  }
+}
+
+</script>
+
+<style lang="scss" scoped></style>

+ 13 - 2
src/components/SimpleProcessDesignerV2/theme/simple-process-designer.scss

@@ -132,16 +132,22 @@
         // 条件节点标题
         .branch-node-title-container {
           display: flex;
-          padding: 4px;
+          padding: 4px 0;
           cursor: pointer;
           border-radius: 4px 4px 0 0;
           align-items: center;
           justify-content: space-between;
 
+          .input-max-width {
+            max-width: 115px !important;
+          }
+
           .branch-title {
-            max-width: 120px;
             font-size: 13px;
             font-weight: 600;
+            white-space: nowrap;
+            overflow: hidden;
+            text-overflow: ellipsis;
             color: #f60;
           }
 
@@ -425,6 +431,7 @@
       // 可编辑的 title 输入框
       .editable-title-input {
         height: 20px;
+        max-width: 145px;
         line-height: 20px;
         font-size: 12px;
         margin-left: 4px;
@@ -480,6 +487,10 @@
   .copy {
     color: #3296fa;
   }
+  
+  .condition {
+    color: #15bc83;
+  }
 
  .handler-item-text {
    margin-top: 4px;