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

!452 BPM 的草稿 PR(非 BPM 开发成员,不用关注)
Merge pull request !452 from 芋道源码/feature/bpm

芋道源码 10 месяцев назад
Родитель
Сommit
b94235adf3
47 измененных файлов с 5697 добавлено и 2663 удалено
  1. 5 1
      src/api/bpm/model/index.ts
  2. 41 1
      src/api/bpm/processInstance/index.ts
  3. 15 0
      src/api/bpm/simple/index.ts
  4. 46 0
      src/api/bpm/task/index.ts
  5. 0 237
      src/components/SimpleProcessDesigner/src/addNode.vue
  6. 0 297
      src/components/SimpleProcessDesigner/src/nodeWrap.vue
  7. 0 165
      src/components/SimpleProcessDesigner/src/util.ts
  8. 0 1292
      src/components/SimpleProcessDesigner/theme/workflow.css
  9. 168 0
      src/components/SimpleProcessDesignerV2/src/NodeHandler.vue
  10. 107 0
      src/components/SimpleProcessDesignerV2/src/ProcessNodeTree.vue
  11. 215 0
      src/components/SimpleProcessDesignerV2/src/SimpleProcessDesigner.vue
  12. 544 0
      src/components/SimpleProcessDesignerV2/src/consts.ts
  13. 4 0
      src/components/SimpleProcessDesignerV2/src/index.ts
  14. 478 0
      src/components/SimpleProcessDesignerV2/src/node.ts
  15. 419 0
      src/components/SimpleProcessDesignerV2/src/nodes-config/ConditionNodeConfig.vue
  16. 307 0
      src/components/SimpleProcessDesignerV2/src/nodes-config/CopyTaskNodeConfig.vue
  17. 136 0
      src/components/SimpleProcessDesignerV2/src/nodes-config/StartUserNodeConfig.vue
  18. 901 0
      src/components/SimpleProcessDesignerV2/src/nodes-config/UserTaskNodeConfig.vue
  19. 79 0
      src/components/SimpleProcessDesignerV2/src/nodes/CopyTaskNode.vue
  20. 13 0
      src/components/SimpleProcessDesignerV2/src/nodes/EndEventNode.vue
  21. 207 0
      src/components/SimpleProcessDesignerV2/src/nodes/ExclusiveNode.vue
  22. 181 0
      src/components/SimpleProcessDesignerV2/src/nodes/ParallelNode.vue
  23. 69 0
      src/components/SimpleProcessDesignerV2/src/nodes/StartUserNode.vue
  24. 88 0
      src/components/SimpleProcessDesignerV2/src/nodes/UserTaskNode.vue
  25. 33 0
      src/components/SimpleProcessDesignerV2/src/utils.ts
  26. BIN
      src/components/SimpleProcessDesignerV2/theme/iconfont.ttf
  27. BIN
      src/components/SimpleProcessDesignerV2/theme/iconfont.woff
  28. BIN
      src/components/SimpleProcessDesignerV2/theme/iconfont.woff2
  29. 714 0
      src/components/SimpleProcessDesignerV2/theme/simple-process-designer.scss
  30. 11 0
      src/directives/index.ts
  31. 4 2
      src/main.ts
  32. 9 2
      src/router/modules/remaining.ts
  33. 10 10
      src/store/modules/bpm/simpleWorkflow.ts
  34. 12 0
      src/utils/constants.ts
  35. 1 0
      src/utils/dict.ts
  36. 139 83
      src/views/bpm/model/ModelForm.vue
  37. 0 141
      src/views/bpm/model/ModelImportForm.vue
  38. 3 3
      src/views/bpm/model/editor/index.vue
  39. 133 144
      src/views/bpm/model/index.vue
  40. 85 18
      src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue
  41. 202 130
      src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue
  42. 121 23
      src/views/bpm/processInstance/detail/index.vue
  43. 176 85
      src/views/bpm/processInstance/detail/index_new.vue
  44. 9 24
      src/views/bpm/simpleWorkflow/index.vue
  45. 8 3
      src/views/bpm/task/copy/index.vue
  46. 2 1
      src/views/bpm/task/done/index.vue
  47. 2 1
      src/views/bpm/task/todo/index.vue

+ 5 - 1
src/api/bpm/model/index.ts

@@ -30,7 +30,7 @@ export const getModelPage = async (params) => {
   return await request.get({ url: '/bpm/model/page', params })
 }
 
-export const getModel = async (id: number) => {
+export const getModel = async (id: string) => {
   return await request.get({ url: '/bpm/model/get?id=' + id })
 }
 
@@ -38,6 +38,10 @@ export const updateModel = async (data: ModelVO) => {
   return await request.put({ url: '/bpm/model/update', data: data })
 }
 
+export const updateModelBpmn = async (data: ModelVO) => {
+  return await request.put({ url: '/bpm/model/update-bpmn', data: data })
+}
+
 // 任务状态修改
 export const updateModelState = async (id: number, state: number) => {
   const data = {

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

@@ -1,6 +1,6 @@
 import request from '@/config/axios'
 import { ProcessDefinitionVO } from '@/api/bpm/model'
-
+import { NodeType } from '@/components/SimpleProcessDesignerV2/src/consts'
 export type Task = {
   id: string
   name: string
@@ -22,6 +22,35 @@ export type ProcessInstanceVO = {
   processDefinition?: ProcessDefinitionVO
 }
 
+// 用户信息
+export type User = {
+  id: number,
+  nickname: string,
+  avatar: string
+}
+
+// 审批任务信息
+export type ApprovalTaskInfo = {
+  id: number,
+  ownerUser: User,
+  assigneeUser: User,
+  status: number,
+  reason: string
+
+}
+
+// 审批节点信息
+export type ApprovalNodeInfo = {
+  id : number
+  name: string
+  nodeType: NodeType
+  status: number
+  startTime?: Date
+  endTime?: Date
+  candidateUserList?: User[]
+  tasks: ApprovalTaskInfo[]
+}
+
 export const getProcessInstanceMyPage = async (params: any) => {
   return await request.get({ url: '/bpm/process-instance/my-page', params })
 }
@@ -57,3 +86,14 @@ export const getProcessInstance = async (id: string) => {
 export const getProcessInstanceCopyPage = async (params: any) => {
   return await request.get({ url: '/bpm/process-instance/copy/page', params })
 }
+
+// 获取审批详情
+export const getApprovalDetail = async (processInstanceId?:string, processDefinitionId?:string) => {
+  const param = processInstanceId ? '?processInstanceId='+ processInstanceId : '?processDefinitionId='+ processDefinitionId
+  return await request.get({ url: 'bpm/process-instance/get-approval-detail'+ param })
+}
+
+// 获取表单字段权限
+export const getFormFieldsPermission = async (params: any) => {
+  return await request.get({ url: '/bpm/process-instance/get-form-fields-permission', params })
+}

+ 15 - 0
src/api/bpm/simple/index.ts

@@ -0,0 +1,15 @@
+import request from '@/config/axios'
+
+
+export const updateBpmSimpleModel = async (data) => {
+  return await request.post({
+    url: '/bpm/model/simple/update',
+    data: data
+  })
+}
+
+export const getBpmSimpleModel = async (id) => {
+  return await request.get({
+    url: '/bpm/model/simple/get?id=' + id
+  })
+}

+ 46 - 0
src/api/bpm/task/index.ts

@@ -1,5 +1,51 @@
 import request from '@/config/axios'
 
+/**
+ * 任务状态枚举
+ */
+export enum TaskStatusEnum {
+  /**
+   * 未开始
+   */
+  NOT_START = -1,
+
+   /**
+   * 待审批
+   */
+   WAIT = 0,
+  /**
+   * 审批中
+   */
+  RUNNING = 1,
+  /**
+   * 审批通过
+   */
+  APPROVE = 2,
+
+  /**
+   * 审批不通过
+   */
+  REJECT = 3,
+  
+  /**
+   * 已取消
+   */
+  CANCEL = 4,
+  /**
+   * 已退回
+   */
+  RETURN = 5,
+  /**
+   * 委派中
+   */
+  DELEGATE = 6,
+  /**
+   * 审批通过中
+   */
+  APPROVING = 7,
+
+}
+
 export type TaskVO = {
   id: number
 }

+ 0 - 237
src/components/SimpleProcessDesigner/src/addNode.vue

@@ -1,237 +0,0 @@
-/* stylelint-disable order/properties-order */
-<template>
-  <div class="add-node-btn-box">
-    <div class="add-node-btn">
-      <el-popover placement="right-start" v-model="visible" width="auto">
-        <div class="add-node-popover-body">
-          <a class="add-node-popover-item approver" @click="addType(1)">
-            <div class="item-wrapper">
-              <span class="iconfont"></span>
-            </div>
-            <p>审批人</p>
-          </a>
-          <a class="add-node-popover-item notifier" @click="addType(2)">
-            <div class="item-wrapper">
-              <span class="iconfont"></span>
-            </div>
-            <p>抄送人</p>
-          </a>
-          <a class="add-node-popover-item condition" @click="addType(4)">
-            <div class="item-wrapper">
-              <span class="iconfont"></span>
-            </div>
-            <p>条件分支</p>
-          </a>
-        </div>
-        <template #reference>
-          <button class="btn" type="button">
-            <span class="iconfont"></span>
-          </button>
-        </template>
-      </el-popover>
-    </div>
-  </div>
-</template>
-<script setup>
-import { ref } from 'vue'
-let props = defineProps({
-  childNodeP: {
-    type: Object,
-    default: () => ({})
-  }
-})
-let emits = defineEmits(['update:childNodeP'])
-let visible = ref(false)
-const addType = (type) => {
-  visible.value = false
-  if (type != 4) {
-    var data
-    if (type == 1) {
-      data = {
-        nodeName: '审核人',
-        error: true,
-        type: 1,
-        settype: 1,
-        selectMode: 0,
-        selectRange: 0,
-        directorLevel: 1,
-        examineMode: 1,
-        noHanderAction: 1,
-        examineEndDirectorLevel: 0,
-        childNode: props.childNodeP,
-        nodeUserList: []
-      }
-    } else if (type == 2) {
-      data = {
-        nodeName: '抄送人',
-        type: 2,
-        ccSelfSelectFlag: 1,
-        childNode: props.childNodeP,
-        nodeUserList: []
-      }
-    }
-    emits('update:childNodeP', data)
-  } else {
-    emits('update:childNodeP', {
-      nodeName: '路由',
-      type: 4,
-      childNode: null,
-      conditionNodes: [
-        {
-          nodeName: '条件1',
-          error: true,
-          type: 3,
-          priorityLevel: 1,
-          conditionList: [],
-          nodeUserList: [],
-          childNode: props.childNodeP
-        },
-        {
-          nodeName: '条件2',
-          type: 3,
-          priorityLevel: 2,
-          conditionList: [],
-          nodeUserList: [],
-          childNode: null
-        }
-      ]
-    })
-  }
-}
-</script>
-<style scoped lang="scss">
-.add-node-btn-box {
-  width: 240px;
-  display: inline-flex;
-  -ms-flex-negative: 0;
-  flex-shrink: 0;
-  -webkit-box-flex: 1;
-  -ms-flex-positive: 1;
-  position: relative;
-
-  &:before {
-    content: '';
-    position: absolute;
-    top: 0;
-    left: 0;
-    right: 0;
-    bottom: 0;
-    z-index: -1;
-    margin: auto;
-    width: 2px;
-    height: 100%;
-    background-color: #cacaca;
-  }
-
-  .add-node-btn {
-    user-select: none;
-    width: 240px;
-    padding: 20px 0 32px;
-    display: flex;
-    -webkit-box-pack: center;
-    justify-content: center;
-    flex-shrink: 0;
-    -webkit-box-flex: 1;
-    flex-grow: 1;
-
-    .btn {
-      outline: none;
-      box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
-      width: 30px;
-      height: 30px;
-      background: #3296fa;
-      border-radius: 50%;
-      position: relative;
-      border: none;
-      line-height: 30px;
-      -webkit-transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
-      transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
-
-      .iconfont {
-        color: #fff;
-        font-size: 16px;
-      }
-
-      &:hover {
-        transform: scale(1.3);
-        box-shadow: 0 13px 27px 0 rgba(0, 0, 0, 0.1);
-      }
-
-      &:active {
-        transform: none;
-        background: #1e83e9;
-        box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
-      }
-    }
-  }
-}
-
-.add-node-popover-body {
-  display: flex;
-
-  .add-node-popover-item {
-    margin-right: 10px;
-    cursor: pointer;
-    text-align: center;
-    flex: 1;
-    color: #191f25 !important;
-
-    .item-wrapper {
-      user-select: none;
-      display: inline-block;
-      width: 80px;
-      height: 80px;
-      margin-bottom: 5px;
-      background: #fff;
-      border: 1px solid #e2e2e2;
-      border-radius: 50%;
-      transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
-
-      .iconfont {
-        font-size: 35px;
-        line-height: 80px;
-      }
-    }
-
-    &.approver {
-      .item-wrapper {
-        color: #ff943e;
-      }
-    }
-
-    &.notifier {
-      .item-wrapper {
-        color: #3296fa;
-      }
-    }
-
-    &.condition {
-      .item-wrapper {
-        color: #15bc83;
-      }
-    }
-
-    &:hover {
-      .item-wrapper {
-        background: #3296fa;
-        box-shadow: 0 10px 20px 0 rgba(50, 150, 250, 0.4);
-      }
-
-      .iconfont {
-        color: #fff;
-      }
-    }
-
-    &:active {
-      .item-wrapper {
-        box-shadow: none;
-        background: #eaeaea;
-      }
-
-      .iconfont {
-        color: inherit;
-      }
-    }
-  }
-}
-</style>

+ 0 - 297
src/components/SimpleProcessDesigner/src/nodeWrap.vue

@@ -1,297 +0,0 @@
-<!-- eslint-disable vue/no-mutating-props -->
-<!--
- * @Date: 2022-09-21 14:41:53
- * @LastEditors: StavinLi 495727881@qq.com
- * @LastEditTime: 2023-05-24 15:20:24
- * @FilePath: /Workflow-Vue3/src/components/nodeWrap.vue
--->
-<template>
-     <div class="node-wrap" v-if="nodeConfig.type < 3">
-      <div class="node-wrap-box" :class="(nodeConfig.type == 0 ? 'start-node ' : '') +(isTried && nodeConfig.error ? 'active error' : '')">
-          <div class="title" :style="`background: rgb(${bgColors[nodeConfig.type]});`">
-            <span v-if="nodeConfig.type == 0">{{ nodeConfig.nodeName }}</span>
-            <template v-else>
-              <span class="iconfont">{{nodeConfig.type == 1?'':''}}</span>
-              <input
-                v-if="isInput"
-                type="text"
-                class="ant-input editable-title-input"
-                @blur="blurEvent()"
-                @focus="$event.currentTarget.select()"
-                v-focus
-                v-model="nodeConfig.nodeName"
-                :placeholder="defaultText"
-              />
-              <span v-else class="editable-title" @click="clickEvent()">{{ nodeConfig.nodeName }}</span>
-              <i class="anticon anticon-close close" @click="delNode"></i>
-            </template>
-          </div>
-          <div class="content" @click="setPerson">
-            <div class="text">
-                <span class="placeholder" v-if="!showText">请选择{{defaultText}}</span>
-                {{showText}}
-            </div>
-            <i class="anticon anticon-right arrow"></i>
-          </div>
-          <div class="error_tip" v-if="isTried && nodeConfig.error">
-            <i class="anticon anticon-exclamation-circle"></i>
-          </div>
-      </div>
-      <addNode v-model:childNodeP="nodeConfig.childNode" />
-    </div>
-    <div class="branch-wrap" v-if="nodeConfig.type == 4">
-    <div class="branch-box-wrap">
-      <div class="branch-box">
-        <button class="add-branch" @click="addTerm">添加条件</button>
-        <div class="col-box" v-for="(item, index) in nodeConfig.conditionNodes" :key="index">
-          <div class="condition-node">
-            <div class="condition-node-box">
-              <div class="auto-judge" :class="isTried && item.error ? 'error active' : ''">
-                <div class="sort-left" v-if="index != 0" @click="arrTransfer(index, -1)">&lt;</div>
-                <div class="title-wrapper">
-                  <input
-                    v-if="isInputList[index]"
-                    type="text"
-                    class="ant-input editable-title-input"
-                    @blur="blurEvent(index)"
-                    @focus="$event.currentTarget.select()"
-                    v-model="item.nodeName"
-                  />
-                  <span v-else class="editable-title" @click="clickEvent(index)">{{ item.nodeName }}</span>
-                  <span class="priority-title" @click="setPerson(item.priorityLevel)">优先级{{ item.priorityLevel }}</span>
-                  <i class="anticon anticon-close close" @click="delTerm(index)"></i>
-                </div>
-                <div class="sort-right" v-if="index != nodeConfig.conditionNodes.length - 1" @click="arrTransfer(index)">&gt;</div>
-                <div class="content" @click="setPerson(item.priorityLevel)">{{ conditionStr(nodeConfig, index) }}</div>
-                <div class="error_tip" v-if="isTried && item.error">
-                    <i class="anticon anticon-exclamation-circle"></i>
-                </div>
-              </div>
-              <addNode v-model:childNodeP="item.childNode" />
-            </div>
-          </div>
-          <nodeWrap v-if="item.childNode" v-model:nodeConfig="item.childNode" />
-          <template v-if="index == 0">
-            <div class="top-left-cover-line"></div>
-            <div class="bottom-left-cover-line"></div>
-          </template>
-          <template v-if="index == nodeConfig.conditionNodes.length - 1">
-            <div class="top-right-cover-line"></div>
-            <div class="bottom-right-cover-line"></div>
-          </template>
-        </div>
-      </div>
-      <addNode v-model:childNodeP="nodeConfig.childNode" />
-    </div>
-  </div>
-    <nodeWrap v-if="nodeConfig.childNode" v-model:nodeConfig="nodeConfig.childNode" />
-</template>
-<script  setup>
-import addNode from './addNode.vue'
-import { onMounted, ref, watch, getCurrentInstance, computed } from 'vue'
-import {
-  arrToStr,
-  conditionStr,
-  setApproverStr,
-  copyerStr,
-  bgColors,
-  placeholderList
-} from './util'
-import { useWorkFlowStoreWithOut } from '@/store/modules/simpleWorkflow'
-let _uid = getCurrentInstance().uid
-
-let props = defineProps({
-  nodeConfig: {
-    type: Object,
-    default: () => ({})
-  },
-  flowPermission: {
-    type: Object,
-    // eslint-disable-next-line vue/require-valid-default-prop
-    default: () => []
-  }
-})
-
-let defaultText = computed(() => {
-  return placeholderList[props.nodeConfig.type]
-})
-let showText = computed(() => {
-  if (props.nodeConfig.type == 0) return arrToStr(props.flowPermission) || '所有人'
-  if (props.nodeConfig.type == 1) return setApproverStr(props.nodeConfig)
-  return copyerStr(props.nodeConfig)
-})
-
-let isInputList = ref([])
-let isInput = ref(false)
-const resetConditionNodesErr = () => {
-  for (var i = 0; i < props.nodeConfig.conditionNodes.length; i++) {
-    // eslint-disable-next-line vue/no-mutating-props
-    props.nodeConfig.conditionNodes[i].error =
-      conditionStr(props.nodeConfig, i) == '请设置条件' &&
-      i != props.nodeConfig.conditionNodes.length - 1
-  }
-}
-onMounted(() => {
-  if (props.nodeConfig.type == 1) {
-    // eslint-disable-next-line vue/no-mutating-props
-    props.nodeConfig.error = !setApproverStr(props.nodeConfig)
-  } else if (props.nodeConfig.type == 2) {
-    // eslint-disable-next-line vue/no-mutating-props
-    props.nodeConfig.error = !copyerStr(props.nodeConfig)
-  } else if (props.nodeConfig.type == 4) {
-    resetConditionNodesErr()
-  }
-})
-let emits = defineEmits(['update:flowPermission', 'update:nodeConfig'])
-let store = useWorkFlowStoreWithOut()
-let {
-  setPromoter,
-  setApprover,
-  setCopyer,
-  setCondition,
-  setFlowPermission,
-  setApproverConfig,
-  setCopyerConfig,
-  setConditionsConfig
-} = store
-let isTried = computed(() => store.isTried)
-let flowPermission1 = computed(() => store.flowPermission1)
-let approverConfig1 = computed(() => store.approverConfig1)
-let copyerConfig1 = computed(() => store.copyerConfig1)
-let conditionsConfig1 = computed(() => store.conditionsConfig1)
-watch(flowPermission1, (flow) => {
-  if (flow.flag && flow.id === _uid) {
-    emits('update:flowPermission', flow.value)
-  }
-})
-watch(approverConfig1, (approver) => {
-  if (approver.flag && approver.id === _uid) {
-    emits('update:nodeConfig', approver.value)
-  }
-})
-watch(copyerConfig1, (copyer) => {
-  if (copyer.flag && copyer.id === _uid) {
-    emits('update:nodeConfig', copyer.value)
-  }
-})
-watch(conditionsConfig1, (condition) => {
-  if (condition.flag && condition.id === _uid) {
-    emits('update:nodeConfig', condition.value)
-  }
-})
-
-const clickEvent = (index) => {
-  if (index || index === 0) {
-    isInputList.value[index] = true
-  } else {
-    isInput.value = true
-  }
-}
-const blurEvent = (index) => {
-  if (index || index === 0) {
-    isInputList.value[index] = false
-    // eslint-disable-next-line vue/no-mutating-props
-    props.nodeConfig.conditionNodes[index].nodeName =
-      props.nodeConfig.conditionNodes[index].nodeName || '条件'
-  } else {
-    isInput.value = false
-    // eslint-disable-next-line vue/no-mutating-props
-    props.nodeConfig.nodeName = props.nodeConfig.nodeName || defaultText
-  }
-}
-const delNode = () => {
-  emits('update:nodeConfig', props.nodeConfig.childNode)
-}
-const addTerm = () => {
-  let len = props.nodeConfig.conditionNodes.length + 1
-  // eslint-disable-next-line vue/no-mutating-props
-  props.nodeConfig.conditionNodes.push({
-    nodeName: '条件' + len,
-    type: 3,
-    priorityLevel: len,
-    conditionList: [],
-    nodeUserList: [],
-    childNode: null
-  })
-  resetConditionNodesErr()
-  emits('update:nodeConfig', props.nodeConfig)
-}
-const delTerm = (index) => {
-  // eslint-disable-next-line vue/no-mutating-props
-  props.nodeConfig.conditionNodes.splice(index, 1)
-  props.nodeConfig.conditionNodes.map((item, index) => {
-    item.priorityLevel = index + 1
-    item.nodeName = `条件${index + 1}`
-  })
-  resetConditionNodesErr()
-  emits('update:nodeConfig', props.nodeConfig)
-  if (props.nodeConfig.conditionNodes.length == 1) {
-    if (props.nodeConfig.childNode) {
-      if (props.nodeConfig.conditionNodes[0].childNode) {
-        reData(props.nodeConfig.conditionNodes[0].childNode, props.nodeConfig.childNode)
-      } else {
-        // eslint-disable-next-line vue/no-mutating-props
-        props.nodeConfig.conditionNodes[0].childNode = props.nodeConfig.childNode
-      }
-    }
-    emits('update:nodeConfig', props.nodeConfig.conditionNodes[0].childNode)
-  }
-}
-const reData = (data, addData) => {
-  if (!data.childNode) {
-    data.childNode = addData
-  } else {
-    reData(data.childNode, addData)
-  }
-}
-const setPerson = (priorityLevel) => {
-  var { type } = props.nodeConfig
-  if (type == 0) {
-    setPromoter(true)
-    setFlowPermission({
-      value: props.flowPermission,
-      flag: false,
-      id: _uid
-    })
-  } else if (type == 1) {
-    setApprover(true)
-    setApproverConfig({
-      value: {
-        ...JSON.parse(JSON.stringify(props.nodeConfig)),
-        ...{ settype: props.nodeConfig.settype ? props.nodeConfig.settype : 1 }
-      },
-      flag: false,
-      id: _uid
-    })
-  } else if (type == 2) {
-    setCopyer(true)
-    setCopyerConfig({
-      value: JSON.parse(JSON.stringify(props.nodeConfig)),
-      flag: false,
-      id: _uid
-    })
-  } else {
-    setCondition(true)
-    setConditionsConfig({
-      value: JSON.parse(JSON.stringify(props.nodeConfig)),
-      priorityLevel,
-      flag: false,
-      id: _uid
-    })
-  }
-}
-const arrTransfer = (index, type = 1) => {
-  //向左-1,向右1
-  // eslint-disable-next-line vue/no-mutating-props
-  props.nodeConfig.conditionNodes[index] = props.nodeConfig.conditionNodes.splice(
-    index + type,
-    1,
-    props.nodeConfig.conditionNodes[index]
-  )[0]
-  props.nodeConfig.conditionNodes.map((item, index) => {
-    item.priorityLevel = index + 1
-  })
-  resetConditionNodesErr()
-  emits('update:nodeConfig', props.nodeConfig)
-}
-</script>

+ 0 - 165
src/components/SimpleProcessDesigner/src/util.ts

@@ -1,165 +0,0 @@
-/**
- * todo
- */
-export const arrToStr = (arr?: [{ name: string }]) => {
-  if (arr) {
-    return arr
-      .map((item) => {
-        return item.name
-      })
-      .toString()
-  }
-}
-
-export const setApproverStr = (nodeConfig: any) => {
-  if (nodeConfig.settype == 1) {
-    if (nodeConfig.nodeUserList.length == 1) {
-      return nodeConfig.nodeUserList[0].name
-    } else if (nodeConfig.nodeUserList.length > 1) {
-      if (nodeConfig.examineMode == 1) {
-        return arrToStr(nodeConfig.nodeUserList)
-      } else if (nodeConfig.examineMode == 2) {
-        return nodeConfig.nodeUserList.length + '人会签'
-      }
-    }
-  } else if (nodeConfig.settype == 2) {
-    const level =
-      nodeConfig.directorLevel == 1 ? '直接主管' : '第' + nodeConfig.directorLevel + '级主管'
-    if (nodeConfig.examineMode == 1) {
-      return level
-    } else if (nodeConfig.examineMode == 2) {
-      return level + '会签'
-    }
-  } else if (nodeConfig.settype == 4) {
-    if (nodeConfig.selectRange == 1) {
-      return '发起人自选'
-    } else {
-      if (nodeConfig.nodeUserList.length > 0) {
-        if (nodeConfig.selectRange == 2) {
-          return '发起人自选'
-        } else {
-          return '发起人从' + nodeConfig.nodeUserList[0].name + '中自选'
-        }
-      } else {
-        return ''
-      }
-    }
-  } else if (nodeConfig.settype == 5) {
-    return '发起人自己'
-  } else if (nodeConfig.settype == 7) {
-    return '从直接主管到通讯录中级别最高的第' + nodeConfig.examineEndDirectorLevel + '个层级主管'
-  }
-}
-
-export const copyerStr = (nodeConfig: any) => {
-  if (nodeConfig.nodeUserList.length != 0) {
-    return arrToStr(nodeConfig.nodeUserList)
-  } else {
-    if (nodeConfig.ccSelfSelectFlag == 1) {
-      return '发起人自选'
-    }
-  }
-}
-export const conditionStr = (nodeConfig, index) => {
-  const { conditionList, nodeUserList } = nodeConfig.conditionNodes[index]
-  if (conditionList.length == 0) {
-    return index == nodeConfig.conditionNodes.length - 1 &&
-      nodeConfig.conditionNodes[0].conditionList.length != 0
-      ? '其他条件进入此流程'
-      : '请设置条件'
-  } else {
-    let str = ''
-    for (let i = 0; i < conditionList.length; i++) {
-      const {
-        columnId,
-        columnType,
-        showType,
-        showName,
-        optType,
-        zdy1,
-        opt1,
-        zdy2,
-        opt2,
-        fixedDownBoxValue
-      } = conditionList[i]
-      if (columnId == 0) {
-        if (nodeUserList.length != 0) {
-          str += '发起人属于:'
-          str +=
-            nodeUserList
-              .map((item) => {
-                return item.name
-              })
-              .join('或') + ' 并且 '
-        }
-      }
-      if (columnType == 'String' && showType == '3') {
-        if (zdy1) {
-          str += showName + '属于:' + dealStr(zdy1, JSON.parse(fixedDownBoxValue)) + ' 并且 '
-        }
-      }
-      if (columnType == 'Double') {
-        if (optType != 6 && zdy1) {
-          const optTypeStr = ['', '<', '>', '≤', '=', '≥'][optType]
-          str += `${showName} ${optTypeStr} ${zdy1} 并且 `
-        } else if (optType == 6 && zdy1 && zdy2) {
-          str += `${zdy1} ${opt1} ${showName} ${opt2} ${zdy2} 并且 `
-        }
-      }
-    }
-    return str ? str.substring(0, str.length - 4) : '请设置条件'
-  }
-}
-
-export const dealStr = (str: string, obj) => {
-  const arr = []
-  const list = str.split(',')
-  for (const elem in obj) {
-    list.map((item) => {
-      if (item == elem) {
-        arr.push(obj[elem].value)
-      }
-    })
-  }
-  return arr.join('或')
-}
-
-export const removeEle = (arr, elem, key = 'id') => {
-  let includesIndex
-  arr.map((item, index) => {
-    if (item[key] == elem[key]) {
-      includesIndex = index
-    }
-  })
-  arr.splice(includesIndex, 1)
-}
-
-export const bgColors = ['87, 106, 149', '255, 148, 62', '50, 150, 250']
-export const placeholderList = ['发起人', '审核人', '抄送人']
-export const setTypes = [
-  { value: 1, label: '指定成员' },
-  { value: 2, label: '主管' },
-  { value: 4, label: '发起人自选' },
-  { value: 5, label: '发起人自己' },
-  { value: 7, label: '连续多级主管' }
-]
-
-export const selectModes = [
-  { value: 1, label: '选一个人' },
-  { value: 2, label: '选多个人' }
-]
-
-export const selectRanges = [
-  { value: 1, label: '全公司' },
-  { value: 2, label: '指定成员' },
-  { value: 3, label: '指定角色' }
-]
-
-export const optTypes = [
-  { value: '1', label: '小于' },
-  { value: '2', label: '大于' },
-  { value: '3', label: '小于等于' },
-  { value: '4', label: '等于' },
-  { value: '5', label: '大于等于' },
-  { value: '6', label: '介于两个数之间' }
-]

+ 0 - 1292
src/components/SimpleProcessDesigner/theme/workflow.css

@@ -1,1292 +0,0 @@
-
-.clearfix {
-    zoom: 1
-}
-
-.clearfix:after,
-.clearfix:before {
-    content: "";
-    display: table
-}
-
-.clearfix:after {
-    clear: both
-}
-
-@font-face {
-    font-family: anticon;
-    font-display: fallback;
-    src: url("https://at.alicdn.com/t/font_148784_v4ggb6wrjmkotj4i.eot");
-    src: url("https://at.alicdn.com/t/font_148784_v4ggb6wrjmkotj4i.woff") format("woff"), url("https://at.alicdn.com/t/font_148784_v4ggb6wrjmkotj4i.ttf") format("truetype"), url("https://at.alicdn.com/t/font_148784_v4ggb6wrjmkotj4i.svg#iconfont") format("svg")
-}
-
-.anticon {
-    display: inline-block;
-    font-style: normal;
-    vertical-align: baseline;
-    text-align: center;
-    text-transform: none;
-    line-height: 1;
-    text-rendering: optimizeLegibility;
-    -webkit-font-smoothing: antialiased;
-    -moz-osx-font-smoothing: grayscale
-}
-
-.anticon:before {
-    display: block;
-    font-family: anticon!important
-}
-.anticon-close:before {
-  content: "\E633"
-}
-.anticon-right:before {
-    content: "\E61F"
-}
-.anticon-exclamation-circle{
-    color: rgb(242, 86, 67)
-}
-.anticon-exclamation-circle:before {
-    content: "\E62C"
-}
-
-.anticon-left:before {
-    content: "\E620"
-}
-
-.anticon-close-circle:before {
-    content: "\E62E"
-}
-  
-.ant-btn {
-    line-height: 1.5;
-    display: inline-block;
-    font-weight: 400;
-    text-align: center;
-    touch-action: manipulation;
-    cursor: pointer;
-    background-image: none;
-    border: 1px solid transparent;
-    white-space: nowrap;
-    padding: 0 15px;
-    font-size: 14px;
-    border-radius: 4px;
-    height: 32px;
-    user-select: none;
-    transition: all .3s cubic-bezier(.645, .045, .355, 1);
-    position: relative;
-    color: rgba(0, 0, 0, .65);
-    background-color: #fff;
-    border-color: #d9d9d9
-}
-
-.ant-btn>.anticon {
-    line-height: 1
-}
-
-.ant-btn,
-.ant-btn:active,
-.ant-btn:focus {
-    outline: 0
-}
-
-.ant-btn>a:only-child {
-    color: currentColor
-}
-
-.ant-btn>a:only-child:after {
-    content: "";
-    position: absolute;
-    top: 0;
-    left: 0;
-    bottom: 0;
-    right: 0;
-    background: transparent
-}
-
-.ant-btn:focus,
-.ant-btn:hover {
-    color: #40a9ff;
-    background-color: #fff;
-    border-color: #40a9ff
-}
-
-.ant-btn:focus>a:only-child,
-.ant-btn:hover>a:only-child {
-    color: currentColor
-}
-
-.ant-btn:focus>a:only-child:after,
-.ant-btn:hover>a:only-child:after {
-    content: "";
-    position: absolute;
-    top: 0;
-    left: 0;
-    bottom: 0;
-    right: 0;
-    background: transparent
-}
-
-.ant-btn.active,
-.ant-btn:active {
-    color: #096dd9;
-    background-color: #fff;
-    border-color: #096dd9
-}
-
-.ant-btn.active>a:only-child,
-.ant-btn:active>a:only-child {
-    color: currentColor
-}
-
-.ant-btn.active>a:only-child:after,
-.ant-btn:active>a:only-child:after {
-    content: "";
-    position: absolute;
-    top: 0;
-    left: 0;
-    bottom: 0;
-    right: 0;
-    background: transparent
-}
-
-.ant-btn.active,
-.ant-btn:active,
-.ant-btn:focus,
-.ant-btn:hover {
-    background: #fff;
-    text-decoration: none
-}
-
-.ant-btn>i,
-.ant-btn>span {
-    pointer-events: none
-}
-
-.ant-btn:before {
-    position: absolute;
-    top: -1px;
-    left: -1px;
-    bottom: -1px;
-    right: -1px;
-    background: #fff;
-    opacity: .35;
-    content: "";
-    border-radius: inherit;
-    z-index: 1;
-    transition: opacity .2s;
-    pointer-events: none;
-    display: none
-}
-
-.ant-btn .anticon {
-    transition: margin-left .3s cubic-bezier(.645, .045, .355, 1)
-}
-
-.ant-btn:active>span,
-.ant-btn:focus>span {
-    position: relative
-}
-
-.ant-btn>.anticon+span,
-.ant-btn>span+.anticon {
-    margin-left: 8px
-}
-
-.ant-input {
-    font-family: Chinese Quote, -apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Helvetica Neue, Helvetica, Arial, sans-serif;
-    font-variant: tabular-nums;
-    box-sizing: border-box;
-    margin: 0;
-    padding: 0;
-    list-style: none;
-    position: relative;
-    display: inline-block;
-    padding: 4px 11px;
-    width: 100%;
-    height: 32px;
-    font-size: 14px;
-    line-height: 1.5;
-    color: rgba(0, 0, 0, .65);
-    background-color: #fff;
-    background-image: none;
-    border: 1px solid #d9d9d9;
-    border-radius: 4px;
-    transition: all .3s
-}
-
-.ant-input::-moz-placeholder {
-    color: #bfbfbf;
-    opacity: 1
-}
-
-.ant-input:-ms-input-placeholder {
-    color: #bfbfbf
-}
-
-.ant-input::-webkit-input-placeholder {
-    color: #bfbfbf
-}
-
-.ant-input:focus,
-.ant-input:hover {
-    border-color: #40a9ff;
-    border-right-width: 1px!important
-}
-
-.ant-input:focus {
-    outline: 0;
-    box-shadow: 0 0 0 2px rgba(24, 144, 255, .2)
-}
-
-textarea.ant-input {
-    max-width: 100%;
-    height: auto;
-    vertical-align: bottom;
-    transition: all .3s, height 0s;
-    min-height: 32px
-}
-
-a,
-abbr,
-acronym,
-address,
-applet,
-article,
-aside,
-audio,
-b,
-big,
-blockquote,
-body,
-canvas,
-caption,
-center,
-cite,
-code,
-dd,
-del,
-details,
-dfn,
-div,
-dl,
-dt,
-em,
-fieldset,
-figcaption,
-figure,
-footer,
-form,
-h1,
-h2,
-h3,
-h4,
-h5,
-h6,
-header,
-hgroup,
-html,
-i,
-iframe,
-img,
-ins,
-kbd,
-label,
-legend,
-li,
-mark,
-menu,
-nav,
-object,
-ol,
-p,
-pre,
-q,
-s,
-samp,
-section,
-small,
-span,
-strike,
-strong,
-sub,
-summary,
-sup,
-table,
-tbody,
-td,
-tfoot,
-th,
-thead,
-time,
-tr,
-tt,
-u,
-ul,
-var,
-video {
-    margin: 0;
-    padding: 0;
-    border: 0;
-    outline: 0;
-    font-size: 100%;
-    font: inherit;
-    vertical-align: baseline
-}
-
-*,
-:after,
-:before {
-    -webkit-box-sizing: border-box;
-    -moz-box-sizing: border-box;
-    box-sizing: border-box
-}
-
-html {
-    font-family: sans-serif;
-    -ms-text-size-adjust: 100%;
-    -webkit-text-size-adjust: 100%
-}
-
-body,
-html {
-    font-size: 14px
-}
-
-body {
-    font-family: Microsoft Yahei, Lucida Grande, Lucida Sans Unicode, Helvetica, Arial, Verdana, sans-serif;
-    line-height: 1.6;
-    background-color: #fff;
-    position: static!important;
-    -webkit-tap-highlight-color: rgba(0, 0, 0, 0)
-}
-
-ol,
-ul {
-    list-style-type: none
-}
-
-b,
-strong {
-    font-weight: 700
-}
-
-img {
-    border: 0
-}
-
-button,
-input,
-select,
-textarea {
-    font-family: inherit;
-    font-size: 100%;
-    margin: 0
-}
-
-textarea {
-    overflow: auto;
-    vertical-align: top;
-    -webkit-appearance: none
-}
-
-button,
-input {
-    line-height: normal
-}
-
-button,
-select {
-    text-transform: none
-}
-
-button,
-html input[type=button],
-input[type=reset],
-input[type=submit] {
-    -webkit-appearance: button;
-    cursor: pointer
-}
-
-input[type=search] {
-    -webkit-appearance: textfield;
-    -moz-box-sizing: content-box;
-    -webkit-box-sizing: content-box;
-    box-sizing: content-box
-}
-
-input[type=search]::-webkit-search-cancel-button,
-input[type=search]::-webkit-search-decoration {
-    -webkit-appearance: none
-}
-
-button::-moz-focus-inner,
-input::-moz-focus-inner {
-    border: 0;
-    padding: 0
-}
-
-table {
-    width: 100%;
-    border-spacing: 0;
-    border-collapse: collapse
-}
-
-table,
-td,
-th {
-    border: 0
-}
-
-td,
-th {
-    padding: 0;
-    vertical-align: top
-}
-
-th {
-    font-weight: 700;
-    text-align: left
-}
-
-thead th {
-    white-space: nowrap
-}
-
-a {
-    text-decoration: none;
-    cursor: pointer;
-    color: #3296fa
-}
-
-a:active,
-a:hover {
-    outline: 0;
-    color: #3296fa
-}
-
-small {
-    font-size: 80%
-}
-
-body,
-html {
-    font-size: 12px!important;
-    color: #191f25!important;
-    background: #f6f6f6!important
-}
-
-.wrap {
-    display: -webkit-box;
-    display: -ms-flexbox;
-    display: flex;
-    -webkit-box-orient: vertical;
-    -webkit-box-direction: normal;
-    -ms-flex-direction: column;
-    flex-direction: column;
-    height: 100%
-}
-
-@font-face {
-    font-family: IconFont;
-    src: url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.eot");
-    src: url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.eot?#iefix") format("embedded-opentype"), url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.woff") format("woff"), url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.ttf") format("truetype"), url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.svg#IconFont") format("svg")
-}
-
-.iconfont {
-    font-family: IconFont!important;
-    font-size: 16px;
-    font-style: normal;
-    -webkit-font-smoothing: antialiased;
-    -webkit-text-stroke-width: .2px;
-    -moz-osx-font-smoothing: grayscale
-}
-
-.fd-nav {
-    position: fixed;
-    top: 0;
-    left: 0;
-    right: 0;
-    z-index: 997;
-    width: 100%;
-    height: 60px;
-    font-size: 14px;
-    color: #fff;
-    background: #3296fa;
-    display: flex;
-    align-items: center
-}
-
-.fd-nav>* {
-    flex: 1;
-    width: 100%
-}
-
-.fd-nav .fd-nav-left {
-    display: -webkit-box;
-    display: flex;
-    align-items: center
-}
-
-.fd-nav .fd-nav-center {
-    flex: none;
-    width: 600px;
-    text-align: center
-}
-
-.fd-nav .fd-nav-right {
-    display: flex;
-    align-items: center;
-    justify-content: flex-end;
-    text-align: right
-}
-
-.fd-nav .fd-nav-back {
-    display: inline-block;
-    width: 60px;
-    height: 60px;
-    font-size: 22px;
-    border-right: 1px solid #1583f2;
-    text-align: center;
-    cursor: pointer
-}
-
-.fd-nav .fd-nav-back:hover {
-    background: #5af
-}
-
-.fd-nav .fd-nav-back:active {
-    background: #1583f2
-}
-
-.fd-nav .fd-nav-back .anticon {
-    line-height: 60px
-}
-
-.fd-nav .fd-nav-title {
-    width: 0;
-    flex: 1;
-    overflow: hidden;
-    white-space: nowrap;
-    text-overflow: ellipsis;
-    padding: 0 15px
-}
-
-.fd-nav a {
-    color: #fff;
-    margin-left: 12px
-}
-
-.fd-nav .button-publish {
-    min-width: 80px;
-    margin-left: 4px;
-    margin-right: 15px;
-    color: #3296fa;
-    border-color: #fff
-}
-
-.fd-nav .button-publish.ant-btn:focus,
-.fd-nav .button-publish.ant-btn:hover {
-    color: #3296fa;
-    border-color: #fff;
-    box-shadow: 0 10px 20px 0 rgba(0, 0, 0, .3)
-}
-
-.fd-nav .button-publish.ant-btn:active {
-    color: #3296fa;
-    background: #d6eaff;
-    box-shadow: none
-}
-
-.fd-nav .button-preview {
-    min-width: 80px;
-    margin-left: 16px;
-    margin-right: 4px;
-    color: #fff;
-    border-color: #fff;
-    background: transparent
-}
-
-.fd-nav .button-preview.ant-btn:focus,
-.fd-nav .button-preview.ant-btn:hover {
-    color: #fff;
-    border-color: #fff;
-    background: #59acfc
-}
-
-.fd-nav .button-preview.ant-btn:active {
-    color: #fff;
-    border-color: #fff;
-    background: #2186ef
-}
-
-.fd-nav-content {
-    position: fixed;
-    top: 60px;
-    left: 0;
-    right: 0;
-    bottom: 0;
-    z-index: 1;
-    overflow-x: hidden;
-    overflow-y: auto;
-    padding-bottom: 30px
-}
-
-.error-modal-desc {
-    font-size: 13px;
-    color: rgba(25, 31, 37, .56);
-    line-height: 22px;
-    margin-bottom: 14px
-}
-
-.error-modal-list {
-    height: 200px;
-    overflow-y: auto;
-    margin-right: -25px;
-    padding-right: 25px
-}
-
-.error-modal-item {
-    padding: 10px 20px;
-    line-height: 21px;
-    background: #f6f6f6;
-    display: flex;
-    justify-content: space-between;
-    align-items: center;
-    margin-bottom: 8px;
-    border-radius: 4px
-}
-
-.error-modal-item-label {
-    flex: none;
-    font-size: 15px;
-    color: rgba(25, 31, 37, .56);
-    padding-right: 10px
-}
-
-.error-modal-item-content {
-    text-align: right;
-    flex: 1;
-    font-size: 13px;
-    color: #191f25
-}
-
-#body.blur {
-    -webkit-filter: blur(3px);
-    filter: blur(3px)
-}
-
-.zoom {
-    display: flex;
-    position: fixed;
-    -webkit-box-align: center;
-    -ms-flex-align: center;
-    align-items: center;
-    -webkit-box-pack: justify;
-    -ms-flex-pack: justify;
-    justify-content: space-between;
-    height: 40px;
-    width: 125px;
-    right: 40px;
-    margin-top: 30px;
-    z-index: 10
-}
-
-.zoom .zoom-in,
-.zoom .zoom-out {
-    width: 30px;
-    height: 30px;
-    background: #fff;
-    color: #c1c1cd;
-    cursor: pointer;
-    background-size: 100%;
-    background-repeat: no-repeat
-}
-
-.zoom .zoom-out {
-    background-image: url(https://gw.alicdn.com/tfs/TB1s0qhBHGYBuNjy0FoXXciBFXa-90-90.png)
-}
-
-.zoom .zoom-out.disabled {
-    opacity: .5
-}
-
-.zoom .zoom-in {
-    background-image: url(https://gw.alicdn.com/tfs/TB1UIgJBTtYBeNjy1XdXXXXyVXa-90-90.png)
-}
-
-.zoom .zoom-in.disabled {
-    opacity: .5
-}
-
-.auto-judge:hover .editable-title,
-.node-wrap-box:hover .editable-title {
-    border-bottom: 1px dashed #fff
-}
-
-.auto-judge:hover .editable-title.editing,
-.node-wrap-box:hover .editable-title.editing {
-    text-decoration: none;
-    border: 1px solid #d9d9d9
-}
-
-.auto-judge:hover .editable-title {
-    border-color: #15bc83
-}
-
-.editable-title {
-    line-height: 15px;
-    overflow: hidden;
-    white-space: nowrap;
-    text-overflow: ellipsis;
-    border-bottom: 1px dashed transparent
-}
-
-.editable-title:before {
-    content: "";
-    position: absolute;
-    top: 0;
-    left: 0;
-    bottom: 0;
-    right: 40px
-}
-
-.editable-title:hover {
-    border-bottom: 1px dashed #fff
-}
-
-.editable-title-input {
-    flex: none;
-    height: 18px;
-    padding-left: 4px;
-    text-indent: 0;
-    font-size: 12px;
-    line-height: 18px;
-    z-index: 1
-}
-
-.editable-title-input:hover {
-    text-decoration: none
-}
-
-.ant-btn {
-    position: relative
-}
-
-.node-wrap-box {
-    display: -webkit-inline-box;
-    display: -ms-inline-flexbox;
-    display: inline-flex;
-    -webkit-box-orient: vertical;
-    -webkit-box-direction: normal;
-    -ms-flex-direction: column;
-    flex-direction: column;
-    position: relative;
-    width: 220px;
-    min-height: 72px;
-    -ms-flex-negative: 0;
-    flex-shrink: 0;
-    background: #fff;
-    border-radius: 4px;
-    cursor: pointer
-}
-
-.node-wrap-box:after {
-    pointer-events: none;
-    content: "";
-    position: absolute;
-    top: 0;
-    bottom: 0;
-    left: 0;
-    right: 0;
-    z-index: 2;
-    border-radius: 4px;
-    border: 1px solid transparent;
-    transition: all .1s cubic-bezier(.645, .045, .355, 1);
-    box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .1)
-}
-
-.node-wrap-box.active:after,
-.node-wrap-box:active:after,
-.node-wrap-box:hover:after {
-    border: 1px solid #3296fa;
-    box-shadow: 0 0 6px 0 rgba(50, 150, 250, .3)
-}
-
-.node-wrap-box.active .close,
-.node-wrap-box:active .close,
-.node-wrap-box:hover .close {
-    display: block
-}
-
-.node-wrap-box.error:after {
-    border: 1px solid #f25643;
-    box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .1)
-}
-
-.node-wrap-box .title {
-    position: relative;
-    display: flex;
-    align-items: center;
-    padding-left: 16px;
-    padding-right: 30px;
-    width: 100%;
-    height: 24px;
-    line-height: 24px;
-    font-size: 12px;
-    color: #fff;
-    text-align: left;
-    background: #576a95;
-    border-radius: 4px 4px 0 0
-}
-
-.node-wrap-box .title .iconfont {
-    font-size: 12px;
-    margin-right: 5px
-}
-
-.node-wrap-box .placeholder {
-    color: #bfbfbf
-}
-
-.node-wrap-box .close {
-    display: none;
-    position: absolute;
-    right: 10px;
-    top: 50%;
-    transform: translateY(-50%);
-    width: 20px;
-    height: 20px;
-    font-size: 14px;
-    color: #fff;
-    border-radius: 50%;
-    text-align: center;
-    line-height: 20px
-}
-
-.node-wrap-box .content {
-    position: relative;
-    font-size: 14px;
-    padding: 16px;
-    padding-right: 30px
-}
-
-.node-wrap-box .content .text {
-    overflow: hidden;
-    text-overflow: ellipsis;
-    display: -webkit-box;
-    -webkit-line-clamp: 3;
-    -webkit-box-orient: vertical
-}
-
-.node-wrap-box .content .arrow {
-    position: absolute;
-    right: 10px;
-    top: 50%;
-    transform: translateY(-50%);
-    width: 20px;
-    height: 14px;
-    font-size: 14px;
-    color: #979797
-}
-
-.start-node.node-wrap-box .content .text {
-    display: block;
-    white-space: nowrap
-}
-
-.node-wrap-box:before {
-    content: "";
-    position: absolute;
-    top: -12px;
-    left: 50%;
-    -webkit-transform: translateX(-50%);
-    transform: translateX(-50%);
-    width: 0;
-    height: 4px;
-    border-style: solid;
-    border-width: 8px 6px 4px;
-    border-color: #cacaca transparent transparent;
-    background: #f5f5f7
-}
-
-.node-wrap-box.start-node:before {
-    content: none
-}
-
-.top-left-cover-line {
-    left: -1px
-}
-
-.top-left-cover-line,
-.top-right-cover-line {
-    position: absolute;
-    height: 8px;
-    width: 50%;
-    background-color: #f5f5f7;
-    top: -4px
-}
-
-.top-right-cover-line {
-    right: -1px
-}
-
-.bottom-left-cover-line {
-    left: -1px
-}
-
-.bottom-left-cover-line,
-.bottom-right-cover-line {
-    position: absolute;
-    height: 8px;
-    width: 50%;
-    background-color: #f5f5f7;
-    bottom: -4px
-}
-
-.bottom-right-cover-line {
-    right: -1px
-}
-
-.dingflow-design {
-    width: 100%;
-    background-color: #f5f5f7;
-    overflow: auto;
-    position: absolute;
-    bottom: 0;
-    left: 0;
-    right: 0;
-    top: 0
-}
-
-.dingflow-design .box-scale {
-    transform: scale(1);
-    display: inline-block;
-    position: relative;
-    width: 100%;
-    padding: 54.5px 0;
-    -webkit-box-align: start;
-    -ms-flex-align: start;
-    align-items: flex-start;
-    -webkit-box-pack: center;
-    -ms-flex-pack: center;
-    justify-content: center;
-    -ms-flex-wrap: wrap;
-    flex-wrap: wrap;
-    min-width: -webkit-min-content;
-    min-width: -moz-min-content;
-    min-width: min-content;
-    background-color: #f5f5f7;
-    transform-origin: 50% 0px 0px;
-}
-
-.dingflow-design .node-wrap {
-    flex-direction: column;
-    -webkit-box-pack: start;
-    -ms-flex-pack: start;
-    justify-content: flex-start;
-    -webkit-box-align: center;
-    -ms-flex-align: center;
-    align-items: center;
-    -ms-flex-wrap: wrap;
-    flex-wrap: wrap;
-    -webkit-box-flex: 1;
-    -ms-flex-positive: 1;
-    padding: 0 50px;
-    position: relative
-}
-
-.dingflow-design .branch-wrap,
-.dingflow-design .node-wrap {
-    display: inline-flex;
-    width: 100%
-}
-
-.dingflow-design .branch-box-wrap {
-    display: flex;
-    -webkit-box-orient: vertical;
-    -webkit-box-direction: normal;
-    -ms-flex-direction: column;
-    flex-direction: column;
-    -ms-flex-wrap: wrap;
-    flex-wrap: wrap;
-    -webkit-box-align: center;
-    -ms-flex-align: center;
-    align-items: center;
-    min-height: 270px;
-    width: 100%;
-    -ms-flex-negative: 0;
-    flex-shrink: 0
-}
-
-.dingflow-design .branch-box {
-    display: flex;
-    overflow: visible;
-    min-height: 180px;
-    height: auto;
-    border-bottom: 2px solid #ccc;
-    border-top: 2px solid #ccc;
-    position: relative;
-    margin-top: 15px
-}
-
-.dingflow-design .branch-box .col-box {
-    background: #f5f5f7
-}
-
-.dingflow-design .branch-box .col-box:before {
-    content: "";
-    position: absolute;
-    top: 0;
-    left: 0;
-    right: 0;
-    bottom: 0;
-    z-index: 0;
-    margin: auto;
-    width: 2px;
-    height: 100%;
-    background-color: #cacaca
-}
-
-.dingflow-design .add-branch {
-    border: none;
-    outline: none;
-    user-select: none;
-    justify-content: center;
-    font-size: 12px;
-    padding: 0 10px;
-    height: 30px;
-    line-height: 30px;
-    border-radius: 15px;
-    color: #3296fa;
-    background: #fff;
-    box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .1);
-    position: absolute;
-    top: -16px;
-    left: 50%;
-    transform: translateX(-50%);
-    transform-origin: center center;
-    cursor: pointer;
-    z-index: 1;
-    display: inline-flex;
-    align-items: center;
-    -webkit-transition: all .3s cubic-bezier(.645, .045, .355, 1);
-    transition: all .3s cubic-bezier(.645, .045, .355, 1)
-}
-
-.dingflow-design .add-branch:hover {
-    transform: translateX(-50%) scale(1.1);
-    box-shadow: 0 8px 16px 0 rgba(0, 0, 0, .1)
-}
-
-.dingflow-design .add-branch:active {
-    transform: translateX(-50%);
-    box-shadow: none
-}
-
-.dingflow-design .col-box {
-    display: inline-flex;
-    -webkit-box-orient: vertical;
-    -webkit-box-direction: normal;
-    flex-direction: column;
-    -webkit-box-align: center;
-    align-items: center;
-    position: relative
-}
-
-.dingflow-design .condition-node {
-    min-height: 220px
-}
-
-.dingflow-design .condition-node,
-.dingflow-design .condition-node-box {
-    display: inline-flex;
-    -webkit-box-orient: vertical;
-    -webkit-box-direction: normal;
-    flex-direction: column;
-    -webkit-box-flex: 1
-}
-
-.dingflow-design .condition-node-box {
-    padding-top: 30px;
-    padding-right: 50px;
-    padding-left: 50px;
-    -webkit-box-pack: center;
-    justify-content: center;
-    -webkit-box-align: center;
-    align-items: center;
-    flex-grow: 1;
-    position: relative
-}
-
-.dingflow-design .condition-node-box:before {
-    content: "";
-    position: absolute;
-    top: 0;
-    left: 0;
-    right: 0;
-    bottom: 0;
-    margin: auto;
-    width: 2px;
-    height: 100%;
-    background-color: #cacaca
-}
-
-.dingflow-design .auto-judge {
-    position: relative;
-    width: 220px;
-    min-height: 72px;
-    background: #fff;
-    border-radius: 4px;
-    padding: 14px 19px;
-    cursor: pointer
-}
-
-.dingflow-design .auto-judge:after {
-    pointer-events: none;
-    content: "";
-    position: absolute;
-    top: 0;
-    bottom: 0;
-    left: 0;
-    right: 0;
-    z-index: 2;
-    border-radius: 4px;
-    border: 1px solid transparent;
-    transition: all .1s cubic-bezier(.645, .045, .355, 1);
-    box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .1)
-}
-
-.dingflow-design .auto-judge.active:after,
-.dingflow-design .auto-judge:active:after,
-.dingflow-design .auto-judge:hover:after {
-    border: 1px solid #3296fa;
-    box-shadow: 0 0 6px 0 rgba(50, 150, 250, .3)
-}
-
-.dingflow-design .auto-judge.active .close,
-.dingflow-design .auto-judge:active .close,
-.dingflow-design .auto-judge:hover .close {
-    display: block
-}
-
-.dingflow-design .auto-judge.error:after {
-    border: 1px solid #f25643;
-    box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .1)
-}
-
-.dingflow-design .auto-judge .title-wrapper {
-    position: relative;
-    font-size: 12px;
-    color: #15bc83;
-    text-align: left;
-    line-height: 16px
-}
-
-.dingflow-design .auto-judge .title-wrapper .editable-title {
-    display: inline-block;
-    max-width: 120px;
-    overflow: hidden;
-    white-space: nowrap;
-    text-overflow: ellipsis
-}
-
-.dingflow-design .auto-judge .title-wrapper .priority-title {
-    display: inline-block;
-    float: right;
-    margin-right: 10px;
-    color: rgba(25, 31, 37, .56)
-}
-
-.dingflow-design .auto-judge .placeholder {
-    color: #bfbfbf
-}
-
-.dingflow-design .auto-judge .close {
-    display: none;
-    position: absolute;
-    right: -10px;
-    top: -10px;
-    width: 20px;
-    height: 20px;
-    font-size: 14px;
-    color: rgba(0, 0, 0, .25);
-    border-radius: 50%;
-    text-align: center;
-    line-height: 20px;
-    z-index: 2
-}
-
-.dingflow-design .auto-judge .content {
-    font-size: 14px;
-    color: #191f25;
-    text-align: left;
-    margin-top: 6px;
-    overflow: hidden;
-    text-overflow: ellipsis;
-    display: -webkit-box;
-    -webkit-line-clamp: 3;
-    -webkit-box-orient: vertical
-}
-
-.dingflow-design .auto-judge .sort-left,
-.dingflow-design .auto-judge .sort-right {
-    position: absolute;
-    top: 0;
-    bottom: 0;
-    display: none;
-    z-index: 1
-}
-
-.dingflow-design .auto-judge .sort-left {
-    left: 0;
-    border-right: 1px solid #f6f6f6
-}
-
-.dingflow-design .auto-judge .sort-right {
-    right: 0;
-    border-left: 1px solid #f6f6f6
-}
-
-.dingflow-design .auto-judge:hover .sort-left,
-.dingflow-design .auto-judge:hover .sort-right {
-    display: flex;
-    align-items: center
-}
-
-.dingflow-design .auto-judge .sort-left:hover,
-.dingflow-design .auto-judge .sort-right:hover {
-    background: #efefef
-}
-
-.dingflow-design .end-node {
-    border-radius: 50%;
-    font-size: 14px;
-    color: rgba(25, 31, 37, .4);
-    text-align: left
-}
-
-.dingflow-design .end-node .end-node-circle {
-    width: 10px;
-    height: 10px;
-    margin: auto;
-    border-radius: 50%;
-    background: #dbdcdc
-}
-
-.dingflow-design .end-node .end-node-text {
-    margin-top: 5px;
-    text-align: center
-}
-
-.approval-setting {
-    border-radius: 2px;
-    margin: 20px 0;
-    position: relative;
-    background: #fff
-}
-
-.ant-btn {
-    position: relative
-}
-
-

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

@@ -0,0 +1,168 @@
+<template>
+  <div class="node-handler-wrapper">
+    <div class="node-handler" v-if="props.showAdd">
+      <el-popover
+        trigger="hover"
+        v-model:visible="popoverShow"
+        placement="right-start"
+        width="auto"
+      >
+        <div class="handler-item-wrapper">
+          <div class="handler-item" @click="addNode(NodeType.USER_TASK_NODE)">
+            <div class="approve handler-item-icon">
+              <span class="iconfont icon-approve icon-size"></span>
+            </div>
+            <div class="handler-item-text">审批人</div>
+          </div>
+          <div class="handler-item" @click="addNode(NodeType.COPY_TASK_NODE)">
+            <div class="handler-item-icon copy">
+              <span class="iconfont icon-size icon-copy"></span>
+            </div>
+            <div class="handler-item-text">抄送</div>
+          </div>
+          <div class="handler-item" @click="addNode(NodeType.CONDITION_BRANCH_NODE)">
+            <div class="handler-item-icon condition">
+              <span class="iconfont icon-size icon-exclusive"></span>
+            </div>
+            <div class="handler-item-text">条件分支</div>
+          </div>
+          <div class="handler-item" @click="addNode(NodeType.PARALLEL_BRANCH_NODE)">
+            <div class="handler-item-icon condition">
+              <span class="iconfont icon-size icon-parallel"></span>
+            </div>
+            <div class="handler-item-text">并行分支</div>
+          </div>
+        </div>
+        <template #reference>
+          <div class="add-icon"><Icon icon="ep:plus" /></div>
+        </template>
+      </el-popover>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import {
+  ApproveMethodType,
+  AssignEmptyHandlerType,
+  AssignStartUserHandlerType,
+  NODE_DEFAULT_NAME,
+  NodeType,
+  RejectHandlerType,
+  SimpleFlowNode
+} from './consts'
+import { generateUUID } from '@/utils'
+
+defineOptions({
+  name: 'NodeHandler'
+})
+const popoverShow = ref(false)
+
+const props = defineProps({
+  childNode: {
+    type: Object as () => SimpleFlowNode,
+    default: null
+  },
+  showAdd: {
+    // 是否显示添加节点
+    type: Boolean,
+    default: true
+  }
+})
+
+const emits = defineEmits(['update:childNode'])
+
+const addNode = (type: number) => {
+  popoverShow.value = false
+  if (type === NodeType.USER_TASK_NODE) {
+    const id = 'Activity_' + generateUUID()
+    const data: SimpleFlowNode = {
+      id: id,
+      name: NODE_DEFAULT_NAME.get(NodeType.USER_TASK_NODE) as string,
+      showText: '',
+      type: NodeType.USER_TASK_NODE,
+      approveMethod: ApproveMethodType.SEQUENTIAL_APPROVE,
+      // 超时处理
+      rejectHandler: {
+        type: RejectHandlerType.FINISH_PROCESS
+      },
+      timeoutHandler: {
+        enable: false
+      },
+      assignEmptyHandler: {
+        type: AssignEmptyHandlerType.APPROVE
+      },
+      assignStartUserHandlerType: AssignStartUserHandlerType.START_USER_AUDIT,
+      childNode: props.childNode
+    }
+    emits('update:childNode', data)
+  }
+  if (type === NodeType.COPY_TASK_NODE) {
+    const data: SimpleFlowNode = {
+      id: 'Activity_' + generateUUID(),
+      name: NODE_DEFAULT_NAME.get(NodeType.COPY_TASK_NODE) as string,
+      showText: '',
+      type: NodeType.COPY_TASK_NODE,
+      childNode: props.childNode
+    }
+    emits('update:childNode', data)
+  }
+  if (type === NodeType.CONDITION_BRANCH_NODE) {
+    const data: SimpleFlowNode = {
+      name: '条件分支',
+      type: NodeType.CONDITION_BRANCH_NODE,
+      id: 'GateWay_' + generateUUID(),
+      childNode: props.childNode,
+      conditionNodes: [
+        {
+          id: 'Flow_' + generateUUID(),
+          name: '条件1',
+          showText: '',
+          type: NodeType.CONDITION_NODE,
+          childNode: undefined,
+          conditionType: 1,
+          defaultFlow: false
+          
+        },
+        {
+          id: 'Flow_' + generateUUID(),
+          name: '其它情况',
+          showText: '其它情况进入此流程',
+          type: NodeType.CONDITION_NODE,
+          childNode: undefined,
+          conditionType: undefined,
+          defaultFlow: true
+        }
+      ]
+    }
+    emits('update:childNode', data)
+  }
+  if (type === NodeType.PARALLEL_BRANCH_NODE) {
+    const data: SimpleFlowNode = {
+      name: '并行分支',
+      type: NodeType.PARALLEL_BRANCH_NODE,
+      id: 'GateWay_' + generateUUID(),
+      childNode: props.childNode,
+      conditionNodes: [
+        {
+          id: 'Flow_' + generateUUID(),
+          name: '并行1',
+          showText: '无需配置条件同时执行',
+          type: NodeType.CONDITION_NODE,
+          childNode: undefined
+        },
+        {
+          id: 'Flow_' + generateUUID(),
+          name: '并行2',
+          showText: '无需配置条件同时执行',
+          type: NodeType.CONDITION_NODE,
+          childNode: undefined
+        }
+      ]
+    }
+    emits('update:childNode', data)
+  }
+}
+</script>
+
+<style lang="scss" scoped></style>

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

@@ -0,0 +1,107 @@
+<template>
+  <!-- 发起人节点 -->
+  <StartUserNode
+    v-if="currentNode && currentNode.type === NodeType.START_USER_NODE"
+    :flow-node="currentNode"
+  />
+  <!-- 审批节点 -->
+  <UserTaskNode
+    v-if="currentNode && currentNode.type === NodeType.USER_TASK_NODE"
+    :flow-node="currentNode"
+    @update:flow-node="handleModelValueUpdate"
+    @find:parent-node="findFromParentNode"
+  />
+  <!-- 抄送节点 -->
+  <CopyTaskNode
+    v-if="currentNode && currentNode.type === NodeType.COPY_TASK_NODE"
+    :flow-node="currentNode"
+    @update:flow-node="handleModelValueUpdate"
+  />
+  <!-- 条件节点 -->
+  <ExclusiveNode
+    v-if="currentNode && currentNode.type === NodeType.CONDITION_BRANCH_NODE"
+    :flow-node="currentNode"
+    @update:model-value="handleModelValueUpdate"
+    @find:parent-node="findFromParentNode"
+  />
+  <!-- 并行节点 -->
+  <ParallelNode
+    v-if="currentNode && currentNode.type === NodeType.PARALLEL_BRANCH_NODE"
+    :flow-node="currentNode"
+    @update:model-value="handleModelValueUpdate"
+    @find:parent-node="findFromParentNode"
+  />
+  <!-- 递归显示孩子节点  -->
+  <ProcessNodeTree
+    v-if="currentNode && currentNode.childNode"
+    v-model:flow-node="currentNode.childNode"
+    :parent-node="currentNode"
+    @find:recursive-find-parent-node="recursiveFindParentNode"
+  />
+
+  <!-- 结束节点 -->
+  <EndEventNode v-if="currentNode && currentNode.type === NodeType.END_EVENT_NODE" />
+</template>
+<script setup lang="ts">
+import StartUserNode from './nodes/StartUserNode.vue'
+import EndEventNode from './nodes/EndEventNode.vue'
+import UserTaskNode from './nodes/UserTaskNode.vue'
+import CopyTaskNode from './nodes/CopyTaskNode.vue'
+import ExclusiveNode from './nodes/ExclusiveNode.vue'
+import ParallelNode from './nodes/ParallelNode.vue'
+import { SimpleFlowNode, NodeType } from './consts'
+import { useWatchNode } from './node'
+defineOptions({
+  name: 'ProcessNodeTree'
+})
+const props = defineProps({
+  parentNode: {
+    type: Object as () => SimpleFlowNode,
+    default: () => null
+  },
+  flowNode: {
+    type: Object as () => SimpleFlowNode,
+    default: () => null
+  }
+})
+const emits = defineEmits<{
+  'update:flowNode': [node: SimpleFlowNode | undefined]
+  'find:recursiveFindParentNode': [
+    nodeList: SimpleFlowNode[],
+    curentNode: SimpleFlowNode,
+    nodeType: number
+  ]
+}>()
+
+const currentNode = useWatchNode(props)
+
+// 用于删除节点
+const handleModelValueUpdate = (updateValue) => {
+  emits('update:flowNode', updateValue)
+}
+
+const findFromParentNode = (nodeList: SimpleFlowNode[], nodeType: number) => {
+  emits('find:recursiveFindParentNode', nodeList, props.parentNode, nodeType)
+}
+
+// 递归从父节点中查询匹配的节点
+const recursiveFindParentNode = (
+  nodeList: SimpleFlowNode[],
+  findNode: SimpleFlowNode,
+  nodeType: number
+) => {
+  if (!findNode) {
+    return
+  }
+  if (findNode.type === NodeType.START_USER_NODE) {
+    nodeList.push(findNode)
+    return
+  }
+
+  if (findNode.type === nodeType) {
+    nodeList.push(findNode)
+  }
+  emits('find:recursiveFindParentNode', nodeList, props.parentNode, nodeType)
+}
+</script>
+<style lang="scss" scoped></style>

+ 215 - 0
src/components/SimpleProcessDesignerV2/src/SimpleProcessDesigner.vue

@@ -0,0 +1,215 @@
+<template>
+  <div class="simple-flow-canvas" v-loading="loading">
+    <div class="simple-flow-container" >
+      <div class="top-area-container">
+        <div class="top-actions">
+          <div class="canvas-control">
+            <span class="control-scale-group">
+              <span class="control-scale-button"> <Icon icon="ep:plus" @click="zoomOut()" /></span>
+              <span class="control-scale-label">{{ scaleValue }}%</span>
+              <span class="control-scale-button"><Icon icon="ep:minus" @click="zoomIn()" /></span>
+            </span>
+          </div>
+          <el-button type="primary" @click="saveSimpleFlowModel">保存</el-button>
+          <!-- <el-button type="primary">全局设置</el-button> -->
+        </div>
+      </div>
+      <div class="scale-container" :style="`transform: scale(${scaleValue / 100});`">
+        <ProcessNodeTree
+          v-if="processNodeTree"
+          v-model:flow-node="processNodeTree"
+        />
+      </div>
+    </div>
+    <Dialog v-model="errorDialogVisible" title="保存失败" width="400" :fullscreen="false">
+      <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>
+      </template>
+    </Dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import ProcessNodeTree from './ProcessNodeTree.vue'
+import { updateBpmSimpleModel, getBpmSimpleModel } from '@/api/bpm/simple'
+import { SimpleFlowNode, NodeType, NodeId, NODE_DEFAULT_TEXT } from './consts'
+import { getModel } from '@/api/bpm/model'
+import { getForm, FormVO } from '@/api/bpm/form'
+import { 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'
+import { fa } from 'element-plus/es/locale'
+defineOptions({
+  name: 'SimpleProcessDesigner'
+})
+const router = useRouter() // 路由
+const props = defineProps({
+  modelId: {
+    type: String,
+    required: true
+  }
+})
+const loading = ref(true)
+const formFields = ref<string[]>([])
+const formType = ref(20)
+const roleOptions = ref<RoleApi.RoleVO[]>([]) // 角色列表
+const postOptions = ref<PostApi.PostVO[]>([]) // 岗位列表
+const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
+const deptOptions = ref<DeptApi.DeptVO[]>([]) // 部门列表
+const deptTreeOptions = ref()
+const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]) // 用户组列表
+provide('formFields', formFields)
+provide('formType', formType)
+provide('roleList', roleOptions)
+provide('postList', postOptions)
+provide('userList', userOptions)
+provide('deptList', deptOptions)
+provide('userGroupList', userGroupOptions)
+provide('deptTree', deptTreeOptions)
+
+const message = useMessage() // 国际化
+const processNodeTree = ref<SimpleFlowNode | undefined>()
+const errorDialogVisible = ref(false)
+let errorNodes: SimpleFlowNode[] = []
+const saveSimpleFlowModel = async () => {
+  if (!props.modelId) {
+    message.error('缺少模型 modelId 编号')
+    return
+  }
+  errorNodes = []
+  validateNode(processNodeTree.value, errorNodes)
+  if (errorNodes.length > 0) {
+    errorDialogVisible.value = true
+    return
+  }
+  const data = {
+    id: props.modelId,
+    simpleModel: processNodeTree.value
+  }
+
+  const result = await updateBpmSimpleModel(data)
+  if (result) {
+    message.success('修改成功')
+    close()
+  } else {
+    message.alert('修改失败')
+  }
+}
+// 校验节点设置。 暂时以 showText 为空 未节点错误配置
+const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNode[]) => {
+  if (node) {
+    const { type, showText, conditionNodes } = node
+    if (type == NodeType.END_EVENT_NODE) {
+      return
+    }
+    if (type == NodeType.START_USER_NODE) {
+      validateNode(node.childNode, errorNodes)
+    }
+
+    if (type === NodeType.USER_TASK_NODE) {
+      if (!showText) {
+        errorNodes.push(node)
+      }
+      validateNode(node.childNode, errorNodes)
+    }
+    if (type === NodeType.COPY_TASK_NODE) {
+      if (!showText) {
+        errorNodes.push(node)
+      }
+      validateNode(node.childNode, errorNodes)
+    }
+    if (type === NodeType.CONDITION_NODE) {
+      if (!showText) {
+        errorNodes.push(node)
+      }
+      validateNode(node.childNode, errorNodes)
+    }
+
+    if (type == NodeType.CONDITION_BRANCH_NODE) {
+      conditionNodes?.forEach((item) => {
+        validateNode(item, errorNodes)
+      })
+      validateNode(node.childNode, errorNodes)
+    }
+  }
+}
+
+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 == MAX_SCALE_VALUE) {
+    return
+  }
+  scaleValue.value += 10
+}
+// 缩小
+const zoomIn = () => {
+  if (scaleValue.value == MIN_SCALE_VALUE) {
+    return
+  }
+  scaleValue.value -= 10
+}
+
+onMounted(async () => {
+  try {
+    loading.value = true
+    // 获取表单字段
+    const bpmnModel = await getModel(props.modelId)
+    if (bpmnModel) {
+      formType.value = bpmnModel.formType
+      if (formType.value === 10) {
+        const bpmnForm = (await getForm(bpmnModel.formId)) as unknown as FormVO
+        formFields.value = bpmnForm?.fields
+      }
+    }
+    // 获得角色列表
+    roleOptions.value = await RoleApi.getSimpleRoleList()
+    // 获得岗位列表
+    postOptions.value = await PostApi.getSimplePostList()
+    // 获得用户列表
+    userOptions.value = await UserApi.getSimpleUserList()
+    // 获得部门列表
+    deptOptions.value = await DeptApi.getSimpleDeptList()
+
+    deptTreeOptions.value = handleTree(deptOptions.value as DeptApi.DeptVO[], 'id')
+    // 获取用户组列表
+    userGroupOptions.value = await UserGroupApi.getUserGroupSimpleList()
+
+    // 获取 SIMPLE 设计器模型
+    const result = await getBpmSimpleModel(props.modelId)
+    if (result) {
+      processNodeTree.value = result
+    } else {
+      // 初始值
+      processNodeTree.value = {
+        name: '发起人',
+        type: NodeType.START_USER_NODE,
+        id: NodeId.START_USER_NODE_ID,
+        childNode: {
+          id: NodeId.END_EVENT_NODE_ID,
+          name: '结束',
+          type: NodeType.END_EVENT_NODE
+        }
+      }
+    }
+  } finally {
+    loading.value = false
+  }
+})
+</script>

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

@@ -0,0 +1,544 @@
+// @ts-ignore
+import { DictDataVO } from '@/api/system/dict/types'
+
+/**
+ * 节点类型
+ */
+export enum NodeType {
+  /**
+   * 结束节点
+   */
+  END_EVENT_NODE = 1,
+  /**
+   * 发起人节点
+   */
+  START_USER_NODE = 10,
+  /**
+   * 审批人节点
+   */
+  USER_TASK_NODE = 11,
+
+  /**
+   * 抄送人节点
+   */
+  COPY_TASK_NODE = 12,
+
+  /**
+   * 条件节点
+   */
+  CONDITION_NODE = 50,
+  /**
+   * 条件分支节点 (对应排他网关)
+   */
+  CONDITION_BRANCH_NODE = 51,
+  /**
+   * 并行分支节点 (对应并行网关)
+   */
+  PARALLEL_BRANCH_NODE = 52,
+
+  /**
+   * 包容分支节点 (对应包容网关)
+   */
+  INCLUSIVE_BRANCH_NODE = 53
+}
+
+export enum NodeId {
+  /**
+   * 发起人节点 Id
+   */
+  START_USER_NODE_ID = 'StartUserNode',
+
+  /**
+   * 发起人节点 Id
+   */
+  END_EVENT_NODE_ID = 'EndEvent'
+}
+
+/**
+ *  节点结构定义
+ */
+export interface SimpleFlowNode {
+  id: string
+  type: NodeType
+  name: string
+  showText?: string
+  // 孩子节点
+  childNode?: SimpleFlowNode
+  // 条件节点
+  conditionNodes?: SimpleFlowNode[]
+  // 审批类型
+  approveType?: ApproveType
+  // 候选人策略
+  candidateStrategy?: number
+  // 候选人参数
+  candidateParam?: string
+  // 多人审批方式
+  approveMethod?: ApproveMethodType
+  //通过比例
+  approveRatio?: number
+  // 审批按钮设置
+  buttonsSetting?: any[]
+  // 表单权限
+  fieldsPermission?: Array<Record<string, string>>
+  // 审批任务超时处理
+  timeoutHandler?: TimeoutHandler
+  // 审批任务拒绝处理
+  rejectHandler?: RejectHandler
+  // 审批人为空的处理
+  assignEmptyHandler?: AssignEmptyHandler
+  // 审批节点的审批人与发起人相同时,对应的处理类型
+  assignStartUserHandlerType?: number
+  // 条件类型
+  conditionType?: ConditionType
+  // 条件表达式
+  conditionExpression?: string
+  // 条件组
+  conditionGroups?: ConditionGroup
+  // 是否默认的条件
+  defaultFlow?: boolean
+
+}
+// 候选人策略枚举 ( 用于审批节点。抄送节点 )
+export enum CandidateStrategy {
+  /**
+   * 指定角色
+   */
+  ROLE = 10,
+  /**
+   * 部门成员
+   */
+  DEPT_MEMBER = 20,
+  /**
+   * 部门的负责人
+   */
+  DEPT_LEADER = 21,
+  /**
+   * 连续多级部门的负责人
+   */
+  MULTI_LEVEL_DEPT_LEADER = 23,
+  /**
+   * 指定岗位
+   */
+  POST = 22,
+  /**
+   * 指定用户
+   */
+  USER = 30,
+  /**
+   * 发起人自选
+   */
+  START_USER_SELECT = 35,
+  /**
+   * 发起人自己
+   */
+  START_USER = 36,
+  /**
+   * 发起人部门负责人
+   */
+  START_USER_DEPT_LEADER = 37,
+  /**
+   * 发起人连续多级部门的负责人
+   */
+  START_USER_MULTI_LEVEL_DEPT_LEADER = 38,
+  /**
+   * 指定用户组
+   */
+  USER_GROUP = 40,
+  /**
+   * 流程表达式
+   */
+  EXPRESSION = 60
+}
+
+// 多人审批方式类型枚举 ( 用于审批节点 )
+export enum ApproveMethodType {
+  /**
+   * 随机挑选一人审批
+   */
+  RANDOM_SELECT_ONE_APPROVE = 1,
+
+  /**
+   * 多人会签(按通过比例)
+   */
+  APPROVE_BY_RATIO = 2,
+
+  /**
+   * 多人或签(通过只需一人,拒绝只需一人)
+   */
+  ANY_APPROVE = 3,
+  /**
+   * 多人依次审批
+   */
+  SEQUENTIAL_APPROVE = 4
+}
+
+/**
+ * 审批拒绝结构定义
+ */
+export type RejectHandler = {
+  // 审批拒绝类型
+  type: RejectHandlerType
+  // 回退节点 Id
+  returnNodeId?: string
+}
+
+/**
+ * 审批超时结构定义
+ */
+export type TimeoutHandler = {
+  // 是否开启超时处理
+  enable: boolean
+  // 超时执行的动作
+  type?: number
+  // 超时时间设置
+  timeDuration?: string
+  // 执行动作是自动提醒, 最大提醒次数
+  maxRemindCount?: number
+}
+
+/**
+ * 审批人为空的结构定义
+ */
+export type AssignEmptyHandler = {
+  // 审批人为空的处理类型
+  type: AssignEmptyHandlerType
+  // 指定用户的编号数组
+  userIds?: number[]
+}
+
+// 审批拒绝类型枚举
+export enum RejectHandlerType {
+  /**
+   * 结束流程
+   */
+  FINISH_PROCESS = 1,
+  /**
+   * 驳回到指定节点
+   */
+  RETURN_USER_TASK = 2
+}
+// 用户任务超时处理类型枚举
+export enum TimeoutHandlerType {
+  /**
+   * 自动提醒
+   */
+  REMINDER = 1,
+  /**
+   * 自动同意
+   */
+  APPROVE = 2,
+  /**
+   * 自动拒绝
+   */
+  REJECT = 3
+}
+// 用户任务的审批人为空时,处理类型枚举
+export enum AssignEmptyHandlerType {
+  /**
+   * 自动通过
+   */
+  APPROVE = 1,
+  /**
+   * 自动拒绝
+   */
+  REJECT = 2,
+  /**
+   * 指定人员审批
+   */
+  ASSIGN_USER,
+  /**
+   * 转交给流程管理员
+   */
+  ASSIGN_ADMIN = 4
+}
+// 用户任务的审批人与发起人相同时,处理类型枚举
+export enum AssignStartUserHandlerType {
+  /**
+   * 由发起人对自己审批
+   */
+  START_USER_AUDIT = 1,
+  /**
+   * 自动跳过【参考飞书】:1)如果当前节点还有其他审批人,则交由其他审批人进行审批;2)如果当前节点没有其他审批人,则该节点自动通过
+   */
+  SKIP = 2,
+  /**
+   * 转交给部门负责人审批
+   */
+  ASSIGN_DEPT_LEADER = 3
+}
+
+// 用户任务的审批类型。 【参考飞书】
+export enum ApproveType {
+  /**
+   * 人工审批
+   */
+  USER = 1,
+  /**
+   * 自动通过
+   */
+  AUTO_APPROVE = 2,
+  /**
+   * 自动拒绝
+   */
+  AUTO_REJECT = 3
+}
+
+// 时间单位枚举
+export enum TimeUnitType {
+  /**
+   * 分钟
+   */
+  MINUTE = 1,
+  /**
+   * 小时
+   */
+  HOUR = 2,
+  /**
+   * 天
+   */
+  DAY = 3
+}
+
+// 条件配置类型 ( 用于条件节点配置 )
+export enum ConditionType {
+  /**
+   * 条件表达式
+   */
+  EXPRESSION = 1,
+
+  /**
+   * 条件规则
+   */
+  RULE = 2
+}
+/**
+ * 表单权限的枚举
+ */
+export enum FieldPermissionType {
+  /**
+   * 只读
+   */
+  READ = '1',
+  /**
+   * 编辑
+   */
+  WRITE = '2',
+  /**
+   * 隐藏
+   */
+  NONE = '3'
+}
+/**
+ * 操作按钮权限结构定义
+ */
+export type ButtonSetting = {
+  id: OperationButtonType
+  displayName: string
+  enable: boolean
+}
+
+// 操作按钮类型枚举 (用于审批节点)
+export enum OperationButtonType {
+  /**
+   * 通过
+   */
+  APPROVE = 1,
+  /**
+   * 拒绝
+   */
+  REJECT = 2,
+  /**
+   * 转办
+   */
+  TRANSFER = 3,
+  /**
+   * 委派
+   */
+  DELEGATE = 4,
+  /**
+   * 加签
+   */
+  ADD_SIGN = 5,
+  /**
+   * 回退
+   */
+  RETURN = 6
+}
+
+/**
+ * 条件规则结构定义
+ */
+export type ConditionRule = {
+  type: number
+  opName: string
+  opCode: string
+  leftSide: string
+  rightSide: string
+}
+
+/**
+ * 条件组结构定义
+ */
+export type ConditionGroup = {
+  // 条件组的逻辑关系是否为且
+  and: boolean
+  // 条件数组
+  conditions: Condition[]
+}
+
+/**
+ * 条件结构定义
+ */
+export type Condition = {
+  // 条件规则的逻辑关系是否为且
+  and: boolean
+  rules: ConditionRule[]
+}
+
+export const NODE_DEFAULT_TEXT = new Map<number, string>()
+NODE_DEFAULT_TEXT.set(NodeType.USER_TASK_NODE, '请配置审批人')
+NODE_DEFAULT_TEXT.set(NodeType.COPY_TASK_NODE, '请配置抄送人')
+NODE_DEFAULT_TEXT.set(NodeType.CONDITION_NODE, '请设置条件')
+NODE_DEFAULT_TEXT.set(NodeType.START_USER_NODE, '请设置发起人')
+
+export const NODE_DEFAULT_NAME = new Map<number, string>()
+NODE_DEFAULT_NAME.set(NodeType.USER_TASK_NODE, '审批人')
+NODE_DEFAULT_NAME.set(NodeType.COPY_TASK_NODE, '抄送人')
+NODE_DEFAULT_NAME.set(NodeType.CONDITION_NODE, '条件')
+NODE_DEFAULT_NAME.set(NodeType.START_USER_NODE, '发起人')
+
+// 候选人策略。暂时不从字典中取。 后续可能调整。控制显示顺序
+export const CANDIDATE_STRATEGY: DictDataVO[] = [
+  { label: '指定成员', value: CandidateStrategy.USER },
+  { label: '指定角色', value: CandidateStrategy.ROLE },
+  { label: '部门成员', value: CandidateStrategy.DEPT_MEMBER },
+  { label: '部门负责人', value: CandidateStrategy.DEPT_LEADER },
+  { label: '连续多级部门负责人', value: CandidateStrategy.MULTI_LEVEL_DEPT_LEADER },
+  { label: '发起人自选', value: CandidateStrategy.START_USER_SELECT },
+  { label: '发起人本人', value: CandidateStrategy.START_USER },
+  { label: '发起人部门负责人', value: CandidateStrategy.START_USER_DEPT_LEADER },
+  { label: '发起人连续部门负责人', value: CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER },
+  { label: '用户组', value: CandidateStrategy.USER_GROUP },
+  { label: '流程表达式', value: CandidateStrategy.EXPRESSION }
+]
+// 审批节点 的审批类型
+export const APPROVE_TYPE: DictDataVO[] = [
+  { label: '人工审批', value: ApproveType.USER },
+  { label: '自动通过', value: ApproveType.AUTO_APPROVE },
+  { label: '自动拒绝', value: ApproveType.AUTO_REJECT }
+]
+
+export const APPROVE_METHODS: DictDataVO[] = [
+  { label: '按顺序依次审批', value: ApproveMethodType.SEQUENTIAL_APPROVE },
+  { label: '会签(可同时审批,至少 % 人必须审批通过)', value: ApproveMethodType.APPROVE_BY_RATIO },
+  { label: '或签(可同时审批,有一人通过即可)', value: ApproveMethodType.ANY_APPROVE },
+  { label: '随机挑选一人审批', value: ApproveMethodType.RANDOM_SELECT_ONE_APPROVE }
+]
+
+export const CONDITION_CONFIG_TYPES: DictDataVO[] = [
+  { label: '条件表达式', value: ConditionType.EXPRESSION },
+  { label: '条件规则', value: ConditionType.RULE }
+]
+
+// 时间单位类型
+export const TIME_UNIT_TYPES: DictDataVO[] = [
+  { label: '分钟', value: TimeUnitType.MINUTE },
+  { label: '小时', value: TimeUnitType.HOUR },
+  { label: '天', value: TimeUnitType.DAY }
+]
+// 超时处理执行动作类型
+export const TIMEOUT_HANDLER_TYPES: DictDataVO[] = [
+  { label: '自动提醒', value: 1 },
+  { label: '自动同意', value: 2 },
+  { label: '自动拒绝', value: 3 }
+]
+export const REJECT_HANDLER_TYPES: DictDataVO[] = [
+  { label: '终止流程', value: RejectHandlerType.FINISH_PROCESS },
+  { label: '驳回到指定节点', value: RejectHandlerType.RETURN_USER_TASK }
+  // { label: '结束任务', value: RejectHandlerType.FINISH_TASK }
+]
+export const ASSIGN_EMPTY_HANDLER_TYPES: DictDataVO[] = [
+  { label: '自动通过', value: 1 },
+  { label: '自动拒绝', value: 2 },
+  { label: '指定成员审批', value: 3 },
+  { label: '转交给流程管理员', value: 4 }
+]
+export const ASSIGN_START_USER_HANDLER_TYPES: DictDataVO[] = [
+  { label: '由发起人对自己审批', value: 1 },
+  { label: '自动跳过', value: 2 },
+  { label: '转交给部门负责人审批', value: 3 }
+]
+
+// 比较运算符
+export const COMPARISON_OPERATORS: DictDataVO = [
+  {
+    value: '==',
+    label: '等于'
+  },
+  {
+    value: '!=',
+    label: '不等于'
+  },
+  {
+    value: '>',
+    label: '大于'
+  },
+  {
+    value: '>=',
+    label: '大于等于'
+  },
+  {
+    value: '<',
+    label: '小于'
+  },
+  {
+    value: '<=',
+    label: '小于等于'
+  }
+]
+// 审批操作按钮名称
+export const OPERATION_BUTTON_NAME = new Map<number, string>()
+OPERATION_BUTTON_NAME.set(OperationButtonType.APPROVE, '通过')
+OPERATION_BUTTON_NAME.set(OperationButtonType.REJECT, '拒绝')
+OPERATION_BUTTON_NAME.set(OperationButtonType.TRANSFER, '转办')
+OPERATION_BUTTON_NAME.set(OperationButtonType.DELEGATE, '委派')
+OPERATION_BUTTON_NAME.set(OperationButtonType.ADD_SIGN, '加签')
+OPERATION_BUTTON_NAME.set(OperationButtonType.RETURN, '回退')
+
+// 默认的按钮权限设置
+export const DEFAULT_BUTTON_SETTING: ButtonSetting[] = [
+  { id: OperationButtonType.APPROVE, displayName: '通过', enable: true },
+  { id: OperationButtonType.REJECT, displayName: '拒绝', enable: true },
+  { id: OperationButtonType.TRANSFER, displayName: '转办', enable: false },
+  { id: OperationButtonType.DELEGATE, displayName: '委派', enable: false },
+  { id: OperationButtonType.ADD_SIGN, displayName: '加签', enable: false },
+  { id: OperationButtonType.RETURN, displayName: '回退', enable: false }
+]
+
+// 发起人的按钮权限。暂时定死,不可以编辑
+export const START_USER_BUTTON_SETTING: ButtonSetting[] = [
+  { id: OperationButtonType.APPROVE, displayName: '提交', enable: true },
+  { id: OperationButtonType.REJECT, displayName: '拒绝', enable: false },
+  { id: OperationButtonType.TRANSFER, displayName: '转办', enable: false },
+  { id: OperationButtonType.DELEGATE, displayName: '委派', enable: false },
+  { id: OperationButtonType.ADD_SIGN, displayName: '加签', enable: false },
+  { id: OperationButtonType.RETURN, displayName: '回退', enable: false }
+]
+
+export const MULTI_LEVEL_DEPT: DictDataVO = [
+  { label: '第 1 级部门', value: 1 },
+  { label: '第 2 级部门', value: 2 },
+  { label: '第 3 级部门', value: 3 },
+  { label: '第 4 级部门', value: 4 },
+  { label: '第 5 级部门', value: 5 },
+  { label: '第 6 级部门', value: 6 },
+  { label: '第 7 级部门', value: 7 },
+  { label: '第 8 级部门', value: 8 },
+  { label: '第 9 级部门', value: 9 },
+  { label: '第 10 级部门', value: 10 },
+  { label: '第 11 级部门', value: 11 },
+  { label: '第 12 级部门', value: 12 },
+  { label: '第 13 级部门', value: 13 },
+  { label: '第 14 级部门', value: 14 },
+  { label: '第 15 级部门', value: 15 }
+]

+ 4 - 0
src/components/SimpleProcessDesignerV2/src/index.ts

@@ -0,0 +1,4 @@
+import SimpleProcessDesigner from './SimpleProcessDesigner.vue'
+import '../theme/simple-process-designer.scss'
+
+export { SimpleProcessDesigner }

+ 478 - 0
src/components/SimpleProcessDesignerV2/src/node.ts

@@ -0,0 +1,478 @@
+import { cloneDeep } from 'lodash-es'
+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'
+import {
+  SimpleFlowNode,
+  CandidateStrategy,
+  NodeType,
+  ApproveMethodType,
+  RejectHandlerType,
+  NODE_DEFAULT_NAME,
+  AssignStartUserHandlerType,
+  AssignEmptyHandlerType,
+  FieldPermissionType
+} from './consts'
+export function useWatchNode(props: { flowNode: SimpleFlowNode }): Ref<SimpleFlowNode> {
+  const node = ref<SimpleFlowNode>(props.flowNode)
+  watch(
+    () => props.flowNode,
+    (newValue) => {
+      node.value = newValue
+    }
+  )
+  return node
+}
+
+/**
+ * @description 表单数据权限配置,用于发起人节点 、审批节点、抄送节点
+ */
+export function useFormFieldsPermission(defaultPermission: FieldPermissionType) {
+  // 字段权限配置. 需要有 field, title,  permissioin 属性
+  const fieldsPermissionConfig = ref<Array<Record<string, string>>>([])
+
+  const formType = inject<Ref<number>>('formType') // 表单类型
+
+  const formFields = inject<Ref<string[]>>('formFields') // 流程表单字段
+
+  const getNodeConfigFormFields = (nodeFormFields?: Array<Record<string, string>>) => {
+    nodeFormFields = toRaw(nodeFormFields)
+    fieldsPermissionConfig.value =
+      cloneDeep(nodeFormFields) || getDefaultFieldsPermission(unref(formFields))
+  }
+  // 默认的表单权限: 获取表单的所有字段,设置字段默认权限为只读
+  const getDefaultFieldsPermission = (formFields?: string[]) => {
+    const defaultFieldsPermission: Array<Record<string, string>> = []
+    if (formFields) {
+      formFields.forEach((fieldStr: string) => {
+        parseFieldsSetDefaultPermission(JSON.parse(fieldStr), defaultFieldsPermission)
+      })
+    }
+    return defaultFieldsPermission
+  }
+  // 解析字段。赋给默认权限
+  const parseFieldsSetDefaultPermission = (
+    rule: Record<string, any>,
+    fieldsPermission: Array<Record<string, string>>,
+    parentTitle: string = ''
+  ) => {
+    const { /**type,*/ field, title: tempTitle, children } = rule
+    if (field && tempTitle) {
+      let title = tempTitle
+      if (parentTitle) {
+        title = `${parentTitle}.${tempTitle}`
+      }
+      fieldsPermission.push({
+        field,
+        title,
+        permission: defaultPermission
+      })
+      // TODO 子表单 需要处理子表单字段
+      // if (type === 'group' && rule.props?.rule && Array.isArray(rule.props.rule)) {
+      //   // 解析子表单的字段
+      //   rule.props.rule.forEach((item) => {
+      //     parseFieldsSetDefaultPermission(item, fieldsPermission, title)
+      //   })
+      // }
+    }
+    if (children && Array.isArray(children)) {
+      children.forEach((rule) => {
+        parseFieldsSetDefaultPermission(rule, fieldsPermission)
+      })
+    }
+  }
+
+  return {
+    formType,
+    fieldsPermissionConfig,
+    getNodeConfigFormFields
+  }
+}
+/**
+ * @description 获取表单的字段
+ */
+export function useFormFields() {
+  // 解析后的表单字段
+  const formFields = inject<Ref<string[]>>('formFields') // 流程表单字段
+  const parseFormFields = () => {
+    const parsedFormFields: Array<Record<string, string>> = []
+    if (formFields) {
+      formFields.value.forEach((fieldStr: string) => {
+        parseField(JSON.parse(fieldStr), parsedFormFields)
+      })
+    }
+    return parsedFormFields
+  }
+  // 解析字段。
+  const parseField = (
+    rule: Record<string, any>,
+    parsedFormFields: Array<Record<string, string>>,
+    parentTitle: string = ''
+  ) => {
+    const { field, title: tempTitle, children, type } = rule
+    if (field && tempTitle) {
+      let title = tempTitle
+      if (parentTitle) {
+        title = `${parentTitle}.${tempTitle}`
+      }
+      parsedFormFields.push({
+        field,
+        title,
+        type
+      })
+      // TODO 子表单 需要处理子表单字段
+      // if (type === 'group' && rule.props?.rule && Array.isArray(rule.props.rule)) {
+      //   // 解析子表单的字段
+      //   rule.props.rule.forEach((item) => {
+      //     parseFieldsSetDefaultPermission(item, fieldsPermission, title)
+      //   })
+      // }
+    }
+    if (children && Array.isArray(children)) {
+      children.forEach((rule) => {
+        parseField(rule, parsedFormFields)
+      })
+    }
+  }
+
+  return parseFormFields()
+}
+
+export type UserTaskFormType = {
+  //candidateParamArray: any[]
+  candidateStrategy: CandidateStrategy
+  approveMethod: ApproveMethodType
+  roleIds?: number[] // 角色
+  deptIds?: number[] // 部门
+  deptLevel?: number // 部门层级
+  userIds?: number[] // 用户
+  userGroups?: number[] // 用户组
+  postIds?: number[] // 岗位
+  expression?: string // 流程表达式
+  approveRatio?: number
+  rejectHandlerType?: RejectHandlerType
+  returnNodeId?: string
+  timeoutHandlerEnable?: boolean
+  timeoutHandlerType?: number
+  assignEmptyHandlerType?: AssignEmptyHandlerType
+  assignEmptyHandlerUserIds?: number[]
+  assignStartUserHandlerType?: AssignStartUserHandlerType
+  timeDuration?: number
+  maxRemindCount?: number
+  buttonsSetting: any[]
+}
+
+export type CopyTaskFormType = {
+  // candidateParamArray: any[]
+  candidateStrategy: CandidateStrategy
+  roleIds?: number[] // 角色
+  deptIds?: number[] // 部门
+  deptLevel?: number // 部门层级
+  userIds?: number[] // 用户
+  userGroups?: number[] // 用户组
+  postIds?: number[] // 岗位
+  expression?: string // 流程表达式
+}
+
+/**
+ * @description 节点表单数据。 用于审批节点、抄送节点
+ */
+export function useNodeForm(nodeType: NodeType) {
+  const roleOptions = inject<Ref<RoleApi.RoleVO[]>>('roleList') // 角色列表
+  const postOptions = inject<Ref<PostApi.PostVO[]>>('postList') // 岗位列表
+  const userOptions = inject<Ref<UserApi.UserVO[]>>('userList') // 用户列表
+  const deptOptions = inject<Ref<DeptApi.DeptVO[]>>('deptList') // 部门列表
+  const userGroupOptions = inject<Ref<UserGroupApi.UserGroupVO[]>>('userGroupList') // 用户组列表
+  const deptTreeOptions = inject('deptTree') // 部门树
+  const configForm = ref<UserTaskFormType | CopyTaskFormType>()
+  if (nodeType === NodeType.USER_TASK_NODE) {
+    configForm.value = {
+      candidateStrategy: CandidateStrategy.USER,
+      approveMethod: ApproveMethodType.SEQUENTIAL_APPROVE,
+      approveRatio: 100,
+      rejectHandlerType: RejectHandlerType.FINISH_PROCESS,
+      assignStartUserHandlerType: AssignStartUserHandlerType.START_USER_AUDIT,
+      returnNodeId: '',
+      timeoutHandlerEnable: false,
+      timeoutHandlerType: 1,
+      timeDuration: 6, // 默认 6小时
+      maxRemindCount: 1, // 默认 提醒 1次
+      buttonsSetting: []
+    }
+  } else {
+    configForm.value = {
+      candidateStrategy: CandidateStrategy.USER
+    }
+  }
+
+  const getShowText = (): string => {
+    let showText = ''
+    // 指定成员
+    if (configForm.value?.candidateStrategy === CandidateStrategy.USER) {
+      if (configForm.value?.userIds!.length > 0) {
+        const candidateNames: string[] = []
+        userOptions?.value.forEach((item) => {
+          if (configForm.value?.userIds!.includes(item.id)) {
+            candidateNames.push(item.nickname)
+          }
+        })
+        showText = `指定成员:${candidateNames.join(',')}`
+      }
+    }
+    // 指定角色
+    if (configForm.value?.candidateStrategy === CandidateStrategy.ROLE) {
+      if (configForm.value.roleIds!.length > 0) {
+        const candidateNames: string[] = []
+        roleOptions?.value.forEach((item) => {
+          if (configForm.value?.roleIds!.includes(item.id)) {
+            candidateNames.push(item.name)
+          }
+        })
+        showText = `指定角色:${candidateNames.join(',')}`
+      }
+    }
+    // 指定部门
+    if (
+      configForm.value?.candidateStrategy === CandidateStrategy.DEPT_MEMBER ||
+      configForm.value?.candidateStrategy === CandidateStrategy.DEPT_LEADER ||
+      configForm.value?.candidateStrategy === CandidateStrategy.MULTI_LEVEL_DEPT_LEADER
+    ) {
+      if (configForm.value?.deptIds!.length > 0) {
+        const candidateNames: string[] = []
+        deptOptions?.value.forEach((item) => {
+          if (configForm.value?.deptIds!.includes(item.id!)) {
+            candidateNames.push(item.name)
+          }
+        })
+        if (configForm.value.candidateStrategy === CandidateStrategy.DEPT_MEMBER) {
+          showText = `部门成员:${candidateNames.join(',')}`
+        } else if (configForm.value.candidateStrategy === CandidateStrategy.DEPT_LEADER) {
+          showText = `部门的负责人:${candidateNames.join(',')}`
+        } else {
+          showText = `多级部门的负责人:${candidateNames.join(',')}`
+        }
+      }
+    }
+
+    // 指定岗位
+    if (configForm.value?.candidateStrategy === CandidateStrategy.POST) {
+      if (configForm.value.postIds!.length > 0) {
+        const candidateNames: string[] = []
+        postOptions?.value.forEach((item) => {
+          if (configForm.value?.postIds!.includes(item.id!)) {
+            candidateNames.push(item.name)
+          }
+        })
+        showText = `指定岗位: ${candidateNames.join(',')}`
+      }
+    }
+    // 指定用户组
+    if (configForm.value?.candidateStrategy === CandidateStrategy.USER_GROUP) {
+      if (configForm.value?.userGroups!.length > 0) {
+        const candidateNames: string[] = []
+        userGroupOptions?.value.forEach((item) => {
+          if (configForm.value?.userGroups!.includes(item.id)) {
+            candidateNames.push(item.name)
+          }
+        })
+        showText = `指定用户组: ${candidateNames.join(',')}`
+      }
+    }
+
+    // 发起人自选
+    if (configForm.value?.candidateStrategy === CandidateStrategy.START_USER_SELECT) {
+      showText = `发起人自选`
+    }
+    // 发起人自己
+    if (configForm.value?.candidateStrategy === CandidateStrategy.START_USER) {
+      showText = `发起人自己`
+    }
+    // 发起人的部门负责人
+    if (configForm.value?.candidateStrategy === CandidateStrategy.START_USER_DEPT_LEADER) {
+      showText = `发起人的部门负责人`
+    }
+    // 发起人的部门负责人
+    if (
+      configForm.value?.candidateStrategy === CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER
+    ) {
+      showText = `发起人连续部门负责人`
+    }
+    // 流程表达式
+    if (configForm.value?.candidateStrategy === CandidateStrategy.EXPRESSION) {
+      showText = `流程表达式:${configForm.value.expression}`
+    }
+    return showText
+  }
+
+  /**
+   *  处理候选人参数的赋值
+   */
+  const handleCandidateParam = () => {
+    let candidateParam: undefined | string = undefined
+    if (!configForm.value) {
+      return candidateParam
+    }
+    switch (configForm.value.candidateStrategy) {
+      case CandidateStrategy.USER:
+        candidateParam = configForm.value.userIds!.join(',')
+        break
+      case CandidateStrategy.ROLE:
+        candidateParam = configForm.value.roleIds!.join(',')
+        break
+      case CandidateStrategy.POST:
+        candidateParam = configForm.value.postIds!.join(',')
+        break
+      case CandidateStrategy.USER_GROUP:
+        candidateParam = configForm.value.userGroups!.join(',')
+        break
+      case CandidateStrategy.EXPRESSION:
+        candidateParam = configForm.value.expression!
+        break
+      case CandidateStrategy.DEPT_MEMBER:
+      case CandidateStrategy.DEPT_LEADER:
+        candidateParam = configForm.value.deptIds!.join(',')
+        break
+      // 发起人部门负责人
+      case CandidateStrategy.START_USER_DEPT_LEADER:
+      case CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER:
+        candidateParam = configForm.value.deptLevel + ''
+        break
+      // 指定连续多级部门的负责人
+      case CandidateStrategy.MULTI_LEVEL_DEPT_LEADER: {
+        // 候选人参数格式: | 分隔 。左边为部门(多个部门用 , 分隔)。 右边为部门层级
+        const deptIds = configForm.value.deptIds!.join(',')
+        candidateParam = deptIds.concat('|' + configForm.value.deptLevel + '')
+        break
+      }
+      default:
+        break
+    }
+    return candidateParam
+  }
+  /**
+   *  解析候选人参数
+   */
+  const parseCandidateParam = (
+    candidateStrategy: CandidateStrategy,
+    candidateParam: string | undefined
+  ) => {
+    if (!configForm.value || !candidateParam) {
+      return
+    }
+    switch (candidateStrategy) {
+      case CandidateStrategy.USER: {
+        configForm.value.userIds = candidateParam.split(',').map((item) => +item)
+        break
+      }
+      case CandidateStrategy.ROLE:
+        configForm.value.roleIds = candidateParam.split(',').map((item) => +item)
+        break
+      case CandidateStrategy.POST:
+        configForm.value.postIds = candidateParam.split(',').map((item) => +item)
+        break
+      case CandidateStrategy.USER_GROUP:
+        configForm.value.userGroups = candidateParam.split(',').map((item) => +item)
+        break
+      case CandidateStrategy.EXPRESSION:
+        configForm.value.expression = candidateParam
+        break
+      case CandidateStrategy.DEPT_MEMBER:
+      case CandidateStrategy.DEPT_LEADER:
+        configForm.value.deptIds = candidateParam.split(',').map((item) => +item)
+        break
+      // 发起人部门负责人
+      case CandidateStrategy.START_USER_DEPT_LEADER:
+      case CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER:
+        configForm.value.deptLevel = +candidateParam
+        break
+      // 指定连续多级部门的负责人
+      case CandidateStrategy.MULTI_LEVEL_DEPT_LEADER: {
+        // 候选人参数格式: | 分隔 。左边为部门(多个部门用 , 分隔)。 右边为部门层级
+        const paramArray = candidateParam.split('|')
+        configForm.value.deptIds = paramArray[0].split(',').map((item) => +item)
+        configForm.value.deptLevel = +paramArray[1]
+        break
+      }
+      default:
+        break
+    }
+  }
+  return {
+    configForm,
+    roleOptions,
+    postOptions,
+    userOptions,
+    userGroupOptions,
+    deptTreeOptions,
+    handleCandidateParam,
+    parseCandidateParam,
+    getShowText
+  }
+}
+
+/**
+ * @description 抽屉配置
+ */
+export function useDrawer() {
+  // 抽屉配置是否可见
+  const settingVisible = ref(false)
+  // 关闭配置抽屉
+  const closeDrawer = () => {
+    settingVisible.value = false
+  }
+  // 打开配置抽屉
+  const openDrawer = () => {
+    settingVisible.value = true
+  }
+  return {
+    settingVisible,
+    closeDrawer,
+    openDrawer
+  }
+}
+
+/**
+ * @description 节点名称配置
+ */
+export function useNodeName(nodeType: NodeType) {
+  // 节点名称
+  const nodeName = ref<string>()
+  // 节点名称输入框
+  const showInput = ref(false)
+  // 点击节点名称编辑图标
+  const clickIcon = () => {
+    showInput.value = true
+  }
+  // 节点名称输入框失去焦点
+  const blurEvent = () => {
+    showInput.value = false
+    nodeName.value = nodeName.value || (NODE_DEFAULT_NAME.get(nodeType) as string)
+  }
+  return {
+    nodeName,
+    showInput,
+    clickIcon,
+    blurEvent
+  }
+}
+
+export function useNodeName2(node: Ref<SimpleFlowNode>, nodeType: NodeType) {
+  // 显示节点名称输入框
+  const showInput = ref(false)
+  // 节点名称输入框失去焦点
+  const blurEvent = () => {
+    showInput.value = false
+    node.value.name = node.value.name || (NODE_DEFAULT_NAME.get(nodeType) as string)
+  }
+  // 点击节点标题进行输入
+  const clickTitle = () => {
+    showInput.value = true
+  }
+  return {
+    showInput,
+    clickTitle,
+    blurEvent
+  }
+}

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

@@ -0,0 +1,419 @@
+<template>
+  <el-drawer
+    :append-to-body="true"
+    v-model="settingVisible"
+    :show-close="false"
+    :size="588"
+    :before-close="handleClose"
+  >
+    <template #header>
+      <div class="config-header">
+        <input
+          v-if="showInput"
+          type="text"
+          class="config-editable-input"
+          @blur="blurEvent()"
+          v-mountedFocus
+          v-model="currentNode.name"
+          :placeholder="currentNode.name"
+        />
+        <div v-else class="node-name"
+          >{{ currentNode.name }}
+          <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()"
+        /></div>
+
+        <div class="divide-line"></div>
+      </div>
+    </template>
+    <div>
+      <div class="mb-3 font-size-16px" v-if="currentNode.defaultFlow">其它条件不满足进入此分支(该分支不可编辑和删除)</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 fieldsInfo"
+                        :key="index"
+                        :label="item.title"
+                        :value="item.field"
+                      />
+                    </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>
+      </div>
+    </div>
+    <template #footer>
+      <el-divider />
+      <div>
+        <el-button type="primary" @click="saveConfig">确 定</el-button>
+        <el-button @click="closeDrawer">取 消</el-button>
+      </div>
+    </template>
+  </el-drawer>
+</template>
+<script setup lang="ts">
+import {
+  SimpleFlowNode,
+  CONDITION_CONFIG_TYPES,
+  ConditionType,
+  COMPARISON_OPERATORS,
+  ConditionGroup,
+  Condition,
+  ConditionRule
+} from '../consts'
+import { getDefaultConditionNodeName } from '../utils'
+import { useFormFields } from '../node'
+const message = useMessage() // 消息弹窗
+defineOptions({
+  name: 'ConditionNodeConfig'
+})
+const formType = inject<Ref<number>>('formType') // 表单类型
+const conditionConfigTypes = computed(() => {
+  return CONDITION_CONFIG_TYPES.filter((item) => {
+    // 业务表单暂时去掉条件规则选项
+    if (formType?.value !== 10) {
+      return item.value === ConditionType.RULE
+    } else {
+      return true
+    }
+  })
+})
+
+const props = defineProps({
+  conditionNode: {
+    type: Object as () => SimpleFlowNode,
+    required: true
+  },
+  nodeIndex: {
+    type: Number,
+    required: true
+  }
+})
+const settingVisible = ref(false)
+const open = () => {
+  if (currentNode.value.conditionType === ConditionType.RULE) {
+    if (currentNode.value.conditionGroups) {
+      conditionGroups.value = currentNode.value.conditionGroups
+    }
+  }
+  settingVisible.value = true
+}
+
+watch(
+  () => props.conditionNode,
+  (newValue) => {
+    currentNode.value = newValue
+  }
+)
+// 显示名称输入框
+const showInput = ref(false)
+
+const clickIcon = () => {
+  showInput.value = true
+}
+// 输入框失去焦点
+const blurEvent = () => {
+  showInput.value = false
+  currentNode.value.name =
+    currentNode.value.name ||
+    getDefaultConditionNodeName(props.nodeIndex, currentNode.value?.defaultFlow)
+}
+
+const currentNode = ref<SimpleFlowNode>(props.conditionNode)
+
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+// 关闭
+const closeDrawer = () => {
+  settingVisible.value = false
+}
+
+const handleClose = async (done: (cancel?: boolean) => void) => {
+  const isSuccess = await saveConfig()
+  if (!isSuccess) {
+    done(true) // 传入 true 阻止关闭
+  } else {
+    done()
+  }
+}
+// 表单校验规则
+const formRules = reactive({
+  conditionType: [{ required: true, message: '配置方式不能为空', trigger: 'blur' }],
+  conditionExpression: [{ required: true, message: '条件表达式不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+// 保存配置
+const saveConfig = async () => {
+  if (!currentNode.value.defaultFlow) {
+    // 校验表单
+    if (!formRef) return false
+    const valid = await formRef.value.validate()
+    if (!valid) return false
+    const showText = getShowText()
+    if (!showText) {
+      return false
+    }
+    currentNode.value.showText = showText
+    if (currentNode.value.conditionType === ConditionType.EXPRESSION) {
+      currentNode.value.conditionGroups = undefined
+    }
+    if (currentNode.value.conditionType === ConditionType.RULE) {
+      currentNode.value.conditionExpression = undefined
+      currentNode.value.conditionGroups = conditionGroups.value
+    }
+  }
+  settingVisible.value = false
+  return true
+}
+const getShowText = (): string => {
+  let showText = ''
+  if (currentNode.value.conditionType === ConditionType.EXPRESSION) {
+    if (currentNode.value.conditionExpression) {
+      showText = `表达式:${currentNode.value.conditionExpression}`
+    }
+  }
+  if (currentNode.value.conditionType === ConditionType.RULE) {
+    // 条件组是否为与关系
+    const groupAnd = conditionGroups.value.and
+    let warningMesg: undefined | string = undefined
+    const conditionGroup = conditionGroups.value.conditions.map((item) => {
+      return (
+        '(' +
+        item.rules
+          .map((rule) => {
+            if (rule.leftSide && rule.rightSide) {
+              return (
+                getFieldTitle(rule.leftSide) + ' ' + getOpName(rule.opCode) + ' ' + rule.rightSide
+              )
+            } else {
+              // 有一条规则不完善。提示错误
+              warningMesg = '请完善条件规则'
+              return ''
+            }
+          })
+          .join(item.and ? ' 且 ' : ' 或 ') +
+        ' ) '
+      )
+    })
+    if (warningMesg) {
+      message.warning(warningMesg)
+      showText = ''
+    } else {
+      showText = conditionGroup.join(groupAnd ? ' 且 ' : ' 或 ')
+    }
+  }
+  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 getFieldTitle = (field: string) => {
+  const item = fieldsInfo.find((item) => item.field === field)
+  return item?.title
+}
+
+const getOpName = (opCode: string): string => {
+  const opName = COMPARISON_OPERATORS.find((item) => item.value === opCode)
+  return opName?.label
+}
+</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>

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

@@ -0,0 +1,307 @@
+<template>
+  <el-drawer
+    :append-to-body="true"
+    v-model="settingVisible"
+    :show-close="false"
+    :size="550"
+    :before-close="saveConfig"
+  >
+    <template #header>
+      <div class="config-header">
+        <input
+          v-if="showInput"
+          type="text"
+          class="config-editable-input"
+          @blur="blurEvent()"
+          v-mountedFocus
+          v-model="nodeName"
+          :placeholder="nodeName"
+        />
+        <div v-else class="node-name">
+          {{ nodeName }} <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" />
+        </div>
+        <div class="divide-line"></div>
+      </div>
+    </template>
+    <el-tabs type="border-card" v-model="activeTabName">
+      <el-tab-pane label="抄送人" name="user">
+        <div>
+          <el-form ref="formRef" :model="configForm" label-position="top" :rules="formRules">
+            <el-form-item label="抄送人设置" prop="candidateStrategy">
+              <el-radio-group
+                v-model="configForm.candidateStrategy"
+                @change="changeCandidateStrategy"
+              >
+                <el-radio
+                  v-for="(dict, index) in copyUserStrategies"
+                  :key="index"
+                  :value="dict.value"
+                  :label="dict.value"
+                >
+                  {{ dict.label }}
+                </el-radio>
+              </el-radio-group>
+            </el-form-item>
+
+            <el-form-item
+              v-if="configForm.candidateStrategy == CandidateStrategy.ROLE"
+              label="指定角色"
+              prop="roleIds"
+            >
+              <el-select v-model="configForm.roleIds" 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="
+                configForm.candidateStrategy == CandidateStrategy.DEPT_MEMBER ||
+                configForm.candidateStrategy == CandidateStrategy.DEPT_LEADER
+              "
+              label="指定部门"
+              prop="deptIds"
+              span="24"
+            >
+              <el-tree-select
+                ref="treeRef"
+                v-model="configForm.deptIds"
+                :data="deptTreeOptions"
+                :props="defaultProps"
+                empty-text="加载中,请稍后"
+                multiple
+                node-key="id"
+                style="width: 100%"
+                show-checkbox
+              />
+            </el-form-item>
+            <el-form-item
+              v-if="configForm.candidateStrategy == CandidateStrategy.POST"
+              label="指定岗位"
+              prop="postIds"
+              span="24"
+            >
+              <el-select v-model="configForm.postIds" 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="configForm.candidateStrategy == CandidateStrategy.USER"
+              label="指定用户"
+              prop="userIds"
+              span="24"
+            >
+              <el-select v-model="configForm.userIds" clearable multiple style="width: 100%">
+                <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="configForm.candidateStrategy === CandidateStrategy.USER_GROUP"
+              label="指定用户组"
+              prop="userGroups"
+            >
+              <el-select v-model="configForm.userGroups" 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="configForm.candidateStrategy === CandidateStrategy.EXPRESSION"
+              label="流程表达式"
+              prop="expression"
+            >
+              <el-input
+                type="textarea"
+                v-model="configForm.expression"
+                clearable
+                style="width: 100%"
+              />
+            </el-form-item>
+          </el-form>
+        </div>
+      </el-tab-pane>
+      <el-tab-pane label="表单字段权限" name="fields" v-if="formType === 10">
+        <div class="field-setting-pane">
+          <div class="field-setting-desc">字段权限</div>
+          <div class="field-permit-title">
+            <div class="setting-title-label first-title"> 字段名称 </div>
+            <div class="other-titles">
+              <span class="setting-title-label">只读</span>
+              <span class="setting-title-label">可编辑</span>
+              <span class="setting-title-label">隐藏</span>
+            </div>
+          </div>
+          <div
+            class="field-setting-item"
+            v-for="(item, index) in fieldsPermissionConfig"
+            :key="index"
+          >
+            <div class="field-setting-item-label"> {{ item.title }} </div>
+            <el-radio-group class="field-setting-item-group" v-model="item.permission">
+              <div class="item-radio-wrap">
+                <el-radio
+                  :value="FieldPermissionType.READ"
+                  size="large"
+                  :label="FieldPermissionType.WRITE"
+                  ><span></span
+                ></el-radio>
+              </div>
+              <div class="item-radio-wrap">
+                <el-radio
+                  :value="FieldPermissionType.WRITE"
+                  size="large"
+                  :label="FieldPermissionType.WRITE"
+                  disabled
+                  ><span></span
+                ></el-radio>
+              </div>
+              <div class="item-radio-wrap">
+                <el-radio
+                  :value="FieldPermissionType.NONE"
+                  size="large"
+                  :label="FieldPermissionType.NONE"
+                  ><span></span
+                ></el-radio>
+              </div>
+            </el-radio-group>
+          </div>
+        </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,
+  CandidateStrategy,
+  NodeType,
+  CANDIDATE_STRATEGY,
+  FieldPermissionType
+} from '../consts'
+import {
+  useWatchNode,
+  useDrawer,
+  useNodeName,
+  useFormFieldsPermission,
+  useNodeForm,
+  CopyTaskFormType
+} from '../node'
+import { defaultProps } from '@/utils/tree'
+defineOptions({
+  name: 'CopyTaskNodeConfig'
+})
+const props = defineProps({
+  flowNode: {
+    type: Object as () => SimpleFlowNode,
+    required: true
+  }
+})
+// 抽屉配置
+const { settingVisible, closeDrawer, openDrawer } = useDrawer()
+// 当前节点
+const currentNode = useWatchNode(props)
+// 节点名称
+const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.COPY_TASK_NODE)
+// 激活的 Tab 标签页
+const activeTabName = ref('user')
+// 表单字段权限配置
+const { formType, fieldsPermissionConfig, getNodeConfigFormFields } = useFormFieldsPermission(
+  FieldPermissionType.READ
+)
+// 抄送人表单配置
+const formRef = ref() // 表单 Ref
+// 表单校验规则
+const formRules = reactive({
+  candidateStrategy: [{ required: true, message: '抄送人设置不能为空', trigger: 'change' }],
+  userIds: [{ required: true, message: '用户不能为空', trigger: 'change' }],
+  roleIds: [{ required: true, message: '角色不能为空', trigger: 'change' }],
+  deptIds: [{ required: true, message: '部门不能为空', trigger: 'change' }],
+  userGroups: [{ required: true, message: '用户组不能为空', trigger: 'change' }],
+  postIds: [{ required: true, message: '岗位不能为空', trigger: 'change' }],
+  expression: [{ required: true, message: '流程表达式不能为空', trigger: 'blur' }]
+})
+
+const {
+  configForm: tempConfigForm,
+  roleOptions,
+  postOptions,
+  userOptions,
+  userGroupOptions,
+  deptTreeOptions,
+  getShowText,
+  handleCandidateParam,
+  parseCandidateParam
+} = useNodeForm(NodeType.COPY_TASK_NODE)
+const configForm = tempConfigForm as Ref<CopyTaskFormType>
+// 抄送人策略, 去掉发起人自选 和 发起人自己
+const copyUserStrategies = computed(() => {
+  return CANDIDATE_STRATEGY.filter(
+    (item) =>
+      item.value !== CandidateStrategy.START_USER_SELECT &&
+      item.value !== CandidateStrategy.START_USER
+  )
+})
+// 改变抄送人设置策略
+const changeCandidateStrategy = () => {
+  configForm.value.userIds = []
+  configForm.value.deptIds = []
+  configForm.value.roleIds = []
+  configForm.value.postIds = []
+  configForm.value.userGroups = []
+  configForm.value.deptLevel = 1
+}
+// 保存配置
+const saveConfig = async () => {
+  activeTabName.value = 'user'
+  if (!formRef) return false
+  const valid = await formRef.value.validate()
+  if (!valid) return false
+  const showText = getShowText()
+  if (!showText) return false
+  currentNode.value.name = nodeName.value!
+  currentNode.value.candidateParam = handleCandidateParam()
+  currentNode.value.candidateStrategy = configForm.value.candidateStrategy
+  currentNode.value.showText = showText
+  currentNode.value.fieldsPermission = fieldsPermissionConfig.value
+  settingVisible.value = false
+  return true
+}
+// 显示抄送节点配置, 由父组件传过来
+const showCopyTaskNodeConfig = (node: SimpleFlowNode) => {
+  nodeName.value = node.name
+  // 抄送人设置
+  configForm.value.candidateStrategy = node.candidateStrategy!
+  parseCandidateParam(node.candidateStrategy!, node?.candidateParam)
+  // 表单字段权限
+  getNodeConfigFormFields(node.fieldsPermission)
+}
+
+defineExpose({ openDrawer, showCopyTaskNodeConfig }) // 暴露方法给父组件
+</script>
+
+<style lang="scss" scoped></style>

+ 136 - 0
src/components/SimpleProcessDesignerV2/src/nodes-config/StartUserNodeConfig.vue

@@ -0,0 +1,136 @@
+<template>
+  <el-drawer
+    :append-to-body="true"
+    v-model="settingVisible"
+    :show-close="false"
+    :size="550"
+    :before-close="saveConfig"
+  >
+    <template #header>
+      <div class="config-header">
+        <input
+          v-if="showInput"
+          type="text"
+          class="config-editable-input"
+          @blur="blurEvent()"
+          v-mountedFocus
+          v-model="nodeName"
+          :placeholder="nodeName"
+        />
+        <div v-else class="node-name">
+          {{ nodeName }} <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" />
+        </div>
+        <div class="divide-line"></div>
+      </div>
+    </template>
+    <el-tabs type="border-card" v-model="activeTabName">
+      <el-tab-pane label="权限" name="user">
+        <div> 待实现 </div>
+      </el-tab-pane>
+      <el-tab-pane label="表单字段权限" name="fields" v-if="formType === 10">
+        <div class="field-setting-pane">
+          <div class="field-setting-desc">字段权限</div>
+          <div class="field-permit-title">
+            <div class="setting-title-label first-title"> 字段名称 </div>
+            <div class="other-titles">
+              <span class="setting-title-label">只读</span>
+              <span class="setting-title-label">可编辑</span>
+              <span class="setting-title-label">隐藏</span>
+            </div>
+          </div>
+          <div
+            class="field-setting-item"
+            v-for="(item, index) in fieldsPermissionConfig"
+            :key="index"
+          >
+            <div class="field-setting-item-label"> {{ item.title }} </div>
+            <el-radio-group class="field-setting-item-group" v-model="item.permission">
+              <div class="item-radio-wrap">
+                <el-radio
+                  :value="FieldPermissionType.READ"
+                  size="large"
+                  :label="FieldPermissionType.READ"
+                  ><span></span
+                ></el-radio>
+              </div>
+              <div class="item-radio-wrap">
+                <el-radio
+                  :value="FieldPermissionType.WRITE"
+                  size="large"
+                  :label="FieldPermissionType.WRITE"
+                  ><span></span
+                ></el-radio>
+              </div>
+              <div class="item-radio-wrap">
+                <el-radio
+                  :value="FieldPermissionType.NONE"
+                  size="large"
+                  :label="FieldPermissionType.NONE"
+                  ><span></span
+                ></el-radio>
+              </div>
+            </el-radio-group>
+          </div>
+        </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, NodeType, FieldPermissionType, START_USER_BUTTON_SETTING } from '../consts'
+import { useWatchNode, useDrawer, useNodeName, useFormFieldsPermission } from '../node'
+
+defineOptions({
+  name: 'StartUserNodeConfig'
+})
+const props = defineProps({
+  flowNode: {
+    type: Object as () => SimpleFlowNode,
+    required: true
+  }
+})
+// 抽屉配置
+const { settingVisible, closeDrawer, openDrawer } = useDrawer()
+// 当前节点
+const currentNode = useWatchNode(props)
+// 节点名称
+const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.COPY_TASK_NODE)
+// 激活的 Tab 标签页
+const activeTabName = ref('user')
+// 表单字段权限配置
+const { formType, fieldsPermissionConfig, getNodeConfigFormFields } = useFormFieldsPermission(
+  FieldPermissionType.WRITE
+)
+
+// 保存配置
+const saveConfig = async () => {
+  activeTabName.value = 'user'
+  currentNode.value.name = nodeName.value!
+  // TODO 暂时写死。后续可以显示谁有权限可以发起
+  currentNode.value.showText = '已设置'
+  // 设置表单权限
+  currentNode.value.fieldsPermission = fieldsPermissionConfig.value
+  // 设置发起人的按钮权限
+  currentNode.value.buttonsSetting = START_USER_BUTTON_SETTING
+  console.log('currentNode.value.buttonsSetting==>', currentNode.value.buttonsSetting)
+  settingVisible.value = false
+  return true
+}
+// 显示发起人节点配置, 由父组件传过来
+const showStartUserNodeConfig = (node: SimpleFlowNode) => {
+  nodeName.value = node.name
+  // 表单字段权限
+  getNodeConfigFormFields(node.fieldsPermission)
+}
+
+defineExpose({ openDrawer, showStartUserNodeConfig }) // 暴露方法给父组件
+</script>
+
+<style lang="scss" scoped></style>

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

@@ -0,0 +1,901 @@
+<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="config-header">
+        <input
+          v-if="showInput"
+          type="text"
+          class="config-editable-input"
+          @blur="blurEvent()"
+          v-mountedFocus
+          v-model="nodeName"
+          :placeholder="nodeName"
+        />
+        <div v-else class="node-name">
+          {{ nodeName }} <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" />
+        </div>
+        <div class="divide-line"></div>
+      </div>
+    </template>
+    <div class="flex flex-items-center mb-3">
+      <span class="font-size-16px mr-3">审批类型 :</span>
+      <el-radio-group v-model="approveType">
+        <el-radio
+          v-for="(item, index) in APPROVE_TYPE"
+          :key="index"
+          :value="item.value"
+          :label="item.value"
+        >
+          {{ item.label }}
+        </el-radio>
+      </el-radio-group>
+    </div>
+    <el-tabs type="border-card" v-model="activeTabName" v-if="approveType === ApproveType.USER">
+      <el-tab-pane label="审批人" name="user">
+        <div>
+          <el-form ref="formRef" :model="configForm" label-position="top" :rules="formRules">
+            <el-form-item label="审批人设置" prop="candidateStrategy">
+              <el-radio-group
+                v-model="configForm.candidateStrategy"
+                @change="changeCandidateStrategy"
+              >
+                <el-radio
+                  v-for="(dict, index) in 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="configForm.candidateStrategy == CandidateStrategy.ROLE"
+              label="指定角色"
+              prop="roleIds"
+            >
+              <el-select v-model="configForm.roleIds" 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="
+                configForm.candidateStrategy == CandidateStrategy.DEPT_MEMBER ||
+                configForm.candidateStrategy == CandidateStrategy.DEPT_LEADER ||
+                configForm.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER
+              "
+              label="指定部门"
+              prop="deptIds"
+              span="24"
+            >
+              <el-tree-select
+                ref="treeRef"
+                v-model="configForm.deptIds"
+                :data="deptTreeOptions"
+                :props="defaultProps"
+                empty-text="加载中,请稍后"
+                multiple
+                node-key="id"
+                :check-strictly="true"
+                style="width: 100%"
+                show-checkbox
+              />
+            </el-form-item>
+            <el-form-item
+              v-if="
+                configForm.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER ||
+                configForm.candidateStrategy == CandidateStrategy.START_USER_DEPT_LEADER ||
+                configForm.candidateStrategy == CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER
+              "
+              :label="deptLevelLabel!"
+              prop="deptLevel"
+              span="24"
+            >
+              <el-select v-model="configForm.deptLevel" clearable>
+                <el-option
+                  v-for="(item, index) in MULTI_LEVEL_DEPT"
+                  :key="index"
+                  :label="item.label"
+                  :value="item.value"
+                />
+              </el-select>
+            </el-form-item>
+            <el-form-item
+              v-if="configForm.candidateStrategy == CandidateStrategy.POST"
+              label="指定岗位"
+              prop="postIds"
+              span="24"
+            >
+              <el-select v-model="configForm.postIds" 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="configForm.candidateStrategy == CandidateStrategy.USER"
+              label="指定用户"
+              prop="userIds"
+              span="24"
+            >
+              <el-select
+                v-model="configForm.userIds"
+                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="configForm.candidateStrategy === CandidateStrategy.USER_GROUP"
+              label="指定用户组"
+              prop="userGroups"
+            >
+              <el-select v-model="configForm.userGroups" 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>
+            <!-- TODO @jason:后续要支持选择已经存好的表达式 -->
+            <el-form-item
+              v-if="configForm.candidateStrategy === CandidateStrategy.EXPRESSION"
+              label="流程表达式"
+              prop="expression"
+            >
+              <el-input
+                type="textarea"
+                v-model="configForm.expression"
+                clearable
+                style="width: 100%"
+              />
+            </el-form-item>
+            <el-form-item label="多人审批方式" prop="approveMethod">
+              <el-radio-group v-model="configForm.approveMethod" @change="approveMethodChanged">
+                <div class="flex-col">
+                  <div
+                    v-for="(item, index) in APPROVE_METHODS"
+                    :key="index"
+                    class="flex items-center"
+                  >
+                    <el-radio
+                      :value="item.value"
+                      :label="item.value"
+                      :disabled="
+                        item.value !== ApproveMethodType.RANDOM_SELECT_ONE_APPROVE &&
+                        notAllowedMultiApprovers
+                      "
+                    >
+                      {{ item.label }}
+                    </el-radio>
+                    <el-form-item prop="approveRatio">
+                      <el-input-number
+                        v-model="configForm.approveRatio"
+                        :min="10"
+                        :max="100"
+                        :step="10"
+                        size="small"
+                        v-if="
+                          item.value === ApproveMethodType.APPROVE_BY_RATIO &&
+                          configForm.approveMethod === ApproveMethodType.APPROVE_BY_RATIO
+                        "
+                      />
+                    </el-form-item>
+                  </div>
+                </div>
+              </el-radio-group>
+            </el-form-item>
+
+            <el-divider content-position="left">审批人拒绝时</el-divider>
+            <el-form-item prop="rejectHandlerType">
+              <el-radio-group v-model="configForm.rejectHandlerType">
+                <div class="flex-col">
+                  <div v-for="(item, index) in REJECT_HANDLER_TYPES" :key="index">
+                    <el-radio :key="item.value" :value="item.value" :label="item.label" />
+                  </div>
+                </div>
+              </el-radio-group>
+            </el-form-item>
+            <el-form-item
+              v-if="configForm.rejectHandlerType == RejectHandlerType.RETURN_USER_TASK"
+              label="驳回节点"
+              prop="returnNodeId"
+            >
+              <el-select v-model="configForm.returnNodeId" clearable style="width: 100%">
+                <el-option
+                  v-for="item in returnTaskList"
+                  :key="item.id"
+                  :label="item.name"
+                  :value="item.id"
+                />
+              </el-select>
+            </el-form-item>
+
+            <el-divider content-position="left">审批人超时未处理时</el-divider>
+            <el-form-item label="启用开关" prop="timeoutHandlerEnable">
+              <el-switch
+                v-model="configForm.timeoutHandlerEnable"
+                active-text="开启"
+                inactive-text="关闭"
+                @change="timeoutHandlerChange"
+              />
+            </el-form-item>
+            <el-form-item
+              label="执行动作"
+              prop="timeoutHandlerType"
+              v-if="configForm.timeoutHandlerEnable"
+            >
+              <el-radio-group
+                v-model="configForm.timeoutHandlerType"
+                @change="timeoutHandlerTypeChanged"
+              >
+                <el-radio-button
+                  v-for="item in TIMEOUT_HANDLER_TYPES"
+                  :key="item.value"
+                  :value="item.value"
+                  :label="item.label"
+                />
+              </el-radio-group>
+            </el-form-item>
+            <el-form-item label="超时时间设置" v-if="configForm.timeoutHandlerEnable">
+              <span class="mr-2">当超过</span>
+              <el-form-item prop="timeDuration">
+                <el-input-number
+                  class="mr-2"
+                  :style="{ width: '100px' }"
+                  v-model="configForm.timeDuration"
+                  :min="1"
+                  controls-position="right"
+                />
+              </el-form-item>
+              <el-select
+                v-model="timeUnit"
+                class="mr-2"
+                :style="{ width: '100px' }"
+                @change="timeUnitChange"
+              >
+                <el-option
+                  v-for="item in TIME_UNIT_TYPES"
+                  :key="item.value"
+                  :label="item.label"
+                  :value="item.value"
+                />
+              </el-select>
+              未处理
+            </el-form-item>
+            <el-form-item
+              label="最大提醒次数"
+              prop="maxRemindCount"
+              v-if="configForm.timeoutHandlerEnable && configForm.timeoutHandlerType === 1"
+            >
+              <el-input-number v-model="configForm.maxRemindCount" :min="1" :max="10" />
+            </el-form-item>
+
+            <el-divider content-position="left">审批人为空时</el-divider>
+            <el-form-item prop="assignEmptyHandlerType">
+              <el-radio-group v-model="configForm.assignEmptyHandlerType">
+                <div class="flex-col">
+                  <div v-for="(item, index) in ASSIGN_EMPTY_HANDLER_TYPES" :key="index">
+                    <el-radio :key="item.value" :value="item.value" :label="item.label" />
+                  </div>
+                </div>
+              </el-radio-group>
+            </el-form-item>
+            <el-form-item
+              v-if="configForm.assignEmptyHandlerType == AssignEmptyHandlerType.ASSIGN_USER"
+              label="指定用户"
+              prop="assignEmptyHandlerUserIds"
+              span="24"
+            >
+              <el-select
+                v-model="configForm.assignEmptyHandlerUserIds"
+                clearable
+                multiple
+                style="width: 100%"
+              >
+                <el-option
+                  v-for="item in userOptions"
+                  :key="item.id"
+                  :label="item.nickname"
+                  :value="item.id"
+                />
+              </el-select>
+            </el-form-item>
+
+            <el-divider content-position="left">审批人与提交人为同一人时</el-divider>
+            <el-form-item prop="assignStartUserHandlerType">
+              <el-radio-group v-model="configForm.assignStartUserHandlerType">
+                <div class="flex-col">
+                  <div v-for="(item, index) in ASSIGN_START_USER_HANDLER_TYPES" :key="index">
+                    <el-radio :key="item.value" :value="item.value" :label="item.label" />
+                  </div>
+                </div>
+              </el-radio-group>
+            </el-form-item>
+          </el-form>
+        </div>
+      </el-tab-pane>
+      <el-tab-pane label="操作按钮设置" name="buttons">
+        <div class="button-setting-pane">
+          <div class="button-setting-desc">操作按钮</div>
+          <div class="button-setting-title">
+            <div class="button-title-label">操作按钮</div>
+            <div class="pl-4 button-title-label">显示名称</div>
+            <div class="button-title-label">启用</div>
+          </div>
+          <div class="button-setting-item" v-for="(item, index) in buttonsSetting" :key="index">
+            <div class="button-setting-item-label"> {{ OPERATION_BUTTON_NAME.get(item.id) }} </div>
+            <div class="button-setting-item-label">
+              <input
+                type="text"
+                class="editable-title-input"
+                @blur="btnDisplayNameBlurEvent(index)"
+                v-mountedFocus
+                v-model="item.displayName"
+                :placeholder="item.displayName"
+                v-if="btnDisplayNameEdit[index]"
+              />
+              <el-button v-else text @click="changeBtnDisplayName(index)"
+                >{{ item.displayName }} &nbsp;<Icon icon="ep:edit"
+              /></el-button>
+            </div>
+            <div class="button-setting-item-label">
+              <el-switch v-model="item.enable" />
+            </div>
+          </div>
+        </div>
+      </el-tab-pane>
+      <el-tab-pane label="表单字段权限" name="fields" v-if="formType === 10">
+        <div class="field-setting-pane">
+          <div class="field-setting-desc">字段权限</div>
+          <div class="field-permit-title">
+            <div class="setting-title-label first-title"> 字段名称 </div>
+            <div class="other-titles">
+              <span class="setting-title-label">只读</span>
+              <span class="setting-title-label">可编辑</span>
+              <span class="setting-title-label">隐藏</span>
+            </div>
+          </div>
+          <div
+            class="field-setting-item"
+            v-for="(item, index) in fieldsPermissionConfig"
+            :key="index"
+          >
+            <div class="field-setting-item-label"> {{ item.title }} </div>
+            <el-radio-group class="field-setting-item-group" v-model="item.permission">
+              <div class="item-radio-wrap">
+                <el-radio
+                  :value="FieldPermissionType.READ"
+                  size="large"
+                  :label="FieldPermissionType.READ"
+                  ><span></span
+                ></el-radio>
+              </div>
+              <div class="item-radio-wrap">
+                <el-radio
+                  :value="FieldPermissionType.WRITE"
+                  size="large"
+                  :label="FieldPermissionType.WRITE"
+                  ><span></span
+                ></el-radio>
+              </div>
+              <div class="item-radio-wrap">
+                <el-radio
+                  :value="FieldPermissionType.NONE"
+                  size="large"
+                  :label="FieldPermissionType.NONE"
+                  ><span></span
+                ></el-radio>
+              </div>
+            </el-radio-group>
+          </div>
+        </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_TYPE,
+  ApproveType,
+  APPROVE_METHODS,
+  CandidateStrategy,
+  NodeType,
+  ApproveMethodType,
+  TimeUnitType,
+  RejectHandlerType,
+  TIMEOUT_HANDLER_TYPES,
+  TIME_UNIT_TYPES,
+  REJECT_HANDLER_TYPES,
+  DEFAULT_BUTTON_SETTING,
+  OPERATION_BUTTON_NAME,
+  ButtonSetting,
+  MULTI_LEVEL_DEPT,
+  CANDIDATE_STRATEGY,
+  ASSIGN_START_USER_HANDLER_TYPES,
+  TimeoutHandlerType,
+  ASSIGN_EMPTY_HANDLER_TYPES,
+  AssignEmptyHandlerType,
+  FieldPermissionType
+} from '../consts'
+
+import {
+  useWatchNode,
+  useNodeName,
+  useFormFieldsPermission,
+  useNodeForm,
+  UserTaskFormType,
+  useDrawer
+} from '../node'
+import { defaultProps } from '@/utils/tree'
+import { cloneDeep } from 'lodash-es'
+import { convertTimeUnit, getApproveTypeText } from '../utils'
+defineOptions({
+  name: 'UserTaskNodeConfig'
+})
+const props = defineProps({
+  flowNode: {
+    type: Object as () => SimpleFlowNode,
+    required: true
+  }
+})
+const emits = defineEmits<{
+  'find:returnTaskNodes': [nodeList: SimpleFlowNode[]]
+}>()
+const deptLevelLabel = computed(() => {
+  let label = '部门负责人来源'
+  if (configForm.value.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER) {
+    label = label + '(指定部门向上)'
+  } else {
+    label = label + '(发起人部门向上)'
+  }
+  return label
+})
+// 监控节点的变化
+const currentNode = useWatchNode(props)
+// 抽屉配置
+const { settingVisible, closeDrawer, openDrawer } = useDrawer()
+// 节点名称配置
+const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.USER_TASK_NODE)
+// 激活的 Tab 标签页
+const activeTabName = ref('user')
+// 表单字段权限设置
+const { formType, fieldsPermissionConfig, getNodeConfigFormFields } = useFormFieldsPermission(
+  FieldPermissionType.READ
+)
+// 操作按钮设置
+const { buttonsSetting, btnDisplayNameEdit, changeBtnDisplayName, btnDisplayNameBlurEvent } =
+  useButtonsSetting()
+const approveType = ref(ApproveType.USER)
+// 审批人表单设置
+const formRef = ref() // 表单 Ref
+// 表单校验规则
+const formRules = reactive({
+  candidateStrategy: [{ required: true, message: '审批人设置不能为空', trigger: 'change' }],
+  userIds: [{ required: true, message: '用户不能为空', trigger: 'change' }],
+  roleIds: [{ required: true, message: '角色不能为空', trigger: 'change' }],
+  deptIds: [{ required: true, message: '部门不能为空', trigger: 'change' }],
+  userGroups: [{ required: true, message: '用户组不能为空', trigger: 'change' }],
+  postIds: [{ required: true, message: '岗位不能为空', trigger: 'change' }],
+  expression: [{ required: true, message: '流程表达式不能为空', trigger: 'blur' }],
+  approveMethod: [{ required: true, message: '多人审批方式不能为空', trigger: 'change' }],
+  approveRatio: [{ required: true, message: '通过比例不能为空', trigger: 'blur' }],
+  returnNodeId: [{ required: true, message: '驳回节点不能为空', trigger: 'change' }],
+  timeoutHandlerEnable: [{ required: true }],
+  timeoutHandlerType: [{ required: true }],
+  timeDuration: [{ required: true, message: '超时时间不能为空', trigger: 'blur' }],
+  maxRemindCount: [{ required: true, message: '提醒次数不能为空', trigger: 'blur' }],
+  assignEmptyHandlerType: [{ required: true }],
+  assignEmptyHandlerUserIds: [{ required: true, message: '用户不能为空', trigger: 'change' }],
+  assignStartUserHandlerType: [{ required: true }]
+})
+
+const {
+  configForm: tempConfigForm,
+  roleOptions,
+  postOptions,
+  userOptions,
+  userGroupOptions,
+  deptTreeOptions,
+  handleCandidateParam,
+  parseCandidateParam,
+  getShowText
+} = useNodeForm(NodeType.USER_TASK_NODE)
+const configForm = tempConfigForm as Ref<UserTaskFormType>
+// 不允许多人审批
+const notAllowedMultiApprovers = ref(false)
+// 改变审批人设置策略
+const changeCandidateStrategy = () => {
+  configForm.value.userIds = []
+  configForm.value.deptIds = []
+  configForm.value.roleIds = []
+  configForm.value.postIds = []
+  configForm.value.userGroups = []
+  configForm.value.deptLevel = 1
+  configForm.value.approveMethod = ApproveMethodType.SEQUENTIAL_APPROVE
+  if (
+    configForm.value.candidateStrategy === CandidateStrategy.START_USER ||
+    configForm.value.candidateStrategy === CandidateStrategy.USER
+  ) {
+    notAllowedMultiApprovers.value = true
+  } else {
+    notAllowedMultiApprovers.value = false
+  }
+}
+// 改变审批候选人
+const changedCandidateUsers = () => {
+  if (
+    configForm.value.userIds &&
+    configForm.value.userIds?.length <= 1 &&
+    configForm.value.candidateStrategy === CandidateStrategy.USER
+  ) {
+    configForm.value.approveMethod = ApproveMethodType.RANDOM_SELECT_ONE_APPROVE
+    configForm.value.rejectHandlerType = RejectHandlerType.FINISH_PROCESS
+    notAllowedMultiApprovers.value = true
+  } else {
+    notAllowedMultiApprovers.value = false
+  }
+}
+// 审批方式改变
+const approveMethodChanged = () => {
+  configForm.value.rejectHandlerType = RejectHandlerType.FINISH_PROCESS
+  if (configForm.value.approveMethod === ApproveMethodType.APPROVE_BY_RATIO) {
+    configForm.value.approveRatio = 100
+  }
+  formRef.value.clearValidate('approveRatio')
+}
+// 审批拒绝 可回退的节点
+const returnTaskList = ref<SimpleFlowNode[]>([])
+// 审批人超时未处理设置
+const {
+  timeoutHandlerChange,
+  cTimeoutType,
+  timeoutHandlerTypeChanged,
+  timeUnit,
+  timeUnitChange,
+  isoTimeDuration,
+  cTimeoutMaxRemindCount
+} = useTimeoutHandler()
+
+// 保存配置
+const saveConfig = async () => {
+  activeTabName.value = 'user'
+  // 设置审批节点名称
+  currentNode.value.name = nodeName.value!
+  // 设置审批类型
+  currentNode.value.approveType = approveType.value
+  // 如果不是人工审批。返回
+  if (approveType.value !== ApproveType.USER) {
+    currentNode.value.showText = getApproveTypeText(approveType.value)
+    settingVisible.value = false
+    return true
+  }
+
+  if (!formRef) return false
+  const valid = await formRef.value.validate()
+  if (!valid) return false
+  const showText = getShowText()
+  if (!showText) return false
+
+  currentNode.value.candidateStrategy = configForm.value.candidateStrategy
+  // 处理 candidateParam 参数
+  currentNode.value.candidateParam = handleCandidateParam()
+  // 设置审批方式
+  currentNode.value.approveMethod = configForm.value.approveMethod
+  if (configForm.value.approveMethod === ApproveMethodType.APPROVE_BY_RATIO) {
+    currentNode.value.approveRatio = configForm.value.approveRatio
+  }
+  // 设置拒绝处理
+  currentNode.value.rejectHandler = {
+    type: configForm.value.rejectHandlerType!,
+    returnNodeId: configForm.value.returnNodeId
+  }
+  // 设置超时处理
+  currentNode.value.timeoutHandler = {
+    enable: configForm.value.timeoutHandlerEnable!,
+    type: cTimeoutType.value,
+    timeDuration: isoTimeDuration.value,
+    maxRemindCount: cTimeoutMaxRemindCount.value
+  }
+  // 设置审批人为空时
+  currentNode.value.assignEmptyHandler = {
+    type: configForm.value.assignEmptyHandlerType!,
+    userIds:
+      configForm.value.assignEmptyHandlerType === AssignEmptyHandlerType.ASSIGN_USER
+        ? configForm.value.assignEmptyHandlerUserIds
+        : undefined
+  }
+  // 设置审批人与发起人相同时
+  currentNode.value.assignStartUserHandlerType = configForm.value.assignStartUserHandlerType
+  // 设置表单权限
+  currentNode.value.fieldsPermission = fieldsPermissionConfig.value
+  // 设置按钮权限
+  currentNode.value.buttonsSetting = buttonsSetting.value
+
+  currentNode.value.showText = showText
+  settingVisible.value = false
+  return true
+}
+
+// 显示审批节点配置, 由父组件传过来
+const showUserTaskNodeConfig = (node: SimpleFlowNode) => {
+  nodeName.value = node.name
+  // 1 审批类型
+  approveType.value = node.approveType ? node.approveType : ApproveType.USER
+  // 如果审批类型不是人工审批返回
+  if (approveType.value !== ApproveType.USER) {
+    return
+  }
+
+  //2.1 审批人设置
+  configForm.value.candidateStrategy = node.candidateStrategy!
+  // 解析候选人参数
+  parseCandidateParam(node.candidateStrategy!, node?.candidateParam)
+  if (configForm.value.userIds && configForm.value.userIds.length > 1) {
+    notAllowedMultiApprovers.value = true
+  } else {
+    notAllowedMultiApprovers.value = false
+  }
+  // 2.2 设置审批方式
+  configForm.value.approveMethod = node.approveMethod!
+  if (node.approveMethod == ApproveMethodType.APPROVE_BY_RATIO) {
+    configForm.value.approveRatio = node.approveRatio!
+  }
+  // 2.3 设置审批拒绝处理
+  configForm.value.rejectHandlerType = node.rejectHandler!.type
+  configForm.value.returnNodeId = node.rejectHandler?.returnNodeId
+  const matchNodeList = []
+  emits('find:returnTaskNodes', matchNodeList)
+  returnTaskList.value = matchNodeList
+  // 2.4 设置审批超时处理
+  configForm.value.timeoutHandlerEnable = node.timeoutHandler!.enable
+  if (node.timeoutHandler?.enable && node.timeoutHandler?.timeDuration) {
+    const strTimeDuration = node.timeoutHandler.timeDuration
+    let parseTime = strTimeDuration.slice(2, strTimeDuration.length - 1)
+    let parseTimeUnit = strTimeDuration.slice(strTimeDuration.length - 1)
+    configForm.value.timeDuration = parseInt(parseTime)
+    timeUnit.value = convertTimeUnit(parseTimeUnit)
+  }
+  configForm.value.timeoutHandlerType = node.timeoutHandler?.type
+  configForm.value.maxRemindCount = node.timeoutHandler?.maxRemindCount
+  // 2.5 设置审批人为空时
+  configForm.value.assignEmptyHandlerType = node.assignEmptyHandler?.type
+  configForm.value.assignEmptyHandlerUserIds = node.assignEmptyHandler?.userIds
+  // 2.6 设置用户任务的审批人与发起人相同时
+  configForm.value.assignStartUserHandlerType = node.assignStartUserHandlerType
+  // 3. 操作按钮设置
+  buttonsSetting.value = cloneDeep(node.buttonsSetting) || DEFAULT_BUTTON_SETTING
+  // 4. 表单字段权限配置
+  getNodeConfigFormFields(node.fieldsPermission)
+}
+
+defineExpose({ openDrawer, showUserTaskNodeConfig }) // 暴露方法给父组件
+
+/**
+ * @description 操作按钮设置
+ */
+function useButtonsSetting() {
+  const buttonsSetting = ref<ButtonSetting[]>()
+  // 操作按钮显示名称可编辑
+  const btnDisplayNameEdit = ref<boolean[]>([])
+  const changeBtnDisplayName = (index: number) => {
+    btnDisplayNameEdit.value[index] = true
+  }
+  const btnDisplayNameBlurEvent = (index: number) => {
+    btnDisplayNameEdit.value[index] = false
+    const buttonItem = buttonsSetting.value![index]
+    buttonItem.displayName = buttonItem.displayName || OPERATION_BUTTON_NAME.get(buttonItem.id)!
+  }
+  return {
+    buttonsSetting,
+    btnDisplayNameEdit,
+    changeBtnDisplayName,
+    btnDisplayNameBlurEvent
+  }
+}
+
+/**
+ * @description 审批人超时未处理配置
+ */
+function useTimeoutHandler() {
+  // 时间单位
+  const timeUnit = ref(TimeUnitType.HOUR)
+
+  // 超时开关改变
+  const timeoutHandlerChange = () => {
+    if (configForm.value.timeoutHandlerEnable) {
+      timeUnit.value = 2
+      configForm.value.timeDuration = 6
+      configForm.value.timeoutHandlerType = 1
+      configForm.value.maxRemindCount = 1
+    }
+  }
+  // 超时执行的动作
+  const cTimeoutType = computed(() => {
+    if (!configForm.value.timeoutHandlerEnable) {
+      return undefined
+    }
+    return configForm.value.timeoutHandlerType
+  })
+
+  // 超时处理动作改变
+  const timeoutHandlerTypeChanged = () => {
+    if (configForm.value.timeoutHandlerType === TimeoutHandlerType.REMINDER) {
+      configForm.value.maxRemindCount = 1 // 超时提醒次数,默认为1
+    }
+  }
+
+  // 时间单位改变
+  const timeUnitChange = () => {
+    // 分钟,默认是 60 分钟
+    if (timeUnit.value === TimeUnitType.MINUTE) {
+      configForm.value.timeDuration = 60
+    }
+    // 小时,默认是 6 个小时
+    if (timeUnit.value === TimeUnitType.HOUR) {
+      configForm.value.timeDuration = 6
+    }
+    // 天, 默认 1天
+    if (timeUnit.value === TimeUnitType.DAY) {
+      configForm.value.timeDuration = 1
+    }
+  }
+  // 超时时间的 ISO 表示
+  const isoTimeDuration = computed(() => {
+    if (!configForm.value.timeoutHandlerEnable) {
+      return undefined
+    }
+    let strTimeDuration = 'PT'
+    if (timeUnit.value === TimeUnitType.MINUTE) {
+      strTimeDuration += configForm.value.timeDuration + 'M'
+    }
+    if (timeUnit.value === TimeUnitType.HOUR) {
+      strTimeDuration += configForm.value.timeDuration + 'H'
+    }
+    if (timeUnit.value === TimeUnitType.DAY) {
+      strTimeDuration += configForm.value.timeDuration + 'D'
+    }
+    return strTimeDuration
+  })
+
+  // 超时最大提醒次数
+  const cTimeoutMaxRemindCount = computed(() => {
+    if (!configForm.value.timeoutHandlerEnable) {
+      return undefined
+    }
+    if (configForm.value.timeoutHandlerType !== TimeoutHandlerType.REMINDER) {
+      return undefined
+    }
+    return configForm.value.maxRemindCount
+  })
+
+  return {
+    timeoutHandlerChange,
+    cTimeoutType,
+    timeoutHandlerTypeChanged,
+    timeUnit,
+    timeUnitChange,
+    isoTimeDuration,
+    cTimeoutMaxRemindCount
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.button-setting-pane {
+  display: flex;
+  flex-direction: column;
+  font-size: 14px;
+
+  .button-setting-desc {
+    padding-right: 8px;
+    margin-bottom: 16px;
+    font-size: 16px;
+    font-weight: 700;
+  }
+
+  .button-setting-title {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    height: 45px;
+    padding-left: 12px;
+    background-color: #f8fafc0a;
+    border: 1px solid #1f38581a;
+
+    & > :first-child {
+      width: 100px !important;
+      text-align: left !important;
+    }
+
+    & > :last-child {
+      text-align: center !important;
+    }
+
+    .button-title-label {
+      width: 150px;
+      font-size: 13px;
+      font-weight: 700;
+      color: #000;
+      text-align: left;
+    }
+  }
+
+  .button-setting-item {
+    align-items: center;
+    display: flex;
+    justify-content: space-between;
+    height: 38px;
+    padding-left: 12px;
+    border: 1px solid #1f38581a;
+    border-top: 0;
+
+    & > :first-child {
+      width: 100px !important;
+    }
+
+    & > :last-child {
+      text-align: center !important;
+    }
+
+    .button-setting-item-label {
+      width: 150px;
+      overflow: hidden;
+      text-align: left;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+
+    .editable-title-input {
+      height: 24px;
+      max-width: 130px;
+      margin-left: 4px;
+      line-height: 24px;
+      border: 1px solid #d9d9d9;
+      border-radius: 4px;
+      transition: all 0.3s;
+
+      &:focus {
+        border-color: #40a9ff;
+        outline: 0;
+        box-shadow: 0 0 0 2px rgb(24 144 255 / 20%);
+      }
+    }
+  }
+}
+</style>

+ 79 - 0
src/components/SimpleProcessDesignerV2/src/nodes/CopyTaskNode.vue

@@ -0,0 +1,79 @@
+<template>
+  <div class="node-wrapper">
+    <div class="node-container">
+      <div class="node-box" :class="{ 'node-config-error': !currentNode.showText }">
+        <div class="node-title-container">
+          <div class="node-title-icon copy-task"><span class="iconfont icon-copy"></span></div>
+          <input
+            v-if="showInput"
+            type="text"
+            class="editable-title-input"
+            @blur="blurEvent()"
+            v-mountedFocus
+            v-model="currentNode.name"
+            :placeholder="currentNode.name"
+          />
+          <div v-else class="node-title" @click="clickTitle">
+            {{ currentNode.name }}
+          </div>
+        </div>
+        <div class="node-content" @click="openNodeConfig">
+          <div class="node-text" :title="currentNode.showText" v-if="currentNode.showText">
+            {{ currentNode.showText }}
+          </div>
+          <div class="node-text" v-else>
+            {{ NODE_DEFAULT_TEXT.get(NodeType.COPY_TASK_NODE) }}
+          </div>
+          <Icon icon="ep:arrow-right-bold" />
+        </div>
+        <div class="node-toolbar">
+          <div class="toolbar-icon"
+            ><Icon color="#0089ff" icon="ep:circle-close-filled" :size="18" @click="deleteNode"
+          /></div>
+        </div>
+      </div>
+
+      <!-- 传递子节点给添加节点组件。会在子节点前面添加节点 -->
+      <NodeHandler v-if="currentNode" v-model:child-node="currentNode.childNode" />
+    </div>
+    <CopyTaskNodeConfig v-if="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 } from '../node'
+import CopyTaskNodeConfig from '../nodes-config/CopyTaskNodeConfig.vue'
+defineOptions({
+  name: 'CopyTaskNode'
+})
+const props = defineProps({
+  flowNode: {
+    type: Object as () => SimpleFlowNode,
+    required: true
+  }
+})
+// 定义事件,更新父组件。
+const emits = defineEmits<{
+  'update:flowNode': [node: SimpleFlowNode | undefined]
+}>()
+
+// 监控节点的变化
+const currentNode = useWatchNode(props)
+// 节点名称编辑
+const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.COPY_TASK_NODE)
+
+const nodeSetting = ref()
+// 打开节点配置
+const openNodeConfig = () => {
+  nodeSetting.value.showCopyTaskNodeConfig(currentNode.value)
+  nodeSetting.value.openDrawer()
+}
+
+// 删除节点。更新当前节点为孩子节点
+const deleteNode = () => {
+  emits('update:flowNode', currentNode.value.childNode)
+}
+</script>
+
+<style lang="scss" scoped></style>

+ 13 - 0
src/components/SimpleProcessDesignerV2/src/nodes/EndEventNode.vue

@@ -0,0 +1,13 @@
+<template>
+  <div class="end-node-wrapper">
+    <div class="end-node-box">
+      <span class="node-fixed-name" title="结束">结束</span>
+    </div>
+  </div>
+</template>
+<script setup lang="ts">
+defineOptions({
+  name: 'EndEventNode'
+})
+</script>
+<style lang="scss" scoped></style>

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

@@ -0,0 +1,207 @@
+<template>
+  <div class="branch-node-wrapper">
+    <div class="branch-node-container">
+      <div class="branch-node-add" @click="addCondition">添加条件</div>
+      <div
+        class="branch-node-item"
+        v-for="(item, index) in currentNode.conditionNodes"
+        :key="index"
+      >
+        <template v-if="index == 0">
+          <div class="branch-line-first-top"> </div>
+          <div class="branch-line-first-bottom"></div>
+        </template>
+        <template v-if="index + 1 == currentNode.conditionNodes?.length">
+          <div class="branch-line-last-top"></div>
+          <div class="branch-line-last-bottom"></div>
+        </template>
+        <div class="node-wrapper">
+          <div class="node-container">
+            <div class="node-box" :class="{ 'node-config-error': !item.showText }">
+              <div class="branch-node-title-container">
+                <div v-if="showInputs[index]">
+                  <input
+                    type="text"
+                    class="input-max-width editable-title-input"
+                    @blur="blurEvent(index)"
+                    v-mountedFocus
+                    v-model="item.name"
+                  />
+                </div>
+                <div v-else class="branch-title" @click="clickEvent(index)"> {{ item.name }} </div>
+                <div class="branch-priority"> 优先级{{ index + 1 }} </div>
+              </div>
+              <div class="branch-node-content" @click="conditionNodeConfig(item.id)">
+                <div class="branch-node-text" :title="item.showText" v-if="item.showText">
+                  {{ item.showText }}
+                </div>
+                <div class="branch-node-text" v-else>
+                  {{ NODE_DEFAULT_TEXT.get(NodeType.CONDITION_NODE) }}
+                </div>
+              </div>
+              <div class="node-toolbar" v-if="index + 1 !== currentNode.conditionNodes?.length">
+                <div class="toolbar-icon">
+                  <Icon
+                    color="#0089ff"
+                    icon="ep:circle-close-filled"
+                    :size="18"
+                    @click="deleteCondition(index)"
+                  />
+                </div>
+              </div>
+              <div
+                class="branch-node-move move-node-left"
+                v-if="index != 0 && index + 1 !== currentNode.conditionNodes?.length"
+                @click="moveNode(index, -1)"
+              >
+                <Icon icon="ep:arrow-left" />
+              </div>
+
+              <div
+                class="branch-node-move move-node-right"
+                v-if="currentNode.conditionNodes && index < currentNode.conditionNodes.length - 2"
+                @click="moveNode(index, 1)"
+              >
+                <Icon icon="ep:arrow-right" />
+              </div>
+            </div>
+            <NodeHandler v-model:child-node="item.childNode" />
+          </div>
+        </div>
+        <ConditionNodeConfig :node-index="index" :condition-node="item" :ref="item.id" />
+        <!-- 递归显示子节点  -->
+        <ProcessNodeTree
+          v-if="item && item.childNode"
+          :parent-node="item"
+          v-model:flow-node="item.childNode"
+          @find:recursive-find-parent-node="recursiveFindParentNode"
+        />
+      </div>
+    </div>
+    <NodeHandler v-if="currentNode" v-model:child-node="currentNode.childNode" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import NodeHandler from '../NodeHandler.vue'
+import ProcessNodeTree from '../ProcessNodeTree.vue'
+import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
+import { getDefaultConditionNodeName } from '../utils'
+import { generateUUID } from '@/utils'
+import ConditionNodeConfig from '../nodes-config/ConditionNodeConfig.vue'
+const { proxy } = getCurrentInstance() as any
+defineOptions({
+  name: 'ExclusiveNode'
+})
+const props = defineProps({
+  // parentNode : {
+  //   type: Object as () => SimpleFlowNode,
+  //   required: true
+  // },
+  flowNode: {
+    type: Object as () => SimpleFlowNode,
+    required: true
+  }
+})
+// 定义事件,更新父组件
+const emits = defineEmits<{
+  'update:modelValue': [node: SimpleFlowNode | undefined]
+  'find:parentNode': [nodeList: SimpleFlowNode[], nodeType: number]
+  'find:recursiveFindParentNode': [
+    nodeList: SimpleFlowNode[],
+    curentNode: SimpleFlowNode,
+    nodeType: number
+  ]
+}>()
+
+const currentNode = ref<SimpleFlowNode>(props.flowNode)
+// const conditionNodes = computed(() => currentNode.value.conditionNodes);
+
+watch(
+  () => props.flowNode,
+  (newValue) => {
+    currentNode.value = newValue
+  }
+)
+
+const showInputs = ref<boolean[]>([])
+// 失去焦点
+const blurEvent = (index: number) => {
+  showInputs.value[index] = false
+  const conditionNode = currentNode.value.conditionNodes?.at(index) as SimpleFlowNode
+  conditionNode.name =
+    conditionNode.name || getDefaultConditionNodeName(index, conditionNode.defaultFlow)
+}
+
+// 点击条件名称
+const clickEvent = (index: number) => {
+  showInputs.value[index] = true
+}
+
+const conditionNodeConfig = (nodeId: string) => {
+  const conditionNode = proxy.$refs[nodeId][0]
+  conditionNode.open()
+}
+
+// 新增条件
+const addCondition = () => {
+  const conditionNodes = currentNode.value.conditionNodes
+  if (conditionNodes) {
+    const len = conditionNodes.length
+    let lastIndex = len - 1
+    const conditionData: SimpleFlowNode = {
+      id: 'Flow_' + generateUUID(),
+      name: '条件' + len,
+      showText: '',
+      type: NodeType.CONDITION_NODE,
+      childNode: undefined,
+      conditionNodes: [],
+      conditionType: 1,
+      defaultFlow: false
+    }
+    conditionNodes.splice(lastIndex, 0, conditionData)
+  }
+}
+
+// 删除条件
+const deleteCondition = (index: number) => {
+  const conditionNodes = currentNode.value.conditionNodes
+  if (conditionNodes) {
+    conditionNodes.splice(index, 1)
+    if (conditionNodes.length == 1) {
+      const childNode = currentNode.value.childNode
+      // 更新此节点为后续孩子节点
+      emits('update:modelValue', childNode)
+    }
+  }
+}
+
+// 移动节点
+const moveNode = (index: number, to: number) => {
+  // -1 :向左  1: 向右
+  if (currentNode.value.conditionNodes) {
+    currentNode.value.conditionNodes[index] = currentNode.value.conditionNodes.splice(
+      index + to,
+      1,
+      currentNode.value.conditionNodes[index]
+    )[0]
+  }
+}
+// 递归从父节点中查询匹配的节点
+const recursiveFindParentNode = (
+  nodeList: SimpleFlowNode[],
+  node: SimpleFlowNode,
+  nodeType: number
+) => {
+  if (!node || node.type === NodeType.START_EVENT_NODE) {
+    return
+  }
+  if (node.type === nodeType) {
+    nodeList.push(node)
+  }
+  // 条件节点 (NodeType.CONDITION_NODE) 比较特殊。需要调用其父节点条件分支节点(NodeType.EXCLUSIVE_NODE) 继续查找
+  emits('find:parentNode', nodeList, nodeType)
+}
+</script>
+
+<style lang="scss" scoped></style>

+ 181 - 0
src/components/SimpleProcessDesignerV2/src/nodes/ParallelNode.vue

@@ -0,0 +1,181 @@
+<template>
+  <div class="branch-node-wrapper">
+    <div class="branch-node-container">
+      <div class="branch-node-add" @click="addCondition">添加分支</div>
+      <div
+        class="branch-node-item"
+        v-for="(item, index) in currentNode.conditionNodes"
+        :key="index"
+      >
+        <template v-if="index == 0">
+          <div class="branch-line-first-top"></div>
+          <div class="branch-line-first-bottom"></div>
+        </template>
+        <template v-if="index + 1 == currentNode.conditionNodes?.length">
+          <div class="branch-line-last-top"></div>
+          <div class="branch-line-last-bottom"></div>
+        </template>
+        <div class="node-wrapper">
+          <div class="node-container">
+            <div class="node-box">
+              <div class="branch-node-title-container">
+                <div v-if="showInputs[index]">
+                  <input
+                    type="text"
+                    class="input-max-width editable-title-input"
+                    @blur="blurEvent(index)"
+                    v-mountedFocus
+                    v-model="item.name"
+                  />
+                </div>
+                <div v-else class="branch-title" @click="clickEvent(index)"> {{ item.name }} </div>
+                <div class="branch-priority">无优先级</div>
+              </div>
+              <div class="branch-node-content" @click="conditionNodeConfig(item.id)">
+                <div class="branch-node-text" :title="item.showText" v-if="item.showText">
+                  {{ item.showText }}
+                </div>
+                <div class="branch-node-text" v-else>
+                  {{ NODE_DEFAULT_TEXT.get(NodeType.CONDITION_NODE) }}
+                </div>
+              </div>
+              <div class="node-toolbar">
+                <div class="toolbar-icon">
+                  <Icon
+                    color="#0089ff"
+                    icon="ep:circle-close-filled"
+                    :size="18"
+                    @click="deleteCondition(index)"
+                  />
+                </div>
+              </div>
+              <!-- <div 
+                class="branch-node-move move-node-left"
+                v-if="index != 0 && index + 1 !== currentNode.conditionNodes?.length" @click="moveNode(index, -1)">
+                <Icon icon="ep:arrow-left" />
+              </div> -->
+
+              <!-- <div 
+                class="branch-node-move move-node-right"
+                v-if="currentNode.conditionNodes && index < currentNode.conditionNodes.length - 2"
+                @click="moveNode(index, 1)">
+                <Icon icon="ep:arrow-right" />
+              </div> -->
+            </div>
+            <NodeHandler v-model:child-node="item.childNode" />
+          </div>
+        </div>
+        <!-- 递归显示子节点  -->
+        <ProcessNodeTree
+          v-if="item && item.childNode"
+          :parent-node="item"
+          v-model:flow-node="item.childNode"
+          @find:recursive-find-parent-node="recursiveFindParentNode"
+        />
+      </div>
+    </div>
+    <NodeHandler v-if="currentNode" v-model:child-node="currentNode.childNode" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import NodeHandler from '../NodeHandler.vue'
+import ProcessNodeTree from '../ProcessNodeTree.vue'
+import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
+import { generateUUID } from '@/utils'
+
+const { proxy } = getCurrentInstance() as any
+defineOptions({
+  name: 'ParallelNode'
+})
+const props = defineProps({
+  flowNode: {
+    type: Object as () => SimpleFlowNode,
+    required: true
+  }
+})
+// 定义事件,更新父组件
+const emits = defineEmits<{
+  'update:modelValue': [node: SimpleFlowNode | undefined]
+  'find:parentNode': [nodeList: SimpleFlowNode[], nodeType: number]
+  'find:recursiveFindParentNode': [
+    nodeList: SimpleFlowNode[],
+    curentNode: SimpleFlowNode,
+    nodeType: number
+  ]
+}>()
+
+const currentNode = ref<SimpleFlowNode>(props.flowNode)
+
+watch(
+  () => props.flowNode,
+  (newValue) => {
+    currentNode.value = newValue
+  }
+)
+
+const showInputs = ref<boolean[]>([])
+// 失去焦点
+const blurEvent = (index: number) => {
+  showInputs.value[index] = false
+  const conditionNode = currentNode.value.conditionNodes?.at(index) as SimpleFlowNode
+  conditionNode.name = conditionNode.name || `并行${index + 1}`
+}
+
+// 点击条件名称
+const clickEvent = (index: number) => {
+  showInputs.value[index] = true
+}
+
+const conditionNodeConfig = (nodeId: string) => {
+  const conditionNode = proxy.$refs[nodeId][0]
+  conditionNode.open()
+}
+
+// 新增条件
+const addCondition = () => {
+  const conditionNodes = currentNode.value.conditionNodes
+  if (conditionNodes) {
+    const len = conditionNodes.length
+    let lastIndex = len - 1
+    const conditionData: SimpleFlowNode = {
+      id: 'Flow_' + generateUUID(),
+      name: '并行' + len,
+      showText: '无需配置条件同时执行',
+      type: NodeType.CONDITION_NODE,
+      childNode: undefined,
+      conditionNodes: []
+    }
+    conditionNodes.splice(lastIndex, 0, conditionData)
+  }
+}
+
+// 删除条件
+const deleteCondition = (index: number) => {
+  const conditionNodes = currentNode.value.conditionNodes
+  if (conditionNodes) {
+    conditionNodes.splice(index, 1)
+    if (conditionNodes.length == 1) {
+      const childNode = currentNode.value.childNode
+      // 更新此节点为后续孩子节点
+      emits('update:modelValue', childNode)
+    }
+  }
+}
+
+// 递归从父节点中查询匹配的节点
+const recursiveFindParentNode = (
+  nodeList: SimpleFlowNode[],
+  node: SimpleFlowNode,
+  nodeType: number
+) => {
+  if (!node || node.type === NodeType.START_EVENT_NODE) {
+    return
+  }
+  if (node.type === nodeType) {
+    nodeList.push(node)
+  }
+  // 条件节点 (NodeType.CONDITION_NODE) 比较特殊。需要调用其父节点并行节点(NodeType.PARALLEL_NODE) 继续查找
+  emits('find:parentNode', nodeList, nodeType)
+}
+</script>

+ 69 - 0
src/components/SimpleProcessDesignerV2/src/nodes/StartUserNode.vue

@@ -0,0 +1,69 @@
+<template>
+  <div class="node-wrapper">
+    <div class="node-container">
+      <div class="node-box" :class="{ 'node-config-error': !currentNode.showText }">
+        <div class="node-title-container">
+          <div class="node-title-icon start-user"
+            ><span class="iconfont icon-start-user"></span
+          ></div>
+          <input
+            v-if="showInput"
+            type="text"
+            class="editable-title-input"
+            @blur="blurEvent()"
+            v-mountedFocus
+            v-model="currentNode.name"
+            :placeholder="currentNode.name"
+          />
+          <div v-else class="node-title" @click="clickTitle">
+            {{ currentNode.name }}
+          </div>
+        </div>
+        <div class="node-content" @click="openNodeConfig">
+          <div class="node-text" :title="currentNode.showText" v-if="currentNode.showText">
+            {{ currentNode.showText }}
+          </div>
+          <div class="node-text" v-else>
+            {{ NODE_DEFAULT_TEXT.get(NodeType.START_USER_NODE) }}
+          </div>
+          <Icon icon="ep:arrow-right-bold" />
+        </div>
+      </div>
+      <!-- 传递子节点给添加节点组件。会在子节点前面添加节点 -->
+      <NodeHandler v-if="currentNode" v-model:child-node="currentNode.childNode" />
+    </div>
+  </div>
+  <StartUserNodeConfig v-if="currentNode" ref="nodeSetting" :flow-node="currentNode" />
+</template>
+<script setup lang="ts">
+import NodeHandler from '../NodeHandler.vue'
+import { useWatchNode, useNodeName2 } from '../node'
+import { SimpleFlowNode, NODE_DEFAULT_TEXT, NodeType } from '../consts'
+import StartUserNodeConfig from '../nodes-config/StartUserNodeConfig.vue'
+defineOptions({
+  name: 'StartEventNode'
+})
+const props = defineProps({
+  flowNode: {
+    type: Object as () => SimpleFlowNode,
+    default: () => null
+  }
+})
+// 定义事件,更新父组件。
+const emits = defineEmits<{
+  'update:modelValue': [node: SimpleFlowNode | undefined]
+}>()
+// 监控节点变化
+const currentNode = useWatchNode(props)
+// 节点名称编辑
+const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.START_USER_NODE)
+
+const nodeSetting = ref()
+// 打开节点配置
+const openNodeConfig = () => {
+  // 把当前节点传递给配置组件
+  nodeSetting.value.showStartUserNodeConfig(currentNode.value)
+  nodeSetting.value.openDrawer()
+}
+</script>
+<style lang="scss" scoped></style>

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

@@ -0,0 +1,88 @@
+<template>
+  <div class="node-wrapper">
+    <div class="node-container">
+      <div class="node-box" :class="{ 'node-config-error': !currentNode.showText }">
+        <div class="node-title-container">
+          <div class="node-title-icon user-task"><span class="iconfont icon-approve"></span></div>
+          <input
+            v-if="showInput"
+            type="text"
+            class="editable-title-input"
+            @blur="blurEvent()"
+            v-mountedFocus
+            v-model="currentNode.name"
+            :placeholder="currentNode.name"
+          />
+          <div v-else class="node-title" @click="clickTitle">
+            {{ currentNode.name }}
+          </div>
+        </div>
+        <div class="node-content" @click="openNodeConfig">
+          <div class="node-text" :title="currentNode.showText" v-if="currentNode.showText">
+            {{ currentNode.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 color="#0089ff" icon="ep:circle-close-filled" :size="18" @click="deleteNode"
+          /></div>
+        </div>
+      </div>
+      <!-- 传递子节点给添加节点组件。会在子节点前面添加节点 -->
+      <NodeHandler v-if="currentNode" v-model:child-node="currentNode.childNode" />
+    </div>
+  </div>
+  <UserTaskNodeConfig
+    v-if="currentNode"
+    ref="nodeSetting"
+    :flow-node="currentNode"
+    @find:return-task-nodes="findReturnTaskNodes"
+  />
+</template>
+<script setup lang="ts">
+import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
+import { useWatchNode, useNodeName2 } from '../node'
+import NodeHandler from '../NodeHandler.vue'
+import UserTaskNodeConfig from '../nodes-config/UserTaskNodeConfig.vue'
+defineOptions({
+  name: 'UserTaskNode'
+})
+const props = defineProps({
+  flowNode: {
+    type: Object as () => SimpleFlowNode,
+    required: true
+  }
+})
+const emits = defineEmits<{
+  'update:flowNode': [node: SimpleFlowNode | undefined]
+  'find:parentNode': [nodeList: SimpleFlowNode[], nodeType: NodeType]
+}>()
+// 监控节点变化
+const currentNode = useWatchNode(props)
+// 节点名称编辑
+const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.START_USER_NODE)
+const nodeSetting = ref()
+// 打开节点配置
+const openNodeConfig = () => {
+  // 把当前节点传递给配置组件
+  nodeSetting.value.showUserTaskNodeConfig(currentNode.value)
+  nodeSetting.value.openDrawer()
+}
+
+const deleteNode = () => {
+  emits('update:flowNode', currentNode.value.childNode)
+}
+
+// 查找可以驳回用户节点
+const findReturnTaskNodes = (
+  matchNodeList: SimpleFlowNode[] // 匹配的节点
+) => {
+  // 从父节点查找
+  emits('find:parentNode', matchNodeList, NodeType.USER_TASK_NODE)
+}
+</script>
+<style lang="scss" scoped></style>

+ 33 - 0
src/components/SimpleProcessDesignerV2/src/utils.ts

@@ -0,0 +1,33 @@
+import { TimeUnitType, ApproveType, APPROVE_TYPE } from './consts'
+
+// 获取条件节点默认的名称
+export const getDefaultConditionNodeName = (index: number, defaultFlow: boolean | undefined): string => {
+  if (defaultFlow) {
+    return '其它情况'
+  }
+  return '条件' + (index + 1)
+}
+
+export const convertTimeUnit = (strTimeUnit: string) => {
+  if (strTimeUnit === 'M') {
+    return TimeUnitType.MINUTE
+  }
+  if (strTimeUnit === 'H') {
+    return TimeUnitType.HOUR
+  }
+  if (strTimeUnit === 'D') {
+    return TimeUnitType.DAY
+  }
+  return TimeUnitType.HOUR
+}
+
+export const getApproveTypeText = (approveType: ApproveType): string => {
+  let approveTypeText = ''
+  APPROVE_TYPE.forEach((item) => {
+    if (item.value === approveType) {
+      approveTypeText = item.label
+      return
+    }
+  })
+  return approveTypeText
+}

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


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


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


+ 714 - 0
src/components/SimpleProcessDesignerV2/theme/simple-process-designer.scss

@@ -0,0 +1,714 @@
+.simple-flow-canvas {
+  position: absolute;
+  inset: 0;
+  z-index: 1;
+  overflow: auto;
+  background-color: #fafafa;
+  user-select: none;
+
+  .simple-flow-container {
+    position: relative;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+
+    .top-area-container {
+      position: sticky;
+      inset: 0;
+      display: flex;
+      width: 100%;
+      height: 42px;
+      z-index: 1;
+      // padding: 4px 0;
+      background-color: #fff;
+      justify-content: flex-end;
+      align-items: center;
+
+      .top-actions {
+        display: flex;
+        margin: 4px;
+        margin-right: 8px;
+        align-items: center;
+
+        .canvas-control {
+          font-size: 16px;
+
+          .control-scale-group {
+            display: inline-flex;
+            align-items: center;
+            margin-right: 8px;
+
+            .control-scale-button {
+              display: inline-flex;
+              width: 28px;
+              height: 28px;
+              padding: 2px;
+              text-align: center;
+              cursor: pointer;
+              justify-content: center;
+              align-items: center;
+            }
+
+            .control-scale-label {
+              margin: 0 4px;
+              font-size: 14px;
+            }
+          }
+        }
+      }
+    }
+
+    .scale-container {
+      display: flex;
+      flex-direction: column;
+      justify-content: center;
+      align-items: center;
+      margin-top: 16px;
+      background-color: #fafafa;
+      transform-origin: 50% 0 0;
+      transform: scale(1);
+      transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+
+      // 节点容器 定义节点宽度
+      .node-container {
+        width: 200px;
+      }
+      // 节点
+      .node-box {
+        position: relative;
+        display: flex;
+        min-height: 70px;
+        padding: 5px 10px 8px;
+        cursor: pointer;
+        background-color: #fff;
+        flex-direction: column;
+        border: 2px solid transparent;
+        // border-color: #0089ff;
+        border-radius: 8px;
+        // border-color: #0089ff;
+        box-shadow: 0 1px 4px 0 rgba(10, 30, 65, 0.16);
+        transition: all 0.1s cubic-bezier(0.645, 0.045, 0.355, 1);
+
+        &:hover {
+          border-color: #0089ff;
+          .node-toolbar {
+            opacity: 1;
+          }
+
+          .branch-node-move {
+            display: flex;
+          }
+        }
+
+        // 普通节点标题
+        .node-title-container {
+          display: flex;
+          padding: 4px;
+          cursor: pointer;
+          border-radius: 4px 4px 0 0;
+          align-items: center;
+
+          .node-title-icon {
+            display: flex;
+            align-items: center;
+
+            &.user-task {
+              color: #ff943e;
+            }
+            &.copy-task {
+              color: #3296fa;
+            }
+            &.start-user {
+              color: #676565;
+            }
+          }
+
+          .node-title {
+            margin-left: 4px;
+            font-size: 14px;
+            font-weight: 600;
+            white-space: nowrap;
+            overflow: hidden;
+            text-overflow: ellipsis;
+            color: #1f1f1f;
+            line-height: 18px;
+            &:hover {
+              border-bottom: 1px dashed #f60;
+            }
+          }
+        }
+
+        // 条件节点标题
+        .branch-node-title-container {
+          display: flex;
+          padding: 4px 0;
+          cursor: pointer;
+          border-radius: 4px 4px 0 0;
+          align-items: center;
+          justify-content: space-between;
+
+          .input-max-width {
+            max-width: 115px !important;
+          }
+
+          .branch-title {
+            font-size: 13px;
+            font-weight: 600;
+            white-space: nowrap;
+            overflow: hidden;
+            text-overflow: ellipsis;
+            color: #f60;
+            &:hover {
+              border-bottom: 1px dashed #000;
+            }
+          }
+
+          .branch-priority {
+            min-width: 50px;
+            font-size: 13px;
+          }
+        }
+
+        .node-content {
+          display: flex;
+          min-height: 32px;
+          padding: 4px 8px;
+          margin-top: 4px;
+          line-height: 32px;
+          justify-content: space-between;
+          align-items: center;
+          color: #111f2c;
+          background: rgba(0, 0, 0, 0.03);
+          border-radius: 4px;
+
+          .node-text {
+            display: -webkit-box;
+            overflow: hidden;
+            font-size: 14px;
+            line-height: 24px;
+            text-overflow: ellipsis;
+            word-break: break-all;
+            -webkit-line-clamp: 2; /* 这将限制文本显示为两行 */
+            -webkit-box-orient: vertical;
+          }
+        }
+
+        //条件节点内容
+        .branch-node-content {
+          display: flex;
+          min-height: 32px;
+          padding: 4px 8px;
+          margin-top: 4px;
+          line-height: 32px;
+          align-items: center;
+          color: #111f2c;
+          border-radius: 4px;
+
+          .branch-node-text {
+            overflow: hidden;
+            font-size: 14px;
+            line-height: 24px;
+            text-overflow: ellipsis;
+            word-break: break-all;
+            -webkit-line-clamp: 2; /* 这将限制文本显示为两行 */
+            -webkit-box-orient: vertical;
+          }
+        }
+
+        // 节点操作 :删除
+        .node-toolbar {
+          opacity: 0;
+          position: absolute;
+          top: -20px;
+          right: 0px;
+          display: flex;
+
+          .toolbar-icon {
+            text-align: center;
+            vertical-align: middle;
+          }
+        }
+
+        // 条件节点左右移动
+        .branch-node-move {
+          position: absolute;
+          width: 10px;
+          cursor: pointer;
+          display: none;
+          align-items: center;
+          height: 100%;
+          justify-content: center;
+        }
+
+        .move-node-left {
+          left: -2px;
+          top: 0px;
+          background: rgba(126, 134, 142, 0.08);
+          border-top-left-radius: 8px;
+          border-bottom-left-radius: 8px;
+        }
+
+        .move-node-right {
+          right: -2px;
+          top: 0px;
+          background: rgba(126, 134, 142, 0.08);
+          border-top-right-radius: 6px;
+          border-bottom-right-radius: 6px;
+        }
+      }
+
+      .node-config-error {
+        border-color: #ff5219 !important;
+      }
+      // 普通节点包装
+      .node-wrapper {
+        display: flex;
+        flex-direction: column;
+        justify-content: center;
+        align-items: center;
+      }
+      // 节点连线处理
+      .node-handler-wrapper {
+        position: relative;
+        display: flex;
+        height: 70px;
+        align-items: center;
+        user-select: none;
+        justify-content: center;
+        flex-direction: column;
+
+        &::before {
+          position: absolute;
+          top: 0;
+          right: 0;
+          left: 0;
+          // bottom: 5px;
+          bottom: 0px;
+          z-index: 0;
+          width: 2px;
+          height: 100%;
+          // height: calc(100% - 5px);
+          margin: auto;
+          background-color: #dedede;
+          content: '';
+        }
+
+        .node-handler {
+          .add-icon {
+            position: relative;
+            top: -5px;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            cursor: pointer;
+            width: 25px;
+            height: 25px;
+            color: #fff;
+            background-color: #0089ff;
+            border-radius: 50%;
+
+            &:hover {
+              transform: scale(1.1);
+            }
+          }
+        }
+
+        .node-handler-arrow {
+          position: absolute;
+          bottom: 0;
+          left: 50%;
+          display: flex;
+          transform: translateX(-50%);
+        }
+      }
+
+      // 条件节点包装
+      .branch-node-wrapper {
+        position: relative;
+        display: flex;
+        flex-direction: column;
+        justify-content: center;
+        align-items: center;
+        margin-top: 16px;
+
+        .branch-node-container {
+          position: relative;
+          display: flex;
+
+          &::before {
+            position: absolute;
+            height: 100%;
+            width: 4px;
+            background-color: #fafafa;
+            content: '';
+            left: 50%;
+            transform: translate(-50%);
+          }
+
+          .branch-node-add {
+            position: absolute;
+            top: -18px;
+            left: 50%;
+            z-index: 1;
+            height: 36px;
+            padding: 0 10px;
+            font-size: 12px;
+            line-height: 36px;
+            color: #222;
+            cursor: pointer;
+            background: #fff;
+            border: 2px solid #dedede;
+            border-radius: 18px;
+            transform: translateX(-50%);
+            transform-origin: center center;
+            transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+          }
+
+          .branch-node-item {
+            position: relative;
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            min-width: 280px;
+            padding: 40px 40px 0;
+            background: transparent;
+            border-top: 2px solid #dedede;
+            border-bottom: 2px solid #dedede;
+
+            &::before {
+              position: absolute;
+              width: 2px;
+              height: 100%;
+              margin: auto;
+              inset: 0;
+              background-color: #dedede;
+              content: '';
+            }
+          }
+          // 覆盖条件节点第一个节点左上角的线
+          .branch-line-first-top {
+            position: absolute;
+            top: -5px;
+            left: -1px;
+            width: 50%;
+            height: 7px;
+            background-color: #fafafa;
+            content: '';
+          }
+          // 覆盖条件节点第一个节点左下角的线
+          .branch-line-first-bottom {
+            position: absolute;
+            bottom: -5px;
+            left: -1px;
+            width: 50%;
+            height: 7px;
+            background-color: #fafafa;
+            content: '';
+          }
+          // 覆盖条件节点最后一个节点右上角的线
+          .branch-line-last-top {
+            position: absolute;
+            top: -5px;
+            right: -1px;
+            width: 50%;
+            height: 7px;
+            background-color: #fafafa;
+            content: '';
+          }
+          // 覆盖条件节点最后一个节点右下角的线
+          .branch-line-last-bottom {
+            position: absolute;
+            right: -1px;
+            bottom: -5px;
+            width: 50%;
+            height: 7px;
+            background-color: #fafafa;
+            content: '';
+          }
+        }
+      }
+
+      .node-fixed-name {
+        display: inline-block;
+        width: auto;
+        padding: 0 4px;
+        overflow: hidden;
+        text-align: center;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+      }
+      // 开始节点包装
+      .start-node-wrapper {
+        position: relative;
+        margin-top: 16px;
+
+        .start-node-container {
+          display: flex;
+          flex-direction: column;
+          justify-content: center;
+          align-items: center;
+
+          .start-node-box {
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            width: 90px;
+            height: 36px;
+            padding: 3px 4px;
+            color: #212121;
+            cursor: pointer;
+            // background: #2c2c2c;
+            background: #fafafa;
+            border-radius: 30px;
+            box-shadow: 0 1px 5px 0 rgba(10, 30, 65, 0.08);
+            box-sizing: border-box;
+          }
+        }
+      }
+
+      // 结束节点包装
+      .end-node-wrapper {
+        margin-bottom: 16px;
+
+        .end-node-box {
+          display: flex;
+          justify-content: center;
+          align-items: center;
+          width: 80px;
+          height: 36px;
+          color: #212121;
+          // background: #6e6e6e;
+          background: #fafafa;
+          border-radius: 30px;
+          box-shadow: 0 1px 5px 0 rgba(10, 30, 65, 0.08);
+          box-sizing: border-box;
+        }
+      }
+
+      // 可编辑的 title 输入框
+      .editable-title-input {
+        height: 20px;
+        max-width: 145px;
+        line-height: 20px;
+        font-size: 12px;
+        margin-left: 4px;
+        border: 1px solid #d9d9d9;
+        border-radius: 4px;
+        transition: all 0.3s;
+
+        &:focus {
+          border-color: #40a9ff;
+          outline: 0;
+          box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
+        }
+      }
+    }
+  }
+}
+
+// 配置节点头部
+.config-header {
+  display: flex;
+  flex-direction: column;
+
+  .node-name {
+    display: flex;
+    height: 24px;
+    line-height: 24px;
+    font-size: 16px;
+    cursor: pointer;
+    align-items: center;
+  }
+
+  .divide-line {
+    width: 100%;
+    height: 1px;
+    margin-top: 16px;
+    background: #eee;
+  }
+
+  .config-editable-input {
+    height: 24px;
+    max-width: 510px;
+    font-size: 16px;
+    line-height: 24px;
+    border: 1px solid #d9d9d9;
+    border-radius: 4px;
+    transition: all 0.3s;
+
+    &:focus {
+      border-color: #40a9ff;
+      outline: 0;
+      box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
+    }
+  }
+}
+
+// 表单字段权限
+.field-setting-pane {
+  display: flex;
+  flex-direction: column;
+  font-size: 14px;
+
+  .field-setting-desc {
+    padding-right: 8px;
+    margin-bottom: 16px;
+    font-size: 16px;
+    font-weight: 700;
+  }
+
+  .field-permit-title {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    height: 45px;
+    padding-left: 12px;
+    line-height: 45px;
+    background-color: #f8fafc0a;
+    border: 1px solid #1f38581a;
+
+    .first-title {
+      text-align: left !important;
+    }
+
+    .other-titles {
+      display: flex;
+      justify-content: space-between;
+    }
+
+    .setting-title-label {
+      display: inline-block;
+      width: 110px;
+      padding: 5px 0;
+      font-size: 13px;
+      font-weight: 700;
+      color: #000;
+      text-align: center;
+    }
+  }
+
+  .field-setting-item {
+    align-items: center;
+    display: flex;
+    justify-content: space-between;
+    height: 38px;
+    padding-left: 12px;
+    border: 1px solid #1f38581a;
+    border-top: 0;
+
+    .field-setting-item-label {
+      display: inline-block;
+      width: 110px;
+      min-height: 16px;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+      cursor: text;
+    }
+
+    .field-setting-item-group {
+      display: flex;
+      justify-content: space-between;
+
+      .item-radio-wrap {
+        display: inline-block;
+        width: 110px;
+        text-align: center;
+      }
+    }
+  }
+}
+
+// 节点连线气泡卡片样式
+.handler-item-wrapper {
+  display: flex;
+  cursor: pointer;
+
+  .handler-item {
+    margin-right: 8px;
+  }
+
+  .handler-item-icon {
+    width: 80px;
+    height: 80px;
+    background: #fff;
+    border: 1px solid #e2e2e2;
+    border-radius: 50%;
+    transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+    user-select: none;
+    text-align: center;
+
+    &:hover {
+      background: #e2e2e2;
+      box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
+    }
+
+    .icon-size {
+      font-size: 35px;
+      line-height: 80px;
+    }
+  }
+
+  .approve {
+    color: #ff943e;
+  }
+  .copy {
+    color: #3296fa;
+  }
+
+  .condition {
+    color: #15bc83;
+  }
+
+  .handler-item-text {
+    margin-top: 4px;
+    width: 80px;
+    text-align: center;
+  }
+}
+
+// iconfont 样式
+@font-face {
+  font-family: 'iconfont'; /* Project id 4495938 */
+  src:
+    url('iconfont.woff2?t=1724339470412') format('woff2'),
+    url('iconfont.woff?t=1724339470412') format('woff'),
+    url('iconfont.ttf?t=1724339470412') format('truetype');
+}
+
+.iconfont {
+  font-family: 'iconfont' !important;
+  font-size: 16px;
+  font-style: normal;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+.icon-start-user:before {
+  content: '\e679';
+}
+
+.icon-inclusive:before {
+  content: '\e602';
+}
+
+.icon-copy:before {
+  content: '\e7eb';
+}
+
+.icon-handle:before {
+  content: '\e61c';
+}
+
+.icon-exclusive:before {
+  content: '\e717';
+}
+
+.icon-approve:before {
+  content: '\e715';
+}
+
+.icon-parallel:before {
+  content: '\e688';
+}

+ 11 - 0
src/directives/index.ts

@@ -11,3 +11,14 @@ export const setupAuth = (app: App<Element>) => {
   hasRole(app)
   hasPermi(app)
 }
+
+/**
+ * 导出指令:v-mountedFocus
+ */
+export const setupMountedFocus = (app: App<Element>) => {
+  app.directive('mountedFocus', {
+    mounted(el) {
+      el.focus()
+    }
+  })
+}

+ 4 - 2
src/main.ts

@@ -28,8 +28,8 @@ import '@/plugins/animate.css'
 // 路由
 import router, { setupRouter } from '@/router'
 
-// 权限
-import { setupAuth } from '@/directives'
+// 指令
+import { setupAuth, setupMountedFocus } from '@/directives'
 
 import { createApp } from 'vue'
 
@@ -58,7 +58,9 @@ const setupAll = async () => {
 
   setupRouter(app)
 
+  // directives 指令
   setupAuth(app)
+  setupMountedFocus(app)
 
   await router.isReady()
 

+ 9 - 2
src/router/modules/remaining.ts

@@ -292,7 +292,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
       },
       {
         path: 'process-instance/detail',
-        component: () => import('@/views/bpm/processInstance/detail/index.vue'),
+        component: () => import('@/views/bpm/processInstance/detail/index_new.vue'),
         name: 'BpmProcessInstanceDetail',
         meta: {
           noCache: true,
@@ -300,7 +300,14 @@ const remainingRouter: AppRouteRecordRaw[] = [
           canTo: true,
           title: '流程详情',
           activeMenu: '/bpm/task/my'
-        }
+        },
+        props: route => (
+          { 
+            id: route.query.id,
+            taskId: route.query.taskId,
+            activityId: route.query.activityId
+          }
+        )
       },
       {
         path: 'oa/leave/create',

+ 10 - 10
src/store/modules/simpleWorkflow.ts → src/store/modules/bpm/simpleWorkflow.ts

@@ -1,4 +1,4 @@
-import { store } from '../index'
+import { store } from '../../index'
 import { defineStore } from 'pinia'
 
 export const useWorkFlowStore = defineStore('simpleWorkflow', {
@@ -6,15 +6,15 @@ export const useWorkFlowStore = defineStore('simpleWorkflow', {
     tableId: '',
     isTried: false,
     promoterDrawer: false,
-    flowPermission1: {},
     approverDrawer: false,
     approverConfig1: {},
     copyerDrawer: false,
-    copyerConfig1: {},
+    copyerConfig: {},
     conditionDrawer: false,
     conditionsConfig1: {
       conditionNodes: []
-    }
+    },
+    userTaskConfig: {}
   }),
   actions: {
     setTableId(payload) {
@@ -26,26 +26,26 @@ export const useWorkFlowStore = defineStore('simpleWorkflow', {
     setPromoter(payload) {
       this.promoterDrawer = payload
     },
-    setFlowPermission(payload) {
-      this.flowPermission1 = payload
-    },
-    setApprover(payload) {
+    setApproverDrawer(payload) {
       this.approverDrawer = payload
     },
     setApproverConfig(payload) {
       this.approverConfig1 = payload
     },
-    setCopyer(payload) {
+    setCopyerDrawer(payload) {
       this.copyerDrawer = payload
     },
     setCopyerConfig(payload) {
-      this.copyerConfig1 = payload
+      this.copyerConfig = payload
     },
     setCondition(payload) {
       this.conditionDrawer = payload
     },
     setConditionsConfig(payload) {
       this.conditionsConfig1 = payload
+    },
+    setUserTaskConfig(payload) {
+      this.userTaskConfig = payload
     }
   }
 })

+ 12 - 0
src/utils/constants.ts

@@ -437,3 +437,15 @@ export const ErpBizType = {
   SALE_OUT: 21,
   SALE_RETURN: 22
 }
+
+// ========== BPM 模块 ==========
+
+export const BpmModelType = {
+  BPMN: 10, // BPMN 设计器
+  SIMPLE: 20 // 简易设计器
+}
+
+export const BpmModelFormType = {
+  NORMAL: 10, // 流程表单
+  CUSTOM: 20 // 业务表单
+}

+ 1 - 0
src/utils/dict.ts

@@ -143,6 +143,7 @@ export enum DICT_TYPE {
   INFRA_OPERATE_TYPE = 'infra_operate_type',
 
   // ========== BPM 模块 ==========
+  BPM_MODEL_TYPE = 'bpm_model_type',
   BPM_MODEL_FORM_TYPE = 'bpm_model_form_type',
   BPM_TASK_CANDIDATE_STRATEGY = 'bpm_task_candidate_strategy',
   BPM_PROCESS_INSTANCE_STATUS = 'bpm_process_instance_status',

+ 139 - 83
src/views/bpm/model/ModelForm.vue

@@ -8,12 +8,7 @@
       label-width="110px"
     >
       <el-form-item label="流程标识" prop="key">
-        <el-input
-          v-model="formData.key"
-          :disabled="!!formData.id"
-          placeholder="请输入流标标识"
-          style="width: 330px"
-        />
+        <el-input v-model="formData.key" :disabled="!!formData.id" placeholder="请输入流标标识" />
         <el-tooltip
           v-if="!formData.id"
           class="item"
@@ -35,7 +30,7 @@
           placeholder="请输入流程名称"
         />
       </el-form-item>
-      <el-form-item v-if="formData.id" label="流程分类" prop="category">
+      <el-form-item label="流程分类" prop="category">
         <el-select
           v-model="formData.category"
           clearable
@@ -50,73 +45,108 @@
           />
         </el-select>
       </el-form-item>
-      <el-form-item v-if="formData.id" label="流程图标" prop="icon">
-        <UploadImg v-model="formData.icon" :limit="1" height="128px" width="128px" />
+      <el-form-item label="流程图标" prop="icon">
+        <UploadImg v-model="formData.icon" :limit="1" height="64px" width="64px" />
       </el-form-item>
       <el-form-item label="流程描述" prop="description">
         <el-input v-model="formData.description" clearable type="textarea" />
       </el-form-item>
-      <div v-if="formData.id">
-        <el-form-item label="表单类型" prop="formType">
-          <el-radio-group v-model="formData.formType">
-            <el-radio
-              v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_FORM_TYPE)"
-              :key="dict.value"
-              :value="dict.value"
-            >
-              {{ dict.label }}
-            </el-radio>
-          </el-radio-group>
-        </el-form-item>
-        <el-form-item v-if="formData.formType === 10" label="流程表单" prop="formId">
-          <el-select v-model="formData.formId" clearable style="width: 100%">
-            <el-option
-              v-for="form in formList"
-              :key="form.id"
-              :label="form.name"
-              :value="form.id"
-            />
-          </el-select>
-        </el-form-item>
-        <el-form-item
-          v-if="formData.formType === 20"
-          label="表单提交路由"
-          prop="formCustomCreatePath"
+      <el-form-item label="流程类型" prop="type">
+        <el-radio-group v-model="formData.type">
+          <el-radio
+            v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_TYPE)"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="表单类型" prop="formType">
+        <el-radio-group v-model="formData.formType">
+          <el-radio
+            v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_FORM_TYPE)"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item v-if="formData.formType === 10" label="流程表单" prop="formId">
+        <el-select v-model="formData.formId" clearable style="width: 100%">
+          <el-option v-for="form in formList" :key="form.id" :label="form.name" :value="form.id" />
+        </el-select>
+      </el-form-item>
+      <el-form-item
+        v-if="formData.formType === 20"
+        label="表单提交路由"
+        prop="formCustomCreatePath"
+      >
+        <el-input
+          v-model="formData.formCustomCreatePath"
+          placeholder="请输入表单提交路由"
+          style="width: 330px"
+        />
+        <el-tooltip
+          class="item"
+          content="自定义表单的提交路径,使用 Vue 的路由地址,例如说:bpm/oa/leave/create.vue"
+          effect="light"
+          placement="top"
         >
-          <el-input
-            v-model="formData.formCustomCreatePath"
-            placeholder="请输入表单提交路由"
-            style="width: 330px"
-          />
-          <el-tooltip
-            class="item"
-            content="自定义表单的提交路径,使用 Vue 的路由地址,例如说:bpm/oa/leave/create"
-            effect="light"
-            placement="top"
+          <i class="el-icon-question" style="padding-left: 5px"></i>
+        </el-tooltip>
+      </el-form-item>
+      <el-form-item v-if="formData.formType === 20" label="表单查看地址" prop="formCustomViewPath">
+        <el-input
+          v-model="formData.formCustomViewPath"
+          placeholder="请输入表单查看的组件地址"
+          style="width: 330px"
+        />
+        <el-tooltip
+          class="item"
+          content="自定义表单的查看组件地址,使用 Vue 的组件地址,例如说:bpm/oa/leave/detail.vue"
+          effect="light"
+          placement="top"
+        >
+          <i class="el-icon-question" style="padding-left: 5px"></i>
+        </el-tooltip>
+      </el-form-item>
+      <el-form-item label="是否可见" prop="visible">
+        <el-radio-group v-model="formData.visible">
+          <el-radio
+            v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+            :key="dict.value as string"
+            :label="dict.value"
           >
-            <i class="el-icon-question" style="padding-left: 5px"></i>
-          </el-tooltip>
-        </el-form-item>
-        <el-form-item
-          v-if="formData.formType === 20"
-          label="表单查看地址"
-          prop="formCustomViewPath"
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="谁可以发起" prop="startUserIds">
+        <el-select
+          v-model="formData.startUserIds"
+          multiple
+          placeholder="请选择可发起人,默认(不选择)则所有人都可以发起"
         >
-          <el-input
-            v-model="formData.formCustomViewPath"
-            placeholder="请输入表单查看的组件地址"
-            style="width: 330px"
+          <el-option
+            v-for="user in userList"
+            :key="user.id"
+            :label="user.nickname"
+            :value="user.id"
           />
-          <el-tooltip
-            class="item"
-            content="自定义表单的查看组件地址,使用 Vue 的组件地址,例如说:bpm/oa/leave/detail"
-            effect="light"
-            placement="top"
-          >
-            <i class="el-icon-question" style="padding-left: 5px"></i>
-          </el-tooltip>
-        </el-form-item>
-      </div>
+        </el-select>
+      </el-form-item>
+      <el-form-item label="流程管理员" prop="managerUserIds">
+        <el-select v-model="formData.managerUserIds" multiple placeholder="请选择流程管理员">
+          <el-option
+            v-for="user in userList"
+            :key="user.id"
+            :label="user.nickname"
+            :value="user.id"
+          />
+        </el-select>
+      </el-form-item>
     </el-form>
     <template #footer>
       <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
@@ -125,45 +155,62 @@
   </Dialog>
 </template>
 <script lang="ts" setup>
-import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { DICT_TYPE, getBoolDictOptions, getIntDictOptions } from '@/utils/dict'
 import { ElMessageBox } from 'element-plus'
 import * as ModelApi from '@/api/bpm/model'
 import * as FormApi from '@/api/bpm/form'
 import { CategoryApi } from '@/api/bpm/category'
+import { BpmModelFormType, BpmModelType } from '@/utils/constants'
+import { UserVO } from '@/api/system/user'
+import * as UserApi from '@/api/system/user'
+import { useUserStoreWithOut } from '@/store/modules/user'
 
 defineOptions({ name: 'ModelForm' })
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
+const userStore = useUserStoreWithOut() // 用户信息缓存
 
 const dialogVisible = ref(false) // 弹窗的是否展示
 const dialogTitle = ref('') // 弹窗的标题
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 const formType = ref('') // 表单的类型:create - 新增;update - 修改
 const formData = ref({
-  formType: 10,
+  id: undefined,
   name: '',
+  key: '',
   category: undefined,
   icon: undefined,
   description: '',
+  type: BpmModelType.BPMN,
+  formType: BpmModelFormType.NORMAL,
   formId: '',
   formCustomCreatePath: '',
-  formCustomViewPath: ''
+  formCustomViewPath: '',
+  visible: true,
+  startUserIds: [],
+  managerUserIds: []
 })
 const formRules = reactive({
-  name: [{ required: true, message: '参数名称不能为空', trigger: 'blur' }],
-  key: [{ required: true, message: '参数键名不能为空', trigger: 'blur' }],
-  category: [{ required: true, message: '参数分类不能为空', trigger: 'blur' }],
-  icon: [{ required: true, message: '参数图标不能为空', trigger: 'blur' }],
-  value: [{ required: true, message: '参数键值不能为空', trigger: 'blur' }],
-  visible: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }]
+  name: [{ required: true, message: '流程名称不能为空', trigger: 'blur' }],
+  key: [{ required: true, message: '流程标识不能为空', trigger: 'blur' }],
+  category: [{ required: true, message: '流程分类不能为空', trigger: 'blur' }],
+  icon: [{ required: true, message: '流程图标不能为空', trigger: 'blur' }],
+  type: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }],
+  formType: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }],
+  formId: [{ required: true, message: '流程表单不能为空', trigger: 'blur' }],
+  formCustomCreatePath: [{ required: true, message: '表单提交路由不能为空', trigger: 'blur' }],
+  formCustomViewPath: [{ required: true, message: '表单查看地址不能为空', trigger: 'blur' }],
+  visible: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }],
+  managerUserIds: [{ required: true, message: '流程管理员不能为空', trigger: 'blur' }]
 })
 const formRef = ref() // 表单 Ref
 const formList = ref([]) // 流程表单的下拉框的数据
 const categoryList = ref([]) // 流程分类列表
+const userList = ref<UserVO[]>([]) // 用户列表
 
 /** 打开弹窗 */
-const open = async (type: string, id?: number) => {
+const open = async (type: string, id?: string) => {
   dialogVisible.value = true
   dialogTitle.value = t('action.' + type)
   formType.value = type
@@ -176,11 +223,15 @@ const open = async (type: string, id?: number) => {
     } finally {
       formLoading.value = false
     }
+  } else {
+    formData.value.managerUserIds.push(userStore.getUser.id)
   }
   // 获得流程表单的下拉框的数据
   formList.value = await FormApi.getFormSimpleList()
   // 查询流程分类列表
   categoryList.value = await CategoryApi.getCategorySimpleList()
+  // 查询用户列表
+  userList.value = await UserApi.getSimpleUserList()
 }
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 
@@ -199,10 +250,9 @@ const submitForm = async () => {
       await ModelApi.createModel(data)
       // 提示,引导用户做后续的操作
       await ElMessageBox.alert(
-        '<strong>新建模型成功!</strong>后续需要执行如下 3 个步骤:' +
-          '<div>1. 点击【修改流程】按钮,配置流程的分类、表单信息</div>' +
-          '<div>2. 点击【设计流程】按钮,绘制流程图</div>' +
-          '<div>3. 点击【发布流程】按钮,完成流程的最终发布</div>' +
+        '<strong>新建模型成功!</strong>后续需要执行如下 2 个步骤:' +
+          '<div>1. 点击【设计流程】按钮,绘制流程图</div>' +
+          '<div>2. 点击【发布流程】按钮,完成流程的最终发布</div>' +
           '另外,每次流程修改后,都需要点击【发布流程】按钮,才能正式生效!!!',
         '重要提示',
         {
@@ -225,14 +275,20 @@ const submitForm = async () => {
 /** 重置表单 */
 const resetForm = () => {
   formData.value = {
-    formType: 10,
+    id: undefined,
     name: '',
+    key: '',
     category: undefined,
-    icon: '',
+    icon: undefined,
     description: '',
+    type: BpmModelType.BPMN,
+    formType: BpmModelFormType.NORMAL,
     formId: '',
     formCustomCreatePath: '',
-    formCustomViewPath: ''
+    formCustomViewPath: '',
+    visible: true,
+    startUserIds: [],
+    managerUserIds: []
   }
   formRef.value?.resetFields()
 }

+ 0 - 141
src/views/bpm/model/ModelImportForm.vue

@@ -1,141 +0,0 @@
-<template>
-  <Dialog v-model="dialogVisible" title="导入流程" width="400">
-    <div>
-      <el-upload
-        ref="uploadRef"
-        v-model:file-list="fileList"
-        :action="importUrl"
-        :auto-upload="false"
-        :data="formData"
-        :disabled="formLoading"
-        :headers="uploadHeaders"
-        :limit="1"
-        :on-error="submitFormError"
-        :on-exceed="handleExceed"
-        :on-success="submitFormSuccess"
-        accept=".bpmn, .xml"
-        drag
-        name="bpmnFile"
-      >
-        <Icon class="el-icon--upload" icon="ep:upload-filled" />
-        <div class="el-upload__text"> 将文件拖到此处,或 <em>点击上传</em></div>
-        <template #tip>
-          <div class="el-upload__tip" style="color: red">
-            提示:仅允许导入“bpm”或“xml”格式文件!
-          </div>
-          <div>
-            <el-form ref="formRef" :model="formData" :rules="formRules" label-width="120px">
-              <el-form-item label="流程标识" prop="key">
-                <el-input
-                  v-model="formData.key"
-                  placeholder="请输入流标标识"
-                  style="width: 250px"
-                />
-              </el-form-item>
-              <el-form-item label="流程名称" prop="name">
-                <el-input v-model="formData.name" clearable placeholder="请输入流程名称" />
-              </el-form-item>
-              <el-form-item label="流程描述" prop="description">
-                <el-input v-model="formData.description" clearable type="textarea" />
-              </el-form-item>
-            </el-form>
-          </div>
-        </template>
-      </el-upload>
-    </div>
-    <template #footer>
-      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
-      <el-button @click="dialogVisible = false">取 消</el-button>
-    </template>
-  </Dialog>
-</template>
-<script lang="ts" setup>
-import { getAccessToken, getTenantId } from '@/utils/auth'
-
-defineOptions({ name: 'ModelImportForm' })
-
-const message = useMessage() // 消息弹窗
-
-const dialogVisible = ref(false) // 弹窗的是否展示
-const formLoading = ref(false) // 表单的加载中
-const formData = ref({
-  key: '',
-  name: '',
-  description: ''
-})
-const formRules = reactive({
-  key: [{ required: true, message: '流程标识不能为空', trigger: 'blur' }],
-  name: [{ required: true, message: '流程名称不能为空', trigger: 'blur' }]
-})
-const formRef = ref() // 表单 Ref
-const uploadRef = ref() // 上传 Ref
-const importUrl = import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/bpm/model/import'
-const uploadHeaders = ref() // 上传 Header 头
-const fileList = ref([]) // 文件列表
-
-/** 打开弹窗 */
-const open = async () => {
-  dialogVisible.value = true
-  resetForm()
-}
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
-
-/** 提交表单 */
-const submitForm = async () => {
-  // 校验表单
-  if (!formRef) return
-  const valid = await formRef.value.validate()
-  if (!valid) return
-  if (fileList.value.length == 0) {
-    message.error('请上传文件')
-    return
-  }
-  // 提交请求
-  uploadHeaders.value = {
-    Authorization: 'Bearer ' + getAccessToken(),
-    'tenant-id': getTenantId()
-  }
-  formLoading.value = true
-  uploadRef.value!.submit()
-}
-
-/** 文件上传成功 */
-const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
-const submitFormSuccess = async (response: any) => {
-  if (response.code !== 0) {
-    message.error(response.msg)
-    formLoading.value = false
-    return
-  }
-  // 提示成功
-  message.success('导入流程成功!请点击【设计流程】按钮,进行编辑保存后,才可以进行【发布流程】')
-  dialogVisible.value = false
-  // 发送操作成功的事件
-  emit('success')
-}
-
-/** 上传错误提示 */
-const submitFormError = (): void => {
-  message.error('导入流程失败,请您重新上传!')
-  formLoading.value = false
-}
-
-/** 重置表单 */
-const resetForm = () => {
-  // 重置上传状态和文件
-  formLoading.value = false
-  uploadRef.value?.clearFiles()
-  // 重置表单
-  formData.value = {
-    key: '',
-    name: '',
-    description: ''
-  }
-  formRef.value?.resetFields()
-}
-
-/** 文件数超出提示 */
-const handleExceed = (): void => {
-  message.error('最多只能上传一个文件!')
-}
-</script>

+ 3 - 3
src/views/bpm/model/editor/index.vue

@@ -58,17 +58,17 @@ const initModeler = (item) => {
 }
 
 /** 添加/修改模型 */
-const save = async (bpmnXml) => {
+const save = async (bpmnXml: string) => {
   const data = {
     ...model.value,
     bpmnXml: bpmnXml // bpmnXml 只是初始化流程图,后续修改无法通过它获得
   } as unknown as ModelApi.ModelVO
   // 提交
   if (data.id) {
-    await ModelApi.updateModel(data)
+    await ModelApi.updateModelBpmn(data)
     message.success('修改成功')
   } else {
-    await ModelApi.createModel(data)
+    await ModelApi.updateModelBpmn(data)
     message.success('新增成功')
   }
   // 跳转回去

+ 133 - 144
src/views/bpm/model/index.vue

@@ -58,10 +58,7 @@
           @click="openForm('create')"
           v-hasPermi="['bpm:model:create']"
         >
-          <Icon icon="ep:plus" class="mr-5px" /> 新建流程
-        </el-button>
-        <el-button type="success" plain @click="openImportForm" v-hasPermi="['bpm:model:import']">
-          <Icon icon="ep:upload" class="mr-5px" /> 导入流程
+          <Icon icon="ep:plus" class="mr-5px" /> 新建
         </el-button>
       </el-form-item>
     </el-form>
@@ -70,21 +67,34 @@
   <!-- 列表 -->
   <ContentWrap>
     <el-table v-loading="loading" :data="list">
-      <el-table-column label="流程标识" align="center" prop="key" width="200" />
-      <el-table-column label="流程名称" align="center" prop="name" width="200">
+      <el-table-column label="流程名称" align="center" prop="name" min-width="200" />
+      <el-table-column label="流程图标" align="center" prop="icon" min-width="100">
         <template #default="scope">
-          <el-button type="primary" link @click="handleBpmnDetail(scope.row)">
-            <span>{{ scope.row.name }}</span>
-          </el-button>
+          <el-image :src="scope.row.icon" class="h-32px w-32px" />
         </template>
       </el-table-column>
-      <el-table-column label="流程图标" align="center" prop="icon" width="100">
+      <el-table-column label="可见范围" align="center" prop="startUserIds" min-width="100">
         <template #default="scope">
-          <el-image :src="scope.row.icon" class="w-32px h-32px" />
+          <el-text v-if="!scope.row.startUsers || scope.row.startUsers.length === 0">
+            全部可见
+          </el-text>
+          <el-text v-else-if="scope.row.startUsers.length == 1">
+            {{ scope.row.startUsers[0].nickname }}
+          </el-text>
+          <el-text v-else>
+            <el-tooltip
+              class="box-item"
+              effect="dark"
+              placement="top"
+              :content="scope.row.startUsers.map((user: any) => user.nickname).join('、')"
+            >
+              {{ scope.row.startUsers[0].nickname }}等 {{ scope.row.startUsers.length }} 人可见
+            </el-tooltip>
+          </el-text>
         </template>
       </el-table-column>
-      <el-table-column label="流程分类" align="center" prop="categoryName" width="100" />
-      <el-table-column label="表单信息" align="center" prop="formType" width="200">
+      <el-table-column label="流程分类" align="center" prop="categoryName" min-width="100" />
+      <el-table-column label="表单信息" align="center" prop="formType" min-width="200">
         <template #default="scope">
           <el-button
             v-if="scope.row.formType === 10"
@@ -105,101 +115,87 @@
           <label v-else>暂无表单</label>
         </template>
       </el-table-column>
-      <el-table-column
-        label="创建时间"
-        align="center"
-        prop="createTime"
-        width="180"
-        :formatter="dateFormatter"
-      />
-      <el-table-column label="最新部署的流程定义" align="center">
-        <el-table-column
-          label="流程版本"
-          align="center"
-          prop="processDefinition.version"
-          width="100"
-        >
-          <template #default="scope">
-            <el-tag v-if="scope.row.processDefinition">
-              v{{ scope.row.processDefinition.version }}
-            </el-tag>
-            <el-tag v-else type="warning">未部署</el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column
-          label="激活状态"
-          align="center"
-          prop="processDefinition.version"
-          width="85"
-        >
-          <template #default="scope">
-            <el-switch
-              v-if="scope.row.processDefinition"
-              v-model="scope.row.processDefinition.suspensionState"
-              :active-value="1"
-              :inactive-value="2"
-              @change="handleChangeState(scope.row)"
-            />
-          </template>
-        </el-table-column>
-        <el-table-column label="部署时间" align="center" prop="deploymentTime" width="180">
-          <template #default="scope">
-            <span v-if="scope.row.processDefinition">
-              {{ formatDate(scope.row.processDefinition.deploymentTime) }}
-            </span>
-          </template>
-        </el-table-column>
+      <el-table-column label="最后发布" align="center" prop="deploymentTime" min-width="250">
+        <template #default="scope">
+          <span v-if="scope.row.processDefinition">
+            {{ formatDate(scope.row.processDefinition.deploymentTime) }}
+          </span>
+          <el-tag v-if="scope.row.processDefinition" class="ml-10px">
+            v{{ scope.row.processDefinition.version }}
+          </el-tag>
+          <el-tag v-else type="warning">未部署</el-tag>
+          <el-tag
+            v-if="scope.row.processDefinition?.suspensionState === 2"
+            type="warning"
+            class="ml-10px"
+          >
+            已停用
+          </el-tag>
+        </template>
       </el-table-column>
-      <el-table-column label="操作" align="center" min-width="240" fixed="right">
+      <el-table-column label="操作" align="center" width="200" fixed="right">
         <template #default="scope">
           <el-button
             link
             type="primary"
             @click="openForm('update', scope.row.id)"
             v-hasPermi="['bpm:model:update']"
+            :disabled="!isManagerUser(scope.row)"
           >
-            修改流程
+            修改
           </el-button>
           <el-button
             link
+            class="!ml-5px"
             type="primary"
             @click="handleDesign(scope.row)"
             v-hasPermi="['bpm:model:update']"
+            :disabled="!isManagerUser(scope.row)"
           >
-            设计流程
-          </el-button>
-          <el-button
-            link
-            type="primary"
-            @click="handleSimpleDesign(scope.row.id)"
-            v-hasPermi="['bpm:model:update']"
-          >
-            仿钉钉设计流程
+            设计
           </el-button>
           <el-button
             link
+            class="!ml-5px"
             type="primary"
             @click="handleDeploy(scope.row)"
             v-hasPermi="['bpm:model:deploy']"
+            :disabled="!isManagerUser(scope.row)"
           >
-            发布流程
-          </el-button>
-          <el-button
-            link
-            type="primary"
-            v-hasPermi="['bpm:process-definition:query']"
-            @click="handleDefinitionList(scope.row)"
-          >
-            流程定义
+            发布
           </el-button>
-          <el-button
-            link
-            type="danger"
-            @click="handleDelete(scope.row.id)"
-            v-hasPermi="['bpm:model:delete']"
+          <el-dropdown
+            class="!align-middle ml-5px"
+            @command="(command) => handleCommand(command, scope.row)"
+            v-hasPermi="['bpm:process-definition:query', 'bpm:model:update', 'bpm:model:delete']"
           >
-            删除
-          </el-button>
+            <el-button type="primary" link>更多</el-button>
+            <template #dropdown>
+              <el-dropdown-menu>
+                <el-dropdown-item
+                  command="handleDefinitionList"
+                  v-if="checkPermi(['bpm:process-definition:query'])"
+                >
+                  历史
+                </el-dropdown-item>
+                <el-dropdown-item
+                  command="handleChangeState"
+                  v-if="checkPermi(['bpm:model:update']) && scope.row.processDefinition"
+                  :disabled="!isManagerUser(scope.row)"
+                >
+                  {{ scope.row.processDefinition.suspensionState === 1 ? '停用' : '启用' }}
+                </el-dropdown-item>
+                <el-dropdown-item
+                  type="danger"
+                  command="handleDelete"
+                  v-if="checkPermi(['bpm:model:delete'])"
+                  :disabled="!isManagerUser(scope.row)"
+                >
+                  删除
+                </el-dropdown-item>
+              </el-dropdown-menu>
+            </template>
+          </el-dropdown>
         </template>
       </el-table-column>
     </el-table>
@@ -215,41 +211,29 @@
   <!-- 表单弹窗:添加/修改流程 -->
   <ModelForm ref="formRef" @success="getList" />
 
-  <!-- 表单弹窗:导入流程 -->
-  <ModelImportForm ref="importFormRef" @success="getList" />
-
   <!-- 弹窗:表单详情 -->
   <Dialog title="表单详情" v-model="formDetailVisible" width="800">
     <form-create :rule="formDetailPreview.rule" :option="formDetailPreview.option" />
   </Dialog>
-
-  <!-- 弹窗:流程模型图的预览 -->
-  <Dialog title="流程图" v-model="bpmnDetailVisible" width="800">
-    <MyProcessViewer
-      key="designer"
-      v-model="bpmnXML"
-      :value="bpmnXML as any"
-      v-bind="bpmnControlForm"
-      :prefix="bpmnControlForm.prefix"
-    />
-  </Dialog>
 </template>
 
 <script lang="ts" setup>
-import { dateFormatter, formatDate } from '@/utils/formatTime'
-import { MyProcessViewer } from '@/components/bpmnProcessDesigner/package'
+import { formatDate } from '@/utils/formatTime'
 import * as ModelApi from '@/api/bpm/model'
 import * as FormApi from '@/api/bpm/form'
 import ModelForm from './ModelForm.vue'
-import ModelImportForm from '@/views/bpm/model/ModelImportForm.vue'
 import { setConfAndFields2 } from '@/utils/formCreate'
 import { CategoryApi } from '@/api/bpm/category'
+import { BpmModelType } from '@/utils/constants'
+import { checkPermi } from '@/utils/permission'
+import { useUserStoreWithOut } from '@/store/modules/user'
 
 defineOptions({ name: 'BpmModel' })
 
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
 const { push } = useRouter() // 路由
+const userStore = useUserStoreWithOut() // 用户信息缓存
 
 const loading = ref(true) // 列表的加载中
 const total = ref(0) // 列表的总页数
@@ -288,25 +272,36 @@ const resetQuery = () => {
   handleQuery()
 }
 
+/** '更多'操作按钮 */
+const handleCommand = (command: string, row: any) => {
+  switch (command) {
+    case 'handleDefinitionList':
+      handleDefinitionList(row)
+      break
+    case 'handleDelete':
+      handleDelete(row)
+      break
+    case 'handleChangeState':
+      handleChangeState(row)
+      break
+    default:
+      break
+  }
+}
+
 /** 添加/修改操作 */
 const formRef = ref()
 const openForm = (type: string, id?: number) => {
   formRef.value.open(type, id)
 }
 
-/** 添加/修改操作 */
-const importFormRef = ref()
-const openImportForm = () => {
-  importFormRef.value.open()
-}
-
 /** 删除按钮操作 */
-const handleDelete = async (id: number) => {
+const handleDelete = async (row: any) => {
   try {
     // 删除的二次确认
     await message.delConfirm()
     // 发起删除
-    await ModelApi.deleteModel(id)
+    await ModelApi.deleteModel(row.id)
     message.success(t('common.delSuccess'))
     // 刷新列表
     await getList()
@@ -314,45 +309,45 @@ const handleDelete = async (id: number) => {
 }
 
 /** 更新状态操作 */
-const handleChangeState = async (row) => {
+const handleChangeState = async (row: any) => {
   const state = row.processDefinition.suspensionState
+  const newState = state === 1 ? 2 : 1
   try {
     // 修改状态的二次确认
     const id = row.id
-    const statusState = state === 1 ? '激活' : '挂起'
+    debugger
+    const statusState = state === 1 ? '停用' : '启用'
     const content = '是否确认' + statusState + '流程名字为"' + row.name + '"的数据项?'
     await message.confirm(content)
     // 发起修改状态
-    await ModelApi.updateModelState(id, state)
+    await ModelApi.updateModelState(id, newState)
+    message.success(statusState + '成功')
     // 刷新列表
     await getList()
-  } catch {
-    // 取消后,进行恢复按钮
-    row.processDefinition.suspensionState = state === 1 ? 2 : 1
-  }
+  } catch {}
 }
 
 /** 设计流程 */
-const handleDesign = (row) => {
-  push({
-    name: 'BpmModelEditor',
-    query: {
-      modelId: row.id
-    }
-  })
-}
-
-const handleSimpleDesign = (row) => {
-  push({
-    name: 'SimpleWorkflowDesignEditor',
-    query: {
-      modelId: row.id
-    }
-  })
+const handleDesign = (row: any) => {
+  if (row.type == BpmModelType.BPMN) {
+    push({
+      name: 'BpmModelEditor',
+      query: {
+        modelId: row.id
+      }
+    })
+  } else {
+    push({
+      name: 'SimpleWorkflowDesignEditor',
+      query: {
+        modelId: row.id
+      }
+    })
+  }
 }
 
 /** 发布流程 */
-const handleDeploy = async (row) => {
+const handleDeploy = async (row: any) => {
   try {
     // 删除的二次确认
     await message.confirm('是否部署该流程!!')
@@ -380,7 +375,7 @@ const formDetailPreview = ref({
   rule: [],
   option: {}
 })
-const handleFormDetail = async (row) => {
+const handleFormDetail = async (row: any) => {
   if (row.formType == 10) {
     // 设置表单
     const data = await FormApi.getForm(row.formId)
@@ -394,16 +389,10 @@ const handleFormDetail = async (row) => {
   }
 }
 
-/** 流程图的详情按钮操作 */
-const bpmnDetailVisible = ref(false)
-const bpmnXML = ref(null)
-const bpmnControlForm = ref({
-  prefix: 'flowable'
-})
-const handleBpmnDetail = async (row) => {
-  const data = await ModelApi.getModel(row.id)
-  bpmnXML.value = data.bpmnXml || ''
-  bpmnDetailVisible.value = true
+/** 判断是否可以操作 */
+const isManagerUser = (row: any) => {
+  const userId = userStore.getUser.id
+  return row.managerUserIds && row.managerUserIds.includes(userId)
 }
 
 /** 初始化 **/

+ 85 - 18
src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue

@@ -1,13 +1,22 @@
 <template>
   <div
-    class="h-50px position-fixed bottom-10 text-14px flex items-center color-#32373c dark:color-#fff font-bold btn-container"
+    class="h-50px bottom-10 text-14px flex items-center color-#32373c dark:color-#fff font-bold btn-container"
+    v-if="runningTask.id"
   >
-    <el-popover :visible="passVisible" placement="top-end" :width="500" trigger="click">
+    <!-- 【通过】按钮 -->
+    <el-popover
+      :visible="passVisible"
+      placement="top-end"
+      :width="500"
+      trigger="click"
+      v-if="isShowButton(OperationButtonType.APPROVE)"
+    >
       <template #reference>
         <el-button plain type="success" @click="openPopover('1')">
-          <Icon icon="ep:select" />&nbsp; 通过
+          <Icon icon="ep:select" />&nbsp; {{ getButtonDisplayName(OperationButtonType.APPROVE) }}
         </el-button>
       </template>
+      <!-- 审批表单 -->
       <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
         <el-form
           label-position="top"
@@ -50,19 +59,28 @@
 
           <el-form-item>
             <el-button :disabled="formLoading" type="success" @click="handleAudit(true)">
-              通过
+              {{ getButtonDisplayName(OperationButtonType.APPROVE) }}
             </el-button>
             <el-button @click="passVisible = false"> 取消 </el-button>
           </el-form-item>
         </el-form>
       </div>
     </el-popover>
-    <el-popover :visible="rejectVisible" placement="top-end" :width="500" trigger="click">
+
+    <!-- 【拒绝】按钮 -->
+    <el-popover
+      :visible="rejectVisible"
+      placement="top-end"
+      :width="500"
+      trigger="click"
+      v-if="isShowButton(OperationButtonType.REJECT)"
+    >
       <template #reference>
         <el-button class="mr-20px" plain type="danger" @click="openPopover('2')">
-          <Icon icon="ep:close" />&nbsp; 拒绝
+          <Icon icon="ep:close" />&nbsp; {{ getButtonDisplayName(OperationButtonType.REJECT) }}
         </el-button>
       </template>
+      <!-- 审批表单 -->
       <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
         <el-form
           label-position="top"
@@ -105,21 +123,46 @@
 
           <el-form-item>
             <el-button :disabled="formLoading" type="danger" @click="handleAudit(false)">
-              拒绝
+              {{ getButtonDisplayName(OperationButtonType.REJECT) }}
             </el-button>
             <el-button @click="rejectVisible = false"> 取消 </el-button>
           </el-form-item>
         </el-form>
       </div>
     </el-popover>
+
+    <!-- 【抄送】按钮 -->
     <div @click="handleSend"> <Icon :size="14" icon="svg-icon:send" />&nbsp;抄送 </div>
-    <div @click="openTaskUpdateAssigneeForm">
-      <Icon :size="14" icon="fa:share-square-o" />&nbsp;转交
+
+    <!-- 【转交】按钮 -->
+    <div @click="openTaskUpdateAssigneeForm" v-if="isShowButton(OperationButtonType.TRANSFER)">
+      <Icon :size="14" icon="fa:share-square-o" />&nbsp;
+      {{ getButtonDisplayName(OperationButtonType.TRANSFER) }}
+    </div>
+
+    <!-- 【委托】按钮 -->
+    <div @click="handleDelegate" v-if="isShowButton(OperationButtonType.DELEGATE)">
+      <Icon :size="14" icon="ep:position" />&nbsp;
+      {{ getButtonDisplayName(OperationButtonType.DELEGATE) }}
+    </div>
+
+    <!-- 【加签】 -->
+    <div @click="handleSign" v-if="isShowButton(OperationButtonType.ADD_SIGN)">
+      <Icon :size="14" icon="ep:plus" />&nbsp;
+      {{ getButtonDisplayName(OperationButtonType.ADD_SIGN) }}
+    </div>
+    <!-- TODO @jason:减签 -->
+
+    <!-- 【退回】按钮 -->
+    <div @click="handleBack" v-if="isShowButton(OperationButtonType.RETURN)">
+      <Icon :size="14" icon="fa:mail-reply" />&nbsp;
+      {{ getButtonDisplayName(OperationButtonType.RETURN) }}
     </div>
-    <div @click="handleDelegate"> <Icon :size="14" icon="ep:position" />&nbsp;委派 </div>
-    <div @click="handleSign"> <Icon :size="14" icon="ep:plus" />&nbsp;加签 </div>
-    <div @click="handleBack"> <Icon :size="14" icon="fa:mail-reply" />&nbsp;退回 </div>
+
+    <!--TODO @jason:撤回 -->
+    <!--TODO @jason:再次发起 -->
   </div>
+
   <!-- 弹窗:转派审批人 -->
   <TaskTransferForm ref="taskTransferFormRef" @success="getDetail" />
   <!-- 弹窗:回退节点 -->
@@ -129,7 +172,6 @@
   <!-- 弹窗:加签,当前任务审批人为A,向前加签选了一个C,则需要C先审批,然后再是A审批,向后加签B,A审批完,需要B再审批完,才算完成这个任务节点 -->
   <TaskSignCreateForm ref="taskSignCreateFormRef" @success="getDetail" />
 </template>
-
 <script lang="ts" setup>
 import { setConfAndFields2 } from '@/utils/formCreate'
 import { useUserStore } from '@/store/modules/user'
@@ -140,7 +182,10 @@ import TaskDelegateForm from './dialog/TaskDelegateForm.vue'
 import TaskTransferForm from './dialog/TaskTransferForm.vue'
 import TaskSignCreateForm from './dialog/TaskSignCreateForm.vue'
 import { isEmpty } from '@/utils/is'
-
+import {
+  OperationButtonType,
+  OPERATION_BUTTON_NAME
+} from '@/components/SimpleProcessDesignerV2/src/consts'
 defineOptions({ name: 'ProcessInstanceBtnConatiner' })
 
 const userId = useUserStore().getUser.id // 当前登录的编号
@@ -175,15 +220,17 @@ watch(
     deep: true
   }
 )
+
+// TODO @jaosn:具体的审批任务,要不改成后端返回。让前端弱化下
 /**
  * 设置 runningTasks 中的任务
  */
-const loadRunningTask = (tasks) => {
+const loadRunningTask = (tasks: any[]) => {
   runningTask.value = {}
   auditForm.value = {}
   approveForm.value = {}
   approveFormFApi.value = {}
-  tasks.forEach((task) => {
+  tasks.forEach((task: any) => {
     if (!isEmpty(task.children)) {
       loadRunningTask(task.children)
     }
@@ -214,7 +261,7 @@ const loadRunningTask = (tasks) => {
 }
 
 /** 处理审批通过和不通过的操作 */
-const handleAudit = async (pass) => {
+const handleAudit = async (pass: any) => {
   formLoading.value = true
   try {
     const auditFormRef = proxy.$refs['formRef']
@@ -254,6 +301,7 @@ const handleAudit = async (pass) => {
 /* 抄送 TODO */
 const handleSend = () => {}
 
+// TODO 代码优化:这里 flag 改成 approve: boolean 。因为 flag 目前就只有 1 和 2
 const openPopover = (flag) => {
   passVisible.value = false
   rejectVisible.value = false
@@ -289,6 +337,24 @@ const getDetail = () => {
   emit('success')
 }
 
+/** 是否显示按钮 */
+const isShowButton = (btnType: OperationButtonType): boolean => {
+  let isShow = true
+  if (runningTask.value.buttonsSetting && runningTask.value.buttonsSetting[btnType]) {
+    isShow = runningTask.value.buttonsSetting[btnType].enable
+  }
+  return isShow
+}
+
+/** 获取按钮的显示名称 */
+const getButtonDisplayName = (btnType: OperationButtonType) => {
+  let displayName = OPERATION_BUTTON_NAME.get(btnType)
+  if (runningTask.value.buttonsSetting && runningTask.value.buttonsSetting[btnType]) {
+    displayName = runningTask.value.buttonsSetting[btnType].displayName
+  }
+  return displayName
+}
+
 defineExpose({ loadRunningTask })
 </script>
 
@@ -299,10 +365,11 @@ defineExpose({ loadRunningTask })
 
 .btn-container {
   > div {
+    display: flex;
     margin: 0 15px;
     cursor: pointer;
-    display: flex;
     align-items: center;
+
     &:hover {
       color: #6db5ff;
     }

+ 202 - 130
src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue

@@ -1,9 +1,111 @@
+<!-- 审批详情的右侧:审批流 -->
 <template>
   <el-timeline class="pt-20px">
-    <el-timeline-item v-for="(activity, index) in mockData" :key="index" size="large">
+    <!-- 遍历每个审批节点 -->
+    <el-timeline-item
+      v-for="(activity, index) in approveNodes"
+      :key="index"
+      size="large"
+      :icon="getApprovalNodeIcon(activity.status, activity.nodeType)"
+      :color="getApprovalNodeColor(activity.status)"
+    >
       <div class="flex flex-col items-start">
         <div class="font-bold"> {{ activity.name }}</div>
-        <div class="color-#a1a6ae text-12px mb-10px"> {{ activity.assigneeUser.nickname }}</div>
+        <div class="flex items-center mt-1">
+          <!-- 情况一:遍历每个审批节点下的【进行中】task 任务 -->
+          <div v-for="(task, idx) in activity.tasks" :key="idx" class="flex items-center">
+            <div class="flex items-center flex-col pr-2">
+              <div class="position-relative" v-if="task.assigneeUser || task.ownerUser">
+                <!-- 信息:头像 -->
+                <el-avatar
+                  :size="36"
+                  v-if="task.assigneeUser && task.assigneeUser.avatar"
+                  :src="task.assigneeUser.avatar"
+                />
+                <el-avatar v-else-if="task.assigneeUser && task.assigneeUser.nickname">
+                  {{ task.assigneeUser.nickname.substring(0, 1) }}
+                </el-avatar>
+                <el-avatar
+                  v-else-if="task.ownerUser && task.ownerUser.avatar"
+                  :src="task.ownerUser.avatar"
+                />
+                <el-avatar v-else-if="task.ownerUser && task.ownerUser.nickname">
+                  {{ task.ownerUser.nickname.substring(0, 1) }}
+                </el-avatar>
+                <!-- 信息:任务 ICON -->
+                <div
+                  class="position-absolute top-26px left-26px bg-#fff rounded-full flex items-center p-2px"
+                >
+                  <Icon
+                    :size="12"
+                    :icon="statusIconMap2[task.status]?.icon"
+                    :color="statusIconMap2[task.status]?.color"
+                  />
+                </div>
+              </div>
+              <div class="flex flex-col mt-1">
+                <!-- 信息:昵称 -->
+                <div
+                  v-if="task.assigneeUser && task.assigneeUser.nickname"
+                  class="text-10px text-align-center"
+                >
+                  {{ task.assigneeUser.nickname }}
+                </div>
+                <div
+                  v-else-if="task.ownerUser && task.ownerUser.nickname"
+                  class="text-10px text-align-center"
+                >
+                  {{ task.ownerUser.nickname }}
+                </div>
+                <!-- TODO @jason:审批意见,要展示哈。 -->
+                <!-- <div v-if="task.reason" :title="task.reason" class="text-13px text-truncate w-150px mt-1"> 审批意见: {{ task.reason }}</div> -->
+              </div>
+            </div>
+          </div>
+          <!-- 情况二:遍历每个审批节点下的【候选的】task 任务。例如说,1)依次审批,2)未来的审批任务等 -->
+          <div
+            v-for="(user, idx1) in activity.candidateUserList"
+            :key="idx1"
+            class="flex items-center"
+          >
+            <div class="flex items-center flex-col pr-2">
+              <div class="position-relative">
+                <!-- 信息:头像 -->
+                <el-avatar :size="36" v-if="user.avatar" :src="user.avatar" />
+                <el-avatar v-else-if="user.nickname && user.nickname">
+                  {{ user.nickname.substring(0, 1) }}
+                </el-avatar>
+                <!-- 信息:任务 ICON -->
+                <div
+                  class="position-absolute top-26px left-26px bg-#fff rounded-full flex items-center p-2px"
+                >
+                  <Icon
+                    :size="12"
+                    :icon="statusIconMap2['-1']?.icon"
+                    :color="statusIconMap2['-1']?.color"
+                  />
+                </div>
+              </div>
+              <div class="flex flex-col mt-1">
+                <!-- 信息:昵称 -->
+                <div v-if="user.nickname" class="text-10px text-align-center">
+                  {{ user.nickname }}
+                </div>
+                <!-- <div v-if="task.reason" :title="task.reason" class="text-13px text-truncate w-150px mt-1"> 审批意见: {{ task.reason }}</div> -->
+              </div>
+            </div>
+          </div>
+        </div>
+        <!-- 信息:时间 -->
+        <div
+          v-if="activity.status !== TaskStatusEnum.NOT_START"
+          class="text-#a5a5a5 text-13px mt-1"
+        >
+          {{ getApprovalNodeTime(activity) }}
+        </div>
+
+        <!-- TODO @jason:审批意见,要展示哈。 -->
+        <!-- <div class="color-#a1a6ae text-12px mb-10px"> {{ activity.assigneeUser.nickname }}</div>
         <div v-if="activity.opinion" class="text-#a5a5a5 text-12px w-100%">
           <div class="mb-5px">审批意见:</div>
           <div
@@ -14,148 +116,118 @@
         </div>
         <div v-if="activity.createTime" class="text-#a5a5a5 text-13px">
           {{ formatDate(activity.createTime) }}
-        </div>
+        </div> -->
       </div>
-      <!-- 该节点用户的头像 -->
-      <template #dot>
-        <div class="w-35px h-35px position-relative">
-          <img
-            src="@/assets/imgs/avatar.jpg"
-            class="rounded-full w-full h-full position-absolute bottom-6px right-12px"
-            alt=""
-          />
-          <div
-            class="position-absolute top-16px left-8px bg-#fff rounded-full flex items-center content-center p-2px"
-          >
-            <Icon
-              :size="12"
-              :icon="optIconMap[activity.status]?.icon"
-              :color="optIconMap[activity.status]?.color"
-            />
-          </div>
-        </div>
-      </template>
     </el-timeline-item>
   </el-timeline>
 </template>
 
 <script lang="ts" setup>
 import { formatDate } from '@/utils/formatTime'
-import { propTypes } from '@/utils/propTypes'
-
+import * as ProcessInstanceApi from '@/api/bpm/processInstance'
+import { TaskStatusEnum } from '@/api/bpm/task'
+import { NodeType } from '@/components/SimpleProcessDesignerV2/src/consts'
+import { Check, Close, Loading, Clock, Minus, Delete } from '@element-plus/icons-vue'
 defineOptions({ name: 'BpmProcessInstanceTimeline' })
-defineProps({
-  tasks: propTypes.array // 流程任务的数组
+const props = defineProps({
+  // 流程实例编号
+  processInstanceId: {
+    type: String,
+    required: false,
+    default: ''
+  },
+  // 流程定义编号
+  processDefinitionId: {
+    type: String,
+    required: false,
+    default: ''
+  }
 })
 
-const optIconMap = {
+// 审批节点
+const approveNodes = ref<ProcessInstanceApi.ApprovalNodeInfo[]>([])
+
+const statusIconMap2 = {
+  // 未开始
+  '-1': { color: '#e5e7ec', icon: 'ep-clock' },
+  // 待审批
+  '0': { color: '#e5e7ec', icon: 'ep:loading' },
   // 审批中
-  '1': {
-    color: '#00b32a',
-    icon: 'fa-solid:clock'
-  },
+  '1': { color: '#448ef7', icon: 'ep:loading' },
   // 审批通过
-  '2': { color: '#00b32a', icon: 'fa-solid:check-circle' },
+  '2': { color: '#00b32a', icon: 'ep:circle-check-filled' },
   // 审批不通过
-  '3': { color: '#f46b6c', icon: 'fa-solid:times-circle' }
+  '3': { color: '#f46b6c', icon: 'fa-solid:times-circle' },
+  // 取消
+  '4': { color: '#cccccc', icon: 'ep:delete-filled' },
+  // 回退
+  '5': { color: '#f46b6c', icon: 'ep:remove-filled' },
+  // 委派中
+  '6': { color: '#448ef7', icon: 'ep:loading' },
+  // 审批通过中
+  '7': { color: '#00b32a', icon: 'ep:circle-check-filled' }
 }
 
-const mockData: any = [
-  {
-    id: 'fe1190ee-68c3-11ef-9c7d-00a6181404fd',
-    name: '发起人',
-    createTime: 1725237646192,
-    endTime: null,
-    durationInMillis: null,
-    status: 1,
-    reason: null,
-    ownerUser: null,
-    assigneeUser: {
-      id: 104,
-      nickname: '测试号',
-      deptId: 107,
-      deptName: '运维部门'
-    },
-    taskDefinitionKey: 'task-01',
-    processInstanceId: 'fe0c60c6-68c3-11ef-9c7d-00a6181404fd',
-    processInstance: {
-      id: 'fe0c60c6-68c3-11ef-9c7d-00a6181404fd',
-      name: 'oa_leave',
-      createTime: null,
-      processDefinitionId: 'oa_leave:1:6e5ac269-5f87-11ef-bdb6-00a6181404fd',
-      startUser: null
-    },
-    parentTaskId: null,
-    children: null,
-    formId: null,
-    formName: null,
-    formConf: null,
-    formFields: null,
-    formVariables: null
-  },
-  {
-    id: 'fe1190ee-68c3-11ef-9c7d-00a6181404fd',
-    name: '领导审批',
-    createTime: 1725237646192,
-    endTime: null,
-    durationInMillis: null,
-    status: 2,
-    reason: null,
-    ownerUser: null,
-    assigneeUser: {
-      id: 104,
-      nickname: '领导',
-      deptId: 107,
-      deptName: '运维部门'
-    },
-    taskDefinitionKey: 'task-01',
-    processInstanceId: 'fe0c60c6-68c3-11ef-9c7d-00a6181404fd',
-    processInstance: {
-      id: 'fe0c60c6-68c3-11ef-9c7d-00a6181404fd',
-      name: 'oa_leave',
-      createTime: null,
-      processDefinitionId: 'oa_leave:1:6e5ac269-5f87-11ef-bdb6-00a6181404fd',
-      startUser: null
-    },
-    parentTaskId: null,
-    children: null,
-    formId: null,
-    formName: null,
-    formConf: null,
-    formFields: null,
-    formVariables: null
-  },
-  {
-    id: 'fe1190ee-68c3-11ef-9c7d-00a6181404fd',
-    name: '财务总监审核',
-    createTime: 1725237646192,
-    endTime: null,
-    durationInMillis: null,
-    status: 3,
-    reason: null,
-    ownerUser: null,
-    assigneeUser: {
-      id: 104,
-      nickname: '财务总监',
-      deptId: 107,
-      deptName: '运维部门'
-    },
-    taskDefinitionKey: 'task-01',
-    processInstanceId: 'fe0c60c6-68c3-11ef-9c7d-00a6181404fd',
-    processInstance: {
-      id: 'fe0c60c6-68c3-11ef-9c7d-00a6181404fd',
-      name: 'oa_leave',
-      createTime: null,
-      processDefinitionId: 'oa_leave:1:6e5ac269-5f87-11ef-bdb6-00a6181404fd',
-      startUser: null
-    },
-    parentTaskId: null,
-    children: null,
-    formId: null,
-    formName: null,
-    formConf: null,
-    formFields: null,
-    formVariables: null
+const statusIconMap = {
+  // 审批未开始
+  '-1': { color: '#e5e7ec', icon: Clock },
+  '0': { color: '#e5e7ec', icon: Clock },
+  // 审批中
+  '1': { color: '#448ef7', icon: Loading },
+  // 审批通过
+  '2': { color: '#00b32a', icon: Check },
+  // 审批不通过
+  '3': { color: '#f46b6c', icon: Close },
+  // 已取消
+  '4': { color: '#cccccc', icon: Delete },
+  // 回退
+  '5': { color: '#f46b6c', icon: Minus },
+  // 委派中
+  '6': { color: '#448ef7', icon: Loading },
+  // 审批通过中
+  '7': { color: '#00b32a', icon: Check }
+}
+
+/** 获得审批详情 */
+const getApprovalDetail = async () => {
+  const data = await ProcessInstanceApi.getApprovalDetail(
+    props.processInstanceId,
+    props.processDefinitionId
+  )
+  approveNodes.value = data.approveNodes
+}
+
+const getApprovalNodeIcon = (taskStatus: number, nodeType: NodeType) => {
+  if (taskStatus == TaskStatusEnum.NOT_START) {
+    return statusIconMap[taskStatus]?.icon
+  }
+
+  if (nodeType === NodeType.START_USER_NODE || nodeType === NodeType.USER_TASK_NODE) {
+    return statusIconMap[taskStatus]?.icon
+  }
+}
+
+const getApprovalNodeColor = (taskStatus: number) => {
+  return statusIconMap[taskStatus]?.color
+}
+
+const getApprovalNodeTime = (node: ProcessInstanceApi.ApprovalNodeInfo) => {
+  if (node.endTime) {
+    return `结束时间:${formatDate(node.endTime)}`
   }
-]
+  if (node.startTime) {
+    return `创建时间:${formatDate(node.startTime)}`
+  }
+}
+
+/** 重新刷新审批详情 */
+const refresh = () => {
+  getApprovalDetail()
+}
+
+defineExpose({ refresh })
+
+onMounted(async () => {
+  await getApprovalDetail()
+})
 </script>

+ 121 - 23
src/views/bpm/processInstance/detail/index.vue

@@ -56,29 +56,73 @@
           </el-form-item>
         </el-form>
         <div style="margin-bottom: 20px; margin-left: 10%; font-size: 14px">
-          <el-button type="success" @click="handleAudit(item, true)">
+          <!-- TODO @jason:建议搞个 if 来判断,替代现有的 !item.buttonsSetting || item.buttonsSetting[OpsButtonType.APPROVE]?.enable -->
+          <el-button
+            type="success"
+            v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.APPROVE]?.enable"
+            @click="handleAudit(item, true)"
+          >
             <Icon icon="ep:select" />
-            通过
+            <!-- TODO @jason:这个也是类似哈,搞个方法来生成名字 -->
+            {{
+              item.buttonsSetting?.[OperationButtonType.APPROVE]?.displayName ||
+              OPERATION_BUTTON_NAME.get(OperationButtonType.APPROVE)
+            }}
           </el-button>
-          <el-button type="danger" @click="handleAudit(item, false)">
+          <el-button
+            v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.REJECT]?.enable"
+            type="danger"
+            @click="handleAudit(item, false)"
+          >
             <Icon icon="ep:close" />
-            不通过
+            {{
+              item.buttonsSetting?.[OperationButtonType.REJECT].displayName ||
+              OPERATION_BUTTON_NAME.get(OperationButtonType.REJECT)
+            }}
           </el-button>
-          <el-button type="primary" @click="openTaskUpdateAssigneeForm(item.id)">
+          <el-button
+            v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.TRANSFER]?.enable"
+            type="primary"
+            @click="openTaskUpdateAssigneeForm(item.id)"
+          >
             <Icon icon="ep:edit" />
-            转办
+            {{
+              item.buttonsSetting?.[OperationButtonType.TRANSFER]?.displayName ||
+              OPERATION_BUTTON_NAME.get(OperationButtonType.TRANSFER)
+            }}
           </el-button>
-          <el-button type="primary" @click="handleDelegate(item)">
+          <el-button
+            v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.DELEGATE]?.enable"
+            type="primary"
+            @click="handleDelegate(item)"
+          >
             <Icon icon="ep:position" />
-            委派
+            {{
+              item.buttonsSetting?.[OperationButtonType.DELEGATE]?.displayName ||
+              OPERATION_BUTTON_NAME.get(OperationButtonType.DELEGATE)
+            }}
           </el-button>
-          <el-button type="primary" @click="handleSign(item)">
+          <el-button
+            v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.ADD_SIGN]?.enable"
+            type="primary"
+            @click="handleSign(item)"
+          >
             <Icon icon="ep:plus" />
-            加签
+            {{
+              item.buttonsSetting?.[OperationButtonType.ADD_SIGN]?.displayName ||
+              OPERATION_BUTTON_NAME.get(OperationButtonType.ADD_SIGN)
+            }}
           </el-button>
-          <el-button type="warning" @click="handleBack(item)">
+          <el-button
+            v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.RETURN]?.enable"
+            type="warning"
+            @click="handleBack(item)"
+          >
             <Icon icon="ep:back" />
-            回退
+            {{
+              item.buttonsSetting?.[OperationButtonType.RETURN]?.displayName ||
+              OPERATION_BUTTON_NAME.get(OperationButtonType.RETURN)
+            }}
           </el-button>
         </div>
       </el-col>
@@ -147,6 +191,10 @@ import TaskSignCreateForm from './dialog/TaskSignCreateForm.vue'
 import { registerComponent } from '@/utils/routerHelper'
 import { isEmpty } from '@/utils/is'
 import * as UserApi from '@/api/system/user'
+import {
+  OperationButtonType,
+  OPERATION_BUTTON_NAME
+} from '@/components/SimpleProcessDesignerV2/src/consts'
 
 defineOptions({ name: 'BpmProcessInstanceDetail' })
 
@@ -200,7 +248,11 @@ const handleAudit = async (task, pass) => {
   // 1.2 校验表单
   const elForm = unref(auditFormRef)
   if (!elForm) return
-  const valid = await elForm.validate()
+  let valid = await elForm.validate()
+  if (!valid) return
+  // 校验申请表单
+  if (!fApi.value) return
+  valid = await fApi.value.validate()
   if (!valid) return
 
   // 2.1 提交审批
@@ -216,6 +268,9 @@ const handleAudit = async (task, pass) => {
       await formCreateApi.validate()
       data.variables = approveForms.value[index].value
     }
+    // 获取表单可编辑字段的值
+    data.variables = getWritableValueOfForm(task.fieldsPermission)
+
     await TaskApi.approveTask(data)
     message.success('审批通过成功')
   } else {
@@ -251,11 +306,11 @@ const handleSign = async (task: any) => {
 }
 
 /** 获得详情 */
-const getDetail = () => {
-  // 1. 获得流程实例相关
+const getDetail = async () => {
+  // 1. 获得流程任务列表(审批记录)。 需要先获取任务,表单的权限设置需要根据任务来设置
+  await getTaskList()
+  // 2. 获得流程实例相关
   getProcessInstance()
-  // 2. 获得流程任务列表(审批记录)
-  getTaskList()
 }
 
 /** 加载流程实例 */
@@ -273,16 +328,29 @@ const getProcessInstance = async () => {
     // 设置表单信息
     const processDefinition = data.processDefinition
     if (processDefinition.formType === 10) {
-      setConfAndFields2(
-        detailForm,
-        processDefinition.formConf,
-        processDefinition.formFields,
-        data.formVariables
-      )
+      if (detailForm.value.rule.length > 0) {
+        detailForm.value.value = data.formVariables
+      } else {
+        setConfAndFields2(
+          detailForm,
+          processDefinition.formConf,
+          processDefinition.formFields,
+          data.formVariables
+        )
+      }
       nextTick().then(() => {
         fApi.value?.btn.show(false)
         fApi.value?.resetBtn.show(false)
         fApi.value?.disabled(true)
+        // 设置表单权限。后续需要改造成。只处理一个运行中的任务
+        if (runningTasks.value.length > 0) {
+          const task = runningTasks.value.at(0)
+          if (task.fieldsPermission) {
+            Object.keys(task.fieldsPermission).forEach((item) => {
+              setFieldPermission(item, task.fieldsPermission[item])
+            })
+          }
+        }
       })
     } else {
       // 注意:data.processDefinition.formCustomViewPath 是组件的全路径,例如说:/crm/contract/detail/index.vue
@@ -353,6 +421,7 @@ const loadRunningTask = (tasks) => {
     if (!task.assigneeUser || task.assigneeUser.id !== userId) {
       return
     }
+
     // 2.3 添加到处理任务
     runningTasks.value.push({ ...task })
     auditForms.value.push({
@@ -371,6 +440,35 @@ const loadRunningTask = (tasks) => {
   })
 }
 
+/**
+ * 设置表单权限
+ */
+const setFieldPermission = (field: string, permission: string) => {
+  if (permission === '1') {
+    fApi.value?.disabled(true, field)
+  }
+  if (permission === '2') {
+    fApi.value?.disabled(false, field)
+  }
+  if (permission === '3') {
+    fApi.value?.hidden(true, field)
+  }
+}
+/**
+ * 获取可以编辑字段的值
+ */
+const getWritableValueOfForm = (fieldsPermission: Object) => {
+  const fieldsValue = {}
+  if (fieldsPermission && fApi.value) {
+    Object.keys(fieldsPermission).forEach((item) => {
+      if (fieldsPermission[item] === '2') {
+        fieldsValue[item] = fApi.value.getValue(item)
+      }
+    })
+  }
+  return fieldsValue
+}
+
 /** 初始化 */
 const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
 onMounted(async () => {

+ 176 - 85
src/views/bpm/processInstance/detail/index_new.vue

@@ -1,88 +1,106 @@
 <template>
   <ContentWrap :bodyStyle="{ padding: '10px 20px' }" class="position-relative">
-    <img
-      class="position-absolute right-20px"
-      width="150"
-      :src="auditIcons[processInstance.status]"
-      alt=""
-    />
-    <div class="text-#878c93">编号:{{ id }}</div>
-    <el-divider class="!my-8px" />
-    <div class="flex items-center gap-5 mb-10px">
-      <div class="text-26px font-bold mb-5px">{{ processInstance.name }}</div>
-      <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="processInstance.status" />
-    </div>
+    <div class="processInstance-wrap-main">
+      <el-scrollbar>
+        <img
+          class="position-absolute right-20px"
+          width="150"
+          :src="auditIcons[processInstance.status]"
+          alt=""
+        />
+        <div class="text-#878c93 h-15px">编号:{{ id }}</div>
+        <el-divider class="!my-8px" />
+        <div class="flex items-center gap-5 mb-10px h-40px">
+          <div class="text-26px font-bold mb-5px">{{ processInstance.name }}</div>
+          <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="processInstance.status" />
+        </div>
 
-    <div class="flex items-center gap-5 mb-10px text-13px">
-      <div class="bg-gray-100 h-35px rounded-3xl flex items-center p-8px gap-2 dark:color-gray-600">
-        <img class="rounded-full h-28px" src="@/assets/imgs/avatar.jpg" alt="" />
-        {{ processInstance?.startUser?.nickname }}
-      </div>
-      <div class="text-#878c93"> {{ formatDate(processInstance.startTime) }} 提交 </div>
-    </div>
+        <div class="flex items-center gap-5 mb-10px text-13px h-35px">
+          <div
+            class="bg-gray-100 h-35px rounded-3xl flex items-center p-8px gap-2 dark:color-gray-600"
+          >
+            <img class="rounded-full h-28px" src="@/assets/imgs/avatar.jpg" alt="" />
+            {{ processInstance?.startUser?.nickname }}
+          </div>
+          <div class="text-#878c93"> {{ formatDate(processInstance.startTime) }} 提交 </div>
+        </div>
 
-    <el-tabs>
-      <!-- 表单信息 -->
-      <el-tab-pane label="表单信息">
-        <el-row :gutter="10">
-          <el-col :span="18" class="!flex !flex-col formCol">
-            <!-- 表单信息 -->
-            <div v-loading="processInstanceLoading" class="form-box flex flex-col mb-30px flex-1">
-              <!-- 情况一:流程表单 -->
-              <el-col
-                v-if="processInstance?.processDefinition?.formType === 10"
-                :offset="6"
-                :span="16"
-              >
-                <form-create
-                  v-model="detailForm.value"
-                  v-model:api="fApi"
-                  :option="detailForm.option"
-                  :rule="detailForm.rule"
-                />
-              </el-col>
-              <!-- 情况二:业务表单 -->
-              <div v-if="processInstance?.processDefinition?.formType === 20">
-                <BusinessFormComponent :id="processInstance.businessKey" />
-              </div>
+        <el-tabs v-model="activeTab">
+          <!-- 表单信息 -->
+          <el-tab-pane label="审批详情" name="form">
+            <div class="form-scroll-area">
+              <el-scrollbar>
+                <el-row :gutter="10">
+                  <el-col :span="18" class="!flex !flex-col formCol">
+                    <!-- 表单信息 -->
+                    <div
+                      v-loading="processInstanceLoading"
+                      class="form-box flex flex-col mb-30px flex-1"
+                    >
+                      <!-- 情况一:流程表单 -->
+                      <el-col
+                        v-if="processInstance?.processDefinition?.formType === 10"
+                        :offset="6"
+                        :span="16"
+                      >
+                        <form-create
+                          v-model="detailForm.value"
+                          v-model:api="fApi"
+                          :option="detailForm.option"
+                          :rule="detailForm.rule"
+                        />
+                      </el-col>
+                      <!-- 情况二:业务表单 -->
+                      <div v-if="processInstance?.processDefinition?.formType === 20">
+                        <BusinessFormComponent :id="processInstance.businessKey" />
+                      </div>
+                    </div>
+                  </el-col>
+                  <el-col :span="6">
+                    <!-- 审批记录时间线 -->
+                    <ProcessInstanceTimeline ref="timelineRef" :process-instance-id="id" />
+                  </el-col>
+                </el-row>
+              </el-scrollbar>
             </div>
-
-            <!-- 操作栏按钮 -->
-            <ProcessInstanceOperationButton
-              ref="operationButtonRef"
-              :processInstance="processInstance"
-              :userOptions="userOptions"
-              @success="getDetail"
+          </el-tab-pane>
+          <!-- 流程图 -->
+          <el-tab-pane label="流程图" name="diagram">
+            <ProcessInstanceBpmnViewer
+              :id="`${id}`"
+              :bpmn-xml="bpmnXml"
+              :loading="processInstanceLoading"
+              :process-instance="processInstance"
+              :tasks="tasks"
             />
-          </el-col>
-          <el-col :span="6">
-            <!-- 审批记录时间线 -->
-            <ProcessInstanceTimeline :process-instance="processInstance" :tasks="tasks" />
-          </el-col>
-        </el-row>
-      </el-tab-pane>
-      <!-- 流程图 -->
-      <el-tab-pane label="流程图">
-        <ProcessInstanceBpmnViewer
-          :id="`${id}`"
-          :bpmn-xml="bpmnXml"
-          :loading="processInstanceLoading"
-          :process-instance="processInstance"
-          :tasks="tasks"
-        />
-      </el-tab-pane>
-      <!-- 流转记录 -->
-      <el-tab-pane label="流转记录">
-        <ProcessInstanceTaskList
-          :loading="tasksLoad"
-          :process-instance="processInstance"
-          :tasks="tasks"
-          @refresh="getTaskList"
-        />
-      </el-tab-pane>
-      <!-- 流转评论 -->
-      <el-tab-pane label="流转评论"> 流转评论 </el-tab-pane>
-    </el-tabs>
+          </el-tab-pane>
+          <!-- 流转记录 -->
+          <el-tab-pane label="流转记录" name="record">
+            <ProcessInstanceTaskList
+              :loading="tasksLoad"
+              :process-instance="processInstance"
+              :tasks="tasks"
+              @refresh="getTaskList"
+            />
+          </el-tab-pane>
+          <!-- 流转评论 TODO 待开发 -->
+          <el-tab-pane label="流转评论" name="comment"> 流转评论 </el-tab-pane>
+        </el-tabs>
+
+        <div
+          class="b-t-solid border-t-1px border-[var(--el-border-color)]"
+          v-if="activeTab === 'form'"
+        >
+          <!-- 操作栏按钮 -->
+          <ProcessInstanceOperationButton
+            ref="operationButtonRef"
+            :processInstance="processInstance"
+            :userOptions="userOptions"
+            @success="refresh"
+          />
+        </div>
+      </el-scrollbar>
+    </div>
   </ContentWrap>
 </template>
 <script lang="ts" setup>
@@ -99,18 +117,22 @@ import ProcessInstanceOperationButton from './ProcessInstanceOperationButton.vue
 import ProcessInstanceTimeline from './ProcessInstanceTimeline.vue'
 import { registerComponent } from '@/utils/routerHelper'
 import * as UserApi from '@/api/system/user'
+import { FieldPermissionType } from '@/components/SimpleProcessDesignerV2/src/consts'
 import audit1 from '@/assets/svgs/bpm/audit1.svg'
 import audit2 from '@/assets/svgs/bpm/audit2.svg'
 import audit3 from '@/assets/svgs/bpm/audit3.svg'
 
 defineOptions({ name: 'BpmProcessInstanceDetail' })
-
-const { query } = useRoute() // 查询参数
+const props = defineProps<{
+  id: string // 流程实例的编号
+  taskId?: string // 任务编号
+  activityId?: string //流程活动编号,用于抄送查看
+}>()
 const message = useMessage() // 消息弹窗
-const id = query.id as unknown as string // 流程实例的编号
 const processInstanceLoading = ref(false) // 流程实例的加载中
 const processInstance = ref<any>({}) // 流程实例
 const operationButtonRef = ref()
+const timelineRef = ref()
 const bpmnXml = ref('') // BPMN XML
 const tasksLoad = ref(true) // 任务的加载中
 const tasks = ref<any[]>([]) // 任务列表
@@ -141,7 +163,7 @@ const BusinessFormComponent = ref<any>(null) // 异步组件
 const getProcessInstance = async () => {
   try {
     processInstanceLoading.value = true
-    const data = await ProcessInstanceApi.getProcessInstance(id)
+    const data = await ProcessInstanceApi.getProcessInstance(props.id)
     if (!data) {
       message.error('查询不到流程信息!')
       return
@@ -151,6 +173,15 @@ const getProcessInstance = async () => {
     // 设置表单信息
     const processDefinition = data.processDefinition
     if (processDefinition.formType === 10) {
+      // 获取表单字段权限
+      let fieldsPermission = undefined
+      if (props.taskId || props.activityId) {
+        fieldsPermission = await ProcessInstanceApi.getFormFieldsPermission({
+          processInstanceId: props.id,
+          taskId: props.taskId,
+          activityId: props.activityId
+        })
+      }
       setConfAndFields2(
         detailForm,
         processDefinition.formConf,
@@ -161,6 +192,11 @@ const getProcessInstance = async () => {
         fApi.value?.btn.show(false)
         fApi.value?.resetBtn.show(false)
         fApi.value?.disabled(true)
+        if (fieldsPermission) {
+          Object.keys(fieldsPermission).forEach((item) => {
+            setFieldPermission(item, fieldsPermission[item])
+          })
+        }
       })
     } else {
       // 注意:data.processDefinition.formCustomViewPath 是组件的全路径,例如说:/crm/contract/detail/index.vue
@@ -174,15 +210,30 @@ const getProcessInstance = async () => {
   }
 }
 
+/**
+ * 设置表单权限
+ */
+const setFieldPermission = (field: string, permission: string) => {
+  if (permission === FieldPermissionType.READ) {
+    fApi.value?.disabled(true, field)
+  }
+  if (permission === FieldPermissionType.WRITE) {
+    fApi.value?.disabled(false, field)
+  }
+  if (permission === FieldPermissionType.NONE) {
+    fApi.value?.hidden(true, field)
+  }
+}
+
 /** 加载任务列表 */
 const getTaskList = async () => {
   try {
     // 获得未取消的任务
     tasksLoad.value = true
-    const data = await TaskApi.getTaskListByProcessInstanceId(id)
+    const data = await TaskApi.getTaskListByProcessInstanceId(props.id)
     tasks.value = []
     // 1.1 移除已取消的审批
-    data.forEach((task) => {
+    data.forEach((task: any) => {
       if (task.status !== 4) {
         tasks.value.push(task)
       }
@@ -209,6 +260,19 @@ const getTaskList = async () => {
   }
 }
 
+/**
+ * 操作成功后刷新
+ */
+const refresh = () => {
+  // 重新获取详情
+  getDetail()
+  // 刷新审批详情 Timeline
+  timelineRef.value?.refresh()
+}
+
+/** 当前的Tab */
+const activeTab = ref('form')
+
 /** 初始化 */
 const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
 onMounted(async () => {
@@ -219,6 +283,33 @@ onMounted(async () => {
 </script>
 
 <style lang="scss" scoped>
+$wrap-padding-height: 30px;
+$wrap-margin-height: 15px;
+$button-height: 51px;
+$process-header-height: 194px;
+
+.processInstance-wrap-main {
+  height: calc(
+    100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 45px
+  );
+  max-height: calc(
+    100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 45px
+  );
+  overflow: auto;
+
+  .form-scroll-area {
+    height: calc(
+      100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 45px -
+        $process-header-height - 40px
+    );
+    max-height: calc(
+      100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 45px -
+        $process-header-height - 40px
+    );
+    overflow: auto;
+  }
+}
+
 .form-box {
   :deep(.el-card) {
     border: none;

+ 9 - 24
src/views/bpm/simpleWorkflow/index.vue

@@ -1,28 +1,13 @@
 <template>
-  <div>
-    <section class="dingflow-design">
-      <div class="box-scale">
-        <nodeWrap v-model:nodeConfig="nodeConfig" />
-        <div class="end-node">
-          <div class="end-node-circle"></div>
-          <div class="end-node-text">流程结束</div>
-        </div>
-      </div>
-    </section>
-  </div>
+  <SimpleProcessDesigner :model-id="modelId" />
 </template>
-<script lang="ts" setup>
-import nodeWrap from '@/components/SimpleProcessDesigner/src/nodeWrap.vue'
-defineOptions({ name: 'SimpleWorkflowDesignEditor' })
-let nodeConfig = ref({
-  nodeName: '发起人',
-  type: 0,
-  id: 'root',
-  formPerms: {},
-  nodeUserList: [],
-  childNode: {}
+<script setup lang="ts">
+import { SimpleProcessDesigner } from '@/components/SimpleProcessDesignerV2/src/'
+
+defineOptions({
+  name: 'SimpleWorkflowDesignEditor'
 })
+const { query } = useRoute() // 路由的查询
+const modelId = query.modelId as string
 </script>
-<style>
-@import url('@/components/SimpleProcessDesigner/theme/workflow.css');
-</style>
+<style lang="scss" scoped></style>

+ 8 - 3
src/views/bpm/task/copy/index.vue

@@ -111,11 +111,16 @@ const getList = async () => {
 
 /** 处理审批按钮 */
 const handleAudit = (row: any) => {
+  const query = {
+    id: row.processInstanceId,
+    activityId: undefined
+  }
+  if (row.activityId) {
+    query.activityId = row.activityId
+  }
   push({
     name: 'BpmProcessInstanceDetail',
-    query: {
-      id: row.processInstanceId
-    }
+    query: query
   })
 }
 

+ 2 - 1
src/views/bpm/task/done/index.vue

@@ -158,7 +158,8 @@ const handleAudit = (row: any) => {
   push({
     name: 'BpmProcessInstanceDetail',
     query: {
-      id: row.processInstance.id
+      id: row.processInstance.id,
+      taskId: row.id
     }
   })
 }

+ 2 - 1
src/views/bpm/task/todo/index.vue

@@ -140,7 +140,8 @@ const handleAudit = (row: any) => {
   push({
     name: 'BpmProcessInstanceDetail',
     query: {
-      id: row.processInstance.id
+      id: row.processInstance.id,
+      taskId: row.id
     }
   })
 }