Browse Source

!654 Simple设计器功能完善
Merge pull request !654 from Lesan/feature/bpm-n

芋道源码 7 months ago
parent
commit
d11a82a521

+ 1 - 0
package.json

@@ -73,6 +73,7 @@
     "vue-i18n": "9.10.2",
     "vue-router": "4.4.5",
     "vue-types": "^5.1.1",
+    "vue3-signature": "^0.2.4",
     "vuedraggable": "^4.1.0",
     "web-storage-cache": "^1.1.1",
     "xml-js": "^1.6.11"

+ 0 - 3
src/components/ESign/index.ts

@@ -1,3 +0,0 @@
-import ESign from './src/ESign.vue'
-
-export { ESign }

+ 0 - 289
src/components/ESign/src/ESign.vue

@@ -1,289 +0,0 @@
-<!-- TODO @lesan:看着没啥问题,哈哈哈,我还搜了下;嘿嘿,有没人封装好了组件库哈?https://github.com/WangShayne/vue3-signature (https://www.npmjs.com/package/vue3-signature) -->
-<template>
-  <div style="position: relative">
-    <canvas
-      ref="canvasRef"
-      @mousedown="mouseDown"
-      @mousemove="mouseMove"
-      @mouseup="mouseUp"
-      @touchstart="touchStart"
-      @touchmove="touchMove"
-      @touchend="touchEnd"
-      style="border: 1px solid lightgrey; max-width: 100%; display: block"
-    >
-    </canvas>
-
-    <el-button
-      style="position: absolute; bottom: 20px; right: 10px"
-      type="primary"
-      text
-      size="small"
-      @click="reset"
-    >
-      <Icon icon="ep:delete" class="mr-5px" />清除
-    </el-button>
-  </div>
-</template>
-
-<script lang="ts" setup>
-import { propTypes } from '@/utils/propTypes'
-
-defineOptions({ name: 'ESign' })
-
-const emits = defineEmits(['update:bgColor'])
-const props = defineProps({
-  // 画布宽度,即导出图片的宽度
-  width: propTypes.number.def(900),
-  // 画布高度,即导出图片的高度
-  height: propTypes.number.def(400),
-  // 画笔粗细
-  lineWidth: propTypes.number.def(10),
-  // 画笔颜色
-  lineColor: propTypes.string.def('#000000'),
-  // 画布背景色,为空时画布背景透明
-  bgColor: propTypes.string.def(''),
-  // 是否裁剪,在画布设定尺寸基础上裁掉四周空白部分
-  isCrop: propTypes.bool.def(false),
-  // 清空画布时是否同时清空设置的背景色
-  isClearBgColor: propTypes.bool.def(true),
-  // 生成图片格式
-  format: propTypes.string.def('image/png'),
-  // 生成图片质量,0 到 1
-  quality: propTypes.number.def(1)
-})
-const canvasRef = ref()
-const hasDrew = ref(false)
-const resultImg = ref('')
-const points = ref<any>([])
-const canvasTxt = ref()
-const startX = ref(0)
-const startY = ref(0)
-const isDrawing = ref(false)
-const sratio = ref(1)
-
-const ratio = computed(() => {
-  return props.height / props.width
-})
-const stageInfo = computed(() => {
-  return canvasRef.value.getBoundingClientRect()
-})
-const bgColor = computed(() => {
-  return props.bgColor ? props.bgColor : 'rgba(255, 255, 255, 0)'
-})
-
-watch(
-  () => bgColor.value,
-  () => {
-    if (canvasRef.value) {
-      canvasRef.value.style.background = bgColor.value
-    }
-  },
-  {
-    immediate: true
-  }
-)
-
-const resizeHandler = () => {
-  const canvas = canvasRef.value
-  canvas.style.width = props.width + 'px'
-  const realw = parseFloat(window.getComputedStyle(canvas).width)
-  canvas.style.height = ratio.value * realw + 'px'
-  canvasTxt.value = canvas.getContext('2d')
-  canvasTxt.value.scale(1 * sratio.value, 1 * sratio.value)
-  sratio.value = realw / props.width
-  canvasTxt.value.scale(1 / sratio.value, 1 / sratio.value)
-}
-// For PC
-const mouseDown = (e) => {
-  e.preventDefault()
-  isDrawing.value = true
-  hasDrew.value = true
-  let obj = {
-    x: e.offsetX,
-    y: e.offsetY
-  }
-  drawStart(obj)
-}
-const mouseMove = (e) => {
-  e.preventDefault()
-  if (isDrawing.value) {
-    let obj = {
-      x: e.offsetX,
-      y: e.offsetY
-    }
-    drawMove(obj)
-  }
-}
-const mouseUp = (e) => {
-  e.preventDefault()
-  let obj = {
-    x: e.offsetX,
-    y: e.offsetY
-  }
-  drawEnd(obj)
-  isDrawing.value = false
-}
-// For Mobile
-const touchStart = (e) => {
-  e.preventDefault()
-  hasDrew.value = true
-  if (e.touches.length === 1) {
-    let obj = {
-      x: e.targetTouches[0].clientX - canvasRef.value.getBoundingClientRect().left,
-      y: e.targetTouches[0].clientY - canvasRef.value.getBoundingClientRect().top
-    }
-    drawStart(obj)
-  }
-}
-const touchMove = (e) => {
-  e.preventDefault()
-  if (e.touches.length === 1) {
-    let obj = {
-      x: e.targetTouches[0].clientX - canvasRef.value.getBoundingClientRect().left,
-      y: e.targetTouches[0].clientY - canvasRef.value.getBoundingClientRect().top
-    }
-    drawMove(obj)
-  }
-}
-const touchEnd = (e) => {
-  e.preventDefault()
-  if (e.touches.length === 1) {
-    let obj = {
-      x: e.targetTouches[0].clientX - canvasRef.value.getBoundingClientRect().left,
-      y: e.targetTouches[0].clientY - canvasRef.value.getBoundingClientRect().top
-    }
-    drawEnd(obj)
-  }
-}
-// 绘制
-const drawStart = (obj) => {
-  startX.value = obj.x
-  startY.value = obj.y
-  canvasTxt.value.beginPath()
-  canvasTxt.value.moveTo(startX.value, startY.value)
-  canvasTxt.value.lineTo(obj.x, obj.y)
-  canvasTxt.value.lineCap = 'round'
-  canvasTxt.value.lineJoin = 'round'
-  canvasTxt.value.lineWidth = props.lineWidth * sratio.value
-  canvasTxt.value.stroke()
-  canvasTxt.value.closePath()
-  points.value.push(obj)
-}
-const drawMove = (obj) => {
-  canvasTxt.value.beginPath()
-  canvasTxt.value.moveTo(startX.value, startY.value)
-  canvasTxt.value.lineTo(obj.x, obj.y)
-  canvasTxt.value.strokeStyle = props.lineColor
-  canvasTxt.value.lineWidth = props.lineWidth * sratio.value
-  canvasTxt.value.lineCap = 'round'
-  canvasTxt.value.lineJoin = 'round'
-  canvasTxt.value.stroke()
-  canvasTxt.value.closePath()
-  startY.value = obj.y
-  startX.value = obj.x
-  points.value.push(obj)
-}
-const drawEnd = (obj) => {
-  canvasTxt.value.beginPath()
-  canvasTxt.value.moveTo(startX.value, startY.value)
-  canvasTxt.value.lineTo(obj.x, obj.y)
-  canvasTxt.value.lineCap = 'round'
-  canvasTxt.value.lineJoin = 'round'
-  canvasTxt.value.stroke()
-  canvasTxt.value.closePath()
-  points.value.push(obj)
-  points.value.push({ x: -1, y: -1 })
-}
-// 生成
-const generate = (options) => {
-  let imgFormat = options && options.format ? options.format : props.format
-  let imgQuality = options && options.quality ? options.quality : props.quality
-  const pm = new Promise((resolve, reject) => {
-    if (!hasDrew.value) {
-      reject(`Warning: Not Signned!`)
-      return
-    }
-    let resImgData = canvasTxt.value.getImageData(
-      0,
-      0,
-      canvasRef.value.width,
-      canvasRef.value.height
-    )
-    canvasTxt.value.globalCompositeOperation = 'destination-over'
-    canvasTxt.value.fillStyle = bgColor.value
-    canvasTxt.value.fillRect(0, 0, canvasRef.value.width, canvasRef.value.height)
-    resultImg.value = canvasRef.value.toDataURL(imgFormat, imgQuality)
-    canvasTxt.value.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height)
-    canvasTxt.value.putImageData(resImgData, 0, 0)
-    canvasTxt.value.globalCompositeOperation = 'source-over'
-    if (props.isCrop) {
-      const crop_area = getCropArea(resImgData.data)
-      let crop_canvas = document.createElement('canvas')
-      const crop_ctx = crop_canvas.getContext('2d')
-      crop_canvas.width = crop_area[2] - crop_area[0]
-      crop_canvas.height = crop_area[3] - crop_area[1]
-      const crop_imgData = canvasTxt.value.getImageData(...crop_area)
-      crop_ctx.globalCompositeOperation = 'destination-over'
-      crop_ctx.putImageData(crop_imgData, 0, 0)
-      crop_ctx.fillStyle = bgColor.value
-      crop_ctx.fillRect(0, 0, crop_canvas.width, crop_canvas.height)
-      resultImg.value = crop_canvas.toDataURL(imgFormat, imgQuality)
-    }
-    resolve(resultImg.value)
-  })
-  return pm
-}
-const reset = () => {
-  canvasTxt.value.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height)
-  if (props.isClearBgColor) {
-    emits('update:bgColor', '')
-    canvasRef.value.style.background = 'rgba(255, 255, 255, 0)'
-  }
-  points.value = []
-  hasDrew.value = false
-  resultImg.value = ''
-}
-const getCropArea = (imgData) => {
-  let topX = canvasRef.value.width
-  let btmX = 0
-  let topY = canvasRef.value.height
-  let btnY = 0
-  for (let i = 0; i < canvasRef.value.width; i++) {
-    for (let j = 0; j < canvasRef.value.height; j++) {
-      let pos = (i + canvasRef.value.width * j) * 4
-      if (imgData[pos] > 0 || imgData[pos + 1] > 0 || imgData[pos + 2] || imgData[pos + 3] > 0) {
-        btnY = Math.max(j, btnY)
-        btmX = Math.max(i, btmX)
-        topY = Math.min(j, topY)
-        topX = Math.min(i, topX)
-      }
-    }
-  }
-  topX++
-  btmX++
-  topY++
-  btnY++
-  const data = [topX, topY, btmX, btnY]
-  return data
-}
-
-defineExpose({
-  generate
-})
-onBeforeMount(() => {
-  window.addEventListener('resize', resizeHandler)
-})
-onBeforeUnmount(() => {
-  window.removeEventListener('resize', resizeHandler)
-})
-onMounted(() => {
-  canvasRef.value.height = props.height
-  canvasRef.value.width = props.width
-  canvasRef.value.style.background = bgColor.value
-  resizeHandler()
-  // 在画板以外松开鼠标后冻结画笔
-  document.onmouseup = () => {
-    isDrawing.value = false
-  }
-})
-</script>

+ 0 - 1
src/components/SimpleProcessDesignerV2/src/NodeHandler.vue

@@ -242,7 +242,6 @@ const addNode = (type: number) => {
     emits('update:childNode', data)
   }
   if (type === NodeType.ROUTE_BRANCH_NODE) {
-    // TODO @lesan:高亮那边,需要考虑下。
     const data: SimpleFlowNode = {
       id: 'GateWay_' + generateUUID(),
       name: NODE_DEFAULT_NAME.get(NodeType.ROUTE_BRANCH_NODE) as string,

+ 10 - 8
src/components/SimpleProcessDesignerV2/src/consts.ts

@@ -116,8 +116,10 @@ export interface SimpleFlowNode {
   // 延迟设置
   delaySetting?: DelaySetting
   // 路由分支
-  routeGroup?: RouteCondition[]
+  routerGroups?: RouteCondition[]
   defaultFlowId?: string
+  // 签名
+  signEnable?: boolean
 }
 // 候选人策略枚举 ( 用于审批节点。抄送节点 )
 export enum CandidateStrategy {
@@ -241,15 +243,15 @@ export type AssignEmptyHandler = {
 export type ListenerHandler = {
   enable: boolean
   path?: string
-  header?: ListenerMap[]
-  body?: ListenerMap[]
+  header?: ListenerParam[]
+  body?: ListenerParam[]
 }
-export type ListenerMap = {
+export type ListenerParam = {
   key: string
   type: number
   value: string
 }
-export enum ListenerMapTypeEnum {
+export enum ListenerParamTypeEnum {
   /**
    * 固定值
    */
@@ -510,8 +512,8 @@ export const APPROVE_METHODS: DictDataVO[] = [
 ]
 
 export const CONDITION_CONFIG_TYPES: DictDataVO[] = [
-  { label: '条件表达式', value: ConditionType.EXPRESSION },
-  { label: '条件规则', value: ConditionType.RULE }
+  { label: '条件规则', value: ConditionType.RULE },
+  { label: '条件表达式', value: ConditionType.EXPRESSION }
 ]
 
 // 时间单位类型
@@ -660,7 +662,7 @@ export const DELAY_TYPE = [
  */
 export type RouteCondition = {
   nodeId: string
-  conditionType: number // TODO @lesan:ConditionType
+  conditionType: ConditionType
   conditionExpression: string
   conditionGroups: ConditionGroup
 }

+ 8 - 7
src/components/SimpleProcessDesignerV2/src/node.ts

@@ -15,7 +15,7 @@ import {
   AssignStartUserHandlerType,
   AssignEmptyHandlerType,
   FieldPermissionType,
-  ListenerMap
+  ListenerParam
 } from './consts'
 import { parseFormFields } from '@/components/FormCreate/src/utils/index'
 export function useWatchNode(props: { flowNode: SimpleFlowNode }): Ref<SimpleFlowNode> {
@@ -139,16 +139,17 @@ export type UserTaskFormType = {
   buttonsSetting: any[]
   taskCreateListenerEnable?: boolean
   taskCreateListenerPath?: string
-  taskCreateListenerHeader?: ListenerMap[]
-  taskCreateListenerBody?: ListenerMap[]
+  taskCreateListenerHeader?: ListenerParam[]
+  taskCreateListenerBody?: ListenerParam[]
   taskAssignListenerEnable?: boolean
   taskAssignListenerPath?: string
-  taskAssignListenerHeader?: ListenerMap[]
-  taskAssignListenerBody?: ListenerMap[]
+  taskAssignListenerHeader?: ListenerParam[]
+  taskAssignListenerBody?: ListenerParam[]
   taskCompleteListenerEnable?: boolean
   taskCompleteListenerPath?: string
-  taskCompleteListenerHeader?: ListenerMap[]
-  taskCompleteListenerBody?: ListenerMap[]
+  taskCompleteListenerHeader?: ListenerParam[]
+  taskCompleteListenerBody?: ListenerParam[]
+  signEnable: boolean
 }
 
 export type CopyTaskFormType = {

+ 16 - 237
src/components/SimpleProcessDesignerV2/src/nodes-config/RouteNodeConfig.vue

@@ -25,7 +25,7 @@
     </template>
     <div>
       <el-form label-position="top">
-        <el-card class="mb-15px" v-for="(item, index) in routeGroup" :key="index">
+        <el-card class="mb-15px" v-for="(item, index) in routerGroups" :key="index">
           <template #header>
             <div class="flex flex-items-center">
               <el-text size="large">路由{{ index + 1 }}</el-text>
@@ -42,123 +42,7 @@
               >
             </div>
           </template>
-          <el-form-item label="配置方式" prop="conditionType">
-            <el-radio-group v-model="item.conditionType" @change="changeConditionType">
-              <el-radio
-                v-for="(dict, indexConditionType) in conditionConfigTypes"
-                :key="indexConditionType"
-                :value="dict.value"
-                :label="dict.value"
-              >
-                {{ dict.label }}
-              </el-radio>
-            </el-radio-group>
-          </el-form-item>
-          <!-- TODO @lesan:1)1、2 使用枚举;2)默认先 条件组关系,再 条件表达式;3)这种可以封装成一个小组件么? -->
-          <el-form-item
-            v-if="item.conditionType === 1"
-            label="条件表达式"
-            prop="conditionExpression"
-          >
-            <el-input
-              type="textarea"
-              v-model="item.conditionExpression"
-              clearable
-              style="width: 100%"
-            />
-          </el-form-item>
-          <el-form-item v-if="item.conditionType === 2" label="条件规则">
-            <div class="condition-group-tool">
-              <div class="flex items-center">
-                <div class="mr-4">条件组关系</div>
-                <el-switch
-                  v-model="item.conditionGroups.and"
-                  inline-prompt
-                  active-text="且"
-                  inactive-text="或"
-                />
-              </div>
-            </div>
-            <el-space direction="vertical" :spacer="item.conditionGroups.and ? '且' : '或'">
-              <el-card
-                class="condition-group"
-                style="width: 530px"
-                v-for="(condition, cIdx) in item.conditionGroups.conditions"
-                :key="cIdx"
-              >
-                <div
-                  class="condition-group-delete"
-                  v-if="item.conditionGroups.conditions.length > 1"
-                >
-                  <Icon
-                    color="#0089ff"
-                    icon="ep:circle-close-filled"
-                    :size="18"
-                    @click="deleteConditionGroup(item.conditionGroups.conditions, 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="(field, fIdx) in fieldOptions"
-                        :key="fIdx"
-                        :label="field.title"
-                        :value="field.field"
-                        :disabled="!field.required"
-                      />
-                    </el-select>
-                  </div>
-                  <div class="mr-2">
-                    <el-select v-model="rule.opCode" style="width: 100px">
-                      <el-option
-                        v-for="operator in COMPARISON_OPERATORS"
-                        :key="operator.value"
-                        :label="operator.label"
-                        :value="operator.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(item.conditionGroups.conditions)"
-              />
-            </div>
-          </el-form-item>
+          <Condition v-model="routerGroups[index]" />
         </el-card>
       </el-form>
 
@@ -177,18 +61,9 @@
 </template>
 <script setup lang="ts">
 import { Plus } from '@element-plus/icons-vue'
-import {
-  SimpleFlowNode,
-  NodeType,
-  CONDITION_CONFIG_TYPES,
-  ConditionType,
-  COMPARISON_OPERATORS,
-  RouteCondition,
-  ProcessVariableEnum
-} from '../consts'
+import { SimpleFlowNode, NodeType, ConditionType, RouteCondition } from '../consts'
 import { useWatchNode, useDrawer, useNodeName } from '../node'
-import { BpmModelFormType } from '@/utils/constants'
-import { useFormFields } from '../node'
+import Condition from './components/Condition.vue'
 defineOptions({
   name: 'RouteNodeConfig'
 })
@@ -206,29 +81,7 @@ const { settingVisible, closeDrawer, openDrawer } = useDrawer()
 const currentNode = useWatchNode(props)
 // 节点名称
 const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.ROUTE_BRANCH_NODE)
-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 fieldOptions = computed(() => {
-  const fieldsCopy = useFormFields().slice()
-  // 固定添加发起人 ID 字段
-  fieldsCopy.unshift({
-    field: ProcessVariableEnum.START_USER_ID,
-    title: '发起人',
-    required: true
-  })
-  return fieldsCopy
-})
-const routeGroup = ref<RouteCondition[]>([])
+const routerGroups = ref<RouteCondition[]>([])
 const nodeOptions = ref()
 
 // 保存配置
@@ -237,26 +90,26 @@ const saveConfig = async () => {
   if (!showText) return false
   currentNode.value.name = nodeName.value!
   currentNode.value.showText = showText
-  currentNode.value.routeGroup = routeGroup.value
+  currentNode.value.routerGroups = routerGroups.value
   settingVisible.value = false
   return true
 }
 // 显示路由分支节点配置, 由父组件传过来
 const showRouteNodeConfig = (node: SimpleFlowNode) => {
   getRoutableNode()
-  routeGroup.value = []
+  routerGroups.value = []
   nodeName.value = node.name
-  if (node.routeGroup) {
-    routeGroup.value = node.routeGroup
+  if (node.routerGroups) {
+    routerGroups.value = node.routerGroups
   }
 }
 
 const getShowText = () => {
-  if (!routeGroup.value || !Array.isArray(routeGroup.value) || routeGroup.value.length <= 0) {
+  if (!routerGroups.value || !Array.isArray(routerGroups.value) || routerGroups.value.length <= 0) {
     message.warning('请配置路由!')
     return ''
   }
-  for (const route of routeGroup.value) {
+  for (const route of routerGroups.value) {
     if (!route.nodeId || !route.conditionType) {
       message.warning('请完善路由配置项!')
       return ''
@@ -276,51 +129,13 @@ const getShowText = () => {
       }
     }
   }
-  return `${routeGroup.value.length}条路由分支`
-}
-
-// TODO @lesan:这个需要实现么?
-const changeConditionType = () => {}
-
-const deleteConditionGroup = (conditions, index) => {
-  conditions.splice(index, 1)
-}
-
-const deleteConditionRule = (condition, index) => {
-  condition.rules.splice(index, 1)
-}
-
-const addConditionRule = (condition, index) => {
-  const rule = {
-    type: 1,
-    opName: '等于',
-    opCode: '==',
-    leftSide: '',
-    rightSide: ''
-  }
-  condition.rules.splice(index + 1, 0, rule)
-}
-
-const addConditionGroup = (conditions) => {
-  const condition = {
-    and: true,
-    rules: [
-      {
-        type: 1,
-        opName: '等于',
-        opCode: '==',
-        leftSide: '',
-        rightSide: ''
-      }
-    ]
-  }
-  conditions.push(condition)
+  return `${routerGroups.value.length}条路由分支`
 }
 
 const addRouteGroup = () => {
-  routeGroup.value.push({
+  routerGroups.value.push({
     nodeId: '',
-    conditionType: ConditionType.EXPRESSION,
+    conditionType: ConditionType.RULE,
     conditionExpression: '',
     conditionGroups: {
       and: true,
@@ -343,11 +158,11 @@ const addRouteGroup = () => {
 }
 
 const deleteRouteGroup = (index) => {
-  routeGroup.value.splice(index, 1)
+  routerGroups.value.splice(index, 1)
 }
 
 const getRoutableNode = () => {
-  // TODO 还需要满足以下要求
+  // TODO @lesan 还需要满足以下要求
   // 并行分支、包容分支内部节点不能跳转到外部节点
   // 条件分支节点可以向上跳转到外部节点
   let node = processNodeTree?.value
@@ -369,39 +184,3 @@ const getRoutableNode = () => {
 
 defineExpose({ openDrawer, showRouteNodeConfig }) // 暴露方法给父组件
 </script>
-
-<style lang="scss" scoped>
-.condition-group-tool {
-  display: flex;
-  justify-content: space-between;
-  width: 500px;
-  margin-bottom: 20px;
-}
-
-.condition-group {
-  position: relative;
-
-  &:hover {
-    border-color: #0089ff;
-
-    .condition-group-delete {
-      opacity: 1;
-    }
-  }
-
-  .condition-group-delete {
-    position: absolute;
-    top: 0;
-    left: 0;
-    display: flex;
-    cursor: pointer;
-    opacity: 0;
-  }
-}
-
-::v-deep(.el-card__header) {
-  padding: 8px var(--el-card-padding);
-  border-bottom: 1px solid var(--el-card-border-color);
-  box-sizing: border-box;
-}
-</style>

+ 161 - 142
src/components/SimpleProcessDesignerV2/src/nodes-config/UserTaskNodeConfig.vue

@@ -356,6 +356,15 @@
                 </div>
               </el-radio-group>
             </el-form-item>
+
+            <el-divider content-position="left">是否需要签名</el-divider>
+            <el-form-item prop="signEnable">
+              <el-switch
+                v-model="configForm.signEnable"
+                active-text="是"
+                inactive-text="否"
+              />
+            </el-form-item>
           </el-form>
         </div>
       </el-tab-pane>
@@ -436,155 +445,161 @@
         </div>
       </el-tab-pane>
       <el-tab-pane label="监听器" name="listener">
-        <div v-for="(listener, listenerIdx) in taskListener" :key="listenerIdx">
-          <el-form label-position="top">
-            <div>
-              <el-divider content-position="left">
-                <el-text tag="b" size="large">{{ listener.name }}</el-text>
-              </el-divider>
+        <el-form :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>
+            </el-divider>
+            <el-form-item>
+              <el-switch
+                v-model="configForm[`task${listener.type}ListenerEnable`]"
+                active-text="开启"
+                inactive-text="关闭"
+              />
+            </el-form-item>
+            <div v-if="configForm[`task${listener.type}ListenerEnable`]">
               <el-form-item>
-                <el-switch
-                  v-model="configForm[`task${listener.type}ListenerEnable`]"
-                  active-text="开启"
-                  inactive-text="关闭"
+                <el-alert
+                  title="仅支持 POST 请求,以请求体方式接收参数"
+                  type="warning"
+                  show-icon
+                  :closable="false"
                 />
               </el-form-item>
-              <div v-if="configForm[`task${listener.type}ListenerEnable`]">
-                <el-form-item>
-                  <el-alert
-                    title="仅支持 POST 请求,以请求体方式接收参数"
-                    type="warning"
-                    show-icon
-                    :closable="false"
-                  />
-                </el-form-item>
-                <el-form-item label="请求地址">
-                  <el-input v-model="configForm[`task${listener.type}ListenerPath`]" />
-                </el-form-item>
-                <el-form-item label="请求头">
-                  <div
-                    class="flex pt-2"
-                    v-for="(item, index) in configForm[`task${listener.type}ListenerHeader`]"
-                    :key="index"
-                  >
-                    <div class="mr-2">
-                      <el-input class="w-160px" v-model="item.key" />
-                    </div>
-                    <div class="mr-2">
-                      <el-select class="w-100px!" v-model="item.type">
-                        <el-option
-                          v-for="types in LISTENER_MAP_TYPES"
-                          :key="types.value"
-                          :label="types.label"
-                          :value="types.value"
-                        />
-                      </el-select>
-                    </div>
-                    <div class="mr-2">
-                      <el-input
-                        v-if="item.type === ListenerMapTypeEnum.FIXED_VALUE"
-                        class="w-160px"
-                        v-model="item.value"
+              <el-form-item
+                label="请求地址"
+                :prop="`task${listener.type}ListenerPath`"
+                :rules="{
+                  required: true,
+                  message: '请求地址不能为空',
+                  trigger: 'blur'
+                }"
+              >
+                <el-input v-model="configForm[`task${listener.type}ListenerPath`]" />
+              </el-form-item>
+              <el-form-item label="请求头">
+                <div
+                  class="flex pt-2"
+                  v-for="(item, index) in configForm[`task${listener.type}ListenerHeader`]"
+                  :key="index"
+                >
+                  <div class="mr-2">
+                    <el-input class="w-160px" v-model="item.key" />
+                  </div>
+                  <div class="mr-2">
+                    <el-select class="w-100px!" v-model="item.type">
+                      <el-option
+                        v-for="types in LISTENER_MAP_TYPES"
+                        :key="types.value"
+                        :label="types.label"
+                        :value="types.value"
                       />
-                      <el-select
-                        v-if="item.type === ListenerMapTypeEnum.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>
-                    </div>
-                    <div class="mr-1 flex items-center">
-                      <Icon
-                        icon="ep:delete"
-                        :size="18"
-                        @click="
-                          deleteTaskListenerMap(
-                            configForm[`task${listener.type}ListenerHeader`],
-                            index
-                          )
-                        "
+                    </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-option
+                        v-for="(field, fIdx) in formFieldOptions"
+                        :key="fIdx"
+                        :label="field.title"
+                        :value="field.field"
+                        :disabled="!field.required"
                       />
-                    </div>
+                    </el-select>
                   </div>
-                  <el-button
-                    type="primary"
-                    text
-                    @click="addTaskListenerMap(configForm[`task${listener.type}ListenerHeader`])"
-                  >
-                    <Icon icon="ep:plus" class="mr-5px" />添加一行
-                  </el-button>
-                </el-form-item>
-                <el-form-item label="请求体">
-                  <div
-                    class="flex pt-2"
-                    v-for="(item, index) in configForm[`task${listener.type}ListenerBody`]"
-                    :key="index"
-                  >
-                    <div class="mr-2">
-                      <el-input class="w-160px" v-model="item.key" />
-                    </div>
-                    <div class="mr-2">
-                      <el-select class="w-100px!" v-model="item.type">
-                        <el-option
-                          v-for="types in LISTENER_MAP_TYPES"
-                          :key="types.value"
-                          :label="types.label"
-                          :value="types.value"
-                        />
-                      </el-select>
-                    </div>
-                    <div class="mr-2">
-                      <el-input
-                        v-if="item.type === ListenerMapTypeEnum.FIXED_VALUE"
-                        class="w-160px"
-                        v-model="item.value"
+                  <div class="mr-1 flex items-center">
+                    <Icon
+                      icon="ep:delete"
+                      :size="18"
+                      @click="
+                        deleteTaskListenerParam(
+                          configForm[`task${listener.type}ListenerHeader`],
+                          index
+                        )
+                      "
+                    />
+                  </div>
+                </div>
+                <el-button
+                  type="primary"
+                  text
+                  @click="addTaskListenerParam(configForm[`task${listener.type}ListenerHeader`])"
+                >
+                  <Icon icon="ep:plus" class="mr-5px" />添加一行
+                </el-button>
+              </el-form-item>
+              <el-form-item label="请求体">
+                <div
+                  class="flex pt-2"
+                  v-for="(item, index) in configForm[`task${listener.type}ListenerBody`]"
+                  :key="index"
+                >
+                  <div class="mr-2">
+                    <el-input class="w-160px" v-model="item.key" />
+                  </div>
+                  <div class="mr-2">
+                    <el-select class="w-100px!" v-model="item.type">
+                      <el-option
+                        v-for="types in LISTENER_MAP_TYPES"
+                        :key="types.value"
+                        :label="types.label"
+                        :value="types.value"
                       />
-                      <el-select
-                        v-if="item.type === ListenerMapTypeEnum.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>
-                    </div>
-                    <div class="mr-1 flex items-center">
-                      <Icon
-                        icon="ep:delete"
-                        :size="18"
-                        @click="
-                          deleteTaskListenerMap(
-                            configForm[`task${listener.type}ListenerBody`],
-                            index
-                          )
-                        "
+                    </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-option
+                        v-for="(field, fIdx) in formFieldOptions"
+                        :key="fIdx"
+                        :label="field.title"
+                        :value="field.field"
+                        :disabled="!field.required"
                       />
-                    </div>
+                    </el-select>
                   </div>
-                  <el-button
-                    type="primary"
-                    text
-                    @click="addTaskListenerMap(configForm[`task${listener.type}ListenerBody`])"
-                  >
-                    <Icon icon="ep:plus" class="mr-5px" />添加一行
-                  </el-button>
-                </el-form-item>
-              </div>
+                  <div class="mr-1 flex items-center">
+                    <Icon
+                      icon="ep:delete"
+                      :size="18"
+                      @click="
+                        deleteTaskListenerParam(
+                          configForm[`task${listener.type}ListenerBody`],
+                          index
+                        )
+                      "
+                    />
+                  </div>
+                </div>
+                <el-button
+                  type="primary"
+                  text
+                  @click="addTaskListenerParam(configForm[`task${listener.type}ListenerBody`])"
+                >
+                  <Icon icon="ep:plus" class="mr-5px" />添加一行
+                </el-button>
+              </el-form-item>
             </div>
-          </el-form>
-        </div>
+          </div>
+        </el-form>
       </el-tab-pane>
     </el-tabs>
     <template #footer>
@@ -623,7 +638,7 @@ import {
   FieldPermissionType,
   ProcessVariableEnum,
   LISTENER_MAP_TYPES,
-  ListenerMapTypeEnum
+  ListenerParamTypeEnum
 } from '../consts'
 
 import {
@@ -852,6 +867,8 @@ const saveConfig = async () => {
     header: configForm.value.taskCompleteListenerHeader,
     body: configForm.value.taskCompleteListenerBody
   }
+  // 签名
+  currentNode.value.signEnable = configForm.value.signEnable
 
   currentNode.value.showText = showText
   settingVisible.value = false
@@ -919,6 +936,8 @@ const showUserTaskNodeConfig = (node: SimpleFlowNode) => {
   configForm.value.taskCompleteListenerPath = node.taskCompleteListener!.path
   configForm.value.taskCompleteListenerHeader = node.taskCompleteListener?.header ?? []
   configForm.value.taskCompleteListenerBody = node.taskCompleteListener?.body ?? []
+  // 6. 签名
+  configForm.value.signEnable = node.signEnable ?? false
 }
 
 defineExpose({ openDrawer, showUserTaskNodeConfig }) // 暴露方法给父组件
@@ -1032,14 +1051,14 @@ function useTimeoutHandler() {
   }
 }
 
-const addTaskListenerMap = (arr) => {
+const addTaskListenerParam = (arr) => {
   arr.push({
     key: '',
     type: 1,
     value: ''
   })
 }
-const deleteTaskListenerMap = (arr, index) => {
+const deleteTaskListenerParam = (arr, index) => {
   arr.splice(index, 1)
 }
 </script>

+ 242 - 0
src/components/SimpleProcessDesignerV2/src/nodes-config/components/Condition.vue

@@ -0,0 +1,242 @@
+<template>
+  <el-form ref="formRef" :model="condition" :rules="formRules" label-position="top">
+    <el-form-item label="配置方式" prop="conditionType">
+      <el-radio-group v-model="condition.conditionType">
+        <el-radio
+          v-for="(dict, indexConditionType) in conditionConfigTypes"
+          :key="indexConditionType"
+          :value="dict.value"
+          :label="dict.value"
+        >
+          {{ dict.label }}
+        </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">
+          <div class="mr-4">条件组关系</div>
+          <el-switch
+            v-model="condition.conditionGroups.and"
+            inline-prompt
+            active-text="且"
+            inactive-text="或"
+          />
+        </div>
+      </div>
+      <el-space direction="vertical" :spacer="condition.conditionGroups.and ? '且' : '或'">
+        <el-card
+          class="condition-group"
+          style="width: 530px"
+          v-for="(equation, cIdx) in condition.conditionGroups.conditions"
+          :key="cIdx"
+        >
+          <div
+            class="condition-group-delete"
+            v-if="condition.conditionGroups.conditions.length > 1"
+          >
+            <Icon
+              color="#0089ff"
+              icon="ep:circle-close-filled"
+              :size="18"
+              @click="deleteConditionGroup(condition.conditionGroups.conditions, 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="equation.and"
+                  inline-prompt
+                  active-text="且"
+                  inactive-text="或"
+                />
+              </div>
+            </div>
+          </template>
+
+          <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>
+            </div>
+            <div class="mr-2">
+              <el-select v-model="rule.opCode" style="width: 100px">
+                <el-option
+                  v-for="operator in COMPARISON_OPERATORS"
+                  :key="operator.value"
+                  :label="operator.label"
+                  :value="operator.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="equation.rules.length > 1">
+              <Icon icon="ep:delete" :size="18" @click="deleteConditionRule(equation, rIdx)" />
+            </div>
+            <div class="flex items-center">
+              <Icon icon="ep:plus" :size="18" @click="addConditionRule(equation, rIdx)" />
+            </div>
+          </div>
+        </el-card>
+      </el-space>
+      <div title="添加条件组" class="mt-4 cursor-pointer">
+        <Icon
+          color="#0089ff"
+          icon="ep:plus"
+          :size="24"
+          @click="addConditionGroup(condition.conditionGroups.conditions)"
+        />
+      </div>
+    </el-form-item>
+  </el-form>
+</template>
+
+<script setup lang="ts">
+import {
+  CONDITION_CONFIG_TYPES,
+  COMPARISON_OPERATORS,
+  ConditionType,
+  ProcessVariableEnum
+} from '../../consts'
+import { BpmModelFormType } from '@/utils/constants'
+import { useFormFields } from '../../node'
+
+const props = defineProps({
+  modelValue: {
+    type: Object,
+    required: true
+  }
+})
+const emit = defineEmits(['update:modelValue'])
+const condition = computed({
+  get() {
+    return props.modelValue
+  },
+  set(newValue) {
+    emit('update:modelValue', newValue)
+  }
+})
+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 fieldOptions = computed(() => {
+  const fieldsCopy = useFormFields().slice()
+  // 固定添加发起人 ID 字段
+  fieldsCopy.unshift({
+    field: ProcessVariableEnum.START_USER_ID,
+    title: '发起人',
+    required: true
+  })
+  return fieldsCopy
+})
+// 表单校验规则
+const formRules = reactive({
+  conditionType: [{ required: true, message: '配置方式不能为空', trigger: 'blur' }],
+  conditionExpression: [{ required: true, message: '条件表达式不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+const deleteConditionGroup = (conditions, index) => {
+  conditions.splice(index, 1)
+}
+
+const deleteConditionRule = (condition, index) => {
+  condition.rules.splice(index, 1)
+}
+
+const addConditionRule = (condition, index) => {
+  const rule = {
+    type: 1,
+    opName: '等于',
+    opCode: '==',
+    leftSide: '',
+    rightSide: ''
+  }
+  condition.rules.splice(index + 1, 0, rule)
+}
+
+const addConditionGroup = (conditions) => {
+  const condition = {
+    and: true,
+    rules: [
+      {
+        type: 1,
+        opName: '等于',
+        opCode: '==',
+        leftSide: '',
+        rightSide: ''
+      }
+    ]
+  }
+  conditions.push(condition)
+}
+</script>
+
+<style lang="scss" scoped>
+.condition-group-tool {
+  display: flex;
+  justify-content: space-between;
+  width: 500px;
+  margin-bottom: 20px;
+}
+
+.condition-group {
+  position: relative;
+
+  &:hover {
+    border-color: #0089ff;
+
+    .condition-group-delete {
+      opacity: 1;
+    }
+  }
+
+  .condition-group-delete {
+    position: absolute;
+    top: 0;
+    left: 0;
+    display: flex;
+    cursor: pointer;
+    opacity: 0;
+  }
+}
+
+::v-deep(.el-card__header) {
+  padding: 8px var(--el-card-padding);
+  border-bottom: 1px solid var(--el-card-border-color);
+  box-sizing: border-box;
+}
+</style>

+ 3 - 2
src/views/bpm/processInstance/detail/ProcessInstanceSimpleViewer.vue

@@ -128,7 +128,8 @@ const setSimpleModelNodeTaskStatus = (
   if (
     simpleModel.type === NodeType.CONDITION_BRANCH_NODE ||
     simpleModel.type === NodeType.PARALLEL_BRANCH_NODE ||
-    simpleModel.type === NodeType.INCLUSIVE_BRANCH_NODE
+    simpleModel.type === NodeType.INCLUSIVE_BRANCH_NODE ||
+    simpleModel.type === NodeType.ROUTE_BRANCH_NODE
   ) {
     // 网关节点。只有通过和未执行状态
     if (finishedActivityIds.includes(simpleModel.id)) {
@@ -163,7 +164,7 @@ const setSimpleModelNodeTaskStatus = (
 .process-viewer-container {
   width: 100%;
   height: 100%;
-  
+
   :deep(.process-viewer) {
     width: 100%;
     height: 100% !important;