瀏覽代碼

!655 Simple设计器完善及优化
Merge pull request !655 from Lesan/feature/bpm-n

芋道源码 7 月之前
父節點
當前提交
753e44ccd0

+ 1 - 0
src/api/bpm/processInstance/index.ts

@@ -36,6 +36,7 @@ export type ApprovalTaskInfo = {
   assigneeUser: User
   status: number
   reason: string
+  sign: string
 }
 
 // 审批节点信息

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

@@ -46,7 +46,7 @@
             </div>
             <div class="handler-item-text">延迟器</div>
           </div>
-          <div class="handler-item" @click="addNode(NodeType.ROUTE_BRANCH_NODE)">
+          <div class="handler-item" @click="addNode(NodeType.ROUTER_BRANCH_NODE)">
             <!-- TODO @芋艿 需要更换一下iconfont的图标 -->
             <div class="handler-item-icon copy">
               <span class="iconfont icon-size icon-copy"></span>
@@ -67,12 +67,13 @@ import {
   ApproveMethodType,
   AssignEmptyHandlerType,
   AssignStartUserHandlerType,
+  ConditionType,
   NODE_DEFAULT_NAME,
   NodeType,
   RejectHandlerType,
   SimpleFlowNode
 } from './consts'
-import { generateUUID } from '@/utils'
+import {generateUUID} from '@/utils'
 
 defineOptions({
   name: 'NodeHandler'
@@ -163,7 +164,7 @@ const addNode = (type: number) => {
           showText: '',
           type: NodeType.CONDITION_NODE,
           childNode: undefined,
-          conditionType: 1,
+          conditionType: ConditionType.RULE,
           defaultFlow: false
         },
         {
@@ -241,14 +242,13 @@ const addNode = (type: number) => {
     }
     emits('update:childNode', data)
   }
-  if (type === NodeType.ROUTE_BRANCH_NODE) {
+  if (type === NodeType.ROUTER_BRANCH_NODE) {
     const data: SimpleFlowNode = {
       id: 'GateWay_' + generateUUID(),
-      name: NODE_DEFAULT_NAME.get(NodeType.ROUTE_BRANCH_NODE) as string,
+      name: NODE_DEFAULT_NAME.get(NodeType.ROUTER_BRANCH_NODE) as string,
       showText: '',
-      type: NodeType.ROUTE_BRANCH_NODE,
-      childNode: props.childNode,
-      defaultFlowId: 'Flow_' + generateUUID()
+      type: NodeType.ROUTER_BRANCH_NODE,
+      childNode: props.childNode
     }
     emits('update:childNode', data)
   }

+ 3 - 3
src/components/SimpleProcessDesignerV2/src/ProcessNodeTree.vue

@@ -45,8 +45,8 @@
     @update:flow-node="handleModelValueUpdate"
   />
   <!-- 路由分支节点 -->
-  <RouteNode
-    v-if="currentNode && currentNode.type === NodeType.ROUTE_BRANCH_NODE"
+  <RouterNode
+    v-if="currentNode && currentNode.type === NodeType.ROUTER_BRANCH_NODE"
     :flow-node="currentNode"
     @update:flow-node="handleModelValueUpdate"
   />
@@ -73,7 +73,7 @@ import ExclusiveNode from './nodes/ExclusiveNode.vue'
 import ParallelNode from './nodes/ParallelNode.vue'
 import InclusiveNode from './nodes/InclusiveNode.vue'
 import DelayTimerNode from './nodes/DelayTimerNode.vue'
-import RouteNode from './nodes/RouteNode.vue'
+import RouterNode from './nodes/RouterNode.vue'
 import { SimpleFlowNode, NodeType } from './consts'
 import { useWatchNode } from './node'
 defineOptions({

+ 5 - 7
src/components/SimpleProcessDesignerV2/src/consts.ts

@@ -48,7 +48,7 @@ export enum NodeType {
   /**
    * 路由分支节点
    */
-  ROUTE_BRANCH_NODE = 54
+  ROUTER_BRANCH_NODE = 54
 }
 
 export enum NodeId {
@@ -116,7 +116,7 @@ export interface SimpleFlowNode {
   // 延迟设置
   delaySetting?: DelaySetting
   // 路由分支
-  routerGroups?: RouteCondition[]
+  routerGroups?: RouterCondition[]
   defaultFlowId?: string
   // 签名
   signEnable?: boolean
@@ -439,8 +439,6 @@ export enum OperationButtonType {
  * 条件规则结构定义
  */
 export type ConditionRule = {
-  type: number
-  opName: string
   opCode: string
   leftSide: string
   rightSide: string
@@ -471,7 +469,7 @@ NODE_DEFAULT_TEXT.set(NodeType.COPY_TASK_NODE, '请配置抄送人')
 NODE_DEFAULT_TEXT.set(NodeType.CONDITION_NODE, '请设置条件')
 NODE_DEFAULT_TEXT.set(NodeType.START_USER_NODE, '请设置发起人')
 NODE_DEFAULT_TEXT.set(NodeType.DELAY_TIMER_NODE, '请设置延迟器')
-NODE_DEFAULT_TEXT.set(NodeType.ROUTE_BRANCH_NODE, '请设置路由节点')
+NODE_DEFAULT_TEXT.set(NodeType.ROUTER_BRANCH_NODE, '请设置路由节点')
 
 export const NODE_DEFAULT_NAME = new Map<number, string>()
 NODE_DEFAULT_NAME.set(NodeType.USER_TASK_NODE, '审批人')
@@ -479,7 +477,7 @@ NODE_DEFAULT_NAME.set(NodeType.COPY_TASK_NODE, '抄送人')
 NODE_DEFAULT_NAME.set(NodeType.CONDITION_NODE, '条件')
 NODE_DEFAULT_NAME.set(NodeType.START_USER_NODE, '发起人')
 NODE_DEFAULT_NAME.set(NodeType.DELAY_TIMER_NODE, '延迟器')
-NODE_DEFAULT_NAME.set(NodeType.ROUTE_BRANCH_NODE, '路由分支')
+NODE_DEFAULT_NAME.set(NodeType.ROUTER_BRANCH_NODE, '路由分支')
 
 // 候选人策略。暂时不从字典中取。 后续可能调整。控制显示顺序
 export const CANDIDATE_STRATEGY: DictDataVO[] = [
@@ -660,7 +658,7 @@ export const DELAY_TYPE = [
 /**
  * 路由分支结构定义
  */
-export type RouteCondition = {
+export type RouterCondition = {
   nodeId: string
   conditionType: ConditionType
   conditionExpression: string

+ 32 - 204
src/components/SimpleProcessDesignerV2/src/nodes-config/ConditionNodeConfig.vue

@@ -30,117 +30,7 @@
         >未满足其它条件时,将进入此分支(该分支不可编辑和删除)</div
       >
       <div v-else>
-        <el-form ref="formRef" :model="currentNode" :rules="formRules" label-position="top">
-          <el-form-item label="配置方式" prop="conditionType">
-            <el-radio-group v-model="currentNode.conditionType" @change="changeConditionType">
-              <el-radio
-                v-for="(dict, index) in conditionConfigTypes"
-                :key="index"
-                :value="dict.value"
-                :label="dict.value"
-              >
-                {{ dict.label }}
-              </el-radio>
-            </el-radio-group>
-          </el-form-item>
-
-          <el-form-item
-            v-if="currentNode.conditionType === 1"
-            label="条件表达式"
-            prop="conditionExpression"
-          >
-            <el-input
-              type="textarea"
-              v-model="currentNode.conditionExpression"
-              clearable
-              style="width: 100%"
-            />
-          </el-form-item>
-          <el-form-item v-if="currentNode.conditionType === 2" label="条件规则">
-            <div class="condition-group-tool">
-              <div class="flex items-center">
-                <div class="mr-4">条件组关系</div>
-                <el-switch
-                  v-model="conditionGroups.and"
-                  inline-prompt
-                  active-text="且"
-                  inactive-text="或"
-                />
-              </div>
-            </div>
-            <el-space direction="vertical" :spacer="conditionGroups.and ? '且' : '或'">
-              <el-card
-                class="condition-group"
-                style="width: 530px"
-                v-for="(condition, cIdx) in conditionGroups.conditions"
-                :key="cIdx"
-              >
-                <div class="condition-group-delete" v-if="conditionGroups.conditions.length > 1">
-                  <Icon
-                    color="#0089ff"
-                    icon="ep:circle-close-filled"
-                    :size="18"
-                    @click="deleteConditionGroup(cIdx)"
-                  />
-                </div>
-                <template #header>
-                  <div class="flex items-center justify-between">
-                    <div>条件组</div>
-                    <div class="flex">
-                      <div class="mr-4">规则关系</div>
-                      <el-switch
-                        v-model="condition.and"
-                        inline-prompt
-                        active-text="且"
-                        inactive-text="或"
-                      />
-                    </div>
-                  </div>
-                </template>
-
-                <div class="flex pt-2" v-for="(rule, rIdx) in condition.rules" :key="rIdx">
-                  <div class="mr-2">
-                    <el-select style="width: 160px" v-model="rule.leftSide">
-                      <el-option
-                        v-for="(item, index) in fieldOptions"
-                        :key="index"
-                        :label="item.title"
-                        :value="item.field"
-                        :disabled="!item.required"
-                      />
-                    </el-select>
-                  </div>
-                  <div class="mr-2">
-                    <el-select v-model="rule.opCode" style="width: 100px">
-                      <el-option
-                        v-for="item in COMPARISON_OPERATORS"
-                        :key="item.value"
-                        :label="item.label"
-                        :value="item.value"
-                      />
-                    </el-select>
-                  </div>
-                  <div class="mr-2">
-                    <el-input v-model="rule.rightSide" style="width: 160px" />
-                  </div>
-                  <div class="mr-1 flex items-center" v-if="condition.rules.length > 1">
-                    <Icon
-                      icon="ep:delete"
-                      :size="18"
-                      @click="deleteConditionRule(condition, rIdx)"
-                    />
-                  </div>
-                  <div class="flex items-center">
-                    <Icon icon="ep:plus" :size="18" @click="addConditionRule(condition, rIdx)" />
-                  </div>
-                </div>
-              </el-card>
-            </el-space>
-            <div title="添加条件组" class="mt-4 cursor-pointer">
-              <Icon color="#0089ff" icon="ep:plus" :size="24" @click="addConditionGroup" />
-            </div>
-          </el-form-item>
-        </el-form>
+        <Condition ref="conditionRef" v-model="condition" />
       </div>
     </div>
     <template #footer>
@@ -155,33 +45,17 @@
 <script setup lang="ts">
 import {
   SimpleFlowNode,
-  CONDITION_CONFIG_TYPES,
   ConditionType,
   COMPARISON_OPERATORS,
-  ConditionGroup,
-  Condition,
-  ConditionRule,
   ProcessVariableEnum
 } from '../consts'
 import { getDefaultConditionNodeName } from '../utils'
 import { useFormFields } from '../node'
-import { BpmModelFormType } from '@/utils/constants'
+import Condition from './components/Condition.vue'
 const message = useMessage() // 消息弹窗
 defineOptions({
   name: 'ConditionNodeConfig'
 })
-const formType = inject<Ref<number>>('formType') // 表单类型
-const conditionConfigTypes = computed(() => {
-  return CONDITION_CONFIG_TYPES.filter((item) => {
-    // 业务表单暂时去掉条件规则选项
-    if (formType?.value === BpmModelFormType.CUSTOM && item.value === ConditionType.RULE) {
-      return false
-    } else {
-      return true
-    }
-  })
-})
-
 const props = defineProps({
   conditionNode: {
     type: Object as () => SimpleFlowNode,
@@ -193,11 +67,26 @@ const props = defineProps({
   }
 })
 const settingVisible = ref(false)
+const condition = ref<any>()
 const open = () => {
-  if (currentNode.value.conditionType === ConditionType.RULE) {
-    if (currentNode.value.conditionGroups) {
-      conditionGroups.value = currentNode.value.conditionGroups
-    }
+  condition.value = {
+    conditionType: currentNode.value.conditionType,
+    conditionExpression: currentNode.value.conditionExpression ?? '',
+    conditionGroups: currentNode.value.conditionGroups ?? {
+      and: true,
+      conditions: [
+        {
+          and: true,
+          rules: [
+            {
+              opCode: '==',
+              leftSide: '',
+              rightSide: ''
+            }
+          ]
+        }
+      ]
+    },
   }
   settingVisible.value = true
 }
@@ -239,31 +128,27 @@ const handleClose = async (done: (cancel?: boolean) => void) => {
     done()
   }
 }
-// 表单校验规则
-const formRules = reactive({
-  conditionType: [{ required: true, message: '配置方式不能为空', trigger: 'blur' }],
-  conditionExpression: [{ required: true, message: '条件表达式不能为空', trigger: 'blur' }]
-})
-const formRef = ref() // 表单 Ref
 
+const conditionRef = ref()
 // 保存配置
 const saveConfig = async () => {
   if (!currentNode.value.defaultFlow) {
     // 校验表单
-    if (!formRef) return false
-    const valid = await formRef.value.validate()
+    const valid = await conditionRef.value.validate()
     if (!valid) return false
     const showText = getShowText()
     if (!showText) {
       return false
     }
     currentNode.value.showText = showText
+    currentNode.value.conditionType = condition.value.conditionType
     if (currentNode.value.conditionType === ConditionType.EXPRESSION) {
       currentNode.value.conditionGroups = undefined
+      currentNode.value.conditionExpression = condition.value.conditionExpression
     }
     if (currentNode.value.conditionType === ConditionType.RULE) {
       currentNode.value.conditionExpression = undefined
-      currentNode.value.conditionGroups = conditionGroups.value
+      currentNode.value.conditionGroups = condition.value.conditionGroups
     }
   }
   settingVisible.value = false
@@ -271,16 +156,16 @@ const saveConfig = async () => {
 }
 const getShowText = (): string => {
   let showText = ''
-  if (currentNode.value.conditionType === ConditionType.EXPRESSION) {
-    if (currentNode.value.conditionExpression) {
-      showText = `表达式:${currentNode.value.conditionExpression}`
+  if (condition.value.conditionType === ConditionType.EXPRESSION) {
+    if (condition.value.conditionExpression) {
+      showText = `表达式:${condition.value.conditionExpression}`
     }
   }
-  if (currentNode.value.conditionType === ConditionType.RULE) {
+  if (condition.value.conditionType === ConditionType.RULE) {
     // 条件组是否为与关系
-    const groupAnd = conditionGroups.value.and
+    const groupAnd = condition.value.conditionGroups.and
     let warningMesg: undefined | string = undefined
-    const conditionGroup = conditionGroups.value.conditions.map((item) => {
+    const conditionGroup = condition.value.conditionGroups.conditions.map((item) => {
       return (
         '(' +
         item.rules
@@ -309,64 +194,7 @@ const getShowText = (): string => {
   return showText
 }
 
-// 改变条件配置方式
-const changeConditionType = () => {}
-
-const conditionGroups = ref<ConditionGroup>({
-  and: true,
-  conditions: [
-    {
-      and: true,
-      rules: [
-        {
-          type: 1,
-          opName: '等于',
-          opCode: '==',
-          leftSide: '',
-          rightSide: ''
-        }
-      ]
-    }
-  ]
-})
-// 添加条件组
-const addConditionGroup = () => {
-  const condition = {
-    and: true,
-    rules: [
-      {
-        type: 1,
-        opName: '等于',
-        opCode: '==',
-        leftSide: '',
-        rightSide: ''
-      }
-    ]
-  }
-  conditionGroups.value.conditions.push(condition)
-}
-// 删除条件组
-const deleteConditionGroup = (idx: number) => {
-  conditionGroups.value.conditions.splice(idx, 1)
-}
-
-// 添加条件规则
-const addConditionRule = (condition: Condition, idx: number) => {
-  const rule: ConditionRule = {
-    type: 1,
-    opName: '等于',
-    opCode: '==',
-    leftSide: '',
-    rightSide: ''
-  }
-  condition.rules.splice(idx + 1, 0, rule)
-}
-
-const deleteConditionRule = (condition: Condition, idx: number) => {
-  condition.rules.splice(idx, 1)
-}
 const fieldsInfo = useFormFields()
-
 /** 条件规则可选择的表单字段 */
 const fieldOptions = computed(() => {
   const fieldsCopy = fieldsInfo.slice()

+ 24 - 15
src/components/SimpleProcessDesignerV2/src/nodes-config/RouteNodeConfig.vue → src/components/SimpleProcessDesignerV2/src/nodes-config/RouterNodeConfig.vue

@@ -37,16 +37,19 @@
                   :value="node.value"
                 />
               </el-select>
-              <el-button class="mla" type="danger" link @click="deleteRouteGroup(index)"
+              <el-button class="mla" type="danger" link @click="deleteRouterGroup(index)"
                 >删除</el-button
               >
             </div>
           </template>
-          <Condition v-model="routerGroups[index]" />
+          <Condition
+            :ref="($event) => (conditionRef[index] = $event)"
+            v-model="routerGroups[index]"
+          />
         </el-card>
       </el-form>
 
-      <el-button class="w-1/1" type="primary" :icon="Plus" @click="addRouteGroup">
+      <el-button class="w-1/1" type="primary" :icon="Plus" @click="addRouterGroup">
         新增路由分支
       </el-button>
     </div>
@@ -61,11 +64,11 @@
 </template>
 <script setup lang="ts">
 import { Plus } from '@element-plus/icons-vue'
-import { SimpleFlowNode, NodeType, ConditionType, RouteCondition } from '../consts'
+import { SimpleFlowNode, NodeType, ConditionType, RouterCondition } from '../consts'
 import { useWatchNode, useDrawer, useNodeName } from '../node'
 import Condition from './components/Condition.vue'
 defineOptions({
-  name: 'RouteNodeConfig'
+  name: 'RouterNodeConfig'
 })
 const message = useMessage() // 消息弹窗
 const props = defineProps({
@@ -80,12 +83,21 @@ const { settingVisible, closeDrawer, openDrawer } = useDrawer()
 // 当前节点
 const currentNode = useWatchNode(props)
 // 节点名称
-const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.ROUTE_BRANCH_NODE)
-const routerGroups = ref<RouteCondition[]>([])
+const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.ROUTER_BRANCH_NODE)
+const routerGroups = ref<RouterCondition[]>([])
 const nodeOptions = ref()
 
+const conditionRef = ref([])
 // 保存配置
 const saveConfig = async () => {
+  // 校验表单
+  let valid = true
+  for (const item of conditionRef.value) {
+    if (!(await item.validate())) {
+      valid = false
+    }
+  }
+  if (!valid) return false
   const showText = getShowText()
   if (!showText) return false
   currentNode.value.name = nodeName.value!
@@ -96,7 +108,7 @@ const saveConfig = async () => {
 }
 // 显示路由分支节点配置, 由父组件传过来
 const showRouteNodeConfig = (node: SimpleFlowNode) => {
-  getRoutableNode()
+  getRouterNode()
   routerGroups.value = []
   nodeName.value = node.name
   if (node.routerGroups) {
@@ -132,7 +144,7 @@ const getShowText = () => {
   return `${routerGroups.value.length}条路由分支`
 }
 
-const addRouteGroup = () => {
+const addRouterGroup = () => {
   routerGroups.value.push({
     nodeId: '',
     conditionType: ConditionType.RULE,
@@ -144,8 +156,6 @@ const addRouteGroup = () => {
           and: true,
           rules: [
             {
-              type: 1,
-              opName: '等于',
               opCode: '==',
               leftSide: '',
               rightSide: ''
@@ -157,12 +167,11 @@ const addRouteGroup = () => {
   })
 }
 
-const deleteRouteGroup = (index: number) => {
+const deleteRouterGroup = (index: number) => {
   routerGroups.value.splice(index, 1)
 }
 
-// TODO @lesan:还有一些 router 的命名,没改过来呢
-const getRoutableNode = () => {
+const getRouterNode = () => {
   // TODO @lesan 还需要满足以下要求
   // 并行分支、包容分支内部节点不能跳转到外部节点
   // 条件分支节点可以向上跳转到外部节点
@@ -170,7 +179,7 @@ const getRoutableNode = () => {
   nodeOptions.value = []
   while (true) {
     if (!node) break
-    if (node.type !== NodeType.ROUTE_BRANCH_NODE) {
+    if (node.type !== NodeType.ROUTER_BRANCH_NODE) {
       nodeOptions.value.push({
         label: node.name,
         value: node.id

+ 95 - 42
src/components/SimpleProcessDesignerV2/src/nodes-config/UserTaskNodeConfig.vue

@@ -359,11 +359,7 @@
 
             <el-divider content-position="left">是否需要签名</el-divider>
             <el-form-item prop="signEnable">
-              <el-switch
-                v-model="configForm.signEnable"
-                active-text="是"
-                inactive-text="否"
-              />
+              <el-switch v-model="configForm.signEnable" active-text="是" inactive-text="否" />
             </el-form-item>
           </el-form>
         </div>
@@ -445,7 +441,7 @@
         </div>
       </el-tab-pane>
       <el-tab-pane label="监听器" name="listener">
-        <el-form :model="configForm" label-position="top">
+        <el-form ref="listenerFormRef" :model="configForm" label-position="top">
           <div v-for="(listener, listenerIdx) in taskListener" :key="listenerIdx">
             <el-divider content-position="left">
               <el-text tag="b" size="large">{{ listener.name }}</el-text>
@@ -484,7 +480,16 @@
                   :key="index"
                 >
                   <div class="mr-2">
-                    <el-input class="w-160px" v-model="item.key" />
+                    <el-form-item
+                      :prop="`task${listener.type}ListenerHeader.${index}.key`"
+                      :rules="{
+                        required: true,
+                        message: '参数名不能为空',
+                        trigger: 'blur'
+                      }"
+                    >
+                      <el-input class="w-160px" v-model="item.key" />
+                    </el-form-item>
                   </div>
                   <div class="mr-2">
                     <el-select class="w-100px!" v-model="item.type">
@@ -497,24 +502,42 @@
                     </el-select>
                   </div>
                   <div class="mr-2">
-                    <el-input
-                      v-if="item.type === ListenerParamTypeEnum.FIXED_VALUE"
-                      class="w-160px"
-                      v-model="item.value"
-                    />
-                    <el-select
-                      v-if="item.type === ListenerParamTypeEnum.FROM_FORM"
-                      class="w-160px!"
-                      v-model="item.value"
+                    <el-form-item
+                      :prop="`task${listener.type}ListenerHeader.${index}.value`"
+                      :rules="{
+                        required: true,
+                        message: '参数值不能为空',
+                        trigger: 'blur'
+                      }"
                     >
-                      <el-option
-                        v-for="(field, fIdx) in formFieldOptions"
-                        :key="fIdx"
-                        :label="field.title"
-                        :value="field.field"
-                        :disabled="!field.required"
+                      <el-input
+                        v-if="item.type === ListenerParamTypeEnum.FIXED_VALUE"
+                        class="w-160px"
+                        v-model="item.value"
                       />
-                    </el-select>
+                    </el-form-item>
+                    <el-form-item
+                      :prop="`task${listener.type}ListenerHeader.${index}.value`"
+                      :rules="{
+                        required: true,
+                        message: '参数值不能为空',
+                        trigger: 'change'
+                      }"
+                    >
+                      <el-select
+                        v-if="item.type === ListenerParamTypeEnum.FROM_FORM"
+                        class="w-160px!"
+                        v-model="item.value"
+                      >
+                        <el-option
+                          v-for="(field, fIdx) in formFieldOptions"
+                          :key="fIdx"
+                          :label="field.title"
+                          :value="field.field"
+                          :disabled="!field.required"
+                        />
+                      </el-select>
+                    </el-form-item>
                   </div>
                   <div class="mr-1 flex items-center">
                     <Icon
@@ -544,7 +567,16 @@
                   :key="index"
                 >
                   <div class="mr-2">
-                    <el-input class="w-160px" v-model="item.key" />
+                    <el-form-item
+                      :prop="`task${listener.type}ListenerBody.${index}.key`"
+                      :rules="{
+                        required: true,
+                        message: '参数名不能为空',
+                        trigger: 'blur'
+                      }"
+                    >
+                      <el-input class="w-160px" v-model="item.key" />
+                    </el-form-item>
                   </div>
                   <div class="mr-2">
                     <el-select class="w-100px!" v-model="item.type">
@@ -557,24 +589,42 @@
                     </el-select>
                   </div>
                   <div class="mr-2">
-                    <el-input
-                      v-if="item.type === ListenerParamTypeEnum.FIXED_VALUE"
-                      class="w-160px"
-                      v-model="item.value"
-                    />
-                    <el-select
-                      v-if="item.type === ListenerParamTypeEnum.FROM_FORM"
-                      class="w-160px!"
-                      v-model="item.value"
+                    <el-form-item
+                      :prop="`task${listener.type}ListenerBody.${index}.value`"
+                      :rules="{
+                        required: true,
+                        message: '参数值不能为空',
+                        trigger: 'blur'
+                      }"
                     >
-                      <el-option
-                        v-for="(field, fIdx) in formFieldOptions"
-                        :key="fIdx"
-                        :label="field.title"
-                        :value="field.field"
-                        :disabled="!field.required"
+                      <el-input
+                        v-if="item.type === ListenerParamTypeEnum.FIXED_VALUE"
+                        class="w-160px"
+                        v-model="item.value"
                       />
-                    </el-select>
+                    </el-form-item>
+                    <el-form-item
+                      :prop="`task${listener.type}ListenerBody.${index}.value`"
+                      :rules="{
+                        required: true,
+                        message: '参数值不能为空',
+                        trigger: 'change'
+                      }"
+                    >
+                      <el-select
+                        v-if="item.type === ListenerParamTypeEnum.FROM_FORM"
+                        class="w-160px!"
+                        v-model="item.value"
+                      >
+                        <el-option
+                          v-for="(field, fIdx) in formFieldOptions"
+                          :key="fIdx"
+                          :label="field.title"
+                          :value="field.field"
+                          :disabled="!field.required"
+                        />
+                      </el-select>
+                    </el-form-item>
                   </div>
                   <div class="mr-1 flex items-center">
                     <Icon
@@ -792,6 +842,8 @@ const {
   cTimeoutMaxRemindCount
 } = useTimeoutHandler()
 
+const listenerFormRef = ref()
+
 // 保存配置
 const saveConfig = async () => {
   activeTabName.value = 'user'
@@ -807,7 +859,8 @@ const saveConfig = async () => {
   }
 
   if (!formRef) return false
-  const valid = await formRef.value.validate()
+  if (!listenerFormRef) return false
+  const valid = (await formRef.value.validate()) && (await listenerFormRef.value.validate())
   if (!valid) return false
   const showText = getShowText()
   if (!showText) return false
@@ -937,7 +990,7 @@ const showUserTaskNodeConfig = (node: SimpleFlowNode) => {
   configForm.value.taskCompleteListenerHeader = node.taskCompleteListener?.header ?? []
   configForm.value.taskCompleteListenerBody = node.taskCompleteListener?.body ?? []
   // 6. 签名
-  configForm.value.signEnable = node.signEnable ?? false
+  configForm.value.signEnable = node?.signEnable ?? false
 }
 
 defineExpose({ openDrawer, showUserTaskNodeConfig }) // 暴露方法给父组件

+ 48 - 29
src/components/SimpleProcessDesignerV2/src/nodes-config/components/Condition.vue

@@ -1,7 +1,5 @@
-<!-- TODO @lesan:其它路由条件,可以使用这个哇? -->
 <template>
   <el-form ref="formRef" :model="condition" :rules="formRules" label-position="top">
-    <!-- TODO @lesan:1)默认选中 条件规则;2)条件规则放前面,因为更常用!-->
     <el-form-item label="配置方式" prop="conditionType">
       <el-radio-group v-model="condition.conditionType">
         <el-radio
@@ -14,18 +12,6 @@
         </el-radio>
       </el-radio-group>
     </el-form-item>
-    <el-form-item
-      v-if="condition.conditionType === ConditionType.EXPRESSION"
-      label="条件表达式"
-      prop="conditionExpression"
-    >
-      <el-input
-        type="textarea"
-        v-model="condition.conditionExpression"
-        clearable
-        style="width: 100%"
-      />
-    </el-form-item>
     <el-form-item v-if="condition.conditionType === ConditionType.RULE" label="条件规则">
       <div class="condition-group-tool">
         <div class="flex items-center">
@@ -73,15 +59,24 @@
 
           <div class="flex pt-2" v-for="(rule, rIdx) in equation.rules" :key="rIdx">
             <div class="mr-2">
-              <el-select style="width: 160px" v-model="rule.leftSide">
-                <el-option
-                  v-for="(field, fIdx) in fieldOptions"
-                  :key="fIdx"
-                  :label="field.title"
-                  :value="field.field"
-                  :disabled="!field.required"
-                />
-              </el-select>
+              <el-form-item
+                :prop="`conditionGroups.conditions.${cIdx}.rules.${rIdx}.leftSide`"
+                :rules="{
+                  required: true,
+                  message: '左值不能为空',
+                  trigger: 'change'
+                }"
+              >
+                <el-select style="width: 160px" v-model="rule.leftSide">
+                  <el-option
+                    v-for="(field, fIdx) in fieldOptions"
+                    :key="fIdx"
+                    :label="field.title"
+                    :value="field.field"
+                    :disabled="!field.required"
+                  />
+                </el-select>
+              </el-form-item>
             </div>
             <div class="mr-2">
               <el-select v-model="rule.opCode" style="width: 100px">
@@ -94,7 +89,16 @@
               </el-select>
             </div>
             <div class="mr-2">
-              <el-input v-model="rule.rightSide" style="width: 160px" />
+              <el-form-item
+                :prop="`conditionGroups.conditions.${cIdx}.rules.${rIdx}.rightSide`"
+                :rules="{
+                  required: true,
+                  message: '右值不能为空',
+                  trigger: 'blur'
+                }"
+              >
+                <el-input v-model="rule.rightSide" style="width: 160px" />
+              </el-form-item>
             </div>
             <div class="mr-1 flex items-center" v-if="equation.rules.length > 1">
               <Icon icon="ep:delete" :size="18" @click="deleteConditionRule(equation, rIdx)" />
@@ -114,13 +118,25 @@
         />
       </div>
     </el-form-item>
+    <el-form-item
+      v-if="condition.conditionType === ConditionType.EXPRESSION"
+      label="条件表达式"
+      prop="conditionExpression"
+    >
+      <el-input
+        type="textarea"
+        v-model="condition.conditionExpression"
+        clearable
+        style="width: 100%"
+      />
+    </el-form-item>
   </el-form>
 </template>
 
 <script setup lang="ts">
 import {
-  CONDITION_CONFIG_TYPES,
   COMPARISON_OPERATORS,
+  CONDITION_CONFIG_TYPES,
   ConditionType,
   ProcessVariableEnum
 } from '../../consts'
@@ -181,8 +197,6 @@ const deleteConditionRule = (condition, index) => {
 
 const addConditionRule = (condition, index) => {
   const rule = {
-    type: 1,
-    opName: '等于',
     opCode: '==',
     leftSide: '',
     rightSide: ''
@@ -195,8 +209,6 @@ const addConditionGroup = (conditions) => {
     and: true,
     rules: [
       {
-        type: 1, // TODO @lesan:枚举~
-        opName: '等于',
         opCode: '==',
         leftSide: '',
         rightSide: ''
@@ -205,6 +217,13 @@ const addConditionGroup = (conditions) => {
   }
   conditions.push(condition)
 }
+
+const validate = async () => {
+  if (!formRef) return false
+  return await formRef.value.validate()
+}
+
+defineExpose({ validate })
 </script>
 
 <style lang="scss" scoped>

+ 5 - 5
src/components/SimpleProcessDesignerV2/src/nodes/RouteNode.vue → src/components/SimpleProcessDesignerV2/src/nodes/RouterNode.vue

@@ -31,7 +31,7 @@
             {{ currentNode.showText }}
           </div>
           <div class="node-text" v-else>
-            {{ NODE_DEFAULT_TEXT.get(NodeType.ROUTE_BRANCH_NODE) }}
+            {{ NODE_DEFAULT_TEXT.get(NodeType.ROUTER_BRANCH_NODE) }}
           </div>
           <Icon v-if="!readonly" icon="ep:arrow-right-bold" />
         </div>
@@ -49,17 +49,17 @@
         :current-node="currentNode"
       />
     </div>
-    <RouteNodeConfig v-if="!readonly && currentNode" ref="nodeSetting" :flow-node="currentNode" />
+    <RouterNodeConfig v-if="!readonly && currentNode" ref="nodeSetting" :flow-node="currentNode" />
   </div>
 </template>
 <script setup lang="ts">
 import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
 import NodeHandler from '../NodeHandler.vue'
 import { useNodeName2, useWatchNode, useTaskStatusClass } from '../node'
-import RouteNodeConfig from '../nodes-config/RouteNodeConfig.vue'
+import RouterNodeConfig from '../nodes-config/RouterNodeConfig.vue'
 
 defineOptions({
-  name: 'RouteNode'
+  name: 'RouterNode'
 })
 
 const props = defineProps({
@@ -77,7 +77,7 @@ const readonly = inject<Boolean>('readonly')
 // 监控节点的变化
 const currentNode = useWatchNode(props)
 // 节点名称编辑
-const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.ROUTE_BRANCH_NODE)
+const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.ROUTER_BRANCH_NODE)
 
 const nodeSetting = ref()
 // 打开节点配置

+ 23 - 1
src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue

@@ -44,6 +44,12 @@
               :rows="4"
             />
           </el-form-item>
+          <el-form-item v-if="runningTask.signEnable" label="签名" prop="sign" ref="approveSignFormRef">
+            <el-button @click="signRef.open()">点击签名</el-button>
+            <el-image class="w-90px h-40px ml-5px" v-if="approveReasonForm.sign"
+                      :src="approveReasonForm.sign"
+                      :preview-src-list="[approveReasonForm.sign]"/>
+          </el-form-item>
           <el-form-item>
             <el-button :disabled="formLoading" type="success" @click="handleAudit(true, approveFormRef)">
               {{ getButtonDisplayName(OperationButtonType.APPROVE) }}
@@ -471,6 +477,8 @@
       <Icon :size="14" icon="ep:refresh" />&nbsp; 再次提交
     </div>
   </div>
+
+  <SignDialog ref="signRef" @success="handleSignFinish"/>
 </template>
 <script lang="ts" setup>
 import { useUserStoreWithOut } from '@/store/modules/user'
@@ -484,6 +492,7 @@ import {
 } from '@/components/SimpleProcessDesignerV2/src/consts'
 import { BpmProcessInstanceStatus, BpmModelFormType } from '@/utils/constants'
 import type { FormInstance, FormRules } from 'element-plus'
+import SignDialog from "./SignDialog.vue";
 defineOptions({ name: 'ProcessInstanceBtnContainer' })
 
 const router = useRouter() // 路由
@@ -522,11 +531,15 @@ const approveFormFApi = ref<any>({}) // approveForms 的 fAPi
 
 // 审批通过意见表单
 const approveFormRef = ref<FormInstance>()
+const signRef = ref()
+const approveSignFormRef = ref()
 const approveReasonForm = reactive({
-  reason: ''
+  reason: '',
+  sign: ''
 })
 const approveReasonRule = reactive<FormRules<typeof approveReasonForm>>({
   reason: [{ required: true, message: '审批意见不能为空', trigger: 'blur' }],
+  sign: [{ required: true, message: '签名不能为空', trigger: 'change' }]
 })
 // 拒绝表单
 const rejectFormRef = ref<FormInstance>()
@@ -672,6 +685,10 @@ const handleAudit = async (pass: boolean, formRef: FormInstance | undefined) =>
         reason: approveReasonForm.reason,
         variables // 审批通过, 把修改的字段值赋于流程实例变量
       }
+      // 签名
+      if (runningTask.value.signEnable) {
+        data.sign = approveReasonForm.sign
+      }
       // 多表单处理,并且有额外的 approveForm 表单,需要校验 + 拼接到 data 表单里提交
       // TODO 芋艿 任务有多表单这里要如何处理,会和可编辑的字段冲突
       const formCreateApi = approveFormFApi.value
@@ -966,6 +983,11 @@ const getUpdatedProcessInstanceVaiables = ()=> {
   return variables
 }
 
+const handleSignFinish = (url) => {
+  approveReasonForm.sign = url
+  approveSignFormRef.value.validate('change')
+}
+
 defineExpose({ loadTodoTask })
 </script>
 

+ 1 - 1
src/views/bpm/processInstance/detail/ProcessInstanceSimpleViewer.vue

@@ -128,7 +128,7 @@ const setSimpleModelNodeTaskStatus = (
     simpleModel.type === NodeType.CONDITION_BRANCH_NODE ||
     simpleModel.type === NodeType.PARALLEL_BRANCH_NODE ||
     simpleModel.type === NodeType.INCLUSIVE_BRANCH_NODE ||
-    simpleModel.type === NodeType.ROUTE_BRANCH_NODE
+    simpleModel.type === NodeType.ROUTER_BRANCH_NODE
   ) {
     // 网关节点。只有通过和未执行状态
     if (finishedActivityIds.includes(simpleModel.id)) {

+ 9 - 0
src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue

@@ -123,6 +123,15 @@
               >
                 审批意见:{{ task.reason }}
               </div>
+              <div
+                v-if="task.sign && activity.nodeType === NodeType.USER_TASK_NODE"
+                class="text-#a5a5a5 text-13px mt-1 w-full bg-#f8f8fa p2 rounded-md"
+              >
+                签名:
+                <el-image class="w-90px h-40px ml-5px"
+                          :src="task.sign"
+                          :preview-src-list="[task.sign]"/>
+              </div>
             </teleport>
           </div>
           <!-- 情况二:遍历每个审批节点下的【候选的】task 任务。例如说,1)依次审批,2)未来的审批任务等 -->

+ 84 - 0
src/views/bpm/processInstance/detail/SignDialog.vue

@@ -0,0 +1,84 @@
+<template>
+  <el-dialog
+    v-model="signDialogVisible"
+    title="签名"
+    width="935"
+  >
+    <div class="position-relative">
+      <Vue3Signature class="b b-solid b-gray" ref="signature" w="900px" h="400px"/>
+      <el-button
+        style="position: absolute; bottom: 20px; right: 10px"
+        type="primary"
+        text
+        size="small"
+        @click="signature.clear()"
+      >
+        <Icon icon="ep:delete" class="mr-5px"/>
+        清除
+      </el-button>
+    </div>
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button @click="signDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="submit">
+          提交
+        </el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import Vue3Signature from "vue3-signature"
+import * as FileApi from '@/api/infra/file'
+
+const message = useMessage() // 消息弹窗
+const signDialogVisible = ref(false)
+const signature = ref()
+
+const open = async () => {
+  signDialogVisible.value = true
+}
+defineExpose({open})
+
+const emits = defineEmits(['success'])
+const submit = async () => {
+  message.success('签名上传中请稍等。。。')
+  const res = await FileApi.updateFile({file: base64ToFile(signature.value.save('image/png'), '签名')})
+  emits('success', res.data)
+  signDialogVisible.value = false
+}
+
+const base64ToFile = (base64, fileName) => {
+  // 将base64按照 , 进行分割 将前缀  与后续内容分隔开
+  let data = base64.split(',');
+  // 利用正则表达式 从前缀中获取图片的类型信息(image/png、image/jpeg、image/webp等)
+  let type = data[0].match(/:(.*?);/)[1];
+  // 从图片的类型信息中 获取具体的文件格式后缀(png、jpeg、webp)
+  let suffix = type.split('/')[1];
+  // 使用atob()对base64数据进行解码  结果是一个文件数据流 以字符串的格式输出
+  const bstr = window.atob(data[1]);
+  // 获取解码结果字符串的长度
+  let n = bstr.length
+  // 根据解码结果字符串的长度创建一个等长的整形数字数组
+  // 但在创建时 所有元素初始值都为 0
+  const u8arr = new Uint8Array(n)
+  // 将整形数组的每个元素填充为解码结果字符串对应位置字符的UTF-16 编码单元
+  while (n--) {
+    // charCodeAt():获取给定索引处字符对应的 UTF-16 代码单元
+    u8arr[n] = bstr.charCodeAt(n)
+  }
+  // 利用构造函数创建File文件对象
+  // new File(bits, name, options)
+  const file = new File([u8arr], `${fileName}.${suffix}`, {
+    type: type
+  })
+  // 将File文件对象返回给方法的调用者
+  return file;
+}
+
+</script>
+
+<style scoped>
+
+</style>