1、工作流重做更新
2、@import修改为@use以应对sass升级弃用@import
3、进入系统后自动redirect到home页
已修改62个文件
已删除5个文件
已添加50个文件
15246 ■■■■ 文件已修改
package.json 25 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/bpm/category/index.ts 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/bpm/model/index.ts 20 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/bpm/processInstance/index.ts 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/bpm/simple/index.ts 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/bpm/task/index.ts 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/svgs/bpm/add-user.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/svgs/bpm/approve.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/svgs/bpm/auditor.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/svgs/bpm/cancel.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/svgs/bpm/condition.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/svgs/bpm/copy.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/svgs/bpm/finish.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/svgs/bpm/parallel.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/svgs/bpm/reject.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/svgs/bpm/running.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/svgs/bpm/simple-process-bg.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/svgs/bpm/starter.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/FormCreate/src/components/useApiSelect.tsx 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/FormCreate/src/config/useDictSelectRule.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/FormCreate/src/utils/index.ts 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesigner/src/addNode.vue 237 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesigner/src/nodeWrap.vue 297 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesigner/src/util.ts 165 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesigner/theme/workflow.css 1292 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/NodeHandler.vue 214 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/ProcessNodeTree.vue 118 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/SimpleProcessDesigner.vue 179 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/SimpleProcessModel.vue 140 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/SimpleProcessViewer.vue 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/consts.ts 570 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/index.ts 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/node.ts 495 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/nodes-config/ConditionNodeConfig.vue 419 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/nodes-config/CopyTaskNodeConfig.vue 374 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/nodes-config/StartUserNodeConfig.vue 135 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/nodes-config/UserTaskNodeConfig.vue 905 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/nodes/CopyTaskNode.vue 97 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/nodes/EndEventNode.vue 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/nodes/ExclusiveNode.vue 229 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/nodes/InclusiveNode.vue 233 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/nodes/ParallelNode.vue 184 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/nodes/StartUserNode.vue 154 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/nodes/UserTaskNode.vue 174 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/src/utils.ts 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/theme/iconfont.ttf 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/theme/iconfont.woff 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/theme/iconfont.woff2 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SimpleProcessDesignerV2/theme/simple-process-designer.scss 750 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/UserSelectForm/index.vue 152 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/designer/ProcessViewer.vue 945 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/penal/custom-config/ElementCustomConfig.vue 283 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/penal/form/ElementForm.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/penal/listeners/ElementListeners.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/penal/multi-instance/ElementMultiInstance.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/penal/properties/ElementProperties.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/penal/task/ElementTask.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/theme/element-variables.scss 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/theme/index.scss 119 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/theme/process-designer.scss 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/bpmnProcessDesigner/package/utils.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/AppView.vue 23 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Breadcrumb/src/Breadcrumb.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Footer/src/Footer.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Menu/src/Menu.vue 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Message/src/Message.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Setting/src/Setting.vue 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/TabMenu/src/TabMenu.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/TagsView/src/TagsView.vue 146 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/UserInfo/src/UserInfo.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/UserInfo/src/components/LockDialog.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/UserInfo/src/components/LockPage.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/useRenderLayout.tsx 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/plugins/formCreate/index.ts 75 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/modules/remaining.ts 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/modules/bpm/simpleWorkflow.ts 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/modules/permission.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/modules/tagsView.ts 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/constants.ts 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/dict.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/routerHelper.ts 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/category/CategoryForm.vue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/definition/index.vue 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/form/editor/index.vue 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/form/index.vue 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/group/UserGroupForm.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/model/CategoryDraggableModel.vue 532 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/model/ModelForm.vue 146 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/model/ModelImportForm.vue 141 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/model/editor/index.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/model/index.vue 469 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/model/index_old.vue 404 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processExpression/ProcessExpressionForm.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processInstance/create/ProcessDefinitionDetail.vue 259 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processInstance/create/index.vue 389 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processInstance/create/index_old.vue 266 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processInstance/detail/ProcessInstanceBpmnViewer.vue 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue 889 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processInstance/detail/ProcessInstanceSimpleViewer.vue 168 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue 176 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue 292 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processInstance/detail/dialog/TaskSignDeleteForm.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processInstance/detail/index.vue 549 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processInstance/detail/index_new.vue 319 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processInstance/index.vue 114 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processInstance/manager/index.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/processListener/ProcessListenerForm.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/simple/SimpleModelDesign.vue 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/simpleWorkflow/index.vue 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/task/copy/index.vue 27 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/task/done/index.vue 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/task/manager/index.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/task/todo/index.vue 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite.config.ts 13 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package.json
@@ -10,11 +10,11 @@
    "dev": "vite --mode env.local",
    "dev-server": "vite --mode dev",
    "ts:check": "vue-tsc --noEmit",
    "build:local": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build",
    "build:dev": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode dev",
    "build:test": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode test",
    "build:stage": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode stage",
    "build:prod": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode prod",
    "build:local": "node --max_old_space_size=4096 ./node_modules/vite/bin/vite.js build",
    "build:dev": "node --max_old_space_size=4096 ./node_modules/vite/bin/vite.js build --mode dev",
    "build:test": "node --max_old_space_size=4096 ./node_modules/vite/bin/vite.js build --mode test",
    "build:stage": "node --max_old_space_size=4096 ./node_modules/vite/bin/vite.js build --mode stage",
    "build:prod": "node --max_old_space_size=4096 ./node_modules/vite/bin/vite.js build --mode prod",
    "serve:dev": "vite preview --mode dev",
    "serve:prod": "vite preview --mode prod",
    "preview": "pnpm build:local && vite preview",
@@ -27,8 +27,8 @@
  },
  "dependencies": {
    "@element-plus/icons-vue": "^2.1.0",
    "@form-create/designer": "^3.1.3",
    "@form-create/element-ui": "^3.1.24",
    "@form-create/designer": "^3.2.6",
    "@form-create/element-ui": "^3.2.11",
    "@iconify/iconify": "^3.1.1",
    "@microsoft/fetch-event-source": "^2.0.1",
    "@videojs-player/vue": "^1.0.0",
@@ -48,7 +48,7 @@
    "driver.js": "^1.3.1",
    "echarts": "^5.5.0",
    "echarts-wordcloud": "^2.1.0",
    "element-plus": "2.7.0",
    "element-plus": "2.8.4",
    "fast-xml-parser": "^4.3.2",
    "highlight.js": "^11.9.0",
    "jsencrypt": "^3.3.2",
@@ -65,13 +65,14 @@
    "pinia-plugin-persistedstate": "^3.2.1",
    "qrcode": "^1.5.3",
    "qs": "^6.12.0",
    "sortablejs": "^1.15.3",
    "steady-xml": "^0.1.0",
    "url": "^0.11.3",
    "video.js": "^7.21.5",
    "vue": "3.4.21",
    "vue": "3.5.12",
    "vue-dompurify-html": "^4.1.4",
    "vue-i18n": "9.10.2",
    "vue-router": "^4.3.0",
    "vue-router": "4.4.5",
    "vue-types": "^5.1.1",
    "vuedraggable": "^4.1.0",
    "web-storage-cache": "^1.1.1",
@@ -97,7 +98,7 @@
    "@vitejs/plugin-vue": "^5.0.4",
    "@vitejs/plugin-vue-jsx": "^3.1.0",
    "autoprefixer": "^10.4.17",
    "bpmn-js": "8.9.0",
    "bpmn-js": "8.10.0",
    "bpmn-js-properties-panel": "0.46.0",
    "consola": "^3.2.3",
    "eslint": "^8.57.0",
@@ -132,7 +133,7 @@
    "vite-plugin-progress": "^0.0.7",
    "vite-plugin-purge-icons": "^0.10.0",
    "vite-plugin-svg-icons": "^2.0.1",
    "vite-plugin-top-level-await": "^1.3.1",
    "vite-plugin-top-level-await": "^1.4.4",
    "vue-eslint-parser": "^9.3.2",
    "vue-tsc": "^1.8.27"
  },
src/api/bpm/category/index.ts
@@ -36,6 +36,16 @@
    return await request.put({ url: `/bpm/category/update`, data })
  },
  // 批量修改流程分类的排序
  updateCategorySortBatch: async (ids: number[]) => {
    return await request.put({
      url: `/bpm/category/update-sort-batch`,
      params: {
        ids: ids.join(',')
      }
    })
  },
  // 删除流程分类
  deleteCategory: async (id: number) => {
    return await request.delete({ url: `/bpm/category/delete?id=` + id })
src/api/bpm/model/index.ts
@@ -26,11 +26,11 @@
  bpmnXml: string
}
export const getModelPage = async (params) => {
  return await request.get({ url: '/bpm/model/page', params })
export const getModelList = async (name: string | undefined) => {
  return await request.get({ url: '/bpm/model/list', params: { name } })
}
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,20 @@
  return await request.put({ url: '/bpm/model/update', data: data })
}
// 批量修改流程分类的排序
export const updateModelSortBatch = async (ids: number[]) => {
  return await request.put({
    url: `/bpm/model/update-sort-batch`,
    params: {
      ids: ids.join(',')
    }
  })
}
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 = {
src/api/bpm/processInstance/index.ts
@@ -1,6 +1,6 @@
import request from '@/config/axios'
import { ProcessDefinitionVO } from '@/api/bpm/model'
import { NodeType, CandidateStrategy } from '@/components/SimpleProcessDesignerV2/src/consts'
export type Task = {
  id: string
  name: string
@@ -20,6 +20,35 @@
  createTime: string
  endTime: string
  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
  candidateStrategy?: CandidateStrategy
  status: number
  startTime?: Date
  endTime?: Date
  candidateUsers?: User[]
  tasks: ApprovalTaskInfo[]
}
export const getProcessInstanceMyPage = async (params: any) => {
@@ -57,3 +86,18 @@
export const getProcessInstanceCopyPage = async (params: any) => {
  return await request.get({ url: '/bpm/process-instance/copy/page', params })
}
// 获取审批详情
export const getApprovalDetail = async (params: any) => {
  return await request.get({ url: 'bpm/process-instance/get-approval-detail' , params })
}
// 获取表单字段权限
export const getFormFieldsPermission = async (params: any) => {
  return await request.get({ url: '/bpm/process-instance/get-form-fields-permission', params })
}
// 获取流程实例的 BPMN 模型视图
export const getProcessInstanceBpmnModelView = async (id: string) => {
  return await request.get({ url: '/bpm/process-instance/get-bpmn-model-view?id=' + id })
}
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
  })
}
src/api/bpm/task/index.ts
@@ -1,7 +1,44 @@
import request from '@/config/axios'
export type TaskVO = {
  id: number
/**
 * 任务状态枚举
 */
export enum TaskStatusEnum {
  /**
   * 未开始
   */
  NOT_START = -1,
  /**
   * 待审批
   */
  WAIT = 0,
  /**
   * 审批中
   */
  RUNNING = 1,
  /**
   * 审批通过
   */
  APPROVE = 2,
  /**
   * 审批不通过
   */
  REJECT = 3,
  /**
   * 已取消
   */
  CANCEL = 4,
  /**
   * 已退回
   */
  RETURN = 5,
  /**
   * 审批通过中
   */
  APPROVING = 7
}
export const getTaskTodoPage = async (params: any) => {
@@ -30,12 +67,12 @@
  })
}
// 获取所有可回退的节点
// 获取所有可退回的节点
export const getTaskListByReturn = async (id: string) => {
  return await request.get({ url: '/bpm/task/list-by-return', params: { id } })
}
// 回退
// 退回
export const returnTask = async (data: any) => {
  return await request.put({ url: '/bpm/task/return', data })
}
@@ -60,6 +97,16 @@
  return await request.delete({ url: '/bpm/task/delete-sign', data })
}
// 抄送
export const copyTask = async (data: any) => {
  return await request.put({ url: '/bpm/task/copy', data })
}
// 获取我的待办任务
export const myTodoTask = async (processInstanceId: string) => {
  return await request.get({ url: '/bpm/task/my-todo?processInstanceId=' + processInstanceId })
}
// 获取减签任务列表
export const getChildrenTaskList = async (id: string) => {
  return await request.get({ url: '/bpm/task/list-by-parent-task-id?parentTaskId=' + id })
src/assets/svgs/bpm/add-user.svg
对比新文件
@@ -0,0 +1 @@
<svg t="1731390087280" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4297" width="200" height="200"><path d="M639.9 541.7c76.4-44.2 127.9-126.8 127.9-221.5C767.7 179 653.2 64.5 512 64.5S256.3 179 256.3 320.2c0 89.6 46.1 168.4 115.8 214.1C193.5 593 64.5 761.2 64.5 959.5h63.9c0-211.5 172.1-383.6 383.6-383.6 44.9 0 87.8 8.1 127.9 22.4v-56.6zM320.2 320.2c0-105.8 86-191.8 191.8-191.8s191.8 86 191.8 191.8S617.7 512 512 512s-191.8-86-191.8-191.8zM831.6 767.7V639.9h-63.9v127.8H639.9v63.9h127.8v127.9h63.9V831.6h127.9v-63.9z" fill="#5f6266" p-id="4298"></path></svg>
src/assets/svgs/bpm/approve.svg
对比新文件
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1724316565416" class="icon" viewBox="0 0 1300 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1339" xmlns:xlink="http://www.w3.org/1999/xlink" width="253.90625" height="200"><path d="M784.058182 99.258182l10.938182 18.385454-21.294546-2.56-14.196363 16.058182-4.072728-21.061818-19.781818-8.494545 18.734546-10.472728 2.094545-21.294545 15.709091 14.545454 20.945454-4.654545-9.076363 19.549091zM1067.287273 642.443636l-18.501818 10.821819 2.56-21.294546-16.058182-14.196364 21.061818-4.072727 8.494545-19.665454 10.472728 18.734545 21.294545 1.978182-14.661818 15.709091 4.770909 20.945454-19.432727-8.96z" fill="#13C463" p-id="1340"></path><path d="M1067.287273 642.443636l-18.501818 10.821819 2.56-21.294546-16.058182-14.196364 21.061818-4.072727 8.494545-19.665454 10.472728 18.734545 21.294545 1.978182-14.661818 15.709091 4.770909 20.945454-19.432727-8.96zM571.927273 100.072727l-17.454546-12.567272 20.596364-6.167273 6.516364-20.48 12.218181 17.570909 21.410909-0.116364-12.916363 17.105455 6.749091 20.363636-20.247273-6.981818-17.338182 12.683636 0.465455-21.410909zM991.418182 784.407273l-21.178182 3.490909 10.123636-18.967273-9.774545-18.967273 21.061818 3.723637 15.127273-15.243637 2.909091 21.294546 19.2 9.658182-19.316364 9.309091-3.258182 21.178181-14.894545-15.476363zM427.985455 156.741818L407.272727 151.505455l16.872728-13.265455-1.396364-21.410909 17.803636 11.985454 20.014546-7.912727-5.934546 20.596364 13.730909 16.523636-21.410909 0.814546-11.52 18.152727-7.447272-20.247273zM854.225455 896.465455l-20.712728-5.352728 16.872728-13.265454-1.396364-21.294546 17.803636 11.869091 20.014546-7.912727-5.934546 20.712727 13.730909 16.523637-21.527272 0.814545-11.403637 18.036364-7.447272-20.130909zM562.501818 923.694545l10.821818 18.385455-21.294545-2.56-14.196364 16.058182-4.072727-21.061818-19.665455-8.494546 18.734546-10.356363 1.978182-21.41091 15.709091 14.661819 20.945454-4.770909-8.96 19.54909zM242.734545 420.770909l-18.385454 10.938182 2.56-21.294546-16.058182-14.196363 21.061818-4.189091 8.494546-19.665455 10.356363 18.734546 21.410909 2.094545-14.545454 15.709091 4.654545 20.945455-19.549091-9.076364z" fill="#13C463" p-id="1341"></path><path d="M242.734545 420.770909l-18.385454 10.938182 2.56-21.294546-16.058182-14.196363 21.061818-4.189091 8.494546-19.665455 10.356363 18.734546 21.410909 2.094545-14.545454 15.709091 4.654545 20.945455-19.549091-9.076364zM700.858182 943.941818l-17.454546-12.450909 20.48-6.283636 6.516364-20.48 12.334545 17.687272 21.41091-0.116363-12.916364 17.105454 6.632727 20.363637-20.247273-7.098182-17.221818 12.683636 0.465455-21.410909zM303.592727 278.807273l-21.178182 3.490909 10.123637-18.967273-9.890909-18.967273 21.178182 3.723637 15.010909-15.243637 2.909091 21.294546 19.2 9.541818-19.316364 9.425455-3.258182 21.178181-14.778182-15.476363z" fill="#13C463" p-id="1342"></path><path d="M407.272727 90.647273a486.632727 486.632727 0 0 1 504.552728 11.636363l25.018181-14.429091A512 512 0 0 0 139.636364 546.909091l25.018181-14.429091A486.981818 486.981818 0 0 1 407.272727 90.647273zM893.323636 933.352727a486.749091 486.749091 0 0 1-504.669091-11.636363l-24.901818 14.429091A512 512 0 0 0 1161.192727 477.090909l-24.901818 13.963636a486.981818 486.981818 0 0 1-242.967273 442.298182z" fill="#13C463" p-id="1343"></path><path d="M814.545455 795.927273a327.447273 327.447273 0 0 1-258.21091 29.556363l-29.78909 17.105455A353.163636 353.163636 0 0 0 998.865455 570.181818l-29.789091 17.105455A326.865455 326.865455 0 0 1 814.545455 795.927273zM486.865455 228.072727A327.447273 327.447273 0 0 1 744.727273 198.516364l29.789091-17.105455A353.163636 353.163636 0 0 0 302.545455 453.818182l29.78909-17.105455A326.865455 326.865455 0 0 1 486.865455 228.072727zM1288.378182 374.690909a53.294545 53.294545 0 0 1-14.429091 11.636364L229.469091 989.090909a53.876364 53.876364 0 0 1-73.425455-19.665454L7.214545 710.632727a53.527273 53.527273 0 0 1 19.781819-73.309091L1071.476364 34.909091a53.876364 53.876364 0 0 1 73.425454 19.665454l148.829091 258.327273a53.061818 53.061818 0 0 1 5.352727 40.727273 55.272727 55.272727 0 0 1-10.705454 21.061818zM32.232727 665.716364A28.043636 28.043636 0 0 0 29.323636 698.181818l148.829091 257.978182a28.392727 28.392727 0 0 0 38.516364 10.356364l1044.48-601.949091a28.16 28.16 0 0 0 10.356364-38.516364L1122.676364 67.84a28.276364 28.276364 0 0 0-38.4-10.356364L39.68 659.432727a27.810909 27.810909 0 0 0-7.447273 6.283637z" fill="#13C463" p-id="1344"></path><path d="M356.770909 569.250909l22.341818 38.749091-15.476363 8.727273L349.090909 592.64l-153.483636 88.785455 14.778182 25.483636-15.476364 8.96-23.272727-39.912727L256 627.2c-6.283636-4.887273-11.636364-8.843636-16.174545-11.636364L256 602.647273c3.956364 3.141818 9.774545 8.261818 17.338182 15.127272z m-17.338182 199.447273l-49.221818 28.392727 7.563636 13.149091-15.476363 8.96-62.138182-107.52 64.814545-37.469091-12.8-22.574545 15.941819-9.192728 12.8 22.109091 65.396363-37.701818 61.672728 106.821818-15.476364 8.96-7.214546-12.450909-49.92 28.858182 26.065455 45.032727-16.058182 9.192728z m-46.545454-79.825455L244.363636 717.265455l14.778182 25.6 49.221818-28.509091zM267.636364 756.945455l14.778181 25.6 49.221819-28.509091-14.778182-25.483637z m106.938181-80.523637l-14.778181-25.483636-49.92 28.741818 14.778181 25.483636zM346.996364 744.727273l49.803636-28.741818-14.661818-25.483637-49.92 28.741818zM505.832727 609.978182c-4.654545 6.283636-10.123636 13.265455-16.523636 21.061818l35.84 62.021818a18.967273 18.967273 0 0 1-6.749091 29.672727l-19.316364 11.636364-12.450909-13.847273a170.123636 170.123636 0 0 0 17.803637-8.727272 8.494545 8.494545 0 0 0 2.909091-13.614546L477.090909 645.352727l-9.890909 10.472728-10.007273 10.24-12.683636-13.149091c9.309091-8.261818 17.221818-15.941818 23.272727-23.272728l-31.301818-54.341818-25.018182 14.545455-8.843636-15.36 25.018182-14.429091-23.272728-41.076364 15.476364-8.96 23.272727 41.076364L465.454545 538.763636l8.843637 15.36-22.109091 12.567273 28.509091 49.221818c5.469091-6.516364 10.938182-13.498182 16.407273-21.061818z m9.076364-45.730909L572.043636 663.272727a207.825455 207.825455 0 0 0 23.272728-27.461818l11.636363 13.149091a365.381818 365.381818 0 0 1-41.774545 45.498182l-12.567273-12.567273a11.636364 11.636364 0 0 0 1.745455-13.963636L453.818182 493.963636l15.709091-9.076363 36.887272 63.883636 31.301819-18.152727 8.96 15.592727z m129.745454 83.316363a20.596364 20.596364 0 0 1-31.418181-9.774545l-103.098182-178.618182 15.709091-9.192727 38.632727 67.025454a200.261818 200.261818 0 0 0 28.043636-41.076363l16.872728 7.68a303.243636 303.243636 0 0 1-35.723637 49.338182l53.410909 93.090909a9.192727 9.192727 0 0 0 13.963637 4.072727l10.821818-6.283636a14.312727 14.312727 0 0 0 8.029091-11.636364 103.447273 103.447273 0 0 0-15.243637-39.098182l17.338182-3.84c12.567273 25.134545 18.036364 41.658182 16.290909 49.803636A28.392727 28.392727 0 0 1 663.272727 636.741818zM860.276364 521.774545c-7.563636 4.421818-20.829091 11.636364-39.912728 22.574546a179.432727 179.432727 0 0 1-37.352727 16.174545 58.181818 58.181818 0 0 1-33.047273-1.978181 14.312727 14.312727 0 0 0-11.636363-0.581819c-5.352727 3.025455-8.261818 18.385455-8.727273 45.847273l-18.269091-3.956364c1.047273-25.483636 5.003636-42.821818 11.636364-52.014545l-38.865455-67.374545-31.534545 18.152727-8.378182-14.661818 46.545454-26.647273 47.825455 82.850909a55.505455 55.505455 0 0 1 8.494545 1.861818 59.694545 59.694545 0 0 0 25.367273 4.072727 101.701818 101.701818 0 0 0 33.512727-11.636363L849.454545 508.509091l31.418182-18.734546c11.636364-7.214545 19.898182-12.334545 24.087273-15.127272l5.469091 18.152727zM676.072727 413.207273L671.185455 430.545455a279.272727 279.272727 0 0 0-58.181819-13.265455l4.887273-16.64a307.781818 307.781818 0 0 1 58.181818 12.567273zM754.967273 372.363636a261.818182 261.818182 0 0 0 20.247272-38.516363l-98.443636 56.785454-7.796364-13.498182 119.97091-69.46909 6.632727 11.636363a281.134545 281.134545 0 0 1-25.949091 54.807273l5.236364 0.930909L818.734545 349.090909l57.25091 99.025455a18.385455 18.385455 0 0 1-8.843637 27.927272l-18.385454 10.589091-11.636364-11.636363 17.92-9.425455a7.796364 7.796364 0 0 0 3.607273-11.636364L849.454545 437.410909l-37.236363 21.527273 21.992727 38.050909-14.894545 8.610909-21.992728-38.167273L760.203636 488.727273l22.458182 38.749091-15.127273 8.727272L699.461818 418.909091l55.389091-32a306.269091 306.269091 0 0 0-39.330909-1.047273l4.305455-15.127273c13.265455-0.232727 24.901818 0.465455 35.141818 1.629091z m15.825454 49.454546l-11.636363-20.014546-37.003637 21.410909 11.636364 20.014546z m-29.44 34.909091l11.636364 19.549091 37.003636-21.410909-11.636363-19.549091z m81.454546-64.814546l-11.636364-19.898182-37.236364 21.527273 11.636364 19.898182z m-29.556364 34.909091l11.636364 19.432727 37.236363-21.527272-11.636363-19.432728zM1086.370909 391.214545l-19.898182 11.636364-10.589091 6.167273-10.938181 6.050909a186.181818 186.181818 0 0 1-38.749091 16.989091 60.16 60.16 0 0 1-33.978182-1.978182 14.312727 14.312727 0 0 0-11.636364 0c-5.585455 3.258182-8.610909 18.734545-8.96 46.545455l-18.036363-3.723637c0.814545-26.181818 4.770909-43.752727 11.636363-52.945454l-38.865454-67.141819-31.883637 18.385455-8.727272-15.010909 47.243636-27.345455 47.941818 83.2h4.189091a32.465455 32.465455 0 0 1 4.538182 1.163637 71.68 71.68 0 0 0 26.298182 3.490909 112.872727 112.872727 0 0 0 34.210909-13.265455c16.523636-9.192727 31.767273-17.803636 46.545454-25.949091l14.545455-8.727272 14.196363-8.727273c11.636364-6.865455 18.618182-11.636364 22.574546-14.196364l5.352727 18.385455zM896 286.021818l-4.770909 18.385455a296.378182 296.378182 0 0 0-58.181818-14.661818l4.770909-16.872728a311.156364 311.156364 0 0 1 58.181818 13.149091zM1031.098182 384l-12.334546-13.149091c11.636364-5.934545 21.76-11.636364 30.138182-15.941818a9.658182 9.658182 0 0 0 4.189091-14.661818l-54.341818-94.138182-83.781818 48.290909-9.076364-15.709091 83.781818-48.407273-20.712727-35.84 16.174545-9.425454 20.712728 36.072727 32.814545-18.967273 8.610909 15.243637-32.349091 18.850909 56.552728 97.978182a20.247273 20.247273 0 0 1-8.843637 31.185454z m-23.272727-59.345455L1000.727273 340.48a405.876364 405.876364 0 0 0-58.181818-25.6l7.796363-15.127273a393.890909 393.890909 0 0 1 57.716364 24.436364z" fill="#13C463" p-id="1345"></path></svg>
src/assets/svgs/bpm/auditor.svg
对比新文件
@@ -0,0 +1 @@
<svg t="1729561718271" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8640" width="200" height="200"><path d="M908.5952 920.4224H164.7616a31.0784 31.0784 0 0 1-30.976-30.976c0-17.0496 13.9264-30.976 30.976-30.976h743.8336c17.0496 0 31.0272 13.9264 31.0272 30.976a31.0784 31.0784 0 0 1-31.0272 30.976z m0-123.9552H164.7616a31.0784 31.0784 0 0 1-30.976-30.976v-154.9824c0-51.1488 41.8304-92.9792 92.9792-92.9792h198.3488c-6.1952-37.1712-24.7808-72.8064-51.1488-103.8336a216.576 216.576 0 0 1-54.2208-144.128c0-58.88 23.2448-114.688 66.6112-156.4672C429.7728 71.168 485.5296 51.0976 545.9968 52.6848c111.5648 4.608 206.08 100.6592 207.616 212.2752 1.536 55.808-20.1216 110.0288-57.344 151.8592-26.3168 27.904-41.8304 61.952-48.0256 100.7104h198.3488c51.2 0 93.0304 41.8304 93.0304 92.9792v154.9824a31.0784 31.0784 0 0 1-31.0272 30.976z m-712.8064-61.952H877.568v-124.0064a31.0784 31.0784 0 0 0-31.0272-30.976h-232.448a31.0784 31.0784 0 0 1-30.976-31.0272c0-65.024 23.2448-127.0784 66.6624-173.568 27.8528-29.3888 41.8304-68.1472 41.8304-108.4416-1.536-80.5888-68.1984-148.7872-148.7872-151.8592a150.528 150.528 0 0 0-113.152 43.3664 153.6 153.6 0 0 0-48.0256 111.616c0 37.1712 13.9776 74.3424 38.7584 102.2464 44.9536 51.1488 69.7344 113.152 69.7344 176.64a31.0784 31.0784 0 0 1-30.976 31.0272h-232.448a31.0784 31.0784 0 0 0-30.976 30.976v123.9552z" fill="#fff" p-id="8641"></path></svg>
src/assets/svgs/bpm/cancel.svg
对比新文件
@@ -0,0 +1 @@
<svg t="1729178183592" class="icon" viewBox="0 0 1300 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4332" width="200" height="200"><path d="M784.074702 99.196443l10.927871 18.473304-21.302843-2.56935-14.180213 16.066571-4.130475-21.042655-19.676671-8.521137 18.733492-10.440019 2.016452-21.335366 15.708814 14.603017 20.945085-4.683373-9.041512 19.449008zM1067.22363 642.402668l-18.440781 10.92787 2.56935-21.302842-16.099094-14.180213 21.042655-4.130475 8.521137-19.676671 10.440019 18.733492 21.367889 2.016452-14.603017 15.708814 4.683373 20.945085-19.481531-9.041512z" fill="#8a8a8a" p-id="4333"></path><path d="M1067.22363 642.402668l-18.440781 10.92787 2.56935-21.302842-16.099094-14.180213 21.042655-4.130475 8.521137-19.676671 10.440019 18.733492 21.367889 2.016452-14.603017 15.708814 4.683373 20.945085-19.481531-9.041512zM571.924408 100.009528l-17.400031-12.488994 20.52228-6.211974 6.504685-20.457234 12.261331 17.595172 21.432936-0.09757-12.944323 17.074798 6.732349 20.359663-20.262093-7.02506-17.269938 12.716659 0.422804-21.46546zM991.444053 784.43246l-21.172749 3.480006 10.114785-18.928632-9.822074-19.026203 21.107702 3.772717 15.090868-15.253486 2.927109 21.237796 19.156296 9.626933-19.318914 9.366746-3.219819 21.205273-14.863204-15.48115zM428.008258 156.795426l-20.749945-5.333841 16.879657-13.237034-1.365983-21.400413 17.822836 11.936097 19.936859-7.870669-5.88674 20.619851 13.692361 16.521899-21.432936 0.813086-11.513292 18.083024-7.382817-20.132zM854.260251 896.475655l-20.749945-5.333841 16.879657-13.237034-1.365983-21.400413 17.822836 11.96862 19.936859-7.903192-5.854217 20.619851 13.659838 16.554423-21.432936 0.780562-11.513292 18.115547-7.382817-20.164523zM562.460092 923.665237l10.895347 18.440782-21.302843-2.569351-14.180212 16.099095-4.130475-21.042655-19.676672-8.521137 18.733493-10.440019 2.016452-21.36789 15.708814 14.603018 20.945085-4.683373-9.008989 19.48153zM242.787359 420.788058l-18.473305 10.895347 2.569351-21.302843-16.066572-14.180213 21.042656-4.130474 8.521137-19.676672 10.440019 18.733492 21.335366 2.016453-14.603018 15.708813 4.683374 20.945085-19.449008-9.008988z" fill="#8a8a8a" p-id="4334"></path><path d="M242.787359 420.788058l-18.473305 10.895347 2.569351-21.302843-16.066572-14.180213 21.042656-4.130474 8.521137-19.676672 10.440019 18.733492 21.335366 2.016453-14.603018 15.708813 4.683374 20.945085-19.449008-9.008988zM700.814737 943.959854l-17.400032-12.521518 20.522281-6.211974 6.504685-20.42471 12.26133 17.595172 21.432937-0.130094-12.944323 17.107321 6.732349 20.359663-20.262093-7.025059-17.269938 12.684135 0.422804-21.432936zM303.541115 278.823313l-21.140226 3.480006 10.114785-18.928633-9.854597-19.058726 21.107702 3.772717 15.090868-15.220962 2.927109 21.237796 19.156296 9.626933-19.28639 9.366746-3.252342 21.172749-14.863205-15.448626z" fill="#8a8a8a" p-id="4335"></path><path d="M407.648595 90.642782a486.713038 486.713038 0 0 1 504.568397 11.578339l25.010513-14.407877A512.081309 512.081309 0 0 0 139.850723 547.401747l24.977989-14.407877a486.778085 486.778085 0 0 1 242.819883-442.351088zM893.28836 933.422265a486.810608 486.810608 0 0 1-504.568398-11.610863l-25.010513 14.407877a512.081309 512.081309 0 0 0 797.5394-459.621026l-24.97799 14.505447a486.843132 486.843132 0 0 1-242.982499 442.318565z" fill="#8a8a8a" p-id="4336"></path><path d="M814.061299 795.880705a326.665269 326.665269 0 0 1-258.170939 29.563792l-29.791456 17.172368a353.236906 353.236906 0 0 0 472.793013-272.448721l-29.693886 17.172367a326.762839 326.762839 0 0 1-155.136732 208.540194zM486.875655 228.119295a326.795363 326.795363 0 0 1 258.170939-29.563792l29.791456-17.172368a353.236906 353.236906 0 0 0-472.793013 272.448721l29.82398-17.172367a326.762839 326.762839 0 0 1 155.006638-208.540194zM1288.350389 374.73489a53.923837 53.923837 0 0 1-14.34283 12.001143L229.420232 988.712085A53.793743 53.793743 0 0 1 156.112434 968.937843l-148.924757-258.235985a53.76122 53.76122 0 0 1 19.741718-73.437891L1071.516722 35.352962A53.826266 53.826266 0 0 1 1144.82452 55.062157l148.827187 258.268508a53.793743 53.793743 0 0 1-5.398888 61.404225zM32.19819 665.754486a28.360426 28.360426 0 0 0-5.626553 10.73273 28.067715 28.067715 0 0 0 2.699444 21.432936L178.195839 956.188661a28.165285 28.165285 0 0 0 38.442687 10.342449l1044.587328-601.976052a28.132762 28.132762 0 0 0 10.440019-38.442687l-148.924758-258.268509a28.197808 28.197808 0 0 0-38.442687-10.342449L39.711101 659.444942a28.230332 28.230332 0 0 0-7.512911 6.309544z" fill="#8a8a8a" p-id="4337"></path><path d="M498.941845 597.390249l-138.322121 79.877529 38.637827 66.933207q8.000762 13.854979 21.595554 5.98431l114.254788-65.957504a21.172749 21.172749 0 0 0 9.952167-11.123011q2.634397-9.757027-16.91218-47.321582l18.440781-4.130474q20.489757 43.22363 18.148071 56.167953a36.166047 36.166047 0 0 1-16.261712 19.514054l-123.068636 71.031158q-25.17313 14.603017-40.394092-11.77348L317.103383 639.020232l16.066571-9.269176 18.570875 32.133143 122.027886-70.47826-33.596697-58.249452-150.160648 86.707448-9.041511-15.611243 166.454883-96.106718zM691.903319 563.663459c-3.935334 3.837764-9.757027 9.399269-17.497602 16.619469l23.319295 40.394093-15.611244 9.008988-21.237795-36.816516q-31.027346 27.709957-64.754137 54.314118l-12.814229-13.39965 9.171605-7.382818 9.236653-7.122629-79.714912-138.126982-17.627696 10.179832-8.781324-15.155915L601.683341 414.836271l6.960013 12.06619 86.34969-49.858408 8.488614 14.733111q28.197808 65.82741 30.506972 123.39387a274.660314 274.660314 0 0 0 69.339939 27.612387l-3.642623 18.440781a322.177037 322.177037 0 0 1-65.534699-26.40902 220.899095 220.899095 0 0 1-15.38358 72.819946l-18.14807-6.179451a215.272542 215.272542 0 0 0 15.448626-77.340702 312.940384 312.940384 0 0 1-89.374369-86.739971l-8.748801 5.138701-7.2202-12.488995-17.172368 9.919644 71.876767 124.499667q10.570113-10.017215 17.465079-16.61947z m-134.32174-56.948515l40.166428-23.189202-19.969382-34.702493-40.166429 23.189201z m28.067714 48.785135l40.166429-23.189201-19.514055-33.921931-40.166428 23.189201z m48.557472-8.813847l-40.166428 23.189201 21.888264 37.922312q13.334604-10.92787 35.775766-30.767159z m7.2202-117.832365A289.848753 289.848753 0 0 0 715.515325 503.365031a330.437986 330.437986 0 0 0-26.441544-101.92841zM812.760362 400.460918l-4.813467 17.95293a280.482007 280.482007 0 0 0-56.167953-12.781706l5.073654-17.530125a291.637542 291.637542 0 0 1 55.907766 12.358901z m24.360045 28.78323a925.063745 925.063745 0 0 1 10.017214 101.895887l-18.440781 2.016452a812.792886 812.792886 0 0 0-8.878895-101.375512z m-45.923075-86.25212l-4.813467 18.017977a290.922026 290.922026 0 0 0-58.542163-11.513292l5.073655-17.497602a308.972527 308.972527 0 0 1 58.281975 10.992917z m48.459902-17.562649l-9.334223 13.724885A298.792695 298.792695 0 0 0 783.814515 315.477211l9.757027-14.180212a437.635191 437.635191 0 0 1 46.085692 24.13238zM834.355916 269.944418l16.521899-9.529363 35.157821 60.916373 48.199714-27.840051L1003.282579 413.047483q12.716659 22.115928-8.228426 34.214642l-26.018739 15.058345-13.237034-13.009369 25.238177-13.952549c6.992536-4.065428 8.45609-9.561887 4.423186-16.554423l-12.716659-22.018358-80.527997 46.475973L919.762427 491.1037l-16.066572 9.269176-81.926505-141.899698 47.744387-27.579864z m107.750103 73.763125l-14.830682-25.660981-80.56052 46.508496 14.830681 25.726028z m-72.592282 60.330952l14.700587 25.433317 80.560521-46.508496-14.700587-25.433318z m45.532793-166.064603a222.720407 222.720407 0 0 1-2.406733 56.13543l-16.456853 0.878132a242.722312 242.722312 0 0 0 2.081499-55.647578z" fill="#8a8a8a" p-id="4338"></path></svg>
src/assets/svgs/bpm/condition.svg
对比新文件
@@ -0,0 +1 @@
<svg t="1729585232424" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1602" width="200" height="200"><path d="M925.5 898.9H804.9c-19 0-34.5-15.4-34.5-34.4V761.3c0-19 15.4-34.4 34.5-34.4h34.5V572.2c0-19-15.4-34.4-34.5-34.4H529.2V727h34.5c19 0 34.5 15.4 34.5 34.4v103.2c0 19-15.4 34.4-34.5 34.4H443.1c-19 0-34.5-15.4-34.5-34.4V761.3c0-19 15.4-34.4 34.5-34.4h34.5V537.8H219.1c-19 0-34.5 15.4-34.5 34.4V727h34.5c19 0 34.5 15.4 34.5 34.4v103.2c0 19-15.4 34.4-34.5 34.4H98.5c-19 0-34.5-15.4-34.5-34.4V761.3c0-19 15.4-34.4 34.5-34.4H133V555c0-38 30.9-68.8 68.9-68.8h275.7V297.1h-34.5c-19 0-34.5-15.4-34.5-34.4V159.5c0-19 15.4-34.4 34.5-34.4h120.6c19 0 34.5 15.4 34.5 34.4v103.2c0 19-15.4 34.4-34.5 34.4h-34.5v189.2h292.9c38.1 0 68.9 30.8 68.9 68.8v172h34.5c19 0 34.5 15.4 34.5 34.4v103.2c0 18.8-15.4 34.2-34.5 34.2z m0 0" p-id="1603" fill="#fff"></path></svg>
src/assets/svgs/bpm/copy.svg
对比新文件
@@ -0,0 +1 @@
<svg t="1729649333541" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1644" width="200" height="200"><path d="M647.888 893.84L491.904 571.52l393.888-393.888-237.904 716.208zM872.32 123.232L459.872 535.68 134.96 380.88 872.32 123.232z m90.72-68.32a23.968 23.968 0 0 0-24.784-5.568L64.08 354.816a23.984 23.984 0 0 0-2.4 44.32l381.392 181.728 187.36 387.088a24.048 24.048 0 0 0 23.152 13.504 24.032 24.032 0 0 0 21.232-16.4L968.96 79.552c2.88-8.672 0.592-18.24-5.92-24.64z" fill="#fff" p-id="1645"></path></svg>
src/assets/svgs/bpm/finish.svg
对比新文件
@@ -0,0 +1 @@
<svg t="1730189225011" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2651" id="mx_n_1730189225011" width="200" height="200"><path d="M793.889347 200.380242c27.648573 20.615681 42.196018 32.710677 63.781037 56.119312 25.313864 27.453234 43.242957 48.52047 64.502857 86.507991 44.537416 79.580127 53.527718 136.949077 53.517684 212.063821 0 64.933675-15.452562 130.459388-40.138263 187.311893-22.076044 50.841799-61.545336 104.359483-101.886297 138.933914-45.506755 39.001681-81.214423 60.462941-137.605337 81.826531-55.699867 21.102023-114.070267 28.641326-181.379458 27.791064-68.274516-0.862973-129.364283-11.040029-180.533878-31.80489-46.159002-18.731189-98.338744-46.827973-141.596418-87.541551-43.946046-41.361142-70.369064-75.958317-93.88139-127.198155-26.157437-57.004361-40.094111-129.065922-39.680686-191.781288 0-36.980719 4.033895-70.902234 12.252873-105.241856 8.532726-35.651474 20.069131-69.572989 38.13135-102.35257 18.856956-34.221214 36.754607-62.067803 58.869452-88.973149 23.248751-28.285434 39.2104-46.417894 64.295476-63.475987 18.297696-12.442861 36.879036-9.295353 47.199252-2.306612 4.403836 2.982273 8.919391 6.577992 12.933218 12.933217 9.572307 15.156208-0.334486 29.769212-6.69038 38.465836-7.148625 9.781026-23.130343 26.023643-38.738775 43.218205-38.192895 42.075603-55.133918 65.965228-74.986303 106.965794-30.772668 63.552249-37.495827 115.718611-38.131349 166.573791-0.668971 53.517684 9.995096 99.647251 27.427813 140.483919 33.916163 80.572211 94.807915 144.44289 175.270414 178.615938 41.108271 17.845472 113.812713 37.319888 181.960793 38.13135 56.193568 0.668971 125.919751-11.321666 166.574459-28.096784 45.935566-18.954626 97.223569-56.862539 127.10383-94.324918 23.013273-28.852721 52.179742-70.910931 64.413884-105.694749 14.863868-42.260239 24.806784-87.661297 24.559934-132.458943 0-54.414105-11.53373-108.417461-36.918505-156.856317-20.16747-38.483228-46.480777-74.607665-84.66899-108.048189-13.377414-11.714352-23.822728-20.067124-38.808348-31.619586-10.191774-7.857065-36.059546-25.027545-28.923632-47.326356 4.970455-15.53217 18.303717-25.294464 31.887843-27.205046 19.456354-2.736092 28.565733 2.427027 43.705885 12.041479l6.179955 4.322891zM510.755379 531.65738c-8.696624-0.668971-10.034566-0.446204-20.738102-6.689711-11.031333-6.434832-17.839451-21.183637-16.514219-35.175166V92.220334c0-18.178619 0.386665-22.815926 8.988295-31.685813 5.351768-5.519011 10.963097-11.381873 26.08987-11.539751 16.055305-0.167243 21.407073 3.846584 27.929542 9.700081 9.70677 8.711341 10.703537 17.56049 10.377078 33.525483v397.5715c-0.509756 15.273947 0.326458 22.967114-11.380535 33.502739-3.884046 3.495374-8.027653 7.693167-20.96087 8.362138l-3.791059 0.000669z m4.453341 0.573308" p-id="2652" fill="#ffffff"></path></svg>
src/assets/svgs/bpm/parallel.svg
对比新文件
@@ -0,0 +1 @@
<svg t="1729585239190" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1755" width="200" height="200"><path d="M901.489435 536.822664v-0.931601l-1.001722-198.240726c-0.100172-19.162936-9.21584-37.474409-25.043042-50.246361-14.024104-11.349507-32.265456-17.60025-51.348255-17.610268l-618.062295-0.18031c-19.142902 0-37.424323 6.280795-51.478478 17.690405-15.827203 12.842072-24.902802 31.2437-24.892785 50.486775v196.798247A114.987635 114.987635 0 1 0 195.295664 536.922836V338.782282c1.15198-1.252152 4.808264-3.596181 10.768509-3.596181l276.725622 0.090155v199.753326a114.987635 114.987635 0 1 0 65.612772 1.412428V335.326342l275.693849 0.080138c6.01033 0 9.626546 2.344029 10.768508 3.596181l1.001722 195.70637a114.987635 114.987635 0 1 0 65.592737 2.113633zM214.979496 645.910158a56.437001 56.437001 0 1 1-56.437001-56.437001 56.507122 56.507122 0 0 1 56.437001 56.437001z m354.689623 0a56.437001 56.437001 0 1 1-56.437001-56.437001 56.507122 56.507122 0 0 1 56.437001 56.437001z m295.507904 56.437001a56.437001 56.437001 0 1 1 56.437001-56.437001 56.507122 56.507122 0 0 1-56.457035 56.437001z" p-id="1756" fill='#fff'></path></svg>
src/assets/svgs/bpm/reject.svg
对比新文件
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1724316570161" class="icon" viewBox="0 0 1185 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1505" xmlns:xlink="http://www.w3.org/1999/xlink" width="231.4453125" height="200"><path d="M414.276535 230.004913l-2.443086-31.647244 26.446614 17.351559 29.437984-11.852598-8.143622 30.31685 20.423559 24.221229-31.623055 1.475527-16.722646 26.801386-11.239811-29.760504-30.663559-7.522772zM581.664252 176.902047l13.884472-28.542992 14.206993 28.220473 31.42148 4.321763-22.350614 22.092599 5.684409 31.123149-28.180157-14.513385-27.897953 14.819779 5.28126-31.066709-22.76989-21.689448zM896.507969 672.735748l17.754708 26.398236-31.494047-2.064126-19.560819 24.705008-7.95011-30.502299-29.575055-11.02211 26.744945-16.771024 1.104629-31.526299 24.414741 20.197795 30.268472-8.619338zM777.030551 801.961323l2.112504 31.647244-26.446614-17.682142-29.413795 11.546205 8.466141-30.308787-20.092976-24.221229 31.606929-1.153008 17.045166-26.793323 10.86085 29.704063 30.647433 7.837229zM609.312252 853.451591l-14.198929 28.518803-14.110236-28.542992-31.405355-4.636221 22.673134-22.084535-5.36189-31.12315 27.833449 14.835906 28.188221-14.803654-5.28126 31.066709 22.76989 22.060346zM298.435528 354.828094l-17.448315-26.390173 31.485984 2.394709 19.875275-24.753386 7.611465 30.865134 29.583118 11.288189-27.011024 16.779087-1.419086 31.526299-24.084158-20.504189-30.518425 8.280693zM962.56 91.53915a43.636913 43.636913 0 0 1 59.375874 15.601889l138.627024 236.753638c12.175118 20.447748 5.12 47.208819-15.609953 59.375874L229.13411 938.185575a43.636913 43.636913 0 0 1-59.375874-15.60189L31.12315 685.773606a43.636913 43.636913 0 0 1 15.601889-59.319433z m25.672567 24.108346a13.594205 13.594205 0 0 0-10.441575 1.548095L61.625449 652.054173a13.586142 13.586142 0 0 0-4.853921 18.83515l138.643149 236.793953a13.586142 13.586142 0 0 0 18.843213 4.837795l915.818834-534.915024a13.957039 13.957039 0 0 0 5.160315-18.778708l-138.602834-236.78589a13.594205 13.594205 0 0 0-8.401638-6.393953z" fill="#F5222D" p-id="1506"></path><path d="M395.981606 172.338394c123.670173-72.349228 271.11811-69.462677 388.394331-5.12l29.623433-17.335433a414.574866 414.574866 0 0 0-112.107842-47.071748 429.991307 429.991307 0 0 0-162.009701-10.498016 412.792945 412.792945 0 0 0-158.80063 54.707401 417.856504 417.856504 0 0 0-125.363402 111.922394A426.282331 426.282331 0 0 0 185.206929 405.004094a417.348535 417.348535 0 0 0-13.529701 120.977134l29.623433-17.335433c1.386835-133.958551 70.688252-263.958173 194.672882-336.307401z m397.666772 679.484472c-123.670173 72.365354-271.110047 69.462677-388.394331 5.128063l-29.623433 17.335433a414.679685 414.679685 0 0 0 112.075591 47.087874 429.991307 429.991307 0 0 0 162.009701 10.498016 412.744567 412.744567 0 0 0 158.808692-54.707402 423.145827 423.145827 0 0 0 209.105638-378.976756l-29.623433 17.335434c-1.072378 133.974677-70.712441 263.95011-194.350362 336.307401h-0.008063z" fill="#F5222D" p-id="1507"></path><path d="M478.377323 313.110173a226.271748 226.271748 0 0 1 109.979212-31.219905l45.668788-26.761071c-58.634079-9.13537-118.316346 2.314079-170.612914 32.735748a258.693039 258.693039 0 0 0-111.91433 132.71685l45.67685-26.761071a230.359685 230.359685 0 0 1 81.097575-80.589606l0.104819-0.120945z m232.568945 397.674835a226.328189 226.328189 0 0 1-109.979213 31.227968l-45.668787 26.753008c58.634079 9.13537 118.316346-2.314079 170.612913-32.735748a258.709165 258.709165 0 0 0 111.914331-132.71685l-45.676851 26.761071a225.215496 225.215496 0 0 1-81.097574 80.597669l-0.104819 0.112882zM188.57726 706.938961l-10.062614-17.424126 109.938897-63.471874 9.578835 16.585574 17.093543-9.869102-18.770645-32.509984-63.689575 36.767244c-4.047622-3.918614-7.804976-7.337323-11.272063-10.24l-16.859717 13.747401c3.249386 2.144756 6.595528 4.458835 9.869103 7.038993l-62.173733 35.896441 19.254426 33.348535 17.093543-9.869102zM317.44 781.142677l-19.060913-33.017953 32.679307-18.867401 4.741039 8.216189 17.093543-9.869103-48.474708-83.959937-49.772851 28.736504-7.933984-13.747401-17.432189 10.062614 7.933984 13.747402-49.264882 28.446236 48.764977 84.459842 17.093543-9.869102-5.031307-8.708032 32.171339-18.585196 19.060913 33.017952 17.432189-10.062614z m-12.505701-97.126803l-32.679307 18.867402-8.321008-14.41663 32.679307-18.867402 8.321008 14.41663z m-50.111496 28.930016l-32.171338 18.577134-8.321008-14.41663 32.171338-18.577134 8.321008 14.41663z m16.932284 29.325102l-32.171339 18.577134-8.127496-14.077984 32.171339-18.577134 8.127496 14.077984z m50.111496-28.930016l-32.679307 18.867402-8.127496-14.077984 32.679307-18.867402 8.127496 14.077984z m95.828661 7.684032c11.062425-6.38589 13.368441-15.537386 6.692284-27.099717l-25.05978-43.411149c3.55578-4.289512 7.014803-8.740283 10.48189-13.199118l-9.482079-16.424315c-3.467087 4.458835-6.92611 8.917669-10.48189 13.199118l-17.803086-30.832882 14.755275-8.51452-9.780409-16.932283-14.747213 8.522582-16.738771-28.994519-17.093544 9.869102 16.738772 28.99452-16.924221 9.772346 9.772347 16.924221 16.932283-9.772347 20.891213 36.202835a299.927181 299.927181 0 0 1-16.690394 15.214866l13.868347 14.344063a572.617575 572.617575 0 0 0 12.497638-12.804031l19.157669 33.179212c2.322142 4.031496 1.475528 7.200252-2.20926 9.328882-3.85411 2.225386-8.167811 4.039559-12.578268 5.692472l13.55389 14.964914 14.247307-8.224252z m111.390236-65.205417c6.369764-3.676724 10.15937-8.329071 11.151118-13.586142 1.225575-5.619906-3.201008-18.706142-13.182992-39.089386l-18.827086 4.160504c7.627591 14.368252 11.368819 23.164976 11.570393 26.615937 0.112882 3.289701-0.959496 5.692472-3.467086 7.143811l-6.539087 3.77348c-3.354205 1.935118-6.095622 1.064315-8.224252-2.628535l-38.702362-67.027654c8.933795-10.07874 17.762772-21.874898 26.390173-35.573921l-18.383622-8.603213a168.443969 168.443969 0 0 1-17.972409 26.914268l-26.801386-46.426709-17.254803 9.965859 77.686929 134.571338c6.966425 12.070299 16.077606 15.077795 27.478677 8.498394l15.077795-8.708031z m-78.501291 45.547842c13.626457-12.779843 25.285543-25.100094 34.783748-37.291339l-12.473449-14.247307a157.808882 157.808882 0 0 1-14.706897 17.875654l-38.412095-66.535811 20.617071-11.900976-9.869102-17.093544-20.617071 11.900977-27.18841-47.087874-17.254803 9.965858 72.94589 126.363212c2.999433 5.192567 2.418898 9.99811-1.564221 14.311811l13.739339 13.739339z m201.663496-113.978457l-65.21348-112.946393c0.137071-7.901732-0.16126-15.771213-0.886929-23.624567l53.78822-31.050583-9.869102-17.093543-144.795213 83.597102 9.869102 17.093543 71.05915-41.024504c1.894803 37.331654-9.45789 76.517795-33.848441 117.856756l20.367118 8.570961c14.860094-26.898142 25.05978-53.344756 30.445859-79.243087l50.990362 88.313953 18.093354-10.449638z m28.728441-76.017889l5.716661-21.850709c-21.157291-7.224441-45.330142-12.707276-72.349228-16.54526l-5.603779 19.318929c29.163843 4.837795 53.385071 11.191433 72.244409 19.07704z m18.738394-105.33493l5.265134-19.13348c-12.739528-4.25726-27.414173-7.627591-43.612725-10.127118l-5.410268 18.101417c17.674079 2.74948 32.380976 6.555213 43.757859 11.159181z m88.934803 67.74526l-15.76315-27.317417 21.786205-12.578268 15.674457 27.148095 16.085669-9.288567-15.674457-27.148095 22.455433-12.965291 4.063748 7.038992c2.031874 3.523528 1.249764 6.426205-2.435023 8.554835l-11.852599 6.176252 12.175118 12.183181 12.570205-7.256693c11.393008-6.579402 13.997354-15.230992 7.998488-25.616126l-42.862866-74.244032-33.848441 19.544693-0.532157-0.145133a202.445606 202.445606 0 0 0 18.738393-38.750741L790.173228 306.87748l-92.676031 53.506016 8.321008 14.41663 31.679496-18.286866-3.85411 13.836094c8.401638-0.16126 16.125984 0.08063 23.261732 0.427339l-37.202646 21.479811 52.538457 90.998929 16.424315-9.482079z m-25.35811-117.856756c-6.724535-0.806299-14.126362-1.233638-21.947465-1.628724l33.517858-19.351181c-3.305827 7.047055-7.143811 13.948976-11.570393 20.979905z m47.571653 16.996788l-22.455433 12.965291-6.095622-10.56252 22.455433-12.965291 6.095622 10.56252z m-38.541102 22.253858l-21.786205 12.578268-6.095622-10.56252 21.786205-12.578268 6.095622 10.56252z m-24.253481 137.570772c-0.330583-19.915591 1.112693-30.582929 4.458835-32.518048 1.846425-1.064315 4.628157-0.886929 8.627402 0.604725 8.304882 2.797858 16.400126 3.265512 24.269606 1.402961 8.006551-2.386646 17.464441-6.506835 28.462362-12.626646 10.812472-6.031118 20.96378-11.66715 30.187843-16.988725l38.379842-22.157102-5.781165-19.673701c-4.329827 2.942992-10.675402 7.055118-19.028662 12.320252-8.708031 5.031307-16.996787 10.038425-25.374236 14.876221-13.07011 7.546961-24.398614 13.868346-34.211275 19.302803-10.07874 5.378016-18.230425 8.296819-24.543748 8.587087-5.28126 0.145134-11.070488-0.983685-17.440252-3.120378l-2.902678-0.774048-36.767244-63.681511-38.379842 22.157102 9.288567 16.085669 22.116787-12.771779 26.511118 45.91874c-4.571717 7.555024-7.014803 20.359055-7.651779 38.605606l19.778519 4.450772z m38.476599-112.938331l-21.786205 12.578268-6.095622-10.56252 21.786205-12.578268 6.095622 10.56252z m38.541102-22.253858l-22.455433 12.965291-6.095622-10.56252 22.455433-12.957228 6.095622 10.56252z m172.241638-43.798173c12.062236-6.966425 14.610142-16.488819 7.740472-28.381733l-39.863433-69.051464 23.302048-13.449071-9.869103-17.093543-23.302047 13.44907-14.513386-25.132346-17.424126 10.062614 14.513386 25.132347-62.681701 36.186708 9.869103 17.093544 62.6817-36.186709 37.34778 64.689386c2.515654 4.354016 1.523906 8.062992-2.838173 10.578645-6.692283 3.870236-14.190866 7.522772-21.955528 11.110804l13.529701 14.537574 23.463307-13.545826z m-130.942992-43.725607l5.386079-20.092976c-12.900787-4.168567-27.389984-7.200252-43.65304-9.433701l-5.321575 18.27074c17.682142 2.74948 32.219717 6.643906 43.596599 11.255937z m80.702488 27.148095l8.466142-17.851465c-10.756031-5.853732-24.825953-12.038047-41.846929-18.302992l-8.740284 16.22274c16.883906 6.789039 30.808693 13.497449 42.121071 19.931717z m-31.219905 99.577952c-0.354772-20.350992 1.064315-31.445669 4.418519-33.380787 2.007685-1.161071 5.128063-1.177197 9.119244 0.32252a42.951559 42.951559 0 0 0 24.938835 1.007874c8.175874-2.483402 18.141732-6.893858 29.639559-13.303937 11.320441-6.321386 21.810394-12.150929 31.365039-17.657953l35.525544-20.520315-5.966614-20.012346c-3.999244 2.74948-10.006173 6.668094-17.859528 11.651023-7.95011 4.805543-15.722835 9.522394-23.439118 13.973166a2406.72252 2406.72252 0 0 1-35.719055 20.181669c-10.586709 5.66022-19.165732 8.603213-25.712882 9.256315-5.28126 0.145134-11.401071-0.790173-17.940158-2.822047l-3.080063-0.685355-36.767244-63.681512-39.041008 22.544126 9.482079 16.424315 22.455433-12.965291 26.511118 45.91874c-4.57978 7.555024-7.256693 20.72189-7.700157 39.299024l19.770457 4.450771z" fill="#F5222D" p-id="1508"></path></svg>
src/assets/svgs/bpm/running.svg
对比新文件
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1724304256588" class="icon" viewBox="0 0 1300 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1272" xmlns:xlink="http://www.w3.org/1999/xlink" width="253.90625" height="200"><path d="M784.058182 99.258182l10.938182 18.385454-21.294546-2.56-14.196363 16.058182-4.072728-21.061818-19.781818-8.494545 18.734546-10.472728 2.094545-21.294545 15.709091 14.545454 20.945454-4.654545-9.076363 19.549091zM1067.287273 642.443636l-18.501818 10.821819 2.56-21.294546-16.058182-14.196364 21.061818-4.072727 8.494545-19.665454 10.472728 18.734545 21.294545 1.978182-14.661818 15.709091 4.770909 20.945454-19.432727-8.96z" fill="#2196F3" p-id="1273"></path><path d="M1067.287273 642.443636l-18.501818 10.821819 2.56-21.294546-16.058182-14.196364 21.061818-4.072727 8.494545-19.665454 10.472728 18.734545 21.294545 1.978182-14.661818 15.709091 4.770909 20.945454-19.432727-8.96zM571.927273 100.072727l-17.454546-12.567272 20.596364-6.167273 6.516364-20.48 12.218181 17.570909 21.410909-0.116364-12.916363 17.105455 6.749091 20.363636-20.247273-6.981818-17.338182 12.683636 0.465455-21.410909zM991.418182 784.407273l-21.178182 3.490909 10.123636-18.967273-9.774545-18.967273 21.061818 3.723637 15.127273-15.243637 2.909091 21.294546 19.2 9.658182-19.316364 9.309091-3.258182 21.178181-14.894545-15.476363zM427.985455 156.741818L407.272727 151.505455l16.872728-13.265455-1.396364-21.410909 17.803636 11.985454 20.014546-7.912727-5.934546 20.596364 13.730909 16.523636-21.410909 0.814546-11.52 18.152727-7.447272-20.247273zM854.225455 896.465455l-20.712728-5.352728 16.872728-13.265454-1.396364-21.294546 17.803636 11.869091 20.014546-7.912727-5.934546 20.712727 13.730909 16.523637-21.527272 0.814545-11.403637 18.036364-7.447272-20.130909zM562.501818 923.694545l10.821818 18.385455-21.294545-2.56-14.196364 16.058182-4.072727-21.061818-19.665455-8.494546 18.734546-10.356363 1.978182-21.41091 15.709091 14.661819 20.945454-4.770909-8.96 19.54909zM242.734545 420.770909l-18.385454 10.938182 2.56-21.294546-16.058182-14.196363 21.061818-4.189091 8.494546-19.665455 10.356363 18.734546 21.410909 2.094545-14.545454 15.709091 4.654545 20.945455-19.549091-9.076364z" fill="#2196F3" p-id="1274"></path><path d="M242.734545 420.770909l-18.385454 10.938182 2.56-21.294546-16.058182-14.196363 21.061818-4.189091 8.494546-19.665455 10.356363 18.734546 21.410909 2.094545-14.545454 15.709091 4.654545 20.945455-19.549091-9.076364zM700.858182 943.941818l-17.454546-12.450909 20.48-6.283636 6.516364-20.48 12.334545 17.687272 21.41091-0.116363-12.916364 17.105454 6.632727 20.363637-20.247273-7.098182-17.221818 12.683636 0.465455-21.410909zM303.592727 278.807273l-21.178182 3.490909 10.123637-18.967273-9.890909-18.967273 21.178182 3.723637 15.010909-15.243637 2.909091 21.294546 19.2 9.541818-19.316364 9.425455-3.258182 21.178181-14.778182-15.476363z" fill="#2196F3" p-id="1275"></path><path d="M407.272727 90.647273a486.632727 486.632727 0 0 1 504.552728 11.636363l25.018181-14.429091A512 512 0 0 0 139.636364 546.909091l25.018181-14.429091A486.981818 486.981818 0 0 1 407.272727 90.647273zM893.323636 933.352727a486.749091 486.749091 0 0 1-504.669091-11.636363l-24.901818 14.429091A512 512 0 0 0 1161.192727 477.090909l-24.901818 13.963636a486.981818 486.981818 0 0 1-242.967273 442.298182z" fill="#2196F3" p-id="1276"></path><path d="M814.545455 795.927273a327.447273 327.447273 0 0 1-258.21091 29.556363l-29.78909 17.105455A353.163636 353.163636 0 0 0 998.865455 570.181818l-29.789091 17.105455A326.865455 326.865455 0 0 1 814.545455 795.927273zM486.865455 228.072727A327.447273 327.447273 0 0 1 744.727273 198.516364l29.789091-17.105455A353.163636 353.163636 0 0 0 302.545455 453.818182l29.78909-17.105455A326.865455 326.865455 0 0 1 486.865455 228.072727zM1288.378182 374.690909a53.294545 53.294545 0 0 1-14.429091 11.636364L229.469091 989.090909a53.876364 53.876364 0 0 1-73.425455-19.665454L7.214545 710.632727a53.527273 53.527273 0 0 1 19.781819-73.309091L1071.476364 34.909091a53.876364 53.876364 0 0 1 73.425454 19.665454l148.829091 258.327273a53.061818 53.061818 0 0 1 5.352727 40.727273 55.272727 55.272727 0 0 1-10.705454 21.061818zM32.232727 665.716364A28.043636 28.043636 0 0 0 29.323636 698.181818l148.829091 257.978182a28.392727 28.392727 0 0 0 38.516364 10.356364l1044.48-601.949091a28.16 28.16 0 0 0 10.356364-38.516364L1122.676364 67.84a28.276364 28.276364 0 0 0-38.4-10.356364L39.68 659.432727a27.810909 27.810909 0 0 0-7.447273 6.283637z" fill="#2196F3" p-id="1277"></path><path d="M477.090909 500.945455l22.109091 38.283636-15.36 8.843636-13.963636-24.436363-151.272728 87.621818 14.545455 25.134545-15.243636 8.843637-23.272728-39.330909L377.949091 558.545455c-6.050909-4.887273-11.636364-8.843636-15.825455-11.636364l14.894546-12.450909c3.956364 3.141818 9.658182 8.145455 17.105454 14.894545zM459.869091 698.181818l-48.407273 28.043637 7.447273 12.334545-15.36 8.843636-61.207273-106.007272L406.225455 605.090909l-12.683637-21.876364 15.709091-9.076363 12.683636 21.876363L486.4 558.545455l60.509091 104.727272-15.36 8.843637-7.098182-12.218182-49.105454 28.392727L501.294545 733.090909l-15.70909 9.076364z m-45.381818-78.661818l-48.523637 27.461818 14.545455 25.134546 48.523636-28.043637zM388.538182 686.545455l14.545454 25.134545 48.523637-28.043636-14.545455-25.134546z m105.425454-79.476364L479.418182 581.818182 430.545455 609.861818l14.545454 25.134546z m-26.647272 67.490909l49.221818-28.392727-14.545455-25.134546-49.105454 28.392728zM624.058182 541.090909c-4.654545 6.167273-10.123636 13.149091-16.290909 20.829091l34.909091 61.207273a18.734545 18.734545 0 0 1-6.632728 29.207272l-18.734545 10.938182-11.636364-13.614545a174.545455 174.545455 0 0 0 17.454546-8.610909 8.378182 8.378182 0 0 0 2.327272-12.683637l-30.021818-52.363636-9.774545 10.24-9.890909 10.123636-12.450909-12.916363c9.076364-8.145455 16.872727-15.709091 23.272727-22.574546l-30.836364-53.527272-24.785454 14.196363-8.727273-15.010909L546.909091 492.218182l-23.272727-40.378182 15.36-8.843636 23.272727 40.378181 21.643636-12.450909 8.727273 15.127273-21.643636 12.450909L599.156364 546.909091c5.352727-6.4 10.821818-13.381818 16.290909-20.712727z m8.843636-45.032727L689.221818 593.454545a193.745455 193.745455 0 0 0 22.574546-27.112727l11.636363 13.032727a363.985455 363.985455 0 0 1-41.192727 44.8l-12.334545-12.450909a10.821818 10.821818 0 0 0 1.62909-13.730909l-98.90909-171.403636 15.476363-8.96 36.305455 62.952727 30.836363-17.803636 8.029091 15.476363z m128 81.454545a20.130909 20.130909 0 0 1-30.836363-9.541818L628.363636 392.378182l15.36-8.378182 38.050909 66.094545A206.08 206.08 0 0 0 709.818182 409.018182l16.64 7.563636a297.890909 297.890909 0 0 1-34.909091 48.64l52.712727 91.112727a8.843636 8.843636 0 0 0 13.614546 4.072728l10.821818-6.167273a14.429091 14.429091 0 0 0 7.912727-11.636364 102.981818 102.981818 0 0 0-15.010909-38.516363l17.105455-3.723637c12.334545 24.669091 17.687273 41.076364 16.058181 48.989091a28.16 28.16 0 0 1-15.127272 18.152728zM805.236364 288.116364l16.174545-9.309091 23.272727 39.330909 78.429091-45.265455 59.345455 102.749091-16.64 9.076364-7.912727-13.847273L896 407.272727l42.938182 74.472728-16.174546 9.30909-42.938181-74.472727-62.603637 36.072727 8.029091 13.73091-15.825454 9.192727L749.730909 372.363636l78.196364-45.265454z m2.676363 149.061818l62.603637-36.072727-33.745455-58.181819-62.487273 36.072728z m78.778182-45.381818l62.72-36.189091-33.745454-58.181818-62.72 36.072727z" fill="#2196F3" p-id="1278"></path></svg>
src/assets/svgs/bpm/simple-process-bg.svg
对比新文件
@@ -0,0 +1 @@
<svg width="22" height="22" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path fill="#FAFAFA" d="M0 0h22v22H0z"/><circle fill="#919BAE" cx="1" cy="1" r="1"/></g></svg>
src/assets/svgs/bpm/starter.svg
对比新文件
@@ -0,0 +1 @@
<svg t="1729561814171" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1359" width="200" height="200"><path d="M674.496 603.456c120.256 0 218.176 90.752 221.44 203.84l0.064 5.888v125.888c0 11.52-9.92 20.928-22.144 20.928h-44.352a21.568 21.568 0 0 1-22.144-20.928v-125.888c0-67.712-56.512-123.264-128-125.76l-4.928-0.064H349.568c-71.488 0-130.176 53.504-132.864 121.152l-0.064 4.672v125.888c0 11.52-9.92 20.928-22.144 20.928h-44.352A21.568 21.568 0 0 1 128 939.072v-125.888c0-113.92 95.872-206.528 215.36-209.664l6.208-0.064h324.928zM497.216 128c122.368 0 221.568 93.888 221.568 209.728s-99.2 209.792-221.568 209.792c-122.304 0-221.44-93.952-221.44-209.728C275.712 221.952 374.848 128 497.152 128z m0 83.904c-73.408 0-132.864 56.32-132.864 125.888 0 69.504 59.52 125.824 132.864 125.824 73.408 0 132.928-56.32 132.928-125.824 0-69.504-59.52-125.888-132.928-125.888z" fill="#fff" p-id="1360"></path></svg>
src/components/FormCreate/src/components/useApiSelect.tsx
@@ -104,9 +104,9 @@
          parseOptions0(data)
          return
        }
        // 情况三:不是 iailab-plat 标准返回
        // 情况三:不是 yudao-vue-pro 标准返回
        console.warn(
          `接口[${props.url}] 返回结果不是 iailab-plat 标准返回建议采用自定义解析函数处理`
          `接口[${props.url}] 返回结果不是 yudao-vue-pro 标准返回建议采用自定义解析函数处理`
        )
      }
@@ -185,7 +185,6 @@
            </el-select>
          )
        }
        // debugger
        return (
          <el-select
            class="w-1/1"
src/components/FormCreate/src/config/useDictSelectRule.ts
@@ -48,7 +48,7 @@
        },
        {
          type: 'select',
          field: 'dictValueType',
          field: 'valueType',
          title: '字典值类型',
          value: 'str',
          options: [
src/components/FormCreate/src/utils/index.ts
@@ -16,3 +16,46 @@
    return rule
  })
}
/**
 * 解析表单组件的  field, title 等字段(递归,如果组件包含子组件)
 *
 * @param rule  组件的生成规则 https://www.form-create.com/v3/guide/rule
 * @param fields 解析后表单组件字段
 * @param parentTitle  如果是子表单,子表单的标题,默认为空
 */
export const parseFormFields = (
  rule: Record<string, any>,
  fields: Array<Record<string, any>> = [],
  parentTitle: string = ''
) => {
  const { type, field, $required, title: tempTitle, children } = rule
  if (field && tempTitle) {
    let title = tempTitle
    if (parentTitle) {
      title = `${parentTitle}.${tempTitle}`
    }
    let required = false
    if ($required) {
      required = true
    }
    fields.push({
      field,
      title,
      type,
      required
    })
    // TODO 子表单 需要处理子表单字段
    // if (type === 'group' && rule.props?.rule && Array.isArray(rule.props.rule)) {
    //   // 解析子表单的字段
    //   rule.props.rule.forEach((item) => {
    //     parseFields(item, fieldsPermission, title)
    //   })
    // }
  }
  if (children && Array.isArray(children)) {
    children.forEach((rule) => {
      parseFormFields(rule, fields)
    })
  }
}
src/components/SimpleProcessDesigner/src/addNode.vue
文件已删除
src/components/SimpleProcessDesigner/src/nodeWrap.vue
文件已删除
src/components/SimpleProcessDesigner/src/util.ts
文件已删除
src/components/SimpleProcessDesigner/theme/workflow.css
文件已删除
src/components/SimpleProcessDesignerV2/src/NodeHandler.vue
对比新文件
@@ -0,0 +1,214 @@
<template>
  <div class="node-handler-wrapper">
    <div class="node-handler">
      <el-popover
        trigger="hover"
        v-model:visible="popoverShow"
        placement="right-start"
        width="auto"
        v-if="!readonly"
      >
        <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 parallel">
              <span class="iconfont icon-size icon-parallel"></span>
            </div>
            <div class="handler-item-text">并行分支</div>
          </div>
          <div class="handler-item" @click="addNode(NodeType.INCLUSIVE_BRANCH_NODE)">
            <div class="handler-item-icon inclusive">
              <span class="iconfont icon-size icon-inclusive"></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 message = useMessage() // 消息弹窗
const popoverShow = ref(false)
const props = defineProps({
  childNode: {
    type: Object as () => SimpleFlowNode,
    default: null
  },
  currentNode: {
    type: Object as () => SimpleFlowNode,
    required: true
  }
})
const emits = defineEmits(['update:childNode'])
const readonly = inject<Boolean>('readonly') // 是否只读
const addNode = (type: number) => {
  // 校验:条件分支、包容分支后面,不允许直接添加并行分支
  if (
    type === NodeType.PARALLEL_BRANCH_NODE &&
    [NodeType.CONDITION_BRANCH_NODE, NodeType.INCLUSIVE_BRANCH_NODE].includes(
      props.currentNode?.type
    )
  ) {
    message.error('条件分支、包容分支后面,不允许直接添加并行分支')
    return
  }
  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)
  }
  if (type === NodeType.INCLUSIVE_BRANCH_NODE) {
    const data: SimpleFlowNode = {
      name: '包容分支',
      type: NodeType.INCLUSIVE_BRANCH_NODE,
      id: 'GateWay_' + generateUUID(),
      childNode: props.childNode,
      conditionNodes: [
        {
          id: 'Flow_' + generateUUID(),
          name: '包容条件1',
          showText: '',
          type: NodeType.CONDITION_NODE,
          childNode: undefined,
          defaultFlow: false
        },
        {
          id: 'Flow_' + generateUUID(),
          name: '其它情况',
          showText: '未满足其它条件时,将进入此分支',
          type: NodeType.CONDITION_NODE,
          childNode: undefined,
          defaultFlow: true
        }
      ]
    }
    emits('update:childNode', data)
  }
}
</script>
<style lang="scss" scoped></style>
src/components/SimpleProcessDesignerV2/src/ProcessNodeTree.vue
对比新文件
@@ -0,0 +1,118 @@
<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"
  />
  <!-- 包容分支节点 -->
  <InclusiveNode
    v-if="currentNode && currentNode.type === NodeType.INCLUSIVE_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"
    :flow-node="currentNode"
  />
</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 InclusiveNode from './nodes/InclusiveNode.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>
src/components/SimpleProcessDesignerV2/src/SimpleProcessDesigner.vue
对比新文件
@@ -0,0 +1,179 @@
<template>
  <div v-loading="loading" class="overflow-auto">
    <SimpleProcessModel
      v-if="processNodeTree"
      :flow-node="processNodeTree"
      :readonly="false"
      @save="saveSimpleFlowModel"
    />
    <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 SimpleProcessModel from './SimpleProcessModel.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'
defineOptions({
  name: 'SimpleProcessDesigner'
})
const emits = defineEmits(['success']) // 保存成功事件
const props = defineProps({
  modelId: {
    type: String,
    required: true
  }
})
const loading = ref(false)
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 (simpleModelNode: SimpleFlowNode) => {
  if (!simpleModelNode) {
    message.error('模型数据为空')
    return
  }
  try {
    loading.value = true
    const data = {
      id: props.modelId,
      simpleModel: simpleModelNode
    }
    const result = await updateBpmSimpleModel(data)
    if (result) {
      message.success('修改成功')
      emits('success')
    } else {
      message.alert('修改失败')
    }
  } finally {
    loading.value = false
  }
}
// 校验节点设置。 暂时以 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 ||
      type === NodeType.COPY_TASK_NODE ||
      type === NodeType.CONDITION_NODE
    ) {
      if (!showText) {
        errorNodes.push(node)
      }
      validateNode(node.childNode, errorNodes)
    }
    if (
      type == NodeType.CONDITION_BRANCH_NODE ||
      type == NodeType.PARALLEL_BRANCH_NODE ||
      type == NodeType.INCLUSIVE_BRANCH_NODE
    ) {
      // 分支节点
      // 1. 先校验各个分支
      conditionNodes?.forEach((item) => {
        validateNode(item, errorNodes)
      })
      // 2. 校验孩子节点
      validateNode(node.childNode, errorNodes)
    }
  }
}
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>
src/components/SimpleProcessDesignerV2/src/SimpleProcessModel.vue
对比新文件
@@ -0,0 +1,140 @@
<template>
  <div class="simple-process-model-container position-relative">
    <div class="position-absolute top-0px right-0px bg-#fff">
      <el-row type="flex" justify="end">
        <el-button-group key="scale-control" size="default">
          <el-button size="default" :icon="ScaleToOriginal" @click="processReZoom()" />
          <el-button size="default" :plain="true" :icon="ZoomOut" @click="zoomOut()" />
          <el-button size="default" class="w-80px"> {{ scaleValue }}% </el-button>
          <el-button size="default" :plain="true" :icon="ZoomIn" @click="zoomIn()" />
        </el-button-group>
        <el-button
          v-if="!readonly"
          size="default"
          class="ml-4px"
          type="primary"
          :icon="Select"
          @click="saveSimpleFlowModel"
          >保存模型</el-button
        >
      </el-row>
    </div>
    <div class="simple-process-model" :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>
</template>
<script setup lang="ts">
import ProcessNodeTree from './ProcessNodeTree.vue'
import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from './consts'
import { useWatchNode } from './node'
import { Select, ZoomOut, ZoomIn, ScaleToOriginal } from '@element-plus/icons-vue'
defineOptions({
  name: 'SimpleProcessModel'
})
const props = defineProps({
  flowNode: {
    type: Object as () => SimpleFlowNode,
    required: true
  },
  readonly: {
    type: Boolean,
    required: false,
    default: true
  }
})
const emits = defineEmits<{
  'save': [node: SimpleFlowNode | undefined]
}>()
const processNodeTree = useWatchNode(props)
provide('readonly', props.readonly)
let scaleValue = ref(100)
const MAX_SCALE_VALUE = 200
const MIN_SCALE_VALUE = 50
// 放大
const zoomIn = () => {
  if (scaleValue.value == MAX_SCALE_VALUE) {
    return
  }
  scaleValue.value += 10
}
// 缩小
const zoomOut = () => {
  if (scaleValue.value == MIN_SCALE_VALUE) {
    return
  }
  scaleValue.value -= 10
}
const processReZoom = () => {
  scaleValue.value = 100
}
const errorDialogVisible = ref(false)
let errorNodes: SimpleFlowNode[] = []
const saveSimpleFlowModel = async () => {
  errorNodes = []
  validateNode(processNodeTree.value, errorNodes)
  if (errorNodes.length > 0) {
    errorDialogVisible.value = true
    return
  }
  emits('save', processNodeTree.value)
}
// 校验节点设置。 暂时以 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 ||
      type === NodeType.COPY_TASK_NODE ||
      type === NodeType.CONDITION_NODE
    ) {
      if (!showText) {
        errorNodes.push(node)
      }
      validateNode(node.childNode, errorNodes)
    }
    if (
      type == NodeType.CONDITION_BRANCH_NODE ||
      type == NodeType.PARALLEL_BRANCH_NODE ||
      type == NodeType.INCLUSIVE_BRANCH_NODE
    ) {
      // 分支节点
      // 1. 先校验各个分支
      conditionNodes?.forEach((item) => {
        validateNode(item, errorNodes)
      })
      // 2. 校验孩子节点
      validateNode(node.childNode, errorNodes)
    }
  }
}
</script>
<style lang="scss" scoped></style>
src/components/SimpleProcessDesignerV2/src/SimpleProcessViewer.vue
对比新文件
@@ -0,0 +1,48 @@
<template>
  <SimpleProcessModel :flow-node="simpleModel" :readonly="true" />
</template>
<script setup lang="ts">
import { useWatchNode } from './node'
import { SimpleFlowNode } from './consts'
defineOptions({
  name: 'SimpleProcessViewer'
})
const props = defineProps({
  flowNode: {
    type: Object as () => SimpleFlowNode,
    required: true
  },
  // 流程任务
  tasks: {
    type: Array,
    default: () => [] as any[]
  },
  // 流程实例
  processInstance: {
    type: Object,
    default: () => undefined
  }
})
const approveTasks = ref<any[]>(props.tasks)
const currentProcessInstance = ref(props.processInstance)
const simpleModel = useWatchNode(props)
watch(
  () => props.tasks,
  (newValue) => {
    approveTasks.value = newValue
  }
)
watch(
  () => props.processInstance,
  (newValue) => {
    currentProcessInstance.value = newValue
  }
)
provide('tasks', approveTasks)
provide('processInstance', currentProcessInstance)
</script>
p
src/components/SimpleProcessDesignerV2/src/consts.ts
对比新文件
@@ -0,0 +1,570 @@
// @ts-ignore
import { DictDataVO } from '@/api/system/dict/types'
import { TaskStatusEnum } from '@/api/bpm/task'
/**
 * 节点类型
 */
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, any>>
  // 审批任务超时处理
  timeoutHandler?: TimeoutHandler
  // 审批任务拒绝处理
  rejectHandler?: RejectHandler
  // 审批人为空的处理
  assignEmptyHandler?: AssignEmptyHandler
  // 审批节点的审批人与发起人相同时,对应的处理类型
  assignStartUserHandlerType?: number
  // 条件类型
  conditionType?: ConditionType
  // 条件表达式
  conditionExpression?: string
  // 条件组
  conditionGroups?: ConditionGroup
  // 是否默认的条件
  defaultFlow?: boolean
  // 活动的状态,用于前端节点状态展示
  activityStatus?: TaskStatusEnum
}
// 候选人策略枚举 ( 用于审批节点。抄送节点 )
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,
  /**
   * 表单内用户字段
   */
  FORM_USER = 50,
  /**
   * 表单内部门负责人
   */
  FORM_DEPT_LEADER = 51,
  /**
   * 流程表达式
   */
  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,
  /**
   * 抄送
   */
  COPY = 7
}
/**
 * 条件规则结构定义
 */
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.FORM_USER },
  { label: '表单内部门负责人', value: CandidateStrategy.FORM_DEPT_LEADER },
  { 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, '退回')
OPERATION_BUTTON_NAME.set(OperationButtonType.COPY, '抄送')
// 默认的按钮权限设置
export const DEFAULT_BUTTON_SETTING: ButtonSetting[] = [
  { id: OperationButtonType.APPROVE, displayName: '通过', enable: true },
  { id: OperationButtonType.REJECT, displayName: '拒绝', enable: true },
  { id: OperationButtonType.TRANSFER, displayName: '转办', enable: true },
  { id: OperationButtonType.DELEGATE, displayName: '委派', enable: true },
  { id: OperationButtonType.ADD_SIGN, displayName: '加签', enable: true },
  { id: OperationButtonType.RETURN, displayName: '退回', enable: true }
]
// 发起人的按钮权限。暂时定死,不可以编辑
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 }
]
/**
 * 流程实例的变量枚举
 */
export enum ProcessVariableEnum {
  /**
   * 发起用户 ID
   */
  START_USER_ID = 'PROCESS_START_USER_ID'
}
src/components/SimpleProcessDesignerV2/src/index.ts
对比新文件
@@ -0,0 +1,5 @@
import SimpleProcessDesigner from './SimpleProcessDesigner.vue'
import SimpleProcessViewer from './SimpleProcessViewer.vue'
import '../theme/simple-process-designer.scss'
export { SimpleProcessDesigner, SimpleProcessViewer}
src/components/SimpleProcessDesignerV2/src/node.ts
对比新文件
@@ -0,0 +1,495 @@
import { cloneDeep } from 'lodash-es'
import { TaskStatusEnum } from '@/api/bpm/task'
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,
  ProcessVariableEnum
} from './consts'
import { parseFormFields } from '@/components/FormCreate/src/utils/index'
export function useWatchNode(props: { flowNode: SimpleFlowNode }): Ref<SimpleFlowNode> {
  const node = ref<SimpleFlowNode>(props.flowNode)
  watch(
    () => props.flowNode,
    (newValue) => {
      node.value = newValue
    }
  )
  return node
}
// 解析 formCreate 所有表单字段, 并返回
const parseFormCreateFields = (formFields?: string[]) => {
  const result: Array<Record<string, any>> = []
  if (formFields) {
    formFields.forEach((fieldStr: string) => {
      parseFormFields(JSON.parse(fieldStr), result)
    })
  }
  // 固定添加发起人 ID 字段
  result.unshift({
    field: ProcessVariableEnum.START_USER_ID,
    title: '发起人',
    type: 'UserSelect',
    required: true
  })
  return result
}
/**
 * @description 表单数据权限配置,用于发起人节点 、审批节点、抄送节点
 */
export function useFormFieldsPermission(defaultPermission: FieldPermissionType) {
  // 字段权限配置. 需要有 field, title,  permissioin 属性
  const fieldsPermissionConfig = ref<Array<Record<string, any>>>([])
  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[]) => {
    let defaultFieldsPermission: Array<Record<string, any>> = []
    if (formFields) {
      defaultFieldsPermission = parseFormCreateFields(formFields).map((item) => {
        return {
          field: item.field,
          title: item.title,
          permission: defaultPermission
        }
      })
    }
    return defaultFieldsPermission
  }
  // 获取表单的所有字段,作为下拉框选项
  const formFieldOptions = parseFormCreateFields(unref(formFields))
  return {
    formType,
    fieldsPermissionConfig,
    formFieldOptions,
    getNodeConfigFormFields
  }
}
/**
 * @description 获取表单的字段
 */
export function useFormFields() {
  const formFields = inject<Ref<string[]>>('formFields') // 流程表单字段
  return parseFormCreateFields(unref(formFields))
}
export type UserTaskFormType = {
  //candidateParamArray: any[]
  candidateStrategy: CandidateStrategy
  approveMethod: ApproveMethodType
  roleIds?: number[] // 角色
  deptIds?: number[] // 部门
  deptLevel?: number // 部门层级
  userIds?: number[] // 用户
  userGroups?: number[] // 用户组
  postIds?: number[] // 岗位
  expression?: string // 流程表达式
  formUser?: string // 表单内用户字段
  formDept?: 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[] // 岗位
  formUser?: string // 表单内用户字段
  formDept?: string // 表单内部门字段
  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 formFields = inject<Ref<string[]>>('formFields') // 流程表单字段
  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.FORM_USER) {
      const formFieldOptions = parseFormCreateFields(unref(formFields))
      const item = formFieldOptions.find((item) => item.field === configForm.value?.formUser)
      showText = `表单用户:${item?.title}`
    }
    // 表单内部门负责人
    if (configForm.value?.candidateStrategy === CandidateStrategy.FORM_DEPT_LEADER) {
      showText = `表单内部门负责人`
    }
    // 发起人自选
    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.FORM_USER:
        candidateParam = configForm.value.formUser!
        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
      }
      // 表单内部门的负责人
      case CandidateStrategy.FORM_DEPT_LEADER: {
        // 候选人参数格式: | 分隔 。左边为表单内部门字段。 右边为部门层级
        const deptFieldOnForm = configForm.value.formDept!
        candidateParam = deptFieldOnForm.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.FORM_USER:
        configForm.value.formUser = candidateParam
        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
      }
      // 表单内的部门负责人
      case CandidateStrategy.FORM_DEPT_LEADER: {
        // 候选人参数格式: | 分隔 。左边为表单内的部门字段。 右边为部门层级
        const paramArray = candidateParam.split('|')
        configForm.value.formDept = paramArray[0]
        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
  }
}
/**
 * @description 根据节点任务状态,获取节点任务状态样式
 */
export function useTaskStatusClass(taskStatus: TaskStatusEnum | undefined): string {
  if (!taskStatus) {
    return ''
  }
  if (taskStatus === TaskStatusEnum.APPROVE) {
    return 'status-pass'
  }
  if (taskStatus === TaskStatusEnum.RUNNING) {
    return 'status-running'
  }
  if (taskStatus === TaskStatusEnum.REJECT) {
    return 'status-reject'
  }
  if (taskStatus === TaskStatusEnum.CANCEL) {
    return 'status-cancel'
  }
  return ''
}
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>
src/components/SimpleProcessDesignerV2/src/nodes-config/CopyTaskNodeConfig.vue
对比新文件
@@ -0,0 +1,374 @@
<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 ||
                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"
                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.FORM_USER"
              label="表单内用户字段"
              prop="formUser"
            >
              <el-select v-model="configForm.formUser" clearable style="width: 100%">
                <el-option
                  v-for="(item, idx) in userFieldOnFormOptions"
                  :key="idx"
                  :label="item.title"
                  :value="item.field"
                  :disabled ="!item.required"
                />
              </el-select>
            </el-form-item>
            <el-form-item
              v-if="configForm.candidateStrategy === CandidateStrategy.FORM_DEPT_LEADER"
              label="表单内部门字段"
              prop="formDept"
            >
              <el-select v-model="configForm.formDept" clearable style="width: 100%">
                <el-option
                  v-for="(item, idx) in deptFieldOnFormOptions"
                  :key="idx"
                  :label="item.title"
                  :value="item.field"
                  :disabled ="!item.required"
                />
              </el-select>
            </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 ||
                configForm.candidateStrategy == CandidateStrategy.FORM_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.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,
  MULTI_LEVEL_DEPT
} 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 deptLevelLabel = computed(() => {
  let label = '部门负责人来源'
  if (configForm.value.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER) {
    label = label + '(指定部门向上)'
  } else {
    label = label + '(发起人部门向上)'
  }
  return label
})
// 抽屉配置
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, formFieldOptions, getNodeConfigFormFields } =
  useFormFieldsPermission(FieldPermissionType.READ)
// 表单内用户字段选项, 必须是必填和用户选择器
const userFieldOnFormOptions = computed(() => {
  return formFieldOptions.filter((item) => item.type === 'UserSelect')
})
// 表单内部门字段选项, 必须是必填和部门选择器
const deptFieldOnFormOptions = computed(() => {
  return formFieldOptions.filter((item) => item.type === 'DeptSelect')
})
// 抄送人表单配置
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' }],
  formUser: [{ required: true, message: '表单内用户字段不能为空', trigger: 'change' }],
  formDept: [{ 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)
})
// 改变抄送人设置策略
const changeCandidateStrategy = () => {
  configForm.value.userIds = []
  configForm.value.deptIds = []
  configForm.value.roleIds = []
  configForm.value.postIds = []
  configForm.value.userGroups = []
  configForm.value.deptLevel = 1
  configForm.value.formUser = ''
}
// 保存配置
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>
src/components/SimpleProcessDesignerV2/src/nodes-config/StartUserNodeConfig.vue
对比新文件
@@ -0,0 +1,135 @@
<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
  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>
src/components/SimpleProcessDesignerV2/src/nodes-config/UserTaskNodeConfig.vue
对比新文件
@@ -0,0 +1,905 @@
<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.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.FORM_USER"
              label="表单内用户字段"
              prop="formUser"
            >
              <el-select v-model="configForm.formUser" clearable style="width: 100%">
                <el-option
                  v-for="(item, idx) in userFieldOnFormOptions"
                  :key="idx"
                  :label="item.title"
                  :value="item.field"
                  :disabled ="!item.required"
                />
              </el-select>
            </el-form-item>
            <el-form-item
              v-if="configForm.candidateStrategy === CandidateStrategy.FORM_DEPT_LEADER"
              label="表单内部门字段"
              prop="formDept"
            >
              <el-select v-model="configForm.formDept" clearable style="width: 100%">
                <el-option
                  v-for="(item, idx) in deptFieldOnFormOptions"
                  :key="idx"
                  :label="item.title"
                  :value="item.field"
                  :disabled ="!item.required"
                />
              </el-select>
            </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 ||
                configForm.candidateStrategy == CandidateStrategy.FORM_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>
            <!-- 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">
                      {{ 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 if (configForm.value.candidateStrategy == CandidateStrategy.FORM_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, formFieldOptions, getNodeConfigFormFields } =
  useFormFieldsPermission(FieldPermissionType.READ)
// 表单内用户字段选项, 必须是必填和用户选择器
const userFieldOnFormOptions = computed(() => {
  return formFieldOptions.filter((item) => item.type === 'UserSelect')
})
// 表单内部门字段选项, 必须是必填和部门选择器
const deptFieldOnFormOptions = computed(() => {
  return formFieldOptions.filter((item) => item.type === 'DeptSelect')
})
// 操作按钮设置
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' }],
  formUser: [{ required: true, message: '表单内用户字段不能为空', trigger: 'change' }],
  formDept: [{ 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 changeCandidateStrategy = () => {
  configForm.value.userIds = []
  configForm.value.deptIds = []
  configForm.value.roleIds = []
  configForm.value.postIds = []
  configForm.value.userGroups = []
  configForm.value.deptLevel = 1
  configForm.value.formUser = ''
  configForm.value.formDept = ''
  configForm.value.approveMethod = ApproveMethodType.SEQUENTIAL_APPROVE
}
// 审批方式改变
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)
  // 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>
src/components/SimpleProcessDesignerV2/src/nodes/CopyTaskNode.vue
对比新文件
@@ -0,0 +1,97 @@
<template>
  <div class="node-wrapper">
    <div class="node-container">
      <div
        class="node-box"
        :class="[
          { 'node-config-error': !currentNode.showText },
          `${useTaskStatusClass(currentNode?.activityStatus)}`
        ]"
      >
        <div class="node-title-container">
          <div class="node-title-icon copy-task"><span class="iconfont icon-copy"></span></div>
          <input
            v-if="!readonly && 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 v-if="!readonly" icon="ep:arrow-right-bold" />
        </div>
        <div v-if="!readonly" 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"
        :current-node="currentNode"
      />
    </div>
    <CopyTaskNodeConfig
      v-if="!readonly && currentNode"
      ref="nodeSetting"
      :flow-node="currentNode"
    />
  </div>
</template>
<script setup lang="ts">
import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
import NodeHandler from '../NodeHandler.vue'
import { useNodeName2, useWatchNode, useTaskStatusClass } from '../node'
import 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 readonly = inject<Boolean>('readonly')
// 监控节点的变化
const currentNode = useWatchNode(props)
// 节点名称编辑
const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.COPY_TASK_NODE)
const nodeSetting = ref()
// 打开节点配置
const openNodeConfig = () => {
  if (readonly) {
    return
  }
  nodeSetting.value.showCopyTaskNodeConfig(currentNode.value)
  nodeSetting.value.openDrawer()
}
// 删除节点。更新当前节点为孩子节点
const deleteNode = () => {
  emits('update:flowNode', currentNode.value.childNode)
}
</script>
<style lang="scss" scoped></style>
src/components/SimpleProcessDesignerV2/src/nodes/EndEventNode.vue
对比新文件
@@ -0,0 +1,102 @@
<template>
  <div class="end-node-wrapper">
    <div class="end-node-box cursor-pointer" :class="`${useTaskStatusClass(currentNode?.activityStatus)}`" @click="nodeClick">
      <span class="node-fixed-name" title="结束">结束</span>
    </div>
  </div>
  <el-dialog title="审批信息" v-model="dialogVisible" width="1000px" append-to-body>
      <el-row>
        <el-table
          :data="processInstanceInfos"
          size="small"
          border
          header-cell-class-name="table-header-gray"
        >
          <el-table-column
            label="序号"
            header-align="center"
            align="center"
            type="index"
            width="50"
          />
          <el-table-column
            label="发起人"
            prop="assigneeUser.nickname"
            min-width="100"
            align="center"
          />
          <el-table-column label="部门" min-width="100" align="center">
            <template #default="scope">
              {{ scope.row.assigneeUser?.deptName || scope.row.ownerUser?.deptName }}
            </template>
          </el-table-column>
          <el-table-column
            :formatter="dateFormatter"
            align="center"
            label="开始时间"
            prop="createTime"
            min-width="140"
          />
          <el-table-column
            :formatter="dateFormatter"
            align="center"
            label="结束时间"
            prop="endTime"
            min-width="140"
          />
          <el-table-column align="center" label="审批状态" prop="status" min-width="90">
            <template #default="scope">
              <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="scope.row.status" />
            </template>
          </el-table-column>
          <el-table-column align="center" label="耗时" prop="durationInMillis" width="100">
            <template #default="scope">
              {{ formatPast2(scope.row.durationInMillis) }}
            </template>
          </el-table-column>
        </el-table>
      </el-row>
    </el-dialog>
</template>
<script setup lang="ts">
import { SimpleFlowNode } from '../consts'
import { useWatchNode, useTaskStatusClass } from '../node'
import { dateFormatter, formatPast2 } from '@/utils/formatTime'
import { DICT_TYPE } from '@/utils/dict'
defineOptions({
  name: 'EndEventNode'
})
const props = defineProps({
  flowNode: {
    type: Object as () => SimpleFlowNode,
    default: () => null
  }
})
// 监控节点变化
const currentNode = useWatchNode(props)
// 是否只读
const readonly = inject<Boolean>('readonly')
const processInstance = inject<Ref<any>>('processInstance')
// 审批信息的弹窗显示,用于只读模式
const dialogVisible = ref(false) // 弹窗可见性
const processInstanceInfos = ref<any[]>([]) // 流程的审批信息
const nodeClick = () => {
  if (readonly) {
    if(processInstance && processInstance.value){
      processInstanceInfos.value = [
      {
        assigneeUser: processInstance.value.startUser,
        createTime: processInstance.value.startTime,
        endTime: processInstance.value.endTime,
        status: processInstance.value.status,
        durationInMillis: processInstance.value.durationInMillis
      }
    ]
      dialogVisible.value = true
    }
  }
}
</script>
<style lang="scss" scoped></style>
src/components/SimpleProcessDesignerV2/src/nodes/ExclusiveNode.vue
对比新文件
@@ -0,0 +1,229 @@
<template>
  <div class="branch-node-wrapper">
    <div class="branch-node-container">
      <div
        v-if="readonly"
        class="branch-node-readonly"
        :class="`${useTaskStatusClass(currentNode?.activityStatus)}`"
      >
        <span class="iconfont icon-exclusive icon-size condition"></span>
      </div>
      <el-button v-else class="branch-node-add" color="#67c23a" @click="addCondition" plain
        >添加条件</el-button
      >
      <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 },
                `${useTaskStatusClass(item.activityStatus)}`
              ]"
            >
              <div class="branch-node-title-container">
                <div v-if="!readonly && 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="!readonly && 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" :current-node="item" />
          </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"
      :current-node="currentNode"
    />
  </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 { useTaskStatusClass } from '../node'
import { generateUUID } from '@/utils'
import ConditionNodeConfig from '../nodes-config/ConditionNodeConfig.vue'
const { proxy } = getCurrentInstance() as any
defineOptions({
  name: 'ExclusiveNode'
})
const props = defineProps({
  flowNode: {
    type: Object as () => SimpleFlowNode,
    required: true
  }
})
// 定义事件,更新父组件
const emits = defineEmits<{
  'update:modelValue': [node: SimpleFlowNode | undefined]
  'find:parentNode': [nodeList: SimpleFlowNode[], nodeType: number]
  'find:recursiveFindParentNode': [
    nodeList: SimpleFlowNode[],
    curentNode: SimpleFlowNode,
    nodeType: number
  ]
}>()
// 是否只读
const readonly = inject<Boolean>('readonly')
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 || getDefaultConditionNodeName(index, conditionNode.defaultFlow)
}
// 点击条件名称
const clickEvent = (index: number) => {
  showInputs.value[index] = true
}
const conditionNodeConfig = (nodeId: string) => {
  if (readonly) {
    return
  }
  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_USER_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>
src/components/SimpleProcessDesignerV2/src/nodes/InclusiveNode.vue
对比新文件
@@ -0,0 +1,233 @@
<template>
  <div class="branch-node-wrapper">
    <div class="branch-node-container">
      <div
        v-if="readonly"
        class="branch-node-readonly"
        :class="`${useTaskStatusClass(currentNode?.activityStatus)}`"
      >
        <span class="iconfont icon-inclusive icon-size inclusive"></span>
      </div>
      <el-button v-else class="branch-node-add" color="#345da2" @click="addCondition" plain
        >添加条件</el-button
      >
      <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 },
                `${useTaskStatusClass(item.activityStatus)}`
              ]"
            >
              <div class="branch-node-title-container">
                <div v-if="showInputs[index]">
                  <input
                    type="text"
                    class="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>
              <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="!readonly && 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="!readonly && 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="
                  !readonly &&
                  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" :current-node="item" />
          </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"
      :current-node="currentNode"
    />
  </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 { useTaskStatusClass } from '../node'
import { getDefaultInclusiveConditionNodeName } from '../utils'
import { generateUUID } from '@/utils'
import ConditionNodeConfig from '../nodes-config/ConditionNodeConfig.vue'
const { proxy } = getCurrentInstance() as any
defineOptions({
  name: 'InclusiveNode'
})
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 readonly = inject<Boolean>('readonly')
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 || getDefaultInclusiveConditionNodeName(index, conditionNode.defaultFlow)
}
// 点击条件名称
const clickEvent = (index: number) => {
  showInputs.value[index] = true
}
const conditionNodeConfig = (nodeId: string) => {
  if (readonly) {
    return
  }
  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_USER_NODE) {
    return
  }
  if (node.type === nodeType) {
    nodeList.push(node)
  }
  // 条件节点 (NodeType.CONDITION_NODE) 比较特殊。需要调用其父节点条件分支节点(NodeType.INCLUSIVE_BRANCH_NODE) 继续查找
  emits('find:parentNode', nodeList, nodeType)
}
</script>
<style lang="scss" scoped></style>
src/components/SimpleProcessDesignerV2/src/nodes/ParallelNode.vue
对比新文件
@@ -0,0 +1,184 @@
<template>
  <div class="branch-node-wrapper">
    <div class="branch-node-container">
      <div
        v-if="readonly"
        class="branch-node-readonly"
        :class="`${useTaskStatusClass(currentNode?.activityStatus)}`"
      >
        <span class="iconfont icon-parallel icon-size parallel"></span>
      </div>
      <el-button v-else class="branch-node-add" color="#626aef" @click="addCondition" plain
        >添加分支</el-button
      >
      <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="`${useTaskStatusClass(item.activityStatus)}`">
              <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 v-if="!readonly" class="node-toolbar">
                <div class="toolbar-icon">
                  <Icon
                    color="#0089ff"
                    icon="ep:circle-close-filled"
                    :size="18"
                    @click="deleteCondition(index)"
                  />
                </div>
              </div>
            </div>
            <NodeHandler v-model:child-node="item.childNode" :current-node="item" />
          </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"
      :current-node="currentNode"
    />
  </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 { useTaskStatusClass } from '../node'
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)
// 是否只读
const readonly = inject<Boolean>('readonly')
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_USER_NODE) {
    return
  }
  if (node.type === nodeType) {
    nodeList.push(node)
  }
  // 条件节点 (NodeType.CONDITION_NODE) 比较特殊。需要调用其父节点并行节点(NodeType.PARALLEL_NODE) 继续查找
  emits('find:parentNode', nodeList, nodeType)
}
</script>
src/components/SimpleProcessDesignerV2/src/nodes/StartUserNode.vue
对比新文件
@@ -0,0 +1,154 @@
<template>
  <div class="node-wrapper">
    <div class="node-container">
      <div
        class="node-box"
        :class="[
          { 'node-config-error': !currentNode.showText },
          `${useTaskStatusClass(currentNode?.activityStatus)}`
        ]"
      >
        <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="nodeClick">
          <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" v-if="!readonly" />
        </div>
      </div>
      <!-- 传递子节点给添加节点组件。会在子节点前面添加节点 -->
      <NodeHandler
        v-if="currentNode"
        v-model:child-node="currentNode.childNode"
        :current-node="currentNode"
      />
    </div>
  </div>
  <StartUserNodeConfig v-if="!readonly && currentNode" ref="nodeSetting" :flow-node="currentNode" />
  <!-- 审批记录 -->
  <el-dialog
    :title="dialogTitle || '审批记录'"
    v-model="dialogVisible"
    width="1000px"
    append-to-body
  >
    <el-row>
      <el-table :data="selectTasks" size="small" border header-cell-class-name="table-header-gray">
        <el-table-column
          label="序号"
          header-align="center"
          align="center"
          type="index"
          width="50"
        />
        <el-table-column label="审批人" min-width="100" align="center">
          <template #default="scope">
            {{ scope.row.assigneeUser?.nickname || scope.row.ownerUser?.nickname }}
          </template>
        </el-table-column>
        <el-table-column label="部门" min-width="100" align="center">
          <template #default="scope">
            {{ scope.row.assigneeUser?.deptName || scope.row.ownerUser?.deptName }}
          </template>
        </el-table-column>
        <el-table-column
          :formatter="dateFormatter"
          align="center"
          label="开始时间"
          prop="createTime"
          min-width="140"
        />
        <el-table-column
          :formatter="dateFormatter"
          align="center"
          label="结束时间"
          prop="endTime"
          min-width="140"
        />
        <el-table-column align="center" label="审批状态" prop="status" min-width="90">
          <template #default="scope">
            <dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="scope.row.status" />
          </template>
        </el-table-column>
        <el-table-column align="center" label="审批建议" prop="reason" min-width="120" />
        <el-table-column align="center" label="耗时" prop="durationInMillis" width="100">
          <template #default="scope">
            {{ formatPast2(scope.row.durationInMillis) }}
          </template>
        </el-table-column>
      </el-table>
    </el-row>
  </el-dialog>
</template>
<script setup lang="ts">
import NodeHandler from '../NodeHandler.vue'
import { useWatchNode, useNodeName2, useTaskStatusClass } from '../node'
import { SimpleFlowNode, NODE_DEFAULT_TEXT, NodeType } from '../consts'
import StartUserNodeConfig from '../nodes-config/StartUserNodeConfig.vue'
import { dateFormatter, formatPast2 } from '@/utils/formatTime'
import { DICT_TYPE } from '@/utils/dict'
defineOptions({
  name: 'StartEventNode'
})
const props = defineProps({
  flowNode: {
    type: Object as () => SimpleFlowNode,
    default: () => null
  }
})
const readonly = inject<Boolean>('readonly') // 是否只读
const tasks = inject<Ref<any[]>>('tasks')
// 定义事件,更新父组件。
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 nodeClick = () => {
  if (readonly) {
    // 只读模式,弹窗显示任务信息
    if (tasks && tasks.value) {
      dialogTitle.value = currentNode.value.name
      selectTasks.value = tasks.value.filter(
        (item: any) => item?.taskDefinitionKey === currentNode.value.id
      )
      dialogVisible.value = true
    }
  } else {
    // 编辑模式,打开节点配置、把当前节点传递给配置组件
    nodeSetting.value.showStartUserNodeConfig(currentNode.value)
    nodeSetting.value.openDrawer()
  }
}
// 任务的弹窗显示,用于只读模式
const dialogVisible = ref(false) // 弹窗可见性
const dialogTitle = ref<string | undefined>(undefined) // 弹窗标题
const selectTasks = ref<any[] | undefined>([]) // 选中的任务数组
</script>
<style lang="scss" scoped></style>
src/components/SimpleProcessDesignerV2/src/nodes/UserTaskNode.vue
对比新文件
@@ -0,0 +1,174 @@
<template>
  <div class="node-wrapper">
    <div class="node-container">
      <div
        class="node-box"
        :class="[
          { 'node-config-error': !currentNode.showText },
          `${useTaskStatusClass(currentNode?.activityStatus)}`
        ]"
      >
        <div class="node-title-container">
          <div class="node-title-icon user-task"><span class="iconfont icon-approve"></span></div>
          <input
            v-if="!readonly && 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="nodeClick">
          <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" v-if="!readonly" />
        </div>
        <div v-if="!readonly" 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"
        :current-node="currentNode"
      />
    </div>
  </div>
  <UserTaskNodeConfig
    v-if="currentNode"
    ref="nodeSetting"
    :flow-node="currentNode"
    @find:return-task-nodes="findReturnTaskNodes"
  />
  <!-- 审批记录 -->
  <el-dialog
    :title="dialogTitle || '审批记录'"
    v-model="dialogVisible"
    width="1000px"
    append-to-body
  >
    <el-row>
      <el-table :data="selectTasks" size="small" border header-cell-class-name="table-header-gray">
        <el-table-column
          label="序号"
          header-align="center"
          align="center"
          type="index"
          width="50"
        />
        <el-table-column label="审批人" min-width="100" align="center">
          <template #default="scope">
            {{ scope.row.assigneeUser?.nickname || scope.row.ownerUser?.nickname }}
          </template>
        </el-table-column>
        <el-table-column label="部门" min-width="100" align="center">
          <template #default="scope">
            {{ scope.row.assigneeUser?.deptName || scope.row.ownerUser?.deptName }}
          </template>
        </el-table-column>
        <el-table-column
          :formatter="dateFormatter"
          align="center"
          label="开始时间"
          prop="createTime"
          min-width="140"
        />
        <el-table-column
          :formatter="dateFormatter"
          align="center"
          label="结束时间"
          prop="endTime"
          min-width="140"
        />
        <el-table-column align="center" label="审批状态" prop="status" min-width="90">
          <template #default="scope">
            <dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="scope.row.status" />
          </template>
        </el-table-column>
        <el-table-column align="center" label="审批建议" prop="reason" min-width="120" />
        <el-table-column align="center" label="耗时" prop="durationInMillis" width="100">
          <template #default="scope">
            {{ formatPast2(scope.row.durationInMillis) }}
          </template>
        </el-table-column>
      </el-table>
    </el-row>
  </el-dialog>
</template>
<script setup lang="ts">
import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
import { useWatchNode, useNodeName2, useTaskStatusClass } from '../node'
import NodeHandler from '../NodeHandler.vue'
import UserTaskNodeConfig from '../nodes-config/UserTaskNodeConfig.vue'
import { dateFormatter, formatPast2 } from '@/utils/formatTime'
import { DICT_TYPE } from '@/utils/dict'
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 readonly = inject<Boolean>('readonly')
const tasks = inject<Ref<any[]>>('tasks')
// 监控节点变化
const currentNode = useWatchNode(props)
// 节点名称编辑
const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.START_USER_NODE)
const nodeSetting = ref()
const nodeClick = () => {
  if (readonly) {
    if (tasks && tasks.value) {
      dialogTitle.value = currentNode.value.name
      // 只读模式,弹窗显示任务信息
      selectTasks.value = tasks.value.filter(
        (item: any) => item?.taskDefinitionKey === currentNode.value.id
      )
      dialogVisible.value = true
    }
  } else {
    // 编辑模式,打开节点配置、把当前节点传递给配置组件
    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)
}
// 任务的弹窗显示,用于只读模式
const dialogVisible = ref(false) // 弹窗可见性
const dialogTitle = ref<string | undefined>(undefined) // 弹窗标题
const selectTasks = ref<any[] | undefined>([]) // 选中的任务数组
</script>
<style lang="scss" scoped></style>
src/components/SimpleProcessDesignerV2/src/utils.ts
对比新文件
@@ -0,0 +1,41 @@
import { TimeUnitType, ApproveType, APPROVE_TYPE } from './consts'
// 获取条件节点默认的名称
export const getDefaultConditionNodeName = (index: number, defaultFlow: boolean | undefined): string => {
  if (defaultFlow) {
    return '其它情况'
  }
  return '条件' + (index + 1)
}
// 获取包容分支条件节点默认的名称
export const getDefaultInclusiveConditionNodeName = (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
}
src/components/SimpleProcessDesignerV2/theme/iconfont.ttf
Binary files differ
src/components/SimpleProcessDesignerV2/theme/iconfont.woff
Binary files differ
src/components/SimpleProcessDesignerV2/theme/iconfont.woff2
Binary files differ
src/components/SimpleProcessDesignerV2/theme/simple-process-designer.scss
对比新文件
@@ -0,0 +1,750 @@
// 配置节点头部
.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 {
    display: flex;
    flex-direction: column;
    align-items: center;
  }
  .handler-item-icon {
    width: 60px;
    height: 60px;
    background: #fff;
    border: 1px solid #e2e2e2;
    border-radius: 50%;
    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: 25px;
      line-height: 60px;
    }
  }
  .approve {
    color: #ff943e;
  }
  .copy {
    color: #3296fa;
  }
  .condition {
    color: #67c23a;
  }
  .parallel {
    color: #626aef;
  }
  .inclusive {
    color: #345da2;
  }
  .handler-item-text {
    margin-top: 4px;
    width: 80px;
    text-align: center;
    font-size: 13px;
  }
}
// Simple 流程模型样式
.simple-process-model-container {
  height: 100%;
  padding-top: 32px;
  background-color: #fafafa;
  .simple-process-model {
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    transform-origin: 50% 0 0;
    overflow: auto;
    transform: scale(1);
    transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
    background: url(@/assets/svgs/bpm/simple-process-bg.svg) 0 0 repeat;
    // 节点容器 定义节点宽度
    .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-radius: 8px;
      box-shadow: 0 1px 4px 0 rgb(10 30 65 / 16%);
      transition: all 0.1s cubic-bezier(0.645, 0.045, 0.355, 1);
      &.status-pass {
        background-color: #a9da90;
        border-color: #67c23a;
      }
      &.status-pass:hover {
        border-color: #67c23a;
      }
      &.status-running {
        background-color: #e7f0fe;
        border-color: #5a9cf8;
      }
      &.status-running:hover {
        border-color: #5a9cf8;
      }
      &.status-reject {
        background-color: #f6e5e5;
        border-color: #e47470;
      }
      &.status-reject:hover {
        border-color: #e47470;
      }
      &: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;
          overflow: hidden;
          font-size: 14px;
          font-weight: 600;
          line-height: 18px;
          color: #1f1f1f;
          text-overflow: ellipsis;
          white-space: nowrap;
          &: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 {
          overflow: hidden;
          font-size: 13px;
          font-weight: 600;
          color: #f60;
          text-overflow: ellipsis;
          white-space: nowrap;
          &:hover {
            border-bottom: 1px dashed #000;
          }
        }
        .branch-priority {
          min-width: 50px;
          font-size: 12px;
        }
      }
      .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: rgb(0 0 0 / 3%);
        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 0;
        margin-top: 4px;
        line-height: 32px;
        align-items: center;
        color: #111f2c;
        border-radius: 4px;
        .branch-node-text {
          overflow: hidden;
          font-size: 12px;
          line-height: 24px;
          text-overflow: ellipsis;
          word-break: break-all;
          -webkit-line-clamp: 2; /* 这将限制文本显示为两行 */
          -webkit-box-orient: vertical;
        }
      }
      // 节点操作 :删除
      .node-toolbar {
        position: absolute;
        top: -20px;
        right: 0;
        display: flex;
        opacity: 0;
        .toolbar-icon {
          text-align: center;
          vertical-align: middle;
        }
      }
      // 条件节点左右移动
      .branch-node-move {
        position: absolute;
        display: none;
        width: 10px;
        height: 100%;
        cursor: pointer;
        align-items: center;
        justify-content: center;
      }
      .move-node-left {
        top: 0;
        left: -2px;
        background: rgb(126 134 142 / 8%);
        border-bottom-left-radius: 8px;
        border-top-left-radius: 8px;
      }
      .move-node-right {
        top: 0;
        right: -2px;
        background: rgb(126 134 142 / 8%);
        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;
        z-index: 0;
        width: 2px;
        height: 100%;
        margin: auto;
        background-color: #dedede;
        content: '';
      }
      .node-handler {
        .add-icon {
          position: relative;
          top: -5px;
          display: flex;
          width: 25px;
          height: 25px;
          color: #fff;
          cursor: pointer;
          background-color: #0089ff;
          border-radius: 50%;
          align-items: center;
          justify-content: center;
          &: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;
          left: 50%;
          width: 4px;
          height: 100%;
          background-color: #fafafa;
          content: '';
          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;
          border: 2px solid #dedede;
          border-radius: 18px;
          transform: translateX(-50%);
          transform-origin: center center;
        }
        .branch-node-readonly {
          position: absolute;
          top: -18px;
          left: 50%;
          z-index: 1;
          display: flex;
          width: 36px;
          height: 36px;
          background-color: #fff;
          border: 2px solid #dedede;
          border-radius: 50%;
          transform: translateX(-50%);
          align-items: center;
          justify-content: center;
          transform-origin: center center;
          &.status-pass {
            background-color: #e9f4e2;
            border-color: #6bb63c;
          }
          &.status-pass:hover {
            border-color: #6bb63c;
          }
          .icon-size {
            font-size: 22px;
            &.condition {
              color: #67c23a;
            }
            &.parallel {
              color: #626aef;
            }
            &.inclusive {
              color: #345da2;
            }
          }
        }
        .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: #fafafa;
          border-radius: 30px;
          box-shadow: 0 1px 5px 0 rgb(10 30 65 / 8%);
          box-sizing: border-box;
        }
      }
    }
    // 结束节点包装
    .end-node-wrapper {
      margin-bottom: 16px;
      .end-node-box {
        display: flex;
        width: 80px;
        height: 36px;
        color: #212121;
        border: 2px solid #fafafa;
        border-radius: 30px;
        box-shadow: 0 1px 5px 0 rgb(10 30 65 / 8%);
        box-sizing: border-box;
        justify-content: center;
        align-items: center;
        &.status-pass {
          background-color: #a9da90;
          border-color: #6bb63c;
        }
        &.status-pass:hover {
          border-color: #6bb63c;
        }
        &.status-reject {
          background-color: #f6e5e5;
          border-color: #e47470;
        }
        &.status-reject:hover {
          border-color: #e47470;
        }
        &.status-cancel {
          background-color: #eaeaeb;
          border-color: #919398;
        }
        &.status-cancel:hover {
          border-color: #919398;
        }
      }
    }
    // 可编辑的 title 输入框
    .editable-title-input {
      height: 20px;
      max-width: 145px;
      margin-left: 4px;
      font-size: 12px;
      line-height: 20px;
      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%);
      }
    }
  }
}
// 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';
}
src/components/UserSelectForm/index.vue
对比新文件
@@ -0,0 +1,152 @@
<template>
  <Dialog v-model="dialogVisible" title="人员选择" width="800">
    <el-row class="gap2" v-loading="formLoading">
      <el-col :span="6">
        <ContentWrap class="h-1/1">
          <el-tree
            ref="treeRef"
            :data="deptTree"
            :expand-on-click-node="false"
            :props="defaultProps"
            default-expand-all
            highlight-current
            node-key="id"
            @node-click="handleNodeClick"
          />
        </ContentWrap>
      </el-col>
      <el-col :span="17">
        <el-transfer
          v-model="selectedUserIdList"
          :titles="['未选', '已选']"
          filterable
          filter-placeholder="搜索成员"
          :data="transferUserList"
          :props="{ label: 'nickname', key: 'id' }"
        />
      </el-col>
    </el-row>
    <template #footer>
      <el-button
        :disabled="formLoading || !selectedUserIdList?.length"
        type="primary"
        @click="submitForm"
      >
        确 定
      </el-button>
      <el-button @click="dialogVisible = false">取 消</el-button>
    </template>
  </Dialog>
</template>
<script lang="ts" setup>
import { defaultProps, findTreeNode, handleTree } from '@/utils/tree'
import * as DeptApi from '@/api/system/dept'
import * as UserApi from '@/api/system/user'
defineOptions({ name: 'UserSelectForm' })
const emit = defineEmits<{
  confirm: [id: any, userList: any[]]
}>()
const { t } = useI18n() // 国际
const message = useMessage() // 消息弹窗
const deptTree = ref<Tree[]>([]) // 部门树形结构化
const userList = ref<UserApi.UserVO[]>([]) // 所有用户列表
const filteredUserList = ref<UserApi.UserVO[]>([]) // 当前部门过滤后的用户列表
const selectedUserIdList: any = ref([]) // 选中的用户列表
const dialogVisible = ref(false) // 弹窗的是否展示
const formLoading = ref(false) // 表单的加载中
const activityId = ref()
/** 计算属性:合并已选择的用户和当前部门过滤后的用户 */
const transferUserList = computed(() => {
  // 1.1 获取所有已选择的用户
  const selectedUsers = userList.value.filter((user: any) =>
    selectedUserIdList.value.includes(user.id)
  )
  // 1.2 获取当前部门过滤后的未选择用户
  const filteredUnselectedUsers = filteredUserList.value.filter(
    (user: any) => !selectedUserIdList.value.includes(user.id)
  )
  // 2. 合并并去重
  return [...selectedUsers, ...filteredUnselectedUsers]
})
/** 打开弹窗 */
const open = async (id: number, selectedList?: any[]) => {
  activityId.value = id
  resetForm()
  // 加载部门、用户列表
  deptTree.value = handleTree(await DeptApi.getSimpleDeptList())
  userList.value = await UserApi.getSimpleUserList()
  // 初始状态下,过滤列表等于所有用户列表
  filteredUserList.value = [...userList.value]
  selectedUserIdList.value = selectedList?.map((item: any) => item.id) || []
  dialogVisible.value = true
}
/** 获取部门过滤后的用户列表 */
const getUserList = async (deptId?: number) => {
  formLoading.value = true
  try {
    // @ts-ignore
    // TODO @芋艿:替换到 simple List 暂不支持 deptId 过滤
    // TODO @Zqqq:这个,可以使用前端过滤么?通过 deptList 获取到 deptId 子节点,然后去 userList
    const data = await UserApi.getUserPage({ pageSize: 100, pageNo: 1, deptId })
    // 更新过滤后的用户列表
    filteredUserList.value = data.list
  } finally {
    formLoading.value = false
  }
}
/** 提交选择 */
const submitForm = async () => {
  try {
    message.success(t('common.updateSuccess'))
    dialogVisible.value = false
    // 从所有用户列表中筛选出已选择的用户
    const emitUserList = userList.value.filter((user: any) =>
      selectedUserIdList.value.includes(user.id)
    )
    // 发送操作成功的事件
    emit('confirm', activityId.value, emitUserList)
  } finally {
  }
}
/** 重置表单 */
const resetForm = () => {
  deptTree.value = []
  userList.value = []
  filteredUserList.value = []
  selectedUserIdList.value = []
}
/** 处理部门被点击 */
const handleNodeClick = (row: { [key: string]: any }) => {
  getUserList(row.id)
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
</script>
<style lang="scss" scoped>
:deep() {
  .el-transfer {
    display: flex;
  }
  .el-transfer__buttons {
    display: flex !important;
    flex-direction: column-reverse;
    justify-content: center;
    gap: 20px;
    .el-transfer__button:nth-child(2) {
      margin: 0;
    }
  }
}
</style>
src/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue
@@ -664,7 +664,7 @@
    previewModelVisible.value = true
  })
}
/* ------------------------------------------------ 工业互联网平台 methods ------------------------------------------------------ */
/* ------------------------------------------------ 芋道源码 methods ------------------------------------------------------ */
const processSave = async () => {
  // console.log(bpmnModeler, 'bpmnModelerbpmnModelerbpmnModelerbpmnModeler')
  const { err, xml } = await bpmnModeler.saveXML()
src/components/bpmnProcessDesigner/package/designer/ProcessViewer.vue
@@ -1,664 +1,379 @@
<template>
  <div class="my-process-designer">
    <div class="my-process-designer__container">
      <div class="my-process-designer__canvas" style="height: 760px" ref="bpmnCanvas"></div>
  <div class="process-viewer">
    <div style="height: 100%" ref="processCanvas" v-show="!isLoading"> </div>
    <!-- 自定义箭头样式,用于已完成状态下流程连线箭头 -->
    <defs ref="customDefs">
      <marker
        id="sequenceflow-end-white-success"
        viewBox="0 0 20 20"
        refX="11"
        refY="10"
        markerWidth="10"
        markerHeight="10"
        orient="auto"
      >
        <path
          class="success-arrow"
          d="M 1 5 L 11 10 L 1 15 Z"
          style="stroke-width: 1px; stroke-linecap: round; stroke-dasharray: 10000, 1"
        />
      </marker>
      <marker
        id="conditional-flow-marker-white-success"
        viewBox="0 0 20 20"
        refX="-1"
        refY="10"
        markerWidth="10"
        markerHeight="10"
        orient="auto"
      >
        <path
          class="success-conditional"
          d="M 0 10 L 8 6 L 16 10 L 8 14 Z"
          style="stroke-width: 1px; stroke-linecap: round; stroke-dasharray: 10000, 1"
        />
      </marker>
    </defs>
    <!-- 审批记录 -->
    <el-dialog :title="dialogTitle || '审批记录'" v-model="dialogVisible" width="1000px">
      <el-row>
        <el-table
          :data="selectTasks"
          size="small"
          border
          header-cell-class-name="table-header-gray"
        >
          <el-table-column
            label="序号"
            header-align="center"
            align="center"
            type="index"
            width="50"
          />
          <el-table-column
            label="审批人"
            min-width="100"
            align="center"
            v-if="selectActivityType === 'bpmn:UserTask'"
          >
            <template #default="scope">
              {{ scope.row.assigneeUser?.nickname || scope.row.ownerUser?.nickname }}
            </template>
          </el-table-column>
          <el-table-column
            label="发起人"
            prop="assigneeUser.nickname"
            min-width="100"
            align="center"
            v-else
          />
          <el-table-column label="部门" min-width="100" align="center">
            <template #default="scope">
              {{ scope.row.assigneeUser?.deptName || scope.row.ownerUser?.deptName }}
            </template>
          </el-table-column>
          <el-table-column
            :formatter="dateFormatter"
            align="center"
            label="开始时间"
            prop="createTime"
            min-width="140"
          />
          <el-table-column
            :formatter="dateFormatter"
            align="center"
            label="结束时间"
            prop="endTime"
            min-width="140"
          />
          <el-table-column align="center" label="审批状态" prop="status" min-width="90">
            <template #default="scope">
              <dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="scope.row.status" />
            </template>
          </el-table-column>
          <el-table-column
            align="center"
            label="审批建议"
            prop="reason"
            min-width="120"
            v-if="selectActivityType === 'bpmn:UserTask'"
          />
          <el-table-column align="center" label="耗时" prop="durationInMillis" width="100">
            <template #default="scope">
              {{ formatPast2(scope.row.durationInMillis) }}
            </template>
          </el-table-column>
        </el-table>
      </el-row>
    </el-dialog>
    <!-- Zoom:放大、缩小 -->
    <div style="position: absolute; top: 0; left: 0; width: 100%">
      <el-row type="flex" justify="end">
        <el-button-group key="scale-control" size="default">
          <el-button
            size="default"
            :plain="true"
            :disabled="defaultZoom <= 0.3"
            :icon="ZoomOut"
            @click="processZoomOut()"
          />
          <el-button size="default" style="width: 90px">
            {{ Math.floor(defaultZoom * 10 * 10) + '%' }}
          </el-button>
          <el-button
            size="default"
            :plain="true"
            :disabled="defaultZoom >= 3.9"
            :icon="ZoomIn"
            @click="processZoomIn()"
          />
          <el-button size="default" :icon="ScaleToOriginal" @click="processReZoom()" />
        </el-button-group>
      </el-row>
    </div>
  </div>
</template>
<script lang="ts" setup>
import '../theme/index.scss'
import BpmnViewer from 'bpmn-js/lib/Viewer'
import DefaultEmptyXML from './plugins/defaultEmpty'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { formatDate } from '@/utils/formatTime'
import { isEmpty } from '@/utils/is'
defineOptions({ name: 'MyProcessViewer' })
import MoveCanvasModule from 'diagram-js/lib/navigation/movecanvas'
import { ZoomOut, ZoomIn, ScaleToOriginal } from '@element-plus/icons-vue'
import { DICT_TYPE } from '@/utils/dict'
import { dateFormatter, formatPast2 } from '@/utils/formatTime'
import { BpmProcessInstanceStatus } from '@/utils/constants'
const props = defineProps({
  value: {
    // BPMN XML 字符串
  xml: {
    type: String,
    default: ''
    required: true
  },
  prefix: {
    // 使用哪个引擎
    type: String,
    default: 'camunda'
  },
  activityData: {
    // 活动的数据。传递时,可高亮流程
    type: Array,
    default: () => []
  },
  processInstanceData: {
    // 流程实例的数据。传递时,可展示流程发起人等信息
  view: {
    type: Object,
    default: () => {}
  },
  taskData: {
    // 任务实例的数据。传递时,可展示 UserTask 审核相关的信息
    type: Array,
    default: () => []
    require: true
  }
})
provide('configGlobal', props)
const processCanvas = ref()
const bpmnViewer = ref<BpmnViewer | null>(null)
const customDefs = ref()
const defaultZoom = ref(1) // 默认缩放比例
const isLoading = ref(false) // 是否加载中
const emit = defineEmits(['destroy'])
const processInstance = ref<any>({}) // 流程实例
const tasks = ref([]) // 流程任务
let bpmnModeler
const dialogVisible = ref(false) // 弹窗可见性
const dialogTitle = ref<string | undefined>(undefined) // 弹窗标题
const selectActivityType = ref<string | undefined>(undefined) // 选中 Task 的活动编号
const selectTasks = ref<any[]>([]) // 选中的任务数组
const xml = ref('')
const activityLists = ref<any[]>([])
const processInstance = ref<any>(undefined)
const taskList = ref<any[]>([])
const bpmnCanvas = ref()
// const element = ref()
const elementOverlayIds = ref<any>(null)
const overlays = ref<any>(null)
const initBpmnModeler = () => {
  if (bpmnModeler) return
  bpmnModeler = new BpmnViewer({
    container: bpmnCanvas.value,
    bpmnRenderer: {}
  })
/** Zoom:恢复 */
const processReZoom = () => {
  defaultZoom.value = 1
  bpmnViewer.value?.get('canvas').zoom('fit-viewport', 'auto')
}
/* 创建新的流程图 */
const createNewDiagram = async (xml) => {
  // 将字符串转换成图显示出来
  let newId = `Process_${new Date().getTime()}`
  let newName = `业务流程_${new Date().getTime()}`
  let xmlString = xml || DefaultEmptyXML(newId, newName, props.prefix)
/** Zoom:放大 */
const processZoomIn = (zoomStep = 0.1) => {
  let newZoom = Math.floor(defaultZoom.value * 100 + zoomStep * 100) / 100
  if (newZoom > 4) {
    throw new Error('[Process Designer Warn ]: The zoom ratio cannot be greater than 4')
  }
  defaultZoom.value = newZoom
  bpmnViewer.value?.get('canvas').zoom(defaultZoom.value)
}
/** Zoom:缩小 */
const processZoomOut = (zoomStep = 0.1) => {
  let newZoom = Math.floor(defaultZoom.value * 100 - zoomStep * 100) / 100
  if (newZoom < 0.2) {
    throw new Error('[Process Designer Warn ]: The zoom ratio cannot be less than 0.2')
  }
  defaultZoom.value = newZoom
  bpmnViewer.value?.get('canvas').zoom(defaultZoom.value)
}
/** 流程图预览清空 */
const clearViewer = () => {
  if (processCanvas.value) {
    processCanvas.value.innerHTML = ''
  }
  if (bpmnViewer.value) {
    bpmnViewer.value.destroy()
  }
  bpmnViewer.value = null
}
/** 添加自定义箭头 */
// TODO 芋艿:自定义箭头不生效,有点奇怪!!!!相关的 marker-end、marker-start 暂时也注释了!!!
const addCustomDefs = () => {
  if (!bpmnViewer.value) {
    return
  }
  const canvas = bpmnViewer.value?.get('canvas')
  const svg = canvas?._svg
  svg.appendChild(customDefs.value)
}
/** 节点选中 */
const onSelectElement = (element: any) => {
  // 清空原选中
  selectActivityType.value = undefined
  dialogTitle.value = undefined
  if (!element || !processInstance.value?.id) {
    return
  }
  // UserTask 的情况
  const activityType = element.type
  selectActivityType.value = activityType
  if (activityType === 'bpmn:UserTask') {
    dialogTitle.value = element.businessObject ? element.businessObject.name : undefined
    selectTasks.value = tasks.value.filter((item: any) => item?.taskDefinitionKey === element.id)
    dialogVisible.value = true
  } else if (activityType === 'bpmn:EndEvent' || activityType === 'bpmn:StartEvent') {
    dialogTitle.value = '审批信息'
    selectTasks.value = [
      {
        assigneeUser: processInstance.value.startUser,
        createTime: processInstance.value.startTime,
        endTime: processInstance.value.endTime,
        status: processInstance.value.status,
        durationInMillis: processInstance.value.durationInMillis
      }
    ]
    dialogVisible.value = true
  }
}
/** 初始化 BPMN 视图 */
const importXML = async (xml: string) => {
  // 清空流程图
  clearViewer()
  // 初始化流程图
  if (xml != null && xml !== '') {
  try {
    let { warnings } = await bpmnModeler.importXML(xmlString)
    if (warnings && warnings.length) {
      warnings.forEach((warn) => console.warn(warn))
    }
    // 高亮流程图
    await highlightDiagram()
    const canvas = bpmnModeler.get('canvas')
    canvas.zoom('fit-viewport', 'auto')
      bpmnViewer.value = new BpmnViewer({
        additionalModules: [MoveCanvasModule],
        container: processCanvas.value
      })
      // 增加点击事件
      bpmnViewer.value.on('element.click', ({ element }) => {
        onSelectElement(element)
      })
      // 初始化 BPMN 视图
      isLoading.value = true
      await bpmnViewer.value.importXML(xml)
      // 自定义成功的箭头
      addCustomDefs()
  } catch (e) {
    console.error(e)
    // console.error(`[Process Designer Warn]: ${e?.message || e}`);
      clearViewer()
    } finally {
      isLoading.value = false
      // 高亮流程
      setProcessStatus(props.view)
    }
  }
}
/* 高亮流程图 */
// TODO 芋艿:如果多个 endActivity 的话,目前的逻辑可能有一定的问题。https://www.jdon.com/workflow/multi-events.html
const highlightDiagram = async () => {
  const activityList = activityLists.value
  if (activityList.length === 0) {
/** 高亮流程 */
const setProcessStatus = (view: any) => {
  // 设置相关变量
  if (!view || !view.processInstance) {
    return
  }
  // 参考自 https://gitee.com/tony2y/RuoYi-flowable/blob/master/ruoyi-ui/src/components/Process/index.vue#L222 实现
  // 再次基础上,增加不同审批结果的颜色等等
  let canvas = bpmnModeler.get('canvas')
  let todoActivity: any = activityList.find((m: any) => !m.endTime) // 找到待办的任务
  let endActivity: any = activityList[activityList.length - 1] // 获得最后一个任务
  let findProcessTask = false //是否已经高亮了进行中的任务
  //进行中高亮之后的任务 key 集合,用于过滤掉 taskList 进行中后面的任务,避免进行中后面的数据 Hover 还有数据
  let removeTaskDefinitionKeyList = []
  // debugger
  bpmnModeler.getDefinitions().rootElements[0].flowElements?.forEach((n: any) => {
    let activity: any = activityList.find((m: any) => m.key === n.id) // 找到对应的活动
    if (!activity) {
  processInstance.value = view.processInstance
  tasks.value = view.tasks
  if (isLoading.value || !bpmnViewer.value) {
      return
    }
    if (n.$type === 'bpmn:UserTask') {
      // 用户任务
      // 处理用户任务的高亮
      const task: any = taskList.value.find((m: any) => m.id === activity.taskId) // 找到活动对应的 taskId
      if (!task) {
        return
      }
      // 进行中的任务已经高亮过了,则不高亮后面的任务了
      if (findProcessTask) {
        removeTaskDefinitionKeyList.push(n.id)
        return
      }
      // 高亮任务
      canvas.addMarker(n.id, getResultCss(task.status))
      //标记是否高亮了进行中任务
      if (task.status === 1) {
        findProcessTask = true
      }
      // 如果非通过,就不走后面的线条了
      if (task.status !== 2) {
        return
      }
      // 处理 outgoing 出线
      const outgoing = getActivityOutgoing(activity)
      outgoing?.forEach((nn: any) => {
        // debugger
        let targetActivity: any = activityList.find((m: any) => m.key === nn.targetRef.id)
        // 如果目标活动存在,则根据该活动是否结束,进行【bpmn:SequenceFlow】连线的高亮设置
        if (targetActivity) {
          canvas.addMarker(nn.id, targetActivity.endTime ? 'highlight' : 'highlight-todo')
        } else if (nn.targetRef.$type === 'bpmn:ExclusiveGateway') {
          // TODO 芋艿:这个流程,暂时没走到过
          canvas.addMarker(nn.id, activity.endTime ? 'highlight' : 'highlight-todo')
          canvas.addMarker(nn.targetRef.id, activity.endTime ? 'highlight' : 'highlight-todo')
        } else if (nn.targetRef.$type === 'bpmn:EndEvent') {
          // TODO 芋艿:这个流程,暂时没走到过
          if (!todoActivity && endActivity.key === n.id) {
            canvas.addMarker(nn.id, 'highlight')
            canvas.addMarker(nn.targetRef.id, 'highlight')
          }
          if (!activity.endTime) {
            canvas.addMarker(nn.id, 'highlight-todo')
            canvas.addMarker(nn.targetRef.id, 'highlight-todo')
  const {
    unfinishedTaskActivityIds,
    finishedTaskActivityIds,
    finishedSequenceFlowActivityIds,
    rejectedTaskActivityIds
  } = view
  const canvas = bpmnViewer.value.get('canvas')
  const elementRegistry = bpmnViewer.value.get('elementRegistry')
  // 已完成节点
  if (Array.isArray(finishedSequenceFlowActivityIds)) {
    finishedSequenceFlowActivityIds.forEach((item: any) => {
      if (item != null) {
        canvas.addMarker(item, 'success')
        const element = elementRegistry.get(item)
        const conditionExpression = element.businessObject.conditionExpression
        if (conditionExpression) {
          canvas.addMarker(item, 'condition-expression')
          }
        }
      })
    } else if (n.$type === 'bpmn:ExclusiveGateway') {
      // 排它网关
      // 设置【bpmn:ExclusiveGateway】排它网关的高亮
      canvas.addMarker(n.id, getActivityHighlightCss(activity))
      // 查找需要高亮的连线
      let matchNN: any = undefined
      let matchActivity: any = undefined
      n.outgoing?.forEach((nn: any) => {
        let targetActivity = activityList.find((m: any) => m.key === nn.targetRef.id)
        if (!targetActivity) {
          return
        }
        // 特殊判断 endEvent 类型的原因,ExclusiveGateway 可能后续连有 2 个路径:
        //  1. 一个是 UserTask => EndEvent
        //  2. 一个是 EndEvent
        // 在选择路径 1 时,其实 EndEvent 可能也存在,导致 1 和 2 都高亮,显然是不正确的。
        // 所以,在 matchActivity 为 EndEvent 时,需要进行覆盖~~
        if (!matchActivity || matchActivity.type === 'endEvent') {
          matchNN = nn
          matchActivity = targetActivity
  if (Array.isArray(finishedTaskActivityIds)) {
    finishedTaskActivityIds.forEach((item: any) => canvas.addMarker(item, 'success'))
  }
  // 未完成节点
  if (Array.isArray(unfinishedTaskActivityIds)) {
    unfinishedTaskActivityIds.forEach((item: any) => canvas.addMarker(item, 'primary'))
  }
  // 被拒绝节点
  if (Array.isArray(rejectedTaskActivityIds)) {
    rejectedTaskActivityIds.forEach((item: any) => {
      if (item != null) {
        canvas.addMarker(item, 'danger')
        }
      })
      if (matchNN && matchActivity) {
        canvas.addMarker(matchNN.id, getActivityHighlightCss(matchActivity))
      }
    } else if (n.$type === 'bpmn:ParallelGateway') {
      // 并行网关
      // 设置【bpmn:ParallelGateway】并行网关的高亮
      canvas.addMarker(n.id, getActivityHighlightCss(activity))
      n.outgoing?.forEach((nn: any) => {
        // 获得连线是否有指向目标。如果有,则进行高亮
        const targetActivity = activityList.find((m: any) => m.key === nn.targetRef.id)
        if (targetActivity) {
          canvas.addMarker(nn.id, getActivityHighlightCss(targetActivity)) // 高亮【bpmn:SequenceFlow】连线
          // 高亮【...】目标。其中 ... 可以是 bpm:UserTask、也可以是其它的。当然,如果是 bpm:UserTask 的话,其实不做高亮也没问题,因为上面有逻辑做了这块。
          canvas.addMarker(nn.targetRef.id, getActivityHighlightCss(targetActivity))
        }
      })
    } else if (n.$type === 'bpmn:StartEvent') {
      // 开始节点
      canvas.addMarker(n.id, 'highlight')
      n.outgoing?.forEach((nn) => {
        // outgoing 例如说【bpmn:SequenceFlow】连线
        // 获得连线是否有指向目标。如果有,则进行高亮
        let targetActivity = activityList.find((m: any) => m.key === nn.targetRef.id)
        if (targetActivity) {
          canvas.addMarker(nn.id, 'highlight') // 高亮【bpmn:SequenceFlow】连线
          canvas.addMarker(n.id, 'highlight') // 高亮【bpmn:StartEvent】开始节点(自己)
        }
      })
    } else if (n.$type === 'bpmn:EndEvent') {
      // 结束节点
      if (!processInstance.value || processInstance.value.status === 1) {
        return
      }
      canvas.addMarker(n.id, getResultCss(processInstance.value.status))
    } else if (n.$type === 'bpmn:ServiceTask') {
      //服务任务
      if (activity.startTime > 0 && activity.endTime === 0) {
        //进入执行,标识进行色
        canvas.addMarker(n.id, getResultCss(1))
      }
      if (activity.endTime > 0) {
        // 执行完成,节点标识完成色, 所有outgoing标识完成色。
        canvas.addMarker(n.id, getResultCss(2))
        const outgoing = getActivityOutgoing(activity)
        outgoing?.forEach((out) => {
          canvas.addMarker(out.id, getResultCss(2))
        })
      }
    } else if (n.$type === 'bpmn:SequenceFlow') {
      let targetActivity = activityList.find((m: any) => m.key === n.targetRef.id)
      if (targetActivity) {
        canvas.addMarker(n.id, getActivityHighlightCss(targetActivity))
      }
    }
  })
  if (!isEmpty(removeTaskDefinitionKeyList)) {
    taskList.value = taskList.value.filter(
      (item) => !removeTaskDefinitionKeyList.includes(item.taskDefinitionKey)
  // 特殊:处理 end 节点的高亮。因为 end 在拒绝、取消时,被后端计算成了 finishedTaskActivityIds 里
  if (
    [BpmProcessInstanceStatus.CANCEL, BpmProcessInstanceStatus.REJECT].includes(
      processInstance.value.status
    )
  ) {
    const endNodes = elementRegistry.filter((element: any) => element.type === 'bpmn:EndEvent')
    endNodes.forEach((item: any) => {
      canvas.removeMarker(item.id, 'success')
      if (processInstance.value.status === BpmProcessInstanceStatus.CANCEL) {
        canvas.addMarker(item.id, 'cancel')
      } else {
        canvas.addMarker(item.id, 'danger')
  }
}
const getActivityHighlightCss = (activity) => {
  return activity.endTime ? 'highlight' : 'highlight-todo'
}
const getResultCss = (status) => {
  if (status === 1) {
    // 审批中
    return 'highlight-todo'
  } else if (status === 2) {
    // 已通过
    return 'highlight'
  } else if (status === 3) {
    // 不通过
    return 'highlight-reject'
  } else if (status === 4) {
    // 已取消
    return 'highlight-cancel'
  } else if (status === 5) {
    // 退回
    return 'highlight-return'
  } else if (status === 6) {
    // 委派
    return 'highlight-todo'
  } else if (status === 7) {
    // 审批通过中
    return 'highlight-todo'
  } else if (status === 0) {
    // 待审批
    return 'highlight-todo'
  }
  return ''
}
const getActivityOutgoing = (activity) => {
  // 如果有 outgoing,则直接使用它
  if (activity.outgoing && activity.outgoing.length > 0) {
    return activity.outgoing
  }
  // 如果没有,则遍历获得起点为它的【bpmn:SequenceFlow】节点们。原因是:bpmn-js 的 UserTask 拿不到 outgoing
  const flowElements = bpmnModeler.getDefinitions().rootElements[0].flowElements
  const outgoing: any[] = []
  flowElements.forEach((item: any) => {
    if (item.$type !== 'bpmn:SequenceFlow') {
      return
    }
    if (item.sourceRef.id === activity.key) {
      outgoing.push(item)
    }
  })
  return outgoing
}
const initModelListeners = () => {
  const EventBus = bpmnModeler.get('eventBus')
  // 注册需要的监听事件
  EventBus.on('element.hover', function (eventObj) {
    let element = eventObj ? eventObj.element : null
    elementHover(element)
  })
  EventBus.on('element.out', function (eventObj) {
    let element = eventObj ? eventObj.element : null
    elementOut(element)
  })
}
// 流程图的元素被 hover
const elementHover = (element) => {
  element.value = element
  !elementOverlayIds.value && (elementOverlayIds.value = {})
  !overlays.value && (overlays.value = bpmnModeler.get('overlays'))
  // 展示信息
  // console.log(activityLists.value, 'activityLists.value')
  // console.log(element.value, 'element.value')
  const activity = activityLists.value.find((m) => m.key === element.value.id)
  // console.log(activity, 'activityactivityactivityactivity')
  if (!activity) {
    return
  }
  if (!elementOverlayIds.value[element.value.id] && element.value.type !== 'bpmn:Process') {
    let html = `<div class="element-overlays">
            <p>Elemet id: ${element.value.id}</p>
            <p>Elemet type: ${element.value.type}</p>
          </div>` // 默认值
    if (element.value.type === 'bpmn:StartEvent' && processInstance.value) {
      html = `<p>发起人:${processInstance.value.startUser.nickname}</p>
                  <p>部门:${processInstance.value.startUser.deptName}</p>
                  <p>创建时间:${formatDate(processInstance.value.createTime)}`
    } else if (element.value.type === 'bpmn:UserTask') {
      let task = taskList.value.find((m) => m.id === activity.taskId) // 找到活动对应的 taskId
      if (!task) {
        return
      }
      let optionData = getIntDictOptions(DICT_TYPE.BPM_TASK_STATUS)
      let dataResult = ''
      optionData.forEach((element) => {
        if (element.value == task.status) {
          dataResult = element.label
        }
      })
      html = `<p>审批人:${task.assigneeUser.nickname}</p>
                  <p>部门:${task.assigneeUser.deptName}</p>
                  <p>结果:${dataResult}</p>
                  <p>创建时间:${formatDate(task.createTime)}</p>`
      // html = `<p>审批人:${task.assigneeUser.nickname}</p>
      //             <p>部门:${task.assigneeUser.deptName}</p>
      //             <p>结果:${getIntDictOptions(
      //               DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT,
      //               task.status
      //             )}</p>
      //             <p>创建时间:${formatDate(task.createTime)}</p>`
      if (task.endTime) {
        html += `<p>结束时间:${formatDate(task.endTime)}</p>`
      }
      if (task.reason) {
        html += `<p>审批建议:${task.reason}</p>`
      }
    } else if (element.value.type === 'bpmn:ServiceTask' && processInstance.value) {
      if (activity.startTime > 0) {
        html = `<p>创建时间:${formatDate(activity.startTime)}</p>`
      }
      if (activity.endTime > 0) {
        html += `<p>结束时间:${formatDate(activity.endTime)}</p>`
      }
      console.log(html)
    } else if (element.value.type === 'bpmn:EndEvent' && processInstance.value) {
      let optionData = getIntDictOptions(DICT_TYPE.BPM_TASK_STATUS)
      let dataResult = ''
      optionData.forEach((element) => {
        if (element.value == processInstance.value.status) {
          dataResult = element.label
        }
      })
      html = `<p>结果:${dataResult}</p>`
      // html = `<p>结果:${getIntDictOptions(
      //   DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT,
      //   processInstance.value.status
      // )}</p>`
      if (processInstance.value.endTime) {
        html += `<p>结束时间:${formatDate(processInstance.value.endTime)}</p>`
      }
    }
    // console.log(html, 'html111111111111111')
    elementOverlayIds.value[element.value.id] = toRaw(overlays.value)?.add(element.value, {
      position: { left: 0, bottom: 0 },
      html: `<div class="element-overlays">${html}</div>`
    })
  }
}
// 流程图的元素被 out
const elementOut = (element) => {
  toRaw(overlays.value).remove({ element })
  elementOverlayIds.value[element.id] = null
}
watch(
  () => props.xml,
  (newXml) => {
    importXML(newXml)
  },
  { immediate: true }
)
watch(
  () => props.view,
  (newView) => {
    setProcessStatus(newView)
  },
  { immediate: true }
)
/** mounted:初始化 */
onMounted(() => {
  xml.value = props.value
  activityLists.value = props.activityData
  // 初始化
  initBpmnModeler()
  createNewDiagram(xml.value)
  // 初始模型的监听器
  initModelListeners()
  importXML(props.xml)
  setProcessStatus(props.view)
})
/** unmount:销毁 */
onBeforeUnmount(() => {
  // this.$once('hook:beforeDestroy', () => {
  // })
  if (bpmnModeler) bpmnModeler.destroy()
  emit('destroy', bpmnModeler)
  bpmnModeler = null
  clearViewer()
})
watch(
  () => props.value,
  (newValue) => {
    xml.value = newValue
    createNewDiagram(xml.value)
  }
)
watch(
  () => props.activityData,
  (newActivityData) => {
    activityLists.value = newActivityData
    createNewDiagram(xml.value)
  }
)
watch(
  () => props.processInstanceData,
  (newProcessInstanceData) => {
    processInstance.value = newProcessInstanceData
    createNewDiagram(xml.value)
  }
)
watch(
  () => props.taskData,
  (newTaskListData) => {
    taskList.value = newTaskListData
    createNewDiagram(xml.value)
  }
)
</script>
<style lang="scss">
/** 处理中 */
.highlight-todo.djs-connection > .djs-visual > path {
  stroke: #1890ff !important;
  stroke-dasharray: 4px !important;
  fill-opacity: 0.2 !important;
}
.highlight-todo.djs-shape .djs-visual > :nth-child(1) {
  fill: #1890ff !important;
  stroke: #1890ff !important;
  stroke-dasharray: 4px !important;
  fill-opacity: 0.2 !important;
}
:deep(.highlight-todo.djs-connection > .djs-visual > path) {
  stroke: #1890ff !important;
  stroke-dasharray: 4px !important;
  fill-opacity: 0.2 !important;
  marker-end: url('#sequenceflow-end-_E7DFDF-_E7DFDF-803g1kf6zwzmcig1y2ulm5egr');
}
:deep(.highlight-todo.djs-shape .djs-visual > :nth-child(1)) {
  fill: #1890ff !important;
  stroke: #1890ff !important;
  stroke-dasharray: 4px !important;
  fill-opacity: 0.2 !important;
}
/** 通过 */
.highlight.djs-shape .djs-visual > :nth-child(1) {
  fill: green !important;
  stroke: green !important;
  fill-opacity: 0.2 !important;
}
.highlight.djs-shape .djs-visual > :nth-child(2) {
  fill: green !important;
}
.highlight.djs-shape .djs-visual > path {
  fill: green !important;
  fill-opacity: 0.2 !important;
  stroke: green !important;
}
.highlight.djs-connection > .djs-visual > path {
  stroke: green !important;
}
.highlight:not(.djs-connection) .djs-visual > :nth-child(1) {
  fill: green !important; /* color elements as green */
}
:deep(.highlight.djs-shape .djs-visual > :nth-child(1)) {
  fill: green !important;
  stroke: green !important;
  fill-opacity: 0.2 !important;
}
:deep(.highlight.djs-shape .djs-visual > :nth-child(2)) {
  fill: green !important;
}
:deep(.highlight.djs-shape .djs-visual > path) {
  fill: green !important;
  fill-opacity: 0.2 !important;
  stroke: green !important;
}
:deep(.highlight.djs-connection > .djs-visual > path) {
  stroke: green !important;
}
.djs-element.highlight > .djs-visual > path {
  stroke: green !important;
}
/** 不通过 */
.highlight-reject.djs-shape .djs-visual > :nth-child(1) {
  fill: red !important;
  stroke: red !important;
  fill-opacity: 0.2 !important;
}
.highlight-reject.djs-shape .djs-visual > :nth-child(2) {
  fill: red !important;
}
.highlight-reject.djs-shape .djs-visual > path {
  fill: red !important;
  fill-opacity: 0.2 !important;
  stroke: red !important;
}
.highlight-reject.djs-connection > .djs-visual > path {
  stroke: red !important;
  marker-end: url(#sequenceflow-end-white-success) !important;
}
.highlight-reject:not(.djs-connection) .djs-visual > :nth-child(1) {
  fill: red !important; /* color elements as green */
}
:deep(.highlight-reject.djs-shape .djs-visual > :nth-child(1)) {
  fill: red !important;
  stroke: red !important;
  fill-opacity: 0.2 !important;
}
:deep(.highlight-reject.djs-shape .djs-visual > :nth-child(2)) {
  fill: red !important;
}
:deep(.highlight-reject.djs-shape .djs-visual > path) {
  fill: red !important;
  fill-opacity: 0.2 !important;
  stroke: red !important;
}
:deep(.highlight-reject.djs-connection > .djs-visual > path) {
  stroke: red !important;
}
/** 已取消 */
.highlight-cancel.djs-shape .djs-visual > :nth-child(1) {
  fill: grey !important;
  stroke: grey !important;
  fill-opacity: 0.2 !important;
}
.highlight-cancel.djs-shape .djs-visual > :nth-child(2) {
  fill: grey !important;
}
.highlight-cancel.djs-shape .djs-visual > path {
  fill: grey !important;
  fill-opacity: 0.2 !important;
  stroke: grey !important;
}
.highlight-cancel.djs-connection > .djs-visual > path {
  stroke: grey !important;
}
.highlight-cancel:not(.djs-connection) .djs-visual > :nth-child(1) {
  fill: grey !important; /* color elements as green */
}
:deep(.highlight-cancel.djs-shape .djs-visual > :nth-child(1)) {
  fill: grey !important;
  stroke: grey !important;
  fill-opacity: 0.2 !important;
}
:deep(.highlight-cancel.djs-shape .djs-visual > :nth-child(2)) {
  fill: grey !important;
}
:deep(.highlight-cancel.djs-shape .djs-visual > path) {
  fill: grey !important;
  fill-opacity: 0.2 !important;
  stroke: grey !important;
}
:deep(.highlight-cancel.djs-connection > .djs-visual > path) {
  stroke: grey !important;
}
/** 回退 */
.highlight-return.djs-shape .djs-visual > :nth-child(1) {
  fill: #e6a23c !important;
  stroke: #e6a23c !important;
  fill-opacity: 0.2 !important;
}
.highlight-return.djs-shape .djs-visual > :nth-child(2) {
  fill: #e6a23c !important;
}
.highlight-return.djs-shape .djs-visual > path {
  fill: #e6a23c !important;
  fill-opacity: 0.2 !important;
  stroke: #e6a23c !important;
}
.highlight-return.djs-connection > .djs-visual > path {
  stroke: #e6a23c !important;
}
.highlight-return:not(.djs-connection) .djs-visual > :nth-child(1) {
  fill: #e6a23c !important; /* color elements as green */
}
:deep(.highlight-return.djs-shape .djs-visual > :nth-child(1)) {
  fill: #e6a23c !important;
  stroke: #e6a23c !important;
  fill-opacity: 0.2 !important;
}
:deep(.highlight-return.djs-shape .djs-visual > :nth-child(2)) {
  fill: #e6a23c !important;
}
:deep(.highlight-return.djs-shape .djs-visual > path) {
  fill: #e6a23c !important;
  fill-opacity: 0.2 !important;
  stroke: #e6a23c !important;
}
:deep(.highlight-return.djs-connection > .djs-visual > path) {
  stroke: #e6a23c !important;
}
.element-overlays {
  width: 200px;
  padding: 8px;
  color: #fafafa;
  background: rgb(0 0 0 / 60%);
  border-radius: 4px;
  box-sizing: border-box;
}
</style>
src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json
@@ -1211,6 +1211,76 @@
          "isAttr": true
        }
      ]
    },
    {
      "name": "AssignStartUserHandlerType",
      "superClass": ["Element"],
      "meta": {
        "allowedIn": ["bpmn:StartEvent", "bpmn:UserTask"]
      },
      "properties": [
      {
        "name": "value",
        "type": "Integer",
        "isBody": true
      }
      ]
    },
    {
      "name": "RejectHandlerType",
      "superClass": ["Element"],
      "meta": {
        "allowedIn": ["bpmn:UserTask"]
      },
      "properties": [
        {
          "name": "value",
          "type": "Integer",
          "isBody": true
        }
      ]
    },
    {
      "name": "RejectReturnTaskId",
      "superClass": ["Element"],
      "meta": {
        "allowedIn": ["bpmn:UserTask"]
      },
      "properties": [
        {
          "name": "value",
          "type": "String",
          "isBody": true
        }
      ]
    },
    {
      "name": "AssignEmptyHandlerType",
      "superClass": ["Element"],
      "meta": {
        "allowedIn": ["bpmn:UserTask"]
      },
      "properties": [
        {
          "name": "value",
          "type": "Integer",
          "isBody": true
        }
      ]
    },
    {
      "name": "AssignEmptyUserIds",
      "superClass": ["Element"],
      "meta": {
        "allowedIn": ["bpmn:UserTask"]
      },
      "properties": [
        {
          "name": "value",
          "type": "String",
          "isBody": true
        }
      ]
    }
  ],
  "emumerations": []
src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue
@@ -1,5 +1,5 @@
<template>
  <div class="process-panel__container" :style="{ width: `${width}px` }">
  <div class="process-panel__container" :style="{ width: `${width}px`,maxHeight: '700px' }">
    <el-collapse v-model="activeTab">
      <el-collapse-item name="base">
        <!-- class="panel-tab__title" -->
@@ -54,6 +54,10 @@
        <template #title><Icon icon="ep:promotion" />其他</template>
        <element-other-config :id="elementId" />
      </el-collapse-item>
      <el-collapse-item name="customConfig" v-if="elementType.indexOf('Task') !== -1" key="customConfig">
        <template #title><Icon icon="ep:circle-plus-filled" />自定义配置</template>
        <element-custom-config :id="elementId" :type="elementType" />
      </el-collapse-item>
    </el-collapse>
  </div>
</template>
src/components/bpmnProcessDesigner/package/penal/custom-config/ElementCustomConfig.vue
对比新文件
@@ -0,0 +1,283 @@
<!-- UserTask 自定义配置:
     1. 审批人与提交人为同一人时
     2. 审批人拒绝时
     3. 审批人为空时
-->
<template>
  <div class="panel-tab__content">
    <el-divider content-position="left">审批人拒绝时</el-divider>
    <el-form-item prop="rejectHandlerType">
      <el-radio-group
        v-model="rejectHandlerType"
        :disabled="returnTaskList.length === 0"
        @change="updateRejectHandlerType"
      >
        <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="rejectHandlerType == RejectHandlerType.RETURN_USER_TASK"
      label="驳回节点"
      prop="returnNodeId"
    >
      <el-select v-model="returnNodeId" clearable style="width: 100%" @change="updateReturnNodeId">
        <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 prop="assignEmptyHandlerType">
      <el-radio-group v-model="assignEmptyHandlerType" @change="updateAssignEmptyHandlerType">
        <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="assignEmptyHandlerType == AssignEmptyHandlerType.ASSIGN_USER"
      label="指定用户"
      prop="assignEmptyHandlerUserIds"
      span="24"
    >
      <el-select
        v-model="assignEmptyUserIds"
        clearable
        multiple
        style="width: 100%"
        @change="updateAssignEmptyUserIds"
      >
        <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-radio-group v-model="assignStartUserHandlerType" @change="updateAssignStartUserHandlerType">
      <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>
  </div>
</template>
<script lang="ts" setup>
import {
  ASSIGN_START_USER_HANDLER_TYPES,
  RejectHandlerType,
  REJECT_HANDLER_TYPES,
  ASSIGN_EMPTY_HANDLER_TYPES,
  AssignEmptyHandlerType
} from '@/components/SimpleProcessDesignerV2/src/consts'
import * as UserApi from '@/api/system/user'
defineOptions({ name: 'ElementCustomConfig' })
const props = defineProps({
  id: String,
  type: String
})
const prefix = inject('prefix')
// 审批人与提交人为同一人时
const assignStartUserHandlerTypeEl = ref()
const assignStartUserHandlerType = ref()
// 审批人拒绝时
const rejectHandlerTypeEl = ref()
const rejectHandlerType = ref()
const returnNodeIdEl = ref()
const returnNodeId = ref()
const returnTaskList = ref([])
// 审批人为空时
const assignEmptyHandlerTypeEl = ref()
const assignEmptyHandlerType = ref()
const assignEmptyUserIdsEl = ref()
const assignEmptyUserIds = ref()
const elExtensionElements = ref()
const otherExtensions = ref()
const bpmnElement = ref()
const bpmnInstances = () => (window as any)?.bpmnInstances
const resetCustomConfigList = () => {
  bpmnElement.value = bpmnInstances().bpmnElement
  // 获取可回退的列表
  returnTaskList.value = findAllPredecessorsExcludingStart(
    bpmnElement.value.id,
    bpmnInstances().modeler
  )
  // 获取元素扩展属性 或者 创建扩展属性
  elExtensionElements.value =
    bpmnElement.value.businessObject?.extensionElements ??
    bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] })
  // 审批人与提交人为同一人时
  assignStartUserHandlerTypeEl.value =
    elExtensionElements.value.values?.filter(
      (ex) => ex.$type === `${prefix}:AssignStartUserHandlerType`
    )?.[0] || bpmnInstances().moddle.create(`${prefix}:AssignStartUserHandlerType`, { value: 1 })
  assignStartUserHandlerType.value = assignStartUserHandlerTypeEl.value.value
  // 审批人拒绝时
  rejectHandlerTypeEl.value =
    elExtensionElements.value.values?.filter(
      (ex) => ex.$type === `${prefix}:RejectHandlerType`
    )?.[0] || bpmnInstances().moddle.create(`${prefix}:RejectHandlerType`, { value: 1 })
  rejectHandlerType.value = rejectHandlerTypeEl.value.value
  returnNodeIdEl.value =
    elExtensionElements.value.values?.filter(
      (ex) => ex.$type === `${prefix}:RejectReturnTaskId`
    )?.[0] || bpmnInstances().moddle.create(`${prefix}:RejectReturnTaskId`, { value: '' })
  returnNodeId.value = returnNodeIdEl.value.value
  // 审批人为空时
  assignEmptyHandlerTypeEl.value =
    elExtensionElements.value.values?.filter(
      (ex) => ex.$type === `${prefix}:AssignEmptyHandlerType`
    )?.[0] || bpmnInstances().moddle.create(`${prefix}:AssignEmptyHandlerType`, { value: 1 })
  assignEmptyHandlerType.value = assignEmptyHandlerTypeEl.value.value
  assignEmptyUserIdsEl.value =
    elExtensionElements.value.values?.filter(
      (ex) => ex.$type === `${prefix}:AssignEmptyUserIds`
    )?.[0] || bpmnInstances().moddle.create(`${prefix}:AssignEmptyUserIds`, { value: '' })
  assignEmptyUserIds.value = assignEmptyUserIdsEl.value.value.split(',').map((item) => {
    // 如果数字超出了最大安全整数范围,则将其作为字符串处理
    let num = Number(item)
    return num > Number.MAX_SAFE_INTEGER || num < -Number.MAX_SAFE_INTEGER ? item : num
  })
  // 保留剩余扩展元素,便于后面更新该元素对应属性
  otherExtensions.value =
    elExtensionElements.value.values?.filter(
      (ex) =>
        ex.$type !== `${prefix}:AssignStartUserHandlerType` &&
        ex.$type !== `${prefix}:RejectHandlerType` &&
        ex.$type !== `${prefix}:RejectReturnTaskId` &&
        ex.$type !== `${prefix}:AssignEmptyHandlerType` &&
        ex.$type !== `${prefix}:AssignEmptyUserIds`
    ) ?? []
  // 更新元素扩展属性,避免后续报错
  updateElementExtensions()
}
const updateAssignStartUserHandlerType = () => {
  assignStartUserHandlerTypeEl.value.value = assignStartUserHandlerType.value
  updateElementExtensions()
}
const updateRejectHandlerType = () => {
  rejectHandlerTypeEl.value.value = rejectHandlerType.value
  returnNodeId.value = returnTaskList.value[0].id
  returnNodeIdEl.value.value = returnNodeId.value
  updateElementExtensions()
}
const updateReturnNodeId = () => {
  returnNodeIdEl.value.value = returnNodeId.value
  updateElementExtensions()
}
const updateAssignEmptyHandlerType = () => {
  assignEmptyHandlerTypeEl.value.value = assignEmptyHandlerType.value
  updateElementExtensions()
}
const updateAssignEmptyUserIds = () => {
  assignEmptyUserIdsEl.value.value = assignEmptyUserIds.value.toString()
  updateElementExtensions()
}
const updateElementExtensions = () => {
  const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
    values: [
      ...otherExtensions.value,
      assignStartUserHandlerTypeEl.value,
      rejectHandlerTypeEl.value,
      returnNodeIdEl.value,
      assignEmptyHandlerTypeEl.value,
      assignEmptyUserIdsEl.value
    ]
  })
  bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
    extensionElements: extensions
  })
}
watch(
  () => props.id,
  (val) => {
    val &&
      val.length &&
      nextTick(() => {
        resetCustomConfigList()
      })
  },
  { immediate: true }
)
function findAllPredecessorsExcludingStart(elementId, modeler) {
  const elementRegistry = modeler.get('elementRegistry')
  const allConnections = elementRegistry.filter((element) => element.type === 'bpmn:SequenceFlow')
  const predecessors = new Set() // 使用 Set 来避免重复节点
  // 检查是否是开始事件节点
  function isStartEvent(element) {
    return element.type === 'bpmn:StartEvent'
  }
  function findPredecessorsRecursively(element) {
    // 获取与当前节点相连的所有连接
    const incomingConnections = allConnections.filter((connection) => connection.target === element)
    incomingConnections.forEach((connection) => {
      const source = connection.source // 获取前置节点
      // 只添加不是开始事件的前置节点
      if (!isStartEvent(source)) {
        predecessors.add(source.businessObject)
        // 递归查找前置节点
        findPredecessorsRecursively(source)
      }
    })
  }
  const targetElement = elementRegistry.get(elementId)
  if (targetElement) {
    findPredecessorsRecursively(targetElement)
  }
  return Array.from(predecessors) // 返回前置节点数组
}
const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
onMounted(async () => {
  // 获得用户列表
  userOptions.value = await UserApi.getSimpleUserList()
})
</script>
src/components/bpmnProcessDesigner/package/penal/form/ElementForm.vue
@@ -268,9 +268,9 @@
const resetFormList = () => {
  bpmnELement.value = bpmnInstances().bpmnElement
  formKey.value = bpmnELement.value.businessObject.formKey
  if (formKey.value?.length > 0) {
    formKey.value = parseInt(formKey.value)
  }
  // if (formKey.value?.length > 0) {
  //   formKey.value = parseInt(formKey.value)
  // }
  // 获取元素扩展属性 或者 创建扩展属性
  elExtensionElements.value =
    bpmnELement.value.businessObject.get('extensionElements') ||
src/components/bpmnProcessDesigner/package/penal/listeners/ElementListeners.vue
@@ -370,7 +370,7 @@
}
// 移除监听器
const removeListener = (index) => {
  // debugger
  debugger
  ElMessageBox.confirm('确认移除该监听器吗?', '提示', {
    confirmButtonText: '确 认',
    cancelButtonText: '取 消'
src/components/bpmnProcessDesigner/package/penal/multi-instance/ElementMultiInstance.vue
@@ -45,17 +45,20 @@
          <el-checkbox
            v-model="loopInstanceForm.asyncBefore"
            label="异步前"
            value="异步前"
            @change="updateLoopAsync('asyncBefore')"
          />
          <el-checkbox
            v-model="loopInstanceForm.asyncAfter"
            label="异步后"
            value="异步后"
            @change="updateLoopAsync('asyncAfter')"
          />
          <el-checkbox
            v-model="loopInstanceForm.exclusive"
            v-if="loopInstanceForm.asyncAfter || loopInstanceForm.asyncBefore"
            label="排除"
            value="排除"
            @change="updateLoopAsync('exclusive')"
          />
        </el-form-item>
src/components/bpmnProcessDesigner/package/penal/properties/ElementProperties.vue
@@ -80,7 +80,7 @@
  otherExtensionList.value = [] // 其他扩展配置
  bpmnElementProperties.value =
    // bpmnElement.value.businessObject?.extensionElements?.filter((ex) => {
    bpmnElement.value.businessObject?.extensionElements?.values.filter((ex) => {
    bpmnElement.value.businessObject?.extensionElements?.values?.filter((ex) => {
      if (ex.$type !== `${prefix}:Properties`) {
        otherExtensionList.value.push(ex)
      }
src/components/bpmnProcessDesigner/package/penal/task/ElementTask.vue
@@ -6,13 +6,20 @@
        <el-checkbox
          v-model="taskConfigForm.asyncBefore"
          label="异步前"
          value="异步前"
          @change="changeTaskAsync"
        />
        <el-checkbox v-model="taskConfigForm.asyncAfter" label="异步后" @change="changeTaskAsync" />
        <el-checkbox
          v-model="taskConfigForm.asyncAfter"
          label="异步后"
          value="异步后"
          @change="changeTaskAsync"
        />
        <el-checkbox
          v-model="taskConfigForm.exclusive"
          v-if="taskConfigForm.asyncAfter || taskConfigForm.asyncBefore"
          label="排除"
          value="排除"
          @change="changeTaskAsync"
        />
      </el-form-item>
src/components/bpmnProcessDesigner/package/theme/element-variables.scss
@@ -5,7 +5,7 @@
/* 改变 icon 字体路径变量,必需 */
$--font-path: '~element-ui/lib/theme-chalk/fonts';
@import '~element-ui/packages/theme-chalk/src/index';
@use '~element-ui/packages/theme-chalk/src/index';
.el-table td,
.el-table th {
src/components/bpmnProcessDesigner/package/theme/index.scss
@@ -1,2 +1,117 @@
@import './process-designer.scss';
@import './process-panel.scss';
@use './process-designer.scss';
@use './process-panel.scss';
$success-color: #4eb819;
$primary-color: #409EFF;
$danger-color: #F56C6C;
$cancel-color: #909399;
.process-viewer {
  position: relative;
  border: 1px solid #EFEFEF;
  background: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PHBhdHRlcm4gaWQ9ImEiIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHBhdGggZD0iTTAgMTBoNDBNMTAgMHY0ME0wIDIwaDQwTTIwIDB2NDBNMCAzMGg0ME0zMCAwdjQwIiBmaWxsPSJub25lIiBzdHJva2U9IiNlMGUwZTAiIG9wYWNpdHk9Ii4yIi8+PHBhdGggZD0iTTQwIDBIMHY0MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZTBlMGUwIi8+PC9wYXR0ZXJuPjwvZGVmcz48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSJ1cmwoI2EpIi8+PC9zdmc+') repeat!important;
  .success-arrow {
    fill: $success-color;
    stroke: $success-color;
  }
  .success-conditional {
    fill: white;
    stroke: $success-color;
  }
  .success.djs-connection {
    .djs-visual path {
      stroke: $success-color!important;
      //marker-end: url(#sequenceflow-end-white-success)!important;
    }
  }
  .success.djs-connection.condition-expression {
    .djs-visual path {
      //marker-start: url(#conditional-flow-marker-white-success)!important;
    }
  }
  .success.djs-shape {
    .djs-visual rect {
      stroke: $success-color!important;
      fill: $success-color!important;
      fill-opacity: 0.15!important;
    }
    .djs-visual polygon {
      stroke: $success-color!important;
    }
    .djs-visual path:nth-child(2) {
      stroke: $success-color!important;
      fill: $success-color!important;
    }
    .djs-visual circle {
      stroke: $success-color!important;
      fill: $success-color!important;
      fill-opacity: 0.15!important;
    }
  }
  .primary.djs-shape {
    .djs-visual rect {
      stroke: $primary-color!important;
      fill: $primary-color!important;
      fill-opacity: 0.15!important;
    }
    .djs-visual polygon {
      stroke: $primary-color!important;
    }
    .djs-visual circle {
      stroke: $primary-color!important;
      fill: $primary-color!important;
      fill-opacity: 0.15!important;
    }
  }
  .danger.djs-shape {
    .djs-visual rect {
      stroke: $danger-color!important;
      fill: $danger-color!important;
      fill-opacity: 0.15!important;
    }
    .djs-visual polygon {
      stroke: $danger-color!important;
    }
    .djs-visual circle {
      stroke: $danger-color!important;
      fill: $danger-color!important;
      fill-opacity: 0.15!important;
    }
  }
  .cancel.djs-shape {
    .djs-visual rect {
      stroke: $cancel-color!important;
      fill: $cancel-color!important;
      fill-opacity: 0.15!important;
    }
    .djs-visual polygon {
      stroke: $cancel-color!important;
    }
    .djs-visual circle {
      stroke: $cancel-color!important;
      fill: $cancel-color!important;
      fill-opacity: 0.15!important;
    }
  }
}
.process-viewer .djs-tooltip-container, .process-viewer .djs-overlay-container, .process-viewer .djs-palette {
  display: none;
}
src/components/bpmnProcessDesigner/package/theme/process-designer.scss
@@ -1,6 +1,6 @@
@import 'bpmn-js-token-simulation/assets/css/bpmn-js-token-simulation.css';
@import 'bpmn-js-token-simulation/assets/css/font-awesome.min.css';
@import 'bpmn-js-token-simulation/assets/css/normalize.css';
@use 'bpmn-js-token-simulation/assets/css/bpmn-js-token-simulation.css';
@use 'bpmn-js-token-simulation/assets/css/font-awesome.min.css';
@use 'bpmn-js-token-simulation/assets/css/normalize.css';
// 边框被 token-simulation 样式覆盖了
.djs-palette {
src/components/bpmnProcessDesigner/package/utils.ts
@@ -2,7 +2,7 @@
const bpmnInstances = () => (window as any)?.bpmnInstances
// 创建监听器实例
export function createListenerObject(options, isTask, prefix) {
  // debugger
  debugger
  const listenerObj = Object.create(null)
  listenerObj.event = options.event
  isTask && (listenerObj.id = options.id) // 任务监听器特有的 id 字段
src/layout/components/AppView.vue
@@ -36,27 +36,10 @@
<template>
  <section
    :class="[
      'p-[var(--app-content-padding)] w-[calc(100%-var(--app-content-padding)-var(--app-content-padding))] bg-[var(--app-content-bg-color)] dark:bg-[var(--el-bg-color)]',
      'p-[var(--app-content-padding)] w-full bg-[var(--app-content-bg-color)] dark:bg-[var(--el-bg-color)]',
      {
        '!min-h-[calc(100%-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height))]':
          (fixedHeader &&
            (layout === 'classic' || layout === 'topLeft' || layout === 'top') &&
            footer) ||
          (!tagsView && layout === 'top' && footer),
        '!min-h-[calc(100%-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height)-var(--tags-view-height))]':
          tagsView && layout === 'top' && footer,
        '!min-h-[calc(100%-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-var(--top-tool-height)-var(--app-footer-height))]':
          !fixedHeader && layout === 'classic' && footer,
        '!min-h-[calc(100%-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height))]':
          !fixedHeader && layout === 'topLeft' && footer,
        '!min-h-[calc(100%-var(--top-tool-height)-var(--app-content-padding)-var(--app-content-padding))]':
          fixedHeader && layout === 'cutMenu' && footer,
        '!min-h-[calc(100%-var(--top-tool-height)-var(--app-content-padding)-var(--app-content-padding)-var(--tags-view-height))]':
          !fixedHeader && layout === 'cutMenu' && footer
        '!min-h-[calc(100vh-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))] pb-0':
          footer
      }
    ]"
  >
src/layout/components/Breadcrumb/src/Breadcrumb.vue
@@ -92,7 +92,7 @@
$prefix-cls: #{$elNamespace}-breadcrumb;
.#{$prefix-cls} {
  :deep(&__item) {
  :deep(.#{$prefix-cls}__item) {
    display: flex;
    .#{$prefix-cls}__inner {
      display: flex;
@@ -105,7 +105,7 @@
    }
  }
  :deep(&__item):not(:last-child) {
  :deep(.#{$prefix-cls}__item):not(:last-child) {
    .#{$prefix-cls}__inner {
      color: var(--top-header-text-color);
@@ -115,7 +115,7 @@
    }
  }
  :deep(&__item):last-child {
  :deep(.#{$prefix-cls}__item):last-child {
    .#{$prefix-cls}__inner {
      display: flex;
      align-items: center;
src/layout/components/Footer/src/Footer.vue
@@ -17,8 +17,8 @@
<template>
  <div
    :class="prefixCls"
    class="h-[var(--app-footer-height)] bg-[var(--app-content-bg-color)] text-center leading-[var(--app-footer-height)] text-[var(--el-text-color-placeholder)] dark:bg-[var(--el-bg-color)]"
    class="h-[var(--app-footer-height)] bg-[var(--app-content-bg-color)] text-center leading-[var(--app-footer-height)] text-[var(--el-text-color-placeholder)] dark:bg-[var(--el-bg-color)] overflow-hidden"
  >
    <span class="text-14px">Copyright ©2024-{{ title }}</span>
    <span class="text-14px">Copyright ©2022-{{ title }}</span>
  </div>
</template>
src/layout/components/Menu/src/Menu.vue
@@ -90,6 +90,11 @@
          backgroundColor="var(--left-menu-bg-color)"
          textColor="var(--left-menu-text-color)"
          activeTextColor="var(--left-menu-text-active-color)"
          popperClass={
            unref(menuMode) === 'vertical'
              ? `${prefixCls}-popper--vertical`
              : `${prefixCls}-popper--horizontal`
          }
          onSelect={menuSelect}
        >
          {{
@@ -190,6 +195,16 @@
    }
  }
  // 垂直菜单
  &__vertical {
    :deep(.#{$elNamespace}-menu--vertical) {
      &:not(.#{$elNamespace}-menu--collapse) .#{$elNamespace}-sub-menu__title,
      .#{$elNamespace}-menu-item {
        padding-right: 0;
      }
    }
  }
  // 水平菜单
  &__horizontal {
    height: calc(var(--top-tool-height)) !important;
src/layout/components/Message/src/Message.vue
@@ -1,10 +1,12 @@
<script lang="ts" setup>
import { formatDate } from '@/utils/formatTime'
import * as NotifyMessageApi from '@/api/system/notify/message'
import { useUserStoreWithOut } from '@/store/modules/user'
defineOptions({ name: 'Message' })
const { push } = useRouter()
const userStore = useUserStoreWithOut()
const activeName = ref('notice')
const unreadCount = ref(0) // 未读消息数量
const list = ref<any[]>([]) // 消息列表
@@ -37,7 +39,11 @@
  // 轮询刷新小红点
  setInterval(
    () => {
      if (userStore.getIsSetUser) {
      getUnreadCount()
      } else {
        unreadCount.value = 0
      }
    },
    1000 * 60 * 2
  )
src/layout/components/Setting/src/Setting.vue
@@ -126,8 +126,10 @@
      message: ${appStore.getMessage},
      // 标签页
      tagsView: ${appStore.getTagsView},
      // 标签页
      tagsViewImmerse: ${appStore.getTagsViewImmerse},
      // 标签页图标
      getTagsViewIcon: ${appStore.getTagsViewIcon},
      tagsViewIcon: ${appStore.getTagsViewIcon},
      // logo
      logo: ${appStore.getLogo},
      // 菜单手风琴
@@ -295,5 +297,6 @@
.#{$prefix-cls} {
  border-radius: 6px 0 0 6px;
  z-index: 1200;/*修正没有z-index会被表格层覆盖,值不要超过4000*/
}
</style>
src/layout/components/TabMenu/src/TabMenu.vue
@@ -139,7 +139,7 @@
        id={`${variables.namespace}-menu`}
        class={[
          prefixCls,
          'relative bg-[var(--left-menu-bg-color)] top-1px layout-border__right',
          'relative bg-[var(--left-menu-bg-color)] layout-border__right',
          {
            'w-[var(--tab-menu-max-width)]': !unref(collapse),
            'w-[var(--tab-menu-min-width)]': unref(collapse)
@@ -147,7 +147,7 @@
        ]}
        onMouseleave={mouseleave}
      >
        <ElScrollbar class="!h-[calc(100%-var(--tab-menu-collapse-height)-1px)]">
        <ElScrollbar class="!h-[calc(100%-var(--tab-menu-collapse-height))]">
          <div>
            {() => {
              return unref(tabRouters).map((v) => {
@@ -199,7 +199,7 @@
            {
              '!left-[var(--tab-menu-min-width)]': unref(collapse),
              '!left-[var(--tab-menu-max-width)]': !unref(collapse),
              '!w-[calc(var(--left-menu-max-width)+1px)]': unref(showMenu) || unref(fixedMenu),
              '!w-[var(--left-menu-max-width)]': unref(showMenu) || unref(fixedMenu),
              '!w-0': !unref(showMenu) && !unref(fixedMenu)
            }
          ]}
src/layout/components/TagsView/src/TagsView.vue
@@ -1,7 +1,7 @@
<script lang="ts" setup>
import { onMounted, watch, computed, unref, ref, nextTick } from 'vue'
import { useRouter } from 'vue-router'
import { computed, nextTick, onMounted, ref, unref, watch } from 'vue'
import type { RouteLocationNormalizedLoaded, RouterLinkProps } from 'vue-router'
import { useRouter } from 'vue-router'
import { usePermissionStore } from '@/store/modules/permission'
import { useTagsViewStore } from '@/store/modules/tagsView'
import { useAppStore } from '@/store/modules/app'
@@ -32,6 +32,8 @@
const affixTagArr = ref<RouteLocationNormalizedLoaded[]>([])
const appStore = useAppStore()
const tagsViewImmerse = computed(() => appStore.getTagsViewImmerse)
const tagsViewIcon = computed(() => appStore.getTagsViewIcon)
@@ -125,12 +127,8 @@
const moveToCurrentTag = async () => {
  await nextTick()
  for (const v of unref(visitedViews)) {
    if (v.fullPath === unref(currentRoute).path) {
    if (v.fullPath === unref(currentRoute).fullPath) {
      moveToTarget(v)
      if (v.fullPath !== unref(currentRoute).fullPath) {
        tagsViewStore.updateVisitedView(unref(currentRoute))
      }
      break
    }
  }
@@ -205,7 +203,7 @@
// 是否是当前tag
const isActive = (route: RouteLocationNormalizedLoaded): boolean => {
  return route.path === unref(currentRoute).path
  return route.fullPath === unref(currentRoute).fullPath
}
// 所有右键菜单组件的元素
@@ -266,21 +264,33 @@
    class="relative w-full flex bg-[#fff] dark:bg-[var(--el-bg-color)]"
  >
    <span
      :class="`${prefixCls}__tool ${prefixCls}__tool--first`"
      :class="tagsViewImmerse ? '' : `${prefixCls}__tool ${prefixCls}__tool--first`"
      class="h-[var(--tags-view-height)] w-[var(--tags-view-height)] flex cursor-pointer items-center justify-center"
      @click="move(-200)"
    >
      <Icon
        icon="ep:d-arrow-left"
        color="var(--el-text-color-placeholder)"
        :hover-color="isDark ? '#fff' : 'var(--el-color-black)'"
        color="var(--el-text-color-placeholder)"
        icon="ep:d-arrow-left"
      />
    </span>
    <div class="flex-1 overflow-hidden">
      <ElScrollbar ref="scrollbarRef" class="h-full" @scroll="scroll">
        <div class="h-full flex">
        <div class="h-[var(--tags-view-height)] flex">
          <ContextMenu
            v-for="item in visitedViews"
            :key="item.fullPath"
            :ref="itemRefs.set"
            :class="[
              `${prefixCls}__item`,
              tagsViewImmerse ? `${prefixCls}__item--immerse` : '',
              tagsViewIcon ? `${prefixCls}__item--icon` : '',
              tagsViewImmerse && tagsViewIcon ? `${prefixCls}__item--immerse--icon` : '',
              item?.meta?.affix ? `${prefixCls}__item--affix` : '',
              {
                'is-active': isActive(item)
              }
            ]"
            :schema="[
              {
                icon: 'ep:refresh',
@@ -338,41 +348,36 @@
                }
              }
            ]"
            v-for="item in visitedViews"
            :key="item.fullPath"
            :tag-item="item"
            :class="[
              `${prefixCls}__item`,
              item?.meta?.affix ? `${prefixCls}__item--affix` : '',
              {
                'is-active': isActive(item)
              }
            ]"
            @visible-change="visibleChange"
          >
            <div>
              <router-link :ref="tagLinksRefs.set" :to="{ ...item }" custom v-slot="{ navigate }">
              <router-link :ref="tagLinksRefs.set" v-slot="{ navigate }" :to="{ ...item }" custom>
                <div
                  :class="`h-full flex items-center justify-center whitespace-nowrap pl-15px ${prefixCls}__item--label`"
                  @click="navigate"
                  class="h-full flex items-center justify-center whitespace-nowrap pl-15px"
                >
                  <Icon
                    v-if="
                      item?.matched &&
                      item?.matched[1] &&
                      item?.matched[1]?.meta?.icon &&
                      tagsViewIcon
                      tagsViewIcon &&
                      (item?.meta?.icon ||
                        (item?.matched &&
                          item.matched[0] &&
                          item.matched[item.matched.length - 1].meta?.icon))
                    "
                    :icon="item?.matched[1]?.meta?.icon"
                    :icon="item?.meta?.icon || item.matched[item.matched.length - 1].meta.icon"
                    :size="12"
                    class="mr-5px"
                  />
                  {{ t(item?.meta?.title as string) }}
                  {{
                    t(item?.meta?.title as string) +
                    (item?.meta?.titleSuffix ? ` (${item?.meta?.titleSuffix})` : '')
                  }}
                  <Icon
                    :class="`${prefixCls}__item--close`"
                    :size="12"
                    color="#333"
                    icon="ep:close"
                    :size="12"
                    @click.prevent.stop="closeSelectedTag(item)"
                  />
                </div>
@@ -383,29 +388,28 @@
      </ElScrollbar>
    </div>
    <span
      :class="`${prefixCls}__tool`"
      :class="tagsViewImmerse ? '' : `${prefixCls}__tool`"
      class="h-[var(--tags-view-height)] w-[var(--tags-view-height)] flex cursor-pointer items-center justify-center"
      @click="move(200)"
    >
      <Icon
        icon="ep:d-arrow-right"
        color="var(--el-text-color-placeholder)"
        :hover-color="isDark ? '#fff' : 'var(--el-color-black)'"
        color="var(--el-text-color-placeholder)"
        icon="ep:d-arrow-right"
      />
    </span>
    <span
      :class="`${prefixCls}__tool`"
      :class="tagsViewImmerse ? '' : `${prefixCls}__tool`"
      class="h-[var(--tags-view-height)] w-[var(--tags-view-height)] flex cursor-pointer items-center justify-center"
      @click="refreshSelectedTag(selectedTag)"
    >
      <Icon
        icon="ep:refresh-right"
        color="var(--el-text-color-placeholder)"
        :hover-color="isDark ? '#fff' : 'var(--el-color-black)'"
        color="var(--el-text-color-placeholder)"
        icon="ep:refresh-right"
      />
    </span>
    <ContextMenu
      trigger="click"
      :schema="[
        {
          icon: 'ep:refresh',
@@ -457,15 +461,16 @@
          }
        }
      ]"
      trigger="click"
    >
      <span
        :class="`${prefixCls}__tool`"
        :class="tagsViewImmerse ? '' : `${prefixCls}__tool`"
        class="block h-[var(--tags-view-height)] w-[var(--tags-view-height)] flex cursor-pointer items-center justify-center"
      >
        <Icon
          icon="ep:menu"
          color="var(--el-text-color-placeholder)"
          :hover-color="isDark ? '#fff' : 'var(--el-color-black)'"
          color="var(--el-text-color-placeholder)"
          icon="ep:menu"
        />
      </span>
    </ContextMenu>
@@ -485,10 +490,10 @@
    &::before {
      position: absolute;
      top: 1px;
      top: 0;
      left: 0;
      width: 100%;
      height: calc(100% - 1px);
      height: 100%;
      border-left: 1px solid var(--el-border-color);
      content: '';
    }
@@ -496,10 +501,10 @@
    &--first {
      &::before {
        position: absolute;
        top: 1px;
        top: 0;
        left: 0;
        width: 100%;
        height: calc(100% - 1px);
        height: 100%;
        border-right: 1px solid var(--el-border-color);
        border-left: none;
        content: '';
@@ -509,14 +514,15 @@
  &__item {
    position: relative;
    top: 2px;
    top: 3px;
    height: calc(100% - 6px);
    padding-right: 25px;
    padding-right: 15px;
    margin-left: 4px;
    font-size: 12px;
    cursor: pointer;
    border: 1px solid #d9d9d9;
    border-radius: 2px;
    box-sizing: border-box;
    &--close {
      position: absolute;
@@ -525,11 +531,16 @@
      display: none;
      transform: translate(0, -50%);
    }
    &:not(.#{$prefix-cls}__item--affix):hover {
      .#{$prefix-cls}__item--close {
        display: block;
      }
    }
  }
  &__item--icon {
    padding-right: 20px;
  }
  &__item:not(.is-active) {
@@ -542,9 +553,45 @@
    color: var(--el-color-white);
    background-color: var(--el-color-primary);
    border: 1px solid var(--el-color-primary);
    .#{$prefix-cls}__item--close {
      :deep(span) {
        color: var(--el-color-white) !important;
      }
    }
  }
  &__item--immerse {
    top: 2px;
    height: calc(100% - 3px);
    padding-right: 35px;
    margin: 0 -10px;
    border: none !important;
    -webkit-mask-box-image: url("data:image/svg+xml,%3Csvg width='68' height='34' viewBox='0 0 68 34' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='m27,0c-7.99582,0 -11.95105,0.00205 -12,12l0,6c0,8.284 -0.48549,16.49691 -8.76949,16.49691l54.37857,-0.11145c-8.284,0 -8.60908,-8.10146 -8.60908,-16.38546l0,-6c0.11145,-12.08445 -4.38441,-12 -12,-12l-13,0z' fill='%23409eff'/%3E%3C/svg%3E")
      12 27 15;
    .#{$prefix-cls}__item--label {
      padding-left: 35px;
    }
    .#{$prefix-cls}__item--close {
      right: 20px;
    }
  }
  &__item--immerse--icon {
    padding-right: 35px;
  }
  &__item--immerse:not(.is-active) {
    &:hover {
      color: var(--el-color-white);
      background-color: var(--el-color-primary);
      .#{$prefix-cls}__item--close {
        :deep(span) {
          color: var(--el-color-white) !important;
        }
      }
    }
  }
@@ -574,12 +621,19 @@
      color: var(--el-color-white);
      background-color: var(--el-color-primary);
      border: 1px solid var(--el-color-primary);
      .#{$prefix-cls}__item--close {
        :deep(span) {
          color: var(--el-color-white) !important;
        }
      }
    }
    &__item--immerse:not(.is-active) {
      &:hover {
        color: var(--el-color-white);
      }
    }
  }
}
</style>
src/layout/components/UserInfo/src/UserInfo.vue
@@ -23,7 +23,7 @@
const prefixCls = getPrefixCls('user-info')
const avatar = computed(() => userStore.user.avatar ?? avatarImg)
const avatar = computed(() => userStore.user.avatar || avatarImg)
const userName = computed(() => userStore.user.nickname ?? 'Admin')
// 锁定屏幕
@@ -50,7 +50,7 @@
  push('/user/profile')
}
const toDocument = () => {
  window.open('https://xxxx/')
  window.open('https://doc.iocoder.cn/')
}
</script>
src/layout/components/UserInfo/src/components/LockDialog.vue
@@ -21,7 +21,7 @@
})
const userStore = useUserStore()
const avatar = computed(() => userStore.user.avatar ?? avatarImg)
const avatar = computed(() => userStore.user.avatar || avatarImg)
const userName = computed(() => userStore.user.nickname ?? 'Admin')
const emit = defineEmits(['update:modelValue'])
src/layout/components/UserInfo/src/components/LockPage.vue
@@ -22,7 +22,7 @@
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('lock-page')
const avatar = computed(() => userStore.user.avatar ?? avatarImg)
const avatar = computed(() => userStore.user.avatar || avatarImg)
const userName = computed(() => userStore.user.nickname ?? 'Admin')
const lockStore = useLockStore()
src/layout/components/useRenderLayout.tsx
@@ -126,7 +126,7 @@
          <ToolHeader class="flex-1"></ToolHeader>
        </div>
        <div class="absolute left-0 top-[var(--logo-height)+1px] h-[calc(100%-1px-var(--logo-height))] w-full flex">
        <div class="absolute left-0 top-[var(--logo-height)] h-[calc(100%-var(--logo-height))] w-full flex">
          <Menu class="relative layout-border__right !h-full"></Menu>
          <div
            class={[
@@ -157,9 +157,9 @@
                    'layout-border__bottom absolute',
                    {
                      '!fixed top-0 left-0 z-10': fixedHeader.value,
                      'w-[calc(100%-var(--left-menu-min-width))] !left-[var(--left-menu-min-width)] mt-[calc(var(--logo-height)+1px)]':
                      'w-[calc(100%-var(--left-menu-min-width))] !left-[var(--left-menu-min-width)] mt-[var(--logo-height)]':
                        collapse.value && fixedHeader.value,
                      'w-[calc(100%-var(--left-menu-max-width))] !left-[var(--left-menu-max-width)] mt-[calc(var(--logo-height)+1px)]':
                      'w-[calc(100%-var(--left-menu-max-width))] !left-[var(--left-menu-max-width)] mt-[var(--logo-height)]':
                        !collapse.value && fixedHeader.value
                    }
                  ]}
@@ -190,24 +190,14 @@
          <Menu class="h-[var(--top-tool-height)] flex-1 px-10px"></Menu>
          <ToolHeader></ToolHeader>
        </div>
        <div
          class={[
            `${prefixCls}-content`,
            'w-full',
            {
              'h-[calc(100%-var(--app-footer-height))]': !fixedHeader.value,
              'h-[calc(100%-var(--tags-view-height)-var(--app-footer-height))]': fixedHeader.value
            }
          ]}
        >
        <div class={[`${prefixCls}-content`, 'w-full h-[calc(100%-var(--top-tool-height))]']}>
          <ElScrollbar
            v-loading={pageLoading.value}
            class={[
              `${prefixCls}-content-scrollbar`,
              {
                'mt-[var(--tags-view-height)] !pb-[calc(var(--tags-view-height)+var(--app-footer-height))]':
                  fixedHeader.value,
                'pb-[var(--app-footer-height)]': !fixedHeader.value
                '!h-[calc(100%-var(--tags-view-height))] mt-[calc(var(--tags-view-height))]':
                  fixedHeader.value
              }
            ]}
          >
@@ -216,7 +206,7 @@
                class={[
                  'layout-border__bottom layout-border__top relative',
                  {
                    '!fixed w-full top-[calc(var(--top-tool-height)+1px)] left-0': fixedHeader.value
                    '!fixed w-full top-[var(--top-tool-height)] left-0': fixedHeader.value
                  }
                ]}
                style="transition: width var(--transition-time-02), left var(--transition-time-02);"
@@ -238,7 +228,7 @@
          <ToolHeader class="flex-1"></ToolHeader>
        </div>
        <div class="absolute left-0 top-[var(--logo-height)] h-[calc(100%-var(--logo-height))] w-[calc(100%-2px)] flex">
        <div class="absolute left-0 top-[var(--logo-height)] h-[calc(100%-var(--logo-height))] w-full flex">
          <TabMenu></TabMenu>
          <div
            class={[
@@ -270,18 +260,16 @@
              {tagsView.value ? (
                <TagsView
                  class={[
                    'relative layout-border__bottom layout-border__top',
                    'relative layout-border__bottom',
                    {
                      '!fixed top-0 left-0 z-10': fixedHeader.value,
                      'w-[calc(100%-var(--tab-menu-min-width))] !left-[var(--tab-menu-min-width)] mt-[var(--logo-height)]':
                        collapse.value && fixedHeader.value,
                        collapse.value && fixedHeader.value && !fixedMenu.value,
                      'w-[calc(100%-var(--tab-menu-max-width))] !left-[var(--tab-menu-max-width)] mt-[var(--logo-height)]':
                        !collapse.value && fixedHeader.value,
                      '!fixed top-0 !left-[var(--tab-menu-min-width)+var(--left-menu-max-width)] z-10':
                        fixedHeader.value && fixedMenu.value,
                      'w-[calc(100%-var(--tab-menu-min-width)-var(--left-menu-max-width))] !left-[var(--tab-menu-min-width)+var(--left-menu-max-width)] mt-[var(--logo-height)]':
                        !collapse.value && fixedHeader.value && !fixedMenu.value,
                      'w-[calc(100%-var(--tab-menu-min-width)-var(--left-menu-max-width))] !left-[calc(var(--tab-menu-min-width)+var(--left-menu-max-width))] mt-[var(--logo-height)]':
                        collapse.value && fixedHeader.value && fixedMenu.value,
                      'w-[calc(100%-var(--tab-menu-max-width)-var(--left-menu-max-width))] !left-[var(--tab-menu-max-width)+var(--left-menu-max-width)] mt-[var(--logo-height)]':
                      'w-[calc(100%-var(--tab-menu-max-width)-var(--left-menu-max-width))] !left-[calc(var(--tab-menu-max-width)+var(--left-menu-max-width))] mt-[var(--logo-height)]':
                        !collapse.value && fixedHeader.value && fixedMenu.value
                    }
                  ]}
src/plugins/formCreate/index.ts
@@ -1,7 +1,37 @@
import type { App } from 'vue'
// 👇使用 form-create 需额外全局引入 element plus 组件
import {
  // ElAutocomplete,
  // ElButton,
  // ElCascader,
  // ElCheckbox,
  // ElCheckboxButton,
  // ElCheckboxGroup,
  // ElCol,
  // ElColorPicker,
  // ElDatePicker,
  // ElDialog,
  // ElForm,
  // ElInput,
  // ElInputNumber,
  // ElPopover,
  // ElRadio,
  // ElRadioButton,
  // ElRadioGroup,
  // ElRate,
  // ElRow,
  // ElSelect,
  // ElSlider,
  // ElSwitch,
  // ElTimePicker,
  // ElTooltip,
  // ElTree,
  // ElUpload,
  // ElIcon,
  // ElProgress,
  // 以上会由 @form-create/element-ui/auto-import 自动引入
  ElAlert,
  ElTransfer,
  ElAside,
  ElContainer,
  ElDivider,
@@ -12,7 +42,21 @@
  ElTableColumn,
  ElTabPane,
  ElTabs,
  ElTransfer
  ElDropdown,
  ElDropdownMenu,
  ElDropdownItem,
  ElBadge,
  ElTag,
  ElText,
  ElMenu,
  ElMenuItem,
  ElFooter,
  ElMessage,
  ElCollapse,
  ElCollapseItem,
  ElCard,
  // ElFormItem,
  // ElOption
} from 'element-plus'
import FcDesigner from '@form-create/designer'
import formCreate from '@form-create/element-ui'
@@ -41,18 +85,30 @@
})
const components = [
  ElAlert,
  ElTransfer,
  ElAside,
  ElPopconfirm,
  ElHeader,
  ElMain,
  ElContainer,
  ElDivider,
  ElTransfer,
  ElAlert,
  ElTabs,
  ElHeader,
  ElMain,
  ElPopconfirm,
  ElTable,
  ElTableColumn,
  ElTabPane,
  ElTabs,
  ElDropdown,
  ElDropdownMenu,
  ElDropdownItem,
  ElBadge,
  ElTag,
  ElText,
  ElMenu,
  ElMenuItem,
  ElFooter,
  ElMessage,
  // ElFormItem,
  // ElOption,
  UploadImg,
  UploadImgs,
  UploadFile,
@@ -60,7 +116,10 @@
  UserSelect,
  DeptSelect,
  ApiSelect,
  Editor
  Editor,
  ElCollapse,
  ElCollapseItem,
  ElCard,
]
// 参考 http://www.form-create.com/v3/element-ui/auto-import.html 文档
src/router/modules/remaining.ts
@@ -72,6 +72,7 @@
    path: '/',
    component: Layout,
    name: 'Home',
    redirect: '/index',
    meta: {
      hidden: true,
      noTagsView: true
@@ -287,6 +288,18 @@
        }
      },
      {
        path: 'manager/simple/model',
        component: () => import('@/views/bpm/simple/SimpleModelDesign.vue'),
        name: 'SimpleModelDesign',
        meta: {
          noCache: true,
          hidden: true,
          canTo: true,
          title: '仿钉钉设计流程',
          activeMenu: '/bpm/manager/model'
        }
      },
      {
        path: 'manager/definition',
        component: () => import('@/views/bpm/definition/index.vue'),
        name: 'BpmProcessDefinition',
@@ -308,7 +321,12 @@
          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',
src/store/modules/bpm/simpleWorkflow.ts
对比新文件
@@ -0,0 +1,55 @@
import { store } from '../../index'
import { defineStore } from 'pinia'
export const useWorkFlowStore = defineStore('simpleWorkflow', {
  state: () => ({
    tableId: '',
    isTried: false,
    promoterDrawer: false,
    approverDrawer: false,
    approverConfig1: {},
    copyerDrawer: false,
    copyerConfig: {},
    conditionDrawer: false,
    conditionsConfig1: {
      conditionNodes: []
    },
    userTaskConfig: {}
  }),
  actions: {
    setTableId(payload) {
      this.tableId = payload
    },
    setIsTried(payload) {
      this.isTried = payload
    },
    setPromoter(payload) {
      this.promoterDrawer = payload
    },
    setApproverDrawer(payload) {
      this.approverDrawer = payload
    },
    setApproverConfig(payload) {
      this.approverConfig1 = payload
    },
    setCopyerDrawer(payload) {
      this.copyerDrawer = payload
    },
    setCopyerConfig(payload) {
      this.copyerConfig = payload
    },
    setCondition(payload) {
      this.conditionDrawer = payload
    },
    setConditionsConfig(payload) {
      this.conditionsConfig1 = payload
    },
    setUserTaskConfig(payload) {
      this.userTaskConfig = payload
    }
  }
})
export const useWorkFlowStoreWithOut = () => {
  return useWorkFlowStore(store)
}
src/store/modules/permission.ts
@@ -35,15 +35,18 @@
      return new Promise<void>(async (resolve) => {
        // 获得菜单列表,它在登录的时候,setUserInfoAction 方法中已经进行获取
        let res: AppCustomRouteRecordRaw[] = []
        if (wsSessionCache.get(CACHE_KEY.ROLE_ROUTERS)) {
          res = wsSessionCache.get(CACHE_KEY.ROLE_ROUTERS) as AppCustomRouteRecordRaw[]
        const roleRouters = wsSessionCache.get(CACHE_KEY.ROLE_ROUTERS)
        if (roleRouters) {
          res = roleRouters as AppCustomRouteRecordRaw[]
        }
        const routerMap: AppRouteRecordRaw[] = generateRoute(res)
        // 动态路由,404一定要放到最后面
        // preschooler:vue-router@4以后已支持静态404路由,此处可不再追加
        this.addRouters = routerMap.concat([
          {
            path: '/:path(.*)*',
            redirect: '/404',
            // redirect: '/404',
            component: () => import('@/views/Error/404.vue'),
            name: '404Page',
            meta: {
              hidden: true,
src/store/modules/tagsView.ts
@@ -31,13 +31,27 @@
    },
    // 新增tag
    addVisitedView(view: RouteLocationNormalizedLoaded) {
      if (this.visitedViews.some((v) => v.path === view.path)) return
      if (this.visitedViews.some((v) => v.fullPath === view.fullPath)) return
      if (view.meta?.noTagsView) return
      this.visitedViews.push(
        Object.assign({}, view, {
          title: view.meta?.title || 'no-name'
      const visitedView = Object.assign({}, view, { title: view.meta?.title || 'no-name' })
      if (visitedView.meta) {
        const titleSuffixList: string[] = []
        this.visitedViews.forEach((v) => {
          if (v.path === visitedView.path && v.meta?.title === visitedView.meta?.title) {
            titleSuffixList.push(v.meta?.titleSuffix || '1')
          }
        })
      )
        if (titleSuffixList.length) {
          let titleSuffix = 1
          while (titleSuffixList.includes(`${titleSuffix}`)) {
            titleSuffix += 1
          }
          visitedView.meta.titleSuffix = titleSuffix === 1 ? undefined : `${titleSuffix}`
        }
      }
      this.visitedViews.push(visitedView)
    },
    // 新增缓存
    addCachedView() {
@@ -63,7 +77,7 @@
    // 删除tag
    delVisitedView(view: RouteLocationNormalizedLoaded) {
      for (const [i, v] of this.visitedViews.entries()) {
        if (v.path === view.path) {
        if (v.fullPath === view.fullPath) {
          this.visitedViews.splice(i, 1)
          break
        }
@@ -95,18 +109,18 @@
    // 删除其他tag
    delOthersVisitedViews(view: RouteLocationNormalizedLoaded) {
      this.visitedViews = this.visitedViews.filter((v) => {
        return v?.meta?.affix || v.path === view.path
        return v?.meta?.affix || v.fullPath === view.fullPath
      })
    },
    // 删除左侧
    delLeftViews(view: RouteLocationNormalizedLoaded) {
      const index = findIndex<RouteLocationNormalizedLoaded>(
        this.visitedViews,
        (v) => v.path === view.path
        (v) => v.fullPath === view.fullPath
      )
      if (index > -1) {
        this.visitedViews = this.visitedViews.filter((v, i) => {
          return v?.meta?.affix || v.path === view.path || i > index
          return v?.meta?.affix || v.fullPath === view.fullPath || i > index
        })
        this.addCachedView()
      }
@@ -115,18 +129,18 @@
    delRightViews(view: RouteLocationNormalizedLoaded) {
      const index = findIndex<RouteLocationNormalizedLoaded>(
        this.visitedViews,
        (v) => v.path === view.path
        (v) => v.fullPath === view.fullPath
      )
      if (index > -1) {
        this.visitedViews = this.visitedViews.filter((v, i) => {
          return v?.meta?.affix || v.path === view.path || i < index
          return v?.meta?.affix || v.fullPath === view.fullPath || i < index
        })
        this.addCachedView()
      }
    },
    updateVisitedView(view: RouteLocationNormalizedLoaded) {
      for (let v of this.visitedViews) {
        if (v.path === view.path) {
        if (v.fullPath === view.fullPath) {
          v = Object.assign(v, view)
          break
        }
src/utils/constants.ts
@@ -461,3 +461,23 @@
  TRUE: true, // 启用
  FALSE: false // 禁用
}
// ========== BPM 模块 ==========
export const BpmModelType = {
  BPMN: 10, // BPMN 设计器
  SIMPLE: 20 // 简易设计器
}
export const BpmModelFormType = {
  NORMAL: 10, // 流程表单
  CUSTOM: 20 // 业务表单
}
export const BpmProcessInstanceStatus = {
  NOT_START: -1, // 未开始
  RUNNING: 1, // 审批中
  APPROVE: 2, // 审批通过
  REJECT: 3, // 审批不通过
  CANCEL: 4 // 已取消
}
src/utils/dict.ts
@@ -145,6 +145,7 @@
  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',
src/utils/routerHelper.ts
@@ -21,7 +21,6 @@
/* Layout */
export const Layout = () => import('@/layout/Layout.vue')
export const getParentLayout = () => {
  return () =>
    new Promise((resolve) => {
@@ -89,7 +88,8 @@
    // 2. 生成 data(AppRouteRecordRaw)
    // 路由地址转首字母大写驼峰,作为路由名称,适配keepAlive
    let data: AppRouteRecordRaw = {
      path: route.path.indexOf('?') > -1 ? route.path.split('?')[0] : route.path,
      path:
        route.path.indexOf('?') > -1 && !isUrl(route.path) ? route.path.split('?')[0] : route.path, // 注意,需要排除 http 这种 url,避免它带 ? 参数被截取掉
      name:
        route.componentName && route.componentName.length > 0
          ? route.componentName
@@ -120,7 +120,7 @@
      data.children = [childrenData]
    } else {
      // 目录
      if (route.children) {
      if (route.children?.length) {
        data.component = Layout
        data.redirect = getRedirect(route.path, route.children)
        // 外链
src/views/bpm/category/CategoryForm.vue
@@ -18,7 +18,7 @@
          <el-radio
            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
            :key="dict.value"
            :label="dict.value"
            :value="dict.value"
          >
            {{ dict.label }}
          </el-radio>
@@ -42,6 +42,7 @@
<script setup lang="ts">
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import { CategoryApi, CategoryVO } from '@/api/bpm/category'
import { CommonStatusEnum } from '@/utils/constants'
/** BPM 流程分类 表单 */
defineOptions({ name: 'CategoryForm' })
@@ -57,7 +58,7 @@
  id: undefined,
  name: undefined,
  code: undefined,
  status: undefined,
  status: CommonStatusEnum.ENABLE,
  sort: undefined
})
const formRules = reactive({
@@ -116,7 +117,7 @@
    id: undefined,
    name: undefined,
    code: undefined,
    status: undefined,
    status: CommonStatusEnum.ENABLE,
    sort: undefined
  }
  formRef.value?.resetFields()
src/views/bpm/definition/index.vue
@@ -69,13 +69,7 @@
  <!-- 弹窗:流程模型图的预览 -->
  <Dialog title="流程图" v-model="bpmnDetailVisible" width="800">
    <MyProcessViewer
      key="designer"
      v-model="bpmnXml"
      :value="bpmnXml as any"
      v-bind="bpmnControlForm"
      :prefix="bpmnControlForm.prefix"
    />
    <MyProcessViewer style="height: 700px" key="designer" :xml="bpmnXml" />
  </Dialog>
</template>
@@ -117,7 +111,7 @@
  rule: [],
  option: {}
})
const handleFormDetail = async (row) => {
const handleFormDetail = async (row: any) => {
  if (row.formType == 10) {
    // 设置表单
    setConfAndFields2(formDetailPreview, row.formConf, row.formFields)
@@ -132,13 +126,13 @@
/** 流程图的详情按钮操作 */
const bpmnDetailVisible = ref(false)
const bpmnXml = ref(null)
const bpmnControlForm = ref({
  prefix: 'flowable'
})
const handleBpmnDetail = async (row) => {
  bpmnXml.value = (await DefinitionApi.getProcessDefinition(row.id))?.bpmnXml
const bpmnXml = ref('')
const handleBpmnDetail = async (row: any) => {
  // 设置可见
  bpmnXml.value = ''
  bpmnDetailVisible.value = true
  // 加载 BPMN XML
  bpmnXml.value = (await DefinitionApi.getProcessDefinition(row.id))?.bpmnXml
}
/** 初始化 **/
src/views/bpm/form/editor/index.vue
@@ -1,14 +1,18 @@
<template>
  <ContentWrap>
  <ContentWrap :body-style="{ padding: '0px' }" class="!mb-0">
    <!-- 表单设计器 -->
    <FcDesigner ref="designer" height="780px">
    <div
      class="h-[calc(100vh-var(--top-tool-height)-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-2px)]"
    >
      <fc-designer class="my-designer" ref="designer" :config="designerConfig">
      <template #handle>
        <el-button round size="small" type="primary" @click="handleSave">
          <el-button size="small" type="success" plain @click="handleSave">
          <Icon class="mr-5px" icon="ep:plus" />
          保存
        </el-button>
      </template>
    </FcDesigner>
      </fc-designer>
    </div>
  </ContentWrap>
  <!-- 表单保存的弹窗 -->
@@ -22,7 +26,7 @@
          <el-radio
            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
            :key="dict.value"
            :label="dict.value"
            :value="dict.value"
          >
            {{ dict.label }}
          </el-radio>
@@ -55,6 +59,35 @@
const { query } = useRoute() // 路由信息
const { delView } = useTagsViewStore() // 视图操作
// 表单设计器配置
const designerConfig = ref({
  switchType: [], // 是否可以切换组件类型,或者可以相互切换的字段
  autoActive: true, // 是否自动选中拖入的组件
  useTemplate: false, // 是否生成vue2语法的模板组件
  formOptions: {
    form: {
      labelWidth: '100px' // 设置默认的 label 宽度为 100px
    }
  }, // 定义表单配置默认值
  fieldReadonly: false, // 配置field是否可以编辑
  hiddenDragMenu: false, // 隐藏拖拽操作按钮
  hiddenDragBtn: false, // 隐藏拖拽按钮
  hiddenMenu: [], // 隐藏部分菜单
  hiddenItem: [], // 隐藏部分组件
  hiddenItemConfig: {}, // 隐藏组件的部分配置项
  disabledItemConfig: {}, // 禁用组件的部分配置项
  showSaveBtn: false, // 是否显示保存按钮
  showConfig: true, // 是否显示右侧的配置界面
  showBaseForm: true, // 是否显示组件的基础配置表单
  showControl: true, // 是否显示组件联动
  showPropsForm: true, // 是否显示组件的属性配置表单
  showEventForm: true, // 是否显示组件的事件配置表单
  showValidateForm: true, // 是否显示组件的验证配置表单
  showFormConfig: true, // 是否显示表单配置
  showInputData: true, // 是否显示录入按钮
  showDevice: true, // 是否显示多端适配选项
  appendConfigData: [] // 定义渲染规则所需的formData
})
const designer = ref() // 表单设计器
useFormCreateDesigner(designer) // 表单设计器增强
const dialogVisible = ref(false) // 弹窗是否展示
@@ -119,3 +152,13 @@
  setConfAndFields(designer, data.conf, data.fields)
})
</script>
<style>
.my-designer {
  ._fc-l,
  ._fc-m,
  ._fc-r {
    border-top: none;
  }
}
</style>
src/views/bpm/form/index.vue
@@ -142,8 +142,9 @@
  const toRouter: { name: string; query?: { id: number } } = {
    name: 'BpmFormEditor'
  }
  console.log(typeof id)
  // 表单新建的时候id传的是event需要排除
  if (typeof id === 'number') {
  if (typeof id === 'number' || typeof id === 'string') {
    toRouter.query = {
      id
    }
src/views/bpm/group/UserGroupForm.vue
@@ -28,7 +28,7 @@
          <el-radio
            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
            :key="dict.value"
            :label="dict.value"
            :value="dict.value"
          >
            {{ dict.label }}
          </el-radio>
src/views/bpm/model/CategoryDraggableModel.vue
对比新文件
@@ -0,0 +1,532 @@
<template>
  <div class="flex items-center h-50px">
    <!-- 头部:分类名 -->
    <div class="flex items-center">
      <el-tooltip content="拖动排序" v-if="isCategorySorting">
        <Icon
          :size="22"
          icon="ic:round-drag-indicator"
          class="ml-10px category-drag-icon cursor-move text-#8a909c"
        />
      </el-tooltip>
      <h3 class="ml-20px mr-8px text-18px">{{ categoryInfo.name }}</h3>
      <div class="color-gray-600 text-16px"> ({{ categoryInfo.modelList?.length || 0 }}) </div>
    </div>
    <!-- 头部:操作 -->
    <div class="flex-1 flex" v-if="!isCategorySorting">
      <div
        v-if="categoryInfo.modelList.length > 0"
        class="ml-20px flex items-center"
        :class="[
          'transition-transform duration-300 cursor-pointer',
          isExpand ? 'rotate-180' : 'rotate-0'
        ]"
        @click="isExpand = !isExpand"
      >
        <Icon icon="ep:arrow-down-bold" color="#999" />
      </div>
      <div class="ml-auto flex items-center" :class="isModelSorting ? 'mr-15px' : 'mr-45px'">
        <template v-if="!isModelSorting">
          <el-button
            v-if="categoryInfo.modelList.length > 0"
            link
            type="info"
            class="mr-20px"
            @click.stop="handleModelSort"
          >
            <Icon icon="fa:sort-amount-desc" class="mr-5px" />
            排序
          </el-button>
          <el-button v-else link type="info" class="mr-20px" @click.stop="openModelForm('create')">
            <Icon icon="fa:plus" class="mr-5px" />
            新建
          </el-button>
          <el-dropdown
            @command="(command) => handleCategoryCommand(command, categoryInfo)"
            placement="bottom"
          >
            <el-button link type="info">
              <Icon icon="ep:setting" class="mr-5px" />
              分类
            </el-button>
            <template #dropdown>
              <el-dropdown-menu>
                <el-dropdown-item command="handleRename"> 重命名 </el-dropdown-item>
                <el-dropdown-item command="handleDeleteCategory"> 删除该类 </el-dropdown-item>
              </el-dropdown-menu>
            </template>
          </el-dropdown>
        </template>
        <template v-else>
          <el-button @click.stop="handleModelSortCancel"> 取 消 </el-button>
          <el-button type="primary" @click.stop="handleModelSortSubmit"> 保存排序 </el-button>
        </template>
      </div>
    </div>
  </div>
  <!-- 模型列表 -->
  <el-collapse-transition>
    <div v-show="isExpand">
      <el-table
        :class="categoryInfo.name"
        ref="tableRef"
        :header-cell-style="{ backgroundColor: isDark ? '' : '#edeff0', paddingLeft: '10px' }"
        :cell-style="{ paddingLeft: '10px' }"
        :row-style="{ height: '68px' }"
        :data="modelList"
        row-key="id"
      >
        <el-table-column label="流程名" prop="name" min-width="150">
          <template #default="scope">
            <div class="flex items-center">
              <el-tooltip content="拖动排序" v-if="isModelSorting">
                <Icon
                  icon="ic:round-drag-indicator"
                  class="drag-icon cursor-move text-#8a909c mr-10px"
                />
              </el-tooltip>
              <el-image :src="scope.row.icon" class="h-38px w-38px mr-10px rounded" />
              {{ scope.row.name }}
            </div>
          </template>
        </el-table-column>
        <el-table-column label="可见范围" prop="startUserIds" min-width="100">
          <template #default="scope">
            <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="表单信息" prop="formType" min-width="200">
          <template #default="scope">
            <el-button
              v-if="scope.row.formType === BpmModelFormType.NORMAL"
              type="primary"
              link
              @click="handleFormDetail(scope.row)"
            >
              <span>{{ scope.row.formName }}</span>
            </el-button>
            <el-button
              v-else-if="scope.row.formType === BpmModelFormType.CUSTOM"
              type="primary"
              link
              @click="handleFormDetail(scope.row)"
            >
              <span>{{ scope.row.formCustomCreatePath }}</span>
            </el-button>
            <label v-else>暂无表单</label>
          </template>
        </el-table-column>
        <el-table-column label="最后发布" prop="deploymentTime" min-width="250">
          <template #default="scope">
            <div class="flex items-center">
              <span v-if="scope.row.processDefinition" class="w-150px">
                {{ formatDate(scope.row.processDefinition.deploymentTime) }}
              </span>
              <el-tag v-if="scope.row.processDefinition">
                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>
            </div>
          </template>
        </el-table-column>
        <el-table-column label="操作" width="200" fixed="right">
          <template #default="scope">
            <el-button
              link
              type="primary"
              @click="openModelForm('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
              class="!ml-5px"
              type="primary"
              @click="handleDeploy(scope.row)"
              v-hasPermi="['bpm:model:deploy']"
              :disabled="!isManagerUser(scope.row)"
            >
              发布
            </el-button>
            <el-dropdown
              class="!align-middle ml-5px"
              @command="(command) => handleModelCommand(command, scope.row)"
              v-hasPermi="['bpm:process-definition:query', 'bpm:model:update', 'bpm:model:delete']"
            >
              <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>
    </div>
  </el-collapse-transition>
  <!-- 弹窗:重命名分类 -->
  <Dialog :fullscreen="false" class="rename-dialog" v-model="renameCategoryVisible" width="400">
    <template #title>
      <div class="pl-10px font-bold text-18px"> 重命名分类 </div>
    </template>
    <div class="px-30px">
      <el-input v-model="renameCategoryForm.name" />
    </div>
    <template #footer>
      <div class="pr-25px pb-25px">
        <el-button @click="renameCategoryVisible = false">取 消</el-button>
        <el-button type="primary" @click="handleRenameConfirm">确 定</el-button>
      </div>
    </template>
  </Dialog>
  <!-- 表单弹窗:添加流程模型 -->
  <ModelForm :categoryId="categoryInfo.code" ref="modelFormRef" @success="emit('success')" />
</template>
<script lang="ts" setup>
import ModelForm from './ModelForm.vue'
import { CategoryApi, CategoryVO } from '@/api/bpm/category'
import Sortable from 'sortablejs'
import { propTypes } from '@/utils/propTypes'
import { formatDate } from '@/utils/formatTime'
import * as ModelApi from '@/api/bpm/model'
import * as FormApi from '@/api/bpm/form'
import { setConfAndFields2 } from '@/utils/formCreate'
import { BpmModelFormType, BpmModelType } from '@/utils/constants'
import { checkPermi } from '@/utils/permission'
import { useUserStoreWithOut } from '@/store/modules/user'
import { useAppStore } from '@/store/modules/app'
import { cloneDeep } from 'lodash-es'
defineOptions({ name: 'BpmModel' })
const props = defineProps({
  categoryInfo: propTypes.object.def([]), // 分类后的数据
  isCategorySorting: propTypes.bool.def(false) // 是否分类在排序
})
const emit = defineEmits(['success'])
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const { push } = useRouter() // 路由
const userStore = useUserStoreWithOut() // 用户信息缓存
const isDark = computed(() => useAppStore().getIsDark) // 是否黑暗模式
const isModelSorting = ref(false) // 是否正处于排序状态
const originalData: any = ref([]) // 原始数据
const modelList: any = ref([]) // 模型列表
const isExpand = ref(false) // 是否处于展开状态
/** '更多'操作按钮 */
const handleModelCommand = (command: string, row: any) => {
  switch (command) {
    case 'handleDefinitionList':
      handleDefinitionList(row)
      break
    case 'handleDelete':
      handleDelete(row)
      break
    case 'handleChangeState':
      handleChangeState(row)
      break
    default:
      break
  }
}
/** '分类'操作按钮 */
const handleCategoryCommand = async (command: string, row: any) => {
  switch (command) {
    case 'handleRename':
      renameCategoryForm.value = await CategoryApi.getCategory(row.id)
      renameCategoryVisible.value = true
      break
    case 'handleDeleteCategory':
      await handleDeleteCategory()
      break
    default:
      break
  }
}
/** 删除按钮操作 */
const handleDelete = async (row: any) => {
  try {
    // 删除的二次确认
    await message.delConfirm()
    // 发起删除
    await ModelApi.deleteModel(row.id)
    message.success(t('common.delSuccess'))
    // 刷新列表
    emit('success')
  } catch {}
}
/** 更新状态操作 */
const handleChangeState = async (row: any) => {
  const state = row.processDefinition.suspensionState
  const newState = state === 1 ? 2 : 1
  try {
    // 修改状态的二次确认
    const id = row.id
    debugger
    const statusState = state === 1 ? '停用' : '启用'
    const content = '是否确认' + statusState + '流程名字为"' + row.name + '"的数据项?'
    await message.confirm(content)
    // 发起修改状态
    await ModelApi.updateModelState(id, newState)
    message.success(statusState + '成功')
    // 刷新列表
    emit('success')
  } catch {}
}
/** 设计流程 */
const handleDesign = (row: any) => {
  if (row.type == BpmModelType.BPMN) {
    push({
      name: 'BpmModelEditor',
      query: {
        modelId: row.id
      }
    })
  } else {
    push({
      name: 'SimpleModelDesign',
      query: {
        modelId: row.id
      }
    })
  }
}
/** 发布流程 */
const handleDeploy = async (row: any) => {
  try {
    // 删除的二次确认
    await message.confirm('是否部署该流程!!')
    // 发起部署
    await ModelApi.deployModel(row.id)
    message.success(t('部署成功'))
    // 刷新列表
    emit('success')
  } catch {}
}
/** 跳转到指定流程定义列表 */
const handleDefinitionList = (row: any) => {
  push({
    name: 'BpmProcessDefinition',
    query: {
      key: row.key
    }
  })
}
/** 流程表单的详情按钮操作 */
const formDetailVisible = ref(false)
const formDetailPreview = ref({
  rule: [],
  option: {}
})
const handleFormDetail = async (row: any) => {
  if (row.formType == 10) {
    // 设置表单
    const data = await FormApi.getForm(row.formId)
    setConfAndFields2(formDetailPreview, data.conf, data.fields)
    // 弹窗打开
    formDetailVisible.value = true
  } else {
    await push({
      path: row.formCustomCreatePath
    })
  }
}
/** 判断是否可以操作 */
const isManagerUser = (row: any) => {
  const userId = userStore.getUser.id
  return row.managerUserIds && row.managerUserIds.includes(userId)
}
/** 处理模型的排序 **/
const handleModelSort = () => {
  // 保存初始数据
  originalData.value = cloneDeep(props.categoryInfo.modelList)
  isModelSorting.value = true
  initSort()
}
/** 处理模型的排序提交 */
const handleModelSortSubmit = async () => {
  // 保存排序
  const ids = modelList.value.map((item: any) => item.id)
  await ModelApi.updateModelSortBatch(ids)
  // 刷新列表
  isModelSorting.value = false
  message.success('排序模型成功')
  emit('success')
}
/** 处理模型的排序取消 */
const handleModelSortCancel = () => {
  // 恢复初始数据
  modelList.value = cloneDeep(originalData.value)
  isModelSorting.value = false
}
/** 创建拖拽实例 */
const tableRef = ref()
const initSort = () => {
  const table = document.querySelector(`.${props.categoryInfo.name} .el-table__body-wrapper tbody`)
  Sortable.create(table, {
    group: 'shared',
    animation: 150,
    draggable: '.el-table__row',
    handle: '.drag-icon',
    // 结束拖动事件
    onEnd: ({ newDraggableIndex, oldDraggableIndex }) => {
      if (oldDraggableIndex !== newDraggableIndex) {
        modelList.value.splice(
          newDraggableIndex,
          0,
          modelList.value.splice(oldDraggableIndex, 1)[0]
        )
      }
    }
  })
}
/** 更新 modelList 模型列表 */
const updateModeList = () => {
  modelList.value = cloneDeep(props.categoryInfo.modelList)
  if (props.categoryInfo.modelList.length > 0) {
    isExpand.value = true
  }
}
/** 重命名弹窗确定 */
const renameCategoryVisible = ref(false)
const renameCategoryForm = ref({
  name: ''
})
const handleRenameConfirm = async () => {
  if (renameCategoryForm.value?.name.length === 0) {
    return message.warning('请输入名称')
  }
  // 发起修改
  await CategoryApi.updateCategory(renameCategoryForm.value as CategoryVO)
  message.success('重命名成功')
  // 刷新列表
  renameCategoryVisible.value = false
  emit('success')
}
/** 删除分类 */
const handleDeleteCategory = async () => {
  try {
    if (props.categoryInfo.modelList.length > 0) {
      return message.warning('该分类下仍有流程定义,不允许删除')
    }
    await message.confirm('确认删除分类吗?')
    // 发起删除
    await CategoryApi.deleteCategory(props.categoryInfo.id)
    message.success(t('common.delSuccess'))
    // 刷新列表
    emit('success')
  } catch {}
}
/** 添加流程模型弹窗 */
const modelFormRef = ref()
const openModelForm = (type: string, id?: number) => {
  modelFormRef.value.open(type, id)
}
watch(() => props.categoryInfo.modelList, updateModeList, { immediate: true })
watch(
  () => props.isCategorySorting,
  (val) => {
    if (val) isExpand.value = false
  },
  { immediate: true }
)
</script>
<style lang="scss">
.rename-dialog.el-dialog {
  padding: 0 !important;
  .el-dialog__header {
    border-bottom: none;
  }
  .el-dialog__footer {
    border-top: none !important;
  }
}
</style>
<style lang="scss" scoped>
:deep() {
  .el-table__cell {
    overflow: hidden;
    border-bottom: none !important;
  }
}
</style>
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,13 +45,23 @@
          />
        </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="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
@@ -70,12 +75,7 @@
        </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-option v-for="form in formList" :key="form.id" :label="form.name" :value="form.id" />
          </el-select>
        </el-form-item>
        <el-form-item
@@ -90,18 +90,14 @@
          />
          <el-tooltip
            class="item"
            content="自定义表单的提交路径,使用 Vue 的路由地址,例如说:bpm/oa/leave/create"
          content="自定义表单的提交路径,使用 Vue 的路由地址,例如说:bpm/oa/leave/create.vue"
            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-form-item v-if="formData.formType === 20" label="表单查看地址" prop="formCustomViewPath">
          <el-input
            v-model="formData.formCustomViewPath"
            placeholder="请输入表单查看的组件地址"
@@ -109,14 +105,48 @@
          />
          <el-tooltip
            class="item"
            content="自定义表单的查看组件地址,使用 Vue 的组件地址,例如说:bpm/oa/leave/detail"
          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>
      </div>
      <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"
          >
            {{ 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-option
            v-for="user in userList"
            :key="user.id"
            :label="user.nickname"
            :value="user.id"
          />
        </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,65 @@
  </Dialog>
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { propTypes } from '@/utils/propTypes'
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 props = defineProps({
  categoryId: propTypes.number
})
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 +226,18 @@
    } 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()
  if (props.categoryId) {
    formData.value.category = props.categoryId
  }
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
@@ -199,10 +256,9 @@
      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 +281,20 @@
/** 重置表单 */
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()
}
src/views/bpm/model/ModelImportForm.vue
文件已删除
src/views/bpm/model/editor/index.vue
@@ -58,17 +58,17 @@
}
/** 添加/修改模型 */
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('新增成功')
  }
  // 跳转回去
src/views/bpm/model/index.vue
@@ -1,275 +1,124 @@
<template>
  <ContentWrap>
    <div class="flex justify-between pl-20px items-center">
      <h3 class="font-extrabold">流程模型</h3>
    <!-- 搜索工作栏 -->
    <el-form
      class="-mb-15px"
        v-if="!isCategorySorting"
        class="-mb-15px flex mr-10px"
      :model="queryParams"
      ref="queryFormRef"
      :inline="true"
      label-width="68px"
        @submit.prevent
    >
      <el-form-item label="流程标识" prop="key">
        <el-input
          v-model="queryParams.key"
          placeholder="请输入流程标识"
          clearable
          @keyup.enter="handleQuery"
          class="!w-240px"
        />
      </el-form-item>
      <el-form-item label="流程名称" prop="name">
        <el-form-item prop="name" class="ml-auto">
        <el-input
          v-model="queryParams.name"
          placeholder="请输入流程名称"
            placeholder="搜索流程"
          clearable
          @keyup.enter="handleQuery"
          class="!w-240px"
        />
      </el-form-item>
      <el-form-item label="流程分类" prop="category">
        <el-select
          v-model="queryParams.category"
          placeholder="请选择流程分类"
          clearable
          class="!w-240px"
        >
          <el-option
            v-for="category in categoryList"
            :key="category.code"
            :label="category.name"
            :value="category.code"
          />
        </el-select>
            <template #prefix>
              <Icon icon="ep:search" class="mx-10px" />
            </template>
          </el-input>
        </el-form-item>
        <!-- 右上角:新建模型、更多操作 -->
        <el-form-item>
          <el-button type="primary" @click="openForm('create')" v-hasPermi="['bpm:model:create']">
            <Icon icon="ep:plus" class="mr-5px" /> 新建模型
          </el-button>
      </el-form-item>
      <el-form-item>
        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
        <el-button
          type="primary"
          plain
          @click="openForm('create')"
          v-hasPermi="['bpm:model:create']"
        >
          <Icon icon="ep:plus" class="mr-5px" /> 新建流程
          <el-dropdown @command="(command) => handleCommand(command)" placement="bottom-end">
            <el-button class="w-30px" plain>
              <Icon icon="ep:setting" />
        </el-button>
        <el-button type="success" plain @click="openImportForm" v-hasPermi="['bpm:model:import']">
          <Icon icon="ep:upload" class="mr-5px" /> 导入流程
        </el-button>
            <template #dropdown>
              <el-dropdown-menu>
                <el-dropdown-item command="handleCategoryAdd">
                  <Icon icon="ep:circle-plus" :size="13" class="mr-5px" />
                  新建分类
                </el-dropdown-item>
                <el-dropdown-item command="handleCategorySort">
                  <Icon icon="fa:sort-amount-desc" :size="13" class="mr-5px" />
                  分类排序
                </el-dropdown-item>
              </el-dropdown-menu>
            </template>
          </el-dropdown>
      </el-form-item>
    </el-form>
  </ContentWrap>
      <div class="mr-20px" v-else>
        <el-button @click="handleCategorySortCancel"> 取 消 </el-button>
        <el-button type="primary" @click="handleCategorySortSubmit"> 保存排序 </el-button>
      </div>
    </div>
  <!-- 列表 -->
  <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">
        <template #default="scope">
          <el-button type="primary" link @click="handleBpmnDetail(scope.row)">
            <span>{{ scope.row.name }}</span>
          </el-button>
        </template>
      </el-table-column>
      <el-table-column label="流程图标" align="center" prop="icon" width="100">
        <template #default="scope">
          <el-image :src="scope.row.icon" class="w-32px h-32px" />
        </template>
      </el-table-column>
      <el-table-column label="流程分类" align="center" prop="categoryName" width="100" />
      <el-table-column label="表单信息" align="center" prop="formType" width="200">
        <template #default="scope">
          <el-button
            v-if="scope.row.formType === 10"
            type="primary"
            link
            @click="handleFormDetail(scope.row)"
    <el-divider />
    <!-- 按照分类,展示其所属的模型列表 -->
    <div class="px-15px">
      <draggable
        :disabled="!isCategorySorting"
        v-model="categoryGroup"
        item-key="id"
        :animation="400"
          >
            <span>{{ scope.row.formName }}</span>
          </el-button>
          <el-button
            v-else-if="scope.row.formType === 20"
            type="primary"
            link
            @click="handleFormDetail(scope.row)"
        <template #item="{ element }">
          <ContentWrap
            class="rounded-lg transition-all duration-300 ease-in-out hover:shadow-xl"
            v-loading="loading"
            :body-style="{ padding: 0 }"
            :key="element.id"
          >
            <span>{{ scope.row.formCustomCreatePath }}</span>
          </el-button>
          <label v-else>暂无表单</label>
        </template>
      </el-table-column>
      <el-table-column
        label="创建时间"
        align="center"
        prop="createTime"
        width="180"
        :formatter="dateFormatter"
            <CategoryDraggableModel
              :isCategorySorting="isCategorySorting"
              :categoryInfo="element"
              @success="getList"
      />
      <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>
          </ContentWrap>
          </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>
      <el-table-column label="操作" align="center" width="240" fixed="right">
        <template #default="scope">
          <el-button
            link
            type="primary"
            @click="openForm('update', scope.row.id)"
            v-hasPermi="['bpm:model:update']"
          >
            修改流程
          </el-button>
          <el-button
            link
            type="primary"
            @click="handleDesign(scope.row)"
            v-hasPermi="['bpm:model:update']"
          >
            设计流程
          </el-button>
          <el-button
            link
            type="primary"
            @click="handleDeploy(scope.row)"
            v-hasPermi="['bpm:model:deploy']"
          >
            发布流程
          </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-button>
        </template>
      </el-table-column>
    </el-table>
    <!-- 分页 -->
    <Pagination
      :total="total"
      v-model:page="queryParams.pageNo"
      v-model:limit="queryParams.pageSize"
      @pagination="getList"
    />
      </draggable>
    </div>
  </ContentWrap>
  <!-- 表单弹窗:添加/修改流程 -->
  <ModelForm ref="formRef" @success="getList" />
  <!-- 表单弹窗:导入流程 -->
  <ModelImportForm ref="importFormRef" @success="getList" />
  <!-- 表单弹窗:添加分类 -->
  <CategoryForm ref="categoryFormRef" @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 * 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 draggable from 'vuedraggable'
import { CategoryApi } from '@/api/bpm/category'
import * as ModelApi from '@/api/bpm/model'
import ModelForm from './ModelForm.vue'
import CategoryForm from '../category/CategoryForm.vue'
import { cloneDeep } from 'lodash-es'
import CategoryDraggableModel from './CategoryDraggableModel.vue'
defineOptions({ name: 'BpmModel' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const { push } = useRouter() // 路由
const loading = ref(true) // 列表的加载中
const total = ref(0) // 列表的总页数
const list = ref([]) // 列表的数据
const isCategorySorting = ref(false) // 是否 category 正处于排序状态
const queryParams = reactive({
  pageNo: 1,
  pageSize: 10,
  key: undefined,
  name: undefined,
  category: undefined
  name: undefined
})
const queryFormRef = ref() // 搜索的表单
const categoryList = ref([]) // 流程分类列表
/** 查询列表 */
const getList = async () => {
  loading.value = true
  try {
    const data = await ModelApi.getModelPage(queryParams)
    list.value = data.list
    total.value = data.total
  } finally {
    loading.value = false
  }
}
const categoryGroup: any = ref([]) // 按照 category 分组的数据
const originalData: any = ref([]) // 原始数据
/** 搜索按钮操作 */
const handleQuery = () => {
  queryParams.pageNo = 1
  getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
  queryFormRef.value.resetFields()
  handleQuery()
}
/** 添加/修改操作 */
@@ -278,113 +127,95 @@
  formRef.value.open(type, id)
}
/** 添加/修改操作 */
const importFormRef = ref()
const openImportForm = () => {
  importFormRef.value.open()
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
  try {
    // 删除的二次确认
    await message.delConfirm()
    // 发起删除
    await ModelApi.deleteModel(id)
    message.success(t('common.delSuccess'))
    // 刷新列表
    await getList()
  } catch {}
}
/** 更新状态操作 */
const handleChangeState = async (row) => {
  const state = row.processDefinition.suspensionState
  try {
    // 修改状态的二次确认
    const id = row.id
    const statusState = state === 1 ? '激活' : '挂起'
    const content = '是否确认' + statusState + '流程名字为"' + row.name + '"的数据项?'
    await message.confirm(content)
    // 发起修改状态
    await ModelApi.updateModelState(id, state)
    // 刷新列表
    await getList()
  } catch {
    // 取消后,进行恢复按钮
    row.processDefinition.suspensionState = state === 1 ? 2 : 1
  }
}
/** 设计流程 */
const handleDesign = (row) => {
  push({
    name: 'BpmModelEditor',
    query: {
      modelId: row.id
    }
  })
}
/** 发布流程 */
const handleDeploy = async (row) => {
  try {
    // 删除的二次确认
    await message.confirm('是否部署该流程!!')
    // 发起部署
    await ModelApi.deployModel(row.id)
    message.success(t('部署成功'))
    // 刷新列表
    await getList()
  } catch {}
}
/** 跳转到指定流程定义列表 */
const handleDefinitionList = (row) => {
  push({
    name: 'BpmProcessDefinition',
    query: {
      key: row.key
    }
  })
}
/** 流程表单的详情按钮操作 */
const formDetailVisible = ref(false)
const formDetailPreview = ref({
  rule: [],
  option: {}
})
const handleFormDetail = async (row) => {
  if (row.formType == 10) {
    // 设置表单
    const data = await FormApi.getForm(row.formId)
    setConfAndFields2(formDetailPreview, data.conf, data.fields)
    // 弹窗打开
    formDetailVisible.value = true
  } else {
    await push({
      path: row.formCustomCreatePath
    })
/** 右上角设置按钮 */
const handleCommand = (command: string) => {
  switch (command) {
    case 'handleCategoryAdd':
      handleCategoryAdd()
      break
    case 'handleCategorySort':
      handleCategorySort()
      break
    default:
      break
  }
}
/** 流程图的详情按钮操作 */
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 categoryFormRef = ref()
const handleCategoryAdd = () => {
  categoryFormRef.value.open('create')
}
/** 分类排序的提交 */
const handleCategorySort = () => {
  // 保存初始数据
  originalData.value = cloneDeep(categoryGroup.value)
  isCategorySorting.value = true
}
/** 分类排序的取消 */
const handleCategorySortCancel = () => {
  // 恢复初始数据
  categoryGroup.value = cloneDeep(originalData.value)
  isCategorySorting.value = false
}
/** 分类排序的保存 */
const handleCategorySortSubmit = async () => {
  // 保存排序
  const ids = categoryGroup.value.map((item: any) => item.id)
  await CategoryApi.updateCategorySortBatch(ids)
  // 刷新列表
  isCategorySorting.value = false
  message.success('排序分类成功')
  await getList()
}
/** 加载数据 */
const getList = async () => {
  loading.value = true
  try {
    // 查询模型 + 分裂的列表
    const modelList = await ModelApi.getModelList(queryParams.name)
    const categoryList = await CategoryApi.getCategorySimpleList()
    // 按照 category 聚合
    // 注意:必须一次性赋值给 categoryGroup,否则每次操作后,列表会重新渲染,滚动条的位置会偏离!!!
    categoryGroup.value = categoryList.map((category: any) => ({
      ...category,
      modelList: modelList.filter((model: any) => model.categoryName == category.name)
    }))
  } finally {
    loading.value = false
  }
}
/** 初始化 **/
onMounted(async () => {
  await getList()
  // 查询流程分类列表
  categoryList.value = await CategoryApi.getCategorySimpleList()
onMounted(() => {
  getList()
})
</script>
<style lang="scss" scoped>
:deep() {
  .el-table--fit .el-table__inner-wrapper:before {
    height: 0;
  }
  .el-card {
    border-radius: 8px;
  }
  .el-form--inline .el-form-item {
    margin-right: 10px;
  }
  .el-divider--horizontal {
    margin-top: 6px;
  }
}
</style>
src/views/bpm/model/index_old.vue
对比新文件
@@ -0,0 +1,404 @@
<template>
  <doc-alert title="流程设计器(BPMN)" url="https://doc.iocoder.cn/bpm/model-designer-dingding/" />
  <doc-alert
    title="流程设计器(钉钉、飞书)"
    url="https://doc.iocoder.cn/bpm/model-designer-bpmn/"
  />
  <doc-alert title="选择审批人、发起人自选" url="https://doc.iocoder.cn/bpm/assignee/" />
  <doc-alert title="会签、或签、依次审批" url="https://doc.iocoder.cn/bpm/multi-instance/" />
  <ContentWrap>
    <!-- 搜索工作栏 -->
    <el-form
      class="-mb-15px"
      :model="queryParams"
      ref="queryFormRef"
      :inline="true"
      label-width="68px"
    >
      <el-form-item label="流程标识" prop="key">
        <el-input
          v-model="queryParams.key"
          placeholder="请输入流程标识"
          clearable
          @keyup.enter="handleQuery"
          class="!w-240px"
        />
      </el-form-item>
      <el-form-item label="流程名称" prop="name">
        <el-input
          v-model="queryParams.name"
          placeholder="请输入流程名称"
          clearable
          @keyup.enter="handleQuery"
          class="!w-240px"
        />
      </el-form-item>
      <el-form-item label="流程分类" prop="category">
        <el-select
          v-model="queryParams.category"
          placeholder="请选择流程分类"
          clearable
          class="!w-240px"
        >
          <el-option
            v-for="category in categoryList"
            :key="category.code"
            :label="category.name"
            :value="category.code"
          />
        </el-select>
      </el-form-item>
      <el-form-item>
        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
        <el-button
          type="primary"
          plain
          @click="openForm('create')"
          v-hasPermi="['bpm:model:create']"
        >
          <Icon icon="ep:plus" class="mr-5px" /> 新建
        </el-button>
      </el-form-item>
    </el-form>
  </ContentWrap>
  <!-- 列表 -->
  <ContentWrap>
    <el-table v-loading="loading" :data="list">
      <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-image :src="scope.row.icon" class="h-32px w-32px" />
        </template>
      </el-table-column>
      <el-table-column label="可见范围" align="center" prop="startUserIds" min-width="100">
        <template #default="scope">
          <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" 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"
            type="primary"
            link
            @click="handleFormDetail(scope.row)"
          >
            <span>{{ scope.row.formName }}</span>
          </el-button>
          <el-button
            v-else-if="scope.row.formType === 20"
            type="primary"
            link
            @click="handleFormDetail(scope.row)"
          >
            <span>{{ scope.row.formCustomCreatePath }}</span>
          </el-button>
          <label v-else>暂无表单</label>
        </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" 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
            class="!ml-5px"
            type="primary"
            @click="handleDeploy(scope.row)"
            v-hasPermi="['bpm:model:deploy']"
            :disabled="!isManagerUser(scope.row)"
          >
            发布
          </el-button>
          <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 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>
    <!-- 分页 -->
    <Pagination
      :total="total"
      v-model:page="queryParams.pageNo"
      v-model:limit="queryParams.pageSize"
      @pagination="getList"
    />
  </ContentWrap>
  <!-- 表单弹窗:添加/修改流程 -->
  <ModelForm ref="formRef" @success="getList" />
  <!-- 弹窗:表单详情 -->
  <Dialog title="表单详情" v-model="formDetailVisible" width="800">
    <form-create :rule="formDetailPreview.rule" :option="formDetailPreview.option" />
  </Dialog>
</template>
<script lang="ts" setup>
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 { 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) // 列表的总页数
const list = ref([]) // 列表的数据
const queryParams = reactive({
  pageNo: 1,
  pageSize: 10,
  key: undefined,
  name: undefined,
  category: undefined
})
const queryFormRef = ref() // 搜索的表单
const categoryList = ref([]) // 流程分类列表
/** 查询列表 */
const getList = async () => {
  loading.value = true
  try {
    const data = await ModelApi.getModelList(queryParams)
    list.value = data.list
    total.value = data.total
  } finally {
    loading.value = false
  }
}
/** 搜索按钮操作 */
const handleQuery = () => {
  queryParams.pageNo = 1
  getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
  queryFormRef.value.resetFields()
  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 handleDelete = async (row: any) => {
  try {
    // 删除的二次确认
    await message.delConfirm()
    // 发起删除
    await ModelApi.deleteModel(row.id)
    message.success(t('common.delSuccess'))
    // 刷新列表
    await getList()
  } catch {}
}
/** 更新状态操作 */
const handleChangeState = async (row: any) => {
  const state = row.processDefinition.suspensionState
  const newState = state === 1 ? 2 : 1
  try {
    // 修改状态的二次确认
    const id = row.id
    debugger
    const statusState = state === 1 ? '停用' : '启用'
    const content = '是否确认' + statusState + '流程名字为"' + row.name + '"的数据项?'
    await message.confirm(content)
    // 发起修改状态
    await ModelApi.updateModelState(id, newState)
    message.success(statusState + '成功')
    // 刷新列表
    await getList()
  } catch {}
}
/** 设计流程 */
const handleDesign = (row: any) => {
  if (row.type == BpmModelType.BPMN) {
    push({
      name: 'BpmModelEditor',
      query: {
        modelId: row.id
      }
    })
  } else {
    push({
      name: 'SimpleModelDesign',
      query: {
        modelId: row.id
      }
    })
  }
}
/** 发布流程 */
const handleDeploy = async (row: any) => {
  try {
    // 删除的二次确认
    await message.confirm('是否部署该流程!!')
    // 发起部署
    await ModelApi.deployModel(row.id)
    message.success(t('部署成功'))
    // 刷新列表
    await getList()
  } catch {}
}
/** 跳转到指定流程定义列表 */
const handleDefinitionList = (row) => {
  push({
    name: 'BpmProcessDefinition',
    query: {
      key: row.key
    }
  })
}
/** 流程表单的详情按钮操作 */
const formDetailVisible = ref(false)
const formDetailPreview = ref({
  rule: [],
  option: {}
})
const handleFormDetail = async (row: any) => {
  if (row.formType == 10) {
    // 设置表单
    const data = await FormApi.getForm(row.formId)
    setConfAndFields2(formDetailPreview, data.conf, data.fields)
    // 弹窗打开
    formDetailVisible.value = true
  } else {
    await push({
      path: row.formCustomCreatePath
    })
  }
}
/** 判断是否可以操作 */
const isManagerUser = (row: any) => {
  const userId = userStore.getUser.id
  return row.managerUserIds && row.managerUserIds.includes(userId)
}
/** 初始化 **/
onMounted(async () => {
  await getList()
  // 查询流程分类列表
  categoryList.value = await CategoryApi.getCategorySimpleList()
})
</script>
src/views/bpm/processExpression/ProcessExpressionForm.vue
@@ -15,7 +15,7 @@
          <el-radio
            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
            :key="dict.value"
            :label="dict.value"
            :value="dict.value"
          >
            {{ dict.label }}
          </el-radio>
src/views/bpm/processInstance/create/ProcessDefinitionDetail.vue
对比新文件
@@ -0,0 +1,259 @@
<template>
  <ContentWrap :bodyStyle="{ padding: '10px 20px 0' }">
    <div class="processInstance-wrap-main">
      <el-scrollbar>
        <div class="text-#878c93 h-15px">流程:{{ selectProcessDefinition.name }}</div>
        <el-divider class="!my-8px" />
        <!-- 中间主要内容 tab 栏 -->
        <el-tabs v-model="activeTab">
          <!-- 表单信息 -->
          <el-tab-pane label="表单填写" name="form">
            <div class="form-scroll-area">
              <el-scrollbar>
                <el-row>
                  <el-col :span="17">
                    <form-create
                      :rule="detailForm.rule"
                      v-model:api="fApi"
                      v-model="detailForm.value"
                      :option="detailForm.option"
                      @submit="submitForm"
                    />
                  </el-col>
                  <el-col :span="6" :offset="1">
                    <!-- 流程时间线 -->
                    <ProcessInstanceTimeline
                      ref="timelineRef"
                      :activity-nodes="activityNodes"
                      :show-status-icon="false"
                      @select-user-confirm="selectUserConfirm"
                    />
                  </el-col>
                </el-row>
              </el-scrollbar>
            </div>
          </el-tab-pane>
          <!-- 流程图 -->
          <el-tab-pane label="流程图" name="diagram">
            <div class="form-scroll-area">
              <!-- BPMN 流程图预览 -->
              <ProcessInstanceBpmnViewer
                :bpmn-xml="bpmnXML"
                v-if="BpmModelType.BPMN === selectProcessDefinition.modelType"
              />
              <!-- Simple 流程图预览 -->
              <ProcessInstanceSimpleViewer
                :simple-json="simpleJson"
                v-if="BpmModelType.SIMPLE === selectProcessDefinition.modelType"
              />
            </div>
          </el-tab-pane>
        </el-tabs>
        <!-- 底部操作栏 -->
        <div class="b-t-solid border-t-1px border-[var(--el-border-color)]">
          <!-- 操作栏按钮 -->
          <div
            v-if="activeTab === 'form'"
            class="h-50px bottom-10 text-14px flex items-center color-#32373c dark:color-#fff font-bold btn-container"
          >
            <el-button plain type="success" @click="submitForm">
              <Icon icon="ep:select" />&nbsp; 发起
            </el-button>
            <el-button plain type="danger" @click="handleCancel">
              <Icon icon="ep:close" />&nbsp; 取消
            </el-button>
          </div>
        </div>
      </el-scrollbar>
    </div>
  </ContentWrap>
</template>
<script lang="ts" setup>
import { decodeFields, setConfAndFields2 } from '@/utils/formCreate'
import { BpmModelType } from '@/utils/constants'
import { CandidateStrategy } from '@/components/SimpleProcessDesignerV2/src/consts'
import ProcessInstanceBpmnViewer from '../detail/ProcessInstanceBpmnViewer.vue'
import ProcessInstanceSimpleViewer from '../detail/ProcessInstanceSimpleViewer.vue'
import ProcessInstanceTimeline from '../detail/ProcessInstanceTimeline.vue'
import type { ApiAttrs } from '@form-create/element-ui/types/config'
import { useTagsViewStore } from '@/store/modules/tagsView'
import * as ProcessInstanceApi from '@/api/bpm/processInstance'
import * as DefinitionApi from '@/api/bpm/definition'
import { ApprovalNodeInfo } from '@/api/bpm/processInstance'
defineOptions({ name: 'ProcessDefinitionDetail' })
const props = defineProps<{
  selectProcessDefinition: any
}>()
const emit = defineEmits(['cancel'])
const { push, currentRoute } = useRouter() // 路由
const message = useMessage() // 消息弹窗
const { delView } = useTagsViewStore() // 视图操作
const detailForm: any = ref({
  rule: [],
  option: {},
  value: {}
}) // 流程表单详情
const fApi = ref<ApiAttrs>()
// 指定审批人
const startUserSelectTasks: any = ref([]) // 发起人需要选择审批人或抄送人的任务列表
const startUserSelectAssignees = ref({}) // 发起人选择审批人的数据
const bpmnXML: any = ref(null) // BPMN 数据
const simpleJson = ref<string | undefined>() // Simple 设计器数据 json 格式
const activeTab = ref('form') // 当前的 Tab
const activityNodes = ref<ProcessInstanceApi.ApprovalNodeInfo[]>([]) // 审批节点信息
/** 设置表单信息、获取流程图数据 **/
const initProcessInfo = async (row: any, formVariables?: any) => {
  // 重置指定审批人
  startUserSelectTasks.value = []
  startUserSelectAssignees.value = {}
  // 情况一:流程表单
  if (row.formType == 10) {
    // 设置表单
    // 注意:需要从 formVariables 中,移除不在 row.formFields 的值。
    // 原因是:后端返回的 formVariables 里面,会有一些非表单的信息。例如说,某个流程节点的审批人。
    //        这样,就可能导致一个流程被审批不通过后,重新发起时,会直接后端报错!!!
    const allowedFields = decodeFields(row.formFields).map((fieldObj: any) => fieldObj.field)
    for (const key in formVariables) {
      if (!allowedFields.includes(key)) {
        delete formVariables[key]
      }
    }
    setConfAndFields2(detailForm, row.formConf, row.formFields, formVariables)
    await nextTick()
    fApi.value?.btn.show(false) // 隐藏提交按钮
    // 获取流程审批信息
    await getApprovalDetail(row)
    // 加载流程图
    const processDefinitionDetail = await DefinitionApi.getProcessDefinition(row.id)
    if (processDefinitionDetail) {
      bpmnXML.value = processDefinitionDetail.bpmnXml
      simpleJson.value = processDefinitionDetail.simpleModel
    }
    // 情况二:业务表单
  } else if (row.formCustomCreatePath) {
    await push({
      path: row.formCustomCreatePath
    })
    // 这里暂时无需加载流程图,因为跳出到另外个 Tab;
  }
}
/** 获取审批详情 */
const getApprovalDetail = async (row: any) => {
  try {
    const data = await ProcessInstanceApi.getApprovalDetail({ processDefinitionId: row.id })
    if (!data) {
      message.error('查询不到审批详情信息!')
      return
    }
    // 获取发起人自选的任务
    startUserSelectTasks.value = data.activityNodes?.filter(
      (node: ApprovalNodeInfo) => CandidateStrategy.START_USER_SELECT === node.candidateStrategy
    )
    if (startUserSelectTasks.value?.length > 0) {
      for (const node of startUserSelectTasks.value) {
        startUserSelectAssignees.value[node.id] = []
      }
    }
    // 获取审批节点,显示 Timeline 的数据
    activityNodes.value = data.activityNodes
  } finally {
  }
}
/** 提交按钮 */
const submitForm = async () => {
  if (!fApi.value || !props.selectProcessDefinition) {
    return
  }
  // 如果有指定审批人,需要校验
  if (startUserSelectTasks.value?.length > 0) {
    for (const userTask of startUserSelectTasks.value) {
      if (
        Array.isArray(startUserSelectAssignees.value[userTask.id]) &&
        startUserSelectAssignees.value[userTask.id].length === 0
      )
        return message.warning(`请选择${userTask.name}的候选人`)
    }
  }
  // 提交请求
  fApi.value.btn.loading(true)
  try {
    await ProcessInstanceApi.createProcessInstance({
      processDefinitionId: props.selectProcessDefinition.id,
      variables: detailForm.value.value,
      startUserSelectAssignees: startUserSelectAssignees.value
    })
    // 提示
    message.success('发起流程成功')
    // 跳转回去
    delView(unref(currentRoute))
    await push({
      name: 'BpmProcessInstanceMy'
    })
  } finally {
    fApi.value.btn.loading(false)
  }
}
/** 取消发起审批 */
const handleCancel = () => {
  emit('cancel')
}
/** 选择发起人 */
const selectUserConfirm = (id: string, userList: any[]) => {
  startUserSelectAssignees.value[id] = userList?.map((item: any) => item.id)
}
defineExpose({ initProcessInfo })
</script>
<style lang="scss" scoped>
$wrap-padding-height: 20px;
$wrap-margin-height: 15px;
$button-height: 51px;
$process-header-height: 105px;
.processInstance-wrap-main {
  height: calc(
    100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px
  );
  max-height: calc(
    100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px
  );
  overflow: auto;
  .form-scroll-area {
    height: calc(
      100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px -
        $process-header-height - 40px
    );
    max-height: calc(
      100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px -
        $process-header-height - 40px
    );
    overflow: auto;
  }
}
.form-box {
  :deep(.el-card) {
    border: none;
  }
}
</style>
src/views/bpm/processInstance/create/index.vue
@@ -1,25 +1,59 @@
<template>
  <!-- 第一步,通过流程定义的列表,选择对应的流程 -->
  <ContentWrap v-if="!selectProcessDefinition" v-loading="loading">
    <el-tabs tab-position="left" v-model="categoryActive">
      <el-tab-pane
        :label="category.name"
        :name="category.code"
        :key="category.code"
        v-for="category in categoryList"
  <template v-if="!selectProcessDefinition">
    <el-input
      v-model="searchName"
      class="!w-50% mb-15px"
      placeholder="请输入流程名称"
      clearable
      @input="handleQuery"
      @clear="handleQuery"
      >
        <el-row :gutter="20">
          <el-col
            :lg="6"
            :sm="12"
            :xs="24"
            v-for="definition in categoryProcessDefinitionList"
      <template #prefix>
        <Icon icon="ep:search" />
      </template>
    </el-input>
    <ContentWrap
      :class="{ 'process-definition-container': filteredProcessDefinitionList?.length }"
      class="position-relative pb-20px h-700px"
      v-loading="loading"
    >
      <el-row v-if="filteredProcessDefinitionList?.length" :gutter="20" class="!flex-nowrap">
        <el-col :span="5">
          <div class="flex flex-col">
            <div
              v-for="category in availableCategories"
              :key="category.code"
              class="flex items-center p-10px cursor-pointer text-14px rounded-md"
              :class="categoryActive.code === category.code ? 'text-#3e7bff bg-#e8eeff' : ''"
              @click="handleCategoryClick(category)"
            >
              {{ category.name }}
            </div>
          </div>
        </el-col>
        <el-col :span="19">
          <el-scrollbar ref="scrollWrapper" height="700" @scroll="handleScroll">
            <div
              class="mb-20px pl-10px"
              v-for="(definitions, categoryCode) in processDefinitionGroup"
              :key="categoryCode"
              :ref="`category-${categoryCode}`"
            >
              <h3 class="text-18px font-bold mb-10px mt-5px">
                {{ getCategoryName(categoryCode as any) }}
              </h3>
              <div class="grid grid-cols-3 gap3">
                <el-tooltip
                  v-for="definition in definitions"
            :key="definition.id"
                  :content="definition.description"
                  :disabled="!definition.description || definition.description.trim().length === 0"
                  placement="top"
          >
            <el-card
              shadow="hover"
              class="mb-20px cursor-pointer"
                    class="cursor-pointer definition-item-card"
              @click="handleSelect(definition)"
            >
              <template #default>
@@ -29,104 +63,53 @@
                </div>
              </template>
            </el-card>
                </el-tooltip>
              </div>
            </div>
          </el-scrollbar>
          </el-col>
        </el-row>
      </el-tab-pane>
    </el-tabs>
      <el-empty class="!py-200px" :image-size="200" description="没有找到搜索结果" v-else />
  </ContentWrap>
  </template>
  <!-- 第二步,填写表单,进行流程的提交 -->
  <ContentWrap v-else>
    <el-card class="box-card">
      <div class="clearfix">
        <span class="el-icon-document">申请信息【{{ selectProcessDefinition.name }}】</span>
        <el-button style="float: right" type="primary" @click="selectProcessDefinition = undefined">
          <Icon icon="ep:delete" /> 选择其它流程
        </el-button>
      </div>
      <el-col :span="16" :offset="6" style="margin-top: 20px">
        <form-create
          :rule="detailForm.rule"
          v-model:api="fApi"
          v-model="detailForm.value"
          :option="detailForm.option"
          @submit="submitForm"
        >
          <template #type-startUserSelect>
            <el-col :span="24">
              <el-card class="mb-10px">
                <template #header>指定审批人</template>
                <el-form
                  :model="startUserSelectAssignees"
                  :rules="startUserSelectAssigneesFormRules"
                  ref="startUserSelectAssigneesFormRef"
                >
                  <el-form-item
                    v-for="userTask in startUserSelectTasks"
                    :key="userTask.id"
                    :label="`任务【${userTask.name}】`"
                    :prop="userTask.id"
                  >
                    <el-select
                      v-model="startUserSelectAssignees[userTask.id]"
                      multiple
                      placeholder="请选择审批人"
                    >
                      <el-option
                        v-for="user in userList"
                        :key="user.id"
                        :label="user.nickname"
                        :value="user.id"
  <ProcessDefinitionDetail
    v-else
    ref="processDefinitionDetailRef"
    :selectProcessDefinition="selectProcessDefinition"
    @cancel="selectProcessDefinition = undefined"
                      />
                    </el-select>
                  </el-form-item>
                </el-form>
              </el-card>
            </el-col>
          </template>
        </form-create>
      </el-col>
    </el-card>
    <!-- 流程图预览 -->
    <ProcessInstanceBpmnViewer :bpmn-xml="bpmnXML as any" />
  </ContentWrap>
</template>
<script lang="ts" setup>
import * as DefinitionApi from '@/api/bpm/definition'
import * as ProcessInstanceApi from '@/api/bpm/processInstance'
import { setConfAndFields2 } from '@/utils/formCreate'
import type { ApiAttrs } from '@form-create/element-ui/types/config'
import ProcessInstanceBpmnViewer from '../detail/ProcessInstanceBpmnViewer.vue'
import { CategoryApi } from '@/api/bpm/category'
import { useTagsViewStore } from '@/store/modules/tagsView'
import * as UserApi from '@/api/system/user'
import { CategoryApi, CategoryVO } from '@/api/bpm/category'
import ProcessDefinitionDetail from './ProcessDefinitionDetail.vue'
import { groupBy } from 'lodash-es'
defineOptions({ name: 'BpmProcessInstanceCreate' })
const { proxy } = getCurrentInstance() as any
const route = useRoute() // 路由
const { push, currentRoute } = useRouter() // 路由
const message = useMessage() // 消息
const { delView } = useTagsViewStore() // 视图操作
const processInstanceId = route.query.processInstanceId
const searchName = ref('') // 当前搜索关键字
const processInstanceId: any = route.query.processInstanceId // 流程实例编号。场景:重新发起时
const loading = ref(true) // 加载中
const categoryList = ref([]) // 分类的列表
const categoryActive = ref('') // 选中的分类
const categoryList: any = ref([]) // 分类的列表
const categoryActive: any = ref({}) // 选中的分类
const processDefinitionList = ref([]) // 流程定义的列表
/** 查询列表 */
const getList = async () => {
  loading.value = true
  try {
    // 流程分类
    categoryList.value = await CategoryApi.getCategorySimpleList()
    if (categoryList.value.length > 0) {
      categoryActive.value = categoryList.value[0].code
    }
    // 流程定义
    processDefinitionList.value = await DefinitionApi.getProcessDefinitionList({
      suspensionState: 1
    })
    // 所有流程分类数据
    await getCategoryList()
    // 所有流程定义数据
    await getProcessDefinitionList()
    // 如果 processInstanceId 非空,说明是重新发起
    if (processInstanceId?.length > 0) {
@@ -136,7 +119,7 @@
        return
      }
      const processDefinition = processDefinitionList.value.find(
        (item) => item.key == processInstance.processDefinition?.key
        (item: any) => item.key == processInstance.processDefinition?.key
      )
      if (!processDefinition) {
        message.error('重新发起流程失败,原因:流程定义不存在')
@@ -149,108 +132,168 @@
  }
}
/** 选中分类对应的流程定义列表 */
const categoryProcessDefinitionList = computed(() => {
  return processDefinitionList.value.filter((item) => item.category == categoryActive.value)
/** 获取所有流程分类数据 */
const getCategoryList = async () => {
  try {
    // 流程分类
    categoryList.value = await CategoryApi.getCategorySimpleList()
  } finally {
  }
}
/** 获取所有流程定义数据 */
const getProcessDefinitionList = async () => {
  try {
    // 流程定义
    processDefinitionList.value = await DefinitionApi.getProcessDefinitionList({
      suspensionState: 1
})
    // 初始化过滤列表为全部流程定义
    filteredProcessDefinitionList.value = processDefinitionList.value
    // 在获取完所有数据后,设置第一个有效分类为激活状态
    if (availableCategories.value.length > 0 && !categoryActive.value?.code) {
      categoryActive.value = availableCategories.value[0]
    }
  } finally {
  }
}
/** 搜索流程 */
const filteredProcessDefinitionList = ref([]) // 用于存储搜索过滤后的流程定义
const handleQuery = () => {
  if (searchName.value.trim()) {
    // 如果有搜索关键字,进行过滤
    filteredProcessDefinitionList.value = processDefinitionList.value.filter(
      (definition: any) => definition.name.toLowerCase().includes(searchName.value.toLowerCase()) // 假设搜索依据是流程定义的名称
    )
  } else {
    // 如果没有搜索关键字,恢复所有数据
    filteredProcessDefinitionList.value = processDefinitionList.value
  }
}
/** 流程定义的分组 */
const processDefinitionGroup: any = computed(() => {
  if (!processDefinitionList.value?.length) {
    return {}
  }
  const grouped = groupBy(filteredProcessDefinitionList.value, 'category')
  // 按照 categoryList 的顺序重新组织数据
  const orderedGroup = {}
  categoryList.value.forEach((category: any) => {
    if (grouped[category.code]) {
      orderedGroup[category.code] = grouped[category.code]
    }
  })
  return orderedGroup
})
/** 左侧分类切换 */
const handleCategoryClick = (category: any) => {
  categoryActive.value = category
  const categoryRef = proxy.$refs[`category-${category.code}`] // 获取点击分类对应的 DOM 元素
  if (categoryRef?.length) {
    const scrollWrapper = proxy.$refs.scrollWrapper // 获取右侧滚动容器
    const categoryOffsetTop = categoryRef[0].offsetTop
    // 滚动到对应位置
    scrollWrapper.scrollTo({ top: categoryOffsetTop, behavior: 'smooth' })
  }
}
/** 通过分类 code 获取对应的名称 */
const getCategoryName = (categoryCode: string) => {
  return categoryList.value?.find((ctg: any) => ctg.code === categoryCode)?.name
}
// ========== 表单相关 ==========
const fApi = ref<ApiAttrs>()
const detailForm = ref({
  rule: [],
  option: {},
  value: {}
}) // 流程表单详情
const selectProcessDefinition = ref() // 选择的流程定义
// 指定审批人
const bpmnXML = ref(null) // BPMN 数据
const startUserSelectTasks = ref([]) // 发起人需要选择审批人的用户任务列表
const startUserSelectAssignees = ref({}) // 发起人选择审批人的数据
const startUserSelectAssigneesFormRef = ref() // 发起人选择审批人的表单 Ref
const startUserSelectAssigneesFormRules = ref({}) // 发起人选择审批人的表单 Rules
const userList = ref<any[]>([]) // 用户列表
const processDefinitionDetailRef = ref()
/** 处理选择流程的按钮操作 **/
const handleSelect = async (row, formVariables) => {
const handleSelect = async (row, formVariables?) => {
  // 设置选择的流程
  selectProcessDefinition.value = row
  // 重置指定审批人
  startUserSelectTasks.value = []
  startUserSelectAssignees.value = {}
  startUserSelectAssigneesFormRules.value = {}
  // 情况一:流程表单
  if (row.formType == 10) {
    // 设置表单
    setConfAndFields2(detailForm, row.formConf, row.formFields, formVariables)
    // 加载流程图
    const processDefinitionDetail = await DefinitionApi.getProcessDefinition(row.id)
    if (processDefinitionDetail) {
      bpmnXML.value = processDefinitionDetail.bpmnXml
      startUserSelectTasks.value = processDefinitionDetail.startUserSelectTasks
      // 设置指定审批人
      if (startUserSelectTasks.value?.length > 0) {
        detailForm.value.rule.push({
          type: 'startUserSelect',
          props: {
            title: '指定审批人'
  // 初始化流程定义详情
  await nextTick()
  processDefinitionDetailRef.value?.initProcessInfo(row, formVariables)
          }
/** 处理滚动事件,和左侧分类联动 */
const handleScroll = (e: any) => {
  // 直接使用事件对象获取滚动位置
  const scrollTop = e.scrollTop
  // 获取所有分类区域的位置信息
  const categoryPositions = categoryList.value
    .map((category: CategoryVO) => {
      const categoryRef = proxy.$refs[`category-${category.code}`]
      if (categoryRef?.[0]) {
        return {
          code: category.code,
          offsetTop: categoryRef[0].offsetTop,
          height: categoryRef[0].offsetHeight
        }
      }
      return null
        })
        // 设置校验规则
        for (const userTask of startUserSelectTasks.value) {
          startUserSelectAssignees.value[userTask.id] = []
          startUserSelectAssigneesFormRules.value[userTask.id] = [
            { required: true, message: '请选择审批人', trigger: 'blur' }
          ]
        }
        // 加载用户列表
        userList.value = await UserApi.getSimpleUserList()
      }
    }
    // 情况二:业务表单
  } else if (row.formCustomCreatePath) {
    await push({
      path: row.formCustomCreatePath
    })
    // 这里暂时无需加载流程图,因为跳出到另外个 Tab;
    .filter(Boolean)
  // 查找当前滚动位置对应的分类
  let currentCategory = categoryPositions[0]
  for (const position of categoryPositions) {
    // 为了更好的用户体验,可以添加一个缓冲区域(比如 50px)
    if (scrollTop >= position.offsetTop - 50) {
      currentCategory = position
    } else {
      break
  }
}
/** 提交按钮 */
const submitForm = async (formData) => {
  if (!fApi.value || !selectProcessDefinition.value) {
    return
  // 更新当前 active 的分类
  if (currentCategory && categoryActive.value.code !== currentCategory.code) {
    categoryActive.value = categoryList.value.find(
      (c: CategoryVO) => c.code === currentCategory.code
    )
  }
  // 如果有指定审批人,需要校验
  if (startUserSelectTasks.value?.length > 0) {
    await startUserSelectAssigneesFormRef.value.validate()
  }
  // 提交请求
  fApi.value.btn.loading(true)
  try {
    await ProcessInstanceApi.createProcessInstance({
      processDefinitionId: selectProcessDefinition.value.id,
      variables: formData,
      startUserSelectAssignees: startUserSelectAssignees.value
    })
    // 提示
    message.success('发起流程成功')
    // 跳转回去
    delView(unref(currentRoute))
    await push({
      name: 'BpmProcessInstanceMy'
    })
  } finally {
    fApi.value.btn.loading(false)
/** 过滤出有流程的分类列表。目的:只展示有流程的分类 */
const availableCategories = computed(() => {
  if (!categoryList.value?.length || !processDefinitionGroup.value) {
    return []
  }
}
  // 获取所有有流程的分类代码
  const availableCategoryCodes = Object.keys(processDefinitionGroup.value)
  // 过滤出有流程的分类
  return categoryList.value.filter((category: CategoryVO) =>
    availableCategoryCodes.includes(category.code)
  )
})
/** 初始化 */
onMounted(() => {
  getList()
})
</script>
<style lang="scss" scoped>
.process-definition-container::before {
  content: '';
  border-left: 1px solid #e6e6e6;
  position: absolute;
  left: 20.8%;
  height: 100%;
}
:deep() {
  .definition-item-card {
    .el-card__body {
      padding: 14px;
    }
  }
}
</style>
src/views/bpm/processInstance/create/index_old.vue
对比新文件
@@ -0,0 +1,266 @@
<template>
  <!-- 第一步,通过流程定义的列表,选择对应的流程 -->
  <ContentWrap v-if="!selectProcessDefinition" v-loading="loading">
    <el-tabs tab-position="left" v-model="categoryActive">
      <el-tab-pane
        :label="category.name"
        :name="category.code"
        :key="category.code"
        v-for="category in categoryList"
      >
        <el-row :gutter="20">
          <el-col
            :lg="6"
            :sm="12"
            :xs="24"
            v-for="definition in categoryProcessDefinitionList"
            :key="definition.id"
          >
            <el-card
              shadow="hover"
              class="mb-20px cursor-pointer"
              @click="handleSelect(definition)"
            >
              <template #default>
                <div class="flex">
                  <el-image :src="definition.icon" class="w-32px h-32px" />
                  <el-text class="!ml-10px" size="large">{{ definition.name }}</el-text>
                </div>
              </template>
            </el-card>
          </el-col>
        </el-row>
      </el-tab-pane>
    </el-tabs>
  </ContentWrap>
  <!-- 第二步,填写表单,进行流程的提交 -->
  <ContentWrap v-else>
    <el-card class="box-card">
      <div class="clearfix">
        <span class="el-icon-document">申请信息【{{ selectProcessDefinition.name }}】</span>
        <el-button style="float: right" type="primary" @click="selectProcessDefinition = undefined">
          <Icon icon="ep:delete" /> 选择其它流程
        </el-button>
      </div>
      <el-col :span="16" :offset="6" style="margin-top: 20px">
        <form-create
          :rule="detailForm.rule"
          v-model:api="fApi"
          v-model="detailForm.value"
          :option="detailForm.option"
          @submit="submitForm"
        >
          <template #type-startUserSelect>
            <el-col :span="24">
              <el-card class="mb-10px">
                <template #header>指定审批人</template>
                <el-form
                  :model="startUserSelectAssignees"
                  :rules="startUserSelectAssigneesFormRules"
                  ref="startUserSelectAssigneesFormRef"
                >
                  <el-form-item
                    v-for="userTask in startUserSelectTasks"
                    :key="userTask.id"
                    :label="`任务【${userTask.name}】`"
                    :prop="userTask.id"
                  >
                    <el-select
                      v-model="startUserSelectAssignees[userTask.id]"
                      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>
              </el-card>
            </el-col>
          </template>
        </form-create>
      </el-col>
    </el-card>
    <!-- 流程图预览 -->
    <ProcessInstanceBpmnViewer :bpmn-xml="bpmnXML as any" />
  </ContentWrap>
</template>
<script lang="ts" setup>
import * as DefinitionApi from '@/api/bpm/definition'
import * as ProcessInstanceApi from '@/api/bpm/processInstance'
import { decodeFields, setConfAndFields2 } from '@/utils/formCreate'
import type { ApiAttrs } from '@form-create/element-ui/types/config'
import ProcessInstanceBpmnViewer from '../detail/ProcessInstanceBpmnViewer.vue'
import { CategoryApi } from '@/api/bpm/category'
import { useTagsViewStore } from '@/store/modules/tagsView'
import * as UserApi from '@/api/system/user'
defineOptions({ name: 'BpmProcessInstanceCreate' })
const route = useRoute() // 路由
const { push, currentRoute } = useRouter() // 路由
const message = useMessage() // 消息
const { delView } = useTagsViewStore() // 视图操作
const processInstanceId = route.query.processInstanceId
const loading = ref(true) // 加载中
const categoryList = ref([]) // 分类的列表
const categoryActive = ref('') // 选中的分类
const processDefinitionList = ref([]) // 流程定义的列表
/** 查询列表 */
const getList = async () => {
  loading.value = true
  try {
    // 流程分类
    categoryList.value = await CategoryApi.getCategorySimpleList()
    if (categoryList.value.length > 0) {
      categoryActive.value = categoryList.value[0].code
    }
    // 流程定义
    processDefinitionList.value = await DefinitionApi.getProcessDefinitionList({
      suspensionState: 1
    })
    // 如果 processInstanceId 非空,说明是重新发起
    if (processInstanceId?.length > 0) {
      const processInstance = await ProcessInstanceApi.getProcessInstance(processInstanceId)
      if (!processInstance) {
        message.error('重新发起流程失败,原因:流程实例不存在')
        return
      }
      const processDefinition = processDefinitionList.value.find(
        (item) => item.key == processInstance.processDefinition?.key
      )
      if (!processDefinition) {
        message.error('重新发起流程失败,原因:流程定义不存在')
        return
      }
      await handleSelect(processDefinition, processInstance.formVariables)
    }
  } finally {
    loading.value = false
  }
}
/** 选中分类对应的流程定义列表 */
const categoryProcessDefinitionList = computed(() => {
  return processDefinitionList.value.filter((item) => item.category == categoryActive.value)
})
// ========== 表单相关 ==========
const fApi = ref<ApiAttrs>()
const detailForm = ref({
  rule: [],
  option: {},
  value: {}
}) // 流程表单详情
const selectProcessDefinition = ref() // 选择的流程定义
// 指定审批人
const bpmnXML = ref(null) // BPMN 数据
const startUserSelectTasks = ref([]) // 发起人需要选择审批人的用户任务列表
const startUserSelectAssignees = ref({}) // 发起人选择审批人的数据
const startUserSelectAssigneesFormRef = ref() // 发起人选择审批人的表单 Ref
const startUserSelectAssigneesFormRules = ref({}) // 发起人选择审批人的表单 Rules
const userList = ref<any[]>([]) // 用户列表
/** 处理选择流程的按钮操作 **/
const handleSelect = async (row, formVariables) => {
  // 设置选择的流程
  selectProcessDefinition.value = row
  // 重置指定审批人
  startUserSelectTasks.value = []
  startUserSelectAssignees.value = {}
  startUserSelectAssigneesFormRules.value = {}
  // 情况一:流程表单
  if (row.formType == 10) {
    // 设置表单
    // 注意:需要从 formVariables 中,移除不在 row.formFields 的值。
    // 原因是:后端返回的 formVariables 里面,会有一些非表单的信息。例如说,某个流程节点的审批人。
    //        这样,就可能导致一个流程被审批不通过后,重新发起时,会直接后端报错!!!
    const allowedFields = decodeFields(row.formFields).map((fieldObj: any) => fieldObj.field)
    for (const key in formVariables) {
      if (!allowedFields.includes(key)) {
        delete formVariables[key]
      }
    }
    setConfAndFields2(detailForm, row.formConf, row.formFields, formVariables)
    // 加载流程图
    const processDefinitionDetail = await DefinitionApi.getProcessDefinition(row.id)
    if (processDefinitionDetail) {
      bpmnXML.value = processDefinitionDetail.bpmnXml
      startUserSelectTasks.value = processDefinitionDetail.startUserSelectTasks
      // 设置指定审批人
      if (startUserSelectTasks.value?.length > 0) {
        detailForm.value.rule.push({
          type: 'startUserSelect',
          props: {
            title: '指定审批人'
          }
        })
        // 设置校验规则
        for (const userTask of startUserSelectTasks.value) {
          startUserSelectAssignees.value[userTask.id] = []
          startUserSelectAssigneesFormRules.value[userTask.id] = [
            { required: true, message: '请选择审批人', trigger: 'blur' }
          ]
        }
        // 加载用户列表
        userList.value = await UserApi.getSimpleUserList()
      }
    }
    // 情况二:业务表单
  } else if (row.formCustomCreatePath) {
    await push({
      path: row.formCustomCreatePath
    })
    // 这里暂时无需加载流程图,因为跳出到另外个 Tab;
  }
}
/** 提交按钮 */
const submitForm = async (formData) => {
  if (!fApi.value || !selectProcessDefinition.value) {
    return
  }
  // 如果有指定审批人,需要校验
  if (startUserSelectTasks.value?.length > 0) {
    await startUserSelectAssigneesFormRef.value.validate()
  }
  // 提交请求
  fApi.value.btn.loading(true)
  try {
    await ProcessInstanceApi.createProcessInstance({
      processDefinitionId: selectProcessDefinition.value.id,
      variables: formData,
      startUserSelectAssignees: startUserSelectAssignees.value
    })
    // 提示
    message.success('发起流程成功')
    // 跳转回去
    delView(unref(currentRoute))
    await push({
      name: 'BpmProcessInstanceMy'
    })
  } finally {
    fApi.value.btn.loading(false)
  }
}
/** 初始化 */
onMounted(() => {
  getList()
})
</script>
src/views/bpm/processInstance/detail/ProcessInstanceBpmnViewer.vue
@@ -1,54 +1,61 @@
<template>
  <el-card v-loading="loading" class="box-card">
    <template #header>
      <span class="el-icon-picture-outline">流程图</span>
    </template>
    <MyProcessViewer
      key="designer"
      :activityData="activityList"
      :prefix="bpmnControlForm.prefix"
      :processInstanceData="processInstance"
      :taskData="tasks"
      :value="bpmnXml"
      v-bind="bpmnControlForm"
    />
    <MyProcessViewer key="designer" :xml="view.bpmnXml" :view="view" class="process-viewer" />
  </el-card>
</template>
<script lang="ts" setup>
import { propTypes } from '@/utils/propTypes'
import { MyProcessViewer } from '@/components/bpmnProcessDesigner/package'
import * as ActivityApi from '@/api/bpm/activity'
defineOptions({ name: 'BpmProcessInstanceBpmnViewer' })
const props = defineProps({
  loading: propTypes.bool, // 是否加载中
  id: propTypes.string, // 流程实例的编号
  processInstance: propTypes.any, // 流程实例的信息
  tasks: propTypes.array, // 流程任务的数组
  bpmnXml: propTypes.string // BPMN XML
  loading: propTypes.bool.def(false), // 是否加载中
  bpmnXml: propTypes.string, // BPMN XML
  modelView: propTypes.object
})
const bpmnControlForm = ref({
  prefix: 'flowable'
})
const activityList = ref([]) // 任务列表
const view = ref({
  bpmnXml: ''
}) // BPMN 流程图数据
/** 只有 loading 完成时,才去加载流程列表 */
watch(
  () => props.loading,
  async (value) => {
    if (value && props.id) {
      activityList.value = await ActivityApi.getActivityList({
        processInstanceId: props.id
      })
  () => props.modelView,
  async (newModelView) => {
    // 加载最新
    if (newModelView) {
      //@ts-ignore
      view.value = newModelView
    }
  }
)
/** 监听 bpmnXml */
watch(
  () => props.bpmnXml,
  (value) => {
    view.value.bpmnXml = value
  }
)
</script>
<style>
<style lang="scss" scoped>
.box-card {
  height: 100%;
  width: 100%;
  margin-bottom: 20px;
  margin-bottom: 0;
  :deep(.el-card__body) {
    height: 100%;
    padding: 0;
  }
  :deep(.process-viewer) {
    height: 100% !important;
    min-height: 100%;
    width: 100%;
    overflow: auto;
  }
}
</style>
src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue
对比新文件
@@ -0,0 +1,889 @@
<template>
  <div
    class="h-50px bottom-10 text-14px flex items-center color-#32373c dark:color-#fff font-bold btn-container"
  >
    <!-- 【通过】按钮 -->
    <el-popover
      :visible="popOverVisible.approve"
      placement="top-end"
      :width="420"
      trigger="click"
      v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.APPROVE)"
    >
      <template #reference>
        <el-button plain type="success" @click="openPopover('approve')">
          <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"
          class="mb-auto"
          ref="formRef"
          :model="genericForm"
          :rules="genericRule"
          label-width="100px"
        >
          <el-card v-if="runningTask?.formId > 0" class="mb-15px !-mt-10px">
            <template #header>
              <span class="el-icon-picture-outline"> 填写表单【{{ runningTask?.formName }}】 </span>
            </template>
            <form-create
              v-model="approveForm.value"
              v-model:api="approveFormFApi"
              :option="approveForm.option"
              :rule="approveForm.rule"
            />
          </el-card>
          <el-form-item label="审批意见" prop="reason">
            <el-input
              v-model="genericForm.reason"
              placeholder="请输入审批意见"
              type="textarea"
              :rows="4"
            />
          </el-form-item>
          <el-form-item>
            <el-button :disabled="formLoading" type="success" @click="handleAudit(true)">
              {{ getButtonDisplayName(OperationButtonType.APPROVE) }}
            </el-button>
            <el-button @click="popOverVisible.approve = false"> 取消 </el-button>
          </el-form-item>
        </el-form>
      </div>
    </el-popover>
    <!-- 【拒绝】按钮 -->
    <el-popover
      :visible="popOverVisible.reject"
      placement="top-end"
      :width="420"
      trigger="click"
      v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.REJECT)"
    >
      <template #reference>
        <el-button class="mr-20px" plain type="danger" @click="openPopover('reject')">
          <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"
          class="mb-auto"
          ref="formRef"
          :model="genericForm"
          :rules="genericRule"
          label-width="100px"
        >
          <el-card v-if="runningTask?.formId > 0" class="mb-15px !-mt-10px">
            <template #header>
              <span class="el-icon-picture-outline"> 填写表单【{{ runningTask?.formName }}】 </span>
            </template>
            <form-create
              v-model="approveForm.value"
              v-model:api="approveFormFApi"
              :option="approveForm.option"
              :rule="approveForm.rule"
            />
          </el-card>
          <el-form-item label="审批意见" prop="reason">
            <el-input
              v-model="genericForm.reason"
              placeholder="请输入审批意见"
              type="textarea"
              :rows="4"
            />
          </el-form-item>
          <el-form-item>
            <el-button :disabled="formLoading" type="danger" @click="handleAudit(false)">
              {{ getButtonDisplayName(OperationButtonType.REJECT) }}
            </el-button>
            <el-button @click="popOverVisible.reject = false"> 取消 </el-button>
          </el-form-item>
        </el-form>
      </div>
    </el-popover>
    <!-- 【抄送】按钮 -->
    <el-popover
      :visible="popOverVisible.copy"
      placement="top-start"
      :width="420"
      trigger="click"
      v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.COPY)"
    >
      <template #reference>
        <div @click="openPopover('copy')" class="hover-bg-gray-100 rounded-xl p-6px">
          <Icon :size="14" icon="svg-icon:send" />&nbsp;
          {{ getButtonDisplayName(OperationButtonType.COPY) }}
        </div>
      </template>
      <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
        <el-form
          label-position="top"
          class="mb-auto"
          ref="formRef"
          :model="genericForm"
          :rules="genericRule"
          label-width="100px"
        >
          <el-form-item label="抄送人" prop="copyUserIds">
            <el-select
              v-model="genericForm.copyUserIds"
              clearable
              style="width: 100%"
              multiple
              placeholder="请选择抄送人"
            >
              <el-option
                v-for="item in userOptions"
                :key="item.id"
                :label="item.nickname"
                :value="item.id"
              />
            </el-select>
          </el-form-item>
          <el-form-item label="抄送意见" prop="copyReason">
            <el-input
              v-model="genericForm.copyReason"
              clearable
              placeholder="请输入抄送意见"
              type="textarea"
              :rows="3"
            />
          </el-form-item>
          <el-form-item>
            <el-button :disabled="formLoading" type="primary" @click="handleCopy">
              {{ getButtonDisplayName(OperationButtonType.COPY) }}
            </el-button>
            <el-button @click="popOverVisible.copy = false"> 取消 </el-button>
          </el-form-item>
        </el-form>
      </div>
    </el-popover>
    <!-- 【转交】按钮 -->
    <el-popover
      :visible="popOverVisible.transfer"
      placement="top-start"
      :width="420"
      trigger="click"
      v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.TRANSFER)"
    >
      <template #reference>
        <div @click="openPopover('transfer')" class="hover-bg-gray-100 rounded-xl p-6px">
          <Icon :size="14" icon="fa:share-square-o" />&nbsp;
          {{ getButtonDisplayName(OperationButtonType.TRANSFER) }}
        </div>
      </template>
      <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
        <el-form
          label-position="top"
          class="mb-auto"
          ref="formRef"
          :model="genericForm"
          :rules="genericRule"
          label-width="100px"
        >
          <el-form-item label="新审批人" prop="assigneeUserId">
            <el-select v-model="genericForm.assigneeUserId" clearable 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 label="审批意见" prop="reason">
            <el-input
              v-model="genericForm.reason"
              clearable
              placeholder="请输入审批意见"
              type="textarea"
              :rows="3"
            />
          </el-form-item>
          <el-form-item>
            <el-button :disabled="formLoading" type="primary" @click="handleTransfer()">
              {{ getButtonDisplayName(OperationButtonType.TRANSFER) }}
            </el-button>
            <el-button @click="popOverVisible.transfer = false"> 取消 </el-button>
          </el-form-item>
        </el-form>
      </div>
    </el-popover>
    <!-- 【委派】按钮 -->
    <el-popover
      :visible="popOverVisible.delegate"
      placement="top-start"
      :width="420"
      trigger="click"
      v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.DELEGATE)"
    >
      <template #reference>
        <div @click="openPopover('delegate')" class="hover-bg-gray-100 rounded-xl p-6px">
          <Icon :size="14" icon="ep:position" />&nbsp;
          {{ getButtonDisplayName(OperationButtonType.DELEGATE) }}
        </div>
      </template>
      <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
        <el-form
          label-position="top"
          class="mb-auto"
          ref="formRef"
          :model="genericForm"
          :rules="genericRule"
          label-width="100px"
        >
          <el-form-item label="接收人" prop="delegateUserId">
            <el-select v-model="genericForm.delegateUserId" clearable 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 label="审批意见" prop="reason">
            <el-input
              v-model="genericForm.reason"
              clearable
              placeholder="请输入审批意见"
              type="textarea"
              :rows="3"
            />
          </el-form-item>
          <el-form-item>
            <el-button :disabled="formLoading" type="primary" @click="handleDelegate()">
              {{ getButtonDisplayName(OperationButtonType.DELEGATE) }}
            </el-button>
            <el-button @click="popOverVisible.delegate = false"> 取消 </el-button>
          </el-form-item>
        </el-form>
      </div>
    </el-popover>
    <!-- 【加签】按钮 当前任务审批人为A,向前加签选了一个C,则需要C先审批,然后再是A审批,向后加签B,A审批完,需要B再审批完,才算完成这个任务节点 -->
    <el-popover
      :visible="popOverVisible.addSign"
      placement="top-start"
      :width="420"
      trigger="click"
      v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.ADD_SIGN)"
    >
      <template #reference>
        <div @click="openPopover('addSign')" class="hover-bg-gray-100 rounded-xl p-6px">
          <Icon :size="14" icon="ep:plus" />&nbsp;
          {{ getButtonDisplayName(OperationButtonType.ADD_SIGN) }}
        </div>
      </template>
      <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
        <el-form
          label-position="top"
          class="mb-auto"
          ref="formRef"
          :model="genericForm"
          :rules="genericRule"
          label-width="100px"
        >
          <el-form-item label="加签处理人" prop="addSignUserIds">
            <el-select v-model="genericForm.addSignUserIds" multiple clearable 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 label="审批意见" prop="reason">
            <el-input
              v-model="genericForm.reason"
              clearable
              placeholder="请输入审批意见"
              type="textarea"
              :rows="3"
            />
          </el-form-item>
          <el-form-item>
            <el-button :disabled="formLoading" type="primary" @click="handlerAddSign('before')">
              向前{{ getButtonDisplayName(OperationButtonType.ADD_SIGN) }}
            </el-button>
            <el-button :disabled="formLoading" type="primary" @click="handlerAddSign('after')">
              向后{{ getButtonDisplayName(OperationButtonType.ADD_SIGN) }}
            </el-button>
            <el-button @click="popOverVisible.addSign = false"> 取消 </el-button>
          </el-form-item>
        </el-form>
      </div>
    </el-popover>
    <!-- 【减签】按钮 -->
    <el-popover
      :visible="popOverVisible.deleteSign"
      placement="top-start"
      :width="420"
      trigger="click"
      v-if="runningTask?.children.length > 0"
    >
      <template #reference>
        <div @click="openPopover('deleteSign')" class="hover-bg-gray-100 rounded-xl p-6px">
          <Icon :size="14" icon="ep:semi-select" />&nbsp; 减签
        </div>
      </template>
      <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
        <el-form
          label-position="top"
          class="mb-auto"
          ref="formRef"
          :model="genericForm"
          :rules="genericRule"
          label-width="100px"
        >
          <el-form-item label="减签人员" prop="deleteSignTaskId">
            <el-select v-model="genericForm.deleteSignTaskId" clearable style="width: 100%">
              <el-option
                v-for="item in runningTask.children"
                :key="item.id"
                :label="getDeleteSignUserLabel(item)"
                :value="item.id"
              />
            </el-select>
          </el-form-item>
          <el-form-item label="审批意见" prop="reason">
            <el-input
              v-model="genericForm.reason"
              clearable
              placeholder="请输入审批意见"
              type="textarea"
              :rows="3"
            />
          </el-form-item>
          <el-form-item>
            <el-button :disabled="formLoading" type="primary" @click="handlerDeleteSign()">
              减签
            </el-button>
            <el-button @click="popOverVisible.deleteSign = false"> 取消 </el-button>
          </el-form-item>
        </el-form>
      </div>
    </el-popover>
    <!-- 【退回】按钮 -->
    <el-popover
      :visible="popOverVisible.return"
      placement="top-start"
      :width="420"
      trigger="click"
      v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.RETURN)"
    >
      <template #reference>
        <div @click="openReturnPopover" class="hover-bg-gray-100 rounded-xl p-6px">
          <Icon :size="14" icon="ep:back" />&nbsp;
          {{ getButtonDisplayName(OperationButtonType.RETURN) }}
        </div>
      </template>
      <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
        <el-form
          label-position="top"
          class="mb-auto"
          ref="formRef"
          :model="genericForm"
          :rules="genericRule"
          label-width="100px"
        >
          <el-form-item label="退回节点" prop="targetTaskDefinitionKey">
            <el-select v-model="genericForm.targetTaskDefinitionKey" clearable style="width: 100%">
              <el-option
                v-for="item in returnList"
                :key="item.taskDefinitionKey"
                :label="item.name"
                :value="item.taskDefinitionKey"
              />
            </el-select>
          </el-form-item>
          <el-form-item label="退回理由" prop="returnReason">
            <el-input
              v-model="genericForm.returnReason"
              clearable
              placeholder="请输入退回理由"
              type="textarea"
              :rows="3"
            />
          </el-form-item>
          <el-form-item>
            <el-button :disabled="formLoading" type="primary" @click="handleReturn()">
              {{ getButtonDisplayName(OperationButtonType.RETURN) }}
            </el-button>
            <el-button @click="popOverVisible.return = false"> 取消 </el-button>
          </el-form-item>
        </el-form>
      </div>
    </el-popover>
    <!--【取消】按钮 这个对应发起人的取消, 只有发起人可以取消 -->
    <el-popover
      :visible="popOverVisible.cancel"
      placement="top-start"
      :width="420"
      trigger="click"
      v-if="
        userId === processInstance?.startUser?.id && !isEndProcessStatus(processInstance?.status)
      "
    >
      <template #reference>
        <div @click="openPopover('cancel')" class="hover-bg-gray-100 rounded-xl p-6px">
          <Icon :size="14" icon="fa:mail-reply" />&nbsp; 取消
        </div>
      </template>
      <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
        <el-form
          label-position="top"
          class="mb-auto"
          ref="formRef"
          :model="genericForm"
          :rules="genericRule"
          label-width="100px"
        >
          <el-form-item label="取消理由" prop="cancelReason">
            <span class="text-#878c93 text-12px">&nbsp; 取消后,该审批流程将自动结束</span>
            <el-input
              v-model="genericForm.cancelReason"
              clearable
              placeholder="请输入取消理由"
              type="textarea"
              :rows="3"
            />
          </el-form-item>
          <el-form-item>
            <el-button :disabled="formLoading" type="primary" @click="handleCancel()">
              取消
            </el-button>
            <el-button @click="popOverVisible.cancel = false"> 取消 </el-button>
          </el-form-item>
        </el-form>
      </div>
    </el-popover>
    <!-- 【再次提交】 按钮-->
    <div
      @click="handleReCreate()"
      class="hover-bg-gray-100 rounded-xl p-6px"
      v-if="
        userId === processInstance?.startUser?.id &&
        isEndProcessStatus(processInstance?.status) &&
        processDefinition?.formType === 10
      "
    >
      <Icon :size="14" icon="ep:refresh" />&nbsp; 再次提交
    </div>
  </div>
</template>
<script lang="ts" setup>
import { useUserStoreWithOut } from '@/store/modules/user'
import { setConfAndFields2 } from '@/utils/formCreate'
import * as TaskApi from '@/api/bpm/task'
import * as ProcessInstanceApi from '@/api/bpm/processInstance'
import { propTypes } from '@/utils/propTypes'
import {
  OperationButtonType,
  OPERATION_BUTTON_NAME
} from '@/components/SimpleProcessDesignerV2/src/consts'
import { BpmProcessInstanceStatus } from '@/utils/constants'
defineOptions({ name: 'ProcessInstanceBtnContainer' })
const router = useRouter() // 路由
const message = useMessage() // 消息弹窗
const { proxy } = getCurrentInstance() as any
const userId = useUserStoreWithOut().getUser.id // 当前登录的编号
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const props = defineProps({
  processInstance: propTypes.object, // 流程实例信息
  processDefinition: propTypes.object, // 流程定义信息
  userOptions: propTypes.any
})
const formLoading = ref(false) // 表单加载中
const popOverVisible = ref({
  approve: false,
  reject: false,
  transfer: false,
  delegate: false,
  addSign: false,
  return: false,
  copy: false,
  cancel: false,
  deleteSign: false
}) // 气泡卡是否展示
const returnList = ref([] as any) // 退回节点
// ========== 审批信息 ==========
const runningTask = ref<any>() // 运行中的任务
const genericForm = ref<any>({}) // 通用表单
const approveForm = ref<any>({}) // 审批通过时,额外的补充信息
const approveFormFApi = ref<any>({}) // approveForms 的 fAPi
const formRef = ref()
const genericRule = reactive({
  reason: [{ required: true, message: '审批意见不能为空', trigger: 'blur' }],
  returnReason: [{ required: true, message: '退回理由不能为空', trigger: 'blur' }],
  cancelReason: [{ required: true, message: '取消理由不能为空', trigger: 'blur' }],
  copyUserIds: [{ required: true, message: '抄送人不能为空', trigger: 'change' }],
  assigneeUserId: [{ required: true, message: '新审批人不能为空', trigger: 'change' }],
  delegateUserId: [{ required: true, message: '接收人不能为空', trigger: 'change' }],
  addSignUserIds: [{ required: true, message: '加签处理人不能为空', trigger: 'change' }],
  deleteSignTaskId: [{ required: true, message: '减签人员不能为空', trigger: 'change' }],
  targetTaskDefinitionKey: [{ required: true, message: '退回节点不能为空', trigger: 'change' }]
}) // 表单校验规则
/** 监听 approveFormFApis,实现它对应的 form-create 初始化后,隐藏掉对应的表单提交按钮 */
watch(
  () => approveFormFApi.value,
  (val) => {
    val?.btn?.show(false)
    val?.resetBtn?.show(false)
  },
  {
    deep: true
  }
)
/** 弹出退回气泡卡 */
const openReturnPopover = async () => {
  returnList.value = await TaskApi.getTaskListByReturn(runningTask.value.id)
  if (returnList.value.length === 0) {
    message.warning('当前没有可退回的节点')
    return
  }
  await openPopover('return')
}
/** 弹出气泡卡 */
const openPopover = async (type: string) => {
  Object.keys(popOverVisible.value).forEach((item) => {
    popOverVisible.value[item] = item === type
  })
  await nextTick()
  formRef.value.resetFields()
}
/** 处理审批通过和不通过的操作 */
const handleAudit = async (pass: boolean) => {
  formLoading.value = true
  try {
    const genericFormRef = proxy.$refs['formRef']
    // 1.2 校验表单
    const elForm = unref(genericFormRef)
    if (!elForm) return
    const valid = await elForm.validate()
    if (!valid) return
    // 2.1 提交审批
    const data = {
      id: runningTask.value.id,
      reason: genericForm.value.reason
    }
    if (pass) {
      // 审批通过,并且有额外的 approveForm 表单,需要校验 + 拼接到 data 表单里提交
      const formCreateApi = approveFormFApi.value
      if (Object.keys(formCreateApi)?.length > 0) {
        await formCreateApi.validate()
        // @ts-ignore
        data.variables = approveForm.value.value
      }
      await TaskApi.approveTask(data)
      popOverVisible.value.approve = false
      message.success('审批通过成功')
    } else {
      await TaskApi.rejectTask(data)
      popOverVisible.value.reject = false
      message.success('审批不通过成功')
    }
    // 2.2 加载最新数据
    reload()
  } finally {
    formLoading.value = false
  }
}
/** 处理抄送 */
const handleCopy = async () => {
  formLoading.value = true
  try {
    const copyFormRef = proxy.$refs['formRef']
    // 1. 校验表单
    const elForm = unref(copyFormRef)
    if (!elForm) return
    const valid = await elForm.validate()
    if (!valid) return
    // 2. 提交抄送
    const data = {
      id: runningTask.value.id,
      reason: genericForm.value.copyReason,
      copyUserIds: genericForm.value.copyUserIds
    }
    await TaskApi.copyTask(data)
    popOverVisible.value.copy = false
    message.success('操作成功')
  } finally {
    formLoading.value = false
  }
}
/** 处理转交 */
const handleTransfer = async () => {
  formLoading.value = true
  try {
    const transferFormRef = proxy.$refs['formRef']
    // 1.1 校验表单
    const elForm = unref(transferFormRef)
    if (!elForm) return
    const valid = await elForm.validate()
    if (!valid) return
    // 1.2 提交转交
    const data = {
      id: runningTask.value.id,
      reason: genericForm.value.reason,
      assigneeUserId: genericForm.value.assigneeUserId
    }
    await TaskApi.transferTask(data)
    popOverVisible.value.transfer = false
    message.success('操作成功')
    // 2. 加载最新数据
    reload()
  } finally {
    formLoading.value = false
  }
}
/** 处理委派 */
const handleDelegate = async () => {
  formLoading.value = true
  try {
    const deletegateFormRef = proxy.$refs['formRef']
    // 1.1 校验表单
    const elForm = unref(deletegateFormRef)
    if (!elForm) return
    const valid = await elForm.validate()
    if (!valid) return
    // 1.2 处理委派
    const data = {
      id: runningTask.value.id,
      reason: genericForm.value.reason,
      delegateUserId: genericForm.value.delegateUserId
    }
    await TaskApi.delegateTask(data)
    popOverVisible.value.delegate = false
    message.success('操作成功')
    // 2. 加载最新数据
    reload()
  } finally {
    formLoading.value = false
  }
}
/** 处理加签 */
const handlerAddSign = async (type: string) => {
  formLoading.value = true
  try {
    const transferFormRef = proxy.$refs['formRef']
    // 1.1 校验表单
    const elForm = unref(transferFormRef)
    if (!elForm) return
    const valid = await elForm.validate()
    if (!valid) return
    // 1.2 提交加签
    const data = {
      id: runningTask.value.id,
      type,
      reason: genericForm.value.reason,
      userIds: genericForm.value.addSignUserIds
    }
    await TaskApi.signCreateTask(data)
    message.success('操作成功')
    popOverVisible.value.addSign = false
    // 2 加载最新数据
    reload()
  } finally {
    formLoading.value = false
  }
}
/** 处理退回 */
const handleReturn = async () => {
  formLoading.value = true
  try {
    const returnFormRef = proxy.$refs['formRef']
    // 1.1 校验表单
    const elForm = unref(returnFormRef)
    if (!elForm) return
    const valid = await elForm.validate()
    if (!valid) return
    // 1.2 提交退回
    const data = {
      id: runningTask.value.id,
      reason: genericForm.value.returnReason,
      targetTaskDefinitionKey: genericForm.value.targetTaskDefinitionKey
    }
    await TaskApi.returnTask(data)
    popOverVisible.value.return = false
    message.success('操作成功')
    // 2 重新加载数据
    reload()
  } finally {
    formLoading.value = false
  }
}
/** 处理取消 */
const handleCancel = async () => {
  formLoading.value = true
  try {
    const cancelFormRef = proxy.$refs['formRef']
    // 1.1 校验表单
    const elForm = unref(cancelFormRef)
    if (!elForm) return
    const valid = await elForm.validate()
    if (!valid) return
    // 1.2 提交取消
    await ProcessInstanceApi.cancelProcessInstanceByStartUser(
      props.processInstance.id,
      genericForm.value.cancelReason
    )
    popOverVisible.value.return = false
    message.success('操作成功')
    // 2 重新加载数据
    reload()
  } finally {
    formLoading.value = false
  }
}
/** 处理再次提交 */
const handleReCreate = async () => {
  // 跳转发起流程界面
  await router.push({
    name: 'BpmProcessInstanceCreate',
    query: { processInstanceId: props.processInstance?.id }
  })
}
/** 获取减签人员标签 */
const getDeleteSignUserLabel = (task: any): string => {
  const deptName = task?.assigneeUser?.deptName || task?.ownerUser?.deptName
  const nickname = task?.assigneeUser?.nickname || task?.ownerUser?.nickname
  return `${nickname} ( 所属部门:${deptName} )`
}
/** 处理减签 */
const handlerDeleteSign = async () => {
  formLoading.value = true
  try {
    const deleteFormRef = proxy.$refs['formRef']
    // 1.1 校验表单
    const elForm = unref(deleteFormRef)
    if (!elForm) return
    const valid = await elForm.validate()
    if (!valid) return
    // 1.2 提交减签
    const data = {
      id: genericForm.value.deleteSignTaskId,
      reason: genericForm.value.reason
    }
    await TaskApi.signDeleteTask(data)
    message.success('减签成功')
    popOverVisible.value.deleteSign = false
    // 2 加载最新数据
    reload()
  } finally {
    formLoading.value = false
  }
}
/** 重新加载数据 */
const reload = () => {
  emit('success')
}
/** 任务是否为处理中状态 */
const isHandleTaskStatus = () => {
  let canHandle = false
  if (TaskApi.TaskStatusEnum.RUNNING === runningTask.value?.status) {
    canHandle = true
  }
  return canHandle
}
/** 流程状态是否为结束状态 */
const isEndProcessStatus = (status: number) => {
  let isEndStatus = false
  if (
    BpmProcessInstanceStatus.APPROVE === status ||
    BpmProcessInstanceStatus.REJECT === status ||
    BpmProcessInstanceStatus.CANCEL === status
  ) {
    isEndStatus = true
  }
  return isEndStatus
}
/** 是否显示按钮 */
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
}
const loadTodoTask = (task: any) => {
  genericForm.value = {}
  approveForm.value = {}
  approveFormFApi.value = {}
  runningTask.value = task
  // 处理 approve 表单.
  if (task && task.formId && task.formConf) {
    const tempApproveForm = {}
    setConfAndFields2(tempApproveForm, task.formConf, task.formFields, task.formVariables)
    approveForm.value = tempApproveForm
  } else {
    approveForm.value = {} // 占位,避免为空
  }
}
defineExpose({ loadTodoTask })
</script>
<style lang="scss" scoped>
:deep(.el-affix--fixed) {
  background-color: var(--el-bg-color);
}
.btn-container {
  > div {
    display: flex;
    margin: 0 8px;
    cursor: pointer;
    align-items: center;
    &:hover {
      color: #6db5ff;
    }
  }
}
</style>
src/views/bpm/processInstance/detail/ProcessInstanceSimpleViewer.vue
对比新文件
@@ -0,0 +1,168 @@
<template>
  <div v-loading="loading" class="process-viewer-container">
    <SimpleProcessViewer
      :flow-node="simpleModel"
      :tasks="tasks"
      :process-instance="processInstance"
      class="process-viewer"
    />
  </div>
</template>
<script lang="ts" setup>
import { propTypes } from '@/utils/propTypes'
import { TaskStatusEnum } from '@/api/bpm/task'
import { SimpleFlowNode, NodeType } from '@/components/SimpleProcessDesignerV2/src/consts'
import { SimpleProcessViewer } from '@/components/SimpleProcessDesignerV2/src/'
defineOptions({ name: 'BpmProcessInstanceSimpleViewer' })
const props = defineProps({
  loading: propTypes.bool.def(false), // 是否加载中
  modelView: propTypes.object,
  simpleJson: propTypes.string // Simple 模型结构数据 (json 格式)
})
const simpleModel = ref()
// 用户任务
const tasks = ref([])
// 流程实例
const processInstance = ref()
/** 监控模型视图 包括任务列表、进行中的活动节点编号等 */
watch(
  () => props.modelView,
  async (newModelView) => {
    if (newModelView) {
      tasks.value = newModelView.tasks
      processInstance.value = newModelView.processInstance
      // 已经拒绝的活动节点编号集合,只包括 UserTask
      const rejectedTaskActivityIds: string[] = newModelView.rejectedTaskActivityIds
      // 进行中的活动节点编号集合, 只包括 UserTask
      const unfinishedTaskActivityIds: string[] = newModelView.unfinishedTaskActivityIds
      // 已经完成的活动节点编号集合, 包括 UserTask、Gateway 等
      const finishedActivityIds: string[] = newModelView.finishedTaskActivityIds
      // 已经完成的连线节点编号集合,只包括 SequenceFlow
      const finishedSequenceFlowActivityIds: string[] = newModelView.finishedSequenceFlowActivityIds
      setSimpleModelNodeTaskStatus(
        newModelView.simpleModel,
        newModelView.processInstance.status,
        rejectedTaskActivityIds,
        unfinishedTaskActivityIds,
        finishedActivityIds,
        finishedSequenceFlowActivityIds
      )
      simpleModel.value = newModelView.simpleModel
    }
  }
)
/** 监控模型结构数据 */
watch(
  () => props.simpleJson,
  async (value) => {
    if (value) {
      simpleModel.value = JSON.parse(value)
    }
  }
)
const setSimpleModelNodeTaskStatus = (
  simpleModel: SimpleFlowNode | undefined,
  processStatus: number,
  rejectedTaskActivityIds: string[],
  unfinishedTaskActivityIds: string[],
  finishedActivityIds: string[],
  finishedSequenceFlowActivityIds: string[]
) => {
  if (!simpleModel) {
    return
  }
  // 结束节点
  if (simpleModel.type === NodeType.END_EVENT_NODE) {
    if (finishedActivityIds.includes(simpleModel.id)) {
      simpleModel.activityStatus = processStatus
    } else {
      simpleModel.activityStatus = TaskStatusEnum.NOT_START
    }
    return
  }
  // 审批节点
  if (
    simpleModel.type === NodeType.START_USER_NODE ||
    simpleModel.type === NodeType.USER_TASK_NODE
  ) {
    simpleModel.activityStatus = TaskStatusEnum.NOT_START
    if (rejectedTaskActivityIds.includes(simpleModel.id)) {
      simpleModel.activityStatus = TaskStatusEnum.REJECT
    } else if (unfinishedTaskActivityIds.includes(simpleModel.id)) {
      simpleModel.activityStatus = TaskStatusEnum.RUNNING
    } else if (finishedActivityIds.includes(simpleModel.id)) {
      simpleModel.activityStatus = TaskStatusEnum.APPROVE
    }
    // TODO 是不是还缺一个 cancel 的状态
  }
  // 抄送节点
  if (simpleModel.type === NodeType.COPY_TASK_NODE) {
    // 抄送节点 只有通过和未执行状态
    if (finishedActivityIds.includes(simpleModel.id)) {
      simpleModel.activityStatus = TaskStatusEnum.APPROVE
    } else {
      simpleModel.activityStatus = TaskStatusEnum.NOT_START
    }
  }
  // 条件节点 对应 SequenceFlow
  if (simpleModel.type === NodeType.CONDITION_NODE) {
    // 条件节点。只有通过和未执行状态
    if (finishedSequenceFlowActivityIds.includes(simpleModel.id)) {
      simpleModel.activityStatus = TaskStatusEnum.APPROVE
    } else {
      simpleModel.activityStatus = TaskStatusEnum.NOT_START
    }
  }
  // 网关节点
  if (
    simpleModel.type === NodeType.CONDITION_BRANCH_NODE ||
    simpleModel.type === NodeType.PARALLEL_BRANCH_NODE ||
    simpleModel.type === NodeType.INCLUSIVE_BRANCH_NODE
  ) {
    // 网关节点。只有通过和未执行状态
    if (finishedActivityIds.includes(simpleModel.id)) {
      simpleModel.activityStatus = TaskStatusEnum.APPROVE
    } else {
      simpleModel.activityStatus = TaskStatusEnum.NOT_START
    }
    simpleModel.conditionNodes?.forEach((node) => {
      setSimpleModelNodeTaskStatus(
        node,
        processStatus,
        rejectedTaskActivityIds,
        unfinishedTaskActivityIds,
        finishedActivityIds,
        finishedSequenceFlowActivityIds
      )
    })
  }
  setSimpleModelNodeTaskStatus(
    simpleModel.childNode,
    processStatus,
    rejectedTaskActivityIds,
    unfinishedTaskActivityIds,
    finishedActivityIds,
    finishedSequenceFlowActivityIds
  )
}
</script>
<style lang="scss" scoped>
.process-viewer-container {
  height: 100%;
  width: 100%;
  :deep(.process-viewer) {
    height: 100% !important;
    min-height: 100%;
    width: 100%;
    overflow: auto;
  }
}
</style>
src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue
@@ -1,85 +1,50 @@
<template>
  <el-card v-loading="loading" class="box-card">
    <template #header>
      <span class="el-icon-picture-outline">审批记录</span>
  <el-table :data="tasks" border header-cell-class-name="table-header-gray">
    <el-table-column label="审批节点" prop="name" min-width="120" align="center" />
    <el-table-column label="审批人" min-width="100" align="center">
      <template #default="scope">
        {{ scope.row.assigneeUser?.nickname || scope.row.ownerUser?.nickname }}
    </template>
    <el-col :offset="3" :span="17">
      <div class="block">
        <el-timeline>
          <el-timeline-item
            v-if="processInstance.endTime"
            :type="getProcessInstanceTimelineItemType(processInstance)"
          >
            <p style="font-weight: 700">
              结束流程:在 {{ formatDate(processInstance?.endTime) }} 结束
              <dict-tag
                :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS"
                :value="processInstance.status"
    </el-table-column>
    <el-table-column
      :formatter="dateFormatter"
      align="center"
      label="开始时间"
      prop="createTime"
      min-width="140"
              />
            </p>
          </el-timeline-item>
          <el-timeline-item
            v-for="(item, index) in tasks"
            :key="index"
            :type="getTaskTimelineItemType(item)"
          >
            <p style="font-weight: 700">
              审批任务:{{ item.name }}
              <dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="item.status" />
              <el-button
                class="ml-10px"
                v-if="!isEmpty(item.children)"
                @click="openChildrenTask(item)"
                size="small"
              >
                <Icon icon="ep:memo" /> 子任务
              </el-button>
    <el-table-column
      :formatter="dateFormatter"
      align="center"
      label="结束时间"
      prop="endTime"
      min-width="140"
    />
    <el-table-column align="center" label="审批状态" prop="status" min-width="90">
      <template #default="scope">
        <dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="scope.row.status" />
      </template>
    </el-table-column>
    <el-table-column align="center" label="审批建议" prop="reason" min-width="200">
      <template #default="scope">
        {{ scope.row.reason }}
              <el-button
                class="ml-10px"
                size="small"
                v-if="item.formId > 0"
                @click="handleFormDetail(item)"
          v-if="scope.row.formId > 0"
          @click="handleFormDetail(scope.row)"
              >
                <Icon icon="ep:document" /> 查看表单
              </el-button>
            </p>
            <el-card :body-style="{ padding: '10px' }">
              <label v-if="item.assigneeUser" style="margin-right: 30px; font-weight: normal">
                审批人:{{ item.assigneeUser.nickname }}
                <el-tag size="small" type="info">{{ item.assigneeUser.deptName }}</el-tag>
              </label>
              <label v-if="item.createTime" style="font-weight: normal">创建时间:</label>
              <label style="font-weight: normal; color: #8a909c">
                {{ formatDate(item?.createTime) }}
              </label>
              <label v-if="item.endTime" style="margin-left: 30px; font-weight: normal">
                审批时间:
              </label>
              <label v-if="item.endTime" style="font-weight: normal; color: #8a909c">
                {{ formatDate(item?.endTime) }}
              </label>
              <label v-if="item.durationInMillis" style="margin-left: 30px; font-weight: normal">
                耗时:
              </label>
              <label v-if="item.durationInMillis" style="font-weight: normal; color: #8a909c">
                {{ formatPast2(item?.durationInMillis) }}
              </label>
              <p v-if="item.reason"> 审批建议:{{ item.reason }} </p>
            </el-card>
          </el-timeline-item>
          <el-timeline-item type="success">
            <p style="font-weight: 700">
              发起流程:【{{ processInstance.startUser?.nickname }}】在
              {{ formatDate(processInstance?.startTime) }} 发起【 {{ processInstance.name }} 】流程
            </p>
          </el-timeline-item>
        </el-timeline>
      </div>
    </el-col>
  </el-card>
      </template>
    </el-table-column>
    <el-table-column align="center" label="耗时" prop="durationInMillis" min-width="100">
      <template #default="scope">
        {{ formatPast2(scope.row.durationInMillis) }}
      </template>
    </el-table-column>
  </el-table>
  <!-- 弹窗:子任务  -->
  <TaskSignList ref="taskSignListRef" @success="refresh" />
  <!-- 弹窗:表单 -->
  <Dialog title="表单详情" v-model="taskFormVisible" width="600">
    <form-create
@@ -91,61 +56,20 @@
  </Dialog>
</template>
<script lang="ts" setup>
import { formatDate, formatPast2 } from '@/utils/formatTime'
import { dateFormatter, formatPast2 } from '@/utils/formatTime'
import { propTypes } from '@/utils/propTypes'
import { DICT_TYPE } from '@/utils/dict'
import { isEmpty } from '@/utils/is'
import TaskSignList from './dialog/TaskSignList.vue'
import type { ApiAttrs } from '@form-create/element-ui/types/config'
import { setConfAndFields2 } from '@/utils/formCreate'
import * as TaskApi from '@/api/bpm/task'
defineOptions({ name: 'BpmProcessInstanceTaskList' })
defineProps({
  loading: propTypes.bool, // 是否加载中
  processInstance: propTypes.object, // 流程实例
  tasks: propTypes.arrayOf(propTypes.object) // 流程任务的数组
const props = defineProps({
  loading: propTypes.bool.def(false), // 是否加载中
  id: propTypes.string // 流程实例的编号
})
/** 获得流程实例对应的颜色 */
const getProcessInstanceTimelineItemType = (item: any) => {
  if (item.status === 2) {
    return 'success'
  }
  if (item.status === 3) {
    return 'danger'
  }
  if (item.status === 4) {
    return 'warning'
  }
  return ''
}
/** 获得任务对应的颜色 */
const getTaskTimelineItemType = (item: any) => {
  if ([0, 1, 6, 7].includes(item.status)) {
    return 'primary'
  }
  if (item.status === 2) {
    return 'success'
  }
  if (item.status === 3) {
    return 'danger'
  }
  if (item.status === 4) {
    return 'info'
  }
  if (item.status === 5) {
    return 'warning'
  }
  return ''
}
/** 子任务 */
const taskSignListRef = ref()
const openChildrenTask = (item: any) => {
  taskSignListRef.value.open(item)
}
const tasks = ref([]) // 流程任务的数组
/** 查看表单 */
const fApi = ref<ApiAttrs>() // form-create 的 API 操作类
@@ -155,7 +79,7 @@
  value: {}
}) // 流程任务的表单详情
const taskFormVisible = ref(false)
const handleFormDetail = async (row) => {
const handleFormDetail = async (row: any) => {
  // 设置表单
  setConfAndFields2(taskForm, row.formConf, row.formFields, row.formVariables)
  // 弹窗打开
@@ -167,9 +91,13 @@
  fApi.value?.fapi?.disabled(true)
}
/** 刷新数据 */
const emit = defineEmits(['refresh']) // 定义 success 事件,用于操作成功后的回调
const refresh = () => {
  emit('refresh')
/** 只有 loading 完成时,才去加载流程列表 */
watch(
  () => props.loading,
  async (value) => {
    if (value) {
      tasks.value = await TaskApi.getTaskListByProcessInstanceId(props.id)
}
  }
)
</script>
src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue
对比新文件
@@ -0,0 +1,292 @@
<!-- 审批详情的右侧:审批流 -->
<template>
  <el-timeline class="pt-20px">
    <!-- 遍历每个审批节点 -->
    <el-timeline-item
      v-for="(activity, index) in activityNodes"
      :key="index"
      size="large"
      :icon="getApprovalNodeIcon(activity.status, activity.nodeType)"
      :color="getApprovalNodeColor(activity.status)"
    >
      <template #dot>
        <div
          class="position-absolute left--10px top--6px rounded-full border border-solid border-#dedede w-30px h-30px flex justify-center items-center bg-#3f73f7 p-5px"
        >
          <img class="w-full h-full" :src="getApprovalNodeImg(activity.nodeType)" alt="" />
          <div
            v-if="showStatusIcon"
            class="position-absolute top-17px left-17px rounded-full flex items-center p-1px border-2 border-white border-solid"
            :style="{ backgroundColor: getApprovalNodeColor(activity.status) }"
          >
            <el-icon :size="11" color="#fff">
              <component :is="getApprovalNodeIcon(activity.status, activity.nodeType)" />
            </el-icon>
          </div>
        </div>
      </template>
      <div class="flex flex-col items-start gap2" :id="`activity-task-${activity.id}`">
        <!-- 第一行:节点名称、时间 -->
        <div class="flex w-full">
          <div class="font-bold"> {{ activity.name }}</div>
          <!-- 信息:时间 -->
          <div
            v-if="activity.status !== TaskStatusEnum.NOT_START"
            class="text-#a5a5a5 text-13px mt-1 ml-auto"
          >
            {{ getApprovalNodeTime(activity) }}
          </div>
        </div>
        <!-- 需要自定义选择审批人 -->
        <div
          class="flex flex-wrap gap2 items-center"
          v-if="
            isEmpty(activity.tasks) &&
            isEmpty(activity.candidateUsers) &&
            CandidateStrategy.START_USER_SELECT === activity.candidateStrategy
          "
        >
          <!--  && activity.nodeType === NodeType.USER_TASK_NODE -->
          <el-tooltip content="添加用户" placement="left">
            <el-button
              class="!px-6px"
              @click="handleSelectUser(activity.id, customApproveUsers[activity.id])"
            >
              <img class="w-18px text-#ccc" src="@/assets/svgs/bpm/add-user.svg" alt="" />
            </el-button>
          </el-tooltip>
          <div
            v-for="(user, idx1) in customApproveUsers[activity.id]"
            :key="idx1"
            class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative"
          >
            <el-avatar class="!m-5px" :size="28" v-if="user.avatar" :src="user.avatar" />
            <el-avatar class="!m-5px" :size="28" v-else>
              {{ user.nickname.substring(0, 1) }}
            </el-avatar>
            {{ user.nickname }}
          </div>
        </div>
        <div v-else class="flex items-center flex-wrap mt-1 gap2">
          <!-- 情况一:遍历每个审批节点下的【进行中】task 任务 -->
          <div v-for="(task, idx) in activity.tasks" :key="idx" class="flex flex-col pr-2 gap2">
            <div
              class="position-relative flex flex-wrap gap2"
              v-if="task.assigneeUser || task.ownerUser"
            >
              <!-- 信息:头像昵称 -->
              <div
                class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative"
              >
                <template v-if="task.assigneeUser?.avatar || task.assigneeUser?.nickname">
                  <el-avatar
                    class="!m-5px"
                    :size="28"
                    v-if="task.assigneeUser?.avatar"
                    :src="task.assigneeUser?.avatar"
                  />
                  <el-avatar class="!m-5px" :size="28" v-else>
                    {{ task.assigneeUser?.nickname.substring(0, 1) }}
                  </el-avatar>
                  {{ task.assigneeUser?.nickname }}
                </template>
                <template v-else-if="task.ownerUser?.avatar || task.ownerUser?.nickname">
                  <el-avatar
                    class="!m-5px"
                    :size="28"
                    v-if="task.ownerUser?.avatar"
                    :src="task.ownerUser?.avatar"
                  />
                  <el-avatar class="!m-5px" :size="28" v-else>
                    {{ task.ownerUser?.nickname.substring(0, 1) }}
                  </el-avatar>
                  {{ task.ownerUser?.nickname }}
                </template>
                <!-- 信息:任务 ICON -->
                <div
                  v-if="showStatusIcon && onlyStatusIconShow.includes(task.status)"
                  class="position-absolute top-19px left-23px rounded-full flex items-center p-1px border-2 border-white border-solid"
                  :style="{ backgroundColor: statusIconMap2[task.status]?.color }"
                >
                  <Icon :size="11" :icon="statusIconMap2[task.status]?.icon" color="#FFFFFF" />
                </div>
              </div>
            </div>
            <teleport defer :to="`#activity-task-${activity.id}`">
              <div
                v-if="
                  task.reason &&
                  [NodeType.USER_TASK_NODE, NodeType.END_EVENT_NODE].includes(activity.nodeType)
                "
                class="text-#a5a5a5 text-13px mt-1 w-full bg-#f8f8fa p2 rounded-md"
              >
                审批意见:{{ task.reason }}
              </div>
            </teleport>
          </div>
          <!-- 情况二:遍历每个审批节点下的【候选的】task 任务。例如说,1)依次审批,2)未来的审批任务等 -->
          <div
            v-for="(user, idx1) in activity.candidateUsers"
            :key="idx1"
            class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative"
          >
            <el-avatar class="!m-5px" :size="28" v-if="user.avatar" :src="user.avatar" />
            <el-avatar class="!m-5px" :size="28" v-else>
              {{ user.nickname.substring(0, 1) }}
            </el-avatar>
            {{ user.nickname }}
            <!-- 信息:任务 ICON -->
            <div
              v-if="showStatusIcon"
              class="position-absolute top-20px left-24px rounded-full flex items-center p-1px border-2 border-white border-solid"
              :style="{ backgroundColor: statusIconMap2['-1']?.color }"
            >
              <Icon :size="11" :icon="statusIconMap2['-1']?.icon" color="#FFFFFF" />
            </div>
          </div>
        </div>
      </div>
    </el-timeline-item>
  </el-timeline>
  <!-- 用户选择弹窗 -->
  <UserSelectForm ref="userSelectFormRef" @confirm="handleUserSelectConfirm" />
</template>
<script lang="ts" setup>
import { formatDate } from '@/utils/formatTime'
import * as ProcessInstanceApi from '@/api/bpm/processInstance'
import { TaskStatusEnum } from '@/api/bpm/task'
import { NodeType, CandidateStrategy } from '@/components/SimpleProcessDesignerV2/src/consts'
import { isEmpty } from '@/utils/is'
import { Check, Close, Loading, Clock, Minus, Delete } from '@element-plus/icons-vue'
import starterSvg from '@/assets/svgs/bpm/starter.svg'
import auditorSvg from '@/assets/svgs/bpm/auditor.svg'
import copySvg from '@/assets/svgs/bpm/copy.svg'
import conditionSvg from '@/assets/svgs/bpm/condition.svg'
import parallelSvg from '@/assets/svgs/bpm/parallel.svg'
import finishSvg from '@/assets/svgs/bpm/finish.svg'
defineOptions({ name: 'BpmProcessInstanceTimeline' })
withDefaults(
  defineProps<{
    activityNodes: ProcessInstanceApi.ApprovalNodeInfo[] // 审批节点信息
    showStatusIcon?: boolean // 是否显示头像右下角状态图标
  }>(),
  {
    showStatusIcon: true // 默认值为 true
  }
)
// 审批节点
const statusIconMap2 = {
  // 未开始
  '-1': { color: '#909398', icon: 'ep-clock' },
  // 待审批
  '0': { color: '#00b32a', icon: 'ep:loading' },
  // 审批中
  '1': { color: '#448ef7', icon: 'ep:loading' },
  // 审批通过
  '2': { color: '#00b32a', icon: 'ep:circle-check-filled' },
  // 审批不通过
  '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 statusIconMap = {
  // 审批未开始
  '-1': { color: '#909398', icon: Clock },
  '0': { color: '#00b32a', 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 nodeTypeSvgMap = {
  // 结束节点
  [NodeType.END_EVENT_NODE]: { color: '#909398', svg: finishSvg },
  // 发起人节点
  [NodeType.START_USER_NODE]: { color: '#909398', svg: starterSvg },
  // 审批人节点
  [NodeType.USER_TASK_NODE]: { color: '#ff943e', svg: auditorSvg },
  // 抄送人节点
  [NodeType.COPY_TASK_NODE]: { color: '#3296fb', svg: copySvg },
  // 条件分支节点
  [NodeType.CONDITION_NODE]: { color: '#14bb83', svg: conditionSvg },
  // 并行分支节点
  [NodeType.PARALLEL_BRANCH_NODE]: { color: '#14bb83', svg: parallelSvg }
}
// 只有只有状态是 -1、0、1 才展示头像右小角状态小icon
const onlyStatusIconShow = [-1, 0, 1]
// timeline时间线上icon图标
const getApprovalNodeImg = (nodeType: NodeType) => {
  return nodeTypeSvgMap[nodeType]?.svg
}
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 ||
    nodeType === NodeType.END_EVENT_NODE
  ) {
    return statusIconMap[taskStatus]?.icon
  }
}
const getApprovalNodeColor = (taskStatus: number) => {
  return statusIconMap[taskStatus]?.color
}
const getApprovalNodeTime = (node: ProcessInstanceApi.ApprovalNodeInfo) => {
  if (node.nodeType === NodeType.START_USER_NODE && node.startTime) {
    return `${formatDate(node.startTime)}`
  }
  if (node.endTime) {
    return `${formatDate(node.endTime)}`
  }
  if (node.startTime) {
    return `${formatDate(node.startTime)}`
  }
}
// 选择自定义审批人
const userSelectFormRef = ref()
const handleSelectUser = (activityId, selectedList) => {
  userSelectFormRef.value.open(activityId, selectedList)
}
const emit = defineEmits<{
  selectUserConfirm: [id: any, userList: any[]]
}>()
const customApproveUsers: any = ref({}) // key:activityId,value:用户列表
// 选择完成
const handleUserSelectConfirm = (activityId: string, userList: any[]) => {
  customApproveUsers.value[activityId] = userList || []
  emit('selectUserConfirm', activityId, userList)
}
</script>
src/views/bpm/processInstance/detail/dialog/TaskSignDeleteForm.vue
@@ -9,7 +9,7 @@
    >
      <el-form-item label="减签任务" prop="id">
        <el-radio-group v-model="formData.id">
          <el-radio-button v-for="item in childrenTaskList" :key="item.id" :label="item.id">
          <el-radio-button v-for="item in childrenTaskList" :key="item.id" :value="item.id">
            {{ item.name }}
            ({{ item.assigneeUser?.deptName || item.ownerUser?.deptName }} -
            {{ item.assigneeUser?.nickname || item.ownerUser?.nickname }})
src/views/bpm/processInstance/detail/index.vue
@@ -1,96 +1,55 @@
<template>
  <ContentWrap>
    <!-- 审批信息 -->
    <el-card
      v-for="(item, index) in runningTasks"
      :key="index"
      v-loading="processInstanceLoading"
      class="box-card"
    >
      <template #header>
        <span class="el-icon-picture-outline">审批任务【{{ item.name }}】</span>
      </template>
      <el-col :offset="6" :span="16">
        <el-form
          :ref="'form' + index"
          :model="auditForms[index]"
          :rules="auditRule"
          label-width="100px"
        >
          <el-form-item v-if="processInstance && processInstance.name" label="流程名">
            {{ processInstance.name }}
          </el-form-item>
          <el-form-item v-if="processInstance && processInstance.startUser" label="流程发起人">
            {{ processInstance?.startUser.nickname }}
            <el-tag size="small" type="info">{{ processInstance?.startUser.deptName }}</el-tag>
          </el-form-item>
          <el-card v-if="runningTasks[index].formId > 0" class="mb-15px !-mt-10px">
            <template #header>
              <span class="el-icon-picture-outline">
                填写表单【{{ runningTasks[index]?.formName }}】
              </span>
            </template>
            <form-create
              v-model="approveForms[index].value"
              v-model:api="approveFormFApis[index]"
              :option="approveForms[index].option"
              :rule="approveForms[index].rule"
  <ContentWrap :bodyStyle="{ padding: '10px 20px 0' }" class="position-relative">
    <div class="processInstance-wrap-main">
      <el-scrollbar>
        <img
          class="position-absolute right-20px"
          width="150"
          :src="auditIconsMap[processInstance.status]"
          alt=""
            />
          </el-card>
          <el-form-item label="审批建议" prop="reason">
            <el-input
              v-model="auditForms[index].reason"
              placeholder="请输入审批建议"
              type="textarea"
        <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
            v-if="processInstance.status"
            :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS"
            :value="processInstance.status"
            />
          </el-form-item>
          <el-form-item label="抄送人" prop="copyUserIds">
            <el-select v-model="auditForms[index].copyUserIds" multiple placeholder="请选择抄送人">
              <el-option
                v-for="item in userOptions"
                :key="item.id"
                :label="item.nickname"
                :value="item.id"
              />
            </el-select>
          </el-form-item>
        </el-form>
        <div style="margin-bottom: 20px; margin-left: 10%; font-size: 14px">
          <el-button type="success" @click="handleAudit(item, true)">
            <Icon icon="ep:select" />
            通过
          </el-button>
          <el-button type="danger" @click="handleAudit(item, false)">
            <Icon icon="ep:close" />
            不通过
          </el-button>
          <el-button type="primary" @click="openTaskUpdateAssigneeForm(item.id)">
            <Icon icon="ep:edit" />
            转办
          </el-button>
          <el-button type="primary" @click="handleDelegate(item)">
            <Icon icon="ep:position" />
            委派
          </el-button>
          <el-button type="primary" @click="handleSign(item)">
            <Icon icon="ep:plus" />
            加签
          </el-button>
          <el-button type="warning" @click="handleBack(item)">
            <Icon icon="ep:back" />
            回退
          </el-button>
        </div>
      </el-col>
    </el-card>
    <!-- 申请信息 -->
    <el-card v-loading="processInstanceLoading" class="box-card">
      <template #header>
        <span class="el-icon-document">申请信息【{{ processInstance.name }}】</span>
      </template>
        <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"
          >
            <el-avatar
              :size="28"
              v-if="processInstance?.startUser?.avatar"
              :src="processInstance?.startUser?.avatar"
            />
            <el-avatar :size="28" v-else-if="processInstance?.startUser?.nickname">
              {{ processInstance?.startUser?.nickname.substring(0, 1) }}
            </el-avatar>
            {{ processInstance?.startUser?.nickname }}
          </div>
          <div class="text-#878c93"> {{ formatDate(processInstance.startTime) }} 提交 </div>
        </div>
        <el-tabs v-model="activeTab">
          <!-- 表单信息 -->
          <el-tab-pane label="审批详情" name="form">
            <div class="form-scroll-area">
              <el-scrollbar>
                <el-row>
                  <el-col :span="17" 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">
                      <el-col v-if="processDefinition?.formType === 10">
        <form-create
          v-model="detailForm.value"
          v-model:api="fApi"
@@ -99,76 +58,110 @@
        />
      </el-col>
      <!-- 情况二:业务表单 -->
      <div v-if="processInstance?.processDefinition?.formType === 20">
                      <div v-if="processDefinition?.formType === 20">
        <BusinessFormComponent :id="processInstance.businessKey" />
      </div>
    </el-card>
                    </div>
                  </el-col>
                  <el-col :span="7">
                    <!-- 审批记录时间线 -->
                    <ProcessInstanceTimeline :activity-nodes="activityNodes" />
                  </el-col>
                </el-row>
              </el-scrollbar>
            </div>
          </el-tab-pane>
    <!-- 审批记录 -->
    <ProcessInstanceTaskList
      :loading="tasksLoad"
      :process-instance="processInstance"
      :tasks="tasks"
      @refresh="getTaskList"
    />
    <!-- 高亮流程图 -->
    <ProcessInstanceBpmnViewer
      :id="`${id}`"
      :bpmn-xml="bpmnXml"
          <!-- 流程图 -->
          <el-tab-pane label="流程图" name="diagram">
            <div class="form-scroll-area">
              <ProcessInstanceSimpleViewer
                v-show="
                  processDefinition.modelType && processDefinition.modelType === BpmModelType.SIMPLE
                "
      :loading="processInstanceLoading"
      :process-instance="processInstance"
      :tasks="tasks"
                :model-view="processModelView"
    />
              <ProcessInstanceBpmnViewer
                v-show="
                  processDefinition.modelType && processDefinition.modelType === BpmModelType.BPMN
                "
                :loading="processInstanceLoading"
                :model-view="processModelView"
              />
            </div>
          </el-tab-pane>
    <!-- 弹窗:转派审批人 -->
    <TaskTransferForm ref="taskTransferFormRef" @success="getDetail" />
    <!-- 弹窗:回退节点 -->
    <TaskReturnForm ref="taskReturnFormRef" @success="getDetail" />
    <!-- 弹窗:委派,将任务委派给别人处理,处理完成后,会重新回到原审批人手中-->
    <TaskDelegateForm ref="taskDelegateForm" @success="getDetail" />
    <!-- 弹窗:加签,当前任务审批人为A,向前加签选了一个C,则需要C先审批,然后再是A审批,向后加签B,A审批完,需要B再审批完,才算完成这个任务节点 -->
    <TaskSignCreateForm ref="taskSignCreateFormRef" @success="getDetail" />
          <!-- 流转记录 -->
          <el-tab-pane label="流转记录" name="record">
            <div class="form-scroll-area">
              <el-scrollbar>
                <ProcessInstanceTaskList :loading="processInstanceLoading" :id="id" />
              </el-scrollbar>
            </div>
          </el-tab-pane>
          <!-- 流转评论 TODO 待开发 -->
          <el-tab-pane label="流转评论" name="comment" v-if="false">
            <div class="form-scroll-area">
              <el-scrollbar> 流转评论 </el-scrollbar>
            </div>
          </el-tab-pane>
        </el-tabs>
        <div class="b-t-solid border-t-1px border-[var(--el-border-color)]">
          <!-- 操作栏按钮 -->
          <ProcessInstanceOperationButton
            ref="operationButtonRef"
            :process-instance="processInstance"
            :process-definition="processDefinition"
            :userOptions="userOptions"
            @success="refresh"
          />
        </div>
      </el-scrollbar>
    </div>
  </ContentWrap>
</template>
<script lang="ts" setup>
import { useUserStore } from '@/store/modules/user'
import { formatDate } from '@/utils/formatTime'
import { DICT_TYPE } from '@/utils/dict'
import { BpmModelType } from '@/utils/constants'
import { setConfAndFields2 } from '@/utils/formCreate'
import type { ApiAttrs } from '@form-create/element-ui/types/config'
import * as DefinitionApi from '@/api/bpm/definition'
import * as ProcessInstanceApi from '@/api/bpm/processInstance'
import * as TaskApi from '@/api/bpm/task'
import ProcessInstanceBpmnViewer from './ProcessInstanceBpmnViewer.vue'
import ProcessInstanceTaskList from './ProcessInstanceTaskList.vue'
import TaskReturnForm from './dialog/TaskReturnForm.vue'
import TaskDelegateForm from './dialog/TaskDelegateForm.vue'
import TaskTransferForm from './dialog/TaskTransferForm.vue'
import TaskSignCreateForm from './dialog/TaskSignCreateForm.vue'
import { registerComponent } from '@/utils/routerHelper'
import { isEmpty } from '@/utils/is'
import type { ApiAttrs } from '@form-create/element-ui/types/config'
import * as ProcessInstanceApi from '@/api/bpm/processInstance'
import * as UserApi from '@/api/system/user'
import ProcessInstanceBpmnViewer from './ProcessInstanceBpmnViewer.vue'
import ProcessInstanceSimpleViewer from './ProcessInstanceSimpleViewer.vue'
import ProcessInstanceTaskList from './ProcessInstanceTaskList.vue'
import ProcessInstanceOperationButton from './ProcessInstanceOperationButton.vue'
import ProcessInstanceTimeline from './ProcessInstanceTimeline.vue'
import { FieldPermissionType } from '@/components/SimpleProcessDesignerV2/src/consts'
import { TaskStatusEnum } from '@/api/bpm/task'
import runningSvg from '@/assets/svgs/bpm/running.svg'
import approveSvg from '@/assets/svgs/bpm/approve.svg'
import rejectSvg from '@/assets/svgs/bpm/reject.svg'
import cancelSvg from '@/assets/svgs/bpm/cancel.svg'
defineOptions({ name: 'BpmProcessInstanceDetail' })
const { query } = useRoute() // 查询参数
const props = defineProps<{
  id: string // 流程实例的编号
  taskId?: string // 任务编号
  activityId?: string //流程活动编号,用于抄送查看
}>()
const message = useMessage() // 消息弹窗
const { proxy } = getCurrentInstance() as any
const userId = useUserStore().getUser.id // 当前登录的编号
const id = query.id as unknown as string // 流程实例的编号
const processInstanceLoading = ref(false) // 流程实例的加载中
const processInstance = ref<any>({}) // 流程实例
const bpmnXml = ref('') // BPMN XML
const tasksLoad = ref(true) // 任务的加载中
const tasks = ref<any[]>([]) // 任务列表
// ========== 审批信息 ==========
const runningTasks = ref<any[]>([]) // 运行中的任务
const auditForms = ref<any[]>([]) // 审批任务的表单
const auditRule = reactive({
  reason: [{ required: true, message: '审批建议不能为空', trigger: 'blur' }]
})
const approveForms = ref<any[]>([]) // 审批通过时,额外的补充信息
const approveFormFApis = ref<ApiAttrs[]>([]) // approveForms 的 fAPi
const processDefinition = ref<any>({}) // 流程定义
const processModelView = ref<any>({}) // 流程模型视图
const operationButtonRef = ref() // 操作按钮组件 ref
const auditIconsMap = {
  [TaskStatusEnum.RUNNING]: runningSvg,
  [TaskStatusEnum.APPROVE]: approveSvg,
  [TaskStatusEnum.REJECT]: rejectSvg,
  [TaskStatusEnum.CANCEL]: cancelSvg
}
// ========== 申请信息 ==========
const fApi = ref<ApiAttrs>() //
@@ -178,198 +171,123 @@
  value: {}
}) // 流程实例的表单详情
/** 监听 approveFormFApis,实现它对应的 form-create 初始化后,隐藏掉对应的表单提交按钮 */
watch(
  () => approveFormFApis.value,
  (value) => {
    value?.forEach((api) => {
      api.btn.show(false)
      api.resetBtn.show(false)
    })
  },
  {
    deep: true
  }
)
/** 处理审批通过和不通过的操作 */
const handleAudit = async (task, pass) => {
  // 1.1 获得对应表单
  const index = runningTasks.value.indexOf(task)
  const auditFormRef = proxy.$refs['form' + index][0]
  // 1.2 校验表单
  const elForm = unref(auditFormRef)
  if (!elForm) return
  const valid = await elForm.validate()
  if (!valid) return
  // 2.1 提交审批
  const data = {
    id: task.id,
    reason: auditForms.value[index].reason,
    copyUserIds: auditForms.value[index].copyUserIds
  }
  if (pass) {
    // 审批通过,并且有额外的 approveForm 表单,需要校验 + 拼接到 data 表单里提交
    const formCreateApi = approveFormFApis.value[index]
    if (formCreateApi) {
      await formCreateApi.validate()
      data.variables = approveForms.value[index].value
    }
    await TaskApi.approveTask(data)
    message.success('审批通过成功')
  } else {
    await TaskApi.rejectTask(data)
    message.success('审批不通过成功')
  }
  // 2.2 加载最新数据
  getDetail()
}
/** 转派审批人 */
const taskTransferFormRef = ref()
const openTaskUpdateAssigneeForm = (id: string) => {
  taskTransferFormRef.value.open(id)
}
/** 处理审批退回的操作 */
const taskDelegateForm = ref()
const handleDelegate = async (task) => {
  taskDelegateForm.value.open(task.id)
}
/** 处理审批退回的操作 */
const taskReturnFormRef = ref()
const handleBack = async (task: any) => {
  taskReturnFormRef.value.open(task.id)
}
/** 处理审批加签的操作 */
const taskSignCreateFormRef = ref()
const handleSign = async (task: any) => {
  taskSignCreateFormRef.value.open(task.id)
}
/** 获得详情 */
const getDetail = () => {
  // 1. 获得流程实例相关
  getProcessInstance()
  // 2. 获得流程任务列表(审批记录)
  getTaskList()
  getApprovalDetail()
  getProcessModelView()
}
/** 加载流程实例 */
const BusinessFormComponent = ref(null) // 异步组件
const getProcessInstance = async () => {
  try {
const BusinessFormComponent = ref<any>(null) // 异步组件
/** 获取审批详情 */
const getApprovalDetail = async () => {
    processInstanceLoading.value = true
    const data = await ProcessInstanceApi.getProcessInstance(id)
  try {
    const param = {
      processInstanceId: props.id,
      activityId: props.activityId,
      taskId: props.taskId
    }
    const data = await ProcessInstanceApi.getApprovalDetail(param)
    if (!data) {
      message.error('查询不到审批详情信息!')
      return
    }
    if (!data.processDefinition || !data.processInstance) {
      message.error('查询不到流程信息!')
      return
    }
    processInstance.value = data
    processInstance.value = data.processInstance
    processDefinition.value = data.processDefinition
    // 设置表单信息
    const processDefinition = data.processDefinition
    if (processDefinition.formType === 10) {
    if (processDefinition.value.formType === 10) {
      // 获取表单字段权限
      const formFieldsPermission = data.formFieldsPermission
      if (detailForm.value.rule.length > 0) {
        // 避免刷新 form-create 显示不了
        detailForm.value.value = processInstance.value.formVariables
      } else {
      setConfAndFields2(
        detailForm,
        processDefinition.formConf,
        processDefinition.formFields,
        data.formVariables
          processDefinition.value.formConf,
          processDefinition.value.formFields,
          processInstance.value.formVariables
      )
      }
      nextTick().then(() => {
        fApi.value?.btn.show(false)
        fApi.value?.resetBtn.show(false)
        //@ts-ignore
        fApi.value?.disabled(true)
        // 设置表单字段权限
        if (formFieldsPermission) {
          Object.keys(data.formFieldsPermission).forEach((item) => {
            setFieldPermission(item, formFieldsPermission[item])
          })
        }
      })
    } else {
      // 注意:data.processDefinition.formCustomViewPath 是组件的全路径,例如说:/crm/contract/detail/index.vue
      BusinessFormComponent.value = registerComponent(data.processDefinition.formCustomViewPath)
    }
    // 加载流程图
    bpmnXml.value = (
      await DefinitionApi.getProcessDefinition(processDefinition.id as number)
    )?.bpmnXml
    // 获取审批节点,显示 Timeline 的数据
    activityNodes.value = data.activityNodes
    // 获取待办任务显示操作按钮
    operationButtonRef.value?.loadTodoTask(data.todoTask)
  } finally {
    processInstanceLoading.value = false
  }
}
/** 加载任务列表 */
const getTaskList = async () => {
  runningTasks.value = []
  auditForms.value = []
  approveForms.value = []
  approveFormFApis.value = []
  try {
    // 获得未取消的任务
    tasksLoad.value = true
    const data = await TaskApi.getTaskListByProcessInstanceId(id)
    tasks.value = []
    // 1.1 移除已取消的审批
    data.forEach((task) => {
      if (task.status !== 4) {
        tasks.value.push(task)
/** 获取流程模型视图*/
const getProcessModelView = async () => {
  if (BpmModelType.BPMN === processDefinition.value?.modelType) {
    // 重置,解决 BPMN 流程图刷新不会重新渲染问题
    processModelView.value = {
      bpmnXml: ''
      }
    })
    // 1.2 排序,将未完成的排在前面,已完成的排在后面;
    tasks.value.sort((a, b) => {
      // 有已完成的情况,按照完成时间倒序
      if (a.endTime && b.endTime) {
        return b.endTime - a.endTime
      } else if (a.endTime) {
        return 1
      } else if (b.endTime) {
        return -1
        // 都是未完成,按照创建时间倒序
      } else {
        return b.createTime - a.createTime
      }
    })
  const data = await ProcessInstanceApi.getProcessInstanceBpmnModelView(props.id)
  if (data) {
    processModelView.value = data
  }
}
    // 获得需要自己审批的任务
    loadRunningTask(tasks.value)
  } finally {
    tasksLoad.value = false
// 审批节点信息
const activityNodes = ref<ProcessInstanceApi.ApprovalNodeInfo[]>([])
/**
 * 设置表单权限
 */
const setFieldPermission = (field: string, permission: string) => {
  if (permission === FieldPermissionType.READ) {
    //@ts-ignore
    fApi.value?.disabled(true, field)
  }
  if (permission === FieldPermissionType.WRITE) {
    //@ts-ignore
    fApi.value?.disabled(false, field)
  }
  if (permission === FieldPermissionType.NONE) {
    //@ts-ignore
    fApi.value?.hidden(true, field)
  }
}
/**
 * 设置 runningTasks 中的任务
 * 操作成功后刷新
 */
const loadRunningTask = (tasks) => {
  tasks.forEach((task) => {
    if (!isEmpty(task.children)) {
      loadRunningTask(task.children)
const refresh = () => {
  // 重新获取详情
  getDetail()
    }
    // 2.1 只有待处理才需要
    if (task.status !== 1 && task.status !== 6) {
      return
    }
    // 2.2 自己不是处理人
    if (!task.assigneeUser || task.assigneeUser.id !== userId) {
      return
    }
    // 2.3 添加到处理任务
    runningTasks.value.push({ ...task })
    auditForms.value.push({
      reason: '',
      copyUserIds: []
    })
    // 2.4 处理 approve 表单
    if (task.formId && task.formConf) {
      const approveForm = {}
      setConfAndFields2(approveForm, task.formConf, task.formFields, task.formVariables)
      approveForms.value.push(approveForm)
    } else {
      approveForms.value.push({}) // 占位,避免为空
    }
  })
}
/** 当前的Tab */
const activeTab = ref('form')
/** 初始化 */
const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
@@ -379,3 +297,50 @@
  userOptions.value = await UserApi.getSimpleUserList()
})
</script>
<style lang="scss" scoped>
$wrap-padding-height: 20px;
$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) - 35px
  );
  max-height: calc(
    100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px
  );
  overflow: auto;
  .form-scroll-area {
    height: calc(
      100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px -
        $process-header-height - 40px
    );
    max-height: calc(
      100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px -
        $process-header-height - 40px
    );
    overflow: auto;
    display: flex;
    flex-direction: column;
    :deep(.box-card) {
      height: 100%;
      flex: 1;
      .el-card__body {
        height: 100%;
        padding: 0;
      }
    }
  }
}
.form-box {
  :deep(.el-card) {
    border: none;
  }
}
</style>
src/views/bpm/processInstance/detail/index_new.vue
对比新文件
@@ -0,0 +1,319 @@
<template>
  <ContentWrap :bodyStyle="{ padding: '10px 20px' }" class="position-relative">
    <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 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 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>
          </el-tab-pane>
          <!-- 流程图 -->
          <el-tab-pane label="流程图" name="diagram">
            <ProcessInstanceBpmnViewer
              :id="`${id}`"
              :bpmn-xml="bpmnXml"
              :loading="processInstanceLoading"
              :process-instance="processInstance"
              :tasks="tasks"
            />
          </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>
import { formatDate } from '@/utils/formatTime'
import { DICT_TYPE } from '@/utils/dict'
import { setConfAndFields2 } from '@/utils/formCreate'
import type { ApiAttrs } from '@form-create/element-ui/types/config'
import * as DefinitionApi from '@/api/bpm/definition'
import * as ProcessInstanceApi from '@/api/bpm/processInstance'
import * as TaskApi from '@/api/bpm/task'
import ProcessInstanceBpmnViewer from './ProcessInstanceBpmnViewer.vue'
import ProcessInstanceTaskList from './ProcessInstanceTaskList.vue'
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 props = defineProps<{
  id: string // 流程实例的编号
  taskId?: string // 任务编号
  activityId?: string //流程活动编号,用于抄送查看
}>()
const message = useMessage() // 消息弹窗
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[]>([]) // 任务列表
const auditIcons = {
  1: audit1,
  2: audit2,
  3: audit3
}
// ========== 申请信息 ==========
const fApi = ref<ApiAttrs>() //
const detailForm = ref({
  rule: [],
  option: {},
  value: {}
}) // 流程实例的表单详情
/** 获得详情 */
const getDetail = () => {
  // 1. 获得流程实例相关
  getProcessInstance()
  // 2. 获得流程任务列表(审批记录)
  getTaskList()
}
/** 加载流程实例 */
const BusinessFormComponent = ref<any>(null) // 异步组件
const getProcessInstance = async () => {
  try {
    processInstanceLoading.value = true
    const data = await ProcessInstanceApi.getProcessInstance(props.id)
    if (!data) {
      message.error('查询不到流程信息!')
      return
    }
    processInstance.value = data
    // 设置表单信息
    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,
        processDefinition.formFields,
        data.formVariables
      )
      nextTick().then(() => {
        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
      BusinessFormComponent.value = registerComponent(data.processDefinition.formCustomViewPath)
    }
    // 加载流程图
    bpmnXml.value = (await DefinitionApi.getProcessDefinition(processDefinition.id))?.bpmnXml
    console.log(bpmnXml.value)
  } finally {
    processInstanceLoading.value = false
  }
}
/**
 * 设置表单权限
 */
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(props.id)
    tasks.value = []
    // 1.1 移除已取消的审批
    data.forEach((task: any) => {
      if (task.status !== 4) {
        tasks.value.push(task)
      }
    })
    // 1.2 排序,将未完成的排在前面,已完成的排在后面;
    tasks.value.sort((a, b) => {
      // 有已完成的情况,按照完成时间倒序
      if (a.endTime && b.endTime) {
        return b.endTime - a.endTime
      } else if (a.endTime) {
        return 1
      } else if (b.endTime) {
        return -1
        // 都是未完成,按照创建时间倒序
      } else {
        return b.createTime - a.createTime
      }
    })
    // 获得需要自己审批的任务
    operationButtonRef.value?.loadRunningTask(tasks.value)
  } finally {
    tasksLoad.value = false
  }
}
/**
 * 操作成功后刷新
 */
const refresh = () => {
  // 重新获取详情
  getDetail()
  // 刷新审批详情 Timeline
  timelineRef.value?.refresh()
}
/** 当前的Tab */
const activeTab = ref('form')
/** 初始化 */
const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
onMounted(async () => {
  getDetail()
  // 获得用户列表
  userOptions.value = await UserApi.getSimpleUserList()
})
</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;
  }
}
</style>
src/views/bpm/processInstance/index.vue
@@ -9,7 +9,7 @@
      :inline="true"
      label-width="68px"
    >
      <el-form-item label="流程名称" prop="name">
      <el-form-item label="" prop="name">
        <el-input
          v-model="queryParams.name"
          placeholder="请输入流程名称"
@@ -18,21 +18,19 @@
          class="!w-240px"
        />
      </el-form-item>
      <el-form-item label="所属流程" prop="processDefinitionId">
        <el-input
          v-model="queryParams.processDefinitionId"
          placeholder="请输入流程定义的编号"
          clearable
          @keyup.enter="handleQuery"
          class="!w-240px"
        />
      <el-form-item>
        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
      </el-form-item>
      <el-form-item label="流程分类" prop="category">
      <!-- TODO @ tuituji:style 可以使用 unocss -->
      <el-form-item label="" prop="category" :style="{ position: 'absolute', right: '130px' }">
        <!-- TODO @tuituji:应该选择好分类,就触发搜索啦。 -->
        <el-select
          v-model="queryParams.category"
          placeholder="请选择流程分类"
          clearable
          class="!w-240px"
          class="!w-155px"
        >
          <el-option
            v-for="category in categoryList"
@@ -42,12 +40,57 @@
          />
        </el-select>
      </el-form-item>
      <el-form-item label="流程状态" prop="status">
      <!-- 高级筛选 -->
      <!-- TODO @ tuituji:style 可以使用 unocss -->
      <el-form-item :style="{ position: 'absolute', right: '0px' }">
        <el-button v-popover="popoverRef" v-click-outside="onClickOutside" :icon="List">
          高级筛选
        </el-button>
        <el-popover
          ref="popoverRef"
          trigger="click"
          virtual-triggering
          persistent
          :width="400"
          :show-arrow="false"
          placement="bottom-end"
        >
          <el-form-item label="流程发起人" class="bold-label" label-position="top" prop="category">
            <el-select
              v-model="queryParams.category"
              placeholder="请选择流程发起人"
              clearable
              class="!w-390px"
            >
              <el-option
                v-for="category in categoryList"
                :key="category.code"
                :label="category.name"
                :value="category.code"
              />
            </el-select>
          </el-form-item>
          <el-form-item
            label="所属流程"
            class="bold-label"
            label-position="top"
            prop="processDefinitionKey"
          >
            <el-input
              v-model="queryParams.processDefinitionKey"
              placeholder="请输入流程定义的标识"
              clearable
              @keyup.enter="handleQuery"
              class="!w-390px"
            />
          </el-form-item>
          <el-form-item label="流程状态" class="bold-label" label-position="top" prop="status">
        <el-select
          v-model="queryParams.status"
          placeholder="请选择流程状态"
          clearable
          class="!w-240px"
              class="!w-390px"
        >
          <el-option
            v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS)"
@@ -57,7 +100,7 @@
          />
        </el-select>
      </el-form-item>
      <el-form-item label="发起时间" prop="createTime">
          <el-form-item label="发起时间" class="bold-label" label-position="top" prop="createTime">
        <el-date-picker
          v-model="queryParams.createTime"
          value-format="YYYY-MM-DD HH:mm:ss"
@@ -68,17 +111,8 @@
          class="!w-240px"
        />
      </el-form-item>
      <el-form-item>
        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
        <el-button
          type="primary"
          plain
          v-hasPermi="['bpm:process-instance:query']"
          @click="handleCreate(undefined)"
        >
          <Icon icon="ep:plus" class="mr-5px" /> 发起流程
        </el-button>
        </el-popover>
        <!-- TODO @tuituji:这里应该有确认,和取消、清空搜索条件,三个按钮。 -->
      </el-form-item>
    </el-form>
  </ContentWrap>
@@ -94,6 +128,8 @@
        min-width="100"
        fixed="left"
      />
      <!-- TODO @芋艿:摘要 -->
      <!-- TODO @tuituji:流程状态。可见需求文档里  -->
      <el-table-column label="流程状态" prop="status" width="120">
        <template #default="scope">
          <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="scope.row.status" />
@@ -113,7 +149,7 @@
        width="180"
        :formatter="dateFormatter"
      />
      <el-table-column align="center" label="耗时" prop="durationInMillis" width="160">
      <!--<el-table-column align="center" label="耗时" prop="durationInMillis" width="160">
        <template #default="scope">
          {{ scope.row.durationInMillis > 0 ? formatPast2(scope.row.durationInMillis) : '-' }}
        </template>
@@ -125,7 +161,7 @@
          </el-button>
        </template>
      </el-table-column>
      <el-table-column label="流程编号" align="center" prop="id" min-width="320px" />
      -->
      <el-table-column label="操作" align="center" fixed="right" width="180">
        <template #default="scope">
          <el-button
@@ -161,11 +197,13 @@
  </ContentWrap>
</template>
<script lang="ts" setup>
// TODO @tuituji:List 改成 <Icon icon="ep:plus" class="mr-5px" /> 类似这种组件哈。
import { List } from '@element-plus/icons-vue'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter, formatPast2 } from '@/utils/formatTime'
import { dateFormatter } from '@/utils/formatTime'
import { ElMessageBox } from 'element-plus'
import * as ProcessInstanceApi from '@/api/bpm/processInstance'
import { CategoryApi } from '@/api/bpm/category'
import { CategoryApi, CategoryVO } from '@/api/bpm/category'
import { ProcessInstanceVO } from '@/api/bpm/processInstance'
import * as DefinitionApi from '@/api/bpm/definition'
@@ -182,13 +220,13 @@
  pageNo: 1,
  pageSize: 10,
  name: '',
  processDefinitionId: undefined,
  processDefinitionKey: undefined,
  category: undefined,
  status: undefined,
  createTime: []
})
const queryFormRef = ref() // 搜索的表单
const categoryList = ref([]) // 流程分类列表
const categoryList = ref<CategoryVO[]>([]) // 流程分类列表
/** 查询列表 */
const getList = async () => {
@@ -259,6 +297,15 @@
  await getList()
}
// TODO @tuituji:这个 import 是不是没用哈?
import { ClickOutside as vClickOutside } from 'element-plus'
// TODO @tuituji:onClickAdvancedSearch。方法名叫这个,会更好一些哇?打开高级搜索。
const popoverRef = ref()
const onClickOutside = () => {
  unref(popoverRef).popperRef?.delayHide?.()
}
/** 激活时 **/
onActivated(() => {
  getList()
@@ -270,3 +317,8 @@
  categoryList.value = await CategoryApi.getCategorySimpleList()
})
</script>
<style>
.bold-label .el-form-item__label {
  font-weight: bold; /* 将字体加粗 */
}
</style>
src/views/bpm/processInstance/manager/index.vue
@@ -75,9 +75,13 @@
          start-placeholder="开始日期"
          end-placeholder="结束日期"
          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
          class="!w-220px"
          class="!w-240px"
        />
      </el-form-item>
      <el-form-item>
        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
      </el-form-item>
    </el-form>
  </ContentWrap>
src/views/bpm/processListener/ProcessListenerForm.vue
@@ -15,7 +15,7 @@
          <el-radio
            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
            :key="dict.value"
            :label="dict.value"
            :value="dict.value"
          >
            {{ dict.label }}
          </el-radio>
src/views/bpm/simple/SimpleModelDesign.vue
对比新文件
@@ -0,0 +1,19 @@
<template>
  <ContentWrap :bodyStyle="{ padding: '20px 16px' }">
    <SimpleProcessDesigner :model-id="modelId" @success="close" />
  </ContentWrap>
</template>
<script setup lang="ts">
import { SimpleProcessDesigner } from '@/components/SimpleProcessDesignerV2/src/'
defineOptions({
  name: 'SimpleModelDesign'
})
const router = useRouter() // 路由
const { query } = useRoute() // 路由的查询
const modelId = query.modelId as string
const close = () => {
  router.push({ path: '/bpm/manager/model' })
}
</script>
<style lang="scss" scoped></style>
src/views/bpm/simpleWorkflow/index.vue
对比新文件
@@ -0,0 +1,13 @@
<template>
  <SimpleProcessDesigner :model-id="modelId" />
</template>
<script setup lang="ts">
import { SimpleProcessDesigner } from '@/components/SimpleProcessDesignerV2/src/'
defineOptions({
  name: 'SimpleWorkflowDesignEditor'
})
const { query } = useRoute() // 路由的查询
const modelId = query.modelId as string
</script>
<style lang="scss" scoped></style>
src/views/bpm/task/copy/index.vue
@@ -1,11 +1,13 @@
<!-- 工作流 - 抄送我的流程 -->
<template>
  <ContentWrap>
    <!-- 搜索工作栏 -->
    <el-form ref="queryFormRef" :inline="true" class="-mb-15px" label-width="68px">
      <el-form-item label="流程名称" prop="name">
        <el-input
          v-model="queryParams.processInstanceName"
          @keyup.enter="handleQuery"
          class="!w-240px"
          clearable
          placeholder="请输入流程名称"
@@ -39,7 +41,12 @@
  <ContentWrap>
    <el-table v-loading="loading" :data="list">
      <el-table-column align="center" label="流程名" prop="processInstanceName" min-width="180" />
      <el-table-column align="center" label="流程发起人" prop="startUserName" min-width="100" />
      <el-table-column
        align="center"
        label="流程发起人"
        prop="startUser.nickname"
        min-width="100"
      />
      <el-table-column
        :formatter="dateFormatter"
        align="center"
@@ -47,8 +54,11 @@
        prop="processInstanceStartTime"
        width="180"
      />
      <el-table-column align="center" label="抄送任务" prop="taskName" min-width="180" />
      <el-table-column align="center" label="抄送人" prop="creatorName" min-width="100" />
      <el-table-column align="center" label="抄送节点" prop="activityName" min-width="180" />
      <el-table-column align="center" label="抄送人" min-width="100">
        <template #default="scope"> {{ scope.row.createUser?.nickname || '系统' }} </template>
      </el-table-column>
      <el-table-column align="center" label="抄送意见" prop="reason" width="150" />
      <el-table-column
        align="center"
        label="抄送时间"
@@ -105,11 +115,16 @@
/** 处理审批按钮 */
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
  })
}
src/views/bpm/task/done/index.vue
@@ -1,4 +1,5 @@
<template>
  <ContentWrap>
    <!-- 搜索工作栏 -->
    <el-form
@@ -150,7 +151,8 @@
  push({
    name: 'BpmProcessInstanceDetail',
    query: {
      id: row.processInstance.id
      id: row.processInstance.id,
      taskId: row.id
    }
  })
}
src/views/bpm/task/manager/index.vue
@@ -1,4 +1,5 @@
<template>
  <ContentWrap>
    <!-- 搜索工作栏 -->
    <el-form
src/views/bpm/task/todo/index.vue
@@ -1,4 +1,5 @@
<template>
  <ContentWrap>
    <!-- 搜索工作栏 -->
    <el-form
@@ -132,7 +133,8 @@
  push({
    name: 'BpmProcessInstanceDetail',
    query: {
      id: row.processInstance.id
      id: row.processInstance.id,
      taskId: row.id
    }
  })
}
vite.config.ts
@@ -1,8 +1,8 @@
import { resolve } from 'path'
import type {ConfigEnv, UserConfig} from 'vite'
import { loadEnv } from 'vite'
import type { UserConfig, ConfigEnv } from 'vite'
import { createVitePlugins } from './build/vite'
import { include, exclude } from "./build/vite/optimize"
import {exclude, include} from "./build/vite/optimize"
// 当前执行node命令时文件夹的地址(工作目录)
const root = process.cwd()
@@ -43,7 +43,7 @@
    css: {
      preprocessorOptions: {
        scss: {
          additionalData: '@import "./src/styles/variables.scss";',
          additionalData: '@use "@/styles/variables.scss" as *;',
          javascriptEnabled: true
        }
      }
@@ -71,8 +71,15 @@
          drop_debugger: env.VITE_DROP_DEBUGGER === 'true',
          drop_console: env.VITE_DROP_CONSOLE === 'true'
        }
      },
      rollupOptions: {
        output: {
          manualChunks: {
            echarts: ['echarts'] // 将 echarts 单独打包,参考 https://gitee.com/yudaocode/yudao-ui-admin-vue3/issues/IAB1SX 讨论
      }
    },
      },
    },
    optimizeDeps: { include, exclude }
  }
}