Browse Source

!118 ♥️发布 5.2.0-BETA 公测版本
Merge pull request !118 from 疯狂的狮子Li/dev

疯狂的狮子Li 9 months ago
parent
commit
a63543a5c7
100 changed files with 7792 additions and 784 deletions
  1. 6 4
      .env.development
  2. 4 2
      .env.production
  3. 2 2
      .eslintignore
  4. 16 7
      .eslintrc.cjs
  5. 1 0
      .gitignore
  6. 20 0
      .prettierrc
  7. 0 46
      .prettierrc.cjs
  8. 51 50
      README.md
  9. 0 22
      commitlint.config.js
  10. 1 12
      html/ie.html
  11. 14 17
      index.html
  12. 75 58
      package.json
  13. 6 7
      src/App.vue
  14. 4 2
      src/api/login.ts
  15. 16 0
      src/api/monitor/online/index.ts
  16. 3 3
      src/api/system/client/index.ts
  17. 1 1
      src/api/system/client/types.ts
  18. 1 1
      src/api/system/config/index.ts
  19. 3 0
      src/api/system/dept/types.ts
  20. 12 0
      src/api/system/post/index.ts
  21. 8 0
      src/api/system/post/types.ts
  22. 16 0
      src/api/system/role/index.ts
  23. 2 1
      src/api/system/tenant/index.ts
  24. 16 2
      src/api/system/user/index.ts
  25. 1 2
      src/api/system/user/types.ts
  26. 2 2
      src/api/tool/gen/index.ts
  27. 63 0
      src/api/workflow/category/index.ts
  28. 67 0
      src/api/workflow/category/types.ts
  29. 49 0
      src/api/workflow/definitionConfig/index.ts
  30. 102 0
      src/api/workflow/definitionConfig/types.ts
  31. 76 0
      src/api/workflow/formManage/index.ts
  32. 69 0
      src/api/workflow/formManage/types.ts
  33. 63 0
      src/api/workflow/leave/index.ts
  34. 24 0
      src/api/workflow/leave/types.ts
  35. 104 0
      src/api/workflow/model/index.ts
  36. 66 0
      src/api/workflow/model/types.ts
  37. 63 0
      src/api/workflow/nodeConfig/index.ts
  38. 43 0
      src/api/workflow/nodeConfig/types.ts
  39. 114 0
      src/api/workflow/processDefinition/index.ts
  40. 24 0
      src/api/workflow/processDefinition/types.ts
  41. 136 0
      src/api/workflow/processInstance/index.ts
  42. 27 0
      src/api/workflow/processInstance/types.ts
  43. 264 0
      src/api/workflow/task/index.ts
  44. 49 0
      src/api/workflow/task/types.ts
  45. 29 0
      src/api/workflow/workflowCommon/index.ts
  46. 16 0
      src/api/workflow/workflowCommon/types.ts
  47. 1 0
      src/assets/icons/svg/caret-back.svg
  48. 1 0
      src/assets/icons/svg/caret-forward.svg
  49. 1 0
      src/assets/icons/svg/category.svg
  50. 1 0
      src/assets/icons/svg/finish.svg
  51. 1 0
      src/assets/icons/svg/model.svg
  52. 1 0
      src/assets/icons/svg/my-copy.svg
  53. 0 0
      src/assets/icons/svg/my-task.svg
  54. 0 0
      src/assets/icons/svg/process-definition.svg
  55. 29 0
      src/assets/icons/svg/topiam.svg
  56. 0 0
      src/assets/icons/svg/waiting.svg
  57. 1 0
      src/assets/icons/svg/workflow.svg
  58. 34 1
      src/assets/styles/element-ui.scss
  59. 8 1
      src/assets/styles/index.scss
  60. 4 2
      src/assets/styles/sidebar.scss
  61. 28 0
      src/assets/styles/variables.module.scss
  62. 23 0
      src/bpmn/assets/defaultXML.ts
  63. 126 0
      src/bpmn/assets/lang/zh.ts
  64. 1250 0
      src/bpmn/assets/moddle/flowable.ts
  65. 138 0
      src/bpmn/assets/module/ContextPad/CustomContextPadProvider.ts
  66. 109 0
      src/bpmn/assets/module/Palette/CustomPaletteProvider.ts
  67. 56 0
      src/bpmn/assets/module/Renderer/CustomRenderer.ts
  68. 15 0
      src/bpmn/assets/module/Translate/index.ts
  69. 17 0
      src/bpmn/assets/module/index.ts
  70. 50 0
      src/bpmn/assets/showConfig.ts
  71. 284 0
      src/bpmn/assets/style/index.scss
  72. 145 0
      src/bpmn/hooks/usePanel.ts
  73. 34 0
      src/bpmn/hooks/useParseElement.ts
  74. 496 0
      src/bpmn/index.vue
  75. 68 0
      src/bpmn/panel/GatewayPanel.vue
  76. 68 0
      src/bpmn/panel/ParticipantPanel.vue
  77. 71 0
      src/bpmn/panel/ProcessPanel.vue
  78. 95 0
      src/bpmn/panel/SequenceFlowPanel.vue
  79. 67 0
      src/bpmn/panel/StartEndPanel.vue
  80. 193 0
      src/bpmn/panel/SubProcessPanel.vue
  81. 492 0
      src/bpmn/panel/TaskPanel.vue
  82. 110 0
      src/bpmn/panel/index.vue
  83. 252 0
      src/bpmn/panel/property/DueDate.vue
  84. 308 0
      src/bpmn/panel/property/ExecutionListener.vue
  85. 121 0
      src/bpmn/panel/property/ListenerParam.vue
  86. 310 0
      src/bpmn/panel/property/TaskListener.vue
  87. 71 0
      src/components/BpmnDesign/index.vue
  88. 410 0
      src/components/BpmnView/index.vue
  89. 20 21
      src/components/Breadcrumb/index.vue
  90. 33 36
      src/components/BuildCode/index.vue
  91. 32 37
      src/components/BuildCode/render.vue
  92. 45 36
      src/components/DictTag/index.vue
  93. 97 90
      src/components/Editor/index.vue
  94. 127 115
      src/components/FileUpload/index.vue
  95. 4 4
      src/components/Hamburger/index.vue
  96. 51 49
      src/components/HeaderSearch/index.vue
  97. 12 14
      src/components/IconSelect/index.vue
  98. 11 12
      src/components/ImagePreview/index.vue
  99. 137 119
      src/components/ImageUpload/index.vue
  100. 5 6
      src/components/LangSelect/index.vue

+ 6 - 4
.env.development

@@ -13,11 +13,13 @@ VITE_APP_CONTEXT_PATH = '/'
 # 监控地址
 VITE_APP_MONITRO_ADMIN = 'http://localhost:9090/admin/applications'
 
-# powerjob 控制台地址
-VITE_APP_POWERJOB_ADMIN = 'http://localhost:7700/'
+# SnailJob 控制台地址
+VITE_APP_SNAILJOB_ADMIN = 'http://localhost:8800/snail-job'
 
 VITE_APP_PORT = 80
 
+# 接口加密功能开关(如需关闭 后端也必须对应关闭)
+VITE_APP_ENCRYPT = true
 # 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
 VITE_APP_RSA_PUBLIC_KEY = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
 # 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换
@@ -26,5 +28,5 @@ VITE_APP_RSA_PRIVATE_KEY = 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3C
 # 客户端id
 VITE_APP_CLIENT_ID = 'e5cd7e4891bf95d1d19206ce24a7b32e'
 
-# websocket 开关(开发环境默认关闭ws 因vite的bug导致如ws无法连接则会崩溃)
-VITE_APP_WEBSOCKET = false
+# websocket 开关
+VITE_APP_WEBSOCKET = true

+ 4 - 2
.env.production

@@ -10,8 +10,8 @@ VITE_APP_CONTEXT_PATH = '/'
 # 监控地址
 VITE_APP_MONITRO_ADMIN = '/admin/applications'
 
-# powerjob 控制台地址
-VITE_APP_POWERJOB_ADMIN = '/powerjob'
+# SnailJob 控制台地址
+VITE_APP_SNAILJOB_ADMIN = 'http://localhost:8800/snail-job'
 
 # 生产环境
 VITE_APP_BASE_API = '/prod-api'
@@ -21,6 +21,8 @@ VITE_BUILD_COMPRESS = gzip
 
 VITE_APP_PORT = 80
 
+# 接口加密功能开关(如需关闭 后端也必须对应关闭)
+VITE_APP_ENCRYPT = true
 # 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
 VITE_APP_RSA_PUBLIC_KEY = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
 # 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换

+ 2 - 2
.eslintignore

@@ -11,7 +11,7 @@ dist
 .husky
 .local
 /bin
-.eslintrc.js
+.eslintrc.cjs
 prettier.config.js
 src/assets
-tailwind.config.js
+tailwind.config.js

+ 16 - 7
.eslintrc.js → .eslintrc.cjs

@@ -1,28 +1,37 @@
 module.exports = {
   env: {
     browser: true,
-    es2021: true,
-    node: true
+    node: true,
+    es6: true
   },
   parser: 'vue-eslint-parser',
   extends: [
-    'eslint:recommended',
-    'plugin:vue/vue3-essential',
-    'plugin:@typescript-eslint/recommended',
+    'plugin:vue/vue3-recommended',
     './.eslintrc-auto-import.json',
+    'plugin:@typescript-eslint/recommended',
+    'prettier',
     'plugin:prettier/recommended'
   ],
   parserOptions: {
     ecmaVersion: '2020',
     sourceType: 'module',
+    project: './tsconfig.*?.json',
     parser: '@typescript-eslint/parser'
   },
-  plugins: ['vue', '@typescript-eslint'],
+  plugins: ['vue', '@typescript-eslint', 'import', 'promise', 'node', 'prettier'],
   rules: {
-    'vue/multi-word-component-names': 'off',
     '@typescript-eslint/no-empty-function': 'off',
     '@typescript-eslint/no-explicit-any': 'off',
+    '@typescript-eslint/no-unused-vars': 'off',
+    '@typescript-eslint/no-this-alias': 'off',
+
+    // vue
+    'vue/multi-word-component-names': 'off',
+    'vue/valid-define-props': 'off',
     'vue/no-v-model-argument': 'off',
+    'prefer-rest-params': 'off',
+    // prettier
+    'prettier/prettier': 'error',
     '@typescript-eslint/ban-types': [
       'error',
       {

+ 1 - 0
.gitignore

@@ -22,6 +22,7 @@ selenium-debug.log
 
 package-lock.json
 yarn.lock
+pnpm-lock.yaml
 
 # 编译生成的文件
 auto-imports.d.ts

+ 20 - 0
.prettierrc

@@ -0,0 +1,20 @@
+{
+  "printWidth": 150,
+  "tabWidth": 2,
+  "useTabs": false,
+  "semi": true,
+  "singleQuote": true,
+  "quoteProps": "preserve",
+  "jsxSingleQuote": false,
+  "bracketSameLine": false,
+  "trailingComma": "none",
+  "bracketSpacing": true,
+  "embeddedLanguageFormatting": "auto",
+  "arrowParens": "always",
+  "requirePragma": false,
+  "insertPragma": false,
+  "proseWrap": "preserve",
+  "htmlWhitespaceSensitivity": "css",
+  "vueIndentScriptAndStyle": false,
+  "endOfLine": "auto"
+}

+ 0 - 46
.prettierrc.cjs

@@ -1,46 +0,0 @@
-/**
- * 代码格式化配置
- */
-module.exports = {
-  // 一行最多多少个字符
-  printWidth: 150,
-  // 指定每个缩进级别的空格数
-  tabWidth: 2,
-  // 使用制表符而不是空格缩进行
-  useTabs: false,
-  // 在语句末尾是否需要分号
-  semi: true,
-  // 是否使用单引号
-  singleQuote: true,
-  // 更改引用对象属性的时间 可选值"<as-needed|consistent|preserve>"
-  quoteProps: 'as-needed',
-  // 在JSX中使用单引号而不是双引号
-  jsxSingleQuote: false,
-  // 多行时尽可能打印尾随逗号。(例如,单行数组永远不会出现逗号结尾。) 可选值"<none|es5|all>",默认none
-  trailingComma: 'none',
-  // 在对象文字中的括号之间打印空格
-  bracketSpacing: true,
-  // jsx 标签的反尖括号需要换行
-  jsxBracketSameLine: false,
-  embeddedLanguageFormatting: 'off',
-  // 在单独的箭头函数参数周围包括括号 always:(x) => x \ avoid:x => x
-  arrowParens: 'always',
-  // 这两个选项可用于格式化以给定字符偏移量(分别包括和不包括)开始和结束的代码
-  rangeStart: 0,
-  rangeEnd: Infinity,
-  // 指定要使用的解析器,不需要写文件开头的 @prettier
-  requirePragma: false,
-  // 不需要自动在文件开头插入 @prettier
-  insertPragma: false,
-  // 使用默认的折行标准 always\never\preserve
-  proseWrap: 'preserve',
-  // 指定HTML文件的全局空格敏感度 css\strict\ignore
-  htmlWhitespaceSensitivity: 'css',
-  // Vue文件脚本和样式标签缩进
-  vueIndentScriptAndStyle: false,
-  // 在 windows 操作系统中换行符通常是回车 (CR) 加换行分隔符 (LF),也就是回车换行(CRLF),
-  // 然而在 Linux 和 Unix 中只使用简单的换行分隔符 (LF)。
-  // 对应的控制字符为 "\n" (LF) 和 "\r\n"(CRLF)。auto意为保持现有的行尾
-  // 换行符使用 lf 结尾是 可选值"<auto|lf|crlf|cr>"
-  endOfLine: 'auto'
-};

+ 51 - 50
README.md

@@ -1,9 +1,10 @@
 ## 平台简介
 
-* 本仓库为前端技术栈 [Vue3](https://v3.cn.vuejs.org) + [TS](https://www.typescriptlang.org/) + [Element Plus](https://element-plus.org/zh-CN) + [Vite](https://cn.vitejs.dev) 版本。
-* 配套后端代码仓库地址
-* [RuoYi-Vue-Plus 5.X(注意版本号)](https://gitee.com/dromara/RuoYi-Vue-Plus)
-* [RuoYi-Cloud-Plus 2.X(注意版本号)](https://gitee.com/dromara/RuoYi-Cloud-Plus)
+- 本仓库为前端技术栈 [Vue3](https://v3.cn.vuejs.org) + [TS](https://www.typescriptlang.org/) + [Element Plus](https://element-plus.org/zh-CN) + [Vite](https://cn.vitejs.dev) 版本。
+- 成员项目: 基于 vben(ant-design-vue) 的前端项目 [ruoyi-plus-vben](https://gitee.com/dapppp/ruoyi-plus-vben)
+- 配套后端代码仓库地址
+- [RuoYi-Vue-Plus 5.X(注意版本号)](https://gitee.com/dromara/RuoYi-Vue-Plus)
+- [RuoYi-Cloud-Plus 2.X(注意版本号)](https://gitee.com/dromara/RuoYi-Cloud-Plus)
 
 ## 前端运行
 
@@ -17,7 +18,7 @@ npm install --registry=https://registry.npmmirror.com
 # 启动服务
 npm run dev
 
-# 构建生产环境 
+# 构建生产环境
 npm run build:prod
 
 # 前端访问地址 http://localhost:80
@@ -25,51 +26,51 @@ npm run build:prod
 
 ## 本框架与RuoYi的业务差异
 
-| 业务     | 功能说明                                    | 本框架 | RuoYi            |
-|--------|-----------------------------------------|-----|------------------|
-| 租户管理   | 系统内租户的管理 如:租户套餐、过期时间、用户数量、企业信息等         | 支持  | 无                |
-| 租户套餐管理 | 系统内租户所能使用的套餐管理 如:套餐内所包含的菜单等             | 支持  | 无                |
-| 用户管理   | 用户的管理配置 如:新增用户、分配用户所属部门、角色、岗位等          | 支持  | 支持               |
-| 部门管理   | 配置系统组织机构(公司、部门、小组) 树结构展现支持数据权限          | 支持  | 支持               |
-| 岗位管理   | 配置系统用户所属担任职务                            | 支持  | 支持               |
-| 菜单管理   | 配置系统菜单、操作权限、按钮权限标识等                     | 支持  | 支持               |
-| 角色管理   | 角色菜单权限分配、设置角色按机构进行数据范围权限划分              | 支持  | 支持               |
-| 字典管理   | 对系统中经常使用的一些较为固定的数据进行维护                  | 支持  | 支持               |
-| 参数管理   | 对系统动态配置常用参数                             | 支持  | 支持               |
-| 通知公告   | 系统通知公告信息发布维护                            | 支持  | 支持               |
-| 操作日志   | 系统正常操作日志记录和查询 系统异常信息日志记录和查询             | 支持  | 支持               |
-| 登录日志   | 系统登录日志记录查询包含登录异常                        | 支持  | 支持               |
-| 文件管理   | 系统文件展示、上传、下载、删除等管理                      | 支持  | 无                |
-| 文件配置管理 | 系统文件上传、下载所需要的配置信息动态添加、修改、删除等管理          | 支持  | 无                |
-| 在线用户管理 | 已登录系统的在线用户信息监控与强制踢出操作                   | 支持  | 支持               |
-| 定时任务   | 运行报表、任务管理(添加、修改、删除)、日志管理、执行器管理等         | 支持  | 仅支持任务与日志管理       |
-| 代码生成   | 多数据源前后端代码的生成(java、html、xml、sql)支持CRUD下载 | 支持  | 仅支持单数据源          |
-| 系统接口   | 根据业务代码自动生成相关的api接口文档                    | 支持  | 支持               |
-| 服务监控   | 监视集群系统CPU、内存、磁盘、堆栈、在线日志、Spring相关配置等     | 支持  | 仅支持单机CPU、内存、磁盘监控 |
-| 缓存监控   | 对系统的缓存信息查询,命令统计等。                       | 支持  | 支持               |
-| 在线构建器  | 拖动表单元素生成相应的HTML代码。                      | 支持  | 支持               |
-| 使用案例   | 系统的一些功能案例                               | 支持  | 不支持              |
+| 业务         | 功能说明                                                      | 本框架 | RuoYi                         |
+| ------------ | ------------------------------------------------------------- | ------ | ----------------------------- |
+| 租户管理     | 系统内租户的管理 如:租户套餐、过期时间、用户数量、企业信息等  | 支持   | 无                            |
+| 租户套餐管理 | 系统内租户所能使用的套餐管理 如:套餐内所包含的菜单等          | 支持   | 无                            |
+| 用户管理     | 用户的管理配置 如:新增用户、分配用户所属部门、角色、岗位等    | 支持   | 支持                          |
+| 部门管理     | 配置系统组织机构(公司、部门、小组) 树结构展现支持数据权限   | 支持   | 支持                          |
+| 岗位管理     | 配置系统用户所属担任职务                                      | 支持   | 支持                          |
+| 菜单管理     | 配置系统菜单、操作权限、按钮权限标识等                        | 支持   | 支持                          |
+| 角色管理     | 角色菜单权限分配、设置角色按机构进行数据范围权限划分          | 支持   | 支持                          |
+| 字典管理     | 对系统中经常使用的一些较为固定的数据进行维护                  | 支持   | 支持                          |
+| 参数管理     | 对系统动态配置常用参数                                        | 支持   | 支持                          |
+| 通知公告     | 系统通知公告信息发布维护                                      | 支持   | 支持                          |
+| 操作日志     | 系统正常操作日志记录和查询 系统异常信息日志记录和查询         | 支持   | 支持                          |
+| 登录日志     | 系统登录日志记录查询包含登录异常                              | 支持   | 支持                          |
+| 文件管理     | 系统文件展示、上传、下载、删除等管理                          | 支持   | 无                            |
+| 文件配置管理 | 系统文件上传、下载所需要的配置信息动态添加、修改、删除等管理  | 支持   | 无                            |
+| 在线用户管理 | 已登录系统的在线用户信息监控与强制踢出操作                    | 支持   | 支持                          |
+| 定时任务     | 运行报表、任务管理(添加、修改、删除)、日志管理、执行器管理等  | 支持   | 仅支持任务与日志管理          |
+| 代码生成     | 多数据源前后端代码的生成(java、html、xml、sql)支持CRUD下载  | 支持   | 仅支持单数据源                |
+| 系统接口     | 根据业务代码自动生成相关的api接口文档                         | 支持   | 支持                          |
+| 服务监控     | 监视集群系统CPU、内存、磁盘、堆栈、在线日志、Spring相关配置等 | 支持   | 仅支持单机CPU、内存、磁盘监控 |
+| 缓存监控     | 对系统的缓存信息查询,命令统计等。                            | 支持   | 支持                          |
+| 在线构建器   | 拖动表单元素生成相应的HTML代码。                              | 支持   | 支持                          |
+| 使用案例     | 系统的一些功能案例                                            | 支持   | 不支持                        |
 
 ## 演示图例
 
-|                                                                                            |                                                                                            |
-|--------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------|
-| ![输入图片说明](https://foruda.gitee.com/images/1680077524361362822/270bb429_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680077619939771291/989bf9b6_1766278.png "屏幕截图") |
-| ![输入图片说明](https://foruda.gitee.com/images/1680077681751513929/1c27c5bd_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680077721559267315/74d63e23_1766278.png "屏幕截图") |
-| ![输入图片说明](https://foruda.gitee.com/images/1680077765638904515/1b75d4a6_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680078026375951297/eded7a4b_1766278.png "屏幕截图") |
-| ![输入图片说明](https://foruda.gitee.com/images/1680078237104531207/0eb1b6a7_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680078254306078709/5931e22f_1766278.png "屏幕截图") |
-| ![输入图片说明](https://foruda.gitee.com/images/1680078287971528493/0b9af60a_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680078308138770249/8d3b6696_1766278.png "屏幕截图") |
-| ![输入图片说明](https://foruda.gitee.com/images/1680078352553634393/db5ef880_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680078378238393374/601e4357_1766278.png "屏幕截图") |
-| ![输入图片说明](https://foruda.gitee.com/images/1680078414983206024/2aae27c1_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680078446738419874/ecce7d59_1766278.png "屏幕截图") |
-| ![输入图片说明](https://foruda.gitee.com/images/1680078475971341775/149e8634_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680078491666717143/3fadece7_1766278.png "屏幕截图") |
-| ![输入图片说明](https://foruda.gitee.com/images/1680078558863188826/fb8ced2a_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680078574561685461/ae68a0b2_1766278.png "屏幕截图") |
-| ![输入图片说明](https://foruda.gitee.com/images/1680078594932772013/9d8bfec6_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680078626493093532/fcfe4ff6_1766278.png "屏幕截图") |
-| ![输入图片说明](https://foruda.gitee.com/images/1680078643608812515/0295bd4f_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680078685196286463/d7612c81_1766278.png "屏幕截图") |
-| ![输入图片说明](https://foruda.gitee.com/images/1680078703877318597/56fce0bc_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680078716586545643/b6dbd68f_1766278.png "屏幕截图") |
-| ![输入图片说明](https://foruda.gitee.com/images/1680078734103217688/eb1e6aa6_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680078759131415480/73c525d8_1766278.png "屏幕截图") |
-| ![输入图片说明](https://foruda.gitee.com/images/1680078779416197879/75e3ed02_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680078802329118061/77e10915_1766278.png "屏幕截图") |
-| ![输入图片说明](https://foruda.gitee.com/images/1680078893627848351/34a1c342_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680078928175016986/f126ec4a_1766278.png "屏幕截图") |
-| ![输入图片说明](https://foruda.gitee.com/images/1680078941718318363/b68a0f72_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680078963175518631/3bb769a1_1766278.png "屏幕截图") |
-| ![输入图片说明](https://foruda.gitee.com/images/1680078982294090567/b31c343d_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680079000642440444/77ca82a9_1766278.png "屏幕截图") |
-| ![输入图片说明](https://foruda.gitee.com/images/1680079020995074177/03b7d52e_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680079039367822173/76811806_1766278.png "屏幕截图") |
-| ![输入图片说明](https://foruda.gitee.com/images/1680079274333484664/4dfdc7c0_1766278.png "屏幕截图") | ![输入图片说明](https://foruda.gitee.com/images/1680079290467458224/d6715fcf_1766278.png "屏幕截图") |
+|                                                                                                      |                                                                                                      |
+| ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
+| ![输入图片说明](https://foruda.gitee.com/images/1680077524361362822/270bb429_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680077619939771291/989bf9b6_1766278.png '屏幕截图') |
+| ![输入图片说明](https://foruda.gitee.com/images/1680077681751513929/1c27c5bd_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680077721559267315/74d63e23_1766278.png '屏幕截图') |
+| ![输入图片说明](https://foruda.gitee.com/images/1680077765638904515/1b75d4a6_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680078026375951297/eded7a4b_1766278.png '屏幕截图') |
+| ![输入图片说明](https://foruda.gitee.com/images/1680078237104531207/0eb1b6a7_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680078254306078709/5931e22f_1766278.png '屏幕截图') |
+| ![输入图片说明](https://foruda.gitee.com/images/1680078287971528493/0b9af60a_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680078308138770249/8d3b6696_1766278.png '屏幕截图') |
+| ![输入图片说明](https://foruda.gitee.com/images/1680078352553634393/db5ef880_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680078378238393374/601e4357_1766278.png '屏幕截图') |
+| ![输入图片说明](https://foruda.gitee.com/images/1680078414983206024/2aae27c1_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680078446738419874/ecce7d59_1766278.png '屏幕截图') |
+| ![输入图片说明](https://foruda.gitee.com/images/1680078475971341775/149e8634_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680078491666717143/3fadece7_1766278.png '屏幕截图') |
+| ![输入图片说明](https://foruda.gitee.com/images/1680078558863188826/fb8ced2a_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680078574561685461/ae68a0b2_1766278.png '屏幕截图') |
+| ![输入图片说明](https://foruda.gitee.com/images/1680078594932772013/9d8bfec6_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680078626493093532/fcfe4ff6_1766278.png '屏幕截图') |
+| ![输入图片说明](https://foruda.gitee.com/images/1680078643608812515/0295bd4f_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680078685196286463/d7612c81_1766278.png '屏幕截图') |
+| ![输入图片说明](https://foruda.gitee.com/images/1680078703877318597/56fce0bc_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680078716586545643/b6dbd68f_1766278.png '屏幕截图') |
+| ![输入图片说明](https://foruda.gitee.com/images/1680078734103217688/eb1e6aa6_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680078759131415480/73c525d8_1766278.png '屏幕截图') |
+| ![输入图片说明](https://foruda.gitee.com/images/1680078779416197879/75e3ed02_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680078802329118061/77e10915_1766278.png '屏幕截图') |
+| ![输入图片说明](https://foruda.gitee.com/images/1680078893627848351/34a1c342_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680078928175016986/f126ec4a_1766278.png '屏幕截图') |
+| ![输入图片说明](https://foruda.gitee.com/images/1680078941718318363/b68a0f72_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680078963175518631/3bb769a1_1766278.png '屏幕截图') |
+| ![输入图片说明](https://foruda.gitee.com/images/1680078982294090567/b31c343d_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680079000642440444/77ca82a9_1766278.png '屏幕截图') |
+| ![输入图片说明](https://foruda.gitee.com/images/1680079020995074177/03b7d52e_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680079039367822173/76811806_1766278.png '屏幕截图') |
+| ![输入图片说明](https://foruda.gitee.com/images/1680079274333484664/4dfdc7c0_1766278.png '屏幕截图') | ![输入图片说明](https://foruda.gitee.com/images/1680079290467458224/d6715fcf_1766278.png '屏幕截图') |

+ 0 - 22
commitlint.config.js

@@ -1,22 +0,0 @@
-module.exports = {
-  extends: ['@commitlint/config-conventional'],
-  rules: {
-    'type-enum': [
-      2,
-      'always',
-      [
-        'feat', // 新功能 feature
-        'fix', // 修复 bug
-        'docs', // 文档注释
-        'style', // 代码格式
-        'refactor', // 重构
-        'perf', // 性能优化
-        'test', // 增加测试
-        'chore', // 构建过程或辅助工具的变动
-        'revert', // 回退
-        'build' // 打包
-      ]
-    ],
-    'subject-case': [0]
-  }
-};

File diff suppressed because it is too large
+ 1 - 12
html/ie.html


+ 14 - 17
index.html

@@ -1,4 +1,4 @@
-<!DOCTYPE html>
+<!doctype html>
 <html>
   <head>
     <meta charset="utf-8" />
@@ -9,7 +9,7 @@
     <title>RuoYi-Vue-Plus多租户管理系统</title>
     <!--[if lt IE 11
       ]><script>
-        window.location.href='/html/ie.html';
+        window.location.href = '/html/ie.html';
       </script><!
     [endif]-->
     <style>
@@ -47,7 +47,7 @@
         margin: -75px 0 0 -75px;
         border-radius: 50%;
         border: 3px solid transparent;
-        border-top-color: #FFF;
+        border-top-color: #fff;
         -webkit-animation: spin 2s linear infinite;
         -ms-animation: spin 2s linear infinite;
         -moz-animation: spin 2s linear infinite;
@@ -57,7 +57,7 @@
       }
 
       #loader:before {
-        content: "";
+        content: '';
         position: absolute;
         top: 5px;
         left: 5px;
@@ -65,7 +65,7 @@
         bottom: 5px;
         border-radius: 50%;
         border: 3px solid transparent;
-        border-top-color: #FFF;
+        border-top-color: #fff;
         -webkit-animation: spin 3s linear infinite;
         -moz-animation: spin 3s linear infinite;
         -o-animation: spin 3s linear infinite;
@@ -74,7 +74,7 @@
       }
 
       #loader:after {
-        content: "";
+        content: '';
         position: absolute;
         top: 15px;
         left: 15px;
@@ -82,7 +82,7 @@
         bottom: 15px;
         border-radius: 50%;
         border: 3px solid transparent;
-        border-top-color: #FFF;
+        border-top-color: #fff;
         -moz-animation: spin 1.5s linear infinite;
         -o-animation: spin 1.5s linear infinite;
         -ms-animation: spin 1.5s linear infinite;
@@ -90,7 +90,6 @@
         animation: spin 1.5s linear infinite;
       }
 
-
       @-webkit-keyframes spin {
         0% {
           -webkit-transform: rotate(0deg);
@@ -119,13 +118,12 @@
         }
       }
 
-
       #loader-wrapper .loader-section {
         position: fixed;
         top: 0;
         width: 51%;
         height: 100%;
-        background: #7171C6;
+        background: #7171c6;
         z-index: 1000;
         -webkit-transform: translateX(0);
         -ms-transform: translateX(0);
@@ -140,21 +138,20 @@
         right: 0;
       }
 
-
       .loaded #loader-wrapper .loader-section.section-left {
         -webkit-transform: translateX(-100%);
         -ms-transform: translateX(-100%);
         transform: translateX(-100%);
-        -webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
-        transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
+        -webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+        transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
       }
 
       .loaded #loader-wrapper .loader-section.section-right {
         -webkit-transform: translateX(100%);
         -ms-transform: translateX(100%);
         transform: translateX(100%);
-        -webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
-        transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
+        -webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+        transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
       }
 
       .loaded #loader {
@@ -182,7 +179,7 @@
 
       #loader-wrapper .load_title {
         font-family: 'Open Sans';
-        color: #FFF;
+        color: #fff;
         font-size: 19px;
         width: 100%;
         text-align: center;
@@ -197,7 +194,7 @@
         font-weight: normal;
         font-style: italic;
         font-size: 13px;
-        color: #FFF;
+        color: #fff;
         opacity: 0.5;
       }
     </style>

+ 75 - 58
package.json

@@ -1,15 +1,16 @@
 {
   "name": "ruoyi-vue-plus",
-  "version": "5.1.2",
+  "version": "5.2.0-BETA",
   "description": "RuoYi-Vue-Plus多租户管理系统",
   "author": "LionLi",
   "license": "MIT",
+  "type": "module",
   "scripts": {
     "dev": "vite serve --mode development",
-    "build:prod": "vite build --mode production &&vue-tsc --noEmit",
+    "build:prod": "vite build --mode production",
+    "build:dev": "vite build --mode development",
     "preview": "vite preview",
-    "lint": "eslint src/**/*.{ts,js,vue} --fix",
-    "prepare": "husky install",
+    "lint:eslint": "eslint  --fix --ext .ts,.js,.vue ./src ",
     "prettier": "prettier --write ."
   },
   "repository": {
@@ -17,68 +18,84 @@
     "url": "https://gitee.com/JavaLionLi/plus-ui.git"
   },
   "dependencies": {
-    "@element-plus/icons-vue": "2.1.0",
+    "@element-plus/icons-vue": "2.3.1",
+    "@highlightjs/vue-plugin": "2.1.0",
+    "@lezer/common": "1.2.1",
     "@vueup/vue-quill": "1.2.0",
-    "@vueuse/core": "9.5.0",
+    "@vueuse/core": "10.9.0",
     "animate.css": "4.1.1",
-    "await-to-js": "^3.0.0",
-    "axios": "^1.3.4",
-    "echarts": "5.4.0",
-    "element-plus": "2.2.27",
+    "await-to-js": "3.0.0",
+    "axios": "1.6.8",
+    "bpmn-js": "16.4.0",
+    "camunda-bpmn-js-behaviors": "1.2.2",
+    "camunda-bpmn-moddle": "7.0.1",
+    "crypto-js": "4.2.0",
+    "diagram-js": "12.3.0",
+    "didi": "9.0.2",
+    "echarts": "5.5.0",
+    "element-plus": "2.7.2",
     "file-saver": "2.0.5",
-    "fuse.js": "6.6.2",
-    "js-cookie": "3.0.1",
-    "jsencrypt": "3.3.1",
-    "crypto-js": "^4.1.1",
+    "fuse.js": "7.0.0",
+    "highlight.js": "11.9.0",
+    "image-conversion": "^2.1.1",
+    "js-cookie": "3.0.5",
+    "jsencrypt": "3.3.2",
+    "moddle": "6.2.3",
     "nprogress": "0.2.0",
     "path-browserify": "1.0.1",
-    "path-to-regexp": "6.2.0",
-    "pinia": "2.0.22",
-    "screenfull": "6.0.0",
-    "vform3-builds": "3.0.8",
-    "vue": "3.2.45",
-    "vue-cropper": "1.0.3",
-    "vue-i18n": "9.2.2",
-    "vue-router": "4.1.4",
-    "vue-types": "^5.0.3"
+    "path-to-regexp": "6.2.1",
+    "pinia": "2.1.7",
+    "preact": "10.19.7",
+    "screenfull": "6.0.2",
+    "vform3-builds": "3.0.10",
+    "vue": "3.4.25",
+    "vue-cropper": "1.1.1",
+    "vue-i18n": "9.10.2",
+    "vue-router": "4.3.2",
+    "vue-types": "5.1.1",
+    "vxe-table": "4.5.22",
+    "zeebe-bpmn-moddle": "1.0.0"
   },
   "devDependencies": {
-    "@iconify/json": "^2.2.40",
-    "@intlify/unplugin-vue-i18n": "0.8.2",
-    "@types/crypto-js": "^4.1.1",
-    "@types/file-saver": "2.0.5",
-    "@types/js-cookie": "3.0.3",
-    "@types/node": "18.14.2",
-    "@types/nprogress": "0.2.0",
-    "@types/path-browserify": "^1.0.0",
-    "@typescript-eslint/eslint-plugin": "5.56.0",
-    "@typescript-eslint/parser": "5.56.0",
-    "@unocss/preset-attributify": "^0.50.6",
-    "@unocss/preset-icons": "^0.50.6",
-    "@unocss/preset-uno": "^0.50.6",
-    "@vitejs/plugin-vue": "4.0.0",
-    "@vue/compiler-sfc": "3.2.45",
-    "autoprefixer": "10.4.14",
-    "eslint": "8.36.0",
-    "eslint-config-prettier": "8.8.0",
-    "eslint-plugin-prettier": "4.2.1",
-    "eslint-plugin-vue": "9.9.0",
-    "fast-glob": "^3.2.11",
-    "husky": "7.0.4",
-    "postcss": "^8.4.21",
-    "prettier": "2.8.6",
-    "sass": "1.56.1",
-    "typescript": "4.9.5",
-    "unocss": "^0.50.6",
-    "unplugin-auto-import": "0.13.0",
-    "unplugin-icons": "0.15.1",
-    "unplugin-vue-components": "0.23.0",
-    "vite": "4.3.1",
+    "@iconify/json": "2.2.201",
+    "@intlify/unplugin-vue-i18n": "3.0.1",
+    "@types/crypto-js": "4.2.2",
+    "@types/file-saver": "2.0.7",
+    "@types/js-cookie": "3.0.6",
+    "@types/node": "18.18.2",
+    "@types/nprogress": "0.2.3",
+    "@types/path-browserify": "1.0.2",
+    "@typescript-eslint/eslint-plugin": "7.3.1",
+    "@typescript-eslint/parser": "7.3.1",
+    "@unocss/preset-attributify": "0.58.6",
+    "@unocss/preset-icons": "0.58.6",
+    "@unocss/preset-uno": "0.58.6",
+    "@vitejs/plugin-vue": "5.0.4",
+    "@vue/compiler-sfc": "3.4.23",
+    "autoprefixer": "10.4.18",
+    "eslint": "8.57.0",
+    "eslint-config-prettier": "9.1.0",
+    "eslint-define-config": "2.1.0",
+    "eslint-plugin-prettier": "5.1.3",
+    "eslint-plugin-promise": "6.1.1",
+    "eslint-plugin-node": "11.1.0",
+    "eslint-plugin-import": "2.29.1",
+    "eslint-plugin-vue": "9.23.0",
+    "fast-glob": "3.3.2",
+    "postcss": "8.4.36",
+    "prettier": "3.2.5",
+    "sass": "1.72.0",
+    "typescript": "5.4.5",
+    "unocss": "0.58.6",
+    "unplugin-auto-import": "0.17.5",
+    "unplugin-icons": "0.18.5",
+    "unplugin-vue-components": "0.26.0",
+    "unplugin-vue-setup-extend-plus": "1.0.1",
+    "vite": "5.2.10",
     "vite-plugin-compression": "0.5.1",
     "vite-plugin-svg-icons": "2.0.1",
-    "unplugin-vue-setup-extend-plus": "0.4.9",
-    "vitest": "^0.29.7",
-    "vue-eslint-parser": "9.1.0",
-    "vue-tsc": "0.35.0"
+    "vitest": "1.5.0",
+    "vue-eslint-parser": "9.4.2",
+    "vue-tsc": "2.0.13"
   }
 }

+ 6 - 7
src/App.vue

@@ -1,21 +1,20 @@
 <template>
-  <el-config-provider :locale="appStore.locale" :size="size">
+  <el-config-provider :locale="appStore.locale" :size="appStore.size">
     <router-view />
   </el-config-provider>
 </template>
 
 <script setup lang="ts">
-import useSettingsStore from '@/store/modules/settings'
-import { handleThemeStyle } from '@/utils/theme'
+import useSettingsStore from '@/store/modules/settings';
+import { handleThemeStyle } from '@/utils/theme';
 import useAppStore from '@/store/modules/app';
 
 const appStore = useAppStore();
-const size = computed(() => appStore.size as any);
 
 onMounted(() => {
   nextTick(() => {
     // 初始化主题样式
-    handleThemeStyle(useSettingsStore().theme)
-  })
-})
+    handleThemeStyle(useSettingsStore().theme);
+  });
+});
 </script>

+ 4 - 2
src/api/login.ts

@@ -20,7 +20,8 @@ export function login(data: LoginData): AxiosPromise<LoginResult> {
     url: '/auth/login',
     headers: {
       isToken: false,
-      isEncrypt: true
+      isEncrypt: true,
+      repeatSubmit: false
     },
     method: 'post',
     data: params
@@ -38,7 +39,8 @@ export function register(data: any) {
     url: '/auth/register',
     headers: {
       isToken: false,
-      isEncrypt: true
+      isEncrypt: true,
+      repeatSubmit: false
     },
     method: 'post',
     data: params

+ 16 - 0
src/api/monitor/online/index.ts

@@ -18,3 +18,19 @@ export function forceLogout(tokenId: string) {
     method: 'delete'
   });
 }
+
+// 获取当前用户登录在线设备
+export function getOnline() {
+  return request({
+    url: '/monitor/online',
+    method: 'get'
+  });
+}
+
+// 删除当前在线设备
+export function delOnline(tokenId: string) {
+  return request({
+    url: '/monitor/online/' + tokenId,
+    method: 'post'
+  });
+}

+ 3 - 3
src/api/system/client/index.ts

@@ -64,12 +64,12 @@ export const delClient = (id: string | number | Array<string | number>) => {
 
 /**
  * 状态修改
- * @param id ID
+ * @param clientId 客户端id
  * @param status 状态
  */
-export function changeStatus(id: number | string, status: string) {
+export function changeStatus(clientId: string, status: string) {
   const data = {
-    id,
+    clientId,
     status
   };
   return request({

+ 1 - 1
src/api/system/client/types.ts

@@ -7,7 +7,7 @@ export interface ClientVO {
   /**
    * 客户端id
    */
-  clientId: string | number;
+  clientId: string;
 
   /**
    * 客户端key

+ 1 - 1
src/api/system/config/index.ts

@@ -20,7 +20,7 @@ export function getConfig(configId: string | number): AxiosPromise<ConfigVO> {
 }
 
 // 根据参数键名查询参数值
-export function getConfigKey(configKey: string): AxiosPromise<String> {
+export function getConfigKey(configKey: string): AxiosPromise<string> {
   return request({
     url: '/system/config/configKey/' + configKey,
     method: 'get'

+ 3 - 0
src/api/system/dept/types.ts

@@ -3,6 +3,7 @@
  */
 export interface DeptQuery extends PageQuery {
   deptName?: string;
+  deptCategory?: string;
   status?: number;
 }
 
@@ -16,6 +17,7 @@ export interface DeptVO extends BaseEntity {
   children: DeptVO[];
   deptId: number | string;
   deptName: string;
+  deptCategory: string;
   orderNum: number;
   leader: string;
   phone: string;
@@ -35,6 +37,7 @@ export interface DeptForm {
   children?: DeptForm[];
   deptId?: number | string;
   deptName?: string;
+  deptCategory?: string;
   orderNum?: number;
   leader?: string;
   phone?: string;

+ 12 - 0
src/api/system/post/index.ts

@@ -19,6 +19,18 @@ export function getPost(postId: string | number): AxiosPromise<PostVO> {
   });
 }
 
+// 获取岗位选择框列表
+export function optionselect(deptId?: number | string, postIds?: (number | string)[]): AxiosPromise<PostVO[]> {
+  return request({
+    url: '/system/post/optionselect',
+    method: 'get',
+    params: {
+      postIds: postIds,
+      deptId: deptId
+    }
+  });
+}
+
 // 新增岗位
 export function addPost(data: PostForm) {
   return request({

+ 8 - 0
src/api/system/post/types.ts

@@ -1,7 +1,10 @@
 export interface PostVO extends BaseEntity {
   postId: number | string;
+  deptId: number | string;
   postCode: string;
   postName: string;
+  postCategory: string;
+  deptName: string;
   postSort: number;
   status: string;
   remark: string;
@@ -9,15 +12,20 @@ export interface PostVO extends BaseEntity {
 
 export interface PostForm {
   postId: number | string | undefined;
+  deptId: number | string | undefined;
   postCode: string;
   postName: string;
+  postCategory: string;
   postSort: number;
   status: string;
   remark: string;
 }
 
 export interface PostQuery extends PageQuery {
+  deptId: number | string;
+  belongDeptId: number | string;
   postCode: string;
   postName: string;
+  postCategory: string;
   status: string;
 }

+ 16 - 0
src/api/system/role/index.ts

@@ -12,6 +12,17 @@ export const listRole = (query: RoleQuery): AxiosPromise<RoleVO[]> => {
   });
 };
 
+/**
+ * 通过roleIds查询角色
+ * @param roleIds
+ */
+export const optionSelect = (roleIds: (number | string)[]): AxiosPromise<RoleVO[]> => {
+  return request({
+    url: '/system/role/optionselect?roleIds=' + roleIds,
+    method: 'get'
+  });
+};
+
 /**
  * 查询角色详细
  */
@@ -142,3 +153,8 @@ export const deptTreeSelect = (roleId: string | number): AxiosPromise<RoleDeptTr
     method: 'get'
   });
 };
+
+export default {
+  optionSelect,
+  listRole
+};

+ 2 - 1
src/api/system/tenant/index.ts

@@ -25,7 +25,8 @@ export function addTenant(data: TenantForm) {
     url: '/system/tenant',
     method: 'post',
     headers: {
-      isEncrypt: true
+      isEncrypt: true,
+      repeatSubmit: false
     },
     data: data
   });

+ 16 - 2
src/api/system/user/index.ts

@@ -17,6 +17,17 @@ export const listUser = (query: UserQuery): AxiosPromise<UserVO[]> => {
   });
 };
 
+/**
+ * 通过用户ids查询用户
+ * @param userIds
+ */
+export const optionSelect = (userIds: (number | string)[]): AxiosPromise<UserVO[]> => {
+  return request({
+    url: '/system/user/optionselect?userIds=' + userIds,
+    method: 'get'
+  });
+};
+
 /**
  * 获取用户详情
  * @param userId
@@ -75,7 +86,8 @@ export const resetUserPwd = (userId: string | number, password: string) => {
     url: '/system/user/resetPwd',
     method: 'put',
     headers: {
-      isEncrypt: true
+      isEncrypt: true,
+      repeatSubmit: false
     },
     data: data
   });
@@ -134,7 +146,8 @@ export const updateUserPwd = (oldPassword: string, newPassword: string) => {
     url: '/system/user/profile/updatePwd',
     method: 'put',
     headers: {
-      isEncrypt: true
+      isEncrypt: true,
+      repeatSubmit: false
     },
     data: data
   });
@@ -199,6 +212,7 @@ export const deptTreeSelect = (): AxiosPromise<DeptVO[]> => {
 export default {
   listUser,
   getUser,
+  optionSelect,
   addUser,
   updateUser,
   delUser,

+ 1 - 2
src/api/system/user/types.ts

@@ -1,4 +1,3 @@
-import { DeptVO } from './../dept/types';
 import { RoleVO } from '@/api/system/role/types';
 import { PostVO } from '@/api/system/post/types';
 
@@ -40,7 +39,7 @@ export interface UserVO extends BaseEntity {
   loginIp: string;
   loginDate: string;
   remark: string;
-  dept: DeptVO;
+  deptName: string;
   roles: RoleVO[];
   roleIds: any;
   postIds: any;

+ 2 - 2
src/api/tool/gen/index.ts

@@ -28,7 +28,7 @@ export const getGenTable = (tableId: string | number): AxiosPromise<GenTableVO>
 };
 
 // 修改代码生成信息
-export const updateGenTable = (data: DbTableForm) => {
+export const updateGenTable = (data: DbTableForm): AxiosPromise<GenTableVO> => {
   return request({
     url: '/tool/gen',
     method: 'put',
@@ -37,7 +37,7 @@ export const updateGenTable = (data: DbTableForm) => {
 };
 
 // 导入表
-export const importTable = (data: { tables: string; dataName: string }) => {
+export const importTable = (data: { tables: string; dataName: string }): AxiosPromise<GenTableVO> => {
   return request({
     url: '/tool/gen/importTable',
     method: 'post',

+ 63 - 0
src/api/workflow/category/index.ts

@@ -0,0 +1,63 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { CategoryVO, CategoryForm, CategoryQuery } from '@/api/workflow/category/types';
+
+/**
+ * 查询流程分类列表
+ * @param query
+ * @returns {*}
+ */
+
+export const listCategory = (query?: CategoryQuery): AxiosPromise<CategoryVO[]> => {
+  return request({
+    url: '/workflow/category/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询流程分类详细
+ * @param id
+ */
+export const getCategory = (id: string | number): AxiosPromise<CategoryVO> => {
+  return request({
+    url: '/workflow/category/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增流程分类
+ * @param data
+ */
+export const addCategory = (data: CategoryForm) => {
+  return request({
+    url: '/workflow/category',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改流程分类
+ * @param data
+ */
+export const updateCategory = (data: CategoryForm) => {
+  return request({
+    url: '/workflow/category',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除流程分类
+ * @param id
+ */
+export const delCategory = (id: string | number | Array<string | number>) => {
+  return request({
+    url: '/workflow/category/' + id,
+    method: 'delete'
+  });
+};

+ 67 - 0
src/api/workflow/category/types.ts

@@ -0,0 +1,67 @@
+export interface CategoryVO {
+  /**
+   * 主键
+   */
+  id: string;
+
+  /**
+   * 分类名称
+   */
+  categoryName: string;
+
+  /**
+   * 分类编码
+   */
+  categoryCode: string;
+
+  /**
+   * 父级id
+   */
+  parentId: string | number;
+
+  /**
+   * 排序
+   */
+  sortNum: number;
+
+  children?: CategoryVO[];
+}
+
+export interface CategoryForm extends BaseEntity {
+  /**
+   * 主键
+   */
+  id?: string | number;
+
+  /**
+   * 分类名称
+   */
+  categoryName?: string;
+
+  /**
+   * 分类编码
+   */
+  categoryCode?: string;
+
+  /**
+   * 父级id
+   */
+  parentId?: string | number;
+
+  /**
+   * 排序
+   */
+  sortNum?: number;
+}
+
+export interface CategoryQuery extends PageQuery {
+  /**
+   * 分类名称
+   */
+  categoryName?: string;
+
+  /**
+   * 分类编码
+   */
+  categoryCode?: string;
+}

+ 49 - 0
src/api/workflow/definitionConfig/index.ts

@@ -0,0 +1,49 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { DefinitionConfigVO, DefinitionConfigForm } from '@/api/workflow/definitionConfig/types';
+
+/**
+ * 查询表单配置详细
+ * @param definitionId
+ */
+export const getByDefId = (definitionId: string | number): AxiosPromise<DefinitionConfigVO> => {
+  return request({
+    url: '/workflow/definitionConfig/getByDefId/' + definitionId,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增表单配置
+ * @param data
+ */
+export const saveOrUpdate = (data: DefinitionConfigForm) => {
+  return request({
+    url: '/workflow/definitionConfig/saveOrUpdate',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 删除表单配置
+ * @param id
+ */
+export const deldefinitionConfig = (id: string | number | Array<string | number>) => {
+  return request({
+    url: '/workflow/definitionConfig/' + id,
+    method: 'delete'
+  });
+};
+
+/**
+ * 查询流程定义配置排除当前查询的流程定义
+ * @param tableName
+ * @param definitionId
+ */
+export const getByTableNameNotDefId = (tableName: string, definitionId: string | number) => {
+  return request({
+    url: `/workflow/definitionConfig/getByTableNameNotDefId/${tableName}/${definitionId}`,
+    method: 'get'
+  });
+};

+ 102 - 0
src/api/workflow/definitionConfig/types.ts

@@ -0,0 +1,102 @@
+import { FormManageVO } from '@/api/workflow/formManage/types';
+
+export interface DefinitionConfigVO {
+  /**
+   * 主键
+   */
+  id: string | number;
+
+  /**
+   * 表名
+   */
+  tableName?: string;
+
+  /**
+   * 流程定义ID
+   */
+  definitionId: string | number;
+
+  /**
+   * 流程KEY
+   */
+  processKey: string;
+
+  /**
+   * 流程版本
+   */
+  version?: string | number;
+
+  /**
+   * 备注
+   */
+  remark: string;
+
+  /**
+   * 表单管理
+   */
+  wfFormManageVo: FormManageVO;
+}
+
+export interface DefinitionConfigForm extends BaseEntity {
+  /**
+   * 主键
+   */
+  id?: string | number;
+
+  /**
+   * 表名
+   */
+  tableName?: string;
+
+  /**
+   * 流程定义ID
+   */
+  definitionId?: string | number;
+
+  /**
+   * 流程KEY
+   */
+  processKey?: string;
+
+  /**
+   * 流程版本
+   */
+  version?: string | number;
+
+  /**
+   * 备注
+   */
+  remark?: string;
+
+  /**
+   * 表单管理
+   */
+  wfFormManageVo?: FormManageVO;
+}
+
+export interface DefinitionConfigQuery extends PageQuery {
+  /**
+   * 表名
+   */
+  tableName?: string;
+
+  /**
+   * 流程定义ID
+   */
+  definitionId?: string | number;
+
+  /**
+   * 流程KEY
+   */
+  processKey?: string;
+
+  /**
+   * 流程版本
+   */
+  version?: string | number;
+
+  /**
+   * 表单管理
+   */
+  wfFormManageVo: FormManageVO;
+}

+ 76 - 0
src/api/workflow/formManage/index.ts

@@ -0,0 +1,76 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { FormManageVO, FormManageForm, FormManageQuery } from '@/api/workflow/formManage/types';
+
+/**
+ * 查询表单管理列表
+ * @param query
+ * @returns {*}
+ */
+
+export const listFormManage = (query?: FormManageQuery): AxiosPromise<FormManageVO[]> => {
+  return request({
+    url: '/workflow/formManage/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询表单管理列表
+ * @param query
+ * @returns {*}
+ */
+
+export const selectListFormManage = (): AxiosPromise<FormManageVO[]> => {
+  return request({
+    url: '/workflow/formManage/list/selectList',
+    method: 'get',
+  });
+};
+
+/**
+ * 查询表单管理详细
+ * @param id
+ */
+export const getFormManage = (id: string | number): AxiosPromise<FormManageVO> => {
+  return request({
+    url: '/workflow/formManage/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增表单管理
+ * @param data
+ */
+export const addFormManage = (data: FormManageForm) => {
+  return request({
+    url: '/workflow/formManage',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改表单管理
+ * @param data
+ */
+export const updateFormManage = (data: FormManageForm) => {
+  return request({
+    url: '/workflow/formManage',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除表单管理
+ * @param id
+ */
+export const delFormManage = (id: string | number | Array<string | number>) => {
+  return request({
+    url: '/workflow/formManage/' + id,
+    method: 'delete'
+  });
+};

+ 69 - 0
src/api/workflow/formManage/types.ts

@@ -0,0 +1,69 @@
+export interface FormManageVO {
+  /**
+   * 主键
+   */
+  id: string | number;
+
+  /**
+   * 表单名称
+   */
+  formName: string;
+
+  /**
+   * 表单类型
+   */
+  formType: string;
+  /**
+   * 表单类型名称
+   */
+  formTypeName: string;
+
+  /**
+   * 路由地址/表单ID
+   */
+  router: string;
+
+  /**
+   * 备注
+   */
+  remork: string;
+}
+
+export interface FormManageForm extends BaseEntity {
+  /**
+   * 主键
+   */
+  id?: string | number;
+
+  /**
+   * 表单名称
+   */
+  formName?: string;
+
+  /**
+   * 表单类型
+   */
+  formType?: string;
+
+  /**
+   * 路由地址/表单ID
+   */
+  router?: string;
+
+  /**
+   * 备注
+   */
+  remork?: string;
+}
+
+export interface FormManageQuery extends PageQuery {
+  /**
+   * 表单名称
+   */
+  formName?: string;
+
+  /**
+   * 表单类型
+   */
+  formType?: string;
+}

+ 63 - 0
src/api/workflow/leave/index.ts

@@ -0,0 +1,63 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { LeaveVO, LeaveQuery, LeaveForm } from '@/api/workflow/leave/types';
+
+/**
+ * 查询请假列表
+ * @param query
+ * @returns {*}
+ */
+
+export const listLeave = (query?: LeaveQuery): AxiosPromise<LeaveVO[]> => {
+  return request({
+    url: '/demo/leave/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询请假详细
+ * @param id
+ */
+export const getLeave = (id: string | number): AxiosPromise<LeaveVO> => {
+  return request({
+    url: '/demo/leave/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增请假
+ * @param data
+ */
+export const addLeave = (data: LeaveForm): AxiosPromise<LeaveVO> => {
+  return request({
+    url: '/demo/leave',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改请假
+ * @param data
+ */
+export const updateLeave = (data: LeaveForm): AxiosPromise<LeaveVO> => {
+  return request({
+    url: '/demo/leave',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除请假
+ * @param id
+ */
+export const delLeave = (id: string | number | Array<string | number>) => {
+  return request({
+    url: '/demo/leave/' + id,
+    method: 'delete'
+  });
+};

+ 24 - 0
src/api/workflow/leave/types.ts

@@ -0,0 +1,24 @@
+export interface LeaveVO {
+  id: string | number;
+  leaveType: string;
+  startDate: string;
+  endDate: string;
+  leaveDays: number;
+  remark: string;
+  processInstanceVo: any;
+}
+
+export interface LeaveForm extends BaseEntity {
+  id?: string | number;
+  leaveType?: string;
+  startDate?: string;
+  endDate?: string;
+  leaveDays?: number;
+  remark?: string;
+  processInstanceVo?: any;
+}
+
+export interface LeaveQuery extends PageQuery {
+  startLeaveDays?: number;
+  endLeaveDays?: number;
+}

+ 104 - 0
src/api/workflow/model/index.ts

@@ -0,0 +1,104 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { ModelForm, ModelQuery, ModelVO } from '@/api/workflow/model/types';
+
+/**
+ * 查询模型列表
+ * @param query
+ * @returns {*}
+ */
+export const listModel = (query: ModelQuery): AxiosPromise<ModelVO[]> => {
+  return request({
+    url: '/workflow/model/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询模型信息
+ * @param query
+ * @returns {*}
+ */
+export const getInfo = (id: string): AxiosPromise<ModelForm> => {
+  return request({
+    url: '/workflow/model/getInfo/'+id,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增模型
+ * @param data
+ * @returns {*}
+ */
+export const addModel = (data: ModelForm): AxiosPromise<void> => {
+  return request({
+    url: '/workflow/model/save',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改模型信息
+ * @param data
+ * @returns {*}
+ */
+export function update(data: ModelForm): AxiosPromise<void> {
+  return request({
+    url: '/workflow/model/update',
+    method: 'put',
+    data: data
+  });
+}
+
+/**
+ * 修改模型信息
+ * @param data
+ * @returns {*}
+ */
+export function editModelXml(data: ModelForm): AxiosPromise<void> {
+  return request({
+    url: '/workflow/model/editModelXml',
+    method: 'put',
+    data: data
+  });
+}
+
+/**
+ * 按id删除模型
+ * @returns {*}
+ * @param id 模型id
+ */
+export function delModel(id: string | string[]): AxiosPromise<void> {
+  return request({
+    url: '/workflow/model/' + id,
+    method: 'delete'
+  });
+}
+
+/**
+ * 模型部署
+ * @returns {*}
+ * @param id 模型id
+ */
+export const modelDeploy = (id: string): AxiosPromise<void> => {
+  return request({
+    url: `/workflow/model/modelDeploy/${id}`,
+    method: 'post'
+  });
+};
+
+/**
+ * 复制模型
+ * @param data
+ * @returns {*}
+ */
+export const copyModel = (data: ModelForm): AxiosPromise<void> => {
+  return request({
+    url: '/workflow/model/copyModel',
+    method: 'post',
+    data: data
+  });
+};

+ 66 - 0
src/api/workflow/model/types.ts

@@ -0,0 +1,66 @@
+export interface ModelForm {
+  id: string,
+  name: string;
+  key: string;
+  categoryCode: string;
+  xml:string,
+  svg:string,
+  description: string;
+}
+
+export interface ModelQuery extends PageQuery {
+  name?: string;
+  key?: string;
+  categoryCode?: string;
+}
+
+export interface OriginalPersistentState {
+  metaInfo: string;
+  editorSourceValueId: string;
+  createTime: string;
+  deploymentId?: string;
+  name: string;
+  tenantId: string;
+  category?: string;
+  version: number;
+  editorSourceExtraValueId?: string;
+  key: string;
+  lastUpdateTime: string;
+}
+
+export interface PersistentState {
+  metaInfo: string;
+  editorSourceValueId: string;
+  createTime: string;
+  deploymentId?: string;
+  name: string;
+  tenantId: string;
+  category?: string;
+  version: number;
+  editorSourceExtraValueId?: string;
+  key: string;
+  lastUpdateTime: string;
+}
+
+export interface ModelVO {
+  id: string;
+  revision: number;
+  originalPersistentState: OriginalPersistentState;
+  name: string;
+  key: string;
+  category?: string;
+  createTime: string;
+  lastUpdateTime: string;
+  version: number;
+  metaInfo: string;
+  deploymentId?: string;
+  editorSourceValueId: string;
+  editorSourceExtraValueId?: string;
+  tenantId: string;
+  persistentState: PersistentState;
+  revisionNext: number;
+  idPrefix: string;
+  inserted: boolean;
+  updated: boolean;
+  deleted: boolean;
+}

+ 63 - 0
src/api/workflow/nodeConfig/index.ts

@@ -0,0 +1,63 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { NodeConfigVO, NodeConfigForm, NodeConfigQuery } from '@/api/workflow/nodeConfig/types';
+
+/**
+ * 查询节点配置列表
+ * @param query
+ * @returns {*}
+ */
+
+export const listNodeConfig = (query?: NodeConfigQuery): AxiosPromise<NodeConfigVO[]> => {
+  return request({
+    url: '/workflow/nodeConfig/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询节点配置详细
+ * @param id
+ */
+export const getNodeConfig = (id: string | number): AxiosPromise<NodeConfigVO> => {
+  return request({
+    url: '/workflow/nodeConfig/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增节点配置
+ * @param data
+ */
+export const addNodeConfig = (data: NodeConfigForm) => {
+  return request({
+    url: '/workflow/nodeConfig',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改节点配置
+ * @param data
+ */
+export const updateNodeConfig = (data: NodeConfigForm) => {
+  return request({
+    url: '/workflow/nodeConfig',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除节点配置
+ * @param id
+ */
+export const delNodeConfig = (id: string | number | Array<string | number>) => {
+  return request({
+    url: '/workflow/nodeConfig/' + id,
+    method: 'delete'
+  });
+};

+ 43 - 0
src/api/workflow/nodeConfig/types.ts

@@ -0,0 +1,43 @@
+import { FormManageVO } from '@/api/workflow/formManage/types';
+
+export interface NodeConfigVO {
+  /**
+   * 主键
+   */
+  id: string | number;
+
+  /**
+   * 表单id
+   */
+  formId: string | number;
+
+  /**
+   * 表单类型
+   */
+  formType: string;
+
+  /**
+   * 节点名称
+   */
+  nodeName: string;
+
+  /**
+   * 节点id
+   */
+  nodeId: string | number;
+
+  /**
+   * 流程定义id
+   */
+  definitionId: string | number;
+
+  /**
+   * 表单管理
+   */
+  wfFormManageVo: FormManageVO;
+
+}
+
+
+
+

+ 114 - 0
src/api/workflow/processDefinition/index.ts

@@ -0,0 +1,114 @@
+import request from '@/utils/request';
+import { ProcessDefinitionQuery, ProcessDefinitionVO, definitionXmlVO } from '@/api/workflow/processDefinition/types';
+import { AxiosPromise } from 'axios';
+
+/**
+ * 获取流程定义列表
+ * @param query 流程实例id
+ * @returns
+ */
+export const listProcessDefinition = (query: ProcessDefinitionQuery): AxiosPromise<ProcessDefinitionVO[]> => {
+  return request({
+    url: `/workflow/processDefinition/list`,
+    method: 'get',
+    params: query
+  });
+};
+/**
+ * 按照流程定义key获取流程定义
+ * @param processInstanceId 流程实例id
+ * @returns
+ */
+export const getListByKey = (key: string) => {
+  return request({
+    url: `/workflow/processDefinition/getListByKey/${key}`,
+    method: 'get'
+  });
+};
+
+/**
+ * 通过流程定义id获取流程图
+ */
+export const definitionImage = (processDefinitionId: string): AxiosPromise<any> => {
+  return request({
+    url: `/workflow/processDefinition/definitionImage/${processDefinitionId}` + '?t' + Math.random(),
+    method: 'get'
+  });
+};
+
+/**
+ * 通过流程定义id获取xml
+ * @param processDefinitionId 流程定义id
+ * @returns
+ */
+export const definitionXml = (processDefinitionId: string): AxiosPromise<definitionXmlVO> => {
+  return request({
+    url: `/workflow/processDefinition/definitionXml/${processDefinitionId}`,
+    method: 'get'
+  });
+};
+
+/**
+ * 删除流程定义
+ * @param deploymentId 部署id
+ * @param processDefinitionId 流程定义id
+ * @returns
+ */
+export const deleteProcessDefinition = (deploymentId: string | string[], processDefinitionId: string | string[]) => {
+  return request({
+    url: `/workflow/processDefinition/${deploymentId}/${processDefinitionId}`,
+    method: 'delete'
+  });
+};
+
+/**
+ * 挂起/激活
+ * @param processDefinitionId 流程定义id
+ * @returns
+ */
+export const updateDefinitionState = (processDefinitionId: string) => {
+  return request({
+    url: `/workflow/processDefinition/updateDefinitionState/${processDefinitionId}`,
+    method: 'put'
+  });
+};
+
+/**
+ * 流程定义转换为模型
+ * @param processDefinitionId 流程定义id
+ * @returns
+ */
+export const convertToModel = (processDefinitionId: string) => {
+  return request({
+    url: `/workflow/processDefinition/convertToModel/${processDefinitionId}`,
+    method: 'put'
+  });
+};
+
+/**
+ * 通过zip或xml部署流程定义
+ * @returns
+ */
+export function deployProcessFile(data: any) {
+  return request({
+    url: '/workflow/processDefinition/deployByFile',
+    method: 'post',
+    data: data,
+    headers: {
+      repeatSubmit: false
+    }
+  });
+}
+
+/**
+ * 迁移流程
+ * @param currentProcessDefinitionId
+ * @param fromProcessDefinitionId
+ * @returns
+ */
+export const migrationDefinition = (currentProcessDefinitionId: string, fromProcessDefinitionId: string) => {
+  return request({
+    url: `/workflow/processDefinition/migrationDefinition/${currentProcessDefinitionId}/${fromProcessDefinitionId}`,
+    method: 'put'
+  });
+};

+ 24 - 0
src/api/workflow/processDefinition/types.ts

@@ -0,0 +1,24 @@
+import { DefinitionConfigVO } from '@/api/workflow/definitionConfig/types';
+export interface ProcessDefinitionQuery extends PageQuery {
+  key?: string;
+  name?: string;
+  categoryCode?: string;
+}
+
+export interface ProcessDefinitionVO extends BaseEntity {
+  id: string;
+  name: string;
+  key: string;
+  version: number;
+  suspensionState: number;
+  resourceName: string;
+  diagramResourceName: string;
+  deploymentId: string;
+  deploymentTime: string;
+  wfDefinitionConfigVo: DefinitionConfigVO;
+}
+
+export interface definitionXmlVO {
+  xml: string[];
+  xmlStr: string;
+}

+ 136 - 0
src/api/workflow/processInstance/index.ts

@@ -0,0 +1,136 @@
+import request from '@/utils/request';
+import { ProcessInstanceQuery, ProcessInstanceVO } from '@/api/workflow/processInstance/types';
+import { AxiosPromise } from 'axios';
+
+/**
+ * 查询运行中实例列表
+ * @param query
+ * @returns {*}
+ */
+export const getPageByRunning = (query: ProcessInstanceQuery): AxiosPromise<ProcessInstanceVO[]> => {
+  return request({
+    url: '/workflow/processInstance/getPageByRunning',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询已完成实例列表
+ * @param query
+ * @returns {*}
+ */
+export const getPageByFinish = (query: ProcessInstanceQuery): AxiosPromise<ProcessInstanceVO[]> => {
+  return request({
+    url: '/workflow/processInstance/getPageByFinish',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 通过流程实例id获取历史流程图
+ */
+export const getHistoryImage = (processInstanceId: string) => {
+  return request({
+    url: `/workflow/processInstance/getHistoryImage/${processInstanceId}` + '?t' + Math.random(),
+    method: 'get'
+  });
+};
+
+/**
+ * 通过流程实例id获取历史流程图运行中,历史等节点
+ */
+export const getHistoryList = (instanceId: string): AxiosPromise<Record<string, any>> => {
+  return request({
+    url: `/workflow/processInstance/getHistoryList/${instanceId}` + '?t' + Math.random(),
+    method: 'get'
+  });
+};
+
+/**
+ * 获取审批记录
+ * @param processInstanceId 流程实例id
+ * @returns
+ */
+export const getHistoryRecord = (processInstanceId: string) => {
+  return request({
+    url: `/workflow/processInstance/getHistoryRecord/${processInstanceId}`,
+    method: 'get'
+  });
+};
+
+/**
+ * 作废
+ * @param data 参数
+ * @returns
+ */
+export const deleteRunInstance = (data: object) => {
+  return request({
+    url: `/workflow/processInstance/deleteRunInstance`,
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 运行中的实例 删除程实例,删除历史记录,删除业务与流程关联信息
+ * @param processInstanceId 流程实例id
+ * @returns
+ */
+export const deleteRunAndHisInstance = (processInstanceId: string | string[]) => {
+  return request({
+    url: `/workflow/processInstance/deleteRunAndHisInstance/${processInstanceId}`,
+    method: 'delete'
+  });
+};
+
+/**
+ * 已完成的实例 删除程实例,删除历史记录,删除业务与流程关联信息
+ * @param processInstanceId 流程实例id
+ * @returns
+ */
+export const deleteFinishAndHisInstance = (processInstanceId: string | string[]) => {
+  return request({
+    url: `/workflow/processInstance/deleteFinishAndHisInstance/${processInstanceId}`,
+    method: 'delete'
+  });
+};
+
+/**
+ * 分页查询当前登录人单据
+ * @param query
+ * @returns {*}
+ */
+export const getPageByCurrent = (query: ProcessInstanceQuery): AxiosPromise<ProcessInstanceVO[]> => {
+  return request({
+    url: '/workflow/processInstance/getPageByCurrent',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 撤销流程
+ * @param processInstanceId 流程实例id
+ * @returns
+ */
+export const cancelProcessApply = (processInstanceId: string) => {
+  return request({
+    url: `/workflow/processInstance/cancelProcessApply/${processInstanceId}`,
+    method: 'post'
+  });
+};
+
+export default {
+  getPageByRunning,
+  getPageByFinish,
+  getHistoryImage,
+  getHistoryList,
+  getHistoryRecord,
+  deleteRunInstance,
+  deleteRunAndHisInstance,
+  deleteFinishAndHisInstance,
+  getPageByCurrent,
+  cancelProcessApply
+};

+ 27 - 0
src/api/workflow/processInstance/types.ts

@@ -0,0 +1,27 @@
+import { TaskVO } from '@/api/workflow/task/types';
+
+export interface ProcessInstanceQuery extends PageQuery {
+  categoryCode?: string;
+  name?: string;
+  key?: string;
+  startUserId?: string;
+  businessKey?: string;
+}
+
+export interface ProcessInstanceVO extends BaseEntity {
+  id: string;
+  processDefinitionId: string;
+  processDefinitionName: string;
+  processDefinitionKey: string;
+  processDefinitionVersion: string;
+  deploymentId: string;
+  businessKey: string;
+  isSuspended?: any;
+  tenantId: string;
+  startTime: string;
+  endTime?: string;
+  startUserId: string;
+  businessStatus: string;
+  businessStatusName: string;
+  taskVoList: TaskVO[];
+}

+ 264 - 0
src/api/workflow/task/index.ts

@@ -0,0 +1,264 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { TaskQuery, TaskVO } from '@/api/workflow/task/types';
+
+/**
+ * 查询待办列表
+ * @param query
+ * @returns {*}
+ */
+export const getPageByTaskWait = (query: TaskQuery): AxiosPromise<TaskVO[]> => {
+  return request({
+    url: '/workflow/task/getPageByTaskWait',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询已办列表
+ * @param query
+ * @returns {*}
+ */
+export const getPageByTaskFinish = (query: TaskQuery): AxiosPromise<TaskVO[]> => {
+  return request({
+    url: '/workflow/task/getPageByTaskFinish',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询当前用户的抄送列表
+ * @param query
+ * @returns {*}
+ */
+export const getPageByTaskCopy = (query: TaskQuery): AxiosPromise<TaskVO[]> => {
+  return request({
+    url: '/workflow/task/getPageByTaskCopy',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 当前租户所有待办任务
+ * @param query
+ * @returns {*}
+ */
+export const getPageByAllTaskWait = (query: TaskQuery): AxiosPromise<TaskVO[]> => {
+  return request({
+    url: '/workflow/task/getPageByAllTaskWait',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 当前租户所有已办任务
+ * @param query
+ * @returns {*}
+ */
+export const getPageByAllTaskFinish = (query: TaskQuery): AxiosPromise<TaskVO[]> => {
+  return request({
+    url: '/workflow/task/getPageByAllTaskFinish',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 启动流程
+ * @param data
+ * @returns {*}
+ */
+export const startWorkFlow = (data: object): any => {
+  return request({
+    url: '/workflow/task/startWorkFlow',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 办理流程
+ * @param data
+ * @returns {*}
+ */
+export const completeTask = (data: object) => {
+  return request({
+    url: '/workflow/task/completeTask',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 认领任务
+ * @param taskId
+ * @returns {*}
+ */
+export const claim = (taskId: string): any => {
+  return request({
+    url: '/workflow/task/claim/' + taskId,
+    method: 'post'
+  });
+};
+
+/**
+ * 归还任务
+ * @param taskId
+ * @returns {*}
+ */
+export const returnTask = (taskId: string): any => {
+  return request({
+    url: '/workflow/task/returnTask/' + taskId,
+    method: 'post'
+  });
+};
+
+/**
+ * 任务驳回
+ * @param data
+ * @returns {*}
+ */
+export const backProcess = (data: any): any => {
+  return request({
+    url: '/workflow/task/backProcess',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 获取当前任务
+ * @param taskId
+ * @returns
+ */
+export const getTaskById = (taskId: string) => {
+  return request({
+    url: '/workflow/task/getTaskById/' + taskId,
+    method: 'get'
+  });
+};
+
+/**
+ * 加签
+ * @param data
+ * @returns
+ */
+export const addMultiInstanceExecution = (data: any) => {
+  return request({
+    url: '/workflow/task/addMultiInstanceExecution',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 减签
+ * @param data
+ * @returns
+ */
+export const deleteMultiInstanceExecution = (data: any) => {
+  return request({
+    url: '/workflow/task/deleteMultiInstanceExecution',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改任务办理人
+ * @param taskIds
+ * @param userId
+ * @returns
+ */
+export const updateAssignee = (taskIds: Array<string>, userId: string) => {
+  return request({
+    url: `/workflow/task/updateAssignee/${taskIds}/${userId}`,
+    method: 'put'
+  });
+};
+
+/**
+ * 转办任务
+ * @returns
+ */
+export const transferTask = (data: any) => {
+  return request({
+    url: `/workflow/task/transferTask`,
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 终止任务
+ * @returns
+ */
+export const terminationTask = (data: any) => {
+  return request({
+    url: `/workflow/task/terminationTask`,
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 查询流程变量
+ * @returns
+ */
+export const getInstanceVariable = (taskId: string) => {
+  return request({
+    url: `/workflow/task/getInstanceVariable/${taskId}`,
+    method: 'get'
+  });
+};
+
+/**
+ * 获取可驳回得任务节点
+ * @returns
+ */
+export const getTaskNodeList = (processInstanceId: string) => {
+  return request({
+    url: `/workflow/task/getTaskNodeList/${processInstanceId}`,
+    method: 'get'
+  });
+};
+
+/**
+ * 委托任务
+ * @returns
+ */
+export const delegateTask = (data: any) => {
+  return request({
+    url: `/workflow/task/delegateTask`,
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 查询工作流任务用户选择加签人员
+ * @param taskId
+ * @returns {*}
+ */
+export const getTaskUserIdsByAddMultiInstance = (taskId: string) => {
+  return request({
+    url: '/workflow/task/getTaskUserIdsByAddMultiInstance/' + taskId,
+    method: 'get'
+  });
+};
+
+/**
+ * 查询工作流选择减签人员
+ * @param taskId
+ * @returns {*}
+ */
+export const getListByDeleteMultiInstance = (taskId: string) => {
+  return request({
+    url: '/workflow/task/getListByDeleteMultiInstance/' + taskId,
+    method: 'get'
+  });
+};

+ 49 - 0
src/api/workflow/task/types.ts

@@ -0,0 +1,49 @@
+import { NodeConfigVO } from '@/api/workflow/nodeConfig/types';
+import { DefinitionConfigVO } from '@/api/workflow/definitionConfig/types';
+export interface TaskQuery extends PageQuery {
+  name?: string;
+  processDefinitionKey?: string;
+  processDefinitionName?: string;
+}
+
+export interface ParticipantVo {
+  groupIds?: string[] | number[];
+  candidate: string[] | number[];
+  candidateName: string[];
+  claim: boolean;
+}
+
+export interface TaskVO extends BaseEntity {
+  id: string;
+  name: string;
+  description?: string;
+  priority: number;
+  owner?: string;
+  assignee?: string | number;
+  assigneeName?: string;
+  processInstanceId: string;
+  executionId: string;
+  taskDefinitionId?: any;
+  processDefinitionId: string;
+  endTime?: string;
+  taskDefinitionKey: string;
+  dueDate?: string;
+  category?: any;
+  parentTaskId?: any;
+  tenantId: string;
+  claimTime?: string;
+  businessStatus?: string;
+  businessStatusName?: string;
+  processDefinitionName?: string;
+  processDefinitionKey?: string;
+  participantVo?: ParticipantVo;
+  multiInstance?: boolean;
+  businessKey?: string;
+  wfNodeConfigVo?: NodeConfigVO;
+  wfDefinitionConfigVo?: DefinitionConfigVO;
+}
+
+export interface VariableVo {
+  key: string;
+  value: string;
+}

+ 29 - 0
src/api/workflow/workflowCommon/index.ts

@@ -0,0 +1,29 @@
+import { RouterJumpVo } from '@/api/workflow/workflowCommon/types';
+
+export default {
+    routerJump(routerJumpVo: RouterJumpVo,proxy){
+        if (routerJumpVo.wfNodeConfigVo && routerJumpVo.wfNodeConfigVo.formType === 'static' && routerJumpVo.wfNodeConfigVo.wfFormManageVo) {
+            proxy.$tab.closePage(proxy.$route);
+            proxy.$router.push({
+                path: `${routerJumpVo.wfNodeConfigVo.wfFormManageVo.router}`,
+                query: {
+                    id: routerJumpVo.businessKey,
+                    type: routerJumpVo.type,
+                    taskId: routerJumpVo.taskId
+                }
+            });
+        } else if (routerJumpVo.wfNodeConfigVo && routerJumpVo.wfNodeConfigVo.formType === 'dynamic' && routerJumpVo.wfNodeConfigVo.wfFormManageVo) {
+            proxy.$tab.closePage(proxy.$route);
+            proxy.$router.push({
+                path: `${routerJumpVo.wfNodeConfigVo.wfFormManageVo.router}`,
+                query: {
+                    id: routerJumpVo.businessKey,
+                    type: routerJumpVo.type,
+                    taskId: routerJumpVo.taskId
+                }
+            });
+        }else {
+            proxy?.$modal.msgError('请到模型配置菜单!');
+        }
+    }
+}

+ 16 - 0
src/api/workflow/workflowCommon/types.ts

@@ -0,0 +1,16 @@
+import { NodeConfigVO } from '@/api/workflow/nodeConfig/types';
+import { DefinitionConfigVO } from '@/api/workflow/definitionConfig/types';
+
+export interface RouterJumpVo {
+  wfNodeConfigVo: NodeConfigVO;
+  wfDefinitionConfigVo: DefinitionConfigVO;
+  businessKey: string;
+  taskId: string;
+  type: string;
+}
+
+export interface StartProcessBo {
+  businessKey: string | number;
+  tableName: string;
+  variables: any;
+}

+ 1 - 0
src/assets/icons/svg/caret-back.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"><path d="M321.94 98L158.82 237.78a24 24 0 000 36.44L321.94 414c15.57 13.34 39.62 2.28 39.62-18.22v-279.6c0-20.5-24.05-31.56-39.62-18.18z"/></svg>

+ 1 - 0
src/assets/icons/svg/caret-forward.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"><path d="M190.06 414l163.12-139.78a24 24 0 000-36.44L190.06 98c-15.57-13.34-39.62-2.28-39.62 18.22v279.6c0 20.5 24.05 31.56 39.62 18.18z"/></svg>

+ 1 - 0
src/assets/icons/svg/category.svg

@@ -0,0 +1 @@
+<svg t="1715954426124" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3305" width="200" height="200"><path d="M664.081597 1023.943114a78.246037 78.246037 0 0 1-78.985549-76.795456v-284.996471a78.27448 78.27448 0 0 1 78.985549-76.93767h280.843828A78.189152 78.189152 0 0 1 1023.939417 662.151187v284.996471a78.246037 78.246037 0 0 1-79.013992 76.795456z m-585.067605 0a78.246037 78.246037 0 0 1-78.985549-76.795456v-284.996471a78.160709 78.160709 0 0 1 78.985549-76.93767h280.786942a78.302923 78.302923 0 0 1 79.042434 76.93767v284.996471h-0.170656a78.246037 78.246037 0 0 1-78.985549 76.795456z m0-585.096048a78.217594 78.217594 0 0 1-78.985549-76.93767V76.912925a78.189152 78.189152 0 0 1 78.957106-76.795456h280.786942a78.27448 78.27448 0 0 1 79.042435 76.93767v284.996471a78.27448 78.27448 0 0 1-79.013992 76.795456z m589.675333-5.688552a77.193655 77.193655 0 0 1-77.990052-75.885288V75.888985a77.25054 77.25054 0 0 1 77.990052-75.942173h277.26004a77.25054 77.25054 0 0 1 77.961609 75.942173v281.384241a77.421197 77.421197 0 0 1-78.132266 75.885288z" p-id="3306" fill="currentColor"></path></svg>

+ 1 - 0
src/assets/icons/svg/finish.svg

@@ -0,0 +1 @@
+<svg t="1716006237008" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12400" width="200" height="200"><path d="M738.826039 1005.166431c-150.226824 0-272.00251-121.916235-272.00251-272.303686 0-150.407529 121.775686-272.323765 272.00251-272.323765 150.206745 0 271.982431 121.916235 271.982432 272.323765 0 150.387451-121.775686 272.303686-271.982432 272.303686z m-0.040157-508.225255c-128.582275 0-232.789333 104.347608-232.789333 233.09051s104.207059 233.110588 232.789333 233.110589c128.562196 0 232.769255-104.367686 232.769255-233.110589 0-128.742902-104.207059-233.09051-232.769255-233.09051z m10.561255 318.243138s-3.694431 3.674353-7.408941 3.674353a18.010353 18.010353 0 0 1-25.941333 0l-74.10949-80.916079a17.66902 17.66902 0 0 1 0-25.740549c7.408941-7.368784 22.246902-7.368784 25.941333 0l63.006118 69.872941 129.686588-117.699764a18.010353 18.010353 0 0 1 25.941333 0 17.709176 17.709176 0 0 1 0 25.760627L749.347137 815.184314zM391.529412 682.666667H190.745098a20.078431 20.078431 0 0 1 0-40.156863h200.784314a20.078431 20.078431 0 1 1 0 40.156863zM170.666667 261.019608a20.078431 20.078431 0 0 1 20.078431-20.078432h481.882353a20.078431 20.078431 0 0 1 0 40.156863H190.745098a20.078431 20.078431 0 0 1-20.078431-20.078431z m341.333333 200.784314H190.745098a20.078431 20.078431 0 0 1 0-40.156863h321.254902a20.078431 20.078431 0 0 1 0 40.156863zM813.176471 120.470588a80.313725 80.313725 0 0 0-80.313726-80.313725H130.509804a80.313725 80.313725 0 0 0-80.313726 80.313725v762.980392a80.313725 80.313725 0 0 0 80.313726 80.313726h366.832941a346.112 346.112 0 0 0 40.417882 40.779294H130.509804a120.470588 120.470588 0 0 1-120.470588-120.470588V120.470588a120.470588 120.470588 0 0 1 120.470588-120.470588h602.352941a120.470588 120.470588 0 0 1 120.470588 120.470588v293.667137a340.188863 340.188863 0 0 0-40.156862-8.533333V120.470588z" fill="currentColor" p-id="12401"></path></svg>

+ 1 - 0
src/assets/icons/svg/model.svg

@@ -0,0 +1 @@
+<svg t="1715953291934" class="icon" viewBox="0 0 1061 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1715" id="mx_n_1715953291935" width="200" height="200"><path d="M447.122465 467.332105L49.240301 268.161564A33.501036 33.501036 0 0 0 0.136043 300.744763v441.020484a33.042117 33.042117 0 0 0 16.06214 27.994016L413.162511 1018.034062a33.959954 33.959954 0 0 0 17.438895 5.50702 33.042117 33.042117 0 0 0 33.042117-33.042118V497.161795a33.042117 33.042117 0 0 0-17.438895-29.82969zM398.018207 931.298504l-331.339011-208.348907v-367.134638l331.339011 162.915996zM1046.010843 263.572381a33.042117 33.042117 0 0 0-31.665363 0L550.838 467.332105a33.042117 33.042117 0 0 0-19.733487 30.288608v493.33717a33.042117 33.042117 0 0 0 49.563176 28.452934l463.048562-265.254776a33.042117 33.042117 0 0 0 16.521059-28.452934V291.566398a33.042117 33.042117 0 0 0-14.685386-27.994017z m-50.939931 441.020484L596.72983 931.298504v-413.026468l397.882163-176.224626zM991.399565 178.672496a33.042117 33.042117 0 0 0-22.486996-29.829689L550.838 1.530034a32.583199 32.583199 0 0 0-19.733487 0L83.659173 158.021173a33.042117 33.042117 0 0 0-4.130264 61.036134l397.882163 199.170541a33.042117 33.042117 0 0 0 14.685386 3.212428 33.959954 33.959954 0 0 0 13.30863 0l463.966399-205.595398a33.042117 33.042117 0 0 0 22.028078-37.172382zM494.391049 349.849021L180.490934 195.193555l358.874108-125.743613 328.126583 112.434982z m0 0" fill="currentColor" p-id="1716"></path></svg>

+ 1 - 0
src/assets/icons/svg/my-copy.svg

@@ -0,0 +1 @@
+<svg t="1716006583362" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="38505" width="200" height="200"><path d="M733.696 666.624l56.32-65.536-15.36-12.8c-41.472-34.816-87.552-61.44-136.192-79.36 75.264-49.152 124.928-134.144 124.928-230.912 0-152.576-123.904-276.48-276.48-276.48-74.24 0-143.872 28.672-195.584 80.384-52.224 51.712-80.896 121.344-80.896 195.584 0 92.16 45.568 174.08 115.2 224.256-81.408 26.624-156.672 74.752-215.552 144.896C34.304 736.768-4.096 850.944 0.512 968.192l1.024 20.48 86.528-4.608-1.024-19.968c-4.096-96.256 27.136-188.928 88.576-261.12 136.704-162.816 380.416-184.32 543.232-48.64l14.848 12.288zM296.96 278.016c0-106.496 83.456-189.952 189.952-189.952 104.96 0 189.952 84.992 189.952 189.952 0 106.496-83.456 189.952-189.952 189.952S296.96 384.512 296.96 278.016z m690.688 522.24H802.304c13.824-16.896 32.256-38.4 55.808-67.072 7.68-8.192 11.776-19.456 10.752-31.744-1.024-11.776-6.144-22.528-15.36-29.696-8.192-7.68-19.456-11.264-31.232-10.752-12.288 1.024-23.04 6.656-30.208 15.872-38.4 45.568-96.256 114.176-101.376 119.808-7.68 7.68-10.752 15.36-13.312 22.528-4.096 8.704-4.096 16.384-4.096 24.064 0 5.632 0 12.8 3.584 23.04 2.56 7.68 6.144 15.872 13.824 23.552l104.96 124.416 4.096 2.048c9.216 4.096 18.432 6.144 26.624 6.144 8.704 0 21.504-4.096 28.672-11.776 8.704-8.704 13.824-19.968 14.336-31.744 0-10.752-3.584-20.48-11.264-28.16l-54.272-63.488h183.296c19.456 0 35.84-18.944 36.352-43.008v-0.512c0.512-25.088-14.848-43.52-35.84-43.52z" fill="currentColor" p-id="38506"></path></svg>

File diff suppressed because it is too large
+ 0 - 0
src/assets/icons/svg/my-task.svg


File diff suppressed because it is too large
+ 0 - 0
src/assets/icons/svg/process-definition.svg


+ 29 - 0
src/assets/icons/svg/topiam.svg

@@ -0,0 +1,29 @@
+<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_446_540)">
+<path d="M113.069 160.072C103.717 170.743 93.0453 180.216 81.5345 188.609C61.5105 174.46 44.3642 156.595 30.9349 135.971C23.5009 124.46 17.2659 112.11 12.4697 99.0407C9.592 91.3668 7.19392 83.3332 5.27545 75.2996C2.03803 61.3907 0.359375 47.0022 0.359375 32.1341C0.359375 30.6953 0.359375 29.1365 0.359375 27.6977C6.35459 23.9806 12.7095 20.7432 19.0644 17.7456C20.7431 32.1341 24.1004 46.043 28.8966 59.3524C31.6544 66.9063 34.7719 74.3404 38.4889 81.4147C44.604 93.5251 52.0381 104.796 60.4314 115.228C75.1796 133.093 92.9254 148.321 113.069 160.072Z" fill="url(#paint0_linear_446_540)"/>
+<path d="M196.643 67.6256C195.084 76.3786 192.926 84.8918 190.168 93.1652C178.897 91.1269 167.266 90.0477 155.276 90.0477C154.197 90.0477 153.118 90.0477 152.039 90.0477C126.859 90.4074 102.878 95.6832 80.9352 105.036C72.302 94.8439 64.868 83.453 58.9927 71.3427C81.6546 61.8702 106.475 56.7144 132.614 56.7144C141.487 56.7144 150.24 57.3139 158.753 58.5129C171.823 60.1916 184.533 63.3091 196.643 67.6256Z" fill="url(#paint1_linear_446_540)"/>
+<path d="M199.64 34.0528C199.64 39.2087 199.401 44.3646 199.041 49.4005C186.691 44.1247 173.621 40.048 160.072 37.53C148.321 35.2518 136.211 34.0528 123.981 34.0528C97.7218 34.0528 72.6619 39.3286 49.88 48.9209C42.6858 51.9185 35.7313 55.3958 29.0167 59.2327C24.2205 46.0432 20.8632 32.0144 19.1846 17.6259C26.6186 14.1487 34.2925 11.271 42.2062 8.75301C60.3117 3.11751 79.4964 0 99.4005 0C119.904 0 139.568 3.23741 158.153 9.11272C172.782 13.789 186.691 20.2638 199.52 28.1775C199.64 30.2159 199.64 32.1343 199.64 34.0528Z" fill="url(#paint2_linear_446_540)"/>
+<path d="M190.168 93.2855C182.494 116.547 170.384 137.65 154.796 155.875C149.76 161.751 144.364 167.386 138.609 172.542C126.858 183.214 113.789 192.446 99.7601 200C93.4052 196.523 87.41 192.686 81.5347 188.609C93.0455 180.336 103.717 170.744 113.069 160.072C117.866 154.676 122.302 148.921 126.499 143.046C137.65 127.098 146.403 109.233 152.158 90.1679C153.237 90.1679 154.316 90.1679 155.396 90.1679C167.146 90.048 178.777 91.1272 190.168 93.2855Z" fill="url(#paint3_linear_446_540)"/>
+</g>
+<defs>
+<linearGradient id="paint0_linear_446_540" x1="15.8569" y1="27.5782" x2="86.4712" y2="182.06" gradientUnits="userSpaceOnUse">
+<stop stop-color="#57A4F7"/>
+<stop offset="1" stop-color="#2158F9"/>
+</linearGradient>
+<linearGradient id="paint1_linear_446_540" x1="58.9501" y1="80.8427" x2="196.648" y2="80.8427" gradientUnits="userSpaceOnUse">
+<stop stop-color="#2158F9"/>
+<stop offset="1" stop-color="#33E1E5"/>
+</linearGradient>
+<linearGradient id="paint2_linear_446_540" x1="19.1564" y1="29.6353" x2="199.647" y2="29.6353" gradientUnits="userSpaceOnUse">
+<stop stop-color="#255DF9"/>
+<stop offset="1" stop-color="#7C35BA"/>
+</linearGradient>
+<linearGradient id="paint3_linear_446_540" x1="95.3808" y1="192.567" x2="174.674" y2="97.4815" gradientUnits="userSpaceOnUse">
+<stop stop-color="#54A0F7"/>
+<stop offset="1" stop-color="#2158F9"/>
+</linearGradient>
+<clipPath id="clip0_446_540">
+<rect width="200" height="200" fill="white"/>
+</clipPath>
+</defs>
+</svg>

File diff suppressed because it is too large
+ 0 - 0
src/assets/icons/svg/waiting.svg


+ 1 - 0
src/assets/icons/svg/workflow.svg

@@ -0,0 +1 @@
+<svg t="1716004936483" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2712" width="200" height="200"><path d="M1024.99477 113.778v227.555a57.458 57.458 0 0 1-58.027 56.89H734.86277a57.458 57.458 0 0 1-58.027-56.89v-56.889H560.83877v455.112h115.996v-56.89a57.458 57.458 0 0 1 58.027-56.888h231.936a57.458 57.458 0 0 1 58.197 56.889v227.555a57.458 57.458 0 0 1-58.027 56.89H734.86277a57.458 57.458 0 0 1-58.027-56.89v-56.889H502.86877a57.458 57.458 0 0 1-58.027-56.889V568.89L274.51677 735.972a46.763 46.763 0 0 1-65.252 0l-195.754-192a44.658 44.658 0 0 1 0-64l195.754-192.057a46.763 46.763 0 0 1 65.252 0L445.01277 455.11V227.556a57.458 57.458 0 0 1 58.027-56.89h173.966v-56.888a57.458 57.458 0 0 1 58.026-56.89h231.936a57.458 57.458 0 0 1 58.027 56.89z" fill="currentColor" p-id="2713"></path></svg>

+ 34 - 1
src/assets/styles/element-ui.scss

@@ -1,4 +1,15 @@
-// cover some element-ui styles
+
+.el-collapse {
+  .collapse__title {
+    font-weight: 600;
+    padding: 0 8px;
+    font-size: 1.2em;
+    line-height: 1.1em;
+  }
+  .el-collapse-item__content {
+    padding: 0 8px;
+  }
+}
 
 .el-divider--horizontal {
   margin-bottom: 10px;
@@ -68,6 +79,12 @@
       .el-dialog__body {
         padding: 15px !important;
       }
+      .el-dialog__header {
+        padding: 16px 16px 8px 16px;
+        box-sizing: border-box;
+        border-bottom: 1px solid var(--brder-color);
+        margin-right: 0;
+      }
     }
   }
 }
@@ -114,3 +131,19 @@
 .el-dropdown .el-dropdown-link {
   color: var(--el-color-primary) !important;
 }
+
+/* 当 el-form 的 inline 属性为 true 时 */
+/* 设置 label 的宽度默认为 68px */
+.el-form--inline .el-form-item__label {
+  width: 68px;
+}
+
+/* 设置 el-select 的宽度默认为 240px */
+.el-form--inline .el-select {
+  width: 240px;
+}
+
+/* 设置 el-input 的宽度默认为 240px */
+.el-form--inline .el-input {
+  width: 240px;
+}

+ 8 - 1
src/assets/styles/index.scss

@@ -14,7 +14,14 @@ body {
   -moz-osx-font-smoothing: grayscale;
   -webkit-font-smoothing: antialiased;
   text-rendering: optimizeLegibility;
-  font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
+  font-family:
+    Helvetica Neue,
+    Helvetica,
+    PingFang SC,
+    Hiragino Sans GB,
+    Microsoft YaHei,
+    Arial,
+    sans-serif;
 }
 
 label {

+ 4 - 2
src/assets/styles/sidebar.scss

@@ -28,7 +28,10 @@
 
     // reset element-ui css
     .horizontal-collapse-transition {
-      transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
+      transition:
+        0s width ease-in-out,
+        0s padding-left ease-in-out,
+        0s padding-right ease-in-out;
     }
 
     .scrollbar-wrapper {
@@ -106,7 +109,6 @@
       }
     }
 
-
     & .theme-dark .nest-menu .el-sub-menu > .el-sub-menu__title,
     & .theme-dark .el-sub-menu .el-menu-item {
       background-color: $base-sub-menu-background !important;

+ 28 - 0
src/assets/styles/variables.module.scss

@@ -13,6 +13,14 @@
   --fixedHeaderBg: #ffffff;
   --tableHeaderBg: #f8f8f9;
   --tableHeaderTextColor: #515a6e;
+
+  // 工作流
+  --bpmn-panel-border: #eeeeee;
+  --bpmn-panel-box-shadow: #cccccc;
+  --bpmn-panel-bar-background-color: #f5f7fa;
+
+  // ele
+  --brder-color: #e8e8e8
 }
 html.dark {
   --menuBg: #1d1e1f;
@@ -33,6 +41,26 @@ html.dark {
   .el-tree-node__content {
     --el-color-primary-light-9: #262727;
   }
+  // vxe-table 主题
+  --vxe-font-color: #98989E;
+  --vxe-primary-color: #2C7ECF;
+  --vxe-icon-background-color: #98989E;
+  --vxe-table-font-color: #98989E;
+  --vxe-table-resizable-color: #95969a;
+  --vxe-table-header-background-color: #28282A;
+  --vxe-table-body-background-color: #151518;
+  --vxe-table-background-color: #4a5663;
+  --vxe-table-border-width: 1px;
+  --vxe-table-border-color: #37373A;
+  --vxe-toolbar-background-color: #37373A;
+
+  // 工作流
+  --bpmn-panel-border: #37373A;
+  --bpmn-panel-box-shadow: #37373A;
+  --bpmn-panel-bar-background-color: #37373A;
+
+  // ele
+  --brder-color: #37373A
 }
 
 // base color

+ 23 - 0
src/bpmn/assets/defaultXML.ts

@@ -0,0 +1,23 @@
+function generateRandomValue() {
+  // 生成一个随机数
+  const randomValue = Math.random().toString(36).slice(2, 12);
+  return `Process_${randomValue}`;
+}
+
+const cartage: string = 'default';
+export default `<?xml version="1.0" encoding="UTF-8"?>
+<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:bioc="http://bpmn.io/schema/bpmn/biocolor/1.0" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:flowable="http://flowable.org/bpmn" targetNamespace="http://www.flowable.org/processdef">
+  <process id="process_${generateRandomValue()}" name="name_${generateRandomValue()}">
+    <startEvent id="startNode1" name="开始" />
+  </process>
+  <bpmndi:BPMNDiagram id="BPMNDiagram_flow">
+    <bpmndi:BPMNPlane id="BPMNPlane_flow" bpmnElement="T-2d89e7a3-ba79-4abd-9f64-ea59621c258c">
+      <bpmndi:BPMNShape id="BPMNShape_startNode1" bpmnElement="startNode1" bioc:stroke="">
+        <omgdc:Bounds x="240" y="200" width="30" height="30" />
+        <bpmndi:BPMNLabel>
+          <omgdc:Bounds x="242" y="237" width="23" height="14" />
+        </bpmndi:BPMNLabel>
+      </bpmndi:BPMNShape>
+    </bpmndi:BPMNPlane>
+  </bpmndi:BPMNDiagram>
+</definitions>`;

+ 126 - 0
src/bpmn/assets/lang/zh.ts

@@ -0,0 +1,126 @@
+export const NodeName = {
+  'bpmn:Process': '流程',
+  'bpmn:StartEvent': '开始事件',
+  'bpmn:IntermediateThrowEvent': '中间事件',
+  'bpmn:Task': '任务',
+  'bpmn:SendTask': '发送任务',
+  'bpmn:ReceiveTask': '接收任务',
+  'bpmn:UserTask': '用户任务',
+  'bpmn:ManualTask': '手工任务',
+  'bpmn:BusinessRuleTask': '业务规则任务',
+  'bpmn:ServiceTask': '服务任务',
+  'bpmn:ScriptTask': '脚本任务',
+  'bpmn:EndEvent': '结束事件',
+  'bpmn:SequenceFlow': '流程线',
+  'bpmn:ExclusiveGateway': '互斥网关',
+  'bpmn:ParallelGateway': '并行网关',
+  'bpmn:InclusiveGateway': '相容网关',
+  'bpmn:ComplexGateway': '复杂网关',
+  'bpmn:EventBasedGateway': '事件网关',
+  'bpmn:Participant': '池/参与者',
+  'bpmn:SubProcess': '子流程',
+  'bpmn:DataObjectReference': '数据对象引用',
+  'bpmn:DataStoreReference': '数据存储引用',
+  'bpmn:Group': '组'
+};
+
+export default {
+  'Activate hand tool': '启动手动工具',
+  'Activate lasso tool': '启动 Lasso 工具',
+  'Activate create/remove space tool': '启动创建/删除空间工具',
+  'Activate global connect tool': '启动全局连接工具',
+  'Ad-hoc': 'Ad-hoc',
+  'Add lane above': '在上方添加泳道',
+  'Add lane below': '在下方添加泳道',
+  'Business rule task': '规则任务',
+  'Call activity': '引用流程',
+  'Compensation end event': '结束补偿事件',
+  'Compensation intermediate throw event': '中间补偿抛出事件',
+  'Complex gateway': '复杂网关',
+  'Conditional intermediate catch event': '中间条件捕获事件',
+  'Conditional start event (non-interrupting)': '条件启动事件 (非中断)',
+  'Conditional start event': '条件启动事件',
+  'Connect using association': '文本关联',
+  'Connect using sequence/message flow or association': '消息关联',
+  'Change element': '更改元素',
+  'Change type': '更改类型',
+  'Create data object reference': '创建数据对象引用',
+  'Create data store reference': '创建数据存储引用',
+  'Create expanded sub-process': '创建可折叠子流程',
+  'Create pool/participant': '创建池/参与者',
+  'Collection': '集合',
+  'Connect using data input association': '数据输入关联',
+  'Data store reference': '数据存储引用',
+  'Data object reference': '数据对象引用',
+  'Divide into two lanes': '分成两个泳道',
+  'Divide into three lanes': '分成三个泳道',
+  'End event': '结束事件',
+  'Error end event': '结束错误事件',
+  'Escalation end event': '结束升级事件',
+  'Escalation intermediate throw event': '中间升级抛出事件',
+  'Event sub-process': '事件子流程',
+  'Event-based gateway': '事件网关',
+  'Exclusive gateway': '互斥网关',
+  'Empty pool/participant (removes content)': '清空池/参与者 (删除内容)',
+  'Empty pool/participant': '清空池/参与者',
+  'Expanded pool/participant': '展开池/参与者',
+  'Inclusive gateway': '相容网关',
+  'Intermediate throw event': '中间抛出事件',
+  'Loop': '循环',
+  'Link intermediate catch event': '中间链接捕获事件',
+  'Link intermediate throw event': '中间链接抛出事件',
+  'Manual task': '手动任务',
+  'Message end event': '结束消息事件',
+  'Message intermediate catch event': '中间消息捕获事件',
+  'Message intermediate throw event': '中间消息抛出事件',
+  'Message start event': '消息启动事件',
+  'Parallel gateway': '并行网关',
+  'Parallel multi-instance': '并行多实例',
+  'Participant multiplicity': '参与者多重性',
+  'Receive task': '接受任务',
+  'Remove': '移除',
+  'Script task': '脚本任务',
+  'Send task': '发送任务',
+  'Sequential multi-instance': '串行多实例',
+  'Service task': '服务任务',
+  'Signal end event': '结束信号事件',
+  'Signal intermediate catch event': '中间信号捕获事件',
+  'Signal intermediate throw event': '中间信号抛出事件',
+  'Signal start event (non-interrupting)': '信号启动事件 (非中断)',
+  'Signal start event': '信号启动事件',
+  'Start event': '开始事件',
+  'Sub-process (collapsed)': '可折叠子流程',
+  'Sub-process (expanded)': '可展开子流程',
+  'Sub rocess': '子流程',
+  'Task': '任务',
+  'Transaction': '事务',
+  'Terminate end event': '终止边界事件',
+  'Timer intermediate catch event': '中间定时捕获事件',
+  'Timer start event (non-interrupting)': '定时启动事件 (非中断)',
+  'Timer start event': '定时启动事件',
+  'User task': '用户任务',
+  'Create start event': '创建开始事件',
+  'Create gateway': '创建网关',
+  'Create intermediate/boundary event': '创建中间/边界事件',
+  'Create end event': '创建结束事件',
+  'Create group': '创建组',
+  'Create startEvent': '开始节点',
+  'Create endEvent': '结束节点',
+  'Create exclusiveGateway': '互斥网关',
+  'Create parallelGateway': '并行网关',
+  'Create task': '任务节点',
+  'Create userTask': '用户任务节点',
+  'Condition type': '条件类型',
+  'Append end event': '追加结束事件节点',
+  'Append gateway': '追加网关节点',
+  'Append task': '追加任务',
+  'Append user task': '追加用户任务节点',
+  'Append text annotation': '追加文本注释',
+  'Append intermediate/boundary event': '追加中间或边界事件',
+  'Append receive task': '追加接收任务节点',
+  'Append message intermediate catch event': '追加中间消息捕获事件',
+  'Append timer intermediate catch event': '追加中间定时捕获事件',
+  'Append conditional intermediate catch event': '追加中间条件捕获事件',
+  'Append signal intermediate catch event': '追加中间信号捕获事件',
+  'flow elements must be children of pools/participants': '流程元素必须是池/参与者的子元素'
+};

+ 1250 - 0
src/bpmn/assets/moddle/flowable.ts

@@ -0,0 +1,1250 @@
+export default {
+  'name': 'Flowable',
+  'uri': 'http://flowable.org/bpmn',
+  'prefix': 'flowable',
+  'xml': {
+    'tagAlias': 'lowerCase'
+  },
+  'associations': [],
+  'types': [
+    {
+      'name': 'flowable:extCandidateUsers',
+      'isAbstract': true,
+      'extends': [],
+      'superClass': ['Element'],
+      'meta': {
+        'allowedIn': ['*']
+      },
+      'properties': [
+        {
+          'name': 'body',
+          'type': 'String',
+          'isBody': true
+        }
+      ]
+    },
+    {
+      'name': 'flowable:extAssignee',
+      'isAbstract': true,
+      'extends': [],
+      'superClass': ['Element'],
+      'meta': {
+        'allowedIn': ['*']
+      },
+      'properties': [
+        {
+          'name': 'body',
+          'type': 'String',
+          'isBody': true
+        }
+      ]
+    },
+    {
+      'name': 'flowable:property',
+      'superClass': ['Element'],
+      'properties': [
+        {
+          'name': 'id',
+          'isAttr': true,
+          'type': 'String'
+        },
+        {
+          'name': 'name',
+          'isAttr': true,
+          'type': 'String'
+        },
+        {
+          'name': 'value',
+          'isAttr': true,
+          'type': 'String'
+        }
+      ]
+    },
+    {
+      'name': 'flowable:properties',
+      'isAbstract': true,
+      'extends': [],
+      'superClass': ['Element'],
+      'meta': {
+        'allowedIn': ['*']
+      },
+      'properties': [
+        {
+          'name': 'values',
+          'type': 'flowable:property',
+          'isMany': true
+        }
+      ]
+    },
+    {
+      'name': 'InOutBinding',
+      'superClass': ['Element'],
+      'isAbstract': true,
+      'properties': [
+        {
+          'name': 'source',
+          'isAttr': true,
+          'type': 'String'
+        },
+        {
+          'name': 'sourceExpression',
+          'isAttr': true,
+          'type': 'String'
+        },
+        {
+          'name': 'target',
+          'isAttr': true,
+          'type': 'String'
+        },
+        {
+          'name': 'businessKey',
+          'isAttr': true,
+          'type': 'String'
+        },
+        {
+          'name': 'local',
+          'isAttr': true,
+          'type': 'Boolean',
+          'default': false
+        },
+        {
+          'name': 'variables',
+          'isAttr': true,
+          'type': 'String'
+        }
+      ]
+    },
+    {
+      'name': 'In',
+      'superClass': ['InOutBinding'],
+      'meta': {
+        'allowedIn': ['bpmn:CallActivity']
+      }
+    },
+    {
+      'name': 'Out',
+      'superClass': ['InOutBinding'],
+      'meta': {
+        'allowedIn': ['bpmn:CallActivity']
+      }
+    },
+    {
+      'name': 'AsyncCapable',
+      'isAbstract': true,
+      'extends': ['bpmn:Activity', 'bpmn:Gateway', 'bpmn:Event'],
+      'properties': [
+        {
+          'name': 'async',
+          'isAttr': true,
+          'type': 'Boolean',
+          'default': false
+        },
+        {
+          'name': 'asyncBefore',
+          'isAttr': true,
+          'type': 'Boolean',
+          'default': false
+        },
+        {
+          'name': 'asyncAfter',
+          'isAttr': true,
+          'type': 'Boolean',
+          'default': false
+        },
+        {
+          'name': 'exclusive',
+          'isAttr': true,
+          'type': 'Boolean',
+          'default': true
+        }
+      ]
+    },
+    {
+      'name': 'flowable:in',
+      'superClass': ['Element'],
+      'properties': [
+        {
+          'name': 'source',
+          'type': 'string',
+          'isAttr': true
+        },
+        {
+          'name': 'target',
+          'type': 'string',
+          'isAttr': true
+        }
+      ]
+    },
+    {
+      'name': 'flowable:out',
+      'superClass': ['Element'],
+      'properties': [
+        {
+          'name': 'source',
+          'type': 'string',
+          'isAttr': true
+        },
+        {
+          'name': 'target',
+          'type': 'string',
+          'isAttr': true
+        }
+      ]
+    },
+    {
+      'name': 'BoundaryEvent',
+      'superClass': ['CatchEvent'],
+      'properties': [
+        {
+          'name': 'cancelActivity',
+          'default': true,
+          'isAttr': true,
+          'type': 'Boolean'
+        },
+        {
+          'name': 'attachedToRef',
+          'type': 'Activity',
+          'isAttr': true,
+          'isReference': true
+        }
+      ]
+    },
+    {
+      'name': 'JobPriorized',
+      'isAbstract': true,
+      'extends': ['bpmn:Process', 'flowable:AsyncCapable'],
+      'properties': [
+        {
+          'name': 'jobPriority',
+          'isAttr': true,
+          'type': 'String'
+        }
+      ]
+    },
+    {
+      'name': 'SignalEventDefinition',
+      'isAbstract': true,
+      'extends': ['bpmn:SignalEventDefinition'],
+      'properties': [
+        {
+          'name': 'async',
+          'isAttr': true,
+          'type': 'Boolean',
+          'default': false
+        }
+      ]
+    },
+    {
+      'name': 'ErrorEventDefinition',
+      'isAbstract': true,
+      'extends': ['bpmn:ErrorEventDefinition'],
+      'properties': [
+        {
+          'name': 'errorCodeVariable',
+          'isAttr': true,
+          'type': 'String'
+        },
+        {
+          'name': 'errorMessageVariable',
+          'isAttr': true,
+          'type': 'String'
+        }
+      ]
+    },
+    {
+      'name': 'Error',
+      'isAbstract': true,
+      'extends': ['bpmn:Error'],
+      'properties': [
+        {
+          'name': 'flowable:errorMessage',
+          'isAttr': true,
+          'type': 'String'
+        }
+      ]
+    },
+    {
+      'name': 'PotentialStarter',
+      'superClass': ['Element'],
+      'properties': [
+        {
+          'name': 'resourceAssignmentExpression',
+          'type': 'bpmn:ResourceAssignmentExpression'
+        }
+      ]
+    },
+    {
+      'name': 'UserTask',
+      'isAbstract': true,
+      'extends': ['bpmn:UserTask'],
+      'properties': [
+        {
+          'name': 'timerEventDefinition',
+          'type': 'Expression'
+        },
+        {
+          'name': 'multiInstanceLoopCharacteristics',
+          'type': 'MultiInstanceLoopCharacteristics'
+        }
+      ]
+    },
+    {
+      'name': 'StartEvent',
+      'isAbstract': true,
+      'extends': ['bpmn:StartEvent'],
+      'properties': [
+        {
+          'name': 'timerEventDefinition',
+          'type': 'Expression'
+        }
+      ]
+    },
+    {
+      'name': 'FormSupported',
+      'isAbstract': true,
+      'extends': ['bpmn:StartEvent', 'bpmn:UserTask'],
+      'properties': [
+        {
+          'name': 'formHandlerClass',
+          'isAttr': true,
+          'type': 'String'
+        },
+        {
+          'name': 'formKey',
+          'isAttr': true,
+          'type': 'String'
+        }
+      ]
+    },
+    {
+      'name': 'TemplateSupported',
+      'isAbstract': true,
+      'extends': ['bpmn:Process', 'bpmn:FlowElement'],
+      'properties': [
+        {
+          'name': 'modelerTemplate',
+          'isAttr': true,
+          'type': 'String'
+        }
+      ]
+    },
+    {
+      'name': 'Initiator',
+      'isAbstract': true,
+      'extends': ['bpmn:StartEvent'],
+      'properties': [
+        {
+          'name': 'initiator',
+          'isAttr': true,
+          'type': 'String'
+        }
+      ]
+    },
+    {
+      'name': 'ScriptTask',
+      'isAbstract': true,
+      'extends': ['bpmn:ScriptTask'],
+      'properties': [
+        {
+          'name': 'resultVariable',
+          'isAttr': true,
+          'type': 'String'
+        },
+        {
+          'name': 'resource',
+          'isAttr': true,
+          'type': 'String'
+        }
+      ]
+    },
+    {
+      'name': 'Process',
+      'isAbstract': true,
+      'extends': ['bpmn:Process'],
+      'properties': [
+        {
+          'name': 'candidateStarterGroups',
+          'isAttr': true,
+          'type': 'String'
+        },
+        {
+          'name': 'candidateStarterUsers',
+          'isAttr': true,
+          'type': 'String'
+        },
+        {
+          'name': 'versionTag',
+          'isAttr': true,
+          'type': 'String'
+        },
+        {
+          'name': 'historyTimeToLive',
+          'isAttr': true,
+          'type': 'String'
+        },
+        {
+          'name': 'isStartableInTasklist',
+          'isAttr': true,
+          'type': 'Boolean',
+          'default': true
+        }
+      ]
+    },
+    {
+      'name': 'EscalationEventDefinition',
+      'isAbstract': true,
+      'extends': ['bpmn:EscalationEventDefinition'],
+      'properties': [
+        {
+          'name': 'escalationCodeVariable',
+          'isAttr': true,
+          'type': 'String'
+        }
+      ]
+    },
+    {
+      'name': 'FormalExpression',
+      'isAbstract': true,
+      'extends': ['bpmn:FormalExpression'],
+      'properties': [
+        {
+          'name': 'resource',
+          'isAttr': true,
+          'type': 'String'
+        }
+      ]
+    },
+    {
+      'name': 'Assignable',
+      'extends': ['bpmn:UserTask'],
+      'properties': [
+        {
+          'name': 'candidateGroups',
+          'isAttr': true,
+          'type': 'String'
+        },
+        {
+          'name': 'dueDate',
+          'isAttr': true,
+          'type': 'String'
+        },
+        {
+          'name': 'followUpDate',
+          'isAttr': true,
+          'type': 'String'
+        },
+        {
+          'name': 'priority',
+          'isAttr': true,
+          'type': 'String'
+        }
+      ]
+    },
+    {
+      'name': 'CallActivity',
+      'extends': ['bpmn:CallActivity'],
+      'properties': [
+        {
+          'name': 'calledElementBinding',
+          'isAttr': true,
+          'type': 'String',
+          'default': 'latest'
+        },
+        {
+          'name': 'calledElementVersion',
+          'isAttr': true,
+          'type': 'String'
+        },
+        {
+          'name': 'calledElementVersionTag',
+          'isAttr': true,
+          'type': 'String'
+        },
+        {
+          'name': 'calledElementTenantId',
+          'isAttr': true,
+          'type': 'String'
+        },
+        {
+          'name': 'caseRef',
+          'isAttr': true,
+          'type': 'String'
+        },
+        {
+          'name': 'caseBinding',
+          'isAttr': true,
+          'type': 'String',
+          'default': 'latest'
+        },
+        {
+          'name': 'caseVersion',
+          'isAttr': true,
+          'type': 'String'
+        },
+        {
+          'name': 'caseTenantId',
+          'isAttr': true,
+          'type': 'String'
+        },
+        {
+          'name': 'variableMappingClass',
+          'isAttr': true,
+          'type': 'String'
+        },
+        {
+          'name': 'variableMappingDelegateExpression',
+          'isAttr': true,
+          'type': 'String'
+        }
+      ]
+    },
+    {
+      'name': 'ServiceTaskLike',
+      'extends': ['bpmn:ServiceTask', 'bpmn:BusinessRuleTask', 'bpmn:SendTask', 'bpmn:MessageEventDefinition'],
+      'properties': [
+        {
+          'name': 'expression',
+          'isAttr': true,
+          'type': 'String'
+        },
+        {
+          'name': 'class',
+          'isAttr': true,
+          'type': 'String'
+        },
+        {
+          'name': 'delegateExpression',
+          'isAttr': true,
+          'type': 'String'
+        },
+        {
+          'name': 'resultVariable',
+          'isAttr': true,
+          'type': 'String'
+        }
+      ]
+    },
+    {
+      'name': 'ExclusiveGateway',
+      'isAbstract': true,
+      'extends': ['bpmn:ExclusiveGateway'],
+      'properties': [
+        {
+          'name': 'serviceClass',
+          'isAttr': true,
+          'type': 'String'
+        }
+      ]
+    },
+    {
+      'name': 'DmnCapable',
+      'extends': ['bpmn:BusinessRuleTask'],
+      'properties': [
+        {
+          'name': 'decisionRef',
+          'isAttr': true,
+          'type': 'String'
+        },
+        {
+          'name': 'decisionRefBinding',
+          'isAttr': true,
+          'type': 'String',
+          'default': 'latest'
+        },
+        {
+          'name': 'decisionRefVersion',
+          'isAttr': true,
+          'type': 'String'
+        },
+        {
+          'name': 'mapDecisionResult',
+          'isAttr': true,
+          'type': 'String',
+          'default': 'resultList'
+        },
+        {
+          'name': 'decisionRefTenantId',
+          'isAttr': true,
+          'type': 'String'
+        }
+      ]
+    },
+    {
+      'name': 'ExternalCapable',
+      'extends': ['flowable:ServiceTaskLike'],
+      'properties': [
+        {
+          'name': 'type',
+          'isAttr': true,
+          'type': 'String'
+        },
+        {
+          'name': 'topic',
+          'isAttr': true,
+          'type': 'String'
+        }
+      ]
+    },
+    {
+      'name': 'TaskPriorized',
+      'extends': ['bpmn:Process', 'flowable:ExternalCapable'],
+      'properties': [
+        {
+          'name': 'taskPriority',
+          'isAttr': true,
+          'type': 'String'
+        }
+      ]
+    },
+    {
+      'name': 'Properties',
+      'superClass': ['Element'],
+      'meta': {
+        'allowedIn': ['*']
+      },
+      'properties': [
+        {
+          'name': 'values',
+          'type': 'Property',
+          'isMany': true
+        }
+      ]
+    },
+    {
+      'name': 'Property',
+      'superClass': ['Element'],
+      'properties': [
+        {
+          'name': 'id',
+          'type': 'String',
+          'isAttr': true
+        },
+        {
+          'name': 'name',
+          'type': 'String',
+          'isAttr': true
+        },
+        {
+          'name': 'value',
+          'type': 'String',
+          'isAttr': true
+        }
+      ]
+    },
+    {
+      'name': 'Connector',
+      'superClass': ['Element'],
+      'meta': {
+        'allowedIn': ['flowable:ServiceTaskLike']
+      },
+      'properties': [
+        {
+          'name': 'inputOutput',
+          'type': 'InputOutput'
+        },
+        {
+          'name': 'connectorId',
+          'type': 'String'
+        }
+      ]
+    },
+    {
+      'name': 'InputOutput',
+      'superClass': ['Element'],
+      'meta': {
+        'allowedIn': ['bpmn:FlowNode', 'flowable:Connector']
+      },
+      'properties': [
+        {
+          'name': 'inputOutput',
+          'type': 'InputOutput'
+        },
+        {
+          'name': 'connectorId',
+          'type': 'String'
+        },
+        {
+          'name': 'inputParameters',
+          'isMany': true,
+          'type': 'InputParameter'
+        },
+        {
+          'name': 'outputParameters',
+          'isMany': true,
+          'type': 'OutputParameter'
+        }
+      ]
+    },
+    {
+      'name': 'InputOutputParameter',
+      'properties': [
+        {
+          'name': 'name',
+          'isAttr': true,
+          'type': 'String'
+        },
+        {
+          'name': 'value',
+          'isBody': true,
+          'type': 'String'
+        },
+        {
+          'name': 'definition',
+          'type': 'InputOutputParameterDefinition'
+        }
+      ]
+    },
+    {
+      'name': 'InputOutputParameterDefinition',
+      'isAbstract': true
+    },
+    {
+      'name': 'List',
+      'superClass': ['InputOutputParameterDefinition'],
+      'properties': [
+        {
+          'name': 'items',
+          'isMany': true,
+          'type': 'InputOutputParameterDefinition'
+        }
+      ]
+    },
+    {
+      'name': 'Map',
+      'superClass': ['InputOutputParameterDefinition'],
+      'properties': [
+        {
+          'name': 'entries',
+          'isMany': true,
+          'type': 'Entry'
+        }
+      ]
+    },
+    {
+      'name': 'Entry',
+      'properties': [
+        {
+          'name': 'key',
+          'isAttr': true,
+          'type': 'String'
+        },
+        {
+          'name': 'value',
+          'isBody': true,
+          'type': 'String'
+        },
+        {
+          'name': 'definition',
+          'type': 'InputOutputParameterDefinition'
+        }
+      ]
+    },
+    {
+      'name': 'Value',
+      'superClass': ['InputOutputParameterDefinition'],
+      'properties': [
+        {
+          'name': 'id',
+          'isAttr': true,
+          'type': 'String'
+        },
+        {
+          'name': 'name',
+          'isAttr': true,
+          'type': 'String'
+        },
+        {
+          'name': 'value',
+          'isBody': true,
+          'type': 'String'
+        }
+      ]
+    },
+    {
+      'name': 'Script',
+      'superClass': ['InputOutputParameterDefinition'],
+      'properties': [
+        {
+          'name': 'scriptFormat',
+          'isAttr': true,
+          'type': 'String'
+        },
+        {
+          'name': 'resource',
+          'isAttr': true,
+          'type': 'String'
+        },
+        {
+          'name': 'value',
+          'isBody': true,
+          'type': 'String'
+        }
+      ]
+    },
+    {
+      'name': 'Field',
+      'superClass': ['Element'],
+      'meta': {
+        'allowedIn': ['flowable:ServiceTaskLike', 'flowable:ExecutionListener', 'flowable:TaskListener']
+      },
+      'properties': [
+        {
+          'name': 'name',
+          'isAttr': true,
+          'type': 'String'
+        },
+        {
+          'name': 'expression',
+          'isAttr': true,
+          'type': 'expression'
+        },
+        {
+          'name': 'string',
+          'type': 'string'
+        },
+        {
+          'name': 'stringValue',
+          'isAttr': true,
+          'type': 'String'
+        }
+      ]
+    },
+    {
+      'name': 'string',
+      'superClass': ['Element'],
+      'meta': {
+        'allowedIn': ['flowable:Field']
+      },
+      'properties': [
+        {
+          'name': 'body',
+          'isBody': true,
+          'type': 'String'
+        }
+      ]
+    },
+    {
+      'name': 'expression',
+      'superClass': ['Element'],
+      'meta': {
+        'allowedIn': ['flowable:Field']
+      },
+      'properties': [
+        {
+          'name': 'body',
+          'isBody': true,
+          'type': 'String'
+        }
+      ]
+    },
+    {
+      'name': 'InputParameter',
+      'superClass': ['InputOutputParameter']
+    },
+    {
+      'name': 'OutputParameter',
+      'superClass': ['InputOutputParameter']
+    },
+    {
+      'name': 'Collectable',
+      'isAbstract': true,
+      'extends': ['bpmn:MultiInstanceLoopCharacteristics'],
+      'superClass': ['flowable:AsyncCapable'],
+      'properties': [
+        {
+          'name': 'collection',
+          'isAttr': true,
+          'type': 'String'
+        },
+        {
+          'name': 'elementVariable',
+          'isAttr': true,
+          'type': 'String'
+        }
+      ]
+    },
+    {
+      'name': 'SequenceFlow',
+      'superClass': ['FlowElement'],
+      'properties': [
+        {
+          'name': 'isImmediate',
+          'isAttr': true,
+          'type': 'Boolean'
+        },
+        {
+          'name': 'conditionExpression',
+          'type': 'Expression'
+        },
+        {
+          'name': 'sourceRef',
+          'type': 'FlowNode',
+          'isAttr': true,
+          'isReference': true
+        },
+        {
+          'name': 'targetRef',
+          'type': 'FlowNode',
+          'isAttr': true,
+          'isReference': true
+        }
+      ]
+    },
+    {
+      'name': 'MultiInstanceLoopCharacteristics',
+      'superClass': ['LoopCharacteristics'],
+      'properties': [
+        {
+          'name': 'isSequential',
+          'default': false,
+          'isAttr': true,
+          'type': 'Boolean'
+        },
+        {
+          'name': 'behavior',
+          'type': 'MultiInstanceBehavior',
+          'default': 'All',
+          'isAttr': true
+        },
+        {
+          'name': 'loopCardinality',
+          'type': 'Expression',
+          'xml': {
+            'serialize': 'xsi:type'
+          }
+        },
+        {
+          'name': 'loopDataInputRef',
+          'type': 'ItemAwareElement',
+          'isReference': true
+        },
+        {
+          'name': 'loopDataOutputRef',
+          'type': 'ItemAwareElement',
+          'isReference': true
+        },
+        {
+          'name': 'inputDataItem',
+          'type': 'DataInput',
+          'xml': {
+            'serialize': 'property'
+          }
+        },
+        {
+          'name': 'outputDataItem',
+          'type': 'DataOutput',
+          'xml': {
+            'serialize': 'property'
+          }
+        },
+        {
+          'name': 'complexBehaviorDefinition',
+          'type': 'ComplexBehaviorDefinition',
+          'isMany': true
+        },
+        {
+          'name': 'completionCondition',
+          'type': 'Expression',
+          'xml': {
+            'serialize': 'xsi:type'
+          }
+        },
+        {
+          'name': 'oneBehaviorEventRef',
+          'type': 'EventDefinition',
+          'isAttr': true,
+          'isReference': true
+        },
+        {
+          'name': 'noneBehaviorEventRef',
+          'type': 'EventDefinition',
+          'isAttr': true,
+          'isReference': true
+        }
+      ]
+    },
+    {
+      'name': 'FailedJobRetryTimeCycle',
+      'superClass': ['Element'],
+      'meta': {
+        'allowedIn': ['flowable:AsyncCapable', 'bpmn:MultiInstanceLoopCharacteristics']
+      },
+      'properties': [
+        {
+          'name': 'body',
+          'isBody': true,
+          'type': 'String'
+        }
+      ]
+    },
+    {
+      'name': 'ExecutionListener',
+      'superClass': ['Element'],
+      'meta': {
+        'allowedIn': [
+          'bpmn:Task',
+          'bpmn:ServiceTask',
+          'bpmn:UserTask',
+          'bpmn:BusinessRuleTask',
+          'bpmn:ScriptTask',
+          'bpmn:ReceiveTask',
+          'bpmn:ManualTask',
+          'bpmn:ExclusiveGateway',
+          'bpmn:SequenceFlow',
+          'bpmn:ParallelGateway',
+          'bpmn:InclusiveGateway',
+          'bpmn:EventBasedGateway',
+          'bpmn:StartEvent',
+          'bpmn:IntermediateCatchEvent',
+          'bpmn:IntermediateThrowEvent',
+          'bpmn:EndEvent',
+          'bpmn:BoundaryEvent',
+          'bpmn:CallActivity',
+          'bpmn:SubProcess',
+          'bpmn:Process'
+        ]
+      },
+      'properties': [
+        {
+          'name': 'expression',
+          'isAttr': true,
+          'type': 'String'
+        },
+        {
+          'name': 'class',
+          'isAttr': true,
+          'type': 'String'
+        },
+        {
+          'name': 'delegateExpression',
+          'isAttr': true,
+          'type': 'String'
+        },
+        {
+          'name': 'event',
+          'isAttr': true,
+          'type': 'String'
+        },
+        {
+          'name': 'script',
+          'type': 'Script'
+        },
+        {
+          'name': 'fields',
+          'type': 'Field',
+          'isMany': true
+        }
+      ]
+    },
+    {
+      'name': 'TaskListener',
+      'superClass': ['Element'],
+      'meta': {
+        'allowedIn': ['bpmn:UserTask']
+      },
+      'properties': [
+        {
+          'name': 'expression',
+          'isAttr': true,
+          'type': 'String'
+        },
+        {
+          'name': 'class',
+          'isAttr': true,
+          'type': 'String'
+        },
+        {
+          'name': 'delegateExpression',
+          'isAttr': true,
+          'type': 'String'
+        },
+        {
+          'name': 'event',
+          'isAttr': true,
+          'type': 'String'
+        },
+        {
+          'name': 'script',
+          'type': 'Script'
+        },
+        {
+          'name': 'fields',
+          'type': 'Field',
+          'isMany': true
+        }
+      ]
+    },
+    {
+      'name': 'FormProperty',
+      'superClass': ['Element'],
+      'meta': {
+        'allowedIn': ['bpmn:StartEvent', 'bpmn:UserTask']
+      },
+      'properties': [
+        {
+          'name': 'id',
+          'type': 'String',
+          'isAttr': true
+        },
+        {
+          'name': 'name',
+          'type': 'String',
+          'isAttr': true
+        },
+        {
+          'name': 'type',
+          'type': 'String',
+          'isAttr': true
+        },
+        {
+          'name': 'required',
+          'type': 'String',
+          'isAttr': true
+        },
+        {
+          'name': 'readable',
+          'type': 'String',
+          'isAttr': true
+        },
+        {
+          'name': 'writable',
+          'type': 'String',
+          'isAttr': true
+        },
+        {
+          'name': 'variable',
+          'type': 'String',
+          'isAttr': true
+        },
+        {
+          'name': 'expression',
+          'type': 'String',
+          'isAttr': true
+        },
+        {
+          'name': 'datePattern',
+          'type': 'String',
+          'isAttr': true
+        },
+        {
+          'name': 'default',
+          'type': 'String',
+          'isAttr': true
+        },
+        {
+          'name': 'values',
+          'type': 'Value',
+          'isMany': true
+        }
+      ]
+    },
+    {
+      'name': 'FormData',
+      'superClass': ['Element'],
+      'meta': {
+        'allowedIn': ['bpmn:StartEvent', 'bpmn:UserTask']
+      },
+      'properties': [
+        {
+          'name': 'fields',
+          'type': 'FormField',
+          'isMany': true
+        },
+        {
+          'name': 'businessKey',
+          'type': 'String',
+          'isAttr': true
+        }
+      ]
+    },
+    {
+      'name': 'FormField',
+      'superClass': ['Element'],
+      'properties': [
+        {
+          'name': 'id',
+          'type': 'String',
+          'isAttr': true
+        },
+        {
+          'name': 'label',
+          'type': 'String',
+          'isAttr': true
+        },
+        {
+          'name': 'type',
+          'type': 'String',
+          'isAttr': true
+        },
+        {
+          'name': 'datePattern',
+          'type': 'String',
+          'isAttr': true
+        },
+        {
+          'name': 'defaultValue',
+          'type': 'String',
+          'isAttr': true
+        },
+        {
+          'name': 'properties',
+          'type': 'Properties'
+        },
+        {
+          'name': 'validation',
+          'type': 'Validation'
+        },
+        {
+          'name': 'values',
+          'type': 'Value',
+          'isMany': true
+        }
+      ]
+    },
+    {
+      'name': 'Validation',
+      'superClass': ['Element'],
+      'properties': [
+        {
+          'name': 'constraints',
+          'type': 'Constraint',
+          'isMany': true
+        }
+      ]
+    },
+    {
+      'name': 'Constraint',
+      'superClass': ['Element'],
+      'properties': [
+        {
+          'name': 'name',
+          'type': 'String',
+          'isAttr': true
+        },
+        {
+          'name': 'config',
+          'type': 'String',
+          'isAttr': true
+        }
+      ]
+    },
+    {
+      'name': 'ConditionalEventDefinition',
+      'isAbstract': true,
+      'extends': ['bpmn:ConditionalEventDefinition'],
+      'properties': [
+        {
+          'name': 'variableName',
+          'isAttr': true,
+          'type': 'String'
+        },
+        {
+          'name': 'variableEvent',
+          'isAttr': true,
+          'type': 'String'
+        }
+      ]
+    }
+  ],
+  'emumerations': []
+};

+ 138 - 0
src/bpmn/assets/module/ContextPad/CustomContextPadProvider.ts

@@ -0,0 +1,138 @@
+import ContextPadProvider from 'bpmn-js/lib/features/context-pad/ContextPadProvider';
+import { Injector } from 'didi';
+import EventBus from 'diagram-js/lib/core/EventBus';
+import ContextPad from 'diagram-js/lib/features/context-pad/ContextPad';
+import Modeling from 'bpmn-js/lib/features/modeling/Modeling.js';
+import ElementFactory from 'bpmn-js/lib/features/modeling/ElementFactory';
+import Connect from 'diagram-js/lib/features/connect/Connect';
+import Create from 'diagram-js/lib/features/create/Create';
+import PopupMenu from 'diagram-js/lib/features/popup-menu/PopupMenu';
+import Canvas from 'diagram-js/lib/core/Canvas';
+import Rules from 'diagram-js/lib/features/rules/Rules';
+import { Element, Shape } from 'diagram-js/lib/model/Types';
+import BpmnFactory from 'bpmn-js/lib/features/modeling/BpmnFactory';
+import modeler from '@/store/modules/modeler';
+
+// @Description: 增强元素连线事件
+
+class CustomContextPadProvider extends ContextPadProvider {
+  private _contextPad: ContextPad;
+  private _modeling: Modeling;
+  private _elementFactory: ElementFactory;
+  private _autoPlace: any;
+  private _connect: Connect;
+  private _create: Create;
+  private _popupMenu: PopupMenu;
+  private _canvas: Canvas;
+  private _rules: Rules;
+
+  constructor(
+    config: any,
+    injector: Injector,
+    eventBus: EventBus,
+    contextPad: ContextPad,
+    modeling: Modeling,
+    elementFactory: ElementFactory,
+    connect: Connect,
+    create: Create,
+    popupMenu: PopupMenu,
+    canvas: Canvas,
+    rules: Rules,
+    translate
+  ) {
+    // @ts-ignore
+    super(config, injector, eventBus, contextPad, modeling, elementFactory, connect, create, popupMenu, canvas, rules, translate);
+
+    this._contextPad = contextPad;
+    this._modeling = modeling;
+    this._elementFactory = elementFactory;
+    this._connect = connect;
+    this._create = create;
+    this._popupMenu = popupMenu;
+    this._canvas = canvas;
+    this._rules = rules;
+
+    this._autoPlace = injector.get('autoPlace', false);
+  }
+
+  getContextPadEntries(element: Element) {
+    const actions: Record<string, any> = {};
+
+    const appendUserTask = (event: Event, element: Shape) => {
+      const shape = this._elementFactory.createShape({ type: 'bpmn:UserTask' });
+      this._create.start(event, shape, {
+        source: element
+      });
+    };
+
+    const appendMultiInstanceUserTask = (event: Event, element: Shape) => {
+      const store = modeler();
+      const bpmnFactory = store.getModeler().get('bpmnFactory') as BpmnFactory;
+      const businessObject = bpmnFactory.create('bpmn:UserTask', {
+        // name: '多实例用户任务',
+        isForCompensation: false
+      });
+      businessObject.loopCharacteristics = bpmnFactory.create('bpmn:MultiInstanceLoopCharacteristics');
+      // 创建 Shape
+      const shape = this._elementFactory.createShape({
+        type: 'bpmn:UserTask',
+        businessObject: businessObject
+      });
+      this._create.start(event, shape, { source: element });
+    };
+
+    const appendTask = this._autoPlace
+      ? (event, element) => {
+          const bpmnFactory: BpmnFactory | undefined = modeler().getModeler().get('bpmnFactory');
+          const businessObject = bpmnFactory.create('bpmn:UserTask', {
+            // name: '多实例用户任务',// 右键创建显示
+            isForCompensation: false
+          });
+
+          // 创建多实例属性并分配给用户任务的 loopCharacteristics
+          businessObject.loopCharacteristics = bpmnFactory.create('bpmn:MultiInstanceLoopCharacteristics');
+
+          // 创建 Shape
+          const shape = this._elementFactory.createShape({
+            type: 'bpmn:UserTask',
+            businessObject: businessObject
+          });
+
+          this._autoPlace.append(element, shape);
+        }
+      : appendMultiInstanceUserTask;
+
+    const append = this._autoPlace
+      ? (event: Event, element: Shape) => {
+          const shape = this._elementFactory.createShape({ type: 'bpmn:UserTask' });
+          this._autoPlace.append(element, shape);
+        }
+      : appendUserTask;
+
+    // // 添加创建用户任务按钮
+    actions['append.append-user-task'] = {
+      group: 'model',
+      className: 'bpmn-icon-user-task',
+      title: '用户任务',
+      action: {
+        dragstart: appendUserTask,
+        click: append
+      }
+    };
+
+    // 添加创建多实例用户任务按钮
+    actions['append.append-multi-instance-user-task'] = {
+      group: 'model',
+      className: 'bpmn-icon-user', // 你可以使用多实例用户任务的图标  bpmn-icon-user   bpmn-icon-user-task
+      title: '多实例用户任务',
+      action: {
+        dragstart: appendMultiInstanceUserTask,
+        click: appendTask
+      }
+    };
+
+    return actions;
+  }
+}
+
+export default CustomContextPadProvider;

+ 109 - 0
src/bpmn/assets/module/Palette/CustomPaletteProvider.ts

@@ -0,0 +1,109 @@
+import { assign } from 'min-dash';
+import PaletteProvider from 'bpmn-js/lib/features/palette/PaletteProvider';
+import ElementFactory from 'bpmn-js/lib/features/modeling/ElementFactory';
+import Create from 'diagram-js/lib/features/create/Create';
+import SpaceTool from 'diagram-js/lib/features/space-tool/SpaceTool';
+import LassoTool from 'diagram-js/lib/features/lasso-tool/LassoTool';
+import HandTool from 'diagram-js/lib/features/hand-tool/HandTool';
+import GlobalConnect from 'diagram-js/lib/features/global-connect/GlobalConnect';
+import Palette from 'diagram-js/lib/features/palette/Palette';
+import modeler from '@/store/modules/modeler';
+import BpmnFactory from 'bpmn-js/lib/features/modeling/BpmnFactory';
+
+// @Description: 增强左侧面板
+class CustomPaletteProvider extends PaletteProvider {
+  private readonly _palette: Palette;
+  private readonly _create: Create;
+  private readonly _elementFactory: ElementFactory;
+  private readonly _spaceTool: SpaceTool;
+  private readonly _lassoTool: LassoTool;
+  private readonly _handTool: HandTool;
+  private readonly _globalConnect: GlobalConnect;
+  private readonly _translate: any;
+
+  constructor(palette, create, elementFactory, spaceTool, lassoTool, handTool, globalConnect, translate) {
+    super(palette, create, elementFactory, spaceTool, lassoTool, handTool, globalConnect, translate);
+    this._palette = palette;
+    this._create = create;
+    this._elementFactory = elementFactory;
+    this._spaceTool = spaceTool;
+    this._lassoTool = lassoTool;
+    this._handTool = handTool;
+    this._globalConnect = globalConnect;
+    this._translate = translate;
+  }
+
+  getPaletteEntries() {
+    const actions = {},
+      create = this._create,
+      elementFactory = this._elementFactory,
+      translate = this._translate;
+
+    function createAction(type: string, group: string, className: string, title: string, options?: object) {
+      function createListener(event) {
+        const shape = elementFactory.createShape(assign({ type: type }, options));
+        if (options) {
+          !shape.businessObject.di && (shape.businessObject.di = {});
+          shape.businessObject.di.isExpanded = (options as { [key: string]: any }).isExpanded;
+        }
+        create.start(event, shape, null);
+      }
+      const shortType = type.replace(/^bpmn:/, '');
+      return {
+        group: group,
+        className: className,
+        title: title || translate('Create {type}', { type: shortType }),
+        action: {
+          dragstart: createListener,
+          click: createListener
+        }
+      };
+    }
+
+    function createMultiInstanceUserTask(event) {
+      const bpmnFactory: BpmnFactory | undefined = modeler().getBpmnFactory();
+      // 创建一个 bpmn:UserTask
+      const userTask = bpmnFactory.create('bpmn:UserTask', {
+        // name: '多实例用户任务', // 在画板中显示字段
+        isForCompensation: false
+      });
+      // 将多实例属性分配给 bpmn:UserTask 的 loopCharacteristics
+      userTask.loopCharacteristics = bpmnFactory.create('bpmn:MultiInstanceLoopCharacteristics');
+      const customUserTask = elementFactory.createShape({
+        type: 'bpmn:UserTask',
+        businessObject: userTask // 分配创建的 userTask 到 businessObject
+      });
+      create.start(event, customUserTask, {});
+    }
+
+    assign(actions, {
+      'create.parallel-gateway': createAction('bpmn:ParallelGateway', 'gateway', 'bpmn-icon-gateway-parallel', '并行网关'),
+      'create.event-base-gateway': createAction('bpmn:EventBasedGateway', 'gateway', 'bpmn-icon-gateway-eventbased', '事件网关'),
+      // 分组线
+      'gateway-separator': {
+        group: 'gateway',
+        separator: true
+      },
+      'create.user-task': createAction('bpmn:UserTask', 'activity', 'bpmn-icon-user-task', '创建用户任务'),
+      'create.multi-instance-user-task': {
+        group: 'activity',
+        type: 'bpmn:UserTask',
+        className: 'bpmn-icon-user task-multi-instance',
+        title: '创建多实例用户任务',
+        action: {
+          click: createMultiInstanceUserTask,
+          dragstart: createMultiInstanceUserTask
+        }
+      },
+      'task-separator': {
+        group: 'activity',
+        separator: true
+      }
+    });
+    return actions;
+  }
+}
+
+CustomPaletteProvider['$inject'] = ['palette', 'create', 'elementFactory', 'spaceTool', 'lassoTool', 'handTool', 'globalConnect', 'translate'];
+
+export default CustomPaletteProvider;

+ 56 - 0
src/bpmn/assets/module/Renderer/CustomRenderer.ts

@@ -0,0 +1,56 @@
+import BaseRenderer from 'diagram-js/lib/draw/BaseRenderer';
+import {
+  append as svgAppend,
+  attr as svgAttr,
+  create as svgCreate,
+  select as svgSelect,
+  selectAll as svgSelectAll,
+  clone as svgClone,
+  clear as svgClear,
+  remove as svgRemove
+} from 'tiny-svg';
+
+const HIGH_PRIORITY = 1500;
+export default class CustomRenderer extends BaseRenderer {
+  bpmnRenderer: BaseRenderer;
+  modeling: any;
+  constructor(eventBus, bpmnRenderer, modeling) {
+    super(eventBus, HIGH_PRIORITY);
+    this.bpmnRenderer = bpmnRenderer;
+    this.modeling = modeling;
+  }
+  canRender(element) {
+    // ignore labels
+    return !element.labelTarget;
+  }
+
+  /**
+   * 自定义节点图形
+   * @param {*} parentNode 当前元素的svgNode
+   * @param {*} element
+   * @returns
+   */
+  drawShape(parentNode, element) {
+    const shape = this.bpmnRenderer.drawShape(parentNode, element);
+    const { type, width, height } = element;
+    // 开始 填充绿色
+    if (type === 'bpmn:StartEvent') {
+      svgAttr(shape, { fill: '#77DF6D' });
+      return shape;
+    }
+    if (type === 'bpmn:EndEvent') {
+      svgAttr(shape, { fill: '#EE7B77' });
+      return shape;
+    }
+    if (type === 'bpmn:UserTask') {
+      svgAttr(shape, { fill: '#A9C4F8' });
+      return shape;
+    }
+    return shape;
+  }
+
+  getShapePath(shape) {
+    return this.bpmnRenderer.getShapePath(shape);
+  }
+}
+CustomRenderer['$inject'] = ['eventBus', 'bpmnRenderer'];

+ 15 - 0
src/bpmn/assets/module/Translate/index.ts

@@ -0,0 +1,15 @@
+import zh from '../../lang/zh';
+
+const customTranslate = (template: any, replacements: any) => {
+  replacements = replacements || {};
+  template = zh[template] || template;
+  return template.replace(/{([^}]+)}/g, function (_: any, key: any) {
+    return replacements[key] || '{' + key + '}';
+  });
+};
+
+export const translateModule = {
+  translate: ['value', customTranslate]
+};
+
+export default translateModule;

+ 17 - 0
src/bpmn/assets/module/index.ts

@@ -0,0 +1,17 @@
+// 翻译模块
+import TranslationModule from './Translate';
+import { ModuleDeclaration } from 'didi';
+import CustomPaletteProvider from './Palette/CustomPaletteProvider';
+import CustomRenderer from './Renderer/CustomRenderer';
+import CustomContextPadProvider from './ContextPad/CustomContextPadProvider';
+
+const Module: ModuleDeclaration[] = [
+  {
+    __init__: ['customPaletteProvider', 'customContextPadProvider', 'customRenderer'],
+    customPaletteProvider: ['type', CustomPaletteProvider],
+    customRenderer: ['type', CustomRenderer],
+    customContextPadProvider: ['type', CustomContextPadProvider]
+  },
+  TranslationModule
+];
+export default Module;

+ 50 - 0
src/bpmn/assets/showConfig.ts

@@ -0,0 +1,50 @@
+export default {
+  'bpmn:EndEvent': {},
+  'bpmn:StartEvent': {
+    initiator: true,
+    formKey: true
+  },
+  'bpmn:UserTask': {
+    allocationType: true,
+    specifyDesc: true,
+    multipleUserAuditType: true,
+    async: true,
+    priority: true,
+    skipExpression: true,
+    dueDate: true,
+    taskListener: true,
+    executionListener: true
+  },
+  'bpmn:ServiceTask': {
+    async: true,
+    skipExpression: true,
+    isForCompensation: true,
+    triggerable: true,
+    class: true
+  },
+  'bpmn:ScriptTask': {
+    async: true,
+    isForCompensation: true,
+    autoStoreVariables: true
+  },
+  'bpmn:ManualTask': {
+    async: true,
+    isForCompensation: true
+  },
+  'bpmn:ReceiveTask': {
+    async: true,
+    isForCompensation: true
+  },
+  'bpmn:SendTask': {
+    async: true,
+    isForCompensation: true
+  },
+  'bpmn:BusinessRuleTask': {
+    async: true,
+    isForCompensation: true,
+    ruleVariablesInput: true,
+    rules: true,
+    resultVariable: true,
+    exclude: true
+  }
+};

+ 284 - 0
src/bpmn/assets/style/index.scss

@@ -0,0 +1,284 @@
+.djs-palette {
+  width: 300px;
+
+  .bpmn-icon-hand-tool:hover {
+    &:after {
+      content: '启动手动工具';
+      position: absolute;
+      left: 45px;
+      width: 120px;
+      font-size: 15px;
+      font-weight: bold;
+      color: #3a84de;
+      border-radius: 2px;
+      border: 1px solid #cccccc;
+      background-color: #fafafa;
+      opacity: 0.8;
+    }
+  }
+  .bpmn-icon-lasso-tool:hover {
+    &:after {
+      content: '启动套索工具';
+      position: absolute;
+      left: 100px;
+      width: 120px;
+      font-size: 15px;
+      font-weight: bold;
+      color: #3a84de;
+      border-radius: 2px;
+      border: 1px solid #cccccc;
+      background-color: #fafafa;
+      opacity: 0.8;
+    }
+  }
+  .bpmn-icon-space-tool:hover {
+    &:after {
+      content: '启动创建/删除空间工具';
+      position: absolute;
+      left: 45px;
+      width: 170px;
+      font-size: 15px;
+      font-weight: bold;
+      color: #3a84de;
+      border-radius: 2px;
+      border: 1px solid #cccccc;
+      background-color: #fafafa;
+      opacity: 0.8;
+    }
+  }
+  .bpmn-icon-connection-multi:hover {
+    &:after {
+      content: '启动全局连接工具';
+      position: absolute;
+      left: 100px;
+      width: 140px;
+      font-size: 15px;
+      font-weight: bold;
+      color: #3a84de;
+      border-radius: 2px;
+      border: 1px solid #cccccc;
+      background-color: #fafafa;
+      opacity: 0.8;
+    }
+  }
+  .bpmn-icon-start-event-none:hover {
+    &:after {
+      content: '创建开始事件';
+      position: absolute;
+      left: 45px;
+      width: 120px;
+      font-size: 15px;
+      font-weight: bold;
+      color: #3a84de;
+      border-radius: 2px;
+      border: 1px solid #cccccc;
+      background-color: #fafafa;
+      opacity: 0.8;
+    }
+  }
+  .bpmn-icon-intermediate-event-none:hover {
+    &:after {
+      content: '创建中间/边界事件';
+      position: absolute;
+      left: 100px;
+      width: 140px;
+      font-size: 15px;
+      font-weight: bold;
+      color: #3a84de;
+      border-radius: 2px;
+      border: 1px solid #cccccc;
+      background-color: #fafafa;
+      opacity: 0.8;
+    }
+  }
+  .bpmn-icon-end-event-none:hover {
+    &:after {
+      content: '创建结束事件';
+      position: absolute;
+      left: 45px;
+      width: 120px;
+      font-size: 15px;
+      font-weight: bold;
+      color: #3a84de;
+      border-radius: 2px;
+      border: 1px solid #cccccc;
+      background-color: #fafafa;
+      opacity: 0.8;
+    }
+  }
+  .bpmn-icon-gateway-none:hover {
+    &:after {
+      content: '创建网关';
+      position: absolute;
+      left: 100px;
+      width: 90px;
+      font-size: 15px;
+      font-weight: bold;
+      color: #3a84de;
+      border-radius: 2px;
+      border: 1px solid #cccccc;
+      background-color: #fafafa;
+      opacity: 0.8;
+    }
+  }
+  .bpmn-icon-gateway-parallel:hover {
+    &:after {
+      content: '创建并行网关';
+      position: absolute;
+      left: 45px;
+      width: 120px;
+      font-size: 15px;
+      font-weight: bold;
+      color: #3a84de;
+      border-radius: 2px;
+      border: 1px solid #cccccc;
+      background-color: #fafafa;
+      opacity: 0.8;
+    }
+  }
+  .bpmn-icon-gateway-eventbased:hover {
+    &:after {
+      content: '创建事件网关';
+      position: absolute;
+      left: 100px;
+      width: 120px;
+      font-size: 15px;
+      font-weight: bold;
+      color: #3a84de;
+      border-radius: 2px;
+      border: 1px solid #cccccc;
+      background-color: #fafafa;
+      opacity: 0.8;
+    }
+  }
+  .bpmn-icon-task:hover {
+    &:after {
+      content: '创建任务';
+      position: absolute;
+      left: 45px;
+      width: 80px;
+      font-size: 15px;
+      font-weight: bold;
+      color: #3a84de;
+      border-radius: 2px;
+      border: 1px solid #cccccc;
+      background-color: #fafafa;
+      opacity: 0.8;
+    }
+  }
+  .bpmn-icon-subprocess-expanded:hover {
+    &:after {
+      content: '创建可折叠子流程';
+      position: absolute;
+      left: 100px;
+      width: 140px;
+      font-size: 15px;
+      font-weight: bold;
+      color: #3a84de;
+      border-radius: 2px;
+      border: 1px solid #cccccc;
+      background-color: #fafafa;
+      opacity: 0.8;
+    }
+  }
+  .bpmn-icon-user-task:hover {
+    &:after {
+      content: '创建用户任务';
+      position: absolute;
+      left: 45px;
+      width: 120px;
+      font-size: 15px;
+      font-weight: bold;
+      color: #3a84de;
+      border-radius: 2px;
+      border: 1px solid #cccccc;
+      background-color: #fafafa;
+      opacity: 0.8;
+    }
+  }
+
+  .task-multi-instance:hover {
+    &:after {
+      content: '创建多实例用户任务';
+      position: absolute;
+      left: 100px;
+      width: 160px;
+      font-size: 15px;
+      font-weight: bold;
+      color: #3a84de;
+      border-radius: 2px;
+      border: 1px solid #cccccc;
+      background-color: #fafafa;
+      opacity: 0.8;
+    }
+  }
+  .bpmn-icon-participant:hover {
+    &:after {
+      content: '创建泳池/泳道';
+      position: absolute;
+      left: 45px;
+      width: 120px;
+      font-size: 15px;
+      font-weight: bold;
+      color: #3a84de;
+      border-radius: 2px;
+      border: 1px solid #cccccc;
+      background-color: #fafafa;
+      opacity: 0.8;
+    }
+  }
+  .bpmn-icon-data-object {
+    display: none;
+    &:hover {
+      &:after {
+        content: '创建数据对象';
+        position: absolute;
+        left: 45px;
+        width: 120px;
+        font-size: 15px;
+        font-weight: bold;
+        color: #3a84de;
+        border-radius: 2px;
+        border: 1px solid #cccccc;
+        background-color: #fafafa;
+        opacity: 0.8;
+      }
+    }
+  }
+  .bpmn-icon-data-store {
+    display: none;
+    &:hover {
+      &:after {
+        content: '创建数据存储';
+        position: absolute;
+        left: 100px;
+        width: 120px;
+        font-size: 15px;
+        font-weight: bold;
+        color: #3a84de;
+        border-radius: 2px;
+        border: 1px solid #cccccc;
+        background-color: #fafafa;
+        opacity: 0.8;
+      }
+    }
+  }
+  .bpmn-icon-group {
+    display: none;
+    &:hover {
+      &:after {
+        content: '创建分组';
+        position: absolute;
+        left: 100px;
+        width: 100px;
+        font-size: 15px;
+        font-weight: bold;
+        color: #3a84de;
+        border-radius: 2px;
+        border: 1px solid #cccccc;
+        background-color: #fafafa;
+        opacity: 0.8;
+      }
+    }
+  }
+}

+ 145 - 0
src/bpmn/hooks/usePanel.ts

@@ -0,0 +1,145 @@
+import showConfig from '../assets/showConfig';
+import { ModdleElement } from 'bpmn';
+import useModelerStore from '@/store/modules/modeler';
+import { MultiInstanceTypeEnum } from '@/enums/bpmn/IndexEnums';
+interface Options {
+  element: ModdleElement;
+}
+
+export default (ops: Options) => {
+  const { element } = ops;
+  const { getModeling, getModdle } = useModelerStore();
+  const modeling = getModeling();
+  const moddle = getModdle();
+
+  /**
+   * 当前节点类型
+   */
+  const elementType = computed(() => {
+    const bizObj = element.businessObject;
+    return bizObj.eventDefinitions ? bizObj.eventDefinitions[0].$type : bizObj.$type;
+  });
+
+  /**
+   * 用于控制面板字段显示与隐藏的配置
+   */
+  const config = computed(() => showConfig[elementType.value] || {});
+
+  /**
+   * 创建一个节点
+   * @param elementType 节点类型
+   * @param properties 属性
+   * @param parent 父节点
+   */
+  const createModdleElement = (elementType: string, properties: any, parent: ModdleElement) => {
+    const element = moddle.create(elementType, properties);
+    parent && (element.$parent = parent);
+    return element;
+  };
+
+  /**
+   * 获取扩展属性,如果不存在会自动创建
+   */
+  const getExtensionElements = (create = true) => {
+    let extensionElements = element.businessObject.get<ModdleElement>('extensionElements');
+    if (!extensionElements && create) {
+      extensionElements = createModdleElement('bpmn:ExtensionElements', { values: [] }, element.businessObject);
+      modeling.updateModdleProperties(element, element.businessObject, { extensionElements });
+    }
+    return extensionElements;
+  };
+
+  /**
+   * 获取extensionElements下的properties
+   * @param extensionElements 可选参数,默认获取当前Element下的extensionElements下的Properties
+   */
+  const getPropertiesElements = (extensionElements?: ModdleElement) => {
+    if (!extensionElements) {
+      extensionElements = getExtensionElements();
+    }
+    let propertiesElements = extensionElements.values.find((item) => item.$type === 'flowable:properties');
+    if (!propertiesElements) {
+      propertiesElements = createModdleElement('flowable:properties', { values: [] }, extensionElements);
+      modeling.updateModdleProperties(element, extensionElements, {
+        values: [...extensionElements.get<[]>('values'), propertiesElements]
+      });
+    }
+    return propertiesElements;
+  };
+
+  /**
+   * 更新节点属性
+   * @param properties 属性值
+   */
+  const updateProperties = (properties: any) => {
+    modeling.updateProperties(element, properties);
+  };
+
+  /**
+   * 更新节点信息
+   * @param updateElement 需要更新的节点
+   * @param properties 属性
+   */
+  const updateModdleProperties = (updateElement, properties: any) => {
+    modeling.updateModdleProperties(element, updateElement, properties);
+  };
+
+  /**
+   * 更新Property属性
+   * @param name key值
+   * @param value 值
+   */
+  const updateProperty = (name: string, value: string) => {
+    const propertiesElements = getPropertiesElements();
+
+    let propertyElements = propertiesElements.values.find((item) => item.name === name);
+    if (!propertyElements) {
+      propertyElements = createModdleElement('flowable:property', { name: name, value: value }, propertiesElements);
+      modeling.updateModdleProperties(element, propertiesElements, {
+        values: [...propertiesElements.get('values'), propertyElements]
+      });
+    } else {
+      propertyElements.name = name;
+      propertyElements.value = value;
+    }
+    return propertyElements;
+  };
+
+  const idChange = (newVal: string) => {
+    if (newVal) {
+      updateProperties({ id: newVal });
+    }
+  };
+  const nameChange = (newVal: string) => {
+    if (newVal) {
+      updateProperties({ name: newVal });
+    }
+  };
+  const formKeyChange = (newVal: string) => {
+    updateProperties({ formKey: newVal });
+  };
+  const constant = {
+    MultiInstanceType: [
+      { id: '373d4b81-a0d1-4eb8-8685-0d2fb1b468e2', label: '无', value: MultiInstanceTypeEnum.NONE },
+      { id: 'b5acea7c-b7e5-46b0-8778-390db091bdab', label: '串行', value: MultiInstanceTypeEnum.SERIAL },
+      { id: 'b4f0c683-1ccc-43c4-8380-e1b998986caf', label: '并行', value: MultiInstanceTypeEnum.PARALLEL }
+    ]
+  };
+
+  return {
+    elementType,
+    constant,
+    showConfig: config,
+
+    updateProperties,
+    updateProperty,
+    updateModdleProperties,
+
+    createModdleElement,
+    idChange,
+    nameChange,
+    formKeyChange,
+    getExtensionElements,
+    getPropertiesElements
+  };
+};

+ 34 - 0
src/bpmn/hooks/useParseElement.ts

@@ -0,0 +1,34 @@
+import { ModdleElement } from 'bpmn';
+
+interface Options {
+  element: ModdleElement;
+}
+
+interface Data {
+  id: string;
+}
+
+export default (ops: Options) => {
+  const { element } = ops;
+
+  const parseData = <T>(): T => {
+    const result = {
+      ...element.businessObject,
+      ...element.businessObject.$attrs
+    };
+
+    // 移除flowable前缀,格式化数组
+    for (const key in result) {
+      if (key.indexOf('flowable:') === 0) {
+        const newKey = key.replace('flowable:', '');
+        result[newKey] = result[key];
+        delete result[key];
+      }
+    }
+    return { ...result } as T;
+  };
+
+  return {
+    parseData
+  };
+};

+ 496 - 0
src/bpmn/index.vue

@@ -0,0 +1,496 @@
+<template>
+  <div class="containers-bpmn">
+    <!-- dark模式下 连接线的箭头样式 -->
+    <svg width="0" height="0" style="position: absolute">
+      <defs>
+        <marker id="markerArrow-dark-mode" viewBox="0 0 20 20" refX="11" refY="10" markerWidth="10" markerHeight="10" orient="auto">
+          <path d="M 1 5 L 11 10 L 1 15 Z" class="arrow-dark" />
+        </marker>
+      </defs>
+    </svg>
+    <div v-loading="loading" class="app-containers-bpmn">
+      <el-container class="h-full">
+        <el-container style="align-items: stretch">
+          <el-header>
+            <div class="process-toolbar">
+              <el-space wrap :size="10">
+                <el-tooltip effect="dark" content="自适应屏幕" placement="bottom">
+                  <el-button size="small" icon="Rank" @click="fitViewport" />
+                </el-tooltip>
+                <el-tooltip effect="dark" content="放大" placement="bottom">
+                  <el-button size="small" icon="ZoomIn" @click="zoomViewport(true)" />
+                </el-tooltip>
+                <el-tooltip effect="dark" content="缩小" placement="bottom">
+                  <el-button size="small" icon="ZoomOut" @click="zoomViewport(false)" />
+                </el-tooltip>
+                <el-tooltip effect="dark" content="后退" placement="bottom">
+                  <el-button size="small" icon="Back" @click="bpmnModeler.get('commandStack').undo()" />
+                </el-tooltip>
+                <el-tooltip effect="dark" content="前进" placement="bottom">
+                  <el-button size="small" icon="Right" @click="bpmnModeler.get('commandStack').redo()" />
+                </el-tooltip>
+              </el-space>
+              <el-space wrap :size="10" style="float: right; padding-right: 10px">
+                <el-button size="small" type="primary" @click="saveXml">保 存</el-button>
+                <el-dropdown size="small">
+                  <el-button size="small" type="primary"> 预 览 </el-button>
+                  <template #dropdown>
+                    <el-dropdown-menu>
+                      <el-dropdown-item icon="Document" @click="previewXML">XML预览</el-dropdown-item>
+                      <el-dropdown-item icon="View" @click="previewSVG"> SVG预览</el-dropdown-item>
+                    </el-dropdown-menu>
+                  </template>
+                </el-dropdown>
+                <el-dropdown size="small">
+                  <el-button size="small" type="primary"> 下 载 </el-button>
+                  <template #dropdown>
+                    <el-dropdown-menu>
+                      <el-dropdown-item icon="Download" @click="downloadXML">下载XML</el-dropdown-item>
+                      <el-dropdown-item icon="Download" @click="downloadSVG"> 下载SVG</el-dropdown-item>
+                    </el-dropdown-menu>
+                  </template>
+                </el-dropdown>
+              </el-space>
+            </div>
+          </el-header>
+          <div ref="canvas" class="canvas" />
+        </el-container>
+        <div :class="{ 'process-panel': true, 'hide': panelFlag }">
+          <div class="process-panel-bar" @click="panelBarClick">
+            <div class="open-bar">
+              <el-link type="default" :underline="false">
+                <svg-icon class-name="open-bar" :icon-class="panelFlag ? 'caret-back' : 'caret-forward'"></svg-icon>
+              </el-link>
+            </div>
+          </div>
+          <transition enter-active-class="animate__animated animate__fadeIn">
+            <div v-show="showPanel" v-if="bpmnModeler" class="panel-content">
+              <PropertyPanel :modeler="bpmnModeler" />
+            </div>
+          </transition>
+        </div>
+      </el-container>
+    </div>
+  </div>
+  <div>
+    <el-dialog v-model="perviewXMLShow" title="XML预览" width="80%" append-to-body>
+      <highlightjs :code="xmlStr" language="XML" />
+    </el-dialog>
+  </div>
+  <div>
+    <el-dialog v-model="perviewSVGShow" title="SVG预览" width="80%" append-to-body>
+      <div style="text-align: center" v-html="svgData" />
+    </el-dialog>
+  </div>
+</template>
+
+<script lang="ts" setup name="BpmnDesign">
+import 'bpmn-js/dist/assets/diagram-js.css';
+import 'bpmn-js/dist/assets/bpmn-font/css/bpmn.css';
+import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-codes.css';
+import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css';
+import './assets/style/index.scss';
+import { Canvas, Modeler } from 'bpmn';
+import PropertyPanel from './panel/index.vue';
+import BpmnModeler from 'bpmn-js/lib/Modeler.js';
+import defaultXML from './assets/defaultXML';
+import flowableModdle from './assets/moddle/flowable';
+import Modules from './assets/module/index';
+import useModelerStore from '@/store/modules/modeler';
+import useDialog from '@/hooks/useDialog';
+
+const emit = defineEmits(['closeCallBack', 'saveCallBack']);
+
+const { visible, title, openDialog, closeDialog } = useDialog({
+  title: '编辑流程'
+});
+const modelerStore = useModelerStore();
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const panelFlag = ref(false);
+const showPanel = ref(true);
+const canvas = ref<HTMLDivElement>();
+const panel = ref<HTMLDivElement>();
+const bpmnModeler = ref<Modeler>();
+const zoom = ref(1);
+const perviewXMLShow = ref(false);
+const perviewSVGShow = ref(false);
+const xmlStr = ref('');
+const svgData = ref('');
+const loading = ref(false);
+
+const panelBarClick = () => {
+  // 延迟执行,否则会导致面板收起时,属性面板不显示
+  panelFlag.value = !panelFlag.value;
+  setTimeout(() => {
+    showPanel.value = !panelFlag.value;
+  }, 100);
+};
+
+/**
+ * 初始化Canvas
+ */
+const initCanvas = () => {
+  bpmnModeler.value = new BpmnModeler({
+    container: canvas.value,
+    // 键盘
+    keyboard: {
+      bindTo: window // 或者window,注意与外部表单的键盘监听事件是否冲突
+    },
+    propertiesPanel: {
+      parent: panel.value
+    },
+    additionalModules: Modules,
+    moddleExtensions: {
+      flowable: flowableModdle
+    }
+  });
+};
+
+/**
+ * 初始化Model
+ */
+const initModel = () => {
+  if (modelerStore.getModeler()) {
+    modelerStore.getModeler().destroy();
+    modelerStore.setModeler(undefined);
+  }
+  modelerStore.setModeler(bpmnModeler.value);
+};
+
+/**
+ * 新建
+ */
+const newDiagram = async () => {
+  await proxy?.$modal.confirm('是否确认新建');
+  initDiagram();
+};
+
+/**
+ * 初始化
+ */
+const initDiagram = (xml?: string) => {
+  if (!xml) xml = defaultXML;
+  bpmnModeler.value.importXML(xml);
+};
+
+/**
+ * 自适应屏幕
+ */
+const fitViewport = () => {
+  zoom.value = bpmnModeler.value.get<Canvas>('canvas').zoom('fit-viewport');
+  const bbox = document.querySelector<SVGGElement>('.app-containers-bpmn .viewport').getBBox();
+  const currentViewBox = bpmnModeler.value.get<Canvas>('canvas').viewbox();
+  const elementMid = {
+    x: bbox.x + bbox.width / 2 - 65,
+    y: bbox.y + bbox.height / 2
+  };
+  bpmnModeler.value.get<Canvas>('canvas').viewbox({
+    x: elementMid.x - currentViewBox.width / 2,
+    y: elementMid.y - currentViewBox.height / 2,
+    width: currentViewBox.width,
+    height: currentViewBox.height
+  });
+  zoom.value = (bbox.width / currentViewBox.width) * 1.8;
+};
+/**
+ * 放大或者缩小
+ * @param zoomIn true 放大 | false 缩小
+ */
+const zoomViewport = (zoomIn = true) => {
+  zoom.value = bpmnModeler.value.get<Canvas>('canvas').zoom();
+  zoom.value += zoomIn ? 0.1 : -0.1;
+  bpmnModeler.value.get<Canvas>('canvas').zoom(zoom.value);
+};
+
+/**
+ * 下载XML
+ */
+const downloadXML = async () => {
+  try {
+    const { xml } = await bpmnModeler.value.saveXML({ format: true });
+    downloadFile(`${getProcessElement().name}.bpmn20.xml`, xml, 'application/xml');
+  } catch (e) {
+    proxy?.$modal.msgError(e);
+  }
+};
+
+/**
+ * 下载SVG
+ */
+const downloadSVG = async () => {
+  try {
+    const { svg } = await bpmnModeler.value.saveSVG();
+    downloadFile(getProcessElement().name, svg, 'image/svg+xml');
+  } catch (e) {
+    proxy?.$modal.msgError(e);
+  }
+};
+
+/**
+ * XML预览
+ */
+const previewXML = async () => {
+  try {
+    const { xml } = await bpmnModeler.value.saveXML({ format: true });
+    xmlStr.value = xml;
+    perviewXMLShow.value = true;
+  } catch (e) {
+    proxy?.$modal.msgError(e);
+  }
+};
+
+/**
+ * SVG预览
+ */
+const previewSVG = async () => {
+  try {
+    const { svg } = await bpmnModeler.value.saveSVG();
+    svgData.value = svg;
+    perviewSVGShow.value = true;
+  } catch (e) {
+    proxy?.$modal.msgError(e);
+  }
+};
+
+const curNodeInfo = reactive({
+  curType: '', // 任务类型 用户任务
+  curNode: '',
+  expValue: '' //多用户和部门角色实现
+});
+
+const downloadFile = (fileName: string, data: any, type: string) => {
+  const a = document.createElement('a');
+  const url = window.URL.createObjectURL(new Blob([data], { type: type }));
+  a.href = url;
+  a.download = fileName;
+  a.click();
+  window.URL.revokeObjectURL(url);
+};
+
+const getProcessElement = () => {
+  const rootElements = bpmnModeler.value?.getDefinitions().rootElements;
+  for (let i = 0; i < rootElements.length; i++) {
+    if (rootElements[i].$type === 'bpmn:Process') return rootElements[i];
+  }
+};
+
+const getProcess = () => {
+  const element = getProcessElement();
+  return {
+    id: element.id,
+    name: element.name
+  };
+};
+
+const saveXml = async () => {
+  const { xml } = await bpmnModeler.value.saveXML({ format: true });
+  const { svg } = await bpmnModeler.value.saveSVG();
+  const process = getProcess();
+  let data = {
+    xml: xml,
+    svg: svg,
+    key: process.id,
+    name: process.name,
+    loading: loading
+  };
+  emit('saveCallBack', data);
+};
+
+const open = (xml?: string) => {
+  openDialog();
+  nextTick(() => {
+    initDiagram(xml);
+  });
+};
+const close = () => {
+  closeDialog();
+};
+
+onMounted(() => {
+  nextTick(() => {
+    initCanvas();
+    initModel();
+  });
+});
+
+/**
+ * 对外暴露子组件方法
+ */
+defineExpose({
+  initDiagram,
+  saveXml,
+  open,
+  close
+});
+</script>
+
+<style lang="scss">
+/** 夜间模式 线条的颜色 */
+$stroke-color-dark: white;
+$bpmn-font-size: 12px;
+/** 日间模式 字体颜色 */
+$bpmn-font-color-dark: white;
+/** 夜间模式 字体颜色 */
+$bpmn-font-color-light: #222;
+
+/* 背景网格 */
+@mixin djs-container {
+  background-image: linear-gradient(90deg, hsl(0deg 0% 78.4% / 15%) 10%, transparent 0), linear-gradient(hsl(0deg 0% 78.4% / 15%) 10%, transparent 0) !important;
+  background-size: 10px 10px !important;
+}
+
+html[class='light'] {
+  /** 从左侧拖动时的背景图 */
+  svg.new-parent {
+    @include djs-container;
+  }
+
+  /** 双击编辑元素时样式保持一致 */
+  div.djs-direct-editing-parent {
+    border-radius: 10px;
+    background-color: transparent !important;
+    color: $bpmn-font-color-light;
+  }
+
+  g.djs-visual {
+    .djs-label {
+      fill: $bpmn-font-color-light !important;
+      font-size: $bpmn-font-size !important;
+    }
+  }
+}
+
+html[class='dark'] {
+  /** dark模式下 连接线的箭头样式 */
+  .arrow-dark {
+    stroke-width: 1px;
+    stroke-linecap: round;
+    stroke: $stroke-color-dark;
+    fill: $stroke-color-dark;
+    stroke-linejoin: round;
+  }
+
+  /** 从左侧拖动时的背景图 */
+  svg.new-parent {
+    background-color: black !important;
+    @include djs-container;
+  }
+
+  /** 双击编辑元素时样式保持一致 */
+  div.djs-direct-editing-parent {
+    border-radius: 10px;
+    background-color: transparent !important;
+    color: $bpmn-font-color-dark;
+  }
+
+  /** 元素相关设置 */
+  g.djs-visual {
+    /** 元素边框 需要去除文字(.djs-label) */
+    & > *:first-child:not(.djs-label) {
+      stroke: $stroke-color-dark !important;
+    }
+
+    /** 字体颜色 */
+    .djs-label {
+      fill: $bpmn-font-color-dark !important;
+      font-size: $bpmn-font-size !important;
+    }
+
+    /* 连接线样式 */
+    path[data-corner-radius] {
+      stroke: $stroke-color-dark !important;
+      marker-end: url('#markerArrow-dark-mode') !important;
+    }
+  }
+}
+
+.containers-bpmn {
+  height: 100%;
+  .app-containers-bpmn {
+    width: 100%;
+    height: 100%;
+    .canvas {
+      width: 100%;
+      height: 100%;
+      @include djs-container;
+    }
+    .el-header {
+      height: 35px;
+      padding: 0;
+    }
+
+    .process-panel {
+      transition: width 0.25s ease-in;
+      .process-panel-bar {
+        width: 34px;
+        height: 40px;
+        .open-bar {
+          width: 34px;
+          line-height: 40px;
+        }
+      }
+      // 收起面板样式
+      &.hide {
+        width: 34px;
+        overflow: hidden;
+        padding: 0;
+        .process-panel-bar {
+          width: 34px;
+          height: 100%;
+          box-sizing: border-box;
+          display: block;
+          text-align: left;
+          line-height: 34px;
+        }
+        .process-panel-bar:hover {
+          background-color: var(--bpmn-panel-bar-background-color);
+        }
+      }
+    }
+  }
+}
+pre {
+  margin: 0;
+  height: 100%;
+  max-height: calc(80vh - 32px);
+  overflow-x: hidden;
+  overflow-y: auto;
+  .hljs {
+    word-break: break-word;
+    white-space: pre-wrap;
+    padding: 0.5em;
+  }
+}
+
+.open-bar {
+  font-size: 20px;
+  cursor: pointer;
+  text-align: center;
+}
+.process-panel {
+  box-sizing: border-box;
+  padding: 0 8px 0 8px;
+  border-left: 1px solid var(--bpmn-panel-border);
+  box-shadow: var(--bpmn-panel-box-shadow) 0 0 8px;
+  max-height: 100%;
+  width: 25%;
+  height: calc(100vh - 100px);
+  .el-collapse {
+    height: calc(100vh - 182px);
+    overflow: auto;
+  }
+}
+
+// 任务栏 透明度
+//:deep(.djs-palette) {
+//  opacity: 0.3;
+//  transition: all 1s;
+//}
+//
+//:deep(.djs-palette:hover) {
+//  opacity: 1;
+//  transition: all 1s;
+//}
+</style>

+ 68 - 0
src/bpmn/panel/GatewayPanel.vue

@@ -0,0 +1,68 @@
+<template>
+  <div>
+    <el-collapse v-model="currentCollapseItem">
+      <el-collapse-item name="1">
+        <template #title>
+          <div class="collapse__title">
+            <el-icon>
+              <InfoFilled />
+            </el-icon>
+            常规
+          </div>
+        </template>
+        <div>
+          <el-form ref="formRef" :model="formData" :rules="formRules" label-width="80px">
+            <el-form-item prop="id" label="节点 ID">
+              <el-input v-model="formData.id" @change="idChange"> </el-input>
+            </el-form-item>
+            <el-form-item prop="name" label="节点名称">
+              <el-input v-model="formData.name" @change="nameChange"> </el-input>
+            </el-form-item>
+          </el-form>
+        </div>
+      </el-collapse-item>
+
+      <el-collapse-item name="2">
+        <template #title>
+          <div class="collapse__title">
+            <el-icon>
+              <BellFilled />
+            </el-icon>
+            执行监听器
+          </div>
+        </template>
+        <div>
+          <ExecutionListener :element="element"></ExecutionListener>
+        </div>
+      </el-collapse-item>
+    </el-collapse>
+  </div>
+</template>
+<script setup lang="ts">
+import useParseElement from '../hooks/useParseElement';
+import usePanel from '../hooks/usePanel';
+import { Modeler, ModdleElement } from 'bpmn';
+import { GatewayPanel } from 'bpmnDesign';
+import ExecutionListener from './property/ExecutionListener.vue';
+
+interface PropType {
+  element: ModdleElement;
+}
+const props = withDefaults(defineProps<PropType>(), {});
+const { nameChange, idChange } = usePanel({
+  element: toRaw(props.element)
+});
+const { parseData } = useParseElement({
+  element: toRaw(props.element)
+});
+const currentCollapseItem = ref(['1', '2']);
+const formData = ref(parseData<GatewayPanel>());
+
+const formRules = ref<ElFormRules>({
+  processCategory: [{ required: true, message: '请选择', trigger: 'blur' }],
+  id: [{ required: true, message: '请输入', trigger: 'blur' }],
+  name: [{ required: true, message: '请输入', trigger: 'blur' }]
+});
+</script>
+
+<style lang="scss" scoped></style>

+ 68 - 0
src/bpmn/panel/ParticipantPanel.vue

@@ -0,0 +1,68 @@
+<template>
+  <div>
+    <el-collapse v-model="currentCollapseItem">
+      <el-collapse-item name="1">
+        <template #title>
+          <div class="collapse__title">
+            <el-icon>
+              <InfoFilled />
+            </el-icon>
+            常规
+          </div>
+        </template>
+        <div>
+          <el-form ref="formRef" :model="formData" :rules="formRules" label-width="90px">
+            <el-form-item prop="id" label="节点 ID">
+              <el-input v-model="formData.id" @change="idChange"></el-input>
+            </el-form-item>
+            <el-form-item prop="name" label="节点名称">
+              <el-input v-model="formData.name" @change="nameChange"></el-input>
+            </el-form-item>
+          </el-form>
+        </div>
+      </el-collapse-item>
+
+      <el-collapse-item name="2">
+        <template #title>
+          <div class="collapse__title">
+            <el-icon>
+              <BellFilled />
+            </el-icon>
+            执行监听器
+          </div>
+        </template>
+        <div>
+          <ExecutionListener :element="element"></ExecutionListener>
+        </div>
+      </el-collapse-item>
+    </el-collapse>
+  </div>
+</template>
+<script setup lang="ts">
+import useParseElement from '../hooks/useParseElement';
+import usePanel from '../hooks/usePanel';
+import ExecutionListener from './property/ExecutionListener.vue';
+import { ModdleElement } from 'bpmn';
+import { ParticipantPanel } from 'bpmnDesign';
+
+interface PropType {
+  element: ModdleElement;
+}
+
+const props = withDefaults(defineProps<PropType>(), {});
+const { nameChange, idChange } = usePanel({
+  element: toRaw(props.element)
+});
+const { parseData } = useParseElement({
+  element: toRaw(props.element)
+});
+
+const formData = ref(parseData<ParticipantPanel>());
+const currentCollapseItem = ref(['1', '2']);
+const formRules = ref<ElFormRules>({
+  id: [{ required: true, message: '请输入', trigger: 'blur' }],
+  name: [{ required: true, message: '请输入', trigger: 'blur' }]
+});
+</script>
+
+<style lang="scss" scoped></style>

+ 71 - 0
src/bpmn/panel/ProcessPanel.vue

@@ -0,0 +1,71 @@
+<template>
+  <div>
+    <el-collapse v-model="currentCollapseItem">
+      <el-collapse-item name="1">
+        <template #title>
+          <div class="collapse__title">
+            <el-icon>
+              <InfoFilled />
+            </el-icon>
+            常规
+          </div>
+        </template>
+        <div>
+          <el-form ref="formRef" :model="formData" :rules="formRules" label-width="80px">
+            <el-form-item label="流程标识" prop="id">
+              <el-input v-model="formData.id" @change="idChange"></el-input>
+            </el-form-item>
+            <el-form-item label="流程名称" prop="name">
+              <el-input v-model="formData.name" @change="nameChange"></el-input>
+            </el-form-item>
+          </el-form>
+        </div>
+      </el-collapse-item>
+
+      <el-collapse-item name="2">
+        <template #title>
+          <div class="collapse__title">
+            <el-icon>
+              <BellFilled />
+            </el-icon>
+            执行监听器
+          </div>
+        </template>
+        <div>
+          <ExecutionListener :element="element"></ExecutionListener>
+        </div>
+      </el-collapse-item>
+    </el-collapse>
+  </div>
+</template>
+
+<script setup lang="ts">
+import ExecutionListener from './property/ExecutionListener.vue';
+import useParseElement from '../hooks/useParseElement';
+import usePanel from '../hooks/usePanel';
+import { Modeler, ModdleElement } from 'bpmn';
+import { ProcessPanel } from 'bpmnDesign';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+interface PropType {
+  element: ModdleElement;
+}
+const props = withDefaults(defineProps<PropType>(), {});
+
+const { parseData } = useParseElement({
+  element: toRaw(props.element)
+});
+const { idChange, nameChange } = usePanel({
+  element: toRaw(props.element)
+});
+const currentCollapseItem = ref(['1', '2']);
+const formData = ref<ProcessPanel>(parseData<ProcessPanel>());
+
+const formRules = ref<ElFormRules>({
+  id: [{ required: true, message: '请输入', trigger: 'blur' }],
+  name: [{ required: true, message: '请输入', trigger: 'blur' }]
+});
+</script>
+
+<style scoped lang="scss"></style>

+ 95 - 0
src/bpmn/panel/SequenceFlowPanel.vue

@@ -0,0 +1,95 @@
+<template>
+  <div>
+    <el-collapse v-model="currentCollapseItem">
+      <el-collapse-item name="1">
+        <template #title>
+          <div class="collapse__title">
+            <el-icon>
+              <InfoFilled />
+            </el-icon>
+            常规
+          </div>
+        </template>
+        <div>
+          <el-form ref="formRef" :model="formData" :rules="formRules" label-width="90px">
+            <el-form-item prop="id" label="节点 ID">
+              <el-input v-model="formData.id" @change="idChange"> </el-input>
+            </el-form-item>
+            <el-form-item prop="name" label="节点名称">
+              <el-input v-model="formData.name" @change="nameChange"> </el-input>
+            </el-form-item>
+            <el-form-item prop="conditionExpression" label="跳转条件">
+              <el-input v-model="formData.conditionExpressionValue" @change="conditionExpressionChange"> </el-input>
+            </el-form-item>
+            <el-form-item prop="skipExpression" label="跳过表达式">
+              <el-input v-model="formData.skipExpression" @change="skipExpressionChange"> </el-input>
+            </el-form-item>
+          </el-form>
+        </div>
+      </el-collapse-item>
+
+      <el-collapse-item name="2">
+        <template #title>
+          <div class="collapse__title">
+            <el-icon>
+              <BellFilled />
+            </el-icon>
+            执行监听器
+          </div>
+        </template>
+        <div>
+          <ExecutionListener :element="element"></ExecutionListener>
+        </div>
+      </el-collapse-item>
+    </el-collapse>
+  </div>
+</template>
+<script setup lang="ts">
+import useParseElement from '../hooks/useParseElement';
+import useModelerStore from '@/store/modules/modeler';
+import usePanel from '../hooks/usePanel';
+import ExecutionListener from './property/ExecutionListener.vue';
+import { Modeler, ModdleElement } from 'bpmn';
+import { SequenceFlowPanel } from 'bpmnDesign';
+
+interface PropType {
+  element: ModdleElement;
+}
+const props = withDefaults(defineProps<PropType>(), {});
+const { nameChange, idChange, updateProperties } = usePanel({
+  element: toRaw(props.element)
+});
+const { parseData } = useParseElement({
+  element: toRaw(props.element)
+});
+const moddle = useModelerStore().getModdle();
+const currentCollapseItem = ref(['1', '2']);
+const formData = ref(parseData<SequenceFlowPanel>());
+
+const formRules = ref<ElFormRules>({
+  processCategory: [{ required: true, message: '请选择', trigger: 'blur' }],
+  id: [{ required: true, message: '请输入', trigger: 'blur' }],
+  name: [{ required: true, message: '请输入', trigger: 'blur' }]
+});
+
+const conditionExpressionChange = (val: string) => {
+  if (val) {
+    const newCondition = moddle.create('bpmn:FormalExpression', { body: val });
+    updateProperties({ conditionExpression: newCondition });
+  } else {
+    updateProperties({ conditionExpression: null });
+  }
+};
+
+const skipExpressionChange = (val: string) => {
+  updateProperties({ 'flowable:skipExpression': val });
+};
+
+onBeforeMount(() => {
+  if (formData.value.conditionExpression) {
+    formData.value.conditionExpressionValue = formData.value.conditionExpression.body;
+  }
+});
+</script>
+
+<style lang="scss" scoped></style>

+ 67 - 0
src/bpmn/panel/StartEndPanel.vue

@@ -0,0 +1,67 @@
+<template>
+  <div>
+    <el-collapse v-model="currentCollapseItem">
+      <el-collapse-item name="1">
+        <template #title>
+          <div class="collapse__title">
+            <el-icon>
+              <InfoFilled />
+            </el-icon>
+            常规
+          </div>
+        </template>
+        <div>
+          <el-form ref="formRef" :model="formData" :rules="formRules" label-width="90px">
+            <el-form-item prop="id" label="节点 ID">
+              <el-input v-model="formData.id" @change="idChange"> </el-input>
+            </el-form-item>
+            <el-form-item prop="name" label="节点名称">
+              <el-input v-model="formData.name" @change="nameChange"> </el-input>
+            </el-form-item>
+          </el-form>
+        </div>
+      </el-collapse-item>
+
+      <el-collapse-item name="2">
+        <template #title>
+          <div class="collapse__title">
+            <el-icon>
+              <BellFilled />
+            </el-icon>
+            执行监听器
+          </div>
+        </template>
+        <div>
+          <ExecutionListener :element="element"></ExecutionListener>
+        </div>
+      </el-collapse-item>
+    </el-collapse>
+  </div>
+</template>
+<script setup lang="ts">
+import ExecutionListener from './property/ExecutionListener.vue';
+import useParseElement from '../hooks/useParseElement';
+import usePanel from '../hooks/usePanel';
+import { Modeler, ModdleElement } from 'bpmn';
+import { StartEndPanel } from 'bpmnDesign';
+
+interface PropType {
+  element: ModdleElement;
+}
+const props = withDefaults(defineProps<PropType>(), {});
+const { nameChange, idChange } = usePanel({
+  element: toRaw(props.element)
+});
+const { parseData } = useParseElement({
+  element: toRaw(props.element)
+});
+
+const formData = ref(parseData<StartEndPanel>());
+const currentCollapseItem = ref(['1', '2']);
+const formRules = ref<ElFormRules>({
+  id: [{ required: true, message: '请输入', trigger: 'blur' }],
+  name: [{ required: true, message: '请输入', trigger: 'blur' }]
+});
+</script>
+
+<style lang="scss" scoped></style>

+ 193 - 0
src/bpmn/panel/SubProcessPanel.vue

@@ -0,0 +1,193 @@
+<template>
+  <div>
+    <el-form ref="formRef" :model="formData" :rules="formRules" label-width="90px">
+      <el-collapse v-model="currentCollapseItem">
+        <el-collapse-item name="1">
+          <template #title>
+            <div class="collapse__title">
+              <el-icon>
+                <InfoFilled />
+              </el-icon>
+              常规
+            </div>
+          </template>
+          <div>
+            <el-form-item prop="id" label="节点 ID">
+              <el-input v-model="formData.id" @change="idChange"> </el-input>
+            </el-form-item>
+            <el-form-item prop="name" label="节点名称">
+              <el-input v-model="formData.name" @change="nameChange"> </el-input>
+            </el-form-item>
+          </div>
+        </el-collapse-item>
+
+        <el-collapse-item name="2">
+          <template #title>
+            <div class="collapse__title">
+              <el-icon>
+                <BellFilled />
+              </el-icon>
+              执行监听器
+            </div>
+          </template>
+          <div>
+            <ExecutionListener :element="element"></ExecutionListener>
+          </div>
+        </el-collapse-item>
+        <el-collapse-item name="3">
+          <template #title>
+            <div class="collapse__title">
+              <el-icon>
+                <HelpFilled />
+              </el-icon>
+              多实例
+            </div>
+          </template>
+          <div>
+            <el-form-item label="多实例类型">
+              <el-select v-model="formData.multiInstanceType" @change="multiInstanceTypeChange">
+                <el-option v-for="item in constant.MultiInstanceType" :key="item.id" :value="item.value" :label="item.label"> </el-option>
+              </el-select>
+            </el-form-item>
+
+            <div v-if="formData.multiInstanceType !== MultiInstanceTypeEnum.NONE">
+              <el-form-item label="集合">
+                <template #label>
+                  <span>
+                    集合
+                    <el-tooltip placement="top">
+                      <el-icon><QuestionFilled /></el-icon>
+                      <template #content>
+                        属性会作为表达式进行解析。如果表达式解析为字符串而不是一个集合,<br />
+                        不论是因为本身配置的就是静态字符串值,还是表达式计算结果为字符串,<br />
+                        这个字符串都会被当做变量名,并从流程变量中用于获取实际的集合。
+                      </template>
+                    </el-tooltip>
+                  </span>
+                </template>
+                <el-input v-model="formData.collection" @change="collectionChange"></el-input>
+              </el-form-item>
+              <el-form-item label="元素变量">
+                <template #label>
+                  <span>
+                    元素变量
+                    <el-tooltip placement="top">
+                      <el-icon><QuestionFilled /></el-icon>
+                      <template #content>
+                        每创建一个用户任务前,先以该元素变量为label,集合中的一项为value,<br />
+                        创建(局部)流程变量,该局部流程变量被用于指派用户任务。<br />
+                        一般来说,该字符串应与指定人员变量相同。
+                      </template>
+                    </el-tooltip>
+                  </span>
+                </template>
+                <el-input v-model="formData.elementVariable" @change="elementVariableChange"> </el-input>
+              </el-form-item>
+              <el-form-item label="完成条件">
+                <template #label>
+                  <span>
+                    完成条件
+                    <el-tooltip placement="top">
+                      <el-icon><QuestionFilled /></el-icon>
+                      <template #content>
+                        多实例活动在所有实例都完成时结束,然而也可以指定一个表达式,在每个实例<br />
+                        结束时进行计算。当表达式计算为true时,将销毁所有剩余的实例,并结束多实例<br />
+                        活动,继续执行流程。例如 ${nrOfCompletedInstances/nrOfInstances >= 0.6 },<br />
+                        表示当任务完成60%时,该节点就算完成
+                      </template>
+                    </el-tooltip>
+                  </span>
+                </template>
+                <el-input v-model="formData.completionCondition" @change="completionConditionChange"> </el-input>
+              </el-form-item>
+            </div>
+          </div>
+        </el-collapse-item>
+      </el-collapse>
+    </el-form>
+  </div>
+</template>
+<script setup lang="ts">
+import ExecutionListener from './property/ExecutionListener.vue';
+import useParseElement from '../hooks/useParseElement';
+import usePanel from '../hooks/usePanel';
+import { ModdleElement } from 'bpmn';
+import { SubProcessPanel } from 'bpmnDesign';
+import { MultiInstanceTypeEnum } from '@/enums/bpmn/IndexEnums';
+
+interface PropType {
+  element: ModdleElement;
+}
+const props = withDefaults(defineProps<PropType>(), {});
+const { nameChange, idChange, updateProperties, createModdleElement, constant } = usePanel({
+  element: toRaw(props.element)
+});
+const { parseData } = useParseElement({
+  element: toRaw(props.element)
+});
+
+const formData = ref(parseData<SubProcessPanel>());
+const currentCollapseItem = ref(['1', '2', '3']);
+
+const multiInstanceTypeChange = (newVal) => {
+  if (newVal !== MultiInstanceTypeEnum.NONE) {
+    let loopCharacteristics = props.element.businessObject.get('loopCharacteristics');
+    if (!loopCharacteristics) {
+      loopCharacteristics = createModdleElement('bpmn:MultiInstanceLoopCharacteristics', {}, props.element.businessObject);
+    }
+    loopCharacteristics.isSequential = newVal === MultiInstanceTypeEnum.SERIAL;
+    updateProperties({ loopCharacteristics: loopCharacteristics });
+  } else {
+    updateProperties({ loopCharacteristics: undefined });
+  }
+};
+const collectionChange = (newVal) => {
+  let loopCharacteristics = props.element.businessObject.get('loopCharacteristics');
+  if (!loopCharacteristics) {
+    loopCharacteristics = createModdleElement('bpmn:MultiInstanceLoopCharacteristics', {}, props.element.businessObject);
+  }
+  loopCharacteristics.collection = newVal && newVal.length > 0 ? newVal : undefined;
+  updateProperties({ loopCharacteristics: loopCharacteristics });
+};
+const elementVariableChange = (newVal) => {
+  let loopCharacteristics = props.element.businessObject.get('loopCharacteristics');
+  if (!loopCharacteristics) {
+    loopCharacteristics = createModdleElement('bpmn:MultiInstanceLoopCharacteristics', {}, props.element.businessObject);
+  }
+  loopCharacteristics.elementVariable = newVal && newVal.length > 0 ? newVal : undefined;
+  updateProperties({ loopCharacteristics: loopCharacteristics });
+};
+const completionConditionChange = (newVal) => {
+  let loopCharacteristics = props.element.businessObject.get<ModdleElement>('loopCharacteristics');
+  if (!loopCharacteristics) {
+    loopCharacteristics = createModdleElement('bpmn:MultiInstanceLoopCharacteristics', {}, props.element.businessObject);
+  }
+  if (newVal && newVal.length > 0) {
+    if (!loopCharacteristics.completionCondition) {
+      loopCharacteristics.completionCondition = createModdleElement('bpmn:Expression', { body: newVal }, loopCharacteristics);
+    } else {
+      loopCharacteristics.completionCondition.body = newVal;
+    }
+  } else {
+    loopCharacteristics.completionCondition = undefined;
+  }
+  updateProperties({ loopCharacteristics: loopCharacteristics });
+};
+
+onBeforeMount(() => {
+  if (formData.value.loopCharacteristics) {
+    const loopCharacteristics = formData.value.loopCharacteristics;
+    formData.value.collection = loopCharacteristics.collection || '';
+    formData.value.elementVariable = loopCharacteristics.elementVariable || '';
+    formData.value.completionCondition = loopCharacteristics.completionCondition?.body || '';
+    formData.value.multiInstanceType = loopCharacteristics.isSequential ? MultiInstanceTypeEnum.SERIAL : MultiInstanceTypeEnum.PARALLEL;
+  }
+});
+
+const formRules = ref<ElFormRules>({
+  id: [{ required: true, message: '请输入', trigger: 'blur' }],
+  name: [{ required: true, message: '请输入', trigger: 'blur' }]
+});
+</script>
+
+<style lang="scss" scoped></style>

+ 492 - 0
src/bpmn/panel/TaskPanel.vue

@@ -0,0 +1,492 @@
+<template>
+  <div>
+    <el-form ref="formRef" size="default" :model="formData" :rules="formRules" label-width="100px">
+      <el-collapse v-model="currentCollapseItem">
+        <el-collapse-item name="1">
+          <template #title>
+            <div class="collapse__title">
+              <el-icon>
+                <InfoFilled />
+              </el-icon>
+              常规
+            </div>
+          </template>
+          <div>
+            <el-form-item prop="id" label="节点 ID">
+              <el-input v-model="formData.id" @change="idChange"> </el-input>
+            </el-form-item>
+            <el-form-item prop="name" label="节点名称">
+              <el-input v-model="formData.name" @change="nameChange"> </el-input>
+            </el-form-item>
+            <el-form-item v-if="showConfig.skipExpression" prop="skipExpression" label="跳过表达式">
+              <el-input v-model="formData.skipExpression" @change="skipExpressionChange"> </el-input>
+            </el-form-item>
+            <el-form-item v-loading="formManageListLoading" prop="formKey" label="表单地址">
+              <el-select v-model="formData.formKey" clearable filterable placeholder="请选择表单" style="width: 260px" @change="formKeyChange">
+                <el-option
+                  v-for="item in formManageList"
+                  :key="item.id"
+                  :label="item.formTypeName + ':' + item.formName"
+                  :value="item.formType + ':' + item.id"
+                />
+              </el-select>
+            </el-form-item>
+          </div>
+        </el-collapse-item>
+        <el-collapse-item name="2">
+          <template #title>
+            <div class="collapse__title">
+              <el-icon>
+                <Checked />
+              </el-icon>
+              任务
+            </div>
+          </template>
+          <div>
+            <el-form-item v-if="showConfig.async" prop="sync" label="是否异步">
+              <el-switch v-model="formData.async" inline-prompt active-text="是" inactive-text="否" @change="syncChange" />
+            </el-form-item>
+
+            <el-tabs tab-position="left" class="demo-tabs">
+              <el-tab-pane label="身份存储">
+                <el-form-item label="分配人员">
+                  <el-input v-model="formData.assignee" @blur="blurAssignee(formData.assignee)">
+                    <template #append>
+                      <el-button icon="Search" type="primary" @click="openSingleUserSelect" />
+                    </template>
+                  </el-input>
+                </el-form-item>
+                <el-form-item label="候选人员">
+                  <el-badge :value="selectUserLength" :max="99">
+                    <el-button size="small" type="primary" @click="openUserSelect">选择人员</el-button>
+                  </el-badge>
+                </el-form-item>
+                <el-form-item label="候选组">
+                  <el-badge :value="selectRoleLength" :max="99">
+                    <el-button size="small" type="primary" @click="openRoleSelect">选择组</el-button>
+                  </el-badge>
+                </el-form-item>
+              </el-tab-pane>
+
+              <!-- <el-tab-pane label="固定值">
+                <el-form-item prop="auditUserType" label="分配类型">
+                  <el-select v-model="formData.allocationType">
+                    <el-option v-for="item in AllocationTypeSelect" :key="item.id" :value="item.value" :label="item.label"> </el-option>
+                  </el-select>
+                </el-form-item>
+                <el-form-item v-if="formData.allocationType === AllocationTypeEnum.USER" label="分配人员">
+                  <el-input v-model="formData.assignee">
+                    <template #append>
+                      <el-button icon="Search" type="primary" @click="openSingleUserSelect" />
+                    </template>
+                  </el-input>
+                </el-form-item>
+                <div v-if="formData.allocationType === AllocationTypeEnum.CANDIDATE">
+                  <el-form-item label="候选人员">
+                    <el-badge :value="selectUserLength" :max="99">
+                      <el-button size="small" type="primary" @click="openUserSelect">选择人员</el-button>
+                    </el-badge>
+                  </el-form-item>
+                  <el-form-item label="候选组">
+                    <el-badge :value="selectRoleLength" :max="99">
+                      <el-button size="small" type="primary" @click="openRoleSelect">选择组</el-button>
+                    </el-badge>
+                  </el-form-item>
+                </div>
+                <el-form-item v-if="formData.allocationType === AllocationTypeEnum.SPECIFY && showConfig.specifyDesc" style="">
+                  <el-radio-group v-model="formData.specifyDesc" class="ml-4">
+                    <el-radio v-for="item in SpecifyDesc" :key="item.id" :value="item.value" size="large">{{ item.label }}</el-radio>
+                  </el-radio-group>
+                </el-form-item>
+              </el-tab-pane> -->
+            </el-tabs>
+
+            <el-form-item v-if="showConfig.dueDate" prop="dueDate" label="到期时间">
+              <el-input v-model="formData.dueDate" clearable @change="dueDateChange" @click="openDueDate">
+                <template #append>
+                  <el-button icon="Search" type="primary" @click="openDueDate" />
+                </template>
+              </el-input>
+            </el-form-item>
+            <el-form-item v-if="showConfig.priority" prop="priority" label="优先级">
+              <el-input-number v-model="formData.priority" :min="0" @change="priorityChange"> </el-input-number>
+            </el-form-item>
+          </div>
+        </el-collapse-item>
+        <el-collapse-item name="3">
+          <template #title>
+            <div class="collapse__title">
+              <el-icon>
+                <HelpFilled />
+              </el-icon>
+              多实例
+            </div>
+          </template>
+          <div>
+            <el-form-item label="多实例类型">
+              <el-select v-model="formData.multiInstanceType" @change="multiInstanceTypeChange">
+                <el-option v-for="item in constant.MultiInstanceType" :key="item.id" :value="item.value" :label="item.label"> </el-option>
+              </el-select>
+            </el-form-item>
+
+            <div v-if="formData.multiInstanceType !== MultiInstanceTypeEnum.NONE">
+              <el-form-item label="集合">
+                <template #label>
+                  <span>
+                    集合
+                    <el-tooltip placement="top">
+                      <el-icon><QuestionFilled /></el-icon>
+                      <template #content>
+                        属性会作为表达式进行解析。如果表达式解析为字符串而不是一个集合,<br />
+                        不论是因为本身配置的就是静态字符串值,还是表达式计算结果为字符串,<br />
+                        这个字符串都会被当做变量名,并从流程变量中用于获取实际的集合。
+                      </template>
+                    </el-tooltip>
+                  </span>
+                </template>
+                <el-input v-model="formData.collection" @change="collectionChange"></el-input>
+              </el-form-item>
+              <el-form-item label="元素变量">
+                <template #label>
+                  <span>
+                    元素变量
+                    <el-tooltip placement="top">
+                      <el-icon><QuestionFilled /></el-icon>
+                      <template #content>
+                        每创建一个用户任务前,先以该元素变量为label,集合中的一项为value,<br />
+                        创建(局部)流程变量,该局部流程变量被用于指派用户任务。<br />
+                        一般来说,该字符串应与指定人员变量相同。
+                      </template>
+                    </el-tooltip>
+                  </span>
+                </template>
+                <el-input v-model="formData.elementVariable" @change="elementVariableChange"> </el-input>
+              </el-form-item>
+              <el-form-item label="完成条件">
+                <template #label>
+                  <span>
+                    完成条件
+                    <el-tooltip placement="top">
+                      <el-icon><QuestionFilled /></el-icon>
+                      <template #content>
+                        多实例活动在所有实例都完成时结束,然而也可以指定一个表达式,在每个实例<br />
+                        结束时进行计算。当表达式计算为true时,将销毁所有剩余的实例,并结束多实例<br />
+                        活动,继续执行流程。例如 ${nrOfCompletedInstances/nrOfInstances >= 0.6 },<br />
+                        表示当任务完成60%时,该节点就算完成
+                      </template>
+                    </el-tooltip>
+                  </span>
+                </template>
+                <el-input v-model="formData.completionCondition" @change="completionConditionChange"> </el-input>
+              </el-form-item>
+            </div>
+          </div>
+        </el-collapse-item>
+        <el-collapse-item v-if="showConfig.taskListener" name="4">
+          <template #title>
+            <div class="collapse__title">
+              <el-icon>
+                <BellFilled />
+              </el-icon>
+              任务监听器
+            </div>
+          </template>
+          <div>
+            <TaskListener v-if="showConfig.taskListener" :element="element"></TaskListener>
+          </div>
+        </el-collapse-item>
+        <el-collapse-item v-if="showConfig.executionListener" name="5">
+          <template #title>
+            <div class="collapse__title">
+              <el-icon>
+                <BellFilled />
+              </el-icon>
+              执行监听器
+            </div>
+          </template>
+          <div>
+            <ExecutionListener v-if="showConfig.executionListener" :element="element"></ExecutionListener>
+          </div>
+        </el-collapse-item>
+
+        <el-form-item v-if="showConfig.isForCompensation" prop="isForCompensation" label="是否为补偿">
+          <el-switch v-model="formData.isForCompensation" inline-prompt active-text="是" inactive-text="否" />
+        </el-form-item>
+        <el-form-item v-if="showConfig.triggerServiceTask" prop="triggerServiceTask" label="服务任务可触发">
+          <el-switch v-model="formData.triggerServiceTask" inline-prompt active-text="是" inactive-text="否" />
+        </el-form-item>
+        <el-form-item v-if="showConfig.autoStoreVariables" prop="autoStoreVariables" label="自动存储变量">
+          <el-switch v-model="formData.autoStoreVariables" inline-prompt active-text="是" inactive-text="否" />
+        </el-form-item>
+        <el-form-item v-if="showConfig.ruleVariablesInput" prop="skipExpression" label="输入变量">
+          <el-input v-model="formData.ruleVariablesInput"> </el-input>
+        </el-form-item>
+        <el-form-item v-if="showConfig.exclude" prop="exclude" label="排除">
+          <el-switch v-model="formData.exclude" inline-prompt active-text="是" inactive-text="否" />
+        </el-form-item>
+        <el-form-item v-if="showConfig.class" prop="class" label="类">
+          <el-input v-model="formData.class"> </el-input>
+        </el-form-item>
+      </el-collapse>
+    </el-form>
+    <UserSelect ref="userSelectRef" :data="formData.candidateUsers" @confirm-call-back="userSelectCallBack"></UserSelect>
+    <UserSelect ref="singleUserSelectRef" :data="formData.assignee" :multiple="false" @confirm-call-back="singleUserSelectCallBack"></UserSelect>
+    <RoleSelect ref="roleSelectRef" :data="formData.candidateGroups" @confirm-call-back="roleSelectCallBack"></RoleSelect>
+    <DueDate ref="dueDateRef" v-model="formData.dueDate" :data="formData.dueDate" @confirm-call-back="dueDateCallBack"></DueDate>
+  </div>
+</template>
+<script setup lang="ts">
+import useParseElement from '../hooks/useParseElement';
+import usePanel from '../hooks/usePanel';
+import UserSelect from '@/components/UserSelect';
+import RoleSelect from '@/components/RoleSelect';
+import ExecutionListener from './property/ExecutionListener.vue';
+import TaskListener from './property/TaskListener.vue';
+import DueDate from './property/DueDate.vue';
+import { ModdleElement } from 'bpmn';
+import { TaskPanel } from 'bpmnDesign';
+import { AllocationTypeEnum, MultiInstanceTypeEnum, SpecifyDescEnum } from '@/enums/bpmn/IndexEnums';
+import { UserVO } from '@/api/system/user/types';
+import { RoleVO } from '@/api/system/role/types';
+import { selectListFormManage } from '@/api/workflow/formManage';
+import { FormManageVO } from '@/api/workflow/formManage/types';
+const formManageList = ref<FormManageVO[]>([]);
+const formManageListLoading = ref(false);
+interface PropType {
+  element: ModdleElement;
+}
+const props = withDefaults(defineProps<PropType>(), {});
+const { showConfig, nameChange, formKeyChange, idChange, updateProperties, getExtensionElements, createModdleElement, constant } = usePanel({
+  element: toRaw(props.element)
+});
+const { parseData } = useParseElement({
+  element: toRaw(props.element)
+});
+
+const initFormData = {
+  id: '',
+  name: '',
+  dueDate: '',
+  multiInstanceType: MultiInstanceTypeEnum.NONE,
+  allocationType: AllocationTypeEnum.USER,
+  specifyDesc: SpecifyDescEnum.SPECIFY_SINGLE
+};
+const formData = ref({ ...initFormData, ...parseData<TaskPanel>() });
+const assignee = ref<Partial<UserVO>>({
+  userName: ''
+});
+const currentCollapseItem = ref(['1', '2']);
+const userSelectRef = ref<InstanceType<typeof UserSelect>>();
+const singleUserSelectRef = ref<InstanceType<typeof UserSelect>>();
+const roleSelectRef = ref<InstanceType<typeof RoleSelect>>();
+const dueDateRef = ref<InstanceType<typeof DueDate>>();
+
+const isMultiple = ref(true);
+const openUserSelect = () => {
+  userSelectRef.value.open();
+};
+const openSingleUserSelect = () => {
+  if (formData.value.assignee.includes('$')) {
+    formData.value.assignee = '';
+  }
+  singleUserSelectRef.value.open();
+};
+const openRoleSelect = () => {
+  roleSelectRef.value.open();
+};
+const openDueDate = (e) => {
+  dueDateRef.value.openDialog();
+};
+const blurAssignee = (assignee) => {
+  updateProperties({ 'flowable:assignee': assignee ? assignee : undefined });
+};
+const singleUserSelectCallBack = (data: UserVO[]) => {
+  const user: UserVO = data.length !== 0 ? data[0] : undefined;
+  updateProperties({ 'flowable:assignee': user?.userId });
+  assignee.value = user ? user : { userName: '' };
+  formData.value.assignee = String(user?.userId);
+  let extensionElements = getExtensionElements();
+  extensionElements.values = extensionElements.get('values').filter((item) => item.$type !== 'flowable:extAssignee');
+  if (user) {
+    const extAssigneeElement = createModdleElement('flowable:extAssignee', { body: '' }, extensionElements);
+    extensionElements.get('values').push(extAssigneeElement);
+    extAssigneeElement.body = JSON.stringify({ userName: user.userName, userId: user.userId });
+  }
+  if (extensionElements.values.length === 0) {
+    extensionElements = undefined;
+  }
+  updateProperties({ extensionElements: extensionElements });
+};
+const userSelectCallBack = (data: UserVO[]) => {
+  let extensionElements = getExtensionElements();
+  extensionElements.values = extensionElements.values.filter((item) => item.$type !== 'flowable:extCandidateUsers');
+  if (data.length === 0) {
+    formData.value.candidateUsers = undefined;
+    updateProperties({ 'flowable:candidateUsers': undefined });
+  } else {
+    const userIds = data.map((item) => item.userId).join(',');
+    formData.value.candidateUsers = userIds;
+    updateProperties({ 'flowable:candidateUsers': userIds });
+    const extCandidateUsersElement = createModdleElement('flowable:extCandidateUsers', { body: '' }, extensionElements);
+    extensionElements.values.push(extCandidateUsersElement);
+    const users = data.map((item) => {
+      return {
+        userId: item.userId,
+        userName: item.userName
+      };
+    });
+    extCandidateUsersElement.body = JSON.stringify(users);
+  }
+  if (extensionElements.values.length === 0) {
+    extensionElements = undefined;
+  }
+  updateProperties({ extensionElements: extensionElements });
+};
+const roleSelectCallBack = (data: RoleVO[]) => {
+  if (data.length === 0) {
+    formData.value.candidateGroups = '';
+    updateProperties({ 'flowable:candidateGroups': undefined });
+  } else {
+    const roleIds = data.map((item) => item.roleId).join(',');
+    formData.value.candidateGroups = roleIds;
+    updateProperties({ 'flowable:candidateGroups': roleIds });
+  }
+};
+const dueDateCallBack = (data: string) => {
+  updateProperties({ 'flowable:dueDate': data });
+};
+
+const taskTabClick = (e) => {
+  formData.value.candidateGroups = '';
+  formData.value.candidateUsers = '';
+  formData.value.assignee = '';
+  // formData.value.fixedAssignee = '';
+  assignee.value = {};
+};
+
+const syncChange = (newVal) => {
+  updateProperties({ 'flowable:async': newVal });
+};
+const skipExpressionChange = (newVal) => {
+  updateProperties({ 'flowable:skipExpression': newVal && newVal.length > 0 ? newVal : undefined });
+};
+const priorityChange = (newVal) => {
+  updateProperties({ 'flowable:priority': newVal });
+};
+const fixedAssigneeChange = (newVal) => {
+  updateProperties({ 'flowable:assignee': newVal && newVal.length > 0 ? newVal : undefined });
+};
+const multiInstanceTypeChange = (newVal) => {
+  if (newVal !== MultiInstanceTypeEnum.NONE) {
+    let loopCharacteristics = props.element.businessObject.get('loopCharacteristics');
+    if (!loopCharacteristics) {
+      loopCharacteristics = createModdleElement('bpmn:MultiInstanceLoopCharacteristics', {}, props.element.businessObject);
+    }
+    loopCharacteristics.isSequential = newVal === MultiInstanceTypeEnum.SERIAL;
+    updateProperties({ loopCharacteristics: loopCharacteristics });
+  } else {
+    updateProperties({ loopCharacteristics: undefined });
+  }
+};
+const collectionChange = (newVal) => {
+  let loopCharacteristics = props.element.businessObject.get('loopCharacteristics');
+  if (!loopCharacteristics) {
+    loopCharacteristics = createModdleElement('bpmn:MultiInstanceLoopCharacteristics', {}, props.element.businessObject);
+  }
+  loopCharacteristics.collection = newVal && newVal.length > 0 ? newVal : undefined;
+  updateProperties({ loopCharacteristics: loopCharacteristics });
+};
+const elementVariableChange = (newVal) => {
+  let loopCharacteristics = props.element.businessObject.get('loopCharacteristics');
+  if (!loopCharacteristics) {
+    loopCharacteristics = createModdleElement('bpmn:MultiInstanceLoopCharacteristics', {}, props.element.businessObject);
+  }
+  loopCharacteristics.elementVariable = newVal && newVal.length > 0 ? newVal : undefined;
+  updateProperties({ loopCharacteristics: loopCharacteristics });
+};
+const completionConditionChange = (newVal) => {
+  let loopCharacteristics = props.element.businessObject.get<ModdleElement>('loopCharacteristics');
+  if (!loopCharacteristics) {
+    loopCharacteristics = createModdleElement('bpmn:MultiInstanceLoopCharacteristics', {}, props.element.businessObject);
+  }
+  if (newVal && newVal.length > 0) {
+    if (!loopCharacteristics.completionCondition) {
+      loopCharacteristics.completionCondition = createModdleElement('bpmn:Expression', { body: newVal }, loopCharacteristics);
+    } else {
+      loopCharacteristics.completionCondition.body = newVal;
+    }
+  } else {
+    loopCharacteristics.completionCondition = undefined;
+  }
+  updateProperties({ loopCharacteristics: loopCharacteristics });
+};
+const dueDateChange = (newVal) => {
+  updateProperties({ 'flowable:dueDate': newVal && newVal.length > 0 ? newVal : undefined });
+};
+const selectUserLength = computed(() => {
+  if (formData.value.candidateUsers) {
+    return formData.value.candidateUsers.split(',').length;
+  } else {
+    return 0;
+  }
+});
+const selectRoleLength = computed(() => {
+  if (formData.value.candidateGroups) {
+    return formData.value.candidateGroups.split(',').length;
+  } else {
+    return 0;
+  }
+});
+
+onBeforeMount(() => {
+  const extensionElements = getExtensionElements(false);
+  if (extensionElements && extensionElements.get('values')) {
+    let extAssigneeElement = extensionElements.get('values').find((item) => item.$type === 'flowable:extAssignee');
+    if (extAssigneeElement) {
+      assignee.value = JSON.parse(extAssigneeElement.body);
+    }
+  }
+
+  if (formData.value.loopCharacteristics) {
+    const loopCharacteristics = formData.value.loopCharacteristics;
+    formData.value.collection = loopCharacteristics.collection || '';
+    formData.value.elementVariable = loopCharacteristics.elementVariable || '';
+    formData.value.completionCondition = loopCharacteristics.completionCondition?.body || '';
+    formData.value.multiInstanceType = loopCharacteristics.isSequential ? MultiInstanceTypeEnum.SERIAL : MultiInstanceTypeEnum.PARALLEL;
+  }
+
+  if (formData.value.assignee) {
+    formData.value.fixedAssignee = formData.value.assignee;
+  }
+});
+
+const formRules = ref<ElFormRules>({
+  id: [{ required: true, message: '请输入', trigger: 'blur' }],
+  name: [{ required: true, message: '请输入', trigger: 'blur' }]
+});
+
+const AllocationTypeSelect = [
+  { id: 'b9cdf970-dd91-47c0-819f-42a7010ca2a6', label: '指定人员', value: AllocationTypeEnum.USER },
+  { id: '3f7ccbcd-c464-4602-bb9d-e96649d10585', label: '候选人员', value: AllocationTypeEnum.CANDIDATE },
+  { id: 'c49065e0-7f2d-4c09-aedb-ab2d47d9a454', label: '发起人自己', value: AllocationTypeEnum.YOURSELF },
+  { id: '6ef40a03-7e9a-4898-89b2-c88fe9064542', label: '发起人指定', value: AllocationTypeEnum.SPECIFY }
+];
+const SpecifyDesc = [
+  { id: 'fa253b34-4335-458c-b1bc-b039e2a2b7a6', label: '指定一个人', value: 'specifySingle' },
+  { id: '7365ff54-2e05-4312-9bfb-0b8edd779c5b', label: '指定多个人', value: 'specifyMultiple' }
+];
+
+const listFormManage = async () => {
+  formManageListLoading.value = true;
+  const res = await selectListFormManage();
+  formManageList.value = res.data;
+  formManageListLoading.value = false;
+};
+onMounted(() => {
+  nextTick(() => {
+    listFormManage();
+  });
+});
+</script>
+
+<style lang="scss" scoped></style>

+ 110 - 0
src/bpmn/panel/index.vue

@@ -0,0 +1,110 @@
+<template>
+  <div ref="propertyPanel">
+    <div v-if="nodeName" class="node-name">{{ nodeName }}</div>
+    <component :is="component" v-if="element" :element="element" />
+  </div>
+</template>
+<script setup lang="ts" name="PropertyPanel">
+import { NodeName } from '../assets/lang/zh';
+import TaskPanel from './TaskPanel.vue';
+import ProcessPanel from './ProcessPanel.vue';
+import StartEndPanel from './StartEndPanel.vue';
+import GatewayPanel from './GatewayPanel.vue';
+import SequenceFlowPanel from './SequenceFlowPanel.vue';
+import ParticipantPanel from './ParticipantPanel.vue';
+import SubProcessPanel from './SubProcessPanel.vue';
+import { Modeler, ModdleElement } from 'bpmn';
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+interface propsType {
+  modeler: Modeler;
+}
+const props = withDefaults(defineProps<propsType>(), {});
+
+const element = ref<ModdleElement>();
+const processElement = ref<ModdleElement>();
+
+const startEndType = ['bpmn:IntermediateThrowEvent', 'bpmn:StartEvent', 'bpmn:EndEvent'];
+const taskType = [
+  'bpmn:UserTask',
+  'bpmn:Task',
+  'bpmn:SendTask',
+  'bpmn:ReceiveTask',
+  'bpmn:ManualTask',
+  'bpmn:BusinessRuleTask',
+  'bpmn:ServiceTask',
+  'bpmn:ScriptTask'
+];
+const sequenceType = ['bpmn:SequenceFlow'];
+const gatewayType = ['bpmn:InclusiveGateway', 'bpmn:ExclusiveGateway', 'bpmn:ParallelGateway', 'bpmn:EventBasedGateway', 'bpmn:ComplexGateway'];
+const processType = ['bpmn:Process'];
+
+// 组件计算
+const component = computed(() => {
+  if (!element.value) return null;
+  const type = element.value.type;
+  if (startEndType.includes(type)) return StartEndPanel;
+  if (taskType.includes(type)) return TaskPanel;
+  if (sequenceType.includes(type)) return SequenceFlowPanel;
+  if (gatewayType.includes(type)) return GatewayPanel;
+  if (processType.includes(type)) return ProcessPanel;
+  if (type === 'bpmn:Participant') return ParticipantPanel;
+  if (type === 'bpmn:SubProcess') return SubProcessPanel;
+  //return proxy?.$modal.msgWarning('面板开发中....');
+  return undefined;
+});
+
+const nodeName = computed(() => {
+  if (element.value) {
+    const bizObj = element.value.businessObject;
+    const type = bizObj?.eventDefinitions && bizObj?.eventDefinitions.length > 0 ? bizObj.eventDefinitions[0].$type : bizObj.$type;
+    return NodeName[type] || type;
+  }
+  return '';
+});
+
+const handleModeler = () => {
+  props.modeler.on('root.added', (e: any) => {
+    element.value = null;
+    if (e.element.type === 'bpmn:Process') {
+      nextTick(() => {
+        element.value = e.element;
+        processElement.value = e.element;
+      });
+    }
+  });
+  props.modeler.on('element.click', (e: any) => {
+    if (e.element.type === 'bpmn:Process') {
+      nextTick(() => {
+        element.value = e.element;
+        processElement.value = e.element;
+      });
+    }
+  });
+  props.modeler.on('selection.changed', (e: any) => {
+    // 先给null为了让vue刷新
+    element.value = null;
+    const newElement = e.newSelection[0];
+    if (newElement) {
+      nextTick(() => {
+        element.value = newElement;
+      });
+    } else {
+      nextTick(() => {
+        element.value = processElement.value;
+      });
+    }
+  });
+};
+
+onMounted(() => {
+  handleModeler();
+});
+</script>
+
+<style scoped lang="scss">
+.node-name {
+  font-size: 16px;
+  font-weight: bold;
+  padding: 10px;
+}
+</style>

+ 252 - 0
src/bpmn/panel/property/DueDate.vue

@@ -0,0 +1,252 @@
+<template>
+  <div>
+    <el-dialog v-model="visible" :title="title" width="600px" append-to-body>
+      <el-form label-width="100px">
+        <el-form-item label="小时">
+          <el-radio-group v-model="hourValue" @change="hourChange">
+            <el-radio-button label="4" value="4" />
+            <el-radio-button label="8" value="8" />
+            <el-radio-button label="12" value="12" />
+            <el-radio-button label="24" value="24" />
+            <el-radio-button label="自定义" value="自定义" />
+            <el-input-number v-show="hourValue === '自定义'" v-model="customHourValue" :min="1" @change="customHourValueChange"></el-input-number>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="天">
+          <el-radio-group v-model="dayValue" @change="dayChange">
+            <el-radio-button label="1" value="1" />
+            <el-radio-button label="2" value="2" />
+            <el-radio-button label="3" value="3" />
+            <el-radio-button label="4" value="4" />
+            <el-radio-button label="自定义" value="自定义" />
+            <el-input-number v-show="dayValue === '自定义'" v-model="customDayValue" :min="1" @change="customDayValueChange"></el-input-number>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="周">
+          <el-radio-group v-model="weekValue" @change="weekChange">
+            <el-radio-button label="1" value="1" />
+            <el-radio-button label="2" value="2" />
+            <el-radio-button label="3" value="3" />
+            <el-radio-button label="4" value="4" />
+            <el-radio-button label="自定义" value="自定义" />
+            <el-input-number v-show="weekValue === '自定义'" v-model="customWeekValue" :min="1" @change="customWeekValueChange"></el-input-number>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="月">
+          <el-radio-group v-model="monthValue" @change="monthChange">
+            <el-radio-button label="1" value="1" />
+            <el-radio-button label="2" value="2" />
+            <el-radio-button label="3" value="3" />
+            <el-radio-button label="4" value="4" />
+            <el-radio-button label="自定义" value="自定义" />
+            <el-input-number v-show="monthValue === '自定义'" v-model="customMonthValue" :min="1" @change="customMonthValueChange"></el-input-number>
+          </el-radio-group>
+        </el-form-item>
+      </el-form>
+
+      <template #footer>
+        <div>
+          <el-button @click="closeDialog">取消</el-button>
+          <el-button type="primary" @click="confirm">确定</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import useDialog from '@/hooks/useDialog';
+
+interface PropType {
+  modelValue?: string;
+  data?: string;
+}
+const prop = withDefaults(defineProps<PropType>(), {
+  modelValue: '',
+  data: ''
+});
+const emit = defineEmits(['update:modelValue', 'confirmCallBack']);
+
+const { title, visible, openDialog, closeDialog } = useDialog({
+  title: '设置任务到期时间'
+});
+const formValue = ref();
+const valueType = ref();
+
+const hourValue = ref('');
+const dayValue = ref('');
+const weekValue = ref('');
+const monthValue = ref('');
+
+const customHourValue = ref(1);
+const customDayValue = ref(1);
+const customWeekValue = ref(1);
+const customMonthValue = ref(1);
+
+const hourValueConst = ['4', '8', '12', '24'];
+const dayAndWeekAndMonthValueConst = ['1', '2', '3', '4'];
+
+const initValue = () => {
+  formValue.value = prop.data;
+  if (prop.data) {
+    const lastStr = prop.data.substring(prop.data.length - 1);
+    if (lastStr === 'H') {
+      const hourValueValue = prop.data.substring(2, prop.data.length - 1);
+      if (hourValueConst.includes(hourValueValue)) {
+        hourValue.value = hourValueValue;
+      } else {
+        hourValue.value = '自定义';
+        customHourValue.value = Number(hourValueValue);
+      }
+    }
+    const dayAndWeekAndMonthValue = prop.data.substring(1, prop.data.length - 1);
+    if (lastStr === 'D') {
+      if (dayAndWeekAndMonthValueConst.includes(dayAndWeekAndMonthValue)) {
+        dayValue.value = dayAndWeekAndMonthValue;
+      } else {
+        dayValue.value = '自定义';
+        customDayValue.value = Number(dayAndWeekAndMonthValue);
+      }
+    }
+    if (lastStr === 'W') {
+      if (dayAndWeekAndMonthValueConst.includes(dayAndWeekAndMonthValue)) {
+        weekValue.value = dayAndWeekAndMonthValue;
+      } else {
+        weekValue.value = '自定义';
+        customWeekValue.value = Number(dayAndWeekAndMonthValue);
+      }
+    }
+    if (lastStr === 'M') {
+      if (dayAndWeekAndMonthValueConst.includes(dayAndWeekAndMonthValue)) {
+        monthValue.value = dayAndWeekAndMonthValue;
+      } else {
+        monthValue.value = '自定义';
+        customMonthValue.value = Number(dayAndWeekAndMonthValue);
+      }
+    }
+  }
+};
+
+const confirm = () => {
+  emit('update:modelValue', formValue.value);
+  emit('confirmCallBack', formValue.value);
+  closeDialog();
+};
+
+const customHourValueChange = (customHourValue) => {
+  formValue.value = `PT${customHourValue}H`;
+
+  dayValue.value = '';
+  weekValue.value = '';
+  monthValue.value = '';
+  customDayValue.value = 1;
+  customWeekValue.value = 1;
+  customMonthValue.value = 1;
+};
+const customDayValueChange = (customDayValue) => {
+  formValue.value = `P${customDayValue}D`;
+  hourValue.value = '';
+  weekValue.value = '';
+  monthValue.value = '';
+
+  customHourValue.value = 1;
+  customWeekValue.value = 1;
+  customMonthValue.value = 1;
+};
+
+const customWeekValueChange = (customWeekValue) => {
+  formValue.value = `P${customWeekValue}W`;
+  hourValue.value = '';
+  dayValue.value = '';
+  monthValue.value = '';
+
+  customHourValue.value = 1;
+  customDayValue.value = 1;
+  customMonthValue.value = 1;
+};
+
+const customMonthValueChange = (customMonthValue) => {
+  formValue.value = `P${customMonthValue}M`;
+  hourValue.value = '';
+  dayValue.value = '';
+  weekValue.value = '';
+
+  customHourValue.value = 1;
+  customDayValue.value = 1;
+  customWeekValue.value = 1;
+};
+
+const hourChange = (hourValue) => {
+  if (hourValue === '自定义') {
+    formValue.value = `PT${customHourValue.value}H`;
+  } else {
+    formValue.value = `PT${hourValue}H`;
+  }
+
+  dayValue.value = '';
+  weekValue.value = '';
+  monthValue.value = '';
+  customDayValue.value = 1;
+  customWeekValue.value = 1;
+  customMonthValue.value = 1;
+};
+const dayChange = (dayValue) => {
+  if (dayValue === '自定义') {
+    formValue.value = `P${customDayValue.value}D`;
+  } else {
+    formValue.value = `P${dayValue}D`;
+  }
+
+  hourValue.value = '';
+  weekValue.value = '';
+  monthValue.value = '';
+
+  customHourValue.value = 1;
+  customWeekValue.value = 1;
+  customMonthValue.value = 1;
+};
+const weekChange = (weekValue) => {
+  if (weekValue === '自定义') {
+    formValue.value = `P${customWeekValue.value}W`;
+  } else {
+    formValue.value = `P${weekValue}W`;
+  }
+
+  hourValue.value = '';
+  dayValue.value = '';
+  monthValue.value = '';
+
+  customHourValue.value = 1;
+  customDayValue.value = 1;
+  customMonthValue.value = 1;
+};
+const monthChange = (monthValue) => {
+  if (monthValue === '自定义') {
+    formValue.value = `P${customMonthValue.value}M`;
+  } else {
+    formValue.value = `P${monthValue}M`;
+  }
+
+  hourValue.value = '';
+  dayValue.value = '';
+  weekValue.value = '';
+
+  customHourValue.value = 1;
+  customDayValue.value = 1;
+  customWeekValue.value = 1;
+};
+
+watch(
+  () => visible.value,
+  () => {
+    if (visible.value) {
+      initValue();
+    }
+  }
+);
+
+defineExpose({
+  openDialog,
+  closeDialog
+});
+</script>

+ 308 - 0
src/bpmn/panel/property/ExecutionListener.vue

@@ -0,0 +1,308 @@
+<template>
+  <div>
+    <vxe-toolbar>
+      <template #buttons>
+        <el-button type="primary" link size="small" @click="insertEvent">新增</el-button>
+        <el-button type="primary" link size="small" @click="removeSelectRowEvent">删除</el-button>
+      </template>
+    </vxe-toolbar>
+    <vxe-table
+      ref="tableRef"
+      size="mini"
+      height="100px"
+      border
+      show-overflow
+      keep-source
+      :data="tableData"
+      :menu-config="menuConfig"
+      @cell-dblclick="cellDBLClickEvent"
+      @menu-click="contextMenuClickEvent"
+    >
+      <vxe-column type="checkbox" width="40"></vxe-column>
+      <vxe-column type="seq" width="40"></vxe-column>
+      <vxe-column field="event" title="事件" min-width="100px">
+        <template #default="slotParams">
+          <span>{{ eventSelect.find((e) => e.value === slotParams.row.event)?.label }}</span>
+        </template>
+      </vxe-column>
+      <vxe-column field="type" title="类型" min-width="100px">
+        <template #default="slotParams">
+          <span>{{ typeSelect.find((e) => e.value === slotParams.row.type)?.label }}</span>
+        </template>
+      </vxe-column>
+      <vxe-column field="className" title="Java 类名" min-width="100px"> </vxe-column>
+    </vxe-table>
+
+    <el-dialog
+      v-model="formDialog.visible.value"
+      :title="formDialog.title.value"
+      width="600px"
+      :close-on-click-modal="false"
+      :close-on-press-escape="false"
+      :show-close="false"
+      append-to-body
+    >
+      <el-form ref="formRef" :model="formData" :rules="tableRules" label-width="100px">
+        <el-form-item label="事件" prop="event">
+          <el-select v-model="formData.event">
+            <el-option v-for="item in eventSelect" :key="item.id" :value="item.value" :label="item.label"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="类型" prop="type">
+          <template #label>
+            <span>
+              类型
+              <el-tooltip placement="top">
+                <el-icon><QuestionFilled /></el-icon>
+                <template #content>
+                  类:示例 com.company.MyCustomListener,自定义类必须实现 org.flowable.engine.delegate.TaskListener 接口<br />
+                  表达式:示例 ${myObject.callMethod(task, task.eventName)}<br />
+                  委托表达式:示例 ${myListenerSpringBean} ,该 springBean 需要实现 org.flowable.engine.delegate.TaskListener 接口
+                </template>
+              </el-tooltip>
+            </span>
+          </template>
+          <el-select v-model="formData.type">
+            <el-option v-for="item in typeSelect" :key="item.id" :value="item.value" :label="item.label"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item
+          :label="typeSelect.filter((e) => e.value === formData.type)[0] ? typeSelect.filter((e) => e.value === formData.type)[0]?.label : '表达式'"
+          prop="className"
+        >
+          <el-input v-model="formData.className" type="text"></el-input>
+        </el-form-item>
+      </el-form>
+      <el-tabs type="border-card">
+        <el-tab-pane label="参数">
+          <ListenerParam ref="listenerParamRef" :table-data="formData.params" />
+        </el-tab-pane>
+      </el-tabs>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="formDialog.closeDialog">取 消</el-button>
+          <el-button type="primary" @click="submitEvent">确 定</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+<script setup lang="ts">
+import ListenerParam from './ListenerParam.vue';
+import { VxeTableEvents, VxeTableInstance, VxeTablePropTypes } from 'vxe-table';
+import { ExecutionListenerVO } from 'bpmnDesign';
+import { Moddle, Modeler, ModdleElement } from 'bpmn';
+
+import usePanel from '../../hooks/usePanel';
+import useDialog from '@/hooks/useDialog';
+import useModelerStore from '@/store/modules/modeler';
+
+const emit = defineEmits(['close']);
+interface PropType {
+  element: ModdleElement;
+}
+const props = withDefaults(defineProps<PropType>(), {});
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const selectRow = ref<ExecutionListenerVO | null>();
+const formDialog = useDialog({
+  title: selectRow.value ? '编辑&保存' : '新增&保存'
+});
+
+const { showConfig, elementType, updateProperties } = usePanel({
+  element: toRaw(props.element)
+});
+const { getModdle } = useModelerStore();
+const moddle = getModdle();
+
+const listenerParamRef = ref<InstanceType<typeof ListenerParam>>();
+const tableRef = ref<VxeTableInstance<ExecutionListenerVO>>();
+const formRef = ref<ElFormInstance>();
+
+const initData: ExecutionListenerVO = {
+  event: '',
+  type: '',
+  className: '',
+  params: []
+};
+const formData = ref<ExecutionListenerVO>({ ...initData });
+const tableData = ref<ExecutionListenerVO[]>([]);
+const tableRules = ref<ElFormRules>({
+  event: [{ required: true, message: '请选择', trigger: 'blur' }],
+  type: [{ required: true, message: '请选择', trigger: 'blur' }],
+  className: [{ required: true, message: '请输入', trigger: 'blur' }]
+});
+
+const submitEvent = async () => {
+  const error = await listenerParamRef.value.validate();
+  await formRef.value.validate((validate) => {
+    if (validate && !error) {
+      const $table = tableRef.value;
+      if ($table) {
+        formData.value.params = listenerParamRef.value.getTableData();
+        if (selectRow.value) {
+          Object.assign(selectRow.value, formData.value);
+        } else {
+          $table.insertAt({ ...formData.value }, -1);
+        }
+        updateElement();
+        formDialog.closeDialog();
+      }
+    }
+  });
+};
+
+const removeSelectRowEvent = async () => {
+  const $table = tableRef.value;
+  if ($table) {
+    const selectCount = $table.getCheckboxRecords().length;
+    if (selectCount === 0) {
+      proxy?.$modal.msgWarning('请选择行');
+    } else {
+      await $table.removeCheckboxRow();
+      updateElement();
+    }
+  }
+};
+const insertEvent = async () => {
+  Object.assign(formData.value, initData);
+  selectRow.value = null;
+  formDialog.openDialog();
+};
+
+const editEvent = (row: ExecutionListenerVO) => {
+  Object.assign(formData.value, row);
+  selectRow.value = row;
+  formDialog.openDialog();
+};
+
+const removeEvent = async (row: ExecutionListenerVO) => {
+  await proxy?.$modal.confirm('您确定要删除该数据?');
+  const $table = tableRef.value;
+  if ($table) {
+    await $table.remove(row);
+    updateElement();
+  }
+};
+const updateElement = () => {
+  const $table = tableRef.value;
+  const data = $table.getTableData().fullData;
+  if (data.length) {
+    let extensionElements = props.element.businessObject.get('extensionElements');
+    if (!extensionElements) {
+      extensionElements = moddle.create('bpmn:ExtensionElements');
+    }
+    // 清除旧值
+    extensionElements.values = extensionElements.values?.filter((item) => item.$type !== 'flowable:ExecutionListener') ?? [];
+    data.forEach((item) => {
+      const executionListener = moddle.create('flowable:ExecutionListener');
+      executionListener['event'] = item.event;
+      executionListener[item.type] = item.className;
+      if (item.params && item.params.length) {
+        item.params.forEach((field) => {
+          const fieldElement = moddle.create('flowable:Field');
+          fieldElement['name'] = field.name;
+          fieldElement[field.type] = field.value;
+          executionListener.get('fields').push(fieldElement);
+        });
+      }
+      extensionElements.get('values').push(executionListener);
+    });
+    updateProperties({ extensionElements: extensionElements });
+  } else {
+    const extensionElements = props.element.businessObject[`extensionElements`];
+    if (extensionElements) {
+      extensionElements.values = extensionElements.values?.filter((item) => item.$type !== 'flowable:ExecutionListener') ?? [];
+    }
+  }
+};
+
+const cellDBLClickEvent: VxeTableEvents.CellDblclick<ExecutionListenerVO> = ({ row }) => {
+  editEvent(row);
+};
+
+const menuConfig = reactive<VxeTablePropTypes.MenuConfig<ExecutionListenerVO>>({
+  body: {
+    options: [
+      [
+        { code: 'edit', name: '编辑', prefixIcon: 'vxe-icon-edit', disabled: false },
+        { code: 'remove', name: '删除', prefixIcon: 'vxe-icon-delete', disabled: false }
+      ]
+    ]
+  },
+  visibleMethod({ options, column }) {
+    const isDisabled = !column;
+    options.forEach((list) => {
+      list.forEach((item) => {
+        item.disabled = isDisabled;
+      });
+    });
+    return true;
+  }
+});
+const contextMenuClickEvent: VxeTableEvents.MenuClick<ExecutionListenerVO> = ({ menu, row, column }) => {
+  const $table = tableRef.value;
+  if ($table) {
+    switch (menu.code) {
+      case 'edit':
+        editEvent(row);
+        break;
+      case 'remove':
+        removeEvent(row);
+        break;
+    }
+  }
+};
+
+const initTableData = () => {
+  tableData.value =
+    props.element.businessObject.extensionElements?.values
+      .filter((item) => item.$type === 'flowable:ExecutionListener')
+      .map((item) => {
+        let type;
+        if ('class' in item) type = 'class';
+        if ('expression' in item) type = 'expression';
+        if ('delegateExpression' in item) type = 'delegateExpression';
+        return {
+          event: item.event,
+          type: type,
+          className: item[type],
+          params:
+            item.fields?.map((field) => {
+              let fieldType;
+              if ('stringValue' in field) fieldType = 'stringValue';
+              if ('expression' in field) fieldType = 'expression';
+              return {
+                name: field.name,
+                type: fieldType,
+                value: field[fieldType]
+              };
+            }) ?? []
+        };
+      }) ?? [];
+};
+
+onMounted(() => {
+  initTableData();
+});
+
+const typeSelect = [
+  { id: '742fdeb7-23b4-416b-ac66-cd4ec8b901b7', label: '类', value: 'class' },
+  { id: '660c9c46-8fae-4bae-91a0-0335420019dc', label: '表达式', value: 'expression' },
+  { id: '4b8135ab-6bc3-4a0f-80be-22f58bc6c5fd', label: '委托表达式', value: 'delegateExpression' }
+];
+const eventSelect = [
+  { id: 'e6e0a51a-2d5d-4dc4-b847-b5c14f43a6ab', label: '开始', value: 'start' },
+  { id: '6da97c1e-15fc-4445-8943-75d09f49778e', label: '结束', value: 'end' },
+  { id: '6a2cbcec-e026-4f11-bef7-fff0b5c871e2', label: '启用', value: 'take' }
+];
+</script>
+
+<style scoped lang="scss">
+.el-badge {
+  :deep(.el-badge__content) {
+    top: 10px;
+  }
+}
+</style>

+ 121 - 0
src/bpmn/panel/property/ListenerParam.vue

@@ -0,0 +1,121 @@
+<template>
+  <vxe-toolbar>
+    <template #buttons>
+      <el-button icon="Plus" @click="insertRow">新增</el-button>
+    </template>
+  </vxe-toolbar>
+  <vxe-table
+    ref="tableRef"
+    :height="height"
+    border
+    show-overflow
+    keep-source
+    :data="tableData"
+    :edit-rules="tableRules"
+    :edit-config="{ trigger: 'click', mode: 'row', showStatus: true }"
+  >
+    <vxe-column type="seq" width="40"></vxe-column>
+    <vxe-column field="type" title="类型" :edit-render="{}">
+      <template #default="slotParams">
+        <span>{{ typeSelect.find((e) => e.value === slotParams.row.type)?.label }}</span>
+      </template>
+      <template #edit="slotParams">
+        <vxe-select v-model="slotParams.row.type">
+          <vxe-option v-for="item in typeSelect" :key="item.id" :value="item.value" :label="item.label"></vxe-option>
+        </vxe-select>
+      </template>
+    </vxe-column>
+    <vxe-column field="name" title="名称" :edit-render="{}">
+      <template #edit="slotParams">
+        <vxe-input v-model="slotParams.row.name" type="text"></vxe-input>
+      </template>
+    </vxe-column>
+    <vxe-column field="value" title="值" :edit-render="{}">
+      <template #edit="slotParams">
+        <vxe-input v-model="slotParams.row.value" type="text"></vxe-input>
+      </template>
+    </vxe-column>
+    <vxe-column title="操作" width="100" show-overflow align="center">
+      <template #default="slotParams">
+        <el-tooltip content="删除" placement="top">
+          <el-button link type="danger" icon="Delete" @click="removeRow(slotParams.row)"></el-button>
+        </el-tooltip>
+      </template>
+    </vxe-column>
+  </vxe-table>
+</template>
+
+<script setup lang="ts">
+import { VXETable, VxeTableInstance, VxeTablePropTypes } from 'vxe-table';
+import { ParamVO } from 'bpmnDesign';
+import useDialog from '@/hooks/useDialog';
+
+interface PropType {
+  height?: string;
+  tableData?: ParamVO[];
+}
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const props = withDefaults(defineProps<PropType>(), {
+  height: '200px',
+  tableData: () => []
+});
+
+const tableRules = ref<VxeTablePropTypes.EditRules>({
+  type: [{ required: true, message: '请选择', trigger: 'blur' }],
+  name: [{ required: true, message: '请输入', trigger: 'blur' }],
+  value: [{ required: true, message: '请输入', trigger: 'blur' }]
+});
+
+const { title, visible, openDialog, closeDialog } = useDialog({
+  title: '监听器参数'
+});
+const typeSelect = [
+  { id: '742fdeb7-23b4-416b-ac66-cd4ec8b901b7', label: '字符串', value: 'stringValue' },
+  { id: '660c9c46-8fae-4bae-91a0-0335420019dc', label: '表达式', value: 'expression' }
+];
+
+const tableRef = ref<VxeTableInstance<ParamVO>>();
+
+const getTableData = () => {
+  const $table = tableRef.value;
+  if ($table) {
+    return $table.getTableData().fullData;
+  }
+  return [];
+};
+
+const insertRow = async () => {
+  const $table = tableRef.value;
+  if ($table) {
+    const { row: newRow } = await $table.insertAt({}, -1);
+    // 插入一条数据并触发校验
+    await $table.validate(newRow);
+  }
+};
+
+const removeRow = async (row: ParamVO) => {
+  await proxy?.$modal.confirm('您确定要删除该数据?');
+  const $table = tableRef.value;
+  if ($table) {
+    await $table.remove(row);
+  }
+};
+
+const validate = async () => {
+  const $table = tableRef.value;
+  if ($table) {
+    return await $table.validate(true);
+  }
+};
+
+defineExpose({
+  closeDialog,
+  openDialog,
+  validate,
+  getTableData
+});
+</script>
+
+<style scoped lang="scss"></style>

+ 310 - 0
src/bpmn/panel/property/TaskListener.vue

@@ -0,0 +1,310 @@
+<template>
+  <div>
+    <vxe-toolbar>
+      <template #buttons>
+        <el-button type="primary" link size="small" @click="insertEvent">新增</el-button>
+        <el-button type="primary" link size="small" @click="removeSelectRowEvent">删除</el-button>
+      </template>
+    </vxe-toolbar>
+    <vxe-table
+      ref="tableRef"
+      size="mini"
+      height="100px"
+      border
+      show-overflow
+      keep-source
+      :data="tableData"
+      :menu-config="menuConfig"
+      @cell-dblclick="cellDBLClickEvent"
+      @menu-click="contextMenuClickEvent"
+    >
+      <vxe-column type="checkbox" width="40"></vxe-column>
+      <vxe-column type="seq" width="40"></vxe-column>
+      <vxe-column field="event" title="事件" min-width="100px">
+        <template #default="slotParams">
+          <span>{{ eventSelect.find((e) => e.value === slotParams.row.event)?.label }}</span>
+        </template>
+      </vxe-column>
+      <vxe-column field="type" title="类型" min-width="100px">
+        <template #default="slotParams">
+          <span>{{ typeSelect.find((e) => e.value === slotParams.row.type)?.label }}</span>
+        </template>
+      </vxe-column>
+      <vxe-column field="className" title="Java 类名" min-width="100px"> </vxe-column>
+    </vxe-table>
+
+    <el-dialog
+      v-model="formDialog.visible.value"
+      :title="formDialog.title.value"
+      width="600px"
+      :close-on-click-modal="false"
+      :close-on-press-escape="false"
+      :show-close="false"
+      append-to-body
+    >
+      <el-form ref="formRef" :model="formData" :rules="tableRules" label-width="100px">
+        <el-form-item label="事件" prop="event">
+          <template #label>
+            <span>
+              事件
+              <el-tooltip placement="top">
+                <el-icon><QuestionFilled /></el-icon>
+                <template #content>
+                  create(创建):当任务已经创建,并且所有任务参数都已经设置时触发。<br />
+                  assignment(指派):当任务已经指派给某人时触发。请注意:当流程执行到达用户任务时,在触发create事件之前,会首先触发assignment事件。<br />
+                  complete(完成):当任务已经完成,从运行时数据中删除前触发。<br />
+                  delete(删除):在任务即将被删除前触发。请注意任务由completeTask正常完成时也会触发。
+                </template>
+              </el-tooltip>
+            </span>
+          </template>
+          <el-select v-model="formData.event">
+            <el-option v-for="item in eventSelect" :key="item.id" :value="item.value" :label="item.label"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="类型" prop="type">
+          <el-select v-model="formData.type">
+            <el-option v-for="item in typeSelect" :key="item.id" :value="item.value" :label="item.label"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item
+          :label="typeSelect.filter((e) => e.value === formData.type)[0] ? typeSelect.filter((e) => e.value === formData.type)[0]?.label : '表达式'"
+          prop="className"
+        >
+          <el-input v-model="formData.className" type="text"></el-input>
+        </el-form-item>
+      </el-form>
+      <el-tabs type="border-card">
+        <el-tab-pane label="参数">
+          <ListenerParam ref="listenerParamRef" :table-data="formData.params" />
+        </el-tab-pane>
+      </el-tabs>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="formDialog.closeDialog">取 消</el-button>
+          <el-button type="primary" @click="submitEvent">确 定</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+<script setup lang="ts">
+import ListenerParam from './ListenerParam.vue';
+import { VxeTableEvents, VxeTableInstance, VxeTablePropTypes } from 'vxe-table';
+import { TaskListenerVO } from 'bpmnDesign';
+import { ModdleElement } from 'bpmn';
+
+import usePanel from '../../hooks/usePanel';
+import useDialog from '@/hooks/useDialog';
+import useModelerStore from '@/store/modules/modeler';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+interface PropType {
+  element: ModdleElement;
+}
+const props = withDefaults(defineProps<PropType>(), {});
+
+const selectRow = ref<TaskListenerVO | null>();
+const formDialog = useDialog({
+  title: selectRow.value ? '编辑&保存' : '新增&保存'
+});
+const { showConfig, elementType, updateProperties } = usePanel({
+  element: toRaw(props.element)
+});
+const { getModdle } = useModelerStore();
+const moddle = getModdle();
+
+const listenerParamRef = ref<InstanceType<typeof ListenerParam>>();
+const tableRef = ref<VxeTableInstance<TaskListenerVO>>();
+const formRef = ref<ElFormInstance>();
+
+const initData: TaskListenerVO = {
+  event: '',
+  type: '',
+  className: '',
+  name: '',
+  params: []
+};
+const formData = ref<TaskListenerVO>({ ...initData });
+const currentIndex = ref(0);
+const tableData = ref<TaskListenerVO[]>([]);
+const tableRules = ref<VxeTablePropTypes.EditRules>({
+  event: [{ required: true, message: '请选择', trigger: 'blur' }],
+  type: [{ required: true, message: '请选择', trigger: 'blur' }],
+  name: [{ required: true, message: '请输入', trigger: 'blur' }],
+  className: [{ required: true, message: '请输入', trigger: 'blur' }]
+});
+
+const submitEvent = async () => {
+  const error = await listenerParamRef.value.validate();
+  await formRef.value.validate((validate) => {
+    if (validate && !error) {
+      const $table = tableRef.value;
+      if ($table) {
+        formData.value.params = listenerParamRef.value.getTableData();
+        if (selectRow.value) {
+          Object.assign(selectRow.value, formData.value);
+        } else {
+          $table.insertAt({ ...formData.value }, -1);
+        }
+        updateElement();
+        formDialog.closeDialog();
+      }
+    }
+  });
+};
+
+const insertEvent = async () => {
+  Object.assign(formData.value, initData);
+  selectRow.value = null;
+  formDialog.openDialog();
+};
+
+const editEvent = (row: TaskListenerVO) => {
+  Object.assign(formData.value, row);
+  selectRow.value = row;
+  formDialog.openDialog();
+};
+const removeEvent = async (row: TaskListenerVO) => {
+  await proxy?.$modal.confirm('您确定要删除该数据?');
+  const $table = tableRef.value;
+  if ($table) {
+    await $table.remove(row);
+    updateElement();
+  }
+};
+
+const removeSelectRowEvent = async () => {
+  const $table = tableRef.value;
+  if ($table) {
+    const selectCount = $table.getCheckboxRecords().length;
+    if (selectCount === 0) {
+      proxy?.$modal.msgWarning('请选择行');
+    } else {
+      await $table.removeCheckboxRow();
+      updateElement();
+    }
+  }
+};
+const updateElement = () => {
+  const $table = tableRef.value;
+  const data = $table.getTableData().fullData;
+  if (data.length) {
+    let extensionElements = props.element.businessObject.get('extensionElements');
+    if (!extensionElements) {
+      extensionElements = moddle.create('bpmn:ExtensionElements');
+    }
+    // 清除旧值
+    extensionElements.values = extensionElements.values?.filter((item) => item.$type !== 'flowable:TaskListener') ?? [];
+    data.forEach((item) => {
+      const taskListener = moddle.create('flowable:TaskListener');
+      taskListener['event'] = item.event;
+      taskListener[item.type] = item.className;
+      if (item.params && item.params.length) {
+        item.params.forEach((field) => {
+          const fieldElement = moddle.create('flowable:Field');
+          fieldElement['name'] = field.name;
+          fieldElement[field.type] = field.value;
+          taskListener.get('fields').push(fieldElement);
+        });
+      }
+      extensionElements.get('values').push(taskListener);
+    });
+    updateProperties({ extensionElements: extensionElements });
+  } else {
+    const extensionElements = props.element.businessObject[`extensionElements`];
+    if (extensionElements) {
+      extensionElements.values = extensionElements.values?.filter((item) => item.$type !== 'flowable:TaskListener') ?? [];
+    }
+  }
+};
+
+const cellDBLClickEvent: VxeTableEvents.CellDblclick<TaskListenerVO> = ({ row }) => {
+  editEvent(row);
+};
+
+const menuConfig = reactive<VxeTablePropTypes.MenuConfig<TaskListenerVO>>({
+  body: {
+    options: [
+      [
+        { code: 'edit', name: '编辑', prefixIcon: 'vxe-icon-edit', disabled: false },
+        { code: 'remove', name: '删除', prefixIcon: 'vxe-icon-delete', disabled: false }
+      ]
+    ]
+  },
+  visibleMethod({ options, column }) {
+    const isDisabled = !column;
+    options.forEach((list) => {
+      list.forEach((item) => {
+        item.disabled = isDisabled;
+      });
+    });
+    return true;
+  }
+});
+const contextMenuClickEvent: VxeTableEvents.MenuClick<TaskListenerVO> = ({ menu, row, column }) => {
+  const $table = tableRef.value;
+  if ($table) {
+    switch (menu.code) {
+      case 'edit':
+        editEvent(row);
+        break;
+      case 'remove':
+        removeEvent(row);
+        break;
+    }
+  }
+};
+const initTableData = () => {
+  tableData.value =
+    props.element.businessObject.extensionElements?.values
+      .filter((item) => item.$type === 'flowable:TaskListener')
+      .map((item) => {
+        let type;
+        if ('class' in item) type = 'class';
+        if ('expression' in item) type = 'expression';
+        if ('delegateExpression' in item) type = 'delegateExpression';
+        return {
+          event: item.event,
+          type: type,
+          className: item[type],
+          params:
+            item.fields?.map((field) => {
+              let fieldType;
+              if ('stringValue' in field) fieldType = 'stringValue';
+              if ('expression' in field) fieldType = 'expression';
+              return {
+                name: field.name,
+                type: fieldType,
+                value: field[fieldType]
+              };
+            }) ?? []
+        };
+      }) ?? [];
+};
+
+onMounted(() => {
+  initTableData();
+});
+
+const typeSelect = [
+  { id: '742fdeb7-23b4-416b-ac66-cd4ec8b901b7', label: '类', value: 'class' },
+  { id: '660c9c46-8fae-4bae-91a0-0335420019dc', label: '表达式', value: 'expression' },
+  { id: '4b8135ab-6bc3-4a0f-80be-22f58bc6c5fd', label: '委托表达式', value: 'delegateExpression' }
+];
+const eventSelect = [
+  { id: 'e6e0a51a-2d5d-4dc4-b847-b5c14f43a6ab', label: '创建', value: 'create' },
+  { id: '6da97c1e-15fc-4445-8943-75d09f49778e', label: '指派', value: 'assignment' },
+  { id: '6a2cbcec-e026-4f11-bef7-fff0b5c871e2', label: '完成', value: 'complete' },
+  { id: '68801972-85f1-482f-bd86-1fad015c26ed', label: '删除', value: 'delete' }
+];
+</script>
+
+<style scoped lang="scss">
+.el-badge {
+  :deep(.el-badge__content) {
+    top: 10px;
+  }
+}
+</style>

+ 71 - 0
src/components/BpmnDesign/index.vue

@@ -0,0 +1,71 @@
+<template>
+  <div class="design">
+    <el-dialog v-model="visible" width="100%" fullscreen :title="title">
+      <div class="modeler">
+        <bpmn-design ref="bpmnDesignRef" @save-call-back="saveCallBack"></bpmn-design>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script lang="ts" setup name="Design">
+import { getInfo, editModelXml } from '@/api/workflow/model';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+import { ModelForm } from '@/api/workflow/model/types';
+import BpmnDesign from '@/bpmn/index.vue';
+import useDialog from '@/hooks/useDialog';
+const bpmnDesignRef = ref<InstanceType<typeof BpmnDesign>>();
+const modelForm = ref<ModelForm>();
+const emit = defineEmits(['closeCallBack']);
+const { visible, title } = useDialog({
+  title: '编辑流程'
+});
+const modelId = ref('');
+const open = async (id) => {
+  visible.value = true;
+  modelId.value = id;
+  const { data } = await getInfo(id);
+  modelForm.value = data;
+  bpmnDesignRef.value.initDiagram(modelForm.value.xml);
+};
+//保存模型
+const saveCallBack = async (data) => {
+  await proxy?.$modal.confirm('是否确认保存?');
+  data.loading.value = true;
+  modelForm.value.id = modelId.value;
+  modelForm.value.xml = data.xml;
+  modelForm.value.svg = data.svg;
+  modelForm.value.key = data.key;
+  modelForm.value.name = data.name;
+  editModelXml(modelForm.value).then((res) => {
+    if (res.code === 200) {
+      visible.value = false;
+      proxy?.$modal.msgSuccess('保存成功');
+      emit('closeCallBack', data);
+    }
+  });
+  data.loading.value = false;
+};
+
+/**
+ * 对外暴露子组件方法
+ */
+defineExpose({
+  open
+});
+</script>
+
+<style lang="scss" scoped>
+.design {
+  :deep(.el-dialog .el-dialog__body) {
+    max-height: 100% !important;
+    min-height: calc(100vh - 80px);
+    padding: 10px 0 10px 0 !important;
+  }
+  :deep(.el-dialog__header) {
+    padding: 0 0 5px 0 !important;
+  }
+}
+</style>

+ 410 - 0
src/components/BpmnView/index.vue

@@ -0,0 +1,410 @@
+<template>
+  <div v-loading="loading" class="bpmnDialogContainers">
+    <el-header style="border-bottom: 1px solid rgb(218 218 218); height: auto">
+      <div class="header-div">
+        <div>
+          <el-tooltip effect="dark" content="自适应屏幕" placement="bottom">
+            <el-button size="small" icon="Rank" @click="fitViewport" />
+          </el-tooltip>
+          <el-tooltip effect="dark" content="放大" placement="bottom">
+            <el-button size="small" icon="ZoomIn" @click="zoomViewport(true)" />
+          </el-tooltip>
+          <el-tooltip effect="dark" content="缩小" placement="bottom">
+            <el-button size="small" icon="ZoomOut" @click="zoomViewport(false)" />
+          </el-tooltip>
+        </div>
+        <div>
+          <div class="tips-label">
+            <div class="un-complete">未完成</div>
+            <div class="in-progress">进行中</div>
+            <div class="complete">已完成</div>
+          </div>
+        </div>
+      </div>
+    </el-header>
+    <div class="flow-containers">
+      <el-container class="bpmn-el-container" style="align-items: stretch">
+        <el-main style="padding: 0">
+          <div ref="canvas" class="canvas" />
+        </el-main>
+      </el-container>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import BpmnViewer from 'bpmn-js/lib/Viewer';
+import MoveCanvasModule from 'diagram-js/lib/navigation/movecanvas';
+import ZoomScrollModule from 'diagram-js/lib/navigation/zoomscroll';
+import { ModuleDeclaration } from 'didi';
+import { Canvas, ModdleElement } from 'bpmn';
+import EventBus from 'diagram-js/lib/core/EventBus';
+import Overlays from 'diagram-js/lib/features/overlays/Overlays';
+import processApi from '@/api/workflow/processInstance/index';
+
+const canvas = ref<HTMLElement>();
+const modeler = ref<BpmnViewer>();
+const taskList = ref([]);
+const zoom = ref(1);
+const xml = ref('');
+const loading = ref(false);
+const bpmnVisible = ref(true);
+const historyList = ref([]);
+
+const init = (instanceId) => {
+  loading.value = true;
+  bpmnVisible.value = true;
+  nextTick(async () => {
+    if (modeler.value) modeler.value.destroy();
+    modeler.value = new BpmnViewer({
+      container: canvas.value,
+      additionalModules: [
+        {
+          //禁止滚轮滚动
+          zoomScroll: ['value', '']
+        },
+        ZoomScrollModule,
+        MoveCanvasModule
+      ] as ModuleDeclaration[]
+    });
+    const resp = await processApi.getHistoryList(instanceId);
+    xml.value = resp.data.xml;
+    taskList.value = resp.data.taskList;
+    historyList.value = resp.data.historyList;
+    await createDiagram(xml.value);
+    loading.value = false;
+  });
+};
+
+const initXml = (xmlStr: string) => {
+  loading.value = true;
+  bpmnVisible.value = true;
+  nextTick(async () => {
+    if (modeler.value) modeler.value.destroy();
+    modeler.value = new BpmnViewer({
+      container: canvas.value,
+      additionalModules: [
+        {
+          //禁止滚轮滚动
+          zoomScroll: ['value', '']
+        },
+        ZoomScrollModule,
+        MoveCanvasModule
+      ] as ModuleDeclaration[]
+    });
+    xml.value = xmlStr;
+    await createDiagram(xml.value);
+    loading.value = false;
+  });
+};
+
+const createDiagram = async (data) => {
+  try {
+    await modeler.value.importXML(data);
+    fitViewport();
+    fillColor();
+    loading.value = false;
+    addEventBusListener();
+  } catch (err) {
+    console.log(err);
+  }
+};
+const addEventBusListener = () => {
+  const eventBus = modeler.value.get<EventBus>('eventBus');
+  const overlays = modeler.value.get<Overlays>('overlays');
+  eventBus.on<ModdleElement>('element.hover', (e) => {
+    let data = historyList.value.find((t) => t.taskDefinitionKey === e.element.id);
+    if (e.element.type === 'bpmn:UserTask' && data) {
+      setTimeout(() => {
+        genNodeDetailBox(e, overlays, data);
+      }, 10);
+    }
+  });
+  eventBus.on('element.out', (e) => {
+    overlays.clear();
+  });
+};
+const genNodeDetailBox = (e, overlays, data) => {
+  overlays.add(e.element.id, {
+    position: { top: e.element.height, left: 0 },
+    html: `<div class="verlays">
+                    <p>审批人员: ${data.nickName || ''}<p/>
+                    <p>节点状态:${data.status || ''}</p>
+                    <p>开始时间:${data.startTime || ''}</p>
+                    <p>结束时间:${data.endTime || ''}</p>
+                    <p>审批耗时:${data.runDuration || ''}</p>
+                   </div>`
+  });
+};
+// 让图能自适应屏幕
+const fitViewport = () => {
+  zoom.value = modeler.value.get<Canvas>('canvas').zoom('fit-viewport');
+  const bbox = document.querySelector<SVGGElement>('.flow-containers .viewport').getBBox();
+  const currentViewBox = modeler.value.get('canvas').viewbox();
+  const elementMid = {
+    x: bbox.x + bbox.width / 2 - 65,
+    y: bbox.y + bbox.height / 2
+  };
+  modeler.value.get<Canvas>('canvas').viewbox({
+    x: elementMid.x - currentViewBox.width / 2,
+    y: elementMid.y - currentViewBox.height / 2,
+    width: currentViewBox.width,
+    height: currentViewBox.height
+  });
+  zoom.value = (bbox.width / currentViewBox.width) * 1.8;
+};
+// 放大缩小
+const zoomViewport = (zoomIn = true) => {
+  zoom.value = modeler.value.get<Canvas>('canvas').zoom();
+  zoom.value += zoomIn ? 0.1 : -0.1;
+  modeler.value.get<Canvas>('canvas').zoom(zoom.value);
+};
+//上色
+const fillColor = () => {
+  const canvas = modeler.value.get<Canvas>('canvas');
+  bpmnNodeList(modeler.value._definitions.rootElements[0].flowElements, canvas);
+};
+//递归上色
+const bpmnNodeList = (flowElements, canvas) => {
+  flowElements.forEach((n) => {
+    if (n.$type === 'bpmn:UserTask') {
+      const completeTask = taskList.value.find((m) => m.key === n.id);
+      if (completeTask) {
+        canvas.addMarker(n.id, completeTask.completed ? 'highlight' : 'highlight-todo');
+        n.outgoing?.forEach((nn) => {
+          const targetTask = taskList.value.find((m) => m.key === nn.targetRef.id);
+          if (targetTask) {
+            canvas.addMarker(nn.id, targetTask.completed ? 'highlight' : 'highlight-todo');
+          } else if (nn.targetRef.$type === 'bpmn:ExclusiveGateway') {
+            canvas.addMarker(nn.id, completeTask.completed ? 'highlight' : 'highlight-todo');
+            canvas.addMarker(nn.targetRef.id, completeTask.completed ? 'highlight' : 'highlight-todo');
+            nn.targetRef.outgoing.forEach((e) => {
+              gateway(e.id, e.targetRef.$type, e.targetRef.id, canvas, completeTask.completed);
+            });
+          } else if (nn.targetRef.$type === 'bpmn:ParallelGateway') {
+            canvas.addMarker(nn.id, completeTask.completed ? 'highlight' : 'highlight-todo');
+            canvas.addMarker(nn.targetRef.id, completeTask.completed ? 'highlight' : 'highlight-todo');
+            nn.targetRef.outgoing.forEach((e) => {
+              gateway(e.id, e.targetRef.$type, e.targetRef.id, canvas, completeTask.completed);
+            });
+          } else if (nn.targetRef.$type === 'bpmn:InclusiveGateway') {
+            canvas.addMarker(nn.id, completeTask.completed ? 'highlight' : 'highlight-todo');
+            canvas.addMarker(nn.targetRef.id, completeTask.completed ? 'highlight' : 'highlight-todo');
+            nn.targetRef.outgoing.forEach((e) => {
+              gateway(e.id, e.targetRef.$type, e.targetRef.id, canvas, completeTask.completed);
+            });
+          }
+        });
+      }
+    } else if (n.$type === 'bpmn:ExclusiveGateway') {
+      n.outgoing.forEach((nn) => {
+        const targetTask = taskList.value.find((m) => m.key === nn.targetRef.id);
+        if (targetTask) {
+          canvas.addMarker(nn.id, targetTask.completed ? 'highlight' : 'highlight-todo');
+        }
+      });
+    } else if (n.$type === 'bpmn:ParallelGateway') {
+      n.outgoing.forEach((nn) => {
+        const targetTask = taskList.value.find((m) => m.key === nn.targetRef.id);
+        if (targetTask) {
+          canvas.addMarker(nn.id, targetTask.completed ? 'highlight' : 'highlight-todo');
+        }
+      });
+    } else if (n.$type === 'bpmn:InclusiveGateway') {
+      n.outgoing.forEach((nn) => {
+        const targetTask = taskList.value.find((m) => m.key === nn.targetRef.id);
+        if (targetTask) {
+          canvas.addMarker(nn.id, targetTask.completed ? 'highlight' : 'highlight-todo');
+        }
+      });
+    } else if (n.$type === 'bpmn:SubProcess') {
+      const completeTask = taskList.value.find((m) => m.key === n.id);
+      if (completeTask) {
+        canvas.addMarker(n.id, completeTask.completed ? 'highlight' : 'highlight-todo');
+      }
+      bpmnNodeList(n.flowElements, canvas);
+    } else if (n.$type === 'bpmn:StartEvent') {
+      canvas.addMarker(n.id, 'startEvent');
+      if (n.outgoing) {
+        n.outgoing.forEach((nn) => {
+          const completeTask = taskList.value.find((m) => m.key === nn.targetRef.id);
+          if (completeTask) {
+            canvas.addMarker(nn.id, 'highlight');
+            canvas.addMarker(n.id, 'highlight');
+          }
+        });
+      }
+    } else if (n.$type === 'bpmn:EndEvent') {
+      canvas.addMarker(n.id, 'endEvent');
+      const completeTask = taskList.value.find((m) => m.key === n.id);
+      if (completeTask) {
+        canvas.addMarker(completeTask.key, 'highlight');
+        canvas.addMarker(n.id, 'highlight');
+        return;
+      }
+    }
+  });
+};
+const gateway = (id, targetRefType, targetRefId, canvas, completed) => {
+  if (targetRefType === 'bpmn:ExclusiveGateway') {
+    canvas.addMarker(id, completed ? 'highlight' : 'highlight-todo');
+    canvas.addMarker(targetRefId, completed ? 'highlight' : 'highlight-todo');
+  }
+  if (targetRefType === 'bpmn:ParallelGateway') {
+    canvas.addMarker(id, completed ? 'highlight' : 'highlight-todo');
+    canvas.addMarker(targetRefId, completed ? 'highlight' : 'highlight-todo');
+  }
+  if (targetRefType === 'bpmn:InclusiveGateway') {
+    canvas.addMarker(id, completed ? 'highlight' : 'highlight-todo');
+    canvas.addMarker(targetRefId, completed ? 'highlight' : 'highlight-todo');
+  }
+};
+defineExpose({
+  init,
+  initXml
+});
+</script>
+
+<style lang="scss" scoped>
+.canvas {
+  width: 100%;
+  height: 100%;
+}
+
+.header-div {
+  display: flex;
+  padding: 10px 0;
+  justify-content: space-between;
+
+  .tips-label {
+    display: flex;
+    div {
+      margin-right: 10px;
+      padding: 5px;
+      font-size: 12px;
+    }
+    .un-complete {
+      border: 1px solid #000;
+    }
+    .in-progress {
+      background-color: rgb(255, 237, 204);
+      border: 1px dashed orange;
+    }
+    .complete {
+      background-color: rgb(204, 230, 204);
+      border: 1px solid green;
+    }
+  }
+}
+
+.view-mode {
+  .el-header,
+  .el-aside,
+  .djs-palette,
+  .bjs-powered-by {
+    display: none;
+  }
+  .el-loading-mask {
+    background-color: initial;
+  }
+  .el-loading-spinner {
+    display: none;
+  }
+}
+.bpmn-el-container {
+  height: calc(100vh - 350px);
+}
+.flow-containers {
+  width: 100%;
+  height: 100%;
+  overflow-y: auto;
+  .canvas {
+    width: 100%;
+    height: 100%;
+  }
+  .load {
+    margin-right: 10px;
+  }
+  :deep(.el-form-item__label) {
+    font-size: 13px;
+  }
+
+  :deep(.djs-palette) {
+    left: 0 !important;
+    top: 0;
+    border-top: none;
+  }
+
+  :deep(.djs-container svg) {
+    min-height: 650px;
+  }
+
+  :deep(.startEvent.djs-shape .djs-visual > :nth-child(1)) {
+    fill: #77df6d !important;
+  }
+  :deep(.endEvent.djs-shape .djs-visual > :nth-child(1)) {
+    fill: #ee7b77 !important;
+  }
+  :deep(.highlight.djs-shape .djs-visual > :nth-child(1)) {
+    fill: green !important;
+    stroke: green !important;
+    fill-opacity: 0.2 !important;
+  }
+  :deep(.highlight.djs-shape .djs-visual > :nth-child(2)) {
+    fill: green !important;
+  }
+  :deep(.highlight.djs-shape .djs-visual > path) {
+    fill: green !important;
+    fill-opacity: 0.2 !important;
+    stroke: green !important;
+  }
+  :deep(.highlight.djs-connection > .djs-visual > path) {
+    stroke: green !important;
+  }
+
+  // 边框滚动动画
+  @keyframes path-animation {
+    from {
+      stroke-dashoffset: 100%;
+    }
+
+    to {
+      stroke-dashoffset: 0%;
+    }
+  }
+
+  :deep(.highlight-todo.djs-connection > .djs-visual > path) {
+    animation: path-animation 60s;
+    animation-timing-function: linear;
+    animation-iteration-count: infinite;
+    stroke-dasharray: 4px !important;
+    stroke: orange !important;
+    fill-opacity: 0.2 !important;
+    marker-end: url('#sequenceflow-end-_E7DFDF-_E7DFDF-803g1kf6zwzmcig1y2ulm5egr');
+  }
+
+  :deep(.highlight-todo.djs-shape .djs-visual > :nth-child(1)) {
+    animation: path-animation 60s;
+    animation-timing-function: linear;
+    animation-iteration-count: infinite;
+    stroke-dasharray: 4px !important;
+    stroke: orange !important;
+    fill: orange !important;
+    fill-opacity: 0.2 !important;
+  }
+}
+:deep(.verlays) {
+  width: 250px;
+  background: rgb(102, 102, 102);
+  border-radius: 4px;
+  border: 1px solid #ebeef5;
+  color: #fff;
+  padding: 15px 10px;
+  p {
+    line-height: 28px;
+    margin: 0;
+    padding: 0;
+  }
+  cursor: pointer;
+}
+</style>

+ 20 - 21
src/components/Breadcrumb/index.vue

@@ -2,8 +2,7 @@
   <el-breadcrumb class="app-breadcrumb" separator="/">
     <transition-group name="breadcrumb">
       <el-breadcrumb-item v-for="(item, index) in levelList" :key="item.path">
-        <span v-if="item.redirect === 'noRedirect' || index == levelList.length - 1" class="no-redirect">{{
-          item.meta?.title }}</span>
+        <span v-if="item.redirect === 'noRedirect' || index == levelList.length - 1" class="no-redirect">{{ item.meta?.title }}</span>
         <a v-else @click.prevent="handleLink(item)">{{ item.meta?.title }}</a>
       </el-breadcrumb-item>
     </transition-group>
@@ -11,42 +10,42 @@
 </template>
 
 <script setup lang="ts">
-import { RouteLocationMatched } from 'vue-router'
+import { RouteLocationMatched } from 'vue-router';
 
 const route = useRoute();
 const router = useRouter();
-const levelList = ref<RouteLocationMatched[]>([])
+const levelList = ref<RouteLocationMatched[]>([]);
 
 const getBreadcrumb = () => {
   // only show routes with meta.title
-  let matched = route.matched.filter(item => item.meta && item.meta.title);
-  const first = matched[0]
+  let matched = route.matched.filter((item) => item.meta && item.meta.title);
+  const first = matched[0];
   // 判断是否为首页
   if (!isDashboard(first)) {
-    matched = ([{ path: '/index', meta: { title: '首页' } }] as any).concat(matched)
+    matched = ([{ path: '/index', meta: { title: '首页' } }] as any).concat(matched);
   }
-  levelList.value = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
-}
+  levelList.value = matched.filter((item) => item.meta && item.meta.title && item.meta.breadcrumb !== false);
+};
 const isDashboard = (route: RouteLocationMatched) => {
-  const name = route && route.name as string
+  const name = route && (route.name as string);
   if (!name) {
-    return false
+    return false;
   }
-  return name.trim() === 'Index'
-}
-const handleLink = (item: RouteLocationMatched) => {
-  const { redirect, path } = item
-  redirect ? router.push(redirect as string) : router.push(path)
-}
+  return name.trim() === 'Index';
+};
+const handleLink = (item) => {
+  const { redirect, path } = item;
+  redirect ? router.push(redirect) : router.push(path);
+};
 
 watchEffect(() => {
   // if you go to the redirect page, do not update the breadcrumbs
-  if (route.path.startsWith('/redirect/')) return
-  getBreadcrumb()
-})
+  if (route.path.startsWith('/redirect/')) return;
+  getBreadcrumb();
+});
 onMounted(() => {
   getBreadcrumb();
-})
+});
 </script>
 
 <style lang="scss" scoped>

+ 33 - 36
src/components/BuildCode/index.vue

@@ -1,53 +1,50 @@
-<!-- 代码构建 -->
+<template>
+  <!-- 代码构建 -->
+  <div>
+    <v-form-designer
+      ref="buildRef"
+      class="build"
+      :designer-config="{ importJsonButton: true, exportJsonButton: true, exportCodeButton: true, generateSFCButton: true, formTemplates: true }"
+    >
+      <template v-if="showBtn" #customToolButtons>
+        <el-button link type="primary" icon="Select" @click="getJson">保存</el-button>
+      </template>
+    </v-form-designer>
+  </div>
+</template>
+
 <script setup lang="ts">
+interface Props {
+  showBtn: boolean;
+  formJson: any;
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  showBtn: true,
+  formJson: ''
+});
 
-const props = defineProps({
-  showBtn: {
-    type: Boolean,
-    default: false
-  },
-  formJson: {
-    type: Object,
-    default: undefined
-  }
-})
-const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 const buildRef = ref();
 const emits = defineEmits(['reJson', 'saveDesign']);
 
-
-
 //获取表单json
 const getJson = () => {
-  const formJson = JSON.stringify(buildRef.value.getFormJson())
-  const fieldJson = JSON.stringify(buildRef.value.getFieldWidgets())
+  const formJson = JSON.stringify(buildRef.value.getFormJson());
+  const fieldJson = JSON.stringify(buildRef.value.getFieldWidgets());
   let data = {
-    formJson, fieldJson
-  }
-  emits("saveDesign", data)
-}
+    formJson,
+    fieldJson
+  };
+  emits('saveDesign', data);
+};
 
 onMounted(() => {
   if (props.formJson) {
-    buildRef.value.setFormJson(props.formJson)
+    buildRef.value.setFormJson(props.formJson);
   }
-})
+});
 </script>
 
-<template>
-  <div>
-    <v-form-designer
-      class="build"
-      ref="buildRef"
-      :designer-config="{ importJsonButton: true, exportJsonButton: true, exportCodeButton: true, generateSFCButton: true, formTemplates: true }"
-    >
-      <template #customToolButtons v-if="showBtn">
-        <el-button link type="primary" icon="Select" @click="getJson">保存</el-button>
-      </template>
-    </v-form-designer>
-  </div>
-</template>
-
 <style lang="scss">
 .build {
   margin: 0 !important;

+ 32 - 37
src/components/BuildCode/render.vue

@@ -1,62 +1,57 @@
+<template>
+  <div class="">
+    <v-form-render ref="vFormRef" :form-json="formJson" :form-data="formData" />
+  </div>
+</template>
+
 <!-- 动态表单渲染 -->
-<script setup name="Render">
+<script setup name="Render" lang="ts">
+interface Props {
+  formJson: string | object;
+  formData: string | object;
+  isView: boolean;
+}
 
-const props = defineProps({
-  formJson: {
-    type: [String, Object],
-    default: ""
-  },
-  formData: {
-    type: [String, Object],
-    default: ""
-  },
-  isView: {
-    type: Boolean,
-    default: false
-  }
-})
+const props = withDefaults(defineProps<Props>(), {
+  formJson: '',
+  formData: '',
+  isView: false
+});
 
-const vFormRef = ref(null)
+const vFormRef = ref();
 // 获取表单数据-异步
 const getFormData = () => {
-  return vFormRef.value.getFormData()
-}
+  return vFormRef.value.getFormData();
+};
 
 /**
  * 设置表单内容
  * @param {表单配置} formConf
  * formConfig:{ formTemplate:表单模板,formData:表单数据,hiddenField:需要隐藏的字段字符串集合,disabledField:需要禁用的自读字符串集合}
  */
-const initForm = (formConf) => {
-  const { formTemplate, formData, hiddenField, disabledField } = toRaw(formConf)
+const initForm = (formConf: any) => {
+  const { formTemplate, formData, hiddenField, disabledField } = toRaw(formConf);
   if (formTemplate) {
-    vFormRef.value.setFormJson(formTemplate)
+    vFormRef.value.setFormJson(formTemplate);
     if (formData) {
-      vFormRef.value.setFormData(formData)
+      vFormRef.value.setFormData(formData);
     }
     if (disabledField && disabledField.length > 0) {
       setTimeout(() => {
-        vFormRef.value.disableWidgets(disabledField)
-      }, 200)
+        vFormRef.value.disableWidgets(disabledField);
+      }, 200);
     }
     if (hiddenField && hiddenField.length > 0) {
       setTimeout(() => {
-        vFormRef.value.hideWidgets(hiddenField)
-      }, 200)
+        vFormRef.value.hideWidgets(hiddenField);
+      }, 200);
     }
     if (props.isView) {
-      console.log(props.isView)
       setTimeout(() => {
-        vFormRef.value.disableForm()
-      }, 100)
+        vFormRef.value.disableForm();
+      }, 100);
     }
   }
-}
-defineExpose({ getFormData, initForm })
+};
+defineExpose({ getFormData, initForm });
 </script>
-
-<template>
-  <div class="">
-    <v-form-render ref="vFormRef" :form-json="formJson" :form-data="formData" />
-  </div>
-</template>

+ 45 - 36
src/components/DictTag/index.vue

@@ -2,19 +2,31 @@
   <div>
     <template v-for="(item, index) in options">
       <template v-if="values.includes(item.value)">
-        <span v-if="(item.elTagType === 'default' || item.elTagType === '') && (item.elTagClass === '' || item.elTagClass == null)"
-              :key="item.value" :index="index" :class="item.elTagClass">
-          {{ item.label + " " }}
+        <span
+          v-if="(item.elTagType === 'default' || item.elTagType === '') && (item.elTagClass === '' || item.elTagClass == null)"
+          :key="item.value"
+          :index="index"
+          :class="item.elTagClass"
+        >
+          {{ item.label + ' ' }}
         </span>
         <el-tag
           v-else
-          :disable-transitions="true"
           :key="item.value + ''"
+          :disable-transitions="true"
           :index="index"
-          :type="(item.elTagType === 'primary' || item.elTagType === 'default')? '' : item.elTagType"
+          :type="
+            item.elTagType === 'primary' ||
+            item.elTagType === 'success' ||
+            item.elTagType === 'info' ||
+            item.elTagType === 'warning' ||
+            item.elTagType === 'danger'
+              ? item.elTagType
+              : 'primary'
+          "
           :class="item.elTagClass"
         >
-          {{ item.label + " " }}
+          {{ item.label + ' ' }}
         </el-tag>
       </template>
     </template>
@@ -25,57 +37,54 @@
 </template>
 
 <script setup lang="ts">
-import { propTypes } from '@/utils/propTypes';
-
-
-const props = defineProps({
-  // 数据
-  options: {
-    type: Array as PropType<DictDataOption[]>,
-    default: null,
-  },
-  // 当前的值
-  value: [Number, String, Array] as PropType<number | string | Array<number | string>>,
-  // 当未找到匹配的数据时,显示value
-  showValue: propTypes.bool.def(true),
-  separator: propTypes.string.def(","),
+interface Props {
+  options: Array<DictDataOption>;
+  value: number | string | Array<number | string>;
+  showValue?: boolean;
+  separator?: string;
+}
+const props = withDefaults(defineProps<Props>(), {
+  showValue: true,
+  separator: ','
 });
+
 const values = computed(() => {
-  if (props.value === '' || props.value === null || typeof props.value === "undefined") return []
-  return Array.isArray(props.value) ? props.value.map(item => '' + item) : String(props.value).split(props.separator);
+  if (props.value === '' || props.value === null || typeof props.value === 'undefined') return [];
+  return Array.isArray(props.value) ? props.value.map((item) => '' + item) : String(props.value).split(props.separator);
 });
 
 const unmatch = computed(() => {
-  if (props.options?.length == 0 || props.value === '' || props.value === null || typeof props.value === "undefined") return false
+  if (props.options?.length == 0 || props.value === '' || props.value === null || typeof props.value === 'undefined') return false;
   // 传入值为非数组
-  values.value.forEach(item => {
-    if (!props.options.some(v => v.value === item)) {
-      return true // 如果有未匹配项,将标志设置为true
+  let unmatch = false; // 添加一个标志来判断是否有未匹配项
+  values.value.forEach((item) => {
+    if (!props.options.some((v) => v.value === item)) {
+      unmatch = true; // 如果有未匹配项,将标志设置为true
     }
-  })
-  return false // 返回标志的值
+  });
+  return unmatch; // 返回标志的值
 });
 
 const unmatchArray = computed(() => {
-// 记录未匹配的项
+  // 记录未匹配的项
   const itemUnmatchArray: Array<string | number> = [];
-  if (props.value !== '' && props.value !== null && typeof props.value !== "undefined") {
-    values.value.forEach(item => {
-      if (!props.options.some(v => v.value === item)) {
+  if (props.value !== '' && props.value !== null && typeof props.value !== 'undefined') {
+    values.value.forEach((item) => {
+      if (!props.options.some((v) => v.value === item)) {
         itemUnmatchArray.push(item);
       }
-    })
+    });
   }
   // 没有value不显示
   return handleArray(itemUnmatchArray);
 });
 
 const handleArray = (array: Array<string | number>) => {
-  if (array.length === 0) return "";
+  if (array.length === 0) return '';
   return array.reduce((pre, cur) => {
-    return pre + " " + cur;
+    return pre + ' ' + cur;
   });
-}
+};
 </script>
 
 <style scoped>

+ 97 - 90
src/components/Editor/index.vue

@@ -1,6 +1,7 @@
 <template>
   <div>
     <el-upload
+      v-if="type === 'url'"
       :action="upload.url"
       :before-upload="handleBeforeUpload"
       :on-success="handleUploadSuccess"
@@ -9,28 +10,30 @@
       name="file"
       :show-file-list="false"
       :headers="upload.headers"
-      ref="uploadRef"
-      v-if="type === 'url'"
     >
+      <i ref="uploadRef"></i>
     </el-upload>
-    <div class="editor">
-      <quill-editor
-        ref="quillEditorRef"
-        v-model:content="content"
-        contentType="html"
-        @textChange="(e: any) => $emit('update:modelValue', content)"
-        :options="options"
-        :style="styles"
-      />
-    </div>
+  </div>
+  <div class="editor">
+    <quill-editor
+      ref="quillEditorRef"
+      v-model:content="content"
+      content-type="html"
+      :options="options"
+      :style="styles"
+      @text-change="(e: any) => $emit('update:modelValue', content)"
+    />
   </div>
 </template>
 
 <script setup lang="ts">
-import { QuillEditor, Quill } from '@vueup/vue-quill';
 import '@vueup/vue-quill/dist/vue-quill.snow.css';
+
+import { QuillEditor, Quill } from '@vueup/vue-quill';
 import { propTypes } from '@/utils/propTypes';
-import { globalHeaders } from "@/utils/request";
+import { globalHeaders } from '@/utils/request';
+
+defineEmits(['update:modelValue']);
 
 const props = defineProps({
   /* 编辑器的内容 */
@@ -52,42 +55,43 @@ const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 const upload = reactive<UploadOption>({
   headers: globalHeaders(),
   url: import.meta.env.VITE_APP_BASE_API + '/resource/oss/upload'
-})
+});
 const quillEditorRef = ref();
+const uploadRef = ref<HTMLDivElement>();
 
-const options = ref({
-  theme: "snow",
+const options = ref<any>({
+  theme: 'snow',
   bounds: document.body,
-  debug: "warn",
+  debug: 'warn',
   modules: {
     // 工具栏配置
     toolbar: {
       container: [
-        ["bold", "italic", "underline", "strike"],       // 加粗 斜体 下划线 删除线
-        ["blockquote", "code-block"],                    // 引用  代码块
-        [{ list: "ordered" }, { list: "bullet" }],       // 有序、无序列表
-        [{ indent: "-1" }, { indent: "+1" }],            // 缩进
-        [{ size: ["small", false, "large", "huge"] }],   // 字体大小
-        [{ header: [1, 2, 3, 4, 5, 6, false] }],         // 标题
-        [{ color: [] }, { background: [] }],             // 字体颜色、字体背景颜色
-        [{ align: [] }],                                 // 对齐方式
-        ["clean"],                                       // 清除文本格式
-        ["link", "image", "video"]                       // 链接、图片、视频
+        ['bold', 'italic', 'underline', 'strike'], // 加粗 斜体 下划线 删除线
+        ['blockquote', 'code-block'], // 引用  代码块
+        [{ list: 'ordered' }, { list: 'bullet' }], // 有序、无序列表
+        [{ indent: '-1' }, { indent: '+1' }], // 缩进
+        [{ size: ['small', false, 'large', 'huge'] }], // 字体大小
+        [{ header: [1, 2, 3, 4, 5, 6, false] }], // 标题
+        [{ color: [] }, { background: [] }], // 字体颜色、字体背景颜色
+        [{ align: [] }], // 对齐方式
+        ['clean'], // 清除文本格式
+        ['link', 'image', 'video'] // 链接、图片、视频
       ],
       handlers: {
-        image: function (value: any) {
+        image: (value: boolean) => {
           if (value) {
             // 调用element图片上传
-            (document.querySelector(".editor-img-uploader>.el-upload") as HTMLDivElement)?.click();
+            uploadRef.value.click();
           } else {
-            Quill.format("image", true);
+            Quill.format('image', true);
           }
-        },
-      },
+        }
+      }
     }
   },
-  placeholder: "请输入内容",
-  readOnly: props.readOnly,
+  placeholder: '请输入内容',
+  readOnly: props.readOnly
 });
 
 const styles = computed(() => {
@@ -99,14 +103,18 @@ const styles = computed(() => {
     style.height = `${props.height}px`;
   }
   return style;
-})
+});
 
-const content = ref("");
-watch(() => props.modelValue, (v) => {
-  if (v !== content.value) {
-    content.value = v === undefined ? "<p></p>" : v;
-  }
-}, { immediate: true });
+const content = ref('');
+watch(
+  () => props.modelValue,
+  (v: string) => {
+    if (v !== content.value) {
+      content.value = v === undefined ? '<p></p>' : v;
+    }
+  },
+  { immediate: true }
+);
 
 // 图片上传成功返回图片地址
 const handleUploadSuccess = (res: any) => {
@@ -117,19 +125,19 @@ const handleUploadSuccess = (res: any) => {
     // 获取光标位置
     let length = quill.selection.savedRange.index;
     // 插入图片,res为服务器返回的图片链接地址
-    quill.insertEmbed(length, "image", res.data.url);
+    quill.insertEmbed(length, 'image', res.data.url);
     // 调整光标到最后
     quill.setSelection(length + 1);
     proxy?.$modal.closeLoading();
   } else {
-    proxy?.$modal.loading(res.msg);
+    proxy?.$modal.msgError('图片插入失败');
     proxy?.$modal.closeLoading();
   }
-}
+};
 
 // 图片上传前拦截
 const handleBeforeUpload = (file: any) => {
-  const type = ["image/jpeg", "image/jpg", "image/png", "image/svg"];
+  const type = ['image/jpeg', 'image/jpg', 'image/png', 'image/svg'];
   const isJPG = type.includes(file.type);
   //检验文件格式
   if (!isJPG) {
@@ -146,13 +154,12 @@ const handleBeforeUpload = (file: any) => {
   }
   proxy?.$modal.loading('正在上传文件,请稍候...');
   return true;
-}
+};
 
 // 图片失败拦截
 const handleUploadError = (err: any) => {
-  console.error(err);
   proxy?.$modal.msgError('上传文件失败');
-}
+};
 </script>
 
 <style>
@@ -167,71 +174,71 @@ const handleUploadError = (err: any) => {
 .quill-img {
   display: none;
 }
-.ql-snow .ql-tooltip[data-mode="link"]::before {
-  content: "请输入链接地址:";
+.ql-snow .ql-tooltip[data-mode='link']::before {
+  content: '请输入链接地址:';
 }
 .ql-snow .ql-tooltip.ql-editing a.ql-action::after {
   border-right: 0;
-  content: "保存";
+  content: '保存';
   padding-right: 0;
 }
-.ql-snow .ql-tooltip[data-mode="video"]::before {
-  content: "请输入视频地址:";
+.ql-snow .ql-tooltip[data-mode='video']::before {
+  content: '请输入视频地址:';
 }
 .ql-snow .ql-picker.ql-size .ql-picker-label::before,
 .ql-snow .ql-picker.ql-size .ql-picker-item::before {
-  content: "14px";
+  content: '14px';
 }
-.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="small"]::before,
-.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="small"]::before {
-  content: "10px";
+.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='small']::before,
+.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='small']::before {
+  content: '10px';
 }
-.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="large"]::before,
-.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="large"]::before {
-  content: "18px";
+.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='large']::before,
+.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='large']::before {
+  content: '18px';
 }
-.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="huge"]::before,
-.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="huge"]::before {
-  content: "32px";
+.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='huge']::before,
+.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='huge']::before {
+  content: '32px';
 }
 .ql-snow .ql-picker.ql-header .ql-picker-label::before,
 .ql-snow .ql-picker.ql-header .ql-picker-item::before {
-  content: "文本";
+  content: '文本';
 }
-.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="1"]::before,
-.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]::before {
-  content: "标题1";
+.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='1']::before,
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='1']::before {
+  content: '标题1';
 }
-.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="2"]::before,
-.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before {
-  content: "标题2";
+.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='2']::before,
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='2']::before {
+  content: '标题2';
 }
-.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="3"]::before,
-.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before {
-  content: "标题3";
+.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='3']::before,
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='3']::before {
+  content: '标题3';
 }
-.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="4"]::before,
-.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]::before {
-  content: "标题4";
+.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='4']::before,
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='4']::before {
+  content: '标题4';
 }
-.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="5"]::before,
-.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]::before {
-  content: "标题5";
+.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='5']::before,
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='5']::before {
+  content: '标题5';
 }
-.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="6"]::before,
-.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]::before {
-  content: "标题6";
+.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='6']::before,
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='6']::before {
+  content: '标题6';
 }
 .ql-snow .ql-picker.ql-font .ql-picker-label::before,
 .ql-snow .ql-picker.ql-font .ql-picker-item::before {
-  content: "标准字体";
+  content: '标准字体';
 }
-.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="serif"]::before,
-.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="serif"]::before {
-  content: "衬线字体";
+.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='serif']::before,
+.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='serif']::before {
+  content: '衬线字体';
 }
-.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="monospace"]::before,
-.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="monospace"]::before {
-  content: "等宽字体";
+.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='monospace']::before,
+.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='monospace']::before {
+  content: '等宽字体';
 }
 </style>

+ 127 - 115
src/components/FileUpload/index.vue

@@ -1,6 +1,7 @@
 <template>
   <div class="upload-file">
     <el-upload
+      ref="fileUploadRef"
       multiple
       :action="uploadFileUrl"
       :before-upload="handleBeforeUpload"
@@ -12,30 +13,29 @@
       :show-file-list="false"
       :headers="headers"
       class="upload-file-uploader"
-      ref="fileUploadRef"
     >
       <!-- 上传按钮 -->
       <el-button type="primary">选取文件</el-button>
     </el-upload>
     <!-- 上传提示 -->
-    <div class="el-upload__tip" v-if="showTip">
+    <div v-if="showTip" class="el-upload__tip">
       请上传
       <template v-if="fileSize">
         大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
       </template>
       <template v-if="fileType">
-        格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b>
+        格式为 <b style="color: #f56c6c">{{ fileType.join('/') }}</b>
       </template>
       的文件
     </div>
     <!-- 文件列表 -->
     <transition-group class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul">
-      <li :key="file.uid" class="el-upload-list__item ele-upload-list__item-content" v-for="(file, index) in fileList">
+      <li v-for="(file, index) in fileList" :key="file.uid" class="el-upload-list__item ele-upload-list__item-content">
         <el-link :href="`${file.url}`" :underline="false" target="_blank">
           <span class="el-icon-document"> {{ getFileName(file.name) }} </span>
         </el-link>
         <div class="ele-upload-list__item-content-action">
-          <el-link :underline="false" @click="handleDelete(index)" type="danger">删除</el-link>
+          <el-button type="danger" link @click="handleDelete(index)">删除</el-button>
         </div>
       </li>
     </transition-group>
@@ -43,20 +43,23 @@
 </template>
 
 <script setup lang="ts">
-import { listByIds, delOss } from "@/api/system/oss";
 import { propTypes } from '@/utils/propTypes';
-import { globalHeaders } from "@/utils/request";
+import { delOss, listByIds } from '@/api/system/oss';
+import { globalHeaders } from '@/utils/request';
 
 const props = defineProps({
-    modelValue: [String, Object, Array],
-    // 数量限制
-    limit: propTypes.number.def(5),
-    // 大小限制(MB)
-    fileSize: propTypes.number.def(5),
-    // 文件类型, 例如['png', 'jpg', 'jpeg']
-    fileType: propTypes.array.def(["doc", "xls", "ppt", "txt", "pdf"]),
-    // 是否显示提示
-    isShowTip: propTypes.bool.def(true),
+  modelValue: {
+    type: [String, Object, Array],
+    default: () => []
+  },
+  // 数量限制
+  limit: propTypes.number.def(5),
+  // 大小限制(MB)
+  fileSize: propTypes.number.def(5),
+  // 文件类型, 例如['png', 'jpg', 'jpeg']
+  fileType: propTypes.array.def(['doc', 'xls', 'ppt', 'txt', 'pdf']),
+  // 是否显示提示
+  isShowTip: propTypes.bool.def(true)
 });
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@@ -65,153 +68,162 @@ const number = ref(0);
 const uploadList = ref<any[]>([]);
 
 const baseUrl = import.meta.env.VITE_APP_BASE_API;
-const uploadFileUrl = ref(baseUrl + "/resource/oss/upload"); // 上传文件服务器地址
+const uploadFileUrl = ref(baseUrl + '/resource/oss/upload'); // 上传文件服务器地址
 const headers = ref(globalHeaders());
 
 const fileList = ref<any[]>([]);
-const showTip = computed(
-    () => props.isShowTip && (props.fileType || props.fileSize)
-);
+const showTip = computed(() => props.isShowTip && (props.fileType || props.fileSize));
 
 const fileUploadRef = ref<ElUploadInstance>();
 
-watch(() => props.modelValue, async val => {
+watch(
+  () => props.modelValue,
+  async (val) => {
     if (val) {
-        let temp = 1;
-        // 首先将值转为数组
-        let list = [];
-        if (Array.isArray(val)) {
-            list = val;
-        } else {
-            const res = await listByIds(val as string)
-            list = res.data.map((oss) => {
-                const data = { name: oss.originalName, url: oss.url, ossId: oss.ossId };
-                return data;
-            });
-        }
-        // 然后将数组转为对象数组
-        fileList.value = list.map(item => {
-            item = { name: item.name, url: item.url, ossId: item.ossId };
-            item.uid = item.uid || new Date().getTime() + temp++;
-            return item;
+      let temp = 1;
+      // 首先将值转为数组
+      let list: any[] = [];
+      if (Array.isArray(val)) {
+        list = val;
+      } else {
+        const res = await listByIds(val);
+        list = res.data.map((oss) => {
+          return {
+            name: oss.originalName,
+            url: oss.url,
+            ossId: oss.ossId
+          };
         });
+      }
+      // 然后将数组转为对象数组
+      fileList.value = list.map((item) => {
+        item = { name: item.name, url: item.url, ossId: item.ossId };
+        item.uid = item.uid || new Date().getTime() + temp++;
+        return item;
+      });
     } else {
-        fileList.value = [];
-        return [];
+      fileList.value = [];
+      return [];
     }
-}, { deep: true, immediate: true });
+  },
+  { deep: true, immediate: true }
+);
 
 // 上传前校检格式和大小
 const handleBeforeUpload = (file: any) => {
-    // 校检文件类型
-    if (props.fileType.length) {
-        const fileName = file.name.split('.');
-        const fileExt = fileName[fileName.length - 1];
-        const isTypeOk = props.fileType.indexOf(fileExt) >= 0;
-        if (!isTypeOk) {
-            proxy?.$modal.msgError(`文件格式不正确, 请上传${props.fileType.join("/")}格式文件!`);
-            return false;
-        }
+  // 校检文件类型
+  if (props.fileType.length) {
+    const fileName = file.name.split('.');
+    const fileExt = fileName[fileName.length - 1];
+    const isTypeOk = props.fileType.indexOf(fileExt) >= 0;
+    if (!isTypeOk) {
+      proxy?.$modal.msgError(`文件格式不正确, 请上传${props.fileType.join('/')}格式文件!`);
+      return false;
     }
-    // 校检文件大小
-    if (props.fileSize) {
-        const isLt = file.size / 1024 / 1024 < props.fileSize;
-        if (!isLt) {
-            proxy?.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`);
-            return false;
-        }
+  }
+  // 校检文件大小
+  if (props.fileSize) {
+    const isLt = file.size / 1024 / 1024 < props.fileSize;
+    if (!isLt) {
+      proxy?.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`);
+      return false;
     }
-    proxy?.$modal.loading("正在上传文件,请稍候...");
-    number.value++;
-    return true;
-}
+  }
+  proxy?.$modal.loading('正在上传文件,请稍候...');
+  number.value++;
+  return true;
+};
 
 // 文件个数超出
 const handleExceed = () => {
-    proxy?.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`);
-}
+  proxy?.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`);
+};
 
 // 上传失败
 const handleUploadError = () => {
-    proxy?.$modal.msgError("上传文件失败");
-}
+  proxy?.$modal.msgError('上传文件失败');
+};
 
 // 上传成功回调
 const handleUploadSuccess = (res: any, file: UploadFile) => {
-    if (res.code === 200) {
-        uploadList.value.push({ name: res.data.fileName, url: res.data.url, ossId: res.data.ossId });
-        uploadedSuccessfully();
-    } else {
-        number.value--;
-        proxy?.$modal.closeLoading();
-        proxy?.$modal.msgError(res.msg);
-        fileUploadRef.value?.handleRemove(file);
-        uploadedSuccessfully();
-    }
-}
+  if (res.code === 200) {
+    uploadList.value.push({
+      name: res.data.fileName,
+      url: res.data.url,
+      ossId: res.data.ossId
+    });
+    uploadedSuccessfully();
+  } else {
+    number.value--;
+    proxy?.$modal.closeLoading();
+    proxy?.$modal.msgError(res.msg);
+    fileUploadRef.value?.handleRemove(file);
+    uploadedSuccessfully();
+  }
+};
 
 // 删除文件
 const handleDelete = (index: number) => {
-    let ossId = fileList.value[index].ossId;
-    delOss(ossId);
-    fileList.value.splice(index, 1);
-    emit("update:modelValue", listToString(fileList.value));
-}
+  let ossId = fileList.value[index].ossId;
+  delOss(ossId);
+  fileList.value.splice(index, 1);
+  emit('update:modelValue', listToString(fileList.value));
+};
 
 // 上传结束处理
 const uploadedSuccessfully = () => {
-    if (number.value > 0 && uploadList.value.length === number.value) {
-        fileList.value = fileList.value.filter(f => f.url !== undefined).concat(uploadList.value);
-        uploadList.value = [];
-        number.value = 0;
-        emit("update:modelValue", listToString(fileList.value));
-        proxy?.$modal.closeLoading();
-    }
-}
+  if (number.value > 0 && uploadList.value.length === number.value) {
+    fileList.value = fileList.value.filter((f) => f.url !== undefined).concat(uploadList.value);
+    uploadList.value = [];
+    number.value = 0;
+    emit('update:modelValue', listToString(fileList.value));
+    proxy?.$modal.closeLoading();
+  }
+};
 
 // 获取文件名称
 const getFileName = (name: string) => {
-    // 如果是url那么取最后的名字 如果不是直接返回
-    if (name.lastIndexOf("/") > -1) {
-        return name.slice(name.lastIndexOf("/") + 1);
-    } else {
-        return name;
-    }
-}
+  // 如果是url那么取最后的名字 如果不是直接返回
+  if (name.lastIndexOf('/') > -1) {
+    return name.slice(name.lastIndexOf('/') + 1);
+  } else {
+    return name;
+  }
+};
 
 // 对象转成指定字符串分隔
 const listToString = (list: any[], separator?: string) => {
-    let strs = "";
-    separator = separator || ",";
-    list.forEach(item => {
-        if (item.ossId) {
-            strs += item.ossId + separator;
-        }
-    })
-    return strs != "" ? strs.substring(0, strs.length - 1) : "";
-}
+  let strs = '';
+  separator = separator || ',';
+  list.forEach((item) => {
+    if (item.ossId) {
+      strs += item.ossId + separator;
+    }
+  });
+  return strs != '' ? strs.substring(0, strs.length - 1) : '';
+};
 </script>
 
 <style scoped lang="scss">
 .upload-file-uploader {
-    margin-bottom: 5px;
+  margin-bottom: 5px;
 }
 
 .upload-file-list .el-upload-list__item {
-    border: 1px solid #e4e7ed;
-    line-height: 2;
-    margin-bottom: 10px;
-    position: relative;
+  border: 1px solid #e4e7ed;
+  line-height: 2;
+  margin-bottom: 10px;
+  position: relative;
 }
 
 .upload-file-list .ele-upload-list__item-content {
-    display: flex;
-    justify-content: space-between;
-    align-items: center;
-    color: inherit;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  color: inherit;
 }
 
 .ele-upload-list__item-content-action .el-link {
-    margin-right: 10px;
+  margin-right: 10px;
 }
 </style>

+ 4 - 4
src/components/Hamburger/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <div style="padding: 0 15px;" @click="toggleClick">
+  <div style="padding: 0 15px" @click="toggleClick">
     <svg :class="{ 'is-active': isActive }" class="hamburger" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="64" height="64">
       <path
         d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z"
@@ -13,12 +13,12 @@ import { propTypes } from '@/utils/propTypes';
 
 defineProps({
   isActive: propTypes.bool.def(false)
-})
+});
 
-const emit = defineEmits(['toggleClick'])
+const emit = defineEmits(['toggleClick']);
 const toggleClick = () => {
   emit('toggleClick');
-}
+};
 </script>
 
 <style scoped>

+ 51 - 49
src/components/HeaderSearch/index.vue

@@ -1,6 +1,6 @@
 <template>
-  <div :class="{ 'show': show }" class="header-search">
-    <svg-icon class-name="search-icon" icon-class="search" @click.stop="click"/>
+  <div :class="{ show: show }" class="header-search">
+    <svg-icon class-name="search-icon" icon-class="search" @click.stop="click" />
     <el-select
       ref="headerSearchSelectRef"
       v-model="search"
@@ -12,23 +12,22 @@
       class="header-search-select"
       @change="change"
     >
-      <el-option v-for="option in options" :key="option.item.path" :value="option.item"
-                 :label="option.item.title.join(' > ')"/>
+      <el-option v-for="option in options" :key="option.item.path" :value="option.item" :label="option.item.title.join(' > ')" />
     </el-select>
   </div>
 </template>
 
 <script setup lang="ts" name="HeaderSearch">
 import Fuse from 'fuse.js';
-import {getNormalPath} from '@/utils/ruoyi';
-import {isHttp} from '@/utils/validate';
+import { getNormalPath } from '@/utils/ruoyi';
+import { isHttp } from '@/utils/validate';
 import usePermissionStore from '@/store/modules/permission';
-import {RouteOption} from 'vue-router';
+import { RouteRecordRaw } from 'vue-router';
 
 type Router = Array<{
   path: string;
   title: string[];
-}>
+}>;
 
 const search = ref('');
 const options = ref<any>([]);
@@ -37,39 +36,39 @@ const show = ref(false);
 const fuse = ref();
 const headerSearchSelectRef = ref<ElSelectInstance>();
 const router = useRouter();
-const routes = computed(() => usePermissionStore().routes);
+const routes = computed(() => usePermissionStore().getRoutes());
 
 const click = () => {
-  show.value = !show.value
+  show.value = !show.value;
   if (show.value) {
-    headerSearchSelectRef.value && headerSearchSelectRef.value.focus()
+    headerSearchSelectRef.value && headerSearchSelectRef.value.focus();
   }
 };
 const close = () => {
-  headerSearchSelectRef.value && headerSearchSelectRef.value.blur()
-  options.value = []
-  show.value = false
-}
+  headerSearchSelectRef.value && headerSearchSelectRef.value.blur();
+  options.value = [];
+  show.value = false;
+};
 const change = (val: any) => {
   const path = val.path;
   const query = val.query;
   if (isHttp(path)) {
     // http(s):// 路径新窗口打开
-    const pindex = path.indexOf("http");
-    window.open(path.substr(pindex, path.length), "_blank");
+    const pindex = path.indexOf('http');
+    window.open(path.substr(pindex, path.length), '_blank');
   } else {
     if (query) {
       router.push({ path: path, query: JSON.parse(query) });
     } else {
-      router.push(path)
+      router.push(path);
     }
   }
-  search.value = ''
-  options.value = []
+  search.value = '';
+  options.value = [];
   nextTick(() => {
-    show.value = false
-  })
-}
+    show.value = false;
+  });
+};
 const initFuse = (list: Router) => {
   fuse.value = new Fuse(list, {
     shouldSort: true,
@@ -77,20 +76,23 @@ const initFuse = (list: Router) => {
     location: 0,
     distance: 100,
     minMatchCharLength: 1,
-    keys: [{
-      name: 'title',
-      weight: 0.7
-    }, {
-      name: 'path',
-      weight: 0.3
-    }]
-  })
-}
+    keys: [
+      {
+        name: 'title',
+        weight: 0.7
+      },
+      {
+        name: 'path',
+        weight: 0.3
+      }
+    ]
+  });
+};
 // Filter out the routes that can be displayed in the sidebar
 // And generate the internationalized title
-const generateRoutes = (routes: RouteOption[], basePath = '', prefixTitle: string[] = []) => {
-  let res: Router = []
-  routes.forEach(r => {
+const generateRoutes = (routes: RouteRecordRaw[], basePath = '', prefixTitle: string[] = []) => {
+  let res: Router = [];
+  routes.forEach((r) => {
     // skip hidden router
     if (!r.hidden) {
       const p = r.path.length > 0 && r.path[0] === '/' ? r.path : '/' + r.path;
@@ -98,7 +100,7 @@ const generateRoutes = (routes: RouteOption[], basePath = '', prefixTitle: strin
         path: !isHttp(r.path) ? getNormalPath(basePath + p) : r.path,
         title: [...prefixTitle],
         query: ''
-      }
+      };
       if (r.meta && r.meta.title) {
         data.title = [...data.title, r.meta.title];
         if (r.redirect !== 'noRedirect') {
@@ -109,7 +111,7 @@ const generateRoutes = (routes: RouteOption[], basePath = '', prefixTitle: strin
       }
 
       if (r.query) {
-        data.query = r.query
+        data.query = r.query;
       }
 
       // recursive child routes
@@ -120,20 +122,20 @@ const generateRoutes = (routes: RouteOption[], basePath = '', prefixTitle: strin
         }
       }
     }
-  })
+  });
   return res;
-}
+};
 const querySearch = (query: string) => {
   if (query !== '') {
-    options.value = fuse.value.search(query)
+    options.value = fuse.value.search(query);
   } else {
-    options.value = []
+    options.value = [];
   }
-}
+};
 
 onMounted(() => {
   searchPool.value = generateRoutes(routes.value);
-})
+});
 
 // watchEffect(() => {
 //     searchPool.value = generateRoutes(routes.value)
@@ -141,15 +143,15 @@ onMounted(() => {
 
 watch(show, (value) => {
   if (value) {
-    document.body.addEventListener('click', close)
+    document.body.addEventListener('click', close);
   } else {
-    document.body.removeEventListener('click', close)
+    document.body.removeEventListener('click', close);
   }
-})
+});
 
-watch(searchPool, (list) => {
-  initFuse(list)
-})
+watch(searchPool, (list: Router) => {
+  initFuse(list);
+});
 </script>
 
 <style lang="scss" scoped>

+ 12 - 14
src/components/IconSelect/index.vue

@@ -1,6 +1,6 @@
 <template>
-  <div class="relative" :style="{ width: width }">
-    <el-input v-model="modelValue" readonly @click="visible = !visible" placeholder="点击选择图标">
+  <div class="relative" :style="{ 'width': width }">
+    <el-input v-model="modelValue" readonly placeholder="点击选择图标" @click="visible = !visible">
       <template #prepend>
         <svg-icon :icon-class="modelValue" />
       </template>
@@ -8,18 +8,18 @@
 
     <el-popover shadow="none" :visible="visible" placement="bottom-end" trigger="click" :width="450">
       <template #reference>
-        <div @click="visible = !visible" class="cursor-pointer text-[#999] absolute right-[10px] top-0 height-[32px] leading-[32px]">
+        <div class="cursor-pointer text-[#999] absolute right-[10px] top-0 height-[32px] leading-[32px]" @click="visible = !visible">
           <i-ep-caret-top v-show="visible"></i-ep-caret-top>
           <i-ep-caret-bottom v-show="!visible"></i-ep-caret-bottom>
         </div>
       </template>
 
-      <el-input class="p-2" v-model="filterValue" placeholder="搜索图标" clearable @input="filterIcons" />
+      <el-input v-model="filterValue" class="p-2" placeholder="搜索图标" clearable @input="filterIcons" />
 
       <el-scrollbar height="w-[200px]">
         <ul class="icon-list">
           <el-tooltip v-for="(iconName, index) in iconNames" :key="index" :content="iconName" placement="bottom" effect="light">
-            <li :class="['icon-item', {active: modelValue == iconName}]" @click="selectedIcon(iconName)">
+            <li :class="['icon-item', { active: modelValue == iconName }]" @click="selectedIcon(iconName)">
               <svg-icon color="var(--el-text-color-regular)" :icon-class="iconName" />
             </li>
           </el-tooltip>
@@ -50,13 +50,11 @@ const filterValue = ref('');
  */
 const filterIcons = () => {
   if (filterValue.value) {
-    iconNames.value = icons.filter(iconName =>
-      iconName.includes(filterValue.value)
-    );
+    iconNames.value = icons.filter((iconName) => iconName.includes(filterValue.value));
   } else {
     iconNames.value = icons;
   }
-}
+};
 /**
  * 选择图标
  * @param iconName 选择的图标名称
@@ -64,12 +62,12 @@ const filterIcons = () => {
 const selectedIcon = (iconName: string) => {
   emit('update:modelValue', iconName);
   visible.value = false;
-}
+};
 </script>
 
 <style scoped lang="scss">
 .el-scrollbar {
-  max-height: calc(50vh - 100px)!important;
+  max-height: calc(50vh - 100px) !important;
   overflow-y: auto;
 }
 .el-divider--horizontal {
@@ -99,8 +97,8 @@ const selectedIcon = (iconName: string) => {
     }
   }
   .active {
-      border-color: var(--el-color-primary);
-      color: var(--el-color-primary);
-    }
+    border-color: var(--el-color-primary);
+    color: var(--el-color-primary);
+  }
 }
 </style>

+ 11 - 12
src/components/ImagePreview/index.vue

@@ -15,11 +15,11 @@ const props = defineProps({
   src: propTypes.string.def(''),
   width: {
     type: [Number, String],
-    default: ""
+    default: ''
   },
   height: {
     type: [Number, String],
-    default: ""
+    default: ''
   }
 });
 
@@ -27,29 +27,28 @@ const realSrc = computed(() => {
   if (!props.src) {
     return;
   }
-  let real_src = props.src.split(",")[0];
+  let real_src = props.src.split(',')[0];
   return real_src;
 });
 
 const realSrcList = computed(() => {
   if (!props.src) {
-    return;
+    return [];
   }
-  let real_src_list = props.src.split(",");
+  let real_src_list = props.src.split(',');
   let srcList: string[] = [];
-  real_src_list.forEach(item => {
+  real_src_list.forEach((item: string) => {
+    if(item.trim() === '') {
+      return;
+    }
     return srcList.push(item);
   });
   return srcList;
 });
 
-const realWidth = computed(() =>
-  typeof props.width == "string" ? props.width : `${props.width}px`
-);
+const realWidth = computed(() => (typeof props.width == 'string' ? props.width : `${props.width}px`));
 
-const realHeight = computed(() =>
-  typeof props.height == "string" ? props.height : `${props.height}px`
-);
+const realHeight = computed(() => (typeof props.height == 'string' ? props.height : `${props.height}px`));
 </script>
 
 <style lang="scss" scoped>

+ 137 - 119
src/components/ImageUpload/index.vue

@@ -1,6 +1,7 @@
 <template>
   <div class="component-upload-image">
     <el-upload
+      ref="imageUpload"
       multiple
       :action="uploadImgUrl"
       list-type="picture-card"
@@ -9,7 +10,6 @@
       :limit="limit"
       :on-error="handleUploadError"
       :on-exceed="handleExceed"
-      ref="imageUpload"
       :before-remove="handleDelete"
       :show-file-list="true"
       :headers="headers"
@@ -22,13 +22,13 @@
       </el-icon>
     </el-upload>
     <!-- 上传提示 -->
-    <div class="el-upload__tip" v-if="showTip">
+    <div v-if="showTip" class="el-upload__tip">
       请上传
       <template v-if="fileSize">
         大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
       </template>
       <template v-if="fileType">
-        格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b>
+        格式为 <b style="color: #f56c6c">{{ fileType.join('/') }}</b>
       </template>
       的文件
     </div>
@@ -40,177 +40,195 @@
 </template>
 
 <script setup lang="ts">
-import { listByIds, delOss } from "@/api/system/oss";
-import { ComponentInternalInstance } from "vue";
-import { OssVO } from "@/api/system/oss/types";
+import { listByIds, delOss } from '@/api/system/oss';
+import { OssVO } from '@/api/system/oss/types';
 import { propTypes } from '@/utils/propTypes';
-import {globalHeaders} from "@/utils/request";
+import { globalHeaders } from '@/utils/request';
+import { compressAccurately } from 'image-conversion';
 
 const props = defineProps({
-    modelValue: [String, Object, Array],
-    // 图片数量限制
-    limit: propTypes.number.def(5),
-    // 大小限制(MB)
-    fileSize: propTypes.number.def(5),
-    // 文件类型, 例如['png', 'jpg', 'jpeg']
-    fileType: propTypes.array.def(["png", "jpg", "jpeg"]),
-    // 是否显示提示
-    isShowTip: {
-        type: Boolean,
-        default: true
-    },
+  modelValue: {
+    type: [String, Object, Array],
+    default: () => []
+  },
+  // 图片数量限制
+  limit: propTypes.number.def(5),
+  // 大小限制(MB)
+  fileSize: propTypes.number.def(5),
+  // 文件类型, 例如['png', 'jpg', 'jpeg']
+  fileType: propTypes.array.def(['png', 'jpg', 'jpeg']),
+  // 是否显示提示
+  isShowTip: {
+    type: Boolean,
+    default: true
+  },
+  // 是否支持压缩,默认否
+  compressSupport: {
+    type: Boolean,
+    default: false
+  },
+  // 压缩目标大小,单位KB。默认300KB以上文件才压缩,并压缩至300KB以内
+  compressTargetSize: propTypes.number.def(300)
 });
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 const emit = defineEmits(['update:modelValue']);
 const number = ref(0);
 const uploadList = ref<any[]>([]);
-const dialogImageUrl = ref("");
+const dialogImageUrl = ref('');
 const dialogVisible = ref(false);
 
 const baseUrl = import.meta.env.VITE_APP_BASE_API;
-const uploadImgUrl = ref(baseUrl + "/resource/oss/upload"); // 上传的图片服务器地址
+const uploadImgUrl = ref(baseUrl + '/resource/oss/upload'); // 上传的图片服务器地址
 const headers = ref(globalHeaders());
 
 const fileList = ref<any[]>([]);
-const showTip = computed(
-    () => props.isShowTip && (props.fileType || props.fileSize)
-);
+const showTip = computed(() => props.isShowTip && (props.fileType || props.fileSize));
 
 const imageUploadRef = ref<ElUploadInstance>();
 
-watch(() => props.modelValue, async val => {
+watch(
+  () => props.modelValue,
+  async (val: string) => {
     if (val) {
-        // 首先将值转为数组
-        let list: OssVO[] = [];
-        if (Array.isArray(val)) {
-            list = val as OssVO[];
+      // 首先将值转为数组
+      let list: OssVO[] = [];
+      if (Array.isArray(val)) {
+        list = val as OssVO[];
+      } else {
+        const res = await listByIds(val);
+        list = res.data;
+      }
+      // 然后将数组转为对象数组
+      fileList.value = list.map((item) => {
+        // 字符串回显处理 如果此处存的是url可直接回显 如果存的是id需要调用接口查出来
+        let itemData;
+        if (typeof item === 'string') {
+          itemData = { name: item, url: item };
         } else {
-            const res = await listByIds(val as string)
-            list = res.data
+          // 此处name使用ossId 防止删除出现重名
+          itemData = { name: item.ossId, url: item.url, ossId: item.ossId };
         }
-        // 然后将数组转为对象数组
-        fileList.value = list.map(item => {
-            // 字符串回显处理 如果此处存的是url可直接回显 如果存的是id需要调用接口查出来
-            let itemData;
-            if (typeof item === "string") {
-                itemData = { name: item, url: item };
-            } else {
-                // 此处name使用ossId 防止删除出现重名
-                itemData = { name: item.ossId, url: item.url, ossId: item.ossId };
-            }
-            return itemData;
-        });
+        return itemData;
+      });
     } else {
-        fileList.value = [];
-        return [];
+      fileList.value = [];
+      return [];
     }
-}, { deep: true, immediate: true });
+  },
+  { deep: true, immediate: true }
+);
 
 /** 上传前loading加载 */
 const handleBeforeUpload = (file: any) => {
-    let isImg = false;
-    if (props.fileType.length) {
-        let fileExtension = "";
-        if (file.name.lastIndexOf(".") > -1) {
-            fileExtension = file.name.slice(file.name.lastIndexOf(".") + 1);
-        }
-        isImg = props.fileType.some((type: any) => {
-            if (file.type.indexOf(type) > -1) return true;
-            if (fileExtension && fileExtension.indexOf(type) > -1) return true;
-            return false;
-        });
-    } else {
-        isImg = file.type.indexOf("image") > -1;
+  let isImg = false;
+  if (props.fileType.length) {
+    let fileExtension = '';
+    if (file.name.lastIndexOf('.') > -1) {
+      fileExtension = file.name.slice(file.name.lastIndexOf('.') + 1);
     }
-    if (!isImg) {
-        proxy?.$modal.msgError(
-            `文件格式不正确, 请上传${props.fileType.join("/")}图片格式文件!`
-        );
-        return false;
+    isImg = props.fileType.some((type: any) => {
+      if (file.type.indexOf(type) > -1) return true;
+      if (fileExtension && fileExtension.indexOf(type) > -1) return true;
+      return false;
+    });
+  } else {
+    isImg = file.type.indexOf('image') > -1;
+  }
+  if (!isImg) {
+    proxy?.$modal.msgError(`文件格式不正确, 请上传${props.fileType.join('/')}图片格式文件!`);
+    return false;
+  }
+  if (props.fileSize) {
+    const isLt = file.size / 1024 / 1024 < props.fileSize;
+    if (!isLt) {
+      proxy?.$modal.msgError(`上传头像图片大小不能超过 ${props.fileSize} MB!`);
+      return false;
     }
-    if (props.fileSize) {
-        const isLt = file.size / 1024 / 1024 < props.fileSize;
-        if (!isLt) {
-            proxy?.$modal.msgError(`上传头像图片大小不能超过 ${props.fileSize} MB!`);
-            return false;
-        }
-    }
-    proxy?.$modal.loading("正在上传图片,请稍候...");
+  }
+
+  //压缩图片,开启压缩并且大于指定的压缩大小时才压缩
+  if (props.compressSupport && file.size / 1024 > props.compressTargetSize) {
+    proxy?.$modal.loading('正在上传图片,请稍候...');
     number.value++;
-}
+    return compressAccurately(file, props.compressTargetSize);
+  } else {
+    proxy?.$modal.loading('正在上传图片,请稍候...');
+    number.value++;
+  }
+};
 
 // 文件个数超出
 const handleExceed = () => {
-    proxy?.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`);
-}
+  proxy?.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`);
+};
 
 // 上传成功回调
 const handleUploadSuccess = (res: any, file: UploadFile) => {
-    if (res.code === 200) {
-        uploadList.value.push({ name: res.data.fileName, url: res.data.url, ossId: res.data.ossId });
-        uploadedSuccessfully();
-    } else {
-        number.value--;
-        proxy?.$modal.closeLoading();
-        proxy?.$modal.msgError(res.msg);
-        imageUploadRef.value?.handleRemove(file);
-        uploadedSuccessfully();
-    }
-}
+  if (res.code === 200) {
+    uploadList.value.push({ name: res.data.fileName, url: res.data.url, ossId: res.data.ossId });
+    uploadedSuccessfully();
+  } else {
+    number.value--;
+    proxy?.$modal.closeLoading();
+    proxy?.$modal.msgError(res.msg);
+    imageUploadRef.value?.handleRemove(file);
+    uploadedSuccessfully();
+  }
+};
 
 // 删除图片
 const handleDelete = (file: UploadFile): boolean => {
-    const findex = fileList.value.map(f => f.name).indexOf(file.name);
-    if (findex > -1 && uploadList.value.length === number.value) {
-        let ossId = fileList.value[findex].ossId;
-        delOss(ossId);
-        fileList.value.splice(findex, 1);
-        emit("update:modelValue", listToString(fileList.value));
-        return false;
-    }
-    return true;
-}
+  const findex = fileList.value.map((f) => f.name).indexOf(file.name);
+  if (findex > -1 && uploadList.value.length === number.value) {
+    let ossId = fileList.value[findex].ossId;
+    delOss(ossId);
+    fileList.value.splice(findex, 1);
+    emit('update:modelValue', listToString(fileList.value));
+    return false;
+  }
+  return true;
+};
 
 // 上传结束处理
 const uploadedSuccessfully = () => {
-    if (number.value > 0 && uploadList.value.length === number.value) {
-        fileList.value = fileList.value.filter(f => f.url !== undefined).concat(uploadList.value);
-        uploadList.value = [];
-        number.value = 0;
-        emit("update:modelValue", listToString(fileList.value));
-        proxy?.$modal.closeLoading();
-    }
-}
+  if (number.value > 0 && uploadList.value.length === number.value) {
+    fileList.value = fileList.value.filter((f) => f.url !== undefined).concat(uploadList.value);
+    uploadList.value = [];
+    number.value = 0;
+    emit('update:modelValue', listToString(fileList.value));
+    proxy?.$modal.closeLoading();
+  }
+};
 
 // 上传失败
 const handleUploadError = () => {
-    proxy?.$modal.msgError("上传图片失败");
-    proxy?.$modal.closeLoading();
-}
+  proxy?.$modal.msgError('上传图片失败');
+  proxy?.$modal.closeLoading();
+};
 
 // 预览
 const handlePictureCardPreview = (file: any) => {
-    dialogImageUrl.value = file.url;
-    dialogVisible.value = true;
-}
+  dialogImageUrl.value = file.url;
+  dialogVisible.value = true;
+};
 
 // 对象转成指定字符串分隔
 const listToString = (list: any[], separator?: string) => {
-    let strs = "";
-    separator = separator || ",";
-    for (let i in list) {
-        if (undefined !== list[i].ossId && list[i].url.indexOf("blob:") !== 0) {
-            strs += list[i].ossId + separator;
-        }
+  let strs = '';
+  separator = separator || ',';
+  for (let i in list) {
+    if (undefined !== list[i].ossId && list[i].url.indexOf('blob:') !== 0) {
+      strs += list[i].ossId + separator;
     }
-    return strs != "" ? strs.substring(0, strs.length - 1) : "";
-}
+  }
+  return strs != '' ? strs.substring(0, strs.length - 1) : '';
+};
 </script>
 
 <style scoped lang="scss">
 // .el-upload--picture-card 控制加号部分
 :deep(.hide .el-upload--picture-card) {
-    display: none;
+  display: none;
 }
 </style>

+ 5 - 6
src/components/LangSelect/index.vue

@@ -14,22 +14,21 @@
 
 <script setup lang="ts">
 import { useI18n } from 'vue-i18n';
-import SvgIcon from '@/components/SvgIcon/index.vue';
 import { useAppStore } from '@/store/modules/app';
+import SvgIcon from '@/components/SvgIcon/index.vue';
 
 const appStore = useAppStore();
 const { locale } = useI18n();
 
-
 const message: any = {
   zh_CN: '切换语言成功!',
-  en_US: 'Switch Language Successful!',
-}
-const handleLanguageChange = (lang: string) => {
+  en_US: 'Switch Language Successful!'
+};
+const handleLanguageChange = (lang: any) => {
   locale.value = lang;
   appStore.changeLanguage(lang);
   ElMessage.success(message[lang] || '切换语言成功!');
-}
+};
 </script>
 
 <style lang="scss" scoped>

Some files were not shown because too many files changed in this diff