瀏覽代碼

仿钉钉流程设计器-前端审批人节点改造.

jason 1 年之前
父節點
當前提交
676ed6bd44

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

@@ -25,7 +25,7 @@
 </template>
 
 <script setup lang="ts">
-import { SimpleFlowNode, NodeType, NODE_DEFAULT_NAME } from './consts'
+import { SimpleFlowNode, NodeType, NODE_DEFAULT_NAME, CandidateStrategy, APPROVE_METHODS } from './consts'
 import { generateUUID } from '@/utils'
 defineOptions({
   name: 'NodeHandler'
@@ -57,7 +57,7 @@ const addNode = (type: number) => {
       // 审批节点配置
       attributes: {
         approveMethod: 1,
-        candidateStrategy: 30,
+        candidateStrategy: CandidateStrategy.USER,
         candidateParam: undefined
       },
       childNode: props.childNode

+ 10 - 6
src/components/SimpleProcessDesignerV2/src/ProcessNodeTree.vue

@@ -1,8 +1,12 @@
 <template>
   <!-- 开始节点 -->
   <StartEventNode v-if="currentNode && currentNode.type === NodeType.START_EVENT_NODE" :flow-node ="currentNode" />
+  <!-- 审批节点 -->
+  <UserTaskNode 
+        v-if="currentNode && currentNode.type === NodeType.USER_TASK_NODE" 
+        :flow-node ="currentNode" @update:model-value="handleModelValueUpdate"/>
 
-  <!-- 递归显示子节点  -->
+  <!-- 递归显示子节点  -->
   <ProcessNodeTree v-if="currentNode && currentNode.childNode"  v-model:flow-node="currentNode.childNode"/>
 
   <!-- 结束节点 -->
@@ -11,7 +15,7 @@
 <script setup lang='ts'>
 import StartEventNode from './nodes/StartEventNode.vue';
 import EndEventNode from './nodes/EndEventNode.vue';
-
+import UserTaskNode from './nodes/UserTaskNode.vue';
 import { SimpleFlowNode, NodeType }  from './consts';
 defineOptions({
   name: 'ProcessNodeTree'
@@ -32,10 +36,10 @@ watch(() => props.flowNode, (newValue) => {
   currentNode.value = newValue;  
 });
 
-// const handleModelValueUpdate = (updateValue) => {
-//   console.log('handleModelValueUpdate', updateValue)
-//   emits('update:flowNode', updateValue);
-// } 
+const handleModelValueUpdate = (updateValue) => {
+  console.log('Process Node Tree handleModelValueUpdate', updateValue)
+  emits('update:flowNode', updateValue);
+} 
 </script>
 <style lang='scss' scoped>
 

+ 28 - 19
src/components/SimpleProcessDesignerV2/src/SimpleProcessDesigner.vue

@@ -19,8 +19,9 @@
       </div>
     </div>
     <Dialog v-model="errorDialogVisible" title="保存失败" width="400" :fullscreen="false">
-      <div v-for="(item, index) in errorNodes" :key="index">
-          {{ item.name }}
+      <div class="mb-2">以下节点内容不完善,请修改后保存</div>
+      <div class="mb-3 b-rounded-1 bg-gray-100 p-2 line-height-normal" v-for="(item, index) in errorNodes" :key="index">
+          {{ item.name }} : {{ NODE_DEFAULT_TEXT.get(item.type) }}
       </div>
       <template #footer>
         <el-button type="primary" @click="errorDialogVisible = false" >知道了</el-button>
@@ -32,7 +33,7 @@
 <script setup lang="ts">
 import ProcessNodeTree from './ProcessNodeTree.vue';
 import { saveBpmSimpleModel, getBpmSimpleModel } from '@/api/bpm/simple'
-import { SimpleFlowNode, NodeType } from './consts'
+import { SimpleFlowNode, NodeType,NODE_DEFAULT_TEXT } from './consts'
 
 defineOptions({
   name: 'SimpleProcessDesigner'
@@ -67,21 +68,24 @@ const saveSimpleFlowModel = async () => {
   console.log('errorNodes is ', errorNodes)
   if (errorNodes.length > 0) {
     errorDialogVisible.value = true
+    return;
   }
-  // const data = {
-  //   modelId: props.modelId,
-  //   simpleModelBody: simpleWorkFlowNodes.value
-  // }
-  // console.log('request json data1 is ', data)
-  // const result = await saveBpmSimpleModel(data)
-  // console.log('save the result is ', result)
-  // if (result) {
-  //   message.success('修改成功')
-  //   close()
-  // } else {
-  //   message.alert('修改失败')
-  // }
+  const data = {
+    modelId: props.modelId,
+    simpleModelBody: processNodeTree.value
+  }
+
+  const result = await saveBpmSimpleModel(data)
+  console.log('save the result is ', result)
+  if (result) {
+    message.success('修改成功')
+    close()
+  } else {
+    message.alert('修改失败')
+  }
+  
 }
+// 校验节点设置。 暂时以 showText 为空 未节点错误配置
 const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNode[]) => {
   if (node) {
     const { type, showText, conditionNodes } = node
@@ -113,23 +117,28 @@ const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNo
     }
   }
 }
+
 const close = () => {
   router.push({ path: '/bpm/manager/model' })
 }
 let scaleValue = ref(100)
-
+const MAX_SCALE_VALUE = 200
+const MIN_SCALE_VALUE = 50
+// 放大
 const zoomOut = () => {
-  if (scaleValue.value == 300) {
+  if (scaleValue.value == MAX_SCALE_VALUE) {
     return
   }
   scaleValue.value += 10
 }
+// 缩小
 const zoomIn = () => {
-  if (scaleValue.value == 50) {
+  if (scaleValue.value == MIN_SCALE_VALUE) {
     return
   }
   scaleValue.value -= 10
 }
+
 onMounted(async () => {
   const result = await getBpmSimpleModel(props.modelId)
   if (result) {

+ 37 - 0
src/components/SimpleProcessDesignerV2/src/consts.ts

@@ -42,6 +42,43 @@ export enum NodeType {
   INCLUSIVE_NODE_JOIN = 8
 }
 
+// 候选人策略 (用于审批节点。抄送节点
+export enum CandidateStrategy {
+  /**
+   * 指定角色
+   */
+  ROLE = 10,
+  /**
+   * 指定部门成员
+   */
+  DEPT_MEMBER = 20,
+  /**
+   * 部门的负责人
+   */
+  DEPT_LEADER = 21,
+  /**
+   * 指定岗位
+   */
+  POST = 22,
+  /**
+   * 指定用户
+   */
+  USER = 30,
+  /**
+   * 发起人自选
+   */
+  START_USER_SELECT = 35,
+   /**
+   * 指定用户组
+   */
+   USER_GROUP = 40,
+   /**
+   * 流程表达式
+   */
+   EXPRESSION = 60
+}
+
+
 export type SimpleFlowNode = {
   id: string,
   type: NodeType,

+ 341 - 0
src/components/SimpleProcessDesignerV2/src/nodes-config/UserTaskNodeConfig.vue

@@ -0,0 +1,341 @@
+<template>
+  <el-drawer
+    :append-to-body="true"
+    v-model="settingVisible"
+    :show-close="false"
+    :size="550"
+    :before-close="saveConfig"
+    class="justify-start"
+  >
+    <template #header>
+      <div class="w-full flex flex-col">
+        <div class="mb-2 text-size-2xl">{{ currentNode.name }}</div>
+        <el-divider />
+      </div>
+    </template>
+    <el-tabs type="border-card">
+      <el-tab-pane label="审批人">
+        <div>
+          <el-form label-position="top">
+            <el-form-item label="审批人设置" prop="candidateStrategy">
+              <el-radio-group
+                v-model="currentNode.attributes.candidateStrategy"
+                @change="changeCandidateStrategy"
+              >
+                <el-radio
+                  v-for="(dict, index) in getIntDictOptions(DICT_TYPE.BPM_TASK_CANDIDATE_STRATEGY)"
+                  :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.candidateStrategy == CandidateStrategy.ROLE"
+              label="指定角色"
+              prop="candidateParam"
+            >
+              <el-select v-model="candidateParamArray" clearable multiple style="width: 100%">
+                <el-option
+                  v-for="item in roleOptions"
+                  :key="item.id"
+                  :label="item.name"
+                  :value="item.id"
+                />
+              </el-select>
+            </el-form-item>
+            <el-form-item
+              v-if="
+                currentNode.attributes.candidateStrategy == CandidateStrategy.DEPT_MEMBER ||
+                currentNode.attributes.candidateStrategy == CandidateStrategy.DEPT_LEADER
+              "
+              label="指定部门"
+              prop="candidateParam"
+              span="24"
+            >
+              <el-tree-select
+                ref="treeRef"
+                v-model="candidateParamArray"
+                :data="deptTreeOptions"
+                :props="defaultProps"
+                empty-text="加载中,请稍后"
+                multiple
+                node-key="id"
+                style="width: 100%"
+                show-checkbox
+              />
+            </el-form-item>
+            <el-form-item
+              v-if="currentNode.attributes.candidateStrategy == CandidateStrategy.POST"
+              label="指定岗位"
+              prop="candidateParam"
+              span="24"
+            >
+              <el-select v-model="candidateParamArray" clearable multiple style="width: 100%">
+                <el-option
+                  v-for="item in postOptions"
+                  :key="item.id"
+                  :label="item.name"
+                  :value="item.id"
+                />
+              </el-select>
+            </el-form-item>
+            <el-form-item
+              v-if="currentNode.attributes.candidateStrategy == CandidateStrategy.USER"
+              label="指定用户"
+              prop="candidateParam"
+              span="24"
+            >
+              <el-select
+                v-model="candidateParamArray"
+                clearable
+                multiple
+                style="width: 100%"
+                @change="changedCandidateUsers"
+              >
+                <el-option
+                  v-for="item in userOptions"
+                  :key="item.id"
+                  :label="item.nickname"
+                  :value="item.id"
+                />
+              </el-select>
+            </el-form-item>
+            <el-form-item
+              v-if="currentNode.attributes.candidateStrategy === CandidateStrategy.USER_GROUP"
+              label="指定用户组"
+              prop="candidateParam"
+            >
+              <el-select v-model="candidateParamArray" clearable multiple style="width: 100%">
+                <el-option
+                  v-for="item in userGroupOptions"
+                  :key="item.id"
+                  :label="item.name"
+                  :value="item.id"
+                />
+              </el-select>
+            </el-form-item>
+
+            <el-form-item
+              v-if="currentNode.attributes.candidateStrategy === CandidateStrategy.EXPRESSION"
+              label="流程表达式"
+              prop="candidateParam"
+            >
+              <el-input
+                type="textarea"
+                v-model="candidateParamArray[0]"
+                clearable
+                style="width: 100%"
+              />
+            </el-form-item>
+
+            <el-form-item label="审批方式" prop="approveMethod">
+              <el-radio-group v-model="currentNode.attributes.approveMethod">
+                <div class="flex-col">
+                  <div v-for="(item, index) in APPROVE_METHODS" :key="index">
+                    <el-radio
+                      :value="item.value"
+                      :label="item.value"
+                      :disabled="item.value !== 1 && notAllowedMultiApprovers"
+                    >
+                      {{ item.label }}
+                    </el-radio>
+                  </div>
+                </div>
+              </el-radio-group>
+            </el-form-item>
+          </el-form>
+        </div>
+      </el-tab-pane>
+    </el-tabs>
+    <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, APPROVE_METHODS,CandidateStrategy } from '../consts'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { defaultProps, handleTree } from '@/utils/tree'
+import * as RoleApi from '@/api/system/role'
+import * as DeptApi from '@/api/system/dept'
+import * as PostApi from '@/api/system/post'
+import * as UserApi from '@/api/system/user'
+import * as UserGroupApi from '@/api/bpm/userGroup'
+
+defineOptions({
+  name: 'UserTaskNodeConfig'
+})
+const props = defineProps({
+  flowNode: {
+    type: Object as () => SimpleFlowNode,
+    required: true
+  }
+})
+const emits = defineEmits<{
+  'update:modelValue': [node: SimpleFlowNode]
+}>()
+const notAllowedMultiApprovers = ref(false)
+const currentNode = ref<SimpleFlowNode>(props.flowNode)
+const settingVisible = ref(false)
+const roleOptions = ref<RoleApi.RoleVO[]>([]) // 角色列表
+const postOptions = ref<PostApi.PostVO[]>([]) // 岗位列表
+const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
+let   deptOptions: DeptApi.DeptVO[] = []// 部门列表
+const deptTreeOptions = ref() // 部门树
+const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]) // 用户组列表
+const candidateParamArray = ref<any[]>([])
+
+const closeDrawer = () => {
+  settingVisible.value = false
+}
+const saveConfig = () => {
+  currentNode.value.attributes.candidateParam = candidateParamArray.value?.join(',')
+  currentNode.value.showText = getShowText()
+  console.log('currentNode value is ', currentNode.value)
+  emits('update:modelValue', currentNode.value)
+  settingVisible.value = false
+}
+const getShowText = (): string => {
+  let showText = ''
+  // 指定成员
+  if (currentNode.value.attributes.candidateStrategy === CandidateStrategy.USER) {
+    if (candidateParamArray.value?.length > 0) {
+      const candidateNames: string[] = []
+      userOptions.value.forEach((item) => {
+        if (candidateParamArray.value.includes(item.id)) {
+          candidateNames.push(item.nickname)
+        }
+      })
+      console.log("candidateNames is ", candidateNames)
+      showText = `指定成员:${candidateNames.join(',')}`
+    }
+  }
+  // 指定角色
+  if (currentNode.value.attributes.candidateStrategy === 10) {
+    if (candidateParamArray.value?.length > 0) {
+      const candidateNames: string[] = []
+      roleOptions.value.forEach((item) => {
+        if (candidateParamArray.value.includes(item.id)) {
+          candidateNames.push(item.name)
+        }
+      })
+      showText = `指定角色:${candidateNames.join(',')}`
+    }
+  }
+   // 指定部门
+  if (currentNode.value.attributes.candidateStrategy === CandidateStrategy.DEPT_MEMBER 
+      || currentNode.value.attributes.candidateStrategy === CandidateStrategy.DEPT_LEADER ) {
+    if (candidateParamArray.value?.length > 0) {
+      const candidateNames: string[] = []
+      deptOptions.forEach((item) => {
+        if (candidateParamArray.value.includes(item.id)) {
+          candidateNames.push(item.name)
+        }
+      })
+      if(currentNode.value.attributes.candidateStrategy === CandidateStrategy.DEPT_MEMBER){
+        showText = `部门成员:${candidateNames.join(',')}`
+      } else {
+        showText = `部门的负责人:${candidateNames.join(',')}`
+      }
+    }
+  }
+
+   // 指定岗位
+   if (currentNode.value.attributes.candidateStrategy === CandidateStrategy.POST) {
+    if (candidateParamArray.value?.length > 0) {
+      const candidateNames: string[] = []
+      postOptions.value.forEach((item) => {
+        if (candidateParamArray.value.includes(item.id)) {
+          candidateNames.push(item.name)
+        }
+      })
+      showText = `指定岗位: ${candidateNames.join(',')}`
+    }
+  }
+   // 指定用户组
+  if (currentNode.value.attributes.candidateStrategy === CandidateStrategy.USER_GROUP) {
+    if (candidateParamArray.value?.length > 0) {
+      const candidateNames: string[] = []
+      userGroupOptions.value.forEach((item) => {
+        if (candidateParamArray.value.includes(item.id)) {
+          candidateNames.push(item.name)
+        }
+      })
+      showText = `指定用户组: ${candidateNames.join(',')}`
+    }
+  }
+
+  // 发起人自选
+  if (currentNode.value.attributes.candidateStrategy === CandidateStrategy.START_USER_SELECT ) {
+    showText = `发起人自选`
+  }
+
+   // 流程表达式
+  if (currentNode.value.attributes.candidateStrategy === CandidateStrategy.EXPRESSION) {
+    if (candidateParamArray.value?.length > 0) {
+      showText = `流程表达式:${candidateParamArray.value[0]}`
+    }
+  }
+   return showText
+}
+const open = () => {
+  settingVisible.value = true
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+const changeCandidateStrategy = () => {
+  candidateParamArray.value = []
+  currentNode.value.attributes.approveMethod = 1
+  if (currentNode.value.attributes.candidateStrategy === 30) {
+    notAllowedMultiApprovers.value = true
+  } else {
+    notAllowedMultiApprovers.value = false
+  }
+}
+
+const changedCandidateUsers = () => {
+  if (candidateParamArray.value?.length <= 1 && currentNode.value.attributes?.candidateStrategy === 30) {
+    currentNode.value.attributes.approveMethod = 1;
+    notAllowedMultiApprovers.value = true
+  } else {
+    notAllowedMultiApprovers.value = false
+  }
+}
+onMounted(async () => {
+  // 获得角色列表
+  roleOptions.value = await RoleApi.getSimpleRoleList()
+  postOptions.value = await PostApi.getSimplePostList()
+  // 获得用户列表
+  userOptions.value = await UserApi.getSimpleUserList()
+  // 获得部门列表
+  deptOptions = await DeptApi.getSimpleDeptList()
+  deptTreeOptions.value = handleTree(deptOptions, 'id')
+  // 获得用户组列表
+  userGroupOptions.value = await UserGroupApi.getUserGroupSimpleList()
+  console.log('candidateParam', currentNode.value.attributes?.candidateParam)
+  candidateParamArray.value = currentNode.value.attributes?.candidateParam?.split(',').map(item=> +item)
+  console.log('candidateParamArray.value', candidateParamArray.value)
+  if (currentNode.value.attributes?.candidateStrategy === 30 && candidateParamArray.value?.length <= 1) {
+    notAllowedMultiApprovers.value = true
+  } else {
+    notAllowedMultiApprovers.value = false
+  }
+})
+</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>

+ 122 - 0
src/components/SimpleProcessDesignerV2/src/nodes/UserTaskNode.vue

@@ -0,0 +1,122 @@
+<template>
+  <div class="node-wrapper">
+    <div class="node-container">
+      <div class="node-box" :class="{'node-config-error': !flowNode.showText}">
+        <div class="node-title-container">
+          <div class="node-title-icon"><Icon icon="ep:avatar" /></div>
+          <input
+            v-if="showInput"
+            type="text"
+            class="editable-title-input"
+            @blur="blurEvent($event)"
+            @focus="focusEvent($event)"
+            v-mountedFocus
+            v-model="currentNode.name"
+            :placeholder="currentNode.name"
+          />
+          <div v-else class="node-title" @click="clickEvent">
+            {{ currentNode.name }}
+          </div>
+        </div>
+        <div class="node-content" @click="commonNodeConfig">
+          <div class="node-text" :title="flowNode.showText" v-if="flowNode.showText">
+            {{ flowNode.showText }}
+          </div>
+          <div class="node-text"  v-else>
+            {{ NODE_DEFAULT_TEXT.get(NodeType.USER_TASK_NODE) }}
+          </div>
+          <Icon icon="ep:arrow-right-bold" />
+        </div>
+        <div class="node-toolbar">
+          <div class="toolbar-icon"><Icon icon="ep:document-copy" @click="copyNode" /></div>
+          <div class="toolbar-icon"><Icon icon="ep:delete" @click="deleteNode" /></div>
+        </div>
+      </div>
+
+      <!-- title="朱文宣妈妈, 许颖嘉, 王禹昊妈妈, 王亦心爸爸, 朱彧兮妈妈, 陈语馨爸爸, 蔡知妤爸爸/蔡知妍爸爸, 蒋昂倬妈妈, 胡恒智爸爸, 杨晞宇妈妈, 赵嘉依妈妈, 罗嘉懿爸爸, 郑辰希妈妈, 周慕然爸爸, 方张霖妈妈, 张若轩妈妈, 王亦心妈妈, 董子夏妈妈, 刘昱君妈妈/刘畅妈妈, 李子琦妈妈, 解宛芙妈妈, 洪栎妈妈, 陈涂晟爸爸, 张泽锴妈妈, 杨晞宇爸爸, 高鸿轩妈妈, 张然妈妈, 景晗亿妈妈, 郑炜彤妈妈, 祝安屹妈妈, 王萧婉妈妈, 朱彧兮爸爸, 李沐辰妈妈, 王元昊爸爸, 罗嘉懿妈妈, 陈语馨妈妈" -->
+      <!-- 传递子节点给添加节点组件。会在子节点前面添加节点 -->
+      <NodeHandler v-if="currentNode" v-model:child-node="currentNode.childNode" />
+    </div>
+  </div>
+  <UserTaskNodeConfig
+    v-if="currentNode"
+    ref="nodeSetting"
+    :flow-node="currentNode"
+    @update:model-value="handleModelValueUpdate"
+  />
+</template>
+<script setup lang="ts">
+import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT, NODE_DEFAULT_NAME } from '../consts'
+import NodeHandler from '../NodeHandler.vue'
+import UserTaskNodeConfig from '../nodes-config/UserTaskNodeConfig.vue';
+import { generateUUID } from '@/utils'
+defineOptions({
+  name: 'CommonNode'
+})
+const props = defineProps({
+  flowNode: {
+    type: Object as () => SimpleFlowNode,
+    required: true
+  }
+})
+const emits = defineEmits<{
+  'update:modelValue': [node: SimpleFlowNode | undefined]
+}>()
+const nodeSetting = ref()
+const commonNodeConfig = () => {
+  console.log('nodeSetting', nodeSetting)
+  nodeSetting.value.open()
+}
+
+const currentNode = ref<SimpleFlowNode>(props.flowNode)
+console.log('current common node is ', currentNode)
+
+// 不加这个 COPY 会有问题
+watch(
+  () => props.flowNode,
+  (newValue) => {
+    console.log('new value is ', newValue)
+    currentNode.value = newValue
+  }
+)
+const showInput = ref(false)
+const blurEvent = (event) => {
+  console.log('blurEvent', event)
+  showInput.value = false
+  currentNode.value.name = currentNode.value.name || NODE_DEFAULT_NAME.get(NodeType.USER_TASK_NODE) as string
+}
+const focusEvent = (event: FocusEvent) => {
+  console.log('focusEvent', event)
+}
+const clickEvent = () => {
+  showInput.value = true
+}
+const handleModelValueUpdate = (updateValue) => {
+  emits('update:modelValue', updateValue)
+  console.log('user task node handleModelValueUpdate', updateValue)
+}
+const deleteNode = () => {
+  console.log('the child node is ', currentNode.value.childNode)
+  emits('update:modelValue', currentNode.value.childNode)
+}
+const copyNode = () => {
+  // const oldChildNode = currentNode.value.childNode
+  const newCopyNode: SimpleFlowNode = {
+    id: generateUUID(),
+    name: currentNode.value.name,
+    showText: currentNode.value.showText,
+    type: currentNode.value.type,
+    // 审批节点配置
+    attributes: {
+      approveMethod: currentNode.value.attributes?.approveMethod,
+      candidateStrategy: currentNode.value.attributes?.candidateStrategy,
+      candidateParam: currentNode.value.attributes?.candidateParam
+    },
+    childNode: currentNode.value
+  }
+  currentNode.value = newCopyNode
+  console.log('current node value', currentNode.value)
+  emits('update:modelValue', currentNode.value)
+}
+</script>
+<style lang="scss" scoped></style>