瀏覽代碼

Merge branch 'dev' of gitee.com:yudaocode/yudao-ui-admin-vue3 into hotfix-role

Signed-off-by: AhJindeg <ahjindeg@gmail.com>
AhJindeg 1 年之前
父節點
當前提交
b5fa188805
共有 100 個文件被更改,包括 4756 次插入840 次删除
  1. 3 1
      .eslintrc.js
  2. 二進制
      .image/common/bpm-feature.png
  3. 二進制
      .image/common/infra-feature.png
  4. 二進制
      .image/common/system-feature.png
  5. 6 0
      README.md
  6. 44 43
      package.json
  7. 43 0
      src/api/bpm/category/index.ts
  8. 3 2
      src/api/bpm/definition/index.ts
  9. 2 2
      src/api/bpm/form/index.ts
  10. 1 1
      src/api/bpm/leave/index.ts
  11. 42 0
      src/api/bpm/processExpression/index.ts
  12. 26 28
      src/api/bpm/processInstance/index.ts
  13. 40 0
      src/api/bpm/processListener/index.ts
  14. 25 40
      src/api/bpm/task/index.ts
  15. 0 29
      src/api/bpm/taskAssignRule/index.ts
  16. 3 3
      src/api/bpm/userGroup/index.ts
  17. 13 13
      src/api/crm/statistics/customer.ts
  18. 33 0
      src/api/crm/statistics/performance.ts
  19. 60 0
      src/api/crm/statistics/portrait.ts
  20. 5 1
      src/api/infra/apiAccessLog/index.ts
  21. 1 1
      src/api/mall/statistics/member.ts
  22. 0 39
      src/api/report/ureport/index.ts
  23. 1 30
      src/api/system/operatelog/index.ts
  24. 二進制
      src/assets/imgs/avatar.jpg
  25. 3 0
      src/components/ContentWrap/src/ContentWrap.vue
  26. 5 1
      src/components/Crontab/src/Crontab.vue
  27. 3 0
      src/components/DictSelect/index.ts
  28. 47 0
      src/components/DictSelect/src/DictSelect.vue
  29. 2 2
      src/components/Editor/src/Editor.vue
  30. 4 0
      src/components/FormCreate/index.ts
  31. 33 0
      src/components/FormCreate/src/MyFormCreateDesigner.vue
  32. 13 0
      src/components/FormCreate/src/config/index.ts
  33. 124 0
      src/components/FormCreate/src/config/useDictSelectRule.ts
  34. 80 0
      src/components/FormCreate/src/config/useUploadFileRule.ts
  35. 89 0
      src/components/FormCreate/src/config/useUploadImgRule.ts
  36. 84 0
      src/components/FormCreate/src/config/useUploadImgsRule.ts
  37. 93 0
      src/components/FormCreate/src/config/useUserSelectRule.ts
  38. 45 0
      src/components/FormCreate/src/useFormCreateDesigner.ts
  39. 79 0
      src/components/FormCreate/src/utils/index.ts
  40. 2 2
      src/components/ImageViewer/index.ts
  41. 1 1
      src/components/ImageViewer/src/ImageViewer.vue
  42. 1 1
      src/components/ImageViewer/src/types.ts
  43. 2 2
      src/components/OperateLogV2/src/OperateLogV2.vue
  44. 1 1
      src/components/Pagination/index.vue
  45. 1 1
      src/components/RouterSearch/index.vue
  46. 237 0
      src/components/SimpleProcessDesigner/src/addNode.vue
  47. 297 0
      src/components/SimpleProcessDesigner/src/nodeWrap.vue
  48. 165 0
      src/components/SimpleProcessDesigner/src/util.ts
  49. 1292 0
      src/components/SimpleProcessDesigner/theme/workflow.css
  50. 6 5
      src/components/UploadFile/src/UploadFile.vue
  51. 5 6
      src/components/UploadFile/src/UploadImg.vue
  52. 2 1
      src/components/UploadFile/src/UploadImgs.vue
  53. 4 4
      src/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue
  54. 47 30
      src/components/bpmnProcessDesigner/package/designer/ProcessViewer.vue
  55. 10 0
      src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/activitiDescriptor.json
  56. 10 0
      src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/camundaDescriptor.json
  57. 10 0
      src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json
  58. 3 8
      src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue
  59. 17 21
      src/components/bpmnProcessDesigner/package/penal/base/ElementBaseInfo.vue
  60. 220 207
      src/components/bpmnProcessDesigner/package/penal/form/ElementForm.vue
  61. 46 1
      src/components/bpmnProcessDesigner/package/penal/listeners/ElementListeners.vue
  62. 83 0
      src/components/bpmnProcessDesigner/package/penal/listeners/ProcessListenerDialog.vue
  63. 41 1
      src/components/bpmnProcessDesigner/package/penal/listeners/UserTaskListeners.vue
  64. 27 0
      src/components/bpmnProcessDesigner/package/penal/listeners/utilSelf.ts
  65. 31 5
      src/components/bpmnProcessDesigner/package/penal/multi-instance/ElementMultiInstance.vue
  66. 2 1
      src/components/bpmnProcessDesigner/package/penal/task/ElementTask.vue
  67. 68 0
      src/components/bpmnProcessDesigner/package/penal/task/task-components/ProcessExpressionDialog.vue
  68. 193 59
      src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue
  69. 1 0
      src/components/bpmnProcessDesigner/package/utils.ts
  70. 2 3
      src/config/axios/service.ts
  71. 15 3
      src/hooks/web/useCache.ts
  72. 1 2
      src/layout/components/Collapse/src/Collapse.vue
  73. 0 33
      src/layout/components/Menu/src/Menu.vue
  74. 40 49
      src/layout/components/Menu/src/components/useRenderMenuItem.tsx
  75. 7 2
      src/layout/components/Menu/src/components/useRenderMenuTitle.tsx
  76. 1 1
      src/layout/components/TabMenu/src/TabMenu.vue
  77. 43 2
      src/layout/components/UserInfo/src/UserInfo.vue
  78. 98 0
      src/layout/components/UserInfo/src/components/LockDialog.vue
  79. 270 0
      src/layout/components/UserInfo/src/components/LockPage.vue
  80. 10 0
      src/locales/en.ts
  81. 10 0
      src/locales/zh-CN.ts
  82. 19 9
      src/plugins/formCreate/index.ts
  83. 15 26
      src/router/modules/remaining.ts
  84. 2 0
      src/store/index.ts
  85. 2 1
      src/store/modules/app.ts
  86. 48 0
      src/store/modules/lock.ts
  87. 3 2
      src/store/modules/permission.ts
  88. 55 0
      src/store/modules/simpleWorkflow.ts
  89. 2 1
      src/store/modules/tagsView.ts
  90. 4 3
      src/store/modules/user.ts
  91. 6 27
      src/utils/auth.ts
  92. 3 3
      src/utils/constants.ts
  93. 18 0
      src/utils/dateUtil.ts
  94. 9 8
      src/utils/dict.ts
  95. 9 6
      src/utils/formCreate.ts
  96. 5 5
      src/utils/formatTime.ts
  97. 15 1
      src/utils/index.ts
  98. 4 4
      src/views/Login/components/LoginForm.vue
  99. 124 0
      src/views/bpm/category/CategoryForm.vue
  100. 36 56
      src/views/bpm/category/index.vue

+ 3 - 1
.eslintrc.js

@@ -68,6 +68,8 @@ module.exports = defineConfig({
     ],
     'vue/multi-word-component-names': 'off',
     'vue/no-v-html': 'off',
-    'prettier/prettier': 'off' // 芋艿:默认关闭 prettier 的 ESLint 校验,因为我们使用的是 IDE 的 Prettier 插件
+    'prettier/prettier': 'off', // 芋艿:默认关闭 prettier 的 ESLint 校验,因为我们使用的是 IDE 的 Prettier 插件
+    '@unocss/order': 'off', // 芋艿:禁用 unocss 【css】顺序的提示,因为暂时不需要这么严格,警告也有点繁琐
+    '@unocss/order-attributify': 'off' // 芋艿:禁用 unocss 【属性】顺序的提示,因为暂时不需要这么严格,警告也有点繁琐
   }
 })

二進制
.image/common/bpm-feature.png


二進制
.image/common/infra-feature.png


二進制
.image/common/system-feature.png


+ 6 - 0
README.md

@@ -117,6 +117,8 @@
 | 🚀  | 应用管理  | 管理 SSO 单点登录的应用,支持多种 OAuth2 授权方式 |
 | 🚀  | 地区管理  | 展示省份、城市、区镇等城市信息,支持 IP 对应城市      |
 
+![功能图](/.image/common/system-feature.png)
+
 ### 工作流程
 
 |     | 功能    | 描述                                     |
@@ -129,6 +131,8 @@
 | 🚀  | 已办任务  | 查看自己【已】审批的工作任务,未来会支持回退操作               |
 | 🚀  | OA 请假 | 作为业务自定义接入工作流的使用示例,只需创建请求对应的工作流程,即可进行审批 |
 
+![功能图](/.image/common/bpm-feature.png)
+
 ### 支付系统
 
 |     | 功能   | 描述                        |
@@ -164,6 +168,8 @@ ps:核心功能已经实现,正在对接微信小程序中...
 | 🚀  | 日志服务     | 轻量级日志中心,查看远程服务器的日志                           |
 | 🚀  | 单元测试     | 基于 JUnit + Mockito 实现单元测试,保证功能的正确性、代码的质量等    |
 
+![功能图](/.image/common/infra-feature.png)
+
 ### 数据报表
 
 |     | 功能    | 描述                 |

+ 44 - 43
package.json

@@ -1,6 +1,6 @@
 {
   "name": "yudao-ui-admin-vue3",
-  "version": "2.0.0-snapshot",
+  "version": "2.0.1-snapshot",
   "description": "基于vue3、vite4、element-plus、typesScript",
   "author": "xingyu",
   "private": false,
@@ -30,12 +30,12 @@
     "@form-create/element-ui": "^3.1.24",
     "@iconify/iconify": "^3.1.1",
     "@videojs-player/vue": "^1.0.0",
-    "@vueuse/core": "^10.6.1",
+    "@vueuse/core": "^10.9.0",
     "@wangeditor/editor": "^5.1.23",
     "@wangeditor/editor-for-vue": "^5.1.10",
     "@zxcvbn-ts/core": "^3.0.4",
     "animate.css": "^4.1.1",
-    "axios": "^1.6.1",
+    "axios": "^1.6.7",
     "benz-amr-recorder": "^1.1.5",
     "bpmn-js-token-simulation": "^0.10.0",
     "camunda-bpmn-moddle": "^7.0.1",
@@ -44,9 +44,9 @@
     "dayjs": "^1.11.10",
     "diagram-js": "^12.8.0",
     "driver.js": "^1.3.1",
-    "echarts": "^5.4.3",
+    "echarts": "^5.5.0",
     "echarts-wordcloud": "^2.1.0",
-    "element-plus": "2.4.2",
+    "element-plus": "2.5.3",
     "fast-xml-parser": "^4.3.2",
     "highlight.js": "^11.9.0",
     "jsencrypt": "^3.3.2",
@@ -55,77 +55,78 @@
     "mitt": "^3.0.1",
     "nprogress": "^0.2.0",
     "pinia": "^2.1.7",
+    "pinia-plugin-persistedstate": "^3.2.0",
     "qrcode": "^1.5.3",
     "qs": "^6.11.2",
     "steady-xml": "^0.1.0",
     "url": "^0.11.3",
     "video.js": "^7.21.5",
-    "vue": "^3.3.8",
+    "vue": "3.4.20",
     "vue-dompurify-html": "^4.1.4",
-    "vue-i18n": "^9.6.5",
-    "vue-router": "^4.2.5",
+    "vue-i18n": "9.9.1",
+    "vue-router": "^4.3.0",
     "vue-types": "^5.1.1",
     "vuedraggable": "^4.1.0",
     "web-storage-cache": "^1.1.1",
     "xml-js": "^1.6.11"
   },
   "devDependencies": {
-    "@commitlint/cli": "^18.4.1",
-    "@commitlint/config-conventional": "^18.4.0",
-    "@iconify/json": "^2.2.142",
-    "@intlify/unplugin-vue-i18n": "^1.5.0",
+    "@commitlint/cli": "^19.0.1",
+    "@commitlint/config-conventional": "^19.0.0",
+    "@iconify/json": "^2.2.187",
+    "@intlify/unplugin-vue-i18n": "^2.0.0",
     "@purge-icons/generated": "^0.9.0",
-    "@types/lodash-es": "^4.17.11",
-    "@types/node": "^20.9.0",
+    "@types/lodash-es": "^4.17.12",
+    "@types/node": "^20.11.21",
     "@types/nprogress": "^0.2.3",
     "@types/qrcode": "^1.5.5",
-    "@types/qs": "^6.9.10",
-    "@typescript-eslint/eslint-plugin": "^6.11.0",
-    "@typescript-eslint/parser": "^6.11.0",
-    "@unocss/transformer-variant-group": "^0.57.4",
+    "@types/qs": "^6.9.12",
+    "@typescript-eslint/eslint-plugin": "^7.1.0",
+    "@typescript-eslint/parser": "^7.1.0",
+    "@unocss/transformer-variant-group": "^0.58.5",
     "@unocss/eslint-config": "^0.57.4",
-    "@vitejs/plugin-legacy": "^4.1.1",
-    "@vitejs/plugin-vue": "^4.4.1",
-    "@vitejs/plugin-vue-jsx": "^3.0.2",
-    "autoprefixer": "^10.4.16",
+    "@vitejs/plugin-legacy": "^5.3.1",
+    "@vitejs/plugin-vue": "^5.0.4",
+    "@vitejs/plugin-vue-jsx": "^3.1.0",
+    "autoprefixer": "^10.4.17",
     "bpmn-js": "8.9.0",
     "bpmn-js-properties-panel": "0.46.0",
     "consola": "^3.2.3",
-    "eslint": "^8.53.0",
-    "eslint-config-prettier": "^9.0.0",
-    "eslint-define-config": "^1.24.1",
-    "eslint-plugin-prettier": "^5.0.1",
-    "eslint-plugin-vue": "^9.18.1",
-    "lint-staged": "^15.1.0",
-    "postcss": "^8.4.31",
-    "postcss-html": "^1.5.0",
+    "eslint": "^8.57.0",
+    "eslint-config-prettier": "^9.1.0",
+    "eslint-define-config": "^2.1.0",
+    "eslint-plugin-prettier": "^5.1.3",
+    "eslint-plugin-vue": "^9.22.0",
+    "lint-staged": "^15.2.2",
+    "postcss": "^8.4.35",
+    "postcss-html": "^1.6.0",
     "postcss-scss": "^4.0.9",
-    "prettier": "^3.1.0",
+    "prettier": "^3.2.5",
     "prettier-eslint": "^16.3.0",
     "rimraf": "^5.0.5",
-    "rollup": "^4.4.1",
+    "rollup": "^4.12.0",
     "sass": "^1.69.5",
-    "stylelint": "^15.11.0",
+    "stylelint": "^16.2.1",
     "stylelint-config-html": "^1.1.0",
-    "stylelint-config-recommended": "^13.0.0",
-    "stylelint-config-standard": "^34.0.0",
-    "stylelint-order": "^6.0.3",
-    "terser": "^5.24.0",
-    "typescript": "5.2.2",
-    "unocss": "^0.57.4",
+    "stylelint-config-recommended": "^14.0.0",
+    "stylelint-config-standard": "^36.0.0",
+    "stylelint-order": "^6.0.4",
+    "terser": "^5.28.1",
+    "typescript": "5.3.3",
+    "unocss": "^0.58.5",
     "unplugin-auto-import": "^0.16.7",
     "unplugin-element-plus": "^0.8.0",
     "unplugin-vue-components": "^0.25.2",
-    "vite": "4.5.0",
+    "vite": "5.1.4",
     "vite-plugin-compression": "^0.5.1",
-    "vite-plugin-ejs": "^1.6.4",
+    "vite-plugin-ejs": "^1.7.0",
     "vite-plugin-eslint": "^1.8.1",
     "vite-plugin-progress": "^0.0.7",
-    "vite-plugin-purge-icons": "^0.9.2",
+    "vite-plugin-purge-icons": "^0.10.0",
     "vite-plugin-svg-icons": "^2.0.1",
     "vite-plugin-top-level-await": "^1.3.1",
     "vue-eslint-parser": "^9.3.2",
-    "vue-tsc": "^1.8.22"
+    "vue-tsc": "^1.8.27"
   },
   "license": "MIT",
   "repository": {

+ 43 - 0
src/api/bpm/category/index.ts

@@ -0,0 +1,43 @@
+import request from '@/config/axios'
+
+// BPM 流程分类 VO
+export interface CategoryVO {
+  id: number // 分类编号
+  name: string // 分类名
+  code: string // 分类标志
+  status: number // 分类状态
+  sort: number // 分类排序
+}
+
+// BPM 流程分类 API
+export const CategoryApi = {
+  // 查询流程分类分页
+  getCategoryPage: async (params: any) => {
+    return await request.get({ url: `/bpm/category/page`, params })
+  },
+
+  // 查询流程分类列表
+  getCategorySimpleList: async () => {
+    return await request.get({ url: `/bpm/category/simple-list` })
+  },
+
+  // 查询流程分类详情
+  getCategory: async (id: number) => {
+    return await request.get({ url: `/bpm/category/get?id=` + id })
+  },
+
+  // 新增流程分类
+  createCategory: async (data: CategoryVO) => {
+    return await request.post({ url: `/bpm/category/create`, data })
+  },
+
+  // 修改流程分类
+  updateCategory: async (data: CategoryVO) => {
+    return await request.put({ url: `/bpm/category/update`, data })
+  },
+
+  // 删除流程分类
+  deleteCategory: async (id: number) => {
+    return await request.delete({ url: `/bpm/category/delete?id=` + id })
+  }
+}

+ 3 - 2
src/api/bpm/definition/index.ts

@@ -1,8 +1,9 @@
 import request from '@/config/axios'
 
-export const getProcessDefinitionBpmnXML = async (id: number) => {
+export const getProcessDefinition = async (id: number, key: string) => {
   return await request.get({
-    url: '/bpm/process-definition/get-bpmn-xml?id=' + id
+    url: '/bpm/process-definition/get',
+    params: { id, key }
   })
 }
 

+ 2 - 2
src/api/bpm/form/index.ts

@@ -49,8 +49,8 @@ export const getFormPage = async (params) => {
 }
 
 // 获得动态表单的精简列表
-export const getSimpleFormList = async () => {
+export const getFormSimpleList = async () => {
   return await request.get({
-    url: '/bpm/form/list-all-simple'
+    url: '/bpm/form/simple-list'
   })
 }

+ 1 - 1
src/api/bpm/leave/index.ts

@@ -2,7 +2,7 @@ import request from '@/config/axios'
 
 export type LeaveVO = {
   id: number
-  result: number
+  status: number
   type: number
   reason: string
   processInstanceId: string

+ 42 - 0
src/api/bpm/processExpression/index.ts

@@ -0,0 +1,42 @@
+import request from '@/config/axios'
+
+// BPM 流程表达式 VO
+export interface ProcessExpressionVO {
+  id: number // 编号
+  name: string // 表达式名字
+  status: number // 表达式状态
+  expression: string // 表达式
+}
+
+// BPM 流程表达式 API
+export const ProcessExpressionApi = {
+  // 查询BPM 流程表达式分页
+  getProcessExpressionPage: async (params: any) => {
+    return await request.get({ url: `/bpm/process-expression/page`, params })
+  },
+
+  // 查询BPM 流程表达式详情
+  getProcessExpression: async (id: number) => {
+    return await request.get({ url: `/bpm/process-expression/get?id=` + id })
+  },
+
+  // 新增BPM 流程表达式
+  createProcessExpression: async (data: ProcessExpressionVO) => {
+    return await request.post({ url: `/bpm/process-expression/create`, data })
+  },
+
+  // 修改BPM 流程表达式
+  updateProcessExpression: async (data: ProcessExpressionVO) => {
+    return await request.put({ url: `/bpm/process-expression/update`, data })
+  },
+
+  // 删除BPM 流程表达式
+  deleteProcessExpression: async (id: number) => {
+    return await request.delete({ url: `/bpm/process-expression/delete?id=` + id })
+  },
+
+  // 导出BPM 流程表达式 Excel
+  exportProcessExpression: async (params) => {
+    return await request.download({ url: `/bpm/process-expression/export-excel`, params })
+  }
+}

+ 26 - 28
src/api/bpm/processInstance/index.ts

@@ -20,51 +20,49 @@ export type ProcessInstanceVO = {
   endTime: string
 }
 
-export type ProcessInstanceCCVO = {
-  type: number,
-  taskName: string,
-  taskKey: string,
-  processInstanceName: string,
-  processInstanceKey: string,
-  startUserId: string,
-  options:string [],
+export type ProcessInstanceCopyVO = {
+  type: number
+  taskName: string
+  taskKey: string
+  processInstanceName: string
+  processInstanceKey: string
+  startUserId: string
+  options: string[]
   reason: string
 }
 
-export const getMyProcessInstancePage = async (params) => {
+export const getProcessInstanceMyPage = async (params: any) => {
   return await request.get({ url: '/bpm/process-instance/my-page', params })
 }
 
+export const getProcessInstanceManagerPage = async (params: any) => {
+  return await request.get({ url: '/bpm/process-instance/manager-page', params })
+}
+
 export const createProcessInstance = async (data) => {
   return await request.post({ url: '/bpm/process-instance/create', data: data })
 }
 
-export const cancelProcessInstance = async (id: number, reason: string) => {
+export const cancelProcessInstanceByStartUser = async (id: number, reason: string) => {
   const data = {
     id: id,
     reason: reason
   }
-  return await request.delete({ url: '/bpm/process-instance/cancel', data: data })
+  return await request.delete({ url: '/bpm/process-instance/cancel-by-start-user', data: data })
 }
 
-export const getProcessInstance = async (id: number) => {
-  return await request.get({ url: '/bpm/process-instance/get?id=' + id })
+export const cancelProcessInstanceByAdmin = async (id: number, reason: string) => {
+  const data = {
+    id: id,
+    reason: reason
+  }
+  return await request.delete({ url: '/bpm/process-instance/cancel-by-admin', data: data })
 }
 
-/**
- * 抄送
- * @param data 抄送数据
- * @returns 是否抄送成功
- */
-export const createProcessInstanceCC = async (data) => {
-  return await request.post({ url: '/bpm/process-instance/cc/create', data: data })
+export const getProcessInstance = async (id: string) => {
+  return await request.get({ url: '/bpm/process-instance/get?id=' + id })
 }
 
-/**
- * 抄送列表
- * @param params 
- * @returns 
- */
-export const getProcessInstanceCCPage = async (params) => {
-  return await request.get({ url: '/bpm/process-instance/cc/my-page', params })
-}
+export const getProcessInstanceCopyPage = async (params: any) => {
+  return await request.get({ url: '/bpm/process-instance/copy/page', params })
+}

+ 40 - 0
src/api/bpm/processListener/index.ts

@@ -0,0 +1,40 @@
+import request from '@/config/axios'
+
+// BPM 流程监听器 VO
+export interface ProcessListenerVO {
+  id: number // 编号
+  name: string // 监听器名字
+  type: string // 监听器类型
+  status: number // 监听器状态
+  event: string // 监听事件
+  valueType: string // 监听器值类型
+  value: string // 监听器值
+}
+
+// BPM 流程监听器 API
+export const ProcessListenerApi = {
+  // 查询流程监听器分页
+  getProcessListenerPage: async (params: any) => {
+    return await request.get({ url: `/bpm/process-listener/page`, params })
+  },
+
+  // 查询流程监听器详情
+  getProcessListener: async (id: number) => {
+    return await request.get({ url: `/bpm/process-listener/get?id=` + id })
+  },
+
+  // 新增流程监听器
+  createProcessListener: async (data: ProcessListenerVO) => {
+    return await request.post({ url: `/bpm/process-listener/create`, data })
+  },
+
+  // 修改流程监听器
+  updateProcessListener: async (data: ProcessListenerVO) => {
+    return await request.put({ url: `/bpm/process-listener/update`, data })
+  },
+
+  // 删除流程监听器
+  deleteProcessListener: async (id: number) => {
+    return await request.delete({ url: `/bpm/process-listener/delete?id=` + id })
+  }
+}

+ 25 - 40
src/api/bpm/task/index.ts

@@ -4,78 +4,63 @@ export type TaskVO = {
   id: number
 }
 
-export const getTodoTaskPage = async (params) => {
+export const getTaskTodoPage = async (params: any) => {
   return await request.get({ url: '/bpm/task/todo-page', params })
 }
 
-export const getDoneTaskPage = async (params) => {
+export const getTaskDonePage = async (params: any) => {
   return await request.get({ url: '/bpm/task/done-page', params })
 }
 
-export const completeTask = async (data) => {
-  return await request.put({ url: '/bpm/task/complete', data })
+export const getTaskManagerPage = async (params: any) => {
+  return await request.get({ url: '/bpm/task/manager-page', params })
 }
 
-export const approveTask = async (data) => {
+export const approveTask = async (data: any) => {
   return await request.put({ url: '/bpm/task/approve', data })
 }
 
-export const rejectTask = async (data) => {
+export const rejectTask = async (data: any) => {
   return await request.put({ url: '/bpm/task/reject', data })
 }
-export const backTask = async (data) => {
-  return await request.put({ url: '/bpm/task/back', data })
-}
-
-export const updateTaskAssignee = async (data) => {
-  return await request.put({ url: '/bpm/task/update-assignee', data })
-}
 
-export const getTaskListByProcessInstanceId = async (processInstanceId) => {
+export const getTaskListByProcessInstanceId = async (processInstanceId: string) => {
   return await request.get({
     url: '/bpm/task/list-by-process-instance-id?processInstanceId=' + processInstanceId
   })
 }
 
-// 导出任务
-export const exportTask = async (params) => {
-  return await request.download({ url: '/bpm/task/export', params })
-}
-
 // 获取所有可回退的节点
-export const getReturnList = async (params) => {
-  return await request.get({ url: '/bpm/task/return-list', params })
+export const getTaskListByReturn = async (id: string) => {
+  return await request.get({ url: '/bpm/task/list-by-return', params: { id } })
 }
 
 // 回退
-export const returnTask = async (data) => {
+export const returnTask = async (data: any) => {
   return await request.put({ url: '/bpm/task/return', data })
 }
 
-/**
- * 委派
- */
-export const delegateTask = async (data) => {
+// 委派
+export const delegateTask = async (data: any) => {
   return await request.put({ url: '/bpm/task/delegate', data })
 }
 
-/**
- * 加签
- */
-export const taskAddSign = async (data) => {
-  return await request.put({ url: '/bpm/task/create-sign', data })
+// 转派
+export const transferTask = async (data: any) => {
+  return await request.put({ url: '/bpm/task/transfer', data })
 }
 
-/**
- * 获取减签任务列表
- */
-export const getChildrenTaskList = async (id: string) => {
-  return await request.get({ url: '/bpm/task/children-list?taskId=' + id })
+// 加签
+export const signCreateTask = async (data: any) => {
+  return await request.put({ url: '/bpm/task/create-sign', data })
 }
 
-/**
- * 减签
- */
-export const taskSubSign = async (data) => {
+// 减签
+export const signDeleteTask = async (data: any) => {
   return await request.delete({ url: '/bpm/task/delete-sign', data })
 }
+
+// 获取减签任务列表
+export const getChildrenTaskList = async (id: string) => {
+  return await request.get({ url: '/bpm/task/list-by-parent-task-id?parentTaskId=' + id })
+}

+ 0 - 29
src/api/bpm/taskAssignRule/index.ts

@@ -1,29 +0,0 @@
-import request from '@/config/axios'
-
-export type TaskAssignVO = {
-  id: number
-  modelId: string
-  processDefinitionId: string
-  taskDefinitionKey: string
-  taskDefinitionName: string
-  options: string[]
-  type: number
-}
-
-export const getTaskAssignRuleList = async (params) => {
-  return await request.get({ url: '/bpm/task-assign-rule/list', params })
-}
-
-export const createTaskAssignRule = async (data: TaskAssignVO) => {
-  return await request.post({
-    url: '/bpm/task-assign-rule/create',
-    data: data
-  })
-}
-
-export const updateTaskAssignRule = async (data: TaskAssignVO) => {
-  return await request.put({
-    url: '/bpm/task-assign-rule/update',
-    data: data
-  })
-}

+ 3 - 3
src/api/bpm/userGroup/index.ts

@@ -4,7 +4,7 @@ export type UserGroupVO = {
   id: number
   name: string
   description: string
-  memberUserIds: number[]
+  userIds: number[]
   status: number
   remark: string
   createTime: string
@@ -42,6 +42,6 @@ export const getUserGroupPage = async (params) => {
 }
 
 // 获取用户组精简信息列表
-export const getSimpleUserGroupList = async (): Promise<UserGroupVO[]> => {
-  return await request.get({ url: '/bpm/user-group/list-all-simple' })
+export const getUserGroupSimpleList = async (): Promise<UserGroupVO[]> => {
+  return await request.get({ url: '/bpm/user-group/simple-list' })
 }

+ 13 - 13
src/api/crm/statistics/customer.ts

@@ -14,21 +14,21 @@ export interface CrmStatisticsCustomerSummaryByUserRespVO {
   receivablePrice: number
 }
 
-export interface CrmStatisticsFollowupSummaryByDateRespVO {
+export interface CrmStatisticsFollowUpSummaryByDateRespVO {
   time: string
-  followupRecordCount: number
-  followupCustomerCount: number
+  followUpRecordCount: number
+  followUpCustomerCount: number
 }
 
-export interface CrmStatisticsFollowupSummaryByUserRespVO {
+export interface CrmStatisticsFollowUpSummaryByUserRespVO {
   ownerUserName: string
   followupRecordCount: number
   followupCustomerCount: number
 }
 
-export interface CrmStatisticsFollowupSummaryByTypeRespVO {
-  followupType: string
-  followupRecordCount: number
+export interface CrmStatisticsFollowUpSummaryByTypeRespVO {
+  followUpType: string
+  followUpRecordCount: number
 }
 
 export interface CrmStatisticsCustomerContractSummaryRespVO {
@@ -72,23 +72,23 @@ export const StatisticsCustomerApi = {
     })
   },
   // 2.1 客户跟进次数分析(按日期)
-  getFollowupSummaryByDate: (params: any) => {
+  getFollowUpSummaryByDate: (params: any) => {
     return request.get({
-      url: '/crm/statistics-customer/get-followup-summary-by-date',
+      url: '/crm/statistics-customer/get-follow-up-summary-by-date',
       params
     })
   },
   // 2.2 客户跟进次数分析(按用户)
-  getFollowupSummaryByUser: (params: any) => {
+  getFollowUpSummaryByUser: (params: any) => {
     return request.get({
-      url: '/crm/statistics-customer/get-followup-summary-by-user',
+      url: '/crm/statistics-customer/get-follow-up-summary-by-user',
       params
     })
   },
   // 3.1 获取客户跟进方式统计数
-  getFollowupSummaryByType: (params: any) => {
+  getFollowUpSummaryByType: (params: any) => {
     return request.get({
-      url: '/crm/statistics-customer/get-followup-summary-by-type',
+      url: '/crm/statistics-customer/get-follow-up-summary-by-type',
       params
     })
   },

+ 33 - 0
src/api/crm/statistics/performance.ts

@@ -0,0 +1,33 @@
+import request from '@/config/axios'
+
+export interface StatisticsPerformanceRespVO {
+  time: string
+  currentMonthCount: number
+  lastMonthCount: number
+  lastYearCount: number
+}
+
+// 排行 API
+export const StatisticsPerformanceApi = {
+  // 员工获得合同金额统计
+  getContractPricePerformance: (params: any) => {
+    return request.get({
+      url: '/crm/statistics-performance/get-contract-price-performance',
+      params
+    })
+  },
+  // 员工获得回款统计
+  getReceivablePricePerformance: (params: any) => {
+    return request.get({
+      url: '/crm/statistics-performance/get-receivable-price-performance',
+      params
+    })
+  },
+  //员工获得签约合同数量统计
+  getContractCountPerformance: (params: any) => {
+    return request.get({
+      url: '/crm/statistics-performance/get-contract-count-performance',
+      params
+    })
+  }
+}

+ 60 - 0
src/api/crm/statistics/portrait.ts

@@ -0,0 +1,60 @@
+import request from '@/config/axios'
+
+export interface CrmStatisticCustomerBaseRespVO {
+  customerCount: number
+  dealCount: number
+  dealPortion: string | number
+}
+
+export interface CrmStatisticCustomerIndustryRespVO extends CrmStatisticCustomerBaseRespVO {
+  industryId: number
+  industryPortion: string | number
+}
+
+export interface CrmStatisticCustomerSourceRespVO extends CrmStatisticCustomerBaseRespVO {
+  source: number
+  sourcePortion: string | number
+}
+
+export interface CrmStatisticCustomerLevelRespVO extends CrmStatisticCustomerBaseRespVO {
+  level: number
+  levelPortion: string | number
+}
+
+export interface CrmStatisticCustomerAreaRespVO extends CrmStatisticCustomerBaseRespVO {
+  areaId: number
+  areaName: string
+  areaPortion: string | number
+}
+
+// 客户分析 API
+export const StatisticsPortraitApi = {
+  // 1. 获取客户行业统计数据
+  getCustomerIndustry: (params: any) => {
+    return request.get({
+      url: '/crm/statistics-portrait/get-customer-industry-summary',
+      params
+    })
+  },
+  // 2. 获取客户来源统计数据
+  getCustomerSource: (params: any) => {
+    return request.get({
+      url: '/crm/statistics-portrait/get-customer-source-summary',
+      params
+    })
+  },
+  // 3. 获取客户级别统计数据
+  getCustomerLevel: (params: any) => {
+    return request.get({
+      url: '/crm/statistics-portrait/get-customer-level-summary',
+      params
+    })
+  },
+  // 4. 获取客户地区统计数据
+  getCustomerArea: (params: any) => {
+    return request.get({
+      url: '/crm/statistics-portrait/get-customer-area-summary',
+      params
+    })
+  }
+}

+ 5 - 1
src/api/infra/apiAccessLog/index.ts

@@ -8,11 +8,15 @@ export interface ApiAccessLogVO {
   applicationName: string
   requestMethod: string
   requestParams: string
+  responseBody: string
   requestUrl: string
   userIp: string
   userAgent: string
+  operateModule: string
+  operateName: string
+  operateType: number
   beginTime: Date
-  endTIme: Date
+  endTime: Date
   duration: number
   resultCode: number
   resultMsg: string

+ 1 - 1
src/api/mall/statistics/member.ts

@@ -5,7 +5,7 @@ import { formatDate } from '@/utils/formatTime'
 
 /** 会员分析 Request VO */
 export interface MemberAnalyseReqVO {
-  times: [dayjs.ConfigType, dayjs.ConfigType]
+  times: dayjs.ConfigType[]
 }
 
 /** 会员分析 Response VO */

+ 0 - 39
src/api/report/ureport/index.ts

@@ -1,39 +0,0 @@
-import request from '@/config/axios'
-
-export interface UReportDataVO {
-  id: number
-  name: string
-  status: number
-  content: string
-  remark: string
-}
-
-// 查询Ureport2报表分页
-export const getUReportDataPage = async (params) => {
-  return await request.get({ url: `/report/ureport-data/page`, params })
-}
-
-// 查询Ureport2报表详情
-export const getUReportData = async (id: number) => {
-  return await request.get({ url: `/report/ureport-data/get?id=` + id })
-}
-
-// 新增Ureport2报表
-export const createUReportData = async (data: UReportDataVO) => {
-  return await request.post({ url: `/report/ureport-data/create`, data })
-}
-
-// 修改Ureport2报表
-export const updateUReportData = async (data: UReportDataVO) => {
-  return await request.put({ url: `/report/ureport-data/update`, data })
-}
-
-// 删除Ureport2报表
-export const deleteUReportData = async (id: number) => {
-  return await request.delete({ url: `/report/ureport-data/delete?id=` + id })
-}
-
-// 导出Ureport2报表 Excel
-export const exportUReportData = async (params) => {
-  return await request.download({ url: `/report/ureport-data/export-excel`, params })
-}

+ 1 - 30
src/api/system/operatelog/index.ts

@@ -2,30 +2,6 @@ import request from '@/config/axios'
 
 export type OperateLogVO = {
   id: number
-  userNickname: string
-  traceId: string
-  userId: number
-  module: string
-  name: string
-  type: number
-  content: string
-  exts: Map<String, Object>
-  requestMethod: string
-  requestUrl: string
-  userIp: string
-  userAgent: string
-  javaMethod: string
-  javaMethodArgs: string
-  startTime: Date
-  duration: number
-  resultCode: number
-  resultMsg: string
-  resultData: string
-}
-
-export type OperateLogV2VO = {
-  id: number
-  userNickname: string
   traceId: string
   userType: number
   userId: number
@@ -42,11 +18,6 @@ export type OperateLogV2VO = {
   creator: string
   creatorName: string
   createTime: Date
-  // 数据扩展,渲染时使用
-  title: string // 操作标题(如果为空则取 name 值)
-  colSize: number // 变更记录行数
-  contentStrList: string[]
-  tagsContentList: string[]
 }
 
 // 查询操作日志列表
@@ -54,6 +25,6 @@ export const getOperateLogPage = (params: PageParam) => {
   return request.get({ url: '/system/operate-log/page', params })
 }
 // 导出操作日志
-export const exportOperateLog = (params) => {
+export const exportOperateLog = (params: any) => {
   return request.download({ url: '/system/operate-log/export', params })
 }

二進制
src/assets/imgs/avatar.jpg


+ 3 - 0
src/components/ContentWrap/src/ContentWrap.vue

@@ -25,6 +25,9 @@ defineProps({
           </template>
           <Icon :size="14" class="ml-5px" icon="ep:question-filled" />
         </ElTooltip>
+        <div class="flex flex-grow pl-20px">
+          <slot name="header"></slot>
+        </div>
       </div>
     </template>
     <div>

+ 5 - 1
src/components/Crontab/src/Crontab.vue

@@ -503,9 +503,13 @@ const submit = () => {
   emit('update:modelValue', defaultValue.value)
   dialogVisible.value = false
 }
+
+const inputChange = () => {
+  emit('update:modelValue', defaultValue.value)
+}
 </script>
 <template>
-  <el-input v-model="defaultValue" class="input-with-select" v-bind="$attrs">
+  <el-input v-model="defaultValue" class="input-with-select" v-bind="$attrs" @input="inputChange">
     <template #append>
       <el-select v-model="select" placeholder="生成器" style="width: 115px">
         <el-option label="每分钟" value="0 * * * * ?" />

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

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

+ 47 - 0
src/components/DictSelect/src/DictSelect.vue

@@ -0,0 +1,47 @@
+<!-- 数据字典 Select 选择器 -->
+<template>
+  <el-select class="w-1/1" v-bind="attrs">
+    <template v-if="valueType === 'int'">
+      <el-option
+        v-for="(dict, index) in getIntDictOptions(dictType)"
+        :key="index"
+        :label="dict.label"
+        :value="dict.value"
+      />
+    </template>
+    <template v-if="valueType === 'str'">
+      <el-option
+        v-for="(dict, index) in getStrDictOptions(dictType)"
+        :key="index"
+        :label="dict.label"
+        :value="dict.value"
+      />
+    </template>
+    <template v-if="valueType === 'bool'">
+      <el-option
+        v-for="(dict, index) in getBoolDictOptions(dictType)"
+        :key="index"
+        :label="dict.label"
+        :value="dict.value"
+      />
+    </template>
+  </el-select>
+</template>
+
+<script lang="ts" setup>
+import { getBoolDictOptions, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
+
+// 接受父组件参数
+interface Props {
+  modelValue?: any // 值
+  dictType: string // 字典类型
+  valueType: string // 字典值类型
+}
+
+withDefaults(defineProps<Props>(), {
+  dictType: '',
+  valueType: 'str'
+})
+const attrs = useAttrs()
+defineOptions({ name: 'DictSelect' })
+</script>

+ 2 - 2
src/components/Editor/src/Editor.vue

@@ -180,12 +180,12 @@ defineExpose({
 </script>
 
 <template>
-  <div class="z-99 border-1 border-[var(--el-border-color)] border-solid">
+  <div class="border-1 border-solid border-[var(--tags-view-border-color)] z-10">
     <!-- 工具栏 -->
     <Toolbar
       :editor="editorRef"
       :editorId="editorId"
-      class="border-0 b-b-1 border-[var(--el-border-color)] border-solid"
+      class="border-0 b-b-1 border-solid border-[var(--tags-view-border-color)]"
     />
     <!-- 编辑器 -->
     <Editor

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

@@ -0,0 +1,4 @@
+import MyFormCreateDesigner from './src/MyFormCreateDesigner.vue'
+import { useFormCreateDesigner } from './src/useFormCreateDesigner'
+
+export { MyFormCreateDesigner, useFormCreateDesigner }

+ 33 - 0
src/components/FormCreate/src/MyFormCreateDesigner.vue

@@ -0,0 +1,33 @@
+<!-- TODO puhui999: 没啥问题的话准备移除 -->
+<template>
+  <FcDesigner ref="designer" height="780px" />
+</template>
+
+<script lang="ts" setup>
+import { useUploadFileRule, useUploadImgRule, useUploadImgsRule } from './config'
+
+defineOptions({ name: 'MyFormCreateDesigner' })
+
+const designer = ref() // 表单设计器
+const uploadFileRule = useUploadFileRule()
+const uploadImgRule = useUploadImgRule()
+const uploadImgsRule = useUploadImgsRule()
+
+onMounted(() => {
+  // 移除自带的上传组件规则
+  designer.value?.removeMenuItem('upload')
+  const components = [uploadFileRule, uploadImgRule, uploadImgsRule]
+  components.forEach((component) => {
+    //插入组件规则
+    designer.value?.addComponent(component)
+    //插入拖拽按钮到`main`分类下
+    designer.value?.appendMenuItem('main', {
+      icon: component.icon,
+      name: component.name,
+      label: component.label
+    })
+  })
+})
+</script>
+
+<style lang="scss" scoped></style>

+ 13 - 0
src/components/FormCreate/src/config/index.ts

@@ -0,0 +1,13 @@
+import { useUploadFileRule } from './useUploadFileRule'
+import { useUploadImgRule } from './useUploadImgRule'
+import { useUploadImgsRule } from './useUploadImgsRule'
+import { useDictSelectRule } from './useDictSelectRule'
+import { useUserSelectRule } from './useUserSelectRule'
+
+export {
+  useUploadFileRule,
+  useUploadImgRule,
+  useUploadImgsRule,
+  useDictSelectRule,
+  useUserSelectRule
+}

+ 124 - 0
src/components/FormCreate/src/config/useDictSelectRule.ts

@@ -0,0 +1,124 @@
+import { generateUUID } from '@/utils'
+import * as DictDataApi from '@/api/system/dict/dict.type'
+import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils'
+
+export const useDictSelectRule = () => {
+  const label = '字典选择器'
+  const name = 'DictSelect'
+  const dictOptions = ref<{ label: string; value: string }[]>([]) // 字典类型下拉数据
+  onMounted(async () => {
+    const data = await DictDataApi.getSimpleDictTypeList()
+    if (!data || data.length === 0) {
+      return
+    }
+    dictOptions.value =
+      data?.map((item: DictDataApi.DictTypeVO) => ({
+        label: item.name,
+        value: item.type
+      })) ?? []
+  })
+  return {
+    icon: 'icon-select',
+    label,
+    name,
+    rule() {
+      return {
+        type: name,
+        field: generateUUID(),
+        title: label,
+        info: '',
+        $required: false
+      }
+    },
+    props(_, { t }) {
+      return localeProps(t, name + '.props', [
+        makeRequiredRule(),
+        {
+          type: 'select',
+          field: 'dictType',
+          title: '字典类型',
+          value: '',
+          options: dictOptions.value
+        },
+        {
+          type: 'select',
+          field: 'valueType',
+          title: '字典值类型',
+          value: 'str',
+          options: [
+            { label: '数字', value: 'int' },
+            { label: '字符串', value: 'str' },
+            { label: '布尔值', value: 'bool' }
+          ]
+        },
+        { type: 'switch', field: 'multiple', title: '是否多选' },
+        {
+          type: 'switch',
+          field: 'disabled',
+          title: '是否禁用'
+        },
+        { type: 'switch', field: 'clearable', title: '是否可以清空选项' },
+        {
+          type: 'switch',
+          field: 'collapseTags',
+          title: '多选时是否将选中值按文字的形式展示'
+        },
+        {
+          type: 'inputNumber',
+          field: 'multipleLimit',
+          title: '多选时用户最多可以选择的项目数,为 0 则不限制',
+          props: { min: 0 }
+        },
+        {
+          type: 'input',
+          field: 'autocomplete',
+          title: 'autocomplete 属性'
+        },
+        { type: 'input', field: 'placeholder', title: '占位符' },
+        {
+          type: 'switch',
+          field: 'filterable',
+          title: '是否可搜索'
+        },
+        { type: 'switch', field: 'allowCreate', title: '是否允许用户创建新条目' },
+        {
+          type: 'input',
+          field: 'noMatchText',
+          title: '搜索条件无匹配时显示的文字'
+        },
+        {
+          type: 'switch',
+          field: 'remote',
+          title: '其中的选项是否从服务器远程加载'
+        },
+        {
+          type: 'Struct',
+          field: 'remoteMethod',
+          title: '自定义远程搜索方法'
+        },
+        { type: 'input', field: 'noDataText', title: '选项为空时显示的文字' },
+        {
+          type: 'switch',
+          field: 'reserveKeyword',
+          title: '多选且可搜索时,是否在选中一个选项后保留当前的搜索关键词'
+        },
+        {
+          type: 'switch',
+          field: 'defaultFirstOption',
+          title: '在输入框按下回车,选择第一个匹配项'
+        },
+        {
+          type: 'switch',
+          field: 'popperAppendToBody',
+          title: '是否将弹出框插入至 body 元素',
+          value: true
+        },
+        {
+          type: 'switch',
+          field: 'automaticDropdown',
+          title: '对于不可搜索的 Select,是否在输入框获得焦点后自动弹出选项菜单'
+        }
+      ])
+    }
+  }
+}

+ 80 - 0
src/components/FormCreate/src/config/useUploadFileRule.ts

@@ -0,0 +1,80 @@
+import { generateUUID } from '@/utils'
+import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils'
+
+export const useUploadFileRule = () => {
+  const label = '文件上传'
+  const name = 'UploadFile'
+  return {
+    icon: 'icon-upload',
+    label,
+    name,
+    rule() {
+      return {
+        type: name,
+        field: generateUUID(),
+        title: label,
+        info: '',
+        $required: false
+      }
+    },
+    props(_, { t }) {
+      return localeProps(t, name + '.props', [
+        makeRequiredRule(),
+        {
+          type: 'select',
+          field: 'fileType',
+          title: '文件类型',
+          value: ['doc', 'xls', 'ppt', 'txt', 'pdf'],
+          options: [
+            { label: 'doc', value: 'doc' },
+            { label: 'xls', value: 'xls' },
+            { label: 'ppt', value: 'ppt' },
+            { label: 'txt', value: 'txt' },
+            { label: 'pdf', value: 'pdf' }
+          ],
+          props: {
+            multiple: true
+          }
+        },
+        {
+          type: 'switch',
+          field: 'autoUpload',
+          title: '是否在选取文件后立即进行上传',
+          value: true
+        },
+        {
+          type: 'switch',
+          field: 'drag',
+          title: '拖拽上传',
+          value: false
+        },
+        {
+          type: 'switch',
+          field: 'isShowTip',
+          title: '是否显示提示',
+          value: true
+        },
+        {
+          type: 'inputNumber',
+          field: 'fileSize',
+          title: '大小限制(MB)',
+          value: 5,
+          props: { min: 0 }
+        },
+        {
+          type: 'inputNumber',
+          field: 'limit',
+          title: '数量限制',
+          value: 5,
+          props: { min: 0 }
+        },
+        {
+          type: 'switch',
+          field: 'disabled',
+          title: '是否禁用',
+          value: false
+        }
+      ])
+    }
+  }
+}

+ 89 - 0
src/components/FormCreate/src/config/useUploadImgRule.ts

@@ -0,0 +1,89 @@
+import { generateUUID } from '@/utils'
+import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils'
+
+export const useUploadImgRule = () => {
+  const label = '单图上传'
+  const name = 'UploadImg'
+  return {
+    icon: 'icon-upload',
+    label,
+    name,
+    rule() {
+      return {
+        type: name,
+        field: generateUUID(),
+        title: label,
+        info: '',
+        $required: false
+      }
+    },
+    props(_, { t }) {
+      return localeProps(t, name + '.props', [
+        makeRequiredRule(),
+        {
+          type: 'switch',
+          field: 'drag',
+          title: '拖拽上传',
+          value: false
+        },
+        {
+          type: 'select',
+          field: 'fileType',
+          title: '图片类型限制',
+          value: ['image/jpeg', 'image/png', 'image/gif'],
+          options: [
+            { label: 'image/apng', value: 'image/apng' },
+            { label: 'image/bmp', value: 'image/bmp' },
+            { label: 'image/gif', value: 'image/gif' },
+            { label: 'image/jpeg', value: 'image/jpeg' },
+            { label: 'image/pjpeg', value: 'image/pjpeg' },
+            { label: 'image/svg+xml', value: 'image/svg+xml' },
+            { label: 'image/tiff', value: 'image/tiff' },
+            { label: 'image/webp', value: 'image/webp' },
+            { label: 'image/x-icon', value: 'image/x-icon' }
+          ],
+          props: {
+            multiple: true
+          }
+        },
+        {
+          type: 'inputNumber',
+          field: 'fileSize',
+          title: '大小限制(MB)',
+          value: 5,
+          props: { min: 0 }
+        },
+        {
+          type: 'input',
+          field: 'height',
+          title: '组件高度',
+          value: '150px'
+        },
+        {
+          type: 'input',
+          field: 'width',
+          title: '组件宽度',
+          value: '150px'
+        },
+        {
+          type: 'input',
+          field: 'borderradius',
+          title: '组件边框圆角',
+          value: '8px'
+        },
+        {
+          type: 'switch',
+          field: 'disabled',
+          title: '是否显示删除按钮',
+          value: true
+        },
+        {
+          type: 'switch',
+          field: 'showBtnText',
+          title: '是否显示按钮文字',
+          value: true
+        }
+      ])
+    }
+  }
+}

+ 84 - 0
src/components/FormCreate/src/config/useUploadImgsRule.ts

@@ -0,0 +1,84 @@
+import { generateUUID } from '@/utils'
+import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils'
+
+export const useUploadImgsRule = () => {
+  const label = '多图上传'
+  const name = 'UploadImgs'
+  return {
+    icon: 'icon-upload',
+    label,
+    name,
+    rule() {
+      return {
+        type: name,
+        field: generateUUID(),
+        title: label,
+        info: '',
+        $required: false
+      }
+    },
+    props(_, { t }) {
+      return localeProps(t, name + '.props', [
+        makeRequiredRule(),
+        {
+          type: 'switch',
+          field: 'drag',
+          title: '拖拽上传',
+          value: false
+        },
+        {
+          type: 'select',
+          field: 'fileType',
+          title: '图片类型限制',
+          value: ['image/jpeg', 'image/png', 'image/gif'],
+          options: [
+            { label: 'image/apng', value: 'image/apng' },
+            { label: 'image/bmp', value: 'image/bmp' },
+            { label: 'image/gif', value: 'image/gif' },
+            { label: 'image/jpeg', value: 'image/jpeg' },
+            { label: 'image/pjpeg', value: 'image/pjpeg' },
+            { label: 'image/svg+xml', value: 'image/svg+xml' },
+            { label: 'image/tiff', value: 'image/tiff' },
+            { label: 'image/webp', value: 'image/webp' },
+            { label: 'image/x-icon', value: 'image/x-icon' }
+          ],
+          props: {
+            multiple: true
+          }
+        },
+        {
+          type: 'inputNumber',
+          field: 'fileSize',
+          title: '大小限制(MB)',
+          value: 5,
+          props: { min: 0 }
+        },
+        {
+          type: 'inputNumber',
+          field: 'limit',
+          title: '数量限制',
+          value: 5,
+          props: { min: 0 }
+        },
+        {
+          type: 'input',
+          field: 'height',
+          title: '组件高度',
+          value: '150px'
+        },
+        {
+          type: 'input',
+          field: 'width',
+          title: '组件宽度',
+          value: '150px'
+        },
+        {
+          type: 'input',
+          field: 'borderradius',
+          title: '组件边框圆角',
+          value: '8px'
+        }
+      ])
+    }
+  }
+}

+ 93 - 0
src/components/FormCreate/src/config/useUserSelectRule.ts

@@ -0,0 +1,93 @@
+import { generateUUID } from '@/utils'
+import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils'
+
+export const useUserSelectRule = () => {
+  const label = '用户选择器'
+  const name = 'UserSelect'
+  return {
+    icon: 'icon-select',
+    label,
+    name,
+    rule() {
+      return {
+        type: name,
+        field: generateUUID(),
+        title: label,
+        info: '',
+        $required: false
+      }
+    },
+    props(_, { t }) {
+      return localeProps(t, name + '.props', [
+        makeRequiredRule(),
+        { type: 'switch', field: 'multiple', title: '是否多选' },
+        {
+          type: 'switch',
+          field: 'disabled',
+          title: '是否禁用'
+        },
+        { type: 'switch', field: 'clearable', title: '是否可以清空选项' },
+        {
+          type: 'switch',
+          field: 'collapseTags',
+          title: '多选时是否将选中值按文字的形式展示'
+        },
+        {
+          type: 'inputNumber',
+          field: 'multipleLimit',
+          title: '多选时用户最多可以选择的项目数,为 0 则不限制',
+          props: { min: 0 }
+        },
+        {
+          type: 'input',
+          field: 'autocomplete',
+          title: 'autocomplete 属性'
+        },
+        { type: 'input', field: 'placeholder', title: '占位符' },
+        {
+          type: 'switch',
+          field: 'filterable',
+          title: '是否可搜索'
+        },
+        { type: 'switch', field: 'allowCreate', title: '是否允许用户创建新条目' },
+        {
+          type: 'input',
+          field: 'noMatchText',
+          title: '搜索条件无匹配时显示的文字'
+        },
+        {
+          type: 'switch',
+          field: 'remote',
+          title: '其中的选项是否从服务器远程加载'
+        },
+        {
+          type: 'Struct',
+          field: 'remoteMethod',
+          title: '自定义远程搜索方法'
+        },
+        { type: 'input', field: 'noDataText', title: '选项为空时显示的文字' },
+        {
+          type: 'switch',
+          field: 'reserveKeyword',
+          title: '多选且可搜索时,是否在选中一个选项后保留当前的搜索关键词'
+        },
+        {
+          type: 'switch',
+          field: 'defaultFirstOption',
+          title: '在输入框按下回车,选择第一个匹配项'
+        },
+        {
+          type: 'switch',
+          field: 'popperAppendToBody',
+          title: '是否将弹出框插入至 body 元素',
+          value: true
+        },
+        {
+          type: 'switch',
+          field: 'automaticDropdown',
+          title: '对于不可搜索的 Select,是否在输入框获得焦点后自动弹出选项菜单'
+        }
+      ])
+    }
+  }
+}

+ 45 - 0
src/components/FormCreate/src/useFormCreateDesigner.ts

@@ -0,0 +1,45 @@
+import {
+  useDictSelectRule,
+  useUploadFileRule,
+  useUploadImgRule,
+  useUploadImgsRule,
+  useUserSelectRule
+} from './config'
+import { Ref } from 'vue'
+
+/**
+ * 表单设计器增强 hook
+ * 新增
+ * - 文件上传
+ * - 单图上传
+ * - 多图上传
+ */
+export const useFormCreateDesigner = (designer: Ref) => {
+  const uploadFileRule = useUploadFileRule()
+  const uploadImgRule = useUploadImgRule()
+  const uploadImgsRule = useUploadImgsRule()
+  const dictSelectRule = useDictSelectRule()
+  const userSelectRule = useUserSelectRule()
+
+  onMounted(() => {
+    // 移除自带的上传组件规则,使用 uploadFileRule、uploadImgRule、uploadImgsRule 替代
+    designer.value?.removeMenuItem('upload')
+    const components = [
+      uploadFileRule,
+      uploadImgRule,
+      uploadImgsRule,
+      dictSelectRule,
+      userSelectRule
+    ]
+    components.forEach((component) => {
+      // 插入组件规则
+      designer.value?.addComponent(component)
+      // 插入拖拽按钮到 `main` 分类下
+      designer.value?.appendMenuItem('main', {
+        icon: component.icon,
+        name: component.name,
+        label: component.label
+      })
+    })
+  })
+}

+ 79 - 0
src/components/FormCreate/src/utils/index.ts

@@ -0,0 +1,79 @@
+// TODO puhui999: 借鉴一下 form-create-designer utils 方法 🤣 (导入不了只能先 copy 过来用下)
+export function makeRequiredRule() {
+  return {
+    type: 'Required',
+    field: 'formCreate$required',
+    title: '是否必填'
+  }
+}
+
+export const localeProps = (t, prefix, rules) => {
+  return rules.map((rule) => {
+    if (rule.field === 'formCreate$required') {
+      rule.title = t('props.required') || rule.title
+    } else if (rule.field && rule.field !== '_optionType') {
+      rule.title = t('components.' + prefix + '.' + rule.field) || rule.title
+    }
+    return rule
+  })
+}
+
+export function upper(str) {
+  return str.replace(str[0], str[0].toLocaleUpperCase())
+}
+
+export function makeOptionsRule(t, to, userOptions) {
+  console.log(userOptions[0])
+  const options = [
+    { label: t('props.optionsType.struct'), value: 0 },
+    { label: t('props.optionsType.json'), value: 1 },
+    { label: '用户数据', value: 2 }
+  ]
+
+  const control = [
+    {
+      value: 0,
+      rule: [
+        {
+          type: 'TableOptions',
+          field: 'formCreate' + upper(to).replace('.', '>'),
+          props: { defaultValue: [] }
+        }
+      ]
+    },
+    {
+      value: 1,
+      rule: [
+        {
+          type: 'Struct',
+          field: 'formCreate' + upper(to).replace('.', '>'),
+          props: { defaultValue: [] }
+        }
+      ]
+    },
+    {
+      value: 2,
+      rule: [
+        {
+          type: 'TableOptions',
+          field: 'formCreate' + upper(to).replace('.', '>'),
+          props: { modelValue: [] }
+        }
+      ]
+    }
+  ]
+  options.splice(0, 0)
+  control.push()
+
+  return {
+    type: 'radio',
+    title: t('props.options'),
+    field: '_optionType',
+    value: 0,
+    options,
+    props: {
+      type: 'button'
+    },
+    control
+  }
+}

+ 2 - 2
src/components/ImageViewer/index.ts

@@ -12,7 +12,7 @@ export function createImageViewer(options: ImageViewerProps) {
     initialIndex = 0,
     infinite = true,
     hideOnClickModal = false,
-    appendToBody = false,
+    teleported = false,
     zIndex = 2000,
     show = true
   } = options
@@ -23,7 +23,7 @@ export function createImageViewer(options: ImageViewerProps) {
   propsData.initialIndex = initialIndex
   propsData.infinite = infinite
   propsData.hideOnClickModal = hideOnClickModal
-  propsData.appendToBody = appendToBody
+  propsData.teleported = teleported
   propsData.zIndex = zIndex
   propsData.show = show
 

+ 1 - 1
src/components/ImageViewer/src/ImageViewer.vue

@@ -13,7 +13,7 @@ const props = defineProps({
   initialIndex: propTypes.number.def(0),
   infinite: propTypes.bool.def(true),
   hideOnClickModal: propTypes.bool.def(false),
-  appendToBody: propTypes.bool.def(false),
+  teleported: propTypes.bool.def(false),
   show: propTypes.bool.def(false)
 })
 

+ 1 - 1
src/components/ImageViewer/src/types.ts

@@ -4,6 +4,6 @@ export interface ImageViewerProps {
   initialIndex?: number
   infinite?: boolean
   hideOnClickModal?: boolean
-  appendToBody?: boolean
+  teleported?: boolean
   show?: boolean
 }

+ 2 - 2
src/components/OperateLogV2/src/OperateLogV2.vue

@@ -23,7 +23,7 @@
 </template>
 
 <script lang="ts" setup>
-import { OperateLogV2VO } from '@/api/system/operatelog'
+import { OperateLogVO } from '@/api/system/operatelog'
 import { formatDate } from '@/utils/formatTime'
 import { DICT_TYPE, getDictLabel, getDictObj } from '@/utils/dict'
 import { ElTag } from 'element-plus'
@@ -31,7 +31,7 @@ import { ElTag } from 'element-plus'
 defineOptions({ name: 'OperateLogV2' })
 
 interface Props {
-  logList: OperateLogV2VO[] // 操作日志列表
+  logList: OperateLogVO[] // 操作日志列表
 }
 
 withDefaults(defineProps<Props>(), {

+ 1 - 1
src/components/Pagination/index.vue

@@ -53,7 +53,7 @@ const props = defineProps({
   }
 })
 
-const emit = defineEmits(['update:page', 'update:limit', 'pagination', 'pagination'])
+const emit = defineEmits(['update:page', 'update:limit', 'pagination'])
 const currentPage = computed({
   get() {
     return props.page

+ 1 - 1
src/components/RouterSearch/index.vue

@@ -26,7 +26,7 @@
       placeholder="请输入菜单内容"
       :remote-method="remoteMethod"
       class="overflow-hidden transition-all-600"
-      :class="showTopSearch ? 'w-220px ml2' : 'w-0'"
+      :class="showTopSearch ? '!w-220px ml2' : '!w-0'"
       @change="handleChange"
     >
       <el-option

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

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

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

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

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

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

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

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

+ 6 - 5
src/components/UploadFile/src/UploadFile.vue

@@ -6,7 +6,9 @@
       :action="uploadUrl"
       :auto-upload="autoUpload"
       :before-upload="beforeUpload"
+      :disabled="disabled"
       :drag="drag"
+      :http-request="httpRequest"
       :limit="props.limit"
       :multiple="props.limit > 1"
       :on-error="excelUploadError"
@@ -15,15 +17,14 @@
       :on-remove="handleRemove"
       :on-success="handleFileSuccess"
       :show-file-list="true"
-      :http-request="httpRequest"
       class="upload-file-uploader"
       name="file"
     >
-      <el-button type="primary">
+      <el-button v-if="!disabled" type="primary">
         <Icon icon="ep:upload-filled" />
         选取文件
       </el-button>
-      <template v-if="isShowTip" #tip>
+      <template v-if="isShowTip && !disabled" #tip>
         <div style="font-size: 8px">
           大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
         </div>
@@ -48,13 +49,13 @@ const emit = defineEmits(['update:modelValue'])
 
 const props = defineProps({
   modelValue: propTypes.oneOfType<string | string[]>([String, Array<String>]).isRequired,
-  title: propTypes.string.def('文件上传'),
   fileType: propTypes.array.def(['doc', 'xls', 'ppt', 'txt', 'pdf']), // 文件类型, 例如['png', 'jpg', 'jpeg']
   fileSize: propTypes.number.def(5), // 大小限制(MB)
   limit: propTypes.number.def(5), // 数量限制
   autoUpload: propTypes.bool.def(true), // 自动上传
   drag: propTypes.bool.def(false), // 拖拽上传
-  isShowTip: propTypes.bool.def(true) // 是否显示提示
+  isShowTip: propTypes.bool.def(true), // 是否显示提示
+  disabled: propTypes.bool.def(false) // 是否禁用上传组件 ==> 非必传(默认为 false)
 })
 
 // ========== 上传相关 ==========

+ 5 - 6
src/components/UploadFile/src/UploadImg.vue

@@ -6,17 +6,18 @@
       :action="uploadUrl"
       :before-upload="beforeUpload"
       :class="['upload', drag ? 'no-border' : '']"
+      :disabled="disabled"
       :drag="drag"
+      :http-request="httpRequest"
       :multiple="false"
       :on-error="uploadError"
       :on-success="uploadSuccess"
       :show-file-list="false"
-      :http-request="httpRequest"
     >
       <template v-if="modelValue">
         <img :src="modelValue" class="upload-image" />
         <div class="upload-handle" @click.stop>
-          <div class="handle-icon" @click="editImg" v-if="!disabled">
+          <div v-if="!disabled" class="handle-icon" @click="editImg">
             <Icon icon="ep:edit" />
             <span v-if="showBtnText">{{ t('action.edit') }}</span>
           </div>
@@ -77,10 +78,8 @@ const props = defineProps({
   height: propTypes.string.def('150px'), // 组件高度 ==> 非必传(默认为 150px)
   width: propTypes.string.def('150px'), // 组件宽度 ==> 非必传(默认为 150px)
   borderradius: propTypes.string.def('8px'), // 组件边框圆角 ==> 非必传(默认为 8px)
-  // 是否显示删除按钮
-  showDelete: propTypes.bool.def(true),
-  // 是否显示按钮文字
-  showBtnText: propTypes.bool.def(true)
+  showDelete: propTypes.bool.def(true), // 是否显示删除按钮
+  showBtnText: propTypes.bool.def(true) // 是否显示按钮文字
 })
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗

+ 2 - 1
src/components/UploadFile/src/UploadImgs.vue

@@ -6,13 +6,14 @@
       :action="uploadUrl"
       :before-upload="beforeUpload"
       :class="['upload', drag ? 'no-border' : '']"
+      :disabled="disabled"
       :drag="drag"
+      :http-request="httpRequest"
       :limit="limit"
       :multiple="true"
       :on-error="uploadError"
       :on-exceed="handleExceed"
       :on-success="uploadSuccess"
-      :http-request="httpRequest"
       list-type="picture-card"
     >
       <div class="upload-empty">

+ 4 - 4
src/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue

@@ -436,7 +436,7 @@ const initBpmnModeler = () => {
 
   // bpmnModeler.createDiagram()
 
-  console.log(bpmnModeler, 'bpmnModeler111111')
+  // console.log(bpmnModeler, 'bpmnModeler111111')
   emit('init-finished', bpmnModeler)
   initModelListeners()
 }
@@ -666,10 +666,10 @@ const previewProcessJson = () => {
 }
 /* ------------------------------------------------ 芋道源码 methods ------------------------------------------------------ */
 const processSave = async () => {
-  console.log(bpmnModeler, 'bpmnModelerbpmnModelerbpmnModelerbpmnModeler')
+  // console.log(bpmnModeler, 'bpmnModelerbpmnModelerbpmnModelerbpmnModeler')
   const { err, xml } = await bpmnModeler.saveXML()
-  console.log(err, 'errerrerrerrerr')
-  console.log(xml, 'xmlxmlxmlxmlxml')
+  // console.log(err, 'errerrerrerrerr')
+  // console.log(xml, 'xmlxmlxmlxmlxml')
   // 读取异常时抛出异常
   if (err) {
     // this.$modal.msgError('保存模型失败,请重试!')

+ 47 - 30
src/components/bpmnProcessDesigner/package/designer/ProcessViewer.vue

@@ -115,19 +115,19 @@ const highlightDiagram = async () => {
       if (!task) {
         return
       }
-      //进行中的任务已经高亮过了,则不高亮后面的任务了
+      // 进行中的任务已经高亮过了,则不高亮后面的任务了
       if (findProcessTask) {
         removeTaskDefinitionKeyList.push(n.id)
         return
       }
       // 高亮任务
-      canvas.addMarker(n.id, getResultCss(task.result))
+      canvas.addMarker(n.id, getResultCss(task.status))
       //标记是否高亮了进行中任务
-      if (task.result === 1) {
+      if (task.status === 1) {
         findProcessTask = true
       }
       // 如果非通过,就不走后面的线条了
-      if (task.result !== 2) {
+      if (task.status !== 2) {
         return
       }
       // 处理 outgoing 出线
@@ -194,6 +194,7 @@ const highlightDiagram = async () => {
       })
     } else if (n.$type === 'bpmn:StartEvent') {
       // 开始节点
+      canvas.addMarker(n.id, 'highlight')
       n.outgoing?.forEach((nn) => {
         // outgoing 例如说【bpmn:SequenceFlow】连线
         // 获得连线是否有指向目标。如果有,则进行高亮
@@ -205,10 +206,10 @@ const highlightDiagram = async () => {
       })
     } else if (n.$type === 'bpmn:EndEvent') {
       // 结束节点
-      if (!processInstance.value || processInstance.value.result === 1) {
+      if (!processInstance.value || processInstance.value.status === 1) {
         return
       }
-      canvas.addMarker(n.id, getResultCss(processInstance.value.result))
+      canvas.addMarker(n.id, getResultCss(processInstance.value.status))
     } else if (n.$type === 'bpmn:ServiceTask') {
       //服务任务
       if (activity.startTime > 0 && activity.endTime === 0) {
@@ -223,39 +224,49 @@ const highlightDiagram = async () => {
           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.definitionKey)
+      (item) => !removeTaskDefinitionKeyList.includes(item.taskDefinitionKey)
     )
   }
 }
+
 const getActivityHighlightCss = (activity) => {
   return activity.endTime ? 'highlight' : 'highlight-todo'
 }
-const getResultCss = (result) => {
-  if (result === 1) {
+
+const getResultCss = (status) => {
+  if (status === 1) {
     // 审批中
     return 'highlight-todo'
-  } else if (result === 2) {
+  } else if (status === 2) {
     // 已通过
     return 'highlight'
-  } else if (result === 3) {
+  } else if (status === 3) {
     // 不通过
     return 'highlight-reject'
-  } else if (result === 4) {
+  } else if (status === 4) {
     // 已取消
     return 'highlight-cancel'
-  } else if (result === 5) {
+  } else if (status === 5) {
     // 退回
     return 'highlight-return'
-  } else if (result === 6) {
+  } else if (status === 6) {
     // 委派
-    return 'highlight-return'
-  } else if (result === 7 || result === 8 || result === 9) {
-    // 待后加签任务完成/待前加签任务完成/待前置任务完成
-    return 'highlight-return'
+    return 'highlight-todo'
+  } else if (status === 7) {
+    // 审批通过中
+    return 'highlight-todo'
+  } else if (status === 0) {
+    // 待审批
+    return 'highlight-todo'
   }
   return ''
 }
@@ -296,10 +307,10 @@ const elementHover = (element) => {
   !elementOverlayIds.value && (elementOverlayIds.value = {})
   !overlays.value && (overlays.value = bpmnModeler.get('overlays'))
   // 展示信息
-  console.log(activityLists.value, 'activityLists.value')
-  console.log(element.value, 'element.value')
+  // 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')
+  // console.log(activity, 'activityactivityactivityactivity')
   if (!activity) {
     return
   }
@@ -313,15 +324,14 @@ const elementHover = (element) => {
                   <p>部门:${processInstance.value.startUser.deptName}</p>
                   <p>创建时间:${formatDate(processInstance.value.createTime)}`
     } else if (element.value.type === 'bpmn:UserTask') {
-      // debugger
       let task = taskList.value.find((m) => m.id === activity.taskId) // 找到活动对应的 taskId
       if (!task) {
         return
       }
-      let optionData = getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT)
+      let optionData = getIntDictOptions(DICT_TYPE.BPM_TASK_STATUS)
       let dataResult = ''
       optionData.forEach((element) => {
-        if (element.value == task.result) {
+        if (element.value == task.status) {
           dataResult = element.label
         }
       })
@@ -333,7 +343,7 @@ const elementHover = (element) => {
       //             <p>部门:${task.assigneeUser.deptName}</p>
       //             <p>结果:${getIntDictOptions(
       //               DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT,
-      //               task.result
+      //               task.status
       //             )}</p>
       //             <p>创建时间:${formatDate(task.createTime)}</p>`
       if (task.endTime) {
@@ -351,29 +361,30 @@ const elementHover = (element) => {
       }
       console.log(html)
     } else if (element.value.type === 'bpmn:EndEvent' && processInstance.value) {
-      let optionData = getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT)
+      let optionData = getIntDictOptions(DICT_TYPE.BPM_TASK_STATUS)
       let dataResult = ''
       optionData.forEach((element) => {
-        if (element.value == processInstance.value.result) {
+        if (element.value == processInstance.value.status) {
           dataResult = element.label
         }
       })
       html = `<p>结果:${dataResult}</p>`
       // html = `<p>结果:${getIntDictOptions(
       //   DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT,
-      //   processInstance.value.result
+      //   processInstance.value.status
       // )}</p>`
       if (processInstance.value.endTime) {
         html += `<p>结束时间:${formatDate(processInstance.value.endTime)}</p>`
       }
     }
-    console.log(html, 'html111111111111111')
+    // 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 })
@@ -389,6 +400,7 @@ onMounted(() => {
   // 初始模型的监听器
   initModelListeners()
 })
+
 onBeforeUnmount(() => {
   // this.$once('hook:beforeDestroy', () => {
   // })
@@ -427,7 +439,7 @@ watch(
 )
 </script>
 
-<style>
+<style lang="scss">
 /** 处理中 */
 .highlight-todo.djs-connection > .djs-visual > path {
   stroke: #1890ff !important;
@@ -501,6 +513,10 @@ watch(
   stroke: green !important;
 }
 
+.djs-element.highlight > .djs-visual > path {
+  stroke: green !important;
+}
+
 /** 不通过 */
 .highlight-reject.djs-shape .djs-visual > :nth-child(1) {
   fill: red !important;
@@ -520,6 +536,7 @@ watch(
 
 .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) {

+ 10 - 0
src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/activitiDescriptor.json

@@ -332,6 +332,16 @@
           "name": "multiinstance_condition",
           "isAttr": true,
           "type": "String"
+        },
+        {
+          "name": "candidateStrategy",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "candidateParam",
+          "isAttr": true,
+          "type": "String"
         }
       ]
     },

+ 10 - 0
src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/camundaDescriptor.json

@@ -319,6 +319,16 @@
           "name": "priority",
           "isAttr": true,
           "type": "String"
+        },
+        {
+          "name": "candidateStrategy",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "candidateParam",
+          "isAttr": true,
+          "type": "String"
         }
       ]
     },

+ 10 - 0
src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json

@@ -319,6 +319,16 @@
           "name": "priority",
           "isAttr": true,
           "type": "String"
+        },
+        {
+          "name": "candidateStrategy",
+          "isAttr": true,
+          "type": "String"
+        },
+        {
+          "name": "candidateParam",
+          "isAttr": true,
+          "type": "String"
         }
       ]
     },

+ 3 - 8
src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue

@@ -24,15 +24,10 @@
       </el-collapse-item>
       <el-collapse-item name="condition" v-if="formVisible" key="form">
         <template #title><Icon icon="ep:list" />表单</template>
-        <!-- <element-form :id="elementId" :type="elementType" /> -->
-        友情提示:使用
-        <router-link :to="{ path: '/bpm/manager/form' }"
-          ><el-link type="danger">流程表单</el-link>
-        </router-link>
-        替代,提供更好的表单设计功能
+        <element-form :id="elementId" :type="elementType" />
       </el-collapse-item>
       <el-collapse-item name="task" v-if="elementType.indexOf('Task') !== -1" key="task">
-        <template #title><Icon icon="ep:checked" />任务</template>
+        <template #title><Icon icon="ep:checked" />任务(审批人)</template>
         <element-task :id="elementId" :type="elementType" />
       </el-collapse-item>
       <el-collapse-item
@@ -40,7 +35,7 @@
         v-if="elementType.indexOf('Task') !== -1"
         key="multiInstance"
       >
-        <template #title><Icon icon="ep:help-filled" />多实例</template>
+        <template #title><Icon icon="ep:help-filled" />多实例(会签配置)</template>
         <element-multi-instance :business-object="elementBusinessObject" :type="elementType" />
       </el-collapse-item>
       <el-collapse-item name="listeners" key="listeners">

+ 17 - 21
src/components/bpmnProcessDesigner/package/penal/base/ElementBaseInfo.vue

@@ -3,13 +3,6 @@
     <el-form label-width="90px" :model="needProps" :rules="rules">
       <div v-if="needProps.type == 'bpmn:Process'">
         <!-- 如果是 Process 信息的时候,使用自定义表单 -->
-        <el-link
-          href="https://doc.iocoder.cn/bpm/#_3-%E6%B5%81%E7%A8%8B%E5%9B%BE%E7%A4%BA%E4%BE%8B"
-          type="danger"
-          target="_blank"
-        >
-          如何实现实现会签、或签?
-        </el-link>
         <el-form-item label="流程标识" prop="id">
           <el-input
             v-model="needProps.id"
@@ -68,13 +61,13 @@ const resetBaseInfo = () => {
   console.log(bpmnElement.value, 'bpmnElement')
 
   bpmnElement.value = bpmnInstances()?.bpmnElement
-  console.log(bpmnElement.value, 'resetBaseInfo11111111111')
+  // console.log(bpmnElement.value, 'resetBaseInfo11111111111')
   elementBaseInfo.value = bpmnElement.value.businessObject
   needProps.value['type'] = bpmnElement.value.businessObject.$type
   // elementBaseInfo.value['typess'] = bpmnElement.value.businessObject.$type
 
   // elementBaseInfo.value = JSON.parse(JSON.stringify(bpmnElement.value.businessObject))
-  console.log(elementBaseInfo.value, 'elementBaseInfo22222222222')
+  // console.log(elementBaseInfo.value, 'elementBaseInfo22222222222')
 }
 const handleKeyUpdate = (value) => {
   // 校验 value 的值,只有 XML NCName 通过的情况下,才进行赋值。否则,会导致流程图报错,无法绘制的问题
@@ -121,11 +114,11 @@ const updateBaseInfo = (key) => {
   //   id: elementBaseInfo.value[key]
   //   // di: { id: `${elementBaseInfo.value[key]}_di` }
   // }
-  console.log(elementBaseInfo, 'elementBaseInfo11111111111')
+  // console.log(elementBaseInfo, 'elementBaseInfo11111111111')
   needProps.value = { ...elementBaseInfo.value, ...needProps.value }
 
   if (key === 'id') {
-    console.log('jinru')
+    // console.log('jinru')
     console.log(window, 'window')
     console.log(bpmnElement.value, 'bpmnElement')
     console.log(toRaw(bpmnElement.value), 'bpmnElement')
@@ -138,20 +131,11 @@ const updateBaseInfo = (key) => {
     bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), attrObj)
   }
 }
-onMounted(() => {
-  // 针对上传的 bpmn 流程图时,需要延迟 1 秒的时间,保证 key 和 name 的更新
-  setTimeout(() => {
-    console.log(props.model, 'props.model')
-    handleKeyUpdate(props.model.key)
-    handleNameUpdate(props.model.name)
-    console.log(props, 'propsssssssssssssssssssss')
-  }, 1000)
-})
 
 watch(
   () => props.businessObject,
   (val) => {
-    console.log(val, 'val11111111111111111111')
+    // console.log(val, 'val11111111111111111111')
     if (val) {
       // nextTick(() => {
       resetBaseInfo()
@@ -159,6 +143,18 @@ watch(
     }
   }
 )
+
+watch(
+  () => props.model?.key,
+  (val) => {
+    // 针对上传的 bpmn 流程图时,保证 key 和 name 的更新
+    if (val) {
+      handleKeyUpdate(props.model.key)
+      handleNameUpdate(props.model.name)
+    }
+  }
+)
+
 // watch(
 //   () => ({ ...props }),
 //   (oldVal, newVal) => {

+ 220 - 207
src/components/bpmnProcessDesigner/package/penal/form/ElementForm.vue

@@ -1,228 +1,233 @@
 <template>
   <div class="panel-tab__content">
     <el-form label-width="80px">
-      <el-form-item label="表单标识">
-        <el-input v-model="formKey" clearable @change="updateElementFormKey" />
-      </el-form-item>
-      <el-form-item label="业务标识">
-        <el-select v-model="businessKey" @change="updateElementBusinessKey">
-          <el-option v-for="i in fieldList" :key="i.id" :value="i.id" :label="i.label" />
-          <el-option label="无" value="" />
+      <el-form-item label="流程表单">
+        <!--        <el-input v-model="formKey" clearable @change="updateElementFormKey" />-->
+        <el-select v-model="formKey" clearable @change="updateElementFormKey">
+          <el-option v-for="form in formList" :key="form.id" :label="form.name" :value="form.id" />
         </el-select>
       </el-form-item>
+      <!--      <el-form-item label="业务标识">-->
+      <!--        <el-select v-model="businessKey" @change="updateElementBusinessKey">-->
+      <!--          <el-option v-for="i in fieldList" :key="i.id" :value="i.id" :label="i.label" />-->
+      <!--          <el-option label="无" value="" />-->
+      <!--        </el-select>-->
+      <!--      </el-form-item>-->
     </el-form>
 
     <!--字段列表-->
-    <div class="element-property list-property">
-      <el-divider><Icon icon="ep:coin" /> 表单字段</el-divider>
-      <el-table :data="fieldList" max-height="240" fit border>
-        <el-table-column label="序号" type="index" width="50px" />
-        <el-table-column label="字段名称" prop="label" min-width="80px" show-overflow-tooltip />
-        <el-table-column
-          label="字段类型"
-          prop="type"
-          min-width="80px"
-          :formatter="(row) => fieldType[row.type] || row.type"
-          show-overflow-tooltip
-        />
-        <el-table-column
-          label="默认值"
-          prop="defaultValue"
-          min-width="80px"
-          show-overflow-tooltip
-        />
-        <el-table-column label="操作" width="90px">
-          <template #default="scope">
-            <el-button type="primary" link @click="openFieldForm(scope, scope.$index)"
-              >编辑</el-button
-            >
-            <el-divider direction="vertical" />
-            <el-button
-              type="primary"
-              link
-              style="color: #ff4d4f"
-              @click="removeField(scope, scope.$index)"
-              >移除</el-button
-            >
-          </template>
-        </el-table-column>
-      </el-table>
-    </div>
-    <div class="element-drawer__button">
-      <XButton type="primary" proIcon="ep:plus" title="添加字段" @click="openFieldForm(null, -1)" />
-    </div>
+    <!--    <div class="element-property list-property">-->
+    <!--      <el-divider><Icon icon="ep:coin" /> 表单字段</el-divider>-->
+    <!--      <el-table :data="fieldList" max-height="240" fit border>-->
+    <!--        <el-table-column label="序号" type="index" width="50px" />-->
+    <!--        <el-table-column label="字段名称" prop="label" min-width="80px" show-overflow-tooltip />-->
+    <!--        <el-table-column-->
+    <!--          label="字段类型"-->
+    <!--          prop="type"-->
+    <!--          min-width="80px"-->
+    <!--          :formatter="(row) => fieldType[row.type] || row.type"-->
+    <!--          show-overflow-tooltip-->
+    <!--        />-->
+    <!--        <el-table-column-->
+    <!--          label="默认值"-->
+    <!--          prop="defaultValue"-->
+    <!--          min-width="80px"-->
+    <!--          show-overflow-tooltip-->
+    <!--        />-->
+    <!--        <el-table-column label="操作" width="90px">-->
+    <!--          <template #default="scope">-->
+    <!--            <el-button type="primary" link @click="openFieldForm(scope, scope.$index)"-->
+    <!--              >编辑</el-button-->
+    <!--            >-->
+    <!--            <el-divider direction="vertical" />-->
+    <!--            <el-button-->
+    <!--              type="primary"-->
+    <!--              link-->
+    <!--              style="color: #ff4d4f"-->
+    <!--              @click="removeField(scope, scope.$index)"-->
+    <!--              >移除</el-button-->
+    <!--            >-->
+    <!--          </template>-->
+    <!--        </el-table-column>-->
+    <!--      </el-table>-->
+    <!--    </div>-->
+    <!--    <div class="element-drawer__button">-->
+    <!--      <XButton type="primary" proIcon="ep:plus" title="添加字段" @click="openFieldForm(null, -1)" />-->
+    <!--    </div>-->
 
     <!--字段配置侧边栏-->
-    <el-drawer
-      v-model="fieldModelVisible"
-      title="字段配置"
-      :size="`${width}px`"
-      append-to-body
-      destroy-on-close
-    >
-      <el-form :model="formFieldForm" label-width="90px">
-        <el-form-item label="字段ID">
-          <el-input v-model="formFieldForm.id" clearable />
-        </el-form-item>
-        <el-form-item label="类型">
-          <el-select
-            v-model="formFieldForm.typeType"
-            placeholder="请选择字段类型"
-            clearable
-            @change="changeFieldTypeType"
-          >
-            <el-option v-for="(value, key) of fieldType" :label="value" :value="key" :key="key" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="类型名称" v-if="formFieldForm.typeType === 'custom'">
-          <el-input v-model="formFieldForm.type" clearable />
-        </el-form-item>
-        <el-form-item label="名称">
-          <el-input v-model="formFieldForm.label" clearable />
-        </el-form-item>
-        <el-form-item label="时间格式" v-if="formFieldForm.typeType === 'date'">
-          <el-input v-model="formFieldForm.datePattern" clearable />
-        </el-form-item>
-        <el-form-item label="默认值">
-          <el-input v-model="formFieldForm.defaultValue" clearable />
-        </el-form-item>
-      </el-form>
+    <!--    <el-drawer-->
+    <!--      v-model="fieldModelVisible"-->
+    <!--      title="字段配置"-->
+    <!--      :size="`${width}px`"-->
+    <!--      append-to-body-->
+    <!--      destroy-on-close-->
+    <!--    >-->
+    <!--      <el-form :model="formFieldForm" label-width="90px">-->
+    <!--        <el-form-item label="字段ID">-->
+    <!--          <el-input v-model="formFieldForm.id" clearable />-->
+    <!--        </el-form-item>-->
+    <!--        <el-form-item label="类型">-->
+    <!--          <el-select-->
+    <!--            v-model="formFieldForm.typeType"-->
+    <!--            placeholder="请选择字段类型"-->
+    <!--            clearable-->
+    <!--            @change="changeFieldTypeType"-->
+    <!--          >-->
+    <!--            <el-option v-for="(value, key) of fieldType" :label="value" :value="key" :key="key" />-->
+    <!--          </el-select>-->
+    <!--        </el-form-item>-->
+    <!--        <el-form-item label="类型名称" v-if="formFieldForm.typeType === 'custom'">-->
+    <!--          <el-input v-model="formFieldForm.type" clearable />-->
+    <!--        </el-form-item>-->
+    <!--        <el-form-item label="名称">-->
+    <!--          <el-input v-model="formFieldForm.label" clearable />-->
+    <!--        </el-form-item>-->
+    <!--        <el-form-item label="时间格式" v-if="formFieldForm.typeType === 'date'">-->
+    <!--          <el-input v-model="formFieldForm.datePattern" clearable />-->
+    <!--        </el-form-item>-->
+    <!--        <el-form-item label="默认值">-->
+    <!--          <el-input v-model="formFieldForm.defaultValue" clearable />-->
+    <!--        </el-form-item>-->
+    <!--      </el-form>-->
 
-      <!-- 枚举值设置 -->
-      <template v-if="formFieldForm.type === 'enum'">
-        <el-divider key="enum-divider" />
-        <p class="listener-filed__title" key="enum-title">
-          <span><Icon icon="ep:menu" />枚举值列表:</span>
-          <el-button type="primary" @click="openFieldOptionForm(null, -1, 'enum')"
-            >添加枚举值</el-button
-          >
-        </p>
-        <el-table :data="fieldEnumList" key="enum-table" max-height="240" fit border>
-          <el-table-column label="序号" width="50px" type="index" />
-          <el-table-column label="枚举值编号" prop="id" min-width="100px" show-overflow-tooltip />
-          <el-table-column label="枚举值名称" prop="name" min-width="100px" show-overflow-tooltip />
-          <el-table-column label="操作" width="90px">
-            <template #default="scope">
-              <el-button
-                type="primary"
-                link
-                @click="openFieldOptionForm(scope, scope.$index, 'enum')"
-                >编辑</el-button
-              >
-              <el-divider direction="vertical" />
-              <el-button
-                type="primary"
-                link
-                style="color: #ff4d4f"
-                @click="removeFieldOptionItem(scope, scope.$index, 'enum')"
-                >移除</el-button
-              >
-            </template>
-          </el-table-column>
-        </el-table>
-      </template>
+    <!--      &lt;!&ndash; 枚举值设置 &ndash;&gt;-->
+    <!--      <template v-if="formFieldForm.type === 'enum'">-->
+    <!--        <el-divider key="enum-divider" />-->
+    <!--        <p class="listener-filed__title" key="enum-title">-->
+    <!--          <span><Icon icon="ep:menu" />枚举值列表:</span>-->
+    <!--          <el-button type="primary" @click="openFieldOptionForm(null, -1, 'enum')"-->
+    <!--            >添加枚举值</el-button-->
+    <!--          >-->
+    <!--        </p>-->
+    <!--        <el-table :data="fieldEnumList" key="enum-table" max-height="240" fit border>-->
+    <!--          <el-table-column label="序号" width="50px" type="index" />-->
+    <!--          <el-table-column label="枚举值编号" prop="id" min-width="100px" show-overflow-tooltip />-->
+    <!--          <el-table-column label="枚举值名称" prop="name" min-width="100px" show-overflow-tooltip />-->
+    <!--          <el-table-column label="操作" width="90px">-->
+    <!--            <template #default="scope">-->
+    <!--              <el-button-->
+    <!--                type="primary"-->
+    <!--                link-->
+    <!--                @click="openFieldOptionForm(scope, scope.$index, 'enum')"-->
+    <!--                >编辑</el-button-->
+    <!--              >-->
+    <!--              <el-divider direction="vertical" />-->
+    <!--              <el-button-->
+    <!--                type="primary"-->
+    <!--                link-->
+    <!--                style="color: #ff4d4f"-->
+    <!--                @click="removeFieldOptionItem(scope, scope.$index, 'enum')"-->
+    <!--                >移除</el-button-->
+    <!--              >-->
+    <!--            </template>-->
+    <!--          </el-table-column>-->
+    <!--        </el-table>-->
+    <!--      </template>-->
 
-      <!-- 校验规则 -->
-      <el-divider key="validation-divider" />
-      <p class="listener-filed__title" key="validation-title">
-        <span><Icon icon="ep:menu" />约束条件列表:</span>
-        <el-button type="primary" @click="openFieldOptionForm(null, -1, 'constraint')"
-          >添加约束</el-button
-        >
-      </p>
-      <el-table :data="fieldConstraintsList" key="validation-table" max-height="240" fit border>
-        <el-table-column label="序号" width="50px" type="index" />
-        <el-table-column label="约束名称" prop="name" min-width="100px" show-overflow-tooltip />
-        <el-table-column label="约束配置" prop="config" min-width="100px" show-overflow-tooltip />
-        <el-table-column label="操作" width="90px">
-          <template #default="scope">
-            <el-button
-              type="primary"
-              link
-              @click="openFieldOptionForm(scope, scope.$index, 'constraint')"
-              >编辑</el-button
-            >
-            <el-divider direction="vertical" />
-            <el-button
-              type="primary"
-              link
-              style="color: #ff4d4f"
-              @click="removeFieldOptionItem(scope, scope.$index, 'constraint')"
-              >移除</el-button
-            >
-          </template>
-        </el-table-column>
-      </el-table>
+    <!--      &lt;!&ndash; 校验规则 &ndash;&gt;-->
+    <!--      <el-divider key="validation-divider" />-->
+    <!--      <p class="listener-filed__title" key="validation-title">-->
+    <!--        <span><Icon icon="ep:menu" />约束条件列表:</span>-->
+    <!--        <el-button type="primary" @click="openFieldOptionForm(null, -1, 'constraint')"-->
+    <!--          >添加约束</el-button-->
+    <!--        >-->
+    <!--      </p>-->
+    <!--      <el-table :data="fieldConstraintsList" key="validation-table" max-height="240" fit border>-->
+    <!--        <el-table-column label="序号" width="50px" type="index" />-->
+    <!--        <el-table-column label="约束名称" prop="name" min-width="100px" show-overflow-tooltip />-->
+    <!--        <el-table-column label="约束配置" prop="config" min-width="100px" show-overflow-tooltip />-->
+    <!--        <el-table-column label="操作" width="90px">-->
+    <!--          <template #default="scope">-->
+    <!--            <el-button-->
+    <!--              type="primary"-->
+    <!--              link-->
+    <!--              @click="openFieldOptionForm(scope, scope.$index, 'constraint')"-->
+    <!--              >编辑</el-button-->
+    <!--            >-->
+    <!--            <el-divider direction="vertical" />-->
+    <!--            <el-button-->
+    <!--              type="primary"-->
+    <!--              link-->
+    <!--              style="color: #ff4d4f"-->
+    <!--              @click="removeFieldOptionItem(scope, scope.$index, 'constraint')"-->
+    <!--              >移除</el-button-->
+    <!--            >-->
+    <!--          </template>-->
+    <!--        </el-table-column>-->
+    <!--      </el-table>-->
 
-      <!-- 表单属性 -->
-      <el-divider key="property-divider" />
-      <p class="listener-filed__title" key="property-title">
-        <span><Icon icon="ep:menu" />字段属性列表:</span>
-        <el-button type="primary" @click="openFieldOptionForm(null, -1, 'property')"
-          >添加属性</el-button
-        >
-      </p>
-      <el-table :data="fieldPropertiesList" key="property-table" max-height="240" fit border>
-        <el-table-column label="序号" width="50px" type="index" />
-        <el-table-column label="属性编号" prop="id" min-width="100px" show-overflow-tooltip />
-        <el-table-column label="属性值" prop="value" min-width="100px" show-overflow-tooltip />
-        <el-table-column label="操作" width="90px">
-          <template #default="scope">
-            <el-button
-              type="primary"
-              link
-              @click="openFieldOptionForm(scope, scope.$index, 'property')"
-              >编辑</el-button
-            >
-            <el-divider direction="vertical" />
-            <el-button
-              type="primary"
-              link
-              style="color: #ff4d4f"
-              @click="removeFieldOptionItem(scope, scope.$index, 'property')"
-              >移除</el-button
-            >
-          </template>
-        </el-table-column>
-      </el-table>
+    <!--      &lt;!&ndash; 表单属性 &ndash;&gt;-->
+    <!--      <el-divider key="property-divider" />-->
+    <!--      <p class="listener-filed__title" key="property-title">-->
+    <!--        <span><Icon icon="ep:menu" />字段属性列表:</span>-->
+    <!--        <el-button type="primary" @click="openFieldOptionForm(null, -1, 'property')"-->
+    <!--          >添加属性</el-button-->
+    <!--        >-->
+    <!--      </p>-->
+    <!--      <el-table :data="fieldPropertiesList" key="property-table" max-height="240" fit border>-->
+    <!--        <el-table-column label="序号" width="50px" type="index" />-->
+    <!--        <el-table-column label="属性编号" prop="id" min-width="100px" show-overflow-tooltip />-->
+    <!--        <el-table-column label="属性值" prop="value" min-width="100px" show-overflow-tooltip />-->
+    <!--        <el-table-column label="操作" width="90px">-->
+    <!--          <template #default="scope">-->
+    <!--            <el-button-->
+    <!--              type="primary"-->
+    <!--              link-->
+    <!--              @click="openFieldOptionForm(scope, scope.$index, 'property')"-->
+    <!--              >编辑</el-button-->
+    <!--            >-->
+    <!--            <el-divider direction="vertical" />-->
+    <!--            <el-button-->
+    <!--              type="primary"-->
+    <!--              link-->
+    <!--              style="color: #ff4d4f"-->
+    <!--              @click="removeFieldOptionItem(scope, scope.$index, 'property')"-->
+    <!--              >移除</el-button-->
+    <!--            >-->
+    <!--          </template>-->
+    <!--        </el-table-column>-->
+    <!--      </el-table>-->
 
-      <!-- 底部按钮 -->
-      <div class="element-drawer__button">
-        <el-button>取 消</el-button>
-        <el-button type="primary" @click="saveField">保 存</el-button>
-      </div>
-    </el-drawer>
+    <!--      &lt;!&ndash; 底部按钮 &ndash;&gt;-->
+    <!--      <div class="element-drawer__button">-->
+    <!--        <el-button>取 消</el-button>-->
+    <!--        <el-button type="primary" @click="saveField">保 存</el-button>-->
+    <!--      </div>-->
+    <!--    </el-drawer>-->
 
-    <el-dialog
-      v-model="fieldOptionModelVisible"
-      :title="optionModelTitle"
-      width="600px"
-      append-to-body
-      destroy-on-close
-    >
-      <el-form :model="fieldOptionForm" label-width="96px">
-        <el-form-item label="编号/ID" v-if="fieldOptionType !== 'constraint'" key="option-id">
-          <el-input v-model="fieldOptionForm.id" clearable />
-        </el-form-item>
-        <el-form-item label="名称" v-if="fieldOptionType !== 'property'" key="option-name">
-          <el-input v-model="fieldOptionForm.name" clearable />
-        </el-form-item>
-        <el-form-item label="配置" v-if="fieldOptionType === 'constraint'" key="option-config">
-          <el-input v-model="fieldOptionForm.config" clearable />
-        </el-form-item>
-        <el-form-item label="值" v-if="fieldOptionType === 'property'" key="option-value">
-          <el-input v-model="fieldOptionForm.value" clearable />
-        </el-form-item>
-      </el-form>
-      <template #footer>
-        <el-button @click="fieldOptionModelVisible = false">取 消</el-button>
-        <el-button type="primary" @click="saveFieldOption">确 定</el-button>
-      </template>
-    </el-dialog>
+    <!--    <el-dialog-->
+    <!--      v-model="fieldOptionModelVisible"-->
+    <!--      :title="optionModelTitle"-->
+    <!--      width="600px"-->
+    <!--      append-to-body-->
+    <!--      destroy-on-close-->
+    <!--    >-->
+    <!--      <el-form :model="fieldOptionForm" label-width="96px">-->
+    <!--        <el-form-item label="编号/ID" v-if="fieldOptionType !== 'constraint'" key="option-id">-->
+    <!--          <el-input v-model="fieldOptionForm.id" clearable />-->
+    <!--        </el-form-item>-->
+    <!--        <el-form-item label="名称" v-if="fieldOptionType !== 'property'" key="option-name">-->
+    <!--          <el-input v-model="fieldOptionForm.name" clearable />-->
+    <!--        </el-form-item>-->
+    <!--        <el-form-item label="配置" v-if="fieldOptionType === 'constraint'" key="option-config">-->
+    <!--          <el-input v-model="fieldOptionForm.config" clearable />-->
+    <!--        </el-form-item>-->
+    <!--        <el-form-item label="值" v-if="fieldOptionType === 'property'" key="option-value">-->
+    <!--          <el-input v-model="fieldOptionForm.value" clearable />-->
+    <!--        </el-form-item>-->
+    <!--      </el-form>-->
+    <!--      <template #footer>-->
+    <!--        <el-button @click="fieldOptionModelVisible = false">取 消</el-button>-->
+    <!--        <el-button type="primary" @click="saveFieldOption">确 定</el-button>-->
+    <!--      </template>-->
+    <!--    </el-dialog>-->
   </div>
 </template>
 
 <script lang="ts" setup>
+import * as FormApi from '@/api/bpm/form'
+
 defineOptions({ name: 'ElementForm' })
 
 const props = defineProps({
@@ -263,6 +268,9 @@ const bpmnInstances = () => (window as any)?.bpmnInstances
 const resetFormList = () => {
   bpmnELement.value = bpmnInstances().bpmnElement
   formKey.value = bpmnELement.value.businessObject.formKey
+  if (formKey.value?.length > 0) {
+    formKey.value = parseInt(formKey.value)
+  }
   // 获取元素扩展属性 或者 创建扩展属性
   elExtensionElements.value =
     bpmnELement.value.businessObject.get('extensionElements') ||
@@ -421,7 +429,7 @@ const saveField = () => {
 
 // 移除某个 字段的 配置项
 const removeFieldOptionItem = (option, index, type) => {
-  console.log(option, 'option')
+  // console.log(option, 'option')
   if (type === 'property') {
     fieldPropertiesList.value.splice(index, 1)
     return
@@ -451,6 +459,11 @@ const updateElementExtensions = () => {
   })
 }
 
+const formList = ref([]) // 流程表单的下拉框的数据
+onMounted(async () => {
+  formList.value = await FormApi.getFormSimpleList()
+})
+
 watch(
   () => props.id,
   (val) => {

+ 46 - 1
src/components/bpmnProcessDesigner/package/penal/listeners/ElementListeners.vue

@@ -26,8 +26,16 @@
         type="primary"
         preIcon="ep:plus"
         title="添加监听器"
+        size="small"
         @click="openListenerForm(null)"
       />
+      <XButton
+        type="success"
+        preIcon="ep:select"
+        title="选择监听器"
+        size="small"
+        @click="openProcessListenerDialog"
+      />
     </div>
 
     <!-- 监听器 编辑/创建 部分 -->
@@ -240,11 +248,21 @@
       </template>
     </el-dialog>
   </div>
+
+  <!-- 选择弹窗 -->
+  <ProcessListenerDialog ref="processListenerDialogRef" @select="selectProcessListener" />
 </template>
 <script lang="ts" setup>
 import { ElMessageBox } from 'element-plus'
 import { createListenerObject, updateElementExtensions } from '../../utils'
-import { initListenerType, initListenerForm, listenerType, fieldType } from './utilSelf'
+import {
+  initListenerType,
+  initListenerForm,
+  listenerType,
+  fieldType,
+  initListenerForm2
+} from './utilSelf'
+import ProcessListenerDialog from './ProcessListenerDialog.vue'
 
 defineOptions({ name: 'ElementListeners' })
 
@@ -284,6 +302,7 @@ const resetListenersList = () => {
 }
 // 打开 监听器详情 侧边栏
 const openListenerForm = (listener, index?) => {
+  // debugger
   if (listener) {
     listenerForm.value = initListenerForm(listener)
     editingListenerIndex.value = index
@@ -321,6 +340,7 @@ const openListenerFieldForm = (field, index?) => {
 }
 // 保存监听器注入字段
 const saveListenerFiled = async () => {
+  // debugger
   let validateStatus = await listenerFieldFormRef.value.validate()
   if (!validateStatus) return // 验证不通过直接返回
   if (editingListenerFieldIndex.value === -1) {
@@ -337,6 +357,7 @@ const saveListenerFiled = async () => {
 }
 // 移除监听器字段
 const removeListenerField = (index) => {
+  // debugger
   ElMessageBox.confirm('确认移除该字段吗?', '提示', {
     confirmButtonText: '确 认',
     cancelButtonText: '取 消'
@@ -349,6 +370,7 @@ const removeListenerField = (index) => {
 }
 // 移除监听器
 const removeListener = (index) => {
+  debugger
   ElMessageBox.confirm('确认移除该监听器吗?', '提示', {
     confirmButtonText: '确 认',
     cancelButtonText: '取 消'
@@ -365,6 +387,7 @@ const removeListener = (index) => {
 }
 // 保存监听器配置
 const saveListenerConfig = async () => {
+  // debugger
   let validateStatus = await listenerFormRef.value.validate()
   if (!validateStatus) return // 验证不通过直接返回
   const listenerObject = createListenerObject(listenerForm.value, false, prefix)
@@ -389,6 +412,28 @@ const saveListenerConfig = async () => {
   listenerForm.value = {}
 }
 
+// 打开监听器弹窗
+const processListenerDialogRef = ref()
+const openProcessListenerDialog = async () => {
+  processListenerDialogRef.value.open('execution')
+}
+const selectProcessListener = (listener) => {
+  const listenerForm = initListenerForm2(listener)
+  const listenerObject = createListenerObject(listenerForm, false, prefix)
+  bpmnElementListeners.value.push(listenerObject)
+  elementListenersList.value.push(listenerForm)
+
+  // 保存其他配置
+  otherExtensionList.value =
+    bpmnElement.value.businessObject?.extensionElements?.values?.filter(
+      (ex) => ex.$type !== `${prefix}:ExecutionListener`
+    ) ?? []
+  updateElementExtensions(
+    bpmnElement.value,
+    otherExtensionList.value.concat(bpmnElementListeners.value)
+  )
+}
+
 watch(
   () => props.id,
   (val) => {

+ 83 - 0
src/components/bpmnProcessDesigner/package/penal/listeners/ProcessListenerDialog.vue

@@ -0,0 +1,83 @@
+<!-- 执行器选择 -->
+<template>
+  <Dialog title="请选择监听器" v-model="dialogVisible" width="1024px">
+    <ContentWrap>
+      <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+        <el-table-column label="名字" align="center" prop="name" />
+        <el-table-column label="类型" align="center" prop="type">
+          <template #default="scope">
+            <dict-tag :type="DICT_TYPE.BPM_PROCESS_LISTENER_TYPE" :value="scope.row.type" />
+          </template>
+        </el-table-column>
+        <el-table-column label="事件" align="center" prop="event" />
+        <el-table-column label="值类型" align="center" prop="valueType">
+          <template #default="scope">
+            <dict-tag
+              :type="DICT_TYPE.BPM_PROCESS_LISTENER_VALUE_TYPE"
+              :value="scope.row.valueType"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column label="值" align="center" prop="value" />
+        <el-table-column label="操作" align="center">
+          <template #default="scope">
+            <el-button link type="primary" @click="select(scope.row)"> 选择 </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <!-- 分页 -->
+      <Pagination
+        :total="total"
+        v-model:page="queryParams.pageNo"
+        v-model:limit="queryParams.pageSize"
+        @pagination="getList"
+      />
+    </ContentWrap>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { ProcessListenerApi, ProcessListenerVO } from '@/api/bpm/processListener'
+import { DICT_TYPE } from '@/utils/dict'
+import { CommonStatusEnum } from '@/utils/constants'
+
+/** BPM 流程 表单 */
+defineOptions({ name: 'ProcessListenerDialog' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const loading = ref(true) // 列表的加载中
+const list = ref<ProcessListenerVO[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  type: undefined,
+  status: CommonStatusEnum.ENABLE
+})
+
+/** 打开弹窗 */
+const open = async (type: string) => {
+  dialogVisible.value = true
+  loading.value = true
+  try {
+    queryParams.pageNo = 1
+    queryParams.type = type
+    const data = await ProcessListenerApi.getProcessListenerPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const select = async (row) => {
+  dialogVisible.value = false
+  // 发送操作成功的事件
+  emit('select', row)
+}
+</script>

+ 41 - 1
src/components/bpmnProcessDesigner/package/penal/listeners/UserTaskListeners.vue

@@ -39,6 +39,13 @@
         title="添加监听器"
         @click="openListenerForm(null)"
       />
+      <XButton
+        type="success"
+        preIcon="ep:select"
+        title="选择监听器"
+        size="small"
+        @click="openProcessListenerDialog"
+      />
     </div>
 
     <!-- 监听器 编辑/创建 部分 -->
@@ -286,11 +293,22 @@
       </template>
     </el-dialog>
   </div>
+
+  <!-- 选择弹窗 -->
+  <ProcessListenerDialog ref="processListenerDialogRef" @select="selectProcessListener" />
 </template>
 <script lang="ts" setup>
 import { ElMessageBox } from 'element-plus'
 import { createListenerObject, updateElementExtensions } from '../../utils'
-import { initListenerForm, initListenerType, eventType, listenerType, fieldType } from './utilSelf'
+import {
+  initListenerForm,
+  initListenerType,
+  eventType,
+  listenerType,
+  fieldType,
+  initListenerForm2
+} from './utilSelf'
+import ProcessListenerDialog from '@/components/bpmnProcessDesigner/package/penal/listeners/ProcessListenerDialog.vue'
 
 defineOptions({ name: 'UserTaskListeners' })
 
@@ -437,6 +455,28 @@ const removeListenerField = (field, index) => {
     .catch(() => console.info('操作取消'))
 }
 
+// 打开监听器弹窗
+const processListenerDialogRef = ref()
+const openProcessListenerDialog = async () => {
+  processListenerDialogRef.value.open('task')
+}
+const selectProcessListener = (listener) => {
+  const listenerForm = initListenerForm2(listener)
+  const listenerObject = createListenerObject(listenerForm, true, prefix)
+  bpmnElementListeners.value.push(listenerObject)
+  elementListenersList.value.push(listenerForm)
+
+  // 保存其他配置
+  otherExtensionList.value =
+    bpmnElement.value.businessObject?.extensionElements?.values?.filter(
+      (ex) => ex.$type !== `${prefix}:TaskListener`
+    ) ?? []
+  updateElementExtensions(
+    bpmnElement.value,
+    otherExtensionList.value.concat(bpmnElementListeners.value)
+  )
+}
+
 watch(
   () => props.id,
   (val) => {

+ 27 - 0
src/components/bpmnProcessDesigner/package/penal/listeners/utilSelf.ts

@@ -40,6 +40,33 @@ export function initListenerType(listener) {
   }
 }
 
+/** 将 ProcessListenerDO 转换成 initListenerForm 想同的 Form 对象 */
+export function initListenerForm2(processListener) {
+  if (processListener.valueType === 'class') {
+    return {
+      listenerType: 'classListener',
+      class: processListener.value,
+      event: processListener.event,
+      fields: []
+    }
+  } else if (processListener.valueType === 'expression') {
+    return {
+      listenerType: 'expressionListener',
+      expression: processListener.value,
+      event: processListener.event,
+      fields: []
+    }
+  } else if (processListener.valueType === 'delegateExpression') {
+    return {
+      listenerType: 'delegateExpressionListener',
+      delegateExpression: processListener.value,
+      event: processListener.event,
+      fields: []
+    }
+  }
+  throw new Error('未知的监听器类型')
+}
+
 export const listenerType = {
   classListener: 'Java 类',
   expressionListener: '表达式',

+ 31 - 5
src/components/bpmnProcessDesigner/package/penal/multi-instance/ElementMultiInstance.vue

@@ -1,11 +1,15 @@
 <template>
   <div class="panel-tab__content">
     <el-form label-width="90px">
-      <el-form-item label="回路特性">
+      <el-form-item label="快捷配置">
+        <el-button size="small" @click="changeConfig('依次审批')">依次审批</el-button>
+        <el-button size="small" @click="changeConfig('会签')">会签</el-button>
+        <el-button size="small" @click="changeConfig('或签')">或签</el-button>
+      </el-form-item>
+      <el-form-item label="会签类型">
         <el-select v-model="loopCharacteristics" @change="changeLoopCharacteristicsType">
           <el-option label="并行多重事件" value="ParallelMultiInstance" />
           <el-option label="时序多重事件" value="SequentialMultiInstance" />
-          <el-option label="循环事件" value="StandardLoop" />
           <el-option label="无" value="Null" />
         </el-select>
       </el-form-item>
@@ -15,7 +19,7 @@
           loopCharacteristics === 'SequentialMultiInstance'
         "
       >
-        <el-form-item label="循环数" key="loopCardinality">
+        <el-form-item label="循环数" key="loopCardinality">
           <el-input
             v-model="loopInstanceForm.loopCardinality"
             clearable
@@ -25,7 +29,8 @@
         <el-form-item label="集合" key="collection" v-show="false">
           <el-input v-model="loopInstanceForm.collection" clearable @change="updateLoopBase" />
         </el-form-item>
-        <el-form-item label="元素变量" key="elementVariable">
+        <!-- add by 芋艿:由于「元素变量」暂时用不到,所以这里 display 为 none -->
+        <el-form-item label="元素变量" key="elementVariable" style="display: none">
           <el-input v-model="loopInstanceForm.elementVariable" clearable @change="updateLoopBase" />
         </el-form-item>
         <el-form-item label="完成条件" key="completionCondition">
@@ -35,7 +40,8 @@
             @change="updateLoopCondition"
           />
         </el-form-item>
-        <el-form-item label="异步状态" key="async">
+        <!-- add by 芋艿:由于「异步状态」暂时用不到,所以这里 display 为 none -->
+        <el-form-item label="异步状态" key="async" style="display: none">
           <el-checkbox
             v-model="loopInstanceForm.asyncBefore"
             label="异步前"
@@ -124,6 +130,7 @@ const getElementLoop = (businessObject) => {
       businessObject.loopCharacteristics.extensionElements.values[0].body
   }
 }
+
 const changeLoopCharacteristicsType = (type) => {
   // this.loopInstanceForm = { ...this.defaultLoopInstanceForm }; // 切换类型取消原表单配置
   // 取消多实例配置
@@ -160,6 +167,7 @@ const changeLoopCharacteristicsType = (type) => {
     loopCharacteristics: toRaw(multiLoopInstance.value)
   })
 }
+
 // 循环基数
 const updateLoopCardinality = (cardinality) => {
   let loopCardinality = null
@@ -176,6 +184,7 @@ const updateLoopCardinality = (cardinality) => {
     }
   )
 }
+
 // 完成条件
 const updateLoopCondition = (condition) => {
   let completionCondition = null
@@ -192,6 +201,7 @@ const updateLoopCondition = (condition) => {
     }
   )
 }
+
 // 重试周期
 const updateLoopTimeCycle = (timeCycle) => {
   const extensionElements = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
@@ -209,6 +219,7 @@ const updateLoopTimeCycle = (timeCycle) => {
     }
   )
 }
+
 // 直接更新的基础信息
 const updateLoopBase = () => {
   bpmnInstances().modeling.updateModdleProperties(
@@ -220,6 +231,7 @@ const updateLoopBase = () => {
     }
   )
 }
+
 // 各异步状态
 const updateLoopAsync = (key) => {
   const { asyncBefore, asyncAfter } = loopInstanceForm.value
@@ -238,6 +250,20 @@ const updateLoopAsync = (key) => {
   )
 }
 
+const changeConfig = (config) => {
+  if (config === '依次审批') {
+    changeLoopCharacteristicsType('SequentialMultiInstance')
+    updateLoopCardinality('1')
+    updateLoopCondition('${ nrOfCompletedInstances >= nrOfInstances }')
+  } else if (config === '会签') {
+    changeLoopCharacteristicsType('ParallelMultiInstance')
+    updateLoopCondition('${ nrOfCompletedInstances >= nrOfInstances }')
+  } else if (config === '或签') {
+    changeLoopCharacteristicsType('ParallelMultiInstance')
+    updateLoopCondition('${ nrOfCompletedInstances > 0 }')
+  }
+}
+
 onBeforeUnmount(() => {
   multiLoopInstance.value = null
   bpmnElement.value = null

+ 2 - 1
src/components/bpmnProcessDesigner/package/penal/task/ElementTask.vue

@@ -1,7 +1,8 @@
 <template>
   <div class="panel-tab__content">
     <el-form size="small" label-width="90px">
-      <el-form-item label="异步延续">
+      <!-- add by 芋艿:由于「异步延续」暂时用不到,所以这里 display 为 none -->
+      <el-form-item label="异步延续" style="display: none">
         <el-checkbox
           v-model="taskConfigForm.asyncBefore"
           label="异步前"

+ 68 - 0
src/components/bpmnProcessDesigner/package/penal/task/task-components/ProcessExpressionDialog.vue

@@ -0,0 +1,68 @@
+<!-- 表达式选择 -->
+<template>
+  <Dialog title="请选择表达式" v-model="dialogVisible" width="1024px">
+    <ContentWrap>
+      <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+        <el-table-column label="名字" align="center" prop="name" />
+        <el-table-column label="表达式" align="center" prop="expression" />
+        <el-table-column label="操作" align="center">
+          <template #default="scope">
+            <el-button link type="primary" @click="select(scope.row)"> 选择 </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <!-- 分页 -->
+      <Pagination
+        :total="total"
+        v-model:page="queryParams.pageNo"
+        v-model:limit="queryParams.pageSize"
+        @pagination="getList"
+      />
+    </ContentWrap>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { CommonStatusEnum } from '@/utils/constants'
+import { ProcessExpressionApi, ProcessExpressionVO } from '@/api/bpm/processExpression'
+
+/** BPM 流程 表单 */
+defineOptions({ name: 'ProcessExpressionDialog' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const loading = ref(true) // 列表的加载中
+const list = ref<ProcessExpressionVO[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  type: undefined,
+  status: CommonStatusEnum.ENABLE
+})
+
+/** 打开弹窗 */
+const open = async (type: string) => {
+  dialogVisible.value = true
+  loading.value = true
+  try {
+    queryParams.pageNo = 1
+    queryParams.type = type
+    const data = await ProcessExpressionApi.getProcessExpressionPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const select = async (row) => {
+  dialogVisible.value = false
+  // 发送操作成功的事件
+  emit('select', row)
+}
+</script>

+ 193 - 59
src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue

@@ -1,85 +1,204 @@
 <template>
-  <div style="margin-top: 16px">
-    <!--    <el-form-item label="处理用户">-->
-    <!--      <el-select v-model="userTaskForm.assignee" @change="updateElementTask('assignee')">-->
-    <!--        <el-option v-for="ak in mockData" :key="'ass-' + ak" :label="`用户${ak}`" :value="`user${ak}`" />-->
-    <!--      </el-select>-->
-    <!--    </el-form-item>-->
-    <!--    <el-form-item label="候选用户">-->
-    <!--      <el-select v-model="userTaskForm.candidateUsers" multiple collapse-tags @change="updateElementTask('candidateUsers')">-->
-    <!--        <el-option v-for="uk in mockData" :key="'user-' + uk" :label="`用户${uk}`" :value="`user${uk}`" />-->
-    <!--      </el-select>-->
-    <!--    </el-form-item>-->
-    <!--    <el-form-item label="候选分组">-->
-    <!--      <el-select v-model="userTaskForm.candidateGroups" multiple collapse-tags @change="updateElementTask('candidateGroups')">-->
-    <!--        <el-option v-for="gk in mockData" :key="'ass-' + gk" :label="`分组${gk}`" :value="`group${gk}`" />-->
-    <!--      </el-select>-->
-    <!--    </el-form-item>-->
-    <el-form-item label="到期时间">
-      <el-input v-model="userTaskForm.dueDate" clearable @change="updateElementTask('dueDate')" />
+  <el-form label-width="100px">
+    <el-form-item label="规则类型" prop="candidateStrategy">
+      <el-select
+        v-model="userTaskForm.candidateStrategy"
+        clearable
+        style="width: 100%"
+        @change="changeCandidateStrategy"
+      >
+        <el-option
+          v-for="dict in getIntDictOptions(DICT_TYPE.BPM_TASK_CANDIDATE_STRATEGY)"
+          :key="dict.value"
+          :label="dict.label"
+          :value="dict.value"
+        />
+      </el-select>
     </el-form-item>
-    <el-form-item label="跟踪时间">
-      <el-input
-        v-model="userTaskForm.followUpDate"
+    <el-form-item
+      v-if="userTaskForm.candidateStrategy == 10"
+      label="指定角色"
+      prop="candidateParam"
+    >
+      <el-select
+        v-model="userTaskForm.candidateParam"
         clearable
-        @change="updateElementTask('followUpDate')"
+        multiple
+        style="width: 100%"
+        @change="updateElementTask"
+      >
+        <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="userTaskForm.candidateStrategy == 20 || userTaskForm.candidateStrategy == 21"
+      label="指定部门"
+      prop="candidateParam"
+      span="24"
+    >
+      <el-tree-select
+        ref="treeRef"
+        v-model="userTaskForm.candidateParam"
+        :data="deptTreeOptions"
+        :props="defaultProps"
+        empty-text="加载中,请稍后"
+        multiple
+        node-key="id"
+        show-checkbox
+        @change="updateElementTask"
       />
     </el-form-item>
-    <el-form-item label="优先级">
-      <el-input v-model="userTaskForm.priority" clearable @change="updateElementTask('priority')" />
+    <el-form-item
+      v-if="userTaskForm.candidateStrategy == 22"
+      label="指定岗位"
+      prop="candidateParam"
+      span="24"
+    >
+      <el-select
+        v-model="userTaskForm.candidateParam"
+        clearable
+        multiple
+        style="width: 100%"
+        @change="updateElementTask"
+      >
+        <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="userTaskForm.candidateStrategy == 30"
+      label="指定用户"
+      prop="candidateParam"
+      span="24"
+    >
+      <el-select
+        v-model="userTaskForm.candidateParam"
+        clearable
+        multiple
+        style="width: 100%"
+        @change="updateElementTask"
+      >
+        <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="userTaskForm.candidateStrategy === 40"
+      label="指定用户组"
+      prop="candidateParam"
+    >
+      <el-select
+        v-model="userTaskForm.candidateParam"
+        clearable
+        multiple
+        style="width: 100%"
+        @change="updateElementTask"
+      >
+        <el-option
+          v-for="item in userGroupOptions"
+          :key="item.id"
+          :label="item.name"
+          :value="item.id"
+        />
+      </el-select>
     </el-form-item>
-    友情提示:任务的分配规则,使用
-    <router-link target="_blank" :to="{ path: '/bpm/manager/model' }"
-      ><el-link type="danger">流程模型</el-link>
-    </router-link>
-    下的【分配规则】替代,提供指定角色、部门负责人、部门成员、岗位、工作组、自定义脚本等 7
-    种维护的任务分配维度,更加灵活!
-  </div>
+    <el-form-item
+      v-if="userTaskForm.candidateStrategy === 60"
+      label="流程表达式"
+      prop="candidateParam"
+    >
+      <el-input
+        type="textarea"
+        v-model="userTaskForm.candidateParam[0]"
+        clearable
+        style="width: 72%"
+        @change="updateElementTask"
+      />
+      <el-button class="ml-5px" size="small" type="success" @click="openProcessExpressionDialog"
+        >选择表达式</el-button
+      >
+      <!-- 选择弹窗 -->
+      <ProcessExpressionDialog ref="processExpressionDialogRef" @select="selectProcessExpression" />
+    </el-form-item>
+  </el-form>
 </template>
 
 <script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { defaultProps, handleTree } from '@/utils/tree'
+import * as RoleApi from '@/api/system/role'
+import * as DeptApi from '@/api/system/dept'
+import * as PostApi from '@/api/system/post'
+import * as UserApi from '@/api/system/user'
+import * as UserGroupApi from '@/api/bpm/userGroup'
+import ProcessExpressionDialog from './ProcessExpressionDialog.vue'
+
 defineOptions({ name: 'UserTask' })
 const props = defineProps({
   id: String,
   type: String
 })
-const defaultTaskForm = ref({
-  assignee: '',
-  candidateUsers: [],
-  candidateGroups: [],
-  dueDate: '',
-  followUpDate: '',
-  priority: ''
+const userTaskForm = ref({
+  candidateStrategy: undefined, // 分配规则
+  candidateParam: [] // 分配选项
 })
-const userTaskForm = ref<any>({})
-// const mockData=ref([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
 const bpmnElement = ref()
 const bpmnInstances = () => (window as any)?.bpmnInstances
 
+const roleOptions = ref<RoleApi.RoleVO[]>([]) // 角色列表
+const deptTreeOptions = ref() // 部门树
+const postOptions = ref<PostApi.PostVO[]>([]) // 岗位列表
+const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
+const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]) // 用户组列表
+
 const resetTaskForm = () => {
-  for (let key in defaultTaskForm.value) {
-    let value
-    if (key === 'candidateUsers' || key === 'candidateGroups') {
-      value = bpmnElement.value?.businessObject[key]
-        ? bpmnElement.value.businessObject[key].split(',')
-        : []
+  const businessObject = bpmnElement.value.businessObject
+  if (!businessObject) {
+    return
+  }
+  if (businessObject.candidateStrategy != undefined) {
+    userTaskForm.value.candidateStrategy = parseInt(businessObject.candidateStrategy) as any
+  } else {
+    userTaskForm.value.candidateStrategy = undefined
+  }
+  if (businessObject.candidateParam && businessObject.candidateParam.length > 0) {
+    if (userTaskForm.value.candidateStrategy === 60) {
+      // 特殊:流程表达式,只有一个 input 输入框
+      userTaskForm.value.candidateParam = [businessObject.candidateParam]
     } else {
-      value = bpmnElement.value?.businessObject[key] || defaultTaskForm.value[key]
+      userTaskForm.value.candidateParam = businessObject.candidateParam
+        .split(',')
+        .map((item) => +item)
     }
-    userTaskForm.value[key] = value
-  }
-}
-const updateElementTask = (key) => {
-  const taskAttr = Object.create(null)
-  if (key === 'candidateUsers' || key === 'candidateGroups') {
-    taskAttr[key] =
-      userTaskForm.value[key] && userTaskForm.value[key].length
-        ? userTaskForm.value[key].join()
-        : null
   } else {
-    taskAttr[key] = userTaskForm.value[key] || null
+    userTaskForm.value.candidateParam = []
   }
-  bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), taskAttr)
+}
+
+/** 更新 candidateStrategy 字段时,需要清空 candidateParam,并触发 bpmn 图更新 */
+const changeCandidateStrategy = () => {
+  userTaskForm.value.candidateParam = []
+  updateElementTask()
+}
+
+/** 选中某个 options 时候,更新 bpmn 图  */
+const updateElementTask = () => {
+  bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+    candidateStrategy: userTaskForm.value.candidateStrategy,
+    candidateParam: userTaskForm.value.candidateParam.join(',')
+  })
+}
+
+// 打开监听器弹窗
+const processExpressionDialogRef = ref()
+const openProcessExpressionDialog = async () => {
+  processExpressionDialogRef.value.open()
+}
+const selectProcessExpression = (expression) => {
+  userTaskForm.value.candidateParam = [expression.expression]
 }
 
 watch(
@@ -92,6 +211,21 @@ watch(
   },
   { immediate: true }
 )
+
+onMounted(async () => {
+  // 获得角色列表
+  roleOptions.value = await RoleApi.getSimpleRoleList()
+  // 获得部门列表
+  const deptOptions = await DeptApi.getSimpleDeptList()
+  deptTreeOptions.value = handleTree(deptOptions, 'id')
+  // 获得岗位列表
+  postOptions.value = await PostApi.getSimplePostList()
+  // 获得用户列表
+  userOptions.value = await UserApi.getSimpleUserList()
+  // 获得用户组列表
+  userGroupOptions.value = await UserGroupApi.getUserGroupSimpleList()
+})
+
 onBeforeUnmount(() => {
   bpmnElement.value = null
 })

+ 1 - 0
src/components/bpmnProcessDesigner/package/utils.ts

@@ -2,6 +2,7 @@ import { toRaw } from 'vue'
 const bpmnInstances = () => (window as any)?.bpmnInstances
 // 创建监听器实例
 export function createListenerObject(options, isTask, prefix) {
+  debugger
   const listenerObj = Object.create(null)
   listenerObj.event = options.event
   isTask && (listenerObj.id = options.id) // 任务监听器特有的 id 字段

+ 2 - 3
src/config/axios/service.ts

@@ -13,7 +13,7 @@ import { getAccessToken, getRefreshToken, getTenantId, removeToken, setToken } f
 import errorCode from './errorCode'
 
 import { resetRouter } from '@/router'
-import { useCache } from '@/hooks/web/useCache'
+import { deleteUserCache } from '@/hooks/web/useCache'
 
 const tenantEnable = import.meta.env.VITE_APP_TENANT_ENABLE
 const { result_code, base_url, request_timeout } = config
@@ -217,9 +217,8 @@ const handleAuthorized = () => {
       confirmButtonText: t('login.relogin'),
       type: 'warning'
     }).then(() => {
-      const { wsCache } = useCache()
       resetRouter() // 重置静态路由表
-      wsCache.clear()
+      deleteUserCache() // 删除用户缓存
       removeToken()
       isRelogin.show = false
       // 干掉token后再走一次路由让它过router.beforeEach的校验

+ 15 - 3
src/hooks/web/useCache.ts

@@ -7,13 +7,18 @@ import WebStorageCache from 'web-storage-cache'
 type CacheType = 'localStorage' | 'sessionStorage'
 
 export const CACHE_KEY = {
-  IS_DARK: 'isDark',
+  // 用户相关
+  ROLE_ROUTERS: 'roleRouters',
   USER: 'user',
+  // 系统设置
+  IS_DARK: 'isDark',
   LANG: 'lang',
   THEME: 'theme',
   LAYOUT: 'layout',
-  ROLE_ROUTERS: 'roleRouters',
-  DICT_CACHE: 'dictCache'
+  DICT_CACHE: 'dictCache',
+  // 登录表单
+  LoginForm: 'loginForm',
+  TenantId: 'tenantId'
 }
 
 export const useCache = (type: CacheType = 'localStorage') => {
@@ -25,3 +30,10 @@ export const useCache = (type: CacheType = 'localStorage') => {
     wsCache
   }
 }
+
+export const deleteUserCache = () => {
+  const { wsCache } = useCache()
+  wsCache.delete(CACHE_KEY.USER)
+  wsCache.delete(CACHE_KEY.ROLE_ROUTERS)
+  // 注意,不要清理 LoginForm 登录表单
+}

+ 1 - 2
src/layout/components/Collapse/src/Collapse.vue

@@ -24,13 +24,12 @@ const toggleCollapse = () => {
 </script>
 
 <template>
-  <div :class="prefixCls">
+  <div :class="prefixCls" @click="toggleCollapse">
     <Icon
       :color="color"
       :icon="collapse ? 'ep:expand' : 'ep:fold'"
       :size="18"
       class="cursor-pointer"
-      @click="toggleCollapse"
     />
   </div>
 </template>

+ 0 - 33
src/layout/components/Menu/src/Menu.vue

@@ -124,16 +124,6 @@ export default defineComponent({
 <style lang="scss" scoped>
 $prefix-cls: #{$namespace}-menu;
 
-.is-active--after {
-  position: absolute;
-  top: 0;
-  right: 0;
-  width: 4px;
-  height: 100%;
-  background-color: var(--el-color-primary);
-  content: '';
-}
-
 .#{$prefix-cls} {
   position: relative;
   transition: width var(--transition-time-02);
@@ -159,7 +149,6 @@ $prefix-cls: #{$namespace}-menu;
     }
 
     // 设置选中时的高亮背景和高亮颜色
-    .#{$elNamespace}-sub-menu.is-active,
     .#{$elNamespace}-menu-item.is-active {
       color: var(--left-menu-text-active-color) !important;
       background-color: var(--left-menu-bg-active-color) !important;
@@ -171,10 +160,6 @@ $prefix-cls: #{$namespace}-menu;
 
     .#{$elNamespace}-menu-item.is-active {
       position: relative;
-
-      &::after {
-        @extend .is-active--after;
-      }
     }
 
     // 设置子菜单的背景颜色
@@ -194,10 +179,6 @@ $prefix-cls: #{$namespace}-menu;
     & > .is-active > .#{$elNamespace}-sub-menu__title {
       position: relative;
       background-color: var(--left-menu-collapse-bg-active-color) !important;
-
-      &::after {
-        @extend .is-active--after;
-      }
     }
   }
 
@@ -245,16 +226,6 @@ $prefix-cls: #{$namespace}-menu;
 <style lang="scss">
 $prefix-cls: #{$namespace}-menu-popper;
 
-.is-active--after {
-  position: absolute;
-  top: 0;
-  right: 0;
-  width: 4px;
-  height: 100%;
-  background-color: var(--el-color-primary);
-  content: '';
-}
-
 .#{$prefix-cls}--vertical,
 .#{$prefix-cls}--horizontal {
   // 设置选中时子标题的颜色
@@ -281,10 +252,6 @@ $prefix-cls: #{$namespace}-menu-popper;
     &:hover {
       background-color: var(--left-menu-bg-active-color) !important;
     }
-
-    &::after {
-      @extend .is-active--after;
-    }
   }
 }
 </style>

+ 40 - 49
src/layout/components/Menu/src/components/useRenderMenuItem.tsx

@@ -1,59 +1,50 @@
 import { ElSubMenu, ElMenuItem } from 'element-plus'
-import type { RouteMeta } from 'vue-router'
 import { hasOneShowingChild } from '../helper'
 import { isUrl } from '@/utils/is'
 import { useRenderMenuTitle } from './useRenderMenuTitle'
-import { useDesign } from '@/hooks/web/useDesign'
 import { pathResolve } from '@/utils/routerHelper'
 
-export const useRenderMenuItem = (
-  // allRouters: AppRouteRecordRaw[] = [],
-  menuMode: 'vertical' | 'horizontal'
-) => {
-  const renderMenuItem = (routers: AppRouteRecordRaw[], parentPath = '/') => {
-    return routers.map((v) => {
-      const meta = (v.meta ?? {}) as RouteMeta
-      if (!meta.hidden) {
-        const { oneShowingChild, onlyOneChild } = hasOneShowingChild(v.children, v)
-        const fullPath = isUrl(v.path) ? v.path : pathResolve(parentPath, v.path) // getAllParentPath<AppRouteRecordRaw>(allRouters, v.path).join('/')
-
-        const { renderMenuTitle } = useRenderMenuTitle()
+const { renderMenuTitle } = useRenderMenuTitle()
 
-        if (
-          oneShowingChild &&
-          (!onlyOneChild?.children || onlyOneChild?.noShowingChildren) &&
-          !meta?.alwaysShow
-        ) {
-          return (
-            <ElMenuItem index={onlyOneChild ? pathResolve(fullPath, onlyOneChild.path) : fullPath}>
-              {{
-                default: () => renderMenuTitle(onlyOneChild ? onlyOneChild?.meta : meta)
-              }}
-            </ElMenuItem>
-          )
-        } else {
-          const { getPrefixCls } = useDesign()
+export const useRenderMenuItem = () =>
+  // allRouters: AppRouteRecordRaw[] = [],
+  {
+    const renderMenuItem = (routers: AppRouteRecordRaw[], parentPath = '/') => {
+      return routers
+        .filter((v) => !v.meta?.hidden)
+        .map((v) => {
+          const meta = v.meta ?? {}
+          const { oneShowingChild, onlyOneChild } = hasOneShowingChild(v.children, v)
+          const fullPath = isUrl(v.path) ? v.path : pathResolve(parentPath, v.path) // getAllParentPath<AppRouteRecordRaw>(allRouters, v.path).join('/')
 
-          const preFixCls = getPrefixCls('menu-popper')
-          return (
-            <ElSubMenu
-              index={fullPath}
-              popperClass={
-                menuMode === 'vertical' ? `${preFixCls}--vertical` : `${preFixCls}--horizontal`
-              }
-            >
-              {{
-                title: () => renderMenuTitle(meta),
-                default: () => renderMenuItem(v.children!, fullPath)
-              }}
-            </ElSubMenu>
-          )
-        }
-      }
-    })
-  }
+          if (
+            oneShowingChild &&
+            (!onlyOneChild?.children || onlyOneChild?.noShowingChildren) &&
+            !meta?.alwaysShow
+          ) {
+            return (
+              <ElMenuItem
+                index={onlyOneChild ? pathResolve(fullPath, onlyOneChild.path) : fullPath}
+              >
+                {{
+                  default: () => renderMenuTitle(onlyOneChild ? onlyOneChild?.meta : meta)
+                }}
+              </ElMenuItem>
+            )
+          } else {
+            return (
+              <ElSubMenu index={fullPath}>
+                {{
+                  title: () => renderMenuTitle(meta),
+                  default: () => renderMenuItem(v.children!, fullPath)
+                }}
+              </ElSubMenu>
+            )
+          }
+        })
+    }
 
-  return {
-    renderMenuItem
+    return {
+      renderMenuItem
+    }
   }
-}

+ 7 - 2
src/layout/components/Menu/src/components/useRenderMenuTitle.tsx

@@ -1,5 +1,6 @@
 import type { RouteMeta } from 'vue-router'
 import { Icon } from '@/components/Icon'
+import { useI18n } from '@/hooks/web/useI18n'
 
 export const useRenderMenuTitle = () => {
   const renderMenuTitle = (meta: RouteMeta) => {
@@ -9,10 +10,14 @@ export const useRenderMenuTitle = () => {
     return icon ? (
       <>
         <Icon icon={meta.icon}></Icon>
-        <span class="v-menu__title">{t(title as string)}</span>
+        <span class="v-menu__title overflow-hidden overflow-ellipsis whitespace-nowrap">
+          {t(title as string)}
+        </span>
       </>
     ) : (
-      <span class="v-menu__title">{t(title as string)}</span>
+      <span class="v-menu__title overflow-hidden overflow-ellipsis whitespace-nowrap">
+        {t(title as string)}
+      </span>
     )
   }
 

+ 1 - 1
src/layout/components/TabMenu/src/TabMenu.vue

@@ -139,7 +139,7 @@ export default defineComponent({
         id={`${variables.namespace}-menu`}
         class={[
           prefixCls,
-          'relative bg-[var(--left-menu-bg-color)] top-1px z-3000 layout-border__right',
+          'relative bg-[var(--left-menu-bg-color)] top-1px layout-border__right',
           {
             'w-[var(--tab-menu-max-width)]': !unref(collapse),
             'w-[var(--tab-menu-min-width)]': unref(collapse)

+ 43 - 2
src/layout/components/UserInfo/src/UserInfo.vue

@@ -5,6 +5,9 @@ import avatarImg from '@/assets/imgs/avatar.gif'
 import { useDesign } from '@/hooks/web/useDesign'
 import { useTagsViewStore } from '@/store/modules/tagsView'
 import { useUserStore } from '@/store/modules/user'
+import LockDialog from './components/LockDialog.vue'
+import LockPage from './components/LockPage.vue'
+import { useLockStore } from '@/store/modules/lock'
 
 defineOptions({ name: 'UserInfo' })
 
@@ -23,6 +26,14 @@ const prefixCls = getPrefixCls('user-info')
 const avatar = computed(() => userStore.user.avatar ?? avatarImg)
 const userName = computed(() => userStore.user.nickname ?? 'Admin')
 
+// 锁定屏幕
+const lockStore = useLockStore()
+const getIsLock = computed(() => lockStore.getLockInfo?.isLock ?? false)
+const dialogVisible = ref<boolean>(false)
+const lockScreen = () => {
+  dialogVisible.value = true
+}
+
 const loginOut = async () => {
   try {
     await ElMessageBox.confirm(t('common.loginOutMessage'), t('common.reminder'), {
@@ -33,8 +44,7 @@ const loginOut = async () => {
     await userStore.loginOut()
     tagsViewStore.delAllViews()
     replace('/login?redirect=/index')
-  }
-  catch { }
+  } catch {}
 }
 const toProfile = async () => {
   push('/user/profile')
@@ -62,6 +72,10 @@ const toDocument = () => {
           <Icon icon="ep:menu" />
           <div @click="toDocument">{{ t('common.document') }}</div>
         </ElDropdownItem>
+        <ElDropdownItem divided>
+          <Icon icon="ep:lock" />
+          <div @click="lockScreen">{{ t('lock.lockScreen') }}</div>
+        </ElDropdownItem>
         <ElDropdownItem divided @click="loginOut">
           <Icon icon="ep:switch-button" />
           <div>{{ t('common.loginOut') }}</div>
@@ -69,4 +83,31 @@ const toDocument = () => {
       </ElDropdownMenu>
     </template>
   </ElDropdown>
+
+  <LockDialog v-if="dialogVisible" v-model="dialogVisible" />
+
+  <teleport to="body">
+    <transition name="fade-bottom" mode="out-in">
+      <LockPage v-if="getIsLock" />
+    </transition>
+  </teleport>
 </template>
+
+<style scoped lang="scss">
+.fade-bottom-enter-active,
+.fade-bottom-leave-active {
+  transition:
+    opacity 0.25s,
+    transform 0.3s;
+}
+
+.fade-bottom-enter-from {
+  opacity: 0;
+  transform: translateY(-10%);
+}
+
+.fade-bottom-leave-to {
+  opacity: 0;
+  transform: translateY(10%);
+}
+</style>

+ 98 - 0
src/layout/components/UserInfo/src/components/LockDialog.vue

@@ -0,0 +1,98 @@
+<script setup lang="ts">
+import { useValidator } from '@/hooks/web/useValidator'
+import { useDesign } from '@/hooks/web/useDesign'
+import { useLockStore } from '@/store/modules/lock'
+import avatarImg from '@/assets/imgs/avatar.gif'
+import { useUserStore } from '@/store/modules/user'
+
+const { getPrefixCls } = useDesign()
+const prefixCls = getPrefixCls('lock-dialog')
+
+const { required } = useValidator()
+
+const { t } = useI18n()
+
+const lockStore = useLockStore()
+
+const props = defineProps({
+  modelValue: {
+    type: Boolean
+  }
+})
+
+const userStore = useUserStore()
+const avatar = computed(() => userStore.user.avatar ?? avatarImg)
+const userName = computed(() => userStore.user.nickname ?? 'Admin')
+
+const emit = defineEmits(['update:modelValue'])
+
+const dialogVisible = computed({
+  get: () => props.modelValue,
+  set: (val) => {
+    console.log('set: ', val)
+    emit('update:modelValue', val)
+  }
+})
+
+const dialogTitle = ref(t('lock.lockScreen'))
+
+const formData = ref({
+  password: undefined
+})
+const formRules = reactive({
+  password: [required()]
+})
+
+const formRef = ref() // 表单 Ref
+const handleLock = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+  // 提交请求
+  dialogVisible.value = false
+  lockStore.setLockInfo({
+    ...formData.value,
+    isLock: true
+  })
+}
+</script>
+
+<template>
+  <Dialog
+    v-model="dialogVisible"
+    width="500px"
+    max-height="170px"
+    :class="prefixCls"
+    :title="dialogTitle"
+  >
+    <div class="flex flex-col items-center">
+      <img :src="avatar" alt="" class="w-70px h-70px rounded-[50%]" />
+      <span class="text-14px my-10px text-[var(--top-header-text-color)]">
+        {{ userName }}
+      </span>
+    </div>
+    <el-form ref="formRef" :model="formData" :rules="formRules" label-width="80px">
+      <el-form-item :label="t('lock.lockPassword')" prop="password">
+        <el-input
+          type="password"
+          v-model="formData.password"
+          :placeholder="'请输入' + t('lock.lockPassword')"
+          clearable
+          show-password
+        />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <ElButton type="primary" @click="handleLock">{{ t('lock.lock') }}</ElButton>
+    </template>
+  </Dialog>
+</template>
+
+<style lang="scss" scoped>
+:global(.v-lock-dialog) {
+  @media (max-width: 767px) {
+    max-width: calc(100vw - 16px);
+  }
+}
+</style>

+ 270 - 0
src/layout/components/UserInfo/src/components/LockPage.vue

@@ -0,0 +1,270 @@
+<script lang="ts" setup>
+import { resetRouter } from '@/router'
+import { deleteUserCache } from '@/hooks/web/useCache'
+import { useLockStore } from '@/store/modules/lock'
+import { useNow } from '@/hooks/web/useNow'
+import { useDesign } from '@/hooks/web/useDesign'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import { useUserStore } from '@/store/modules/user'
+import avatarImg from '@/assets/imgs/avatar.gif'
+
+const tagsViewStore = useTagsViewStore()
+
+const { replace } = useRouter()
+
+const userStore = useUserStore()
+
+const password = ref('')
+const loading = ref(false)
+const errMsg = ref(false)
+const showDate = ref(true)
+
+const { getPrefixCls } = useDesign()
+const prefixCls = getPrefixCls('lock-page')
+
+const avatar = computed(() => userStore.user.avatar ?? avatarImg)
+const userName = computed(() => userStore.user.nickname ?? 'Admin')
+
+const lockStore = useLockStore()
+
+const { hour, month, minute, meridiem, year, day, week } = useNow(true)
+
+const { t } = useI18n()
+
+// 解锁
+async function unLock() {
+  if (!password.value) {
+    return
+  }
+  let pwd = password.value
+  try {
+    loading.value = true
+    const res = await lockStore.unLock(pwd)
+    errMsg.value = !res
+  } finally {
+    loading.value = false
+  }
+}
+
+// 返回登录
+async function goLogin() {
+  await userStore.loginOut().catch(() => {})
+  // 登出后清理
+  deleteUserCache() // 清空用户缓存
+  tagsViewStore.delAllViews()
+  resetRouter() // 重置静态路由表
+  lockStore.resetLockInfo()
+  replace('/login')
+}
+
+function handleShowForm(show = false) {
+  showDate.value = show
+}
+</script>
+
+<template>
+  <div
+    :class="prefixCls"
+    class="fixed inset-0 flex h-screen w-screen bg-black items-center justify-center"
+  >
+    <div
+      :class="`${prefixCls}__unlock`"
+      class="absolute top-0 left-1/2 flex pt-5 h-16 items-center justify-center sm:text-md xl:text-xl text-white flex-col cursor-pointer transform translate-x-1/2"
+      @click="handleShowForm(false)"
+      v-show="showDate"
+    >
+      <Icon icon="ep:lock" />
+      <span>{{ t('lock.unlock') }}</span>
+    </div>
+
+    <div class="flex w-screen h-screen justify-center items-center">
+      <div :class="`${prefixCls}__hour`" class="relative mr-5 md:mr-20 w-2/5 h-2/5 md:h-4/5">
+        <span>{{ hour }}</span>
+        <span class="meridiem absolute left-5 top-5 text-md xl:text-xl" v-show="showDate">
+          {{ meridiem }}
+        </span>
+      </div>
+      <div :class="`${prefixCls}__minute w-2/5 h-2/5 md:h-4/5 `">
+        <span> {{ minute }}</span>
+      </div>
+    </div>
+    <transition name="fade-slide">
+      <div :class="`${prefixCls}-entry`" v-show="!showDate">
+        <div :class="`${prefixCls}-entry-content`">
+          <div class="flex flex-col items-center">
+            <img :src="avatar" alt="" class="w-70px h-70px rounded-[50%]" />
+            <span class="text-14px my-10px text-[var(--logo-title-text-color)]">
+              {{ userName }}
+            </span>
+          </div>
+          <ElInput
+            type="password"
+            :placeholder="t('lock.placeholder')"
+            class="enter-x"
+            v-model="password"
+          />
+          <span :class="`text-14px ${prefixCls}-entry__err-msg enter-x`" v-if="errMsg">
+            {{ t('lock.message') }}
+          </span>
+          <div :class="`${prefixCls}-entry__footer enter-x`">
+            <ElButton
+              type="primary"
+              size="small"
+              class="mt-2 mr-2 enter-x"
+              link
+              :disabled="loading"
+              @click="handleShowForm(true)"
+            >
+              {{ t('common.back') }}
+            </ElButton>
+            <ElButton
+              type="primary"
+              size="small"
+              class="mt-2 mr-2 enter-x"
+              link
+              :disabled="loading"
+              @click="goLogin"
+            >
+              {{ t('lock.backToLogin') }}
+            </ElButton>
+            <ElButton
+              type="primary"
+              class="mt-2"
+              size="small"
+              link
+              @click="unLock()"
+              :disabled="loading"
+            >
+              {{ t('lock.entrySystem') }}
+            </ElButton>
+          </div>
+        </div>
+      </div>
+    </transition>
+
+    <div class="absolute bottom-5 w-full text-gray-300 xl:text-xl 2xl:text-3xl text-center enter-y">
+      <div class="text-5xl mb-4 enter-x" v-show="!showDate">
+        {{ hour }}:{{ minute }} <span class="text-3xl">{{ meridiem }}</span>
+      </div>
+      <div class="text-2xl">{{ year }}/{{ month }}/{{ day }} {{ week }}</div>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+$prefix-cls: '#{$namespace}-lock-page';
+
+// Small screen / tablet
+$screen-sm: 576px;
+
+// Medium screen / desktop
+$screen-md: 768px;
+
+// Large screen / wide desktop
+$screen-lg: 992px;
+
+// Extra large screen / full hd
+$screen-xl: 1200px;
+
+// Extra extra large screen / large desktop
+$screen-2xl: 1600px;
+
+$error-color: #ed6f6f;
+
+.#{$prefix-cls} {
+  z-index: 3000;
+
+  &__unlock {
+    transform: translate(-50%, 0);
+  }
+
+  &__hour,
+  &__minute {
+    display: flex;
+    font-weight: 700;
+    color: #bababa;
+    background-color: #141313;
+    border-radius: 30px;
+    justify-content: center;
+    align-items: center;
+
+    @media screen and (max-width: $screen-md) {
+      span:not(.meridiem) {
+        font-size: 160px;
+      }
+    }
+
+    @media screen and (min-width: $screen-md) {
+      span:not(.meridiem) {
+        font-size: 160px;
+      }
+    }
+
+    @media screen and (max-width: $screen-sm) {
+      span:not(.meridiem) {
+        font-size: 90px;
+      }
+    }
+    @media screen and (min-width: $screen-lg) {
+      span:not(.meridiem) {
+        font-size: 220px;
+      }
+    }
+
+    @media screen and (min-width: $screen-xl) {
+      span:not(.meridiem) {
+        font-size: 260px;
+      }
+    }
+    @media screen and (min-width: $screen-2xl) {
+      span:not(.meridiem) {
+        font-size: 320px;
+      }
+    }
+  }
+
+  &-entry {
+    position: absolute;
+    top: 0;
+    left: 0;
+    display: flex;
+    width: 100%;
+    height: 100%;
+    background-color: rgba(0, 0, 0, 0.5);
+    backdrop-filter: blur(8px);
+    justify-content: center;
+    align-items: center;
+
+    &-content {
+      width: 260px;
+    }
+
+    &__header {
+      text-align: center;
+
+      &-img {
+        width: 70px;
+        margin: 0 auto;
+        border-radius: 50%;
+      }
+
+      &-name {
+        margin-top: 5px;
+        font-weight: 500;
+        color: #bababa;
+      }
+    }
+
+    &__err-msg {
+      display: inline-block;
+      margin-top: 10px;
+      color: $error-color;
+    }
+
+    &__footer {
+      display: flex;
+      justify-content: space-between;
+    }
+  }
+}
+</style>

+ 10 - 0
src/locales/en.ts

@@ -56,6 +56,16 @@ export default {
     copySuccess: 'Copy Success',
     copyError: 'Copy Error'
   },
+  lock: {
+    lockScreen: 'Lock screen',
+    lock: 'Lock',
+    lockPassword: 'Lock screen password',
+    unlock: 'Click to unlock',
+    backToLogin: 'Back to login',
+    entrySystem: 'Entry the system',
+    placeholder: 'Please enter the lock screen password',
+    message: 'Lock screen password error'
+  },
   error: {
     noPermission: `Sorry, you don't have permission to access this page.`,
     pageError: 'Sorry, the page you visited does not exist.',

+ 10 - 0
src/locales/zh-CN.ts

@@ -56,6 +56,16 @@ export default {
     copySuccess: '复制成功',
     copyError: '复制失败'
   },
+  lock: {
+    lockScreen: '锁定屏幕',
+    lock: '锁定',
+    lockPassword: '锁屏密码',
+    unlock: '点击解锁',
+    backToLogin: '返回登录',
+    entrySystem: '进入系统',
+    placeholder: '请输入锁屏密码',
+    message: '锁屏密码错误'
+  },
   error: {
     noPermission: `抱歉,您无权访问此页面。`,
     pageError: '抱歉,您访问的页面不存在。',

+ 19 - 9
src/plugins/formCreate/index.ts

@@ -1,22 +1,26 @@
 import type { App } from 'vue'
 // 👇使用 form-create 需额外全局引入 element plus 组件
 import {
+  ElAlert,
   ElAside,
-  ElPopconfirm,
-  ElHeader,
-  ElMain,
   ElContainer,
   ElDivider,
-  ElTransfer,
-  ElAlert,
-  ElTabs,
+  ElHeader,
+  ElMain,
+  ElPopconfirm,
   ElTable,
   ElTableColumn,
-  ElTabPane
+  ElTabPane,
+  ElTabs,
+  ElTransfer
 } from 'element-plus'
-
+import FcDesigner from '@form-create/designer'
 import formCreate from '@form-create/element-ui'
 import install from '@form-create/element-ui/auto-import'
+//======================= 自定义组件 =======================
+import { UploadFile, UploadImg, UploadImgs } from '@/components/UploadFile'
+import { DictSelect } from '@/components/DictSelect'
+import UserSelect from '@/views/system/user/components/UserSelect.vue'
 
 const components = [
   ElAside,
@@ -30,7 +34,12 @@ const components = [
   ElTabs,
   ElTable,
   ElTableColumn,
-  ElTabPane
+  ElTabPane,
+  UploadImg,
+  UploadImgs,
+  UploadFile,
+  DictSelect,
+  UserSelect
 ]
 
 // 参考 http://www.form-create.com/v3/element-ui/auto-import.html 文档
@@ -40,4 +49,5 @@ export const setupFormCreate = (app: App<Element>) => {
   })
   formCreate.use(install)
   app.use(formCreate)
+  app.use(FcDesigner)
 }

+ 15 - 26
src/router/modules/remaining.ts

@@ -243,7 +243,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
     },
     children: [
       {
-        path: '/manager/form/edit',
+        path: 'manager/form/edit',
         component: () => import('@/views/bpm/form/editor/index.vue'),
         name: 'BpmFormEditor',
         meta: {
@@ -255,7 +255,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
         }
       },
       {
-        path: '/manager/model/edit',
+        path: 'manager/model/edit',
         component: () => import('@/views/bpm/model/editor/index.vue'),
         name: 'BpmModelEditor',
         meta: {
@@ -267,42 +267,31 @@ const remainingRouter: AppRouteRecordRaw[] = [
         }
       },
       {
-        path: '/manager/definition',
-        component: () => import('@/views/bpm/definition/index.vue'),
-        name: 'BpmProcessDefinition',
+        path: 'manager/simple/workflow/model/edit',
+        component: () => import('@/views/bpm/simpleWorkflow/index.vue'),
+        name: 'SimpleWorkflowDesignEditor',
         meta: {
           noCache: true,
           hidden: true,
           canTo: true,
-          title: '流程定义',
+          title: '仿钉钉设计流程',
           activeMenu: '/bpm/manager/model'
         }
       },
       {
-        path: '/manager/task-assign-rule',
-        component: () => import('@/views/bpm/taskAssignRule/index.vue'),
-        name: 'BpmTaskAssignRuleList',
-        meta: {
-          noCache: true,
-          hidden: true,
-          canTo: true,
-          title: '任务分配规则'
-        }
-      },
-      {
-        path: '/process-instance/create',
-        component: () => import('@/views/bpm/processInstance/create/index.vue'),
-        name: 'BpmProcessInstanceCreate',
+        path: 'manager/definition',
+        component: () => import('@/views/bpm/definition/index.vue'),
+        name: 'BpmProcessDefinition',
         meta: {
           noCache: true,
           hidden: true,
           canTo: true,
-          title: '发起流程',
-          activeMenu: 'bpm/processInstance/create'
+          title: '流程定义',
+          activeMenu: '/bpm/manager/model'
         }
       },
       {
-        path: '/process-instance/detail',
+        path: 'process-instance/detail',
         component: () => import('@/views/bpm/processInstance/detail/index.vue'),
         name: 'BpmProcessInstanceDetail',
         meta: {
@@ -310,11 +299,11 @@ const remainingRouter: AppRouteRecordRaw[] = [
           hidden: true,
           canTo: true,
           title: '流程详情',
-          activeMenu: 'bpm/processInstance/detail'
+          activeMenu: '/bpm/task/my'
         }
       },
       {
-        path: '/bpm/oa/leave/create',
+        path: 'oa/leave/create',
         component: () => import('@/views/bpm/oa/leave/create.vue'),
         name: 'OALeaveCreate',
         meta: {
@@ -326,7 +315,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
         }
       },
       {
-        path: '/bpm/oa/leave/detail',
+        path: 'oa/leave/detail',
         component: () => import('@/views/bpm/oa/leave/detail.vue'),
         name: 'OALeaveDetail',
         meta: {

+ 2 - 0
src/store/index.ts

@@ -1,7 +1,9 @@
 import type { App } from 'vue'
 import { createPinia } from 'pinia'
+import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
 
 const store = createPinia()
+store.use(piniaPluginPersistedstate)
 
 export const setupStore = (app: App<Element>) => {
   app.use(store)

+ 2 - 1
src/store/modules/app.ts

@@ -268,7 +268,8 @@ export const useAppStore = defineStore('app', {
     setFooter(footer: boolean) {
       this.footer = footer
     }
-  }
+  },
+  persist: false
 })
 
 export const useAppStoreWithOut = () => {

+ 48 - 0
src/store/modules/lock.ts

@@ -0,0 +1,48 @@
+import { defineStore } from 'pinia'
+import { store } from '@/store'
+
+interface lockInfo {
+  isLock?: boolean
+  password?: string
+}
+
+interface LockState {
+  lockInfo: lockInfo
+}
+
+export const useLockStore = defineStore('lock', {
+  state: (): LockState => {
+    return {
+      lockInfo: {
+        // isLock: false, // 是否锁定屏幕
+        // password: '' // 锁屏密码
+      }
+    }
+  },
+  getters: {
+    getLockInfo(): lockInfo {
+      return this.lockInfo
+    }
+  },
+  actions: {
+    setLockInfo(lockInfo: lockInfo) {
+      this.lockInfo = lockInfo
+    },
+    resetLockInfo() {
+      this.lockInfo = {}
+    },
+    unLock(password: string) {
+      if (this.lockInfo?.password === password) {
+        this.resetLockInfo()
+        return true
+      } else {
+        return false
+      }
+    }
+  },
+  persist: true
+})
+
+export const useLockStoreWithOut = () => {
+  return useLockStore(store)
+}

+ 3 - 2
src/store/modules/permission.ts

@@ -1,5 +1,5 @@
 import { defineStore } from 'pinia'
-import { store } from '../index'
+import { store } from '@/store'
 import { cloneDeep } from 'lodash-es'
 import remainingRouter from '@/router/modules/remaining'
 import { flatMultiLevelRoutes, generateRoute } from '@/utils/routerHelper'
@@ -59,7 +59,8 @@ export const usePermissionStore = defineStore('permission', {
     setMenuTabRouters(routers: AppRouteRecordRaw[]): void {
       this.menuTabRouters = routers
     }
-  }
+  },
+  persist: false
 })
 
 export const usePermissionStoreWithOut = () => {

+ 55 - 0
src/store/modules/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,
+    flowPermission1: {},
+    approverDrawer: false,
+    approverConfig1: {},
+    copyerDrawer: false,
+    copyerConfig1: {},
+    conditionDrawer: false,
+    conditionsConfig1: {
+      conditionNodes: []
+    }
+  }),
+  actions: {
+    setTableId(payload) {
+      this.tableId = payload
+    },
+    setIsTried(payload) {
+      this.isTried = payload
+    },
+    setPromoter(payload) {
+      this.promoterDrawer = payload
+    },
+    setFlowPermission(payload) {
+      this.flowPermission1 = payload
+    },
+    setApprover(payload) {
+      this.approverDrawer = payload
+    },
+    setApproverConfig(payload) {
+      this.approverConfig1 = payload
+    },
+    setCopyer(payload) {
+      this.copyerDrawer = payload
+    },
+    setCopyerConfig(payload) {
+      this.copyerConfig1 = payload
+    },
+    setCondition(payload) {
+      this.conditionDrawer = payload
+    },
+    setConditionsConfig(payload) {
+      this.conditionsConfig1 = payload
+    }
+  }
+})
+
+export const useWorkFlowStoreWithOut = () => {
+  return useWorkFlowStore(store)
+}

+ 2 - 1
src/store/modules/tagsView.ts

@@ -132,7 +132,8 @@ export const useTagsViewStore = defineStore('tagsView', {
         }
       }
     }
-  }
+  },
+  persist: false
 })
 
 export const useTagsViewStoreWithOut = () => {

+ 4 - 3
src/store/modules/user.ts

@@ -1,7 +1,7 @@
-import { store } from '../index'
+import { store } from '@/store'
 import { defineStore } from 'pinia'
 import { getAccessToken, removeToken } from '@/utils/auth'
-import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
+import { CACHE_KEY, useCache, deleteUserCache } from '@/hooks/web/useCache'
 import { getInfo, loginOut } from '@/api/login'
 
 const { wsCache } = useCache()
@@ -14,6 +14,7 @@ interface UserVO {
 }
 
 interface UserInfoVO {
+  // USER 缓存
   permissions: string[]
   roles: string[]
   isSetUser: boolean
@@ -80,7 +81,7 @@ export const useUserStore = defineStore('admin-user', {
     async loginOut() {
       await loginOut()
       removeToken()
-      wsCache.clear()
+      deleteUserCache() // 删除用户缓存
       this.resetState()
     },
     resetState() {

+ 6 - 27
src/utils/auth.ts

@@ -1,4 +1,4 @@
-import { useCache } from '@/hooks/web/useCache'
+import { useCache, CACHE_KEY } from '@/hooks/web/useCache'
 import { TokenType } from '@/api/login/types'
 import { decrypt, encrypt } from '@/utils/jsencrypt'
 
@@ -36,8 +36,6 @@ export const formatToken = (token: string): string => {
 }
 // ========== 账号相关 ==========
 
-const LoginFormKey = 'LOGINFORM'
-
 export type LoginFormType = {
   tenantName: string
   username: string
@@ -46,7 +44,7 @@ export type LoginFormType = {
 }
 
 export const getLoginForm = () => {
-  const loginForm: LoginFormType = wsCache.get(LoginFormKey)
+  const loginForm: LoginFormType = wsCache.get(CACHE_KEY.LoginForm)
   if (loginForm) {
     loginForm.password = decrypt(loginForm.password) as string
   }
@@ -55,38 +53,19 @@ export const getLoginForm = () => {
 
 export const setLoginForm = (loginForm: LoginFormType) => {
   loginForm.password = encrypt(loginForm.password) as string
-  wsCache.set(LoginFormKey, loginForm, { exp: 30 * 24 * 60 * 60 })
+  wsCache.set(CACHE_KEY.LoginForm, loginForm, { exp: 30 * 24 * 60 * 60 })
 }
 
 export const removeLoginForm = () => {
-  wsCache.delete(LoginFormKey)
+  wsCache.delete(CACHE_KEY.LoginForm)
 }
 
 // ========== 租户相关 ==========
 
-const TenantIdKey = 'TENANT_ID'
-const TenantNameKey = 'TENANT_NAME'
-
-export const getTenantName = () => {
-  return wsCache.get(TenantNameKey)
-}
-
-export const setTenantName = (username: string) => {
-  wsCache.set(TenantNameKey, username, { exp: 30 * 24 * 60 * 60 })
-}
-
-export const removeTenantName = () => {
-  wsCache.delete(TenantNameKey)
-}
-
 export const getTenantId = () => {
-  return wsCache.get(TenantIdKey)
+  return wsCache.get(CACHE_KEY.TenantId)
 }
 
 export const setTenantId = (username: string) => {
-  wsCache.set(TenantIdKey, username)
-}
-
-export const removeTenantId = () => {
-  wsCache.delete(TenantIdKey)
+  wsCache.set(CACHE_KEY.TenantId, username)
 }

+ 3 - 3
src/utils/constants.ts

@@ -248,15 +248,15 @@ export const CouponTemplateTakeTypeEnum = {
  */
 export const PromotionProductScopeEnum = {
   ALL: {
-    scope: 10,
+    scope: 1,
     name: '通用劵'
   },
   SPU: {
-    scope: 20,
+    scope: 2,
     name: '商品劵'
   },
   CATEGORY: {
-    scope: 30,
+    scope: 3,
     name: '品类劵'
   }
 }

+ 18 - 0
src/utils/dateUtil.ts

@@ -0,0 +1,18 @@
+/**
+ * Independent time operation tool to facilitate subsequent switch to dayjs
+ */
+// TODO 芋艿:【锁屏】可能后面删除掉
+import dayjs from 'dayjs'
+
+const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'
+const DATE_FORMAT = 'YYYY-MM-DD'
+
+export function formatToDateTime(date?: dayjs.ConfigType, format = DATE_TIME_FORMAT): string {
+  return dayjs(date).format(format)
+}
+
+export function formatToDate(date?: dayjs.ConfigType, format = DATE_FORMAT): string {
+  return dayjs(date).format(format)
+}
+
+export const dateUtil = dayjs

+ 9 - 8
src/utils/dict.ts

@@ -1,8 +1,8 @@
 /**
  * 数据字典工具类
  */
-import { useDictStoreWithOut } from '@/store/modules/dict'
-import { ElementPlusInfoType } from '@/types/elementPlus'
+import {useDictStoreWithOut} from '@/store/modules/dict'
+import {ElementPlusInfoType} from '@/types/elementPlus'
 
 const dictStore = useDictStoreWithOut()
 
@@ -104,6 +104,7 @@ export enum DICT_TYPE {
   USER_TYPE = 'user_type',
   COMMON_STATUS = 'common_status',
   TERMINAL = 'terminal', // 终端
+  DATE_INTERVAL = 'date_interval', // 数据间隔
 
   // ========== SYSTEM 模块 ==========
   SYSTEM_USER_SEX = 'system_user_sex',
@@ -111,7 +112,6 @@ export enum DICT_TYPE {
   SYSTEM_ROLE_TYPE = 'system_role_type',
   SYSTEM_DATA_SCOPE = 'system_data_scope',
   SYSTEM_NOTICE_TYPE = 'system_notice_type',
-  SYSTEM_OPERATE_TYPE = 'system_operate_type',
   SYSTEM_LOGIN_TYPE = 'system_login_type',
   SYSTEM_LOGIN_RESULT = 'system_login_result',
   SYSTEM_SMS_CHANNEL_CODE = 'system_sms_channel_code',
@@ -134,15 +134,16 @@ export enum DICT_TYPE {
   INFRA_CODEGEN_FRONT_TYPE = 'infra_codegen_front_type',
   INFRA_CODEGEN_SCENE = 'infra_codegen_scene',
   INFRA_FILE_STORAGE = 'infra_file_storage',
+  INFRA_OPERATE_TYPE = 'infra_operate_type',
 
   // ========== BPM 模块 ==========
-  BPM_MODEL_CATEGORY = 'bpm_model_category',
   BPM_MODEL_FORM_TYPE = 'bpm_model_form_type',
-  BPM_TASK_ASSIGN_RULE_TYPE = 'bpm_task_assign_rule_type',
+  BPM_TASK_CANDIDATE_STRATEGY = 'bpm_task_candidate_strategy',
   BPM_PROCESS_INSTANCE_STATUS = 'bpm_process_instance_status',
-  BPM_PROCESS_INSTANCE_RESULT = 'bpm_process_instance_result',
-  BPM_TASK_ASSIGN_SCRIPT = 'bpm_task_assign_script',
+  BPM_TASK_STATUS = 'bpm_task_status',
   BPM_OA_LEAVE_TYPE = 'bpm_oa_leave_type',
+  BPM_PROCESS_LISTENER_TYPE = 'bpm_process_listener_type',
+  BPM_PROCESS_LISTENER_VALUE_TYPE = 'bpm_process_listener_value_type',
 
   // ========== PAY 模块 ==========
   PAY_CHANNEL_CODE = 'pay_channel_code', // 支付渠道编码类型
@@ -157,7 +158,7 @@ export enum DICT_TYPE {
   MP_AUTO_REPLY_REQUEST_MATCH = 'mp_auto_reply_request_match', // 自动回复请求匹配类型
   MP_MESSAGE_TYPE = 'mp_message_type', // 消息类型
 
-  // ========== MALL - 会员模块 ==========
+  // ========== Member 会员模块 ==========
   MEMBER_POINT_BIZ_TYPE = 'member_point_biz_type', // 积分的业务类型
   MEMBER_EXPERIENCE_BIZ_TYPE = 'member_experience_biz_type', // 会员经验业务类型
 

+ 9 - 6
src/utils/formCreate.ts

@@ -28,7 +28,7 @@ export const decodeFields = (fields: string[]) => {
   return rule
 }
 
-// 设置表单的 Conf 和 Fields
+// 设置表单的 Conf 和 Fields,适用 FcDesigner 场景
 export const setConfAndFields = (designerRef: object, conf: string, fields: string) => {
   // @ts-ignore
   designerRef.value.setOption(JSON.parse(conf))
@@ -36,19 +36,22 @@ export const setConfAndFields = (designerRef: object, conf: string, fields: stri
   designerRef.value.setRule(decodeFields(fields))
 }
 
-// 设置表单的 Conf 和 Fields
+// 设置表单的 Conf 和 Fields,适用 form-create 场景
 export const setConfAndFields2 = (
   detailPreview: object,
   conf: string,
-  fields: string,
+  fields: string[],
   value?: object
 ) => {
+  if (isRef(detailPreview)) {
+    detailPreview = detailPreview.value
+  }
   // @ts-ignore
-  detailPreview.value.option = JSON.parse(conf)
+  detailPreview.option = JSON.parse(conf)
   // @ts-ignore
-  detailPreview.value.rule = decodeFields(fields)
+  detailPreview.rule = decodeFields(fields)
   if (value) {
     // @ts-ignore
-    detailPreview.value.value = value
+    detailPreview.value = value
   }
 }

+ 5 - 5
src/utils/formatTime.ts

@@ -175,18 +175,18 @@ export function formatPast2(ms: number): string {
   const minute = Math.floor(ms / (60 * 1000) - day * 24 * 60 - hour * 60)
   const second = Math.floor(ms / 1000 - day * 24 * 60 * 60 - hour * 60 * 60 - minute * 60)
   if (day > 0) {
-    return day + '天' + hour + '小时' + minute + '分钟'
+    return day + ' 天' + hour + ' 小时 ' + minute + ' 分钟'
   }
   if (hour > 0) {
-    return hour + '小时' + minute + '分钟'
+    return hour + ' 小时 ' + minute + ' 分钟'
   }
   if (minute > 0) {
-    return minute + '分钟'
+    return minute + ' 分钟'
   }
   if (second > 0) {
-    return second + '秒'
+    return second + ' 秒'
   } else {
-    return 0 + '秒'
+    return 0 + ' 秒'
   }
 }
 

+ 15 - 1
src/utils/index.ts

@@ -329,10 +329,11 @@ const ERP_PRICE_DIGIT = 2
  * 例如说:库存数量
  *
  * @param num 数量
+ * @package digit 保留的小数位数
  * @return 格式化后的数量
  */
 export const erpNumberFormatter = (num: number | string | undefined, digit: number) => {
-  if (num === null) {
+  if (num == null) {
     return ''
   }
   if (typeof num === 'string') {
@@ -404,3 +405,16 @@ export const erpPriceMultiply = (price: number, count: number) => {
   }
   return parseFloat((price * count).toFixed(ERP_PRICE_DIGIT))
 }
+
+/**
+ * 【ERP】百分比计算,四舍五入保留两位小数
+ *
+ * 如果 total 为 0,则返回 0
+ *
+ * @param value 当前值
+ * @param total 总值
+ */
+export const erpCalculatePercentage = (value: number, total: number) => {
+  if (total === 0) return 0
+  return ((value / total) * 100).toFixed(2)
+}

+ 4 - 4
src/views/Login/components/LoginForm.vue

@@ -188,7 +188,7 @@ const loginData = reactive({
     username: 'admin',
     password: 'admin123',
     captchaVerification: '',
-    rememberMe: false
+    rememberMe: true // 默认记录我。如果不需要,可手动修改
   }
 })
 
@@ -218,14 +218,14 @@ const getTenantId = async () => {
   }
 }
 // 记住我
-const getCookie = () => {
+const getLoginFormCache = () => {
   const loginForm = authUtil.getLoginForm()
   if (loginForm) {
     loginData.loginForm = {
       ...loginData.loginForm,
       username: loginForm.username ? loginForm.username : loginData.loginForm.username,
       password: loginForm.password ? loginForm.password : loginData.loginForm.password,
-      rememberMe: loginForm.rememberMe ? true : false,
+      rememberMe: loginForm.rememberMe,
       tenantName: loginForm.tenantName ? loginForm.tenantName : loginData.loginForm.tenantName
     }
   }
@@ -326,7 +326,7 @@ watch(
   }
 )
 onMounted(() => {
-  getCookie()
+  getLoginFormCache()
   getTenantByWebsite()
 })
 </script>

+ 124 - 0
src/views/bpm/category/CategoryForm.vue

@@ -0,0 +1,124 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="分类名" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入分类名" />
+      </el-form-item>
+      <el-form-item label="分类标志" prop="code">
+        <el-input v-model="formData.code" placeholder="请输入分类标志" />
+      </el-form-item>
+      <el-form-item label="分类状态" prop="status">
+        <el-radio-group v-model="formData.status">
+          <el-radio
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="分类排序" prop="sort">
+        <el-input-number
+          v-model="formData.sort"
+          placeholder="请输入分类排序"
+          class="!w-1/1"
+          :precision="0"
+        />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { CategoryApi, CategoryVO } from '@/api/bpm/category'
+
+/** BPM 流程分类 表单 */
+defineOptions({ name: 'CategoryForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  name: undefined,
+  code: undefined,
+  status: undefined,
+  sort: undefined
+})
+const formRules = reactive({
+  name: [{ required: true, message: '分类名不能为空', trigger: 'blur' }],
+  code: [{ required: true, message: '分类标志不能为空', trigger: 'blur' }],
+  status: [{ required: true, message: '分类状态不能为空', trigger: 'blur' }],
+  sort: [{ required: true, message: '分类排序不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await CategoryApi.getCategory(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as CategoryVO
+    if (formType.value === 'create') {
+      await CategoryApi.createCategory(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await CategoryApi.updateCategory(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    name: undefined,
+    code: undefined,
+    status: undefined,
+    sort: undefined
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 36 - 56
src/views/report/ureport/index2.vue → src/views/bpm/category/index.vue

@@ -1,4 +1,6 @@
 <template>
+  <doc-alert title="工作流手册" url="https://doc.iocoder.cn/bpm/" />
+
   <ContentWrap>
     <!-- 搜索工作栏 -->
     <el-form
@@ -8,19 +10,28 @@
       :inline="true"
       label-width="68px"
     >
-      <el-form-item label="文件名称" prop="name">
+      <el-form-item label="分类名" prop="name">
         <el-input
           v-model="queryParams.name"
-          placeholder="请输入文件名称"
+          placeholder="请输入分类名"
           clearable
           @keyup.enter="handleQuery"
           class="!w-240px"
         />
       </el-form-item>
-      <el-form-item label="状态" prop="status">
+      <el-form-item label="分类标志" prop="code">
+        <el-input
+          v-model="queryParams.code"
+          placeholder="请输入分类标志"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="分类状态" prop="status">
         <el-select
           v-model="queryParams.status"
-          placeholder="请选择状态"
+          placeholder="请选择分类状态"
           clearable
           class="!w-240px"
         >
@@ -32,15 +43,6 @@
           />
         </el-select>
       </el-form-item>
-      <el-form-item label="备注" prop="remark">
-        <el-input
-          v-model="queryParams.remark"
-          placeholder="请输入备注"
-          clearable
-          @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
-      </el-form-item>
       <el-form-item label="创建时间" prop="createTime">
         <el-date-picker
           v-model="queryParams.createTime"
@@ -59,19 +61,10 @@
           type="primary"
           plain
           @click="openForm('create')"
-          v-hasPermi="['report:ureport-data:create']"
+          v-hasPermi="['bpm:category:create']"
         >
           <Icon icon="ep:plus" class="mr-5px" /> 新增
         </el-button>
-        <el-button
-          type="success"
-          plain
-          @click="handleExport"
-          :loading="exportLoading"
-          v-hasPermi="['report:ureport-data:export']"
-        >
-          <Icon icon="ep:download" class="mr-5px" /> 导出
-        </el-button>
       </el-form-item>
     </el-form>
   </ContentWrap>
@@ -79,15 +72,16 @@
   <!-- 列表 -->
   <ContentWrap>
     <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
-      <el-table-column label="ID" align="center" prop="id" />
-      <el-table-column label="文件名称" align="center" prop="name" />
-      <el-table-column label="状态" align="center" prop="status">
+      <el-table-column label="分类编号" align="center" prop="id" />
+      <el-table-column label="分类名" align="center" prop="name" />
+      <el-table-column label="分类标志" align="center" prop="code" />
+      <el-table-column label="分类描述" align="center" prop="description" />
+      <el-table-column label="分类状态" align="center" prop="status">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
         </template>
       </el-table-column>
-      <el-table-column label="文件内容" align="center" prop="content" />
-      <el-table-column label="备注" align="center" prop="remark" />
+      <el-table-column label="分类排序" align="center" prop="sort" />
       <el-table-column
         label="创建时间"
         align="center"
@@ -101,7 +95,7 @@
             link
             type="primary"
             @click="openForm('update', scope.row.id)"
-            v-hasPermi="['report:ureport-data:update']"
+            v-hasPermi="['bpm:category:update']"
           >
             编辑
           </el-button>
@@ -109,7 +103,7 @@
             link
             type="danger"
             @click="handleDelete(scope.row.id)"
-            v-hasPermi="['report:ureport-data:delete']"
+            v-hasPermi="['bpm:category:delete']"
           >
             删除
           </el-button>
@@ -126,31 +120,32 @@
   </ContentWrap>
 
   <!-- 表单弹窗:添加/修改 -->
-  <UReportDataForm ref="formRef" @success="getList" />
+  <CategoryForm ref="formRef" @success="getList" />
 </template>
 
 <script setup lang="ts">
 import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
 import { dateFormatter } from '@/utils/formatTime'
 import download from '@/utils/download'
-import * as UReportDataApi from '@/api/report/ureport'
-import UReportDataForm from './UReportDataForm.vue'
+import { CategoryApi, CategoryVO } from '@/api/bpm/category'
+import CategoryForm from './CategoryForm.vue'
 
-defineOptions({ name: 'UReportData' })
+/** BPM 流程分类 列表 */
+defineOptions({ name: 'BpmCategory' })
 
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
 
 const loading = ref(true) // 列表的加载中
-const list = ref([]) // 列表的数据
+const list = ref<CategoryVO[]>([]) // 列表的数据
 const total = ref(0) // 列表的总页数
 const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
-  name: null,
-  status: null,
-  remark: null,
-  createTime: [],
+  name: undefined,
+  code: undefined,
+  status: undefined,
+  createTime: []
 })
 const queryFormRef = ref() // 搜索的表单
 const exportLoading = ref(false) // 导出的加载中
@@ -159,7 +154,7 @@ const exportLoading = ref(false) // 导出的加载中
 const getList = async () => {
   loading.value = true
   try {
-    const data = await UReportDataApi.getUReportDataPage(queryParams)
+    const data = await CategoryApi.getCategoryPage(queryParams)
     list.value = data.list
     total.value = data.total
   } finally {
@@ -191,28 +186,13 @@ const handleDelete = async (id: number) => {
     // 删除的二次确认
     await message.delConfirm()
     // 发起删除
-    await UReportDataApi.deleteUReportData(id)
+    await CategoryApi.deleteCategory(id)
     message.success(t('common.delSuccess'))
     // 刷新列表
     await getList()
   } catch {}
 }
 
-/** 导出按钮操作 */
-const handleExport = async () => {
-  try {
-    // 导出的二次确认
-    await message.exportConfirm()
-    // 发起导出
-    exportLoading.value = true
-    const data = await UReportDataApi.exportUReportData(queryParams)
-    download.excel(data, 'Ureport2报表.xls')
-  } catch {
-  } finally {
-    exportLoading.value = false
-  }
-}
-
 /** 初始化 **/
 onMounted(() => {
   getList()

部分文件因文件數量過多而無法顯示