@@ -120,18 +120,22 @@
### 工作流程
-| | 功能 | 描述 |
-|-----|-------|----------------------------------------|
-| 🚀 | 流程模型 | 配置工作流的流程模型,支持文件导入与在线设计流程图,提供 7 种任务分配规则 |
-| 🚀 | 流程表单 | 拖动表单元素生成相应的工作流表单,覆盖 Element UI 所有的表单组件 |
-| 🚀 | 用户分组 | 自定义用户分组,可用于工作流的审批分组 |
-| 🚀 | 我的流程 | 查看我发起的工作流程,支持新建、取消流程等操作,高亮流程图、审批时间线 |
-| 🚀 | 待办任务 | 查看自己【未】审批的工作任务,支持通过、不通过、转发、委派、退回等操作 |
-| 🚀 | 已办任务 | 查看自己【已】审批的工作任务,未来会支持回退操作 |
-| 🚀 | OA 请假 | 作为业务自定义接入工作流的使用示例,只需创建请求对应的工作流程,即可进行审批 |
+| | 功能 | 描述 |
+|----|-------|-----------------------------------------|
+| 🚀 | 流程模型 | 配置工作流的流程模型,支持 BPMN 和仿钉钉/飞书设计器 |
+| 🚀 | 流程表单 | 拖动表单元素生成相应的工作流表单,覆盖 Element UI 所有的表单组件 |
+| 🚀 | 用户分组 | 自定义用户分组,可用于工作流的审批分组 |
+| 🚀 | 我的流程 | 查看我发起的工作流程,支持新建、取消流程等操作,高亮流程图、审批时间线 |
+| 🚀 | 待办任务 | 查看自己【未】审批的工作任务,支持通过、不通过、转派、委派、退回、加减签等操作 |
+| 🚀 | 已办任务 | 查看自己【已】审批的工作任务,支持流程预测,展示未来审批人信息 |
+| 🚀 | OA 请假 | 作为业务自定义接入工作流的使用示例,只需创建请求对应的工作流程,即可进行审批 |

+| BPMN 设计器 | 钉钉/飞书设计器 |
+|------------------------------|--------------------------------|
+|  |  |
+
### 支付系统
| | 功能 | 描述 |
@@ -64,6 +64,7 @@
"pinia-plugin-persistedstate": "^3.2.1",
"qrcode": "^1.5.3",
"qs": "^6.12.0",
+ "sortablejs": "^1.15.3",
"steady-xml": "^0.1.0",
"url": "^0.11.3",
"video.js": "^7.21.5",
@@ -95,7 +96,7 @@
"@vitejs/plugin-vue": "^5.0.4",
"@vitejs/plugin-vue-jsx": "^3.1.0",
"autoprefixer": "^10.4.17",
- "bpmn-js": "8.9.0",
+ "bpmn-js": "8.10.0",
"bpmn-js-properties-panel": "0.46.0",
"consola": "^3.2.3",
"eslint": "^8.57.0",
@@ -13,10 +13,10 @@ importers:
version: 2.3.1(vue@3.5.12(typescript@5.3.3))
'@form-create/designer':
specifier: ^3.2.6
- version: 3.2.7(vue@3.5.12(typescript@5.3.3))
+ version: 3.2.8(vue@3.5.12(typescript@5.3.3))
'@form-create/element-ui':
specifier: ^3.2.11
- version: 3.2.11(vue@3.5.12(typescript@5.3.3))
+ version: 3.2.13(vue@3.5.12(typescript@5.3.3))
'@iconify/iconify':
specifier: ^3.1.1
version: 3.1.1
@@ -125,6 +125,9 @@ importers:
qs:
specifier: ^6.12.0
version: 6.12.1
+ sortablejs:
+ specifier: ^1.15.3
+ version: 1.15.3
steady-xml:
specifier: ^0.1.0
version: 0.1.0
@@ -214,11 +217,11 @@ importers:
specifier: ^10.4.17
version: 10.4.19(postcss@8.4.38)
bpmn-js:
- specifier: 8.9.0
- version: 8.9.0
+ specifier: 8.10.0
+ version: 8.10.0
bpmn-js-properties-panel:
specifier: 0.46.0
- version: 0.46.0(bpmn-js@8.9.0)
+ version: 0.46.0(bpmn-js@8.10.0)
consola:
specifier: ^3.2.3
version: 3.2.3
@@ -296,7 +299,7 @@ importers:
version: 0.8.0(rollup@4.17.1)
unplugin-vue-components:
specifier: ^0.25.2
- version: 0.25.2(@babel/parser@7.25.8)(rollup@4.17.1)(vue@3.5.12(typescript@5.3.3))
+ version: 0.25.2(@babel/parser@7.26.2)(rollup@4.17.1)(vue@3.5.12(typescript@5.3.3))
vite:
specifier: 5.1.4
version: 5.1.4(@types/node@20.12.7)(sass@1.75.0)(terser@5.30.4)
@@ -451,16 +454,16 @@ packages:
resolution: {integrity: sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==}
engines: {node: '>=6.9.0'}
- '@babel/helper-string-parser@7.25.7':
- resolution: {integrity: sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==, tarball: https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz}
+ '@babel/helper-string-parser@7.25.9':
+ resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==, tarball: https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz}
'@babel/helper-validator-identifier@7.22.20':
resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==}
- '@babel/helper-validator-identifier@7.25.7':
- resolution: {integrity: sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==, tarball: https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz}
+ '@babel/helper-validator-identifier@7.25.9':
+ resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==, tarball: https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz}
'@babel/helper-validator-option@7.23.5':
@@ -484,8 +487,8 @@ packages:
engines: {node: '>=6.0.0'}
hasBin: true
- '@babel/parser@7.25.8':
- resolution: {integrity: sha512-HcttkxzdPucv3nNFmfOOMfFf64KgdJVqm1KaCm25dPGMLElo9nsLvXeJECQg8UzPuBGLyTSA0ZzqCtDSzKTEoQ==, tarball: https://registry.npmmirror.com/@babel/parser/-/parser-7.25.8.tgz}
+ '@babel/parser@7.26.2':
+ resolution: {integrity: sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==, tarball: https://registry.npmmirror.com/@babel/parser/-/parser-7.26.2.tgz}
@@ -961,8 +964,8 @@ packages:
resolution: {integrity: sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==}
- '@babel/types@7.25.8':
- resolution: {integrity: sha512-JWtuCu8VQsMladxVz/P4HzHUGCAwpuqacmowgXFs5XjxIgKuNjnLokQzuVjlTvIzODaDmpjT3oxcC48vyk9EWg==, tarball: https://registry.npmmirror.com/@babel/types/-/types-7.25.8.tgz}
+ '@babel/types@7.26.0':
+ resolution: {integrity: sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==, tarball: https://registry.npmmirror.com/@babel/types/-/types-7.26.0.tgz}
'@bpmn-io/diagram-js-ui@0.2.3':
@@ -1070,7 +1073,7 @@ packages:
postcss-selector-parser: ^6.0.13
'@ctrl/tinycolor@3.6.1':
- resolution: {integrity: sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==, tarball: https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz}
+ resolution: {integrity: sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==}
engines: {node: '>=10'}
'@dual-bundle/import-meta-resolve@4.0.0':
@@ -1238,13 +1241,13 @@ packages:
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
'@floating-ui/core@1.6.1':
- resolution: {integrity: sha512-42UH54oPZHPdRHdw6BgoBD6cg/eVTmVrFcgeRDM3jbO7uxSoipVcmcIGFcA5jmOHO5apcyvBhkSKES3fQJnu7A==, tarball: https://registry.npmmirror.com/@floating-ui/core/-/core-1.6.1.tgz}
+ resolution: {integrity: sha512-42UH54oPZHPdRHdw6BgoBD6cg/eVTmVrFcgeRDM3jbO7uxSoipVcmcIGFcA5jmOHO5apcyvBhkSKES3fQJnu7A==}
'@floating-ui/dom@1.6.4':
- resolution: {integrity: sha512-0G8R+zOvQsAG1pg2Q99P21jiqxqGBW1iRe/iXHsBRBxnpXKFI8QwbB4x5KmYLggNO5m34IQgOIu9SCRfR/WWiQ==, tarball: https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.6.4.tgz}
+ resolution: {integrity: sha512-0G8R+zOvQsAG1pg2Q99P21jiqxqGBW1iRe/iXHsBRBxnpXKFI8QwbB4x5KmYLggNO5m34IQgOIu9SCRfR/WWiQ==}
'@floating-ui/utils@0.2.2':
- resolution: {integrity: sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw==, tarball: https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.2.tgz}
+ resolution: {integrity: sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw==}
'@form-create/component-elm-checkbox@3.2.8':
resolution: {integrity: sha512-ol/SyzzeDueUTd87MPnYydOe7Sc6cL8S/Uhv5QmWofMY1TuuBet9DPb65JjyS6Lk51/cl3TabvtJj93EAxL6KA==, tarball: https://registry.npmmirror.com/@form-create/component-elm-checkbox/-/component-elm-checkbox-3.2.8.tgz}
@@ -1264,8 +1267,8 @@ packages:
'@form-create/component-elm-tree@3.2.9':
resolution: {integrity: sha512-5NG4YeFZ5jzN9Aa0JFuFD8OGKXBqSHSN0KRgxxUgdhzRg8hcRq/JODuN7yYMa7YrBP0ecTKyel8Q4ufR5Ct8iw==, tarball: https://registry.npmmirror.com/@form-create/component-elm-tree/-/component-elm-tree-3.2.9.tgz}
- '@form-create/component-elm-upload@3.2.9':
- resolution: {integrity: sha512-PdYlUCRs7x/zQjkDkTX9q3116ysKUPZ4R4OwzhSc430JPLSVUCx/CqlhenbAnqZFEj5khwnvppbYSzrTTaDa4A==, tarball: https://registry.npmmirror.com/@form-create/component-elm-upload/-/component-elm-upload-3.2.9.tgz}
+ '@form-create/component-elm-upload@3.2.13':
+ resolution: {integrity: sha512-qngh1Hzb/Oo51gbh3LDiMmUnDaa2+k7sXS4GEZujoDuKCctBjG60y3pi214CmOqBq9PiynM8knf6yQKqpjlRqA==, tarball: https://registry.npmmirror.com/@form-create/component-elm-upload/-/component-elm-upload-3.2.13.tgz}
'@form-create/component-subform@3.1.34':
resolution: {integrity: sha512-OJcFH/7MTHx7JLEjDK/weS27qfuFWAI+OK+gXTJ2jIt9aZkGWF/EWkjetiJLt5a0KMw4Z15wOS2XCY9pVK9vlA==, tarball: https://registry.npmmirror.com/@form-create/component-subform/-/component-subform-3.1.34.tgz}
@@ -1273,18 +1276,18 @@ packages:
'@form-create/component-wangeditor@3.1.20':
resolution: {integrity: sha512-lAjpltmYfr3a2AeXasCehGsZNL/1WB6vWqqV9TIsJ4pleTr0/D/oPwEYQjfv+gG+NoB2Sa25SRGhtlnephjyhg==, tarball: https://registry.npmmirror.com/@form-create/component-wangeditor/-/component-wangeditor-3.1.20.tgz}
- '@form-create/core@3.2.11':
- resolution: {integrity: sha512-xcaAxFSpAaVRWSpZ3ikrr89OmGidtN1y2YC7mQcQ/Hs7KvdbipH2I27JF5qm98+S7gs/e3Z9jrscngmSwsLw7g==, tarball: https://registry.npmmirror.com/@form-create/core/-/core-3.2.11.tgz}
+ '@form-create/core@3.2.13':
+ resolution: {integrity: sha512-HVLfZ5gf9DRO74OJTw3bt/GwFXhyBWvMmrOG9WkRTEQEMIeGOWudH843iaYp2ljgJN6jrn3RcCfONC9nzAmk8g==, tarball: https://registry.npmmirror.com/@form-create/core/-/core-3.2.13.tgz}
peerDependencies:
vue: ^3.1.0
- '@form-create/designer@3.2.7':
- resolution: {integrity: sha512-jLpX51yXt2SOmsGOiDey5wq6K6gQLfd7CcGtW6zH2tDQTJd4ddS/QstVKmei6ddIwA9GWuk3JWnktGLk4ry2sg==, tarball: https://registry.npmmirror.com/@form-create/designer/-/designer-3.2.7.tgz}
+ '@form-create/designer@3.2.8':
+ resolution: {integrity: sha512-SgrGiWOFaQTARAmysepHDtFyRi97rERrlkv1joz+DCOAzZME3RKRTXVqA7ALzJ2jI3psiCosGAK4rPSLh6EvgA==, tarball: https://registry.npmmirror.com/@form-create/designer/-/designer-3.2.8.tgz}
vue: ^3.1.5
- '@form-create/element-ui@3.2.11':
- resolution: {integrity: sha512-cJpKuu5zGNJK5TlsXTLqfc972aAVYk4q2ljn0ERfxM89oRl+2tkatOVr2vPYqGj/Z4Ufpr1R/ZP+RGGl2jVIHQ==, tarball: https://registry.npmmirror.com/@form-create/element-ui/-/element-ui-3.2.11.tgz}
+ '@form-create/element-ui@3.2.13':
+ resolution: {integrity: sha512-b/ilL9/huwQhXhGM3irzKTqlF7n69ld/CPiLQzuzSEUFC4VqBNFcy8kpCQ9/j4+VvuEPCvro1ax3v4V7Mxol9g==, tarball: https://registry.npmmirror.com/@form-create/element-ui/-/element-ui-3.2.13.tgz}
@@ -1437,7 +1440,7 @@ packages:
resolution: {integrity: sha512-s2t+1oVtGDV6KtqfCXtUOhxfeYvOdDF90IVm+nMs/6bUP0HeGZLslguuL/AibpwtfL4FA/oCsIu/RhwapgAdJw==}
'@rollup/plugin-virtual@3.0.2':
- resolution: {integrity: sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==, tarball: https://registry.npmmirror.com/@rollup/plugin-virtual/-/plugin-virtual-3.0.2.tgz}
+ resolution: {integrity: sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==}
engines: {node: '>=14.0.0'}
rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
@@ -1615,7 +1618,7 @@ packages:
os: [win32]
'@swc/core@1.7.26':
- resolution: {integrity: sha512-f5uYFf+TmMQyYIoxkn/evWhNGuUzC730dFwAKGwBVHHVoPyak1/GvJUm6i1SKl+2Hrj9oN0i3WSoWWZ4pgI8lw==, tarball: https://registry.npmmirror.com/@swc/core/-/core-1.7.26.tgz}
+ resolution: {integrity: sha512-f5uYFf+TmMQyYIoxkn/evWhNGuUzC730dFwAKGwBVHHVoPyak1/GvJUm6i1SKl+2Hrj9oN0i3WSoWWZ4pgI8lw==}
'@swc/helpers': '*'
@@ -1624,10 +1627,10 @@ packages:
optional: true
'@swc/counter@0.1.3':
- resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==, tarball: https://registry.npmmirror.com/@swc/counter/-/counter-0.1.3.tgz}
+ resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
'@swc/types@0.1.12':
- resolution: {integrity: sha512-wBJA+SdtkbFhHjTMYH+dEH1y4VpfGdAc2Kw/LK09i9bXd/K6j6PkDcFCEzb6iVfZMkPRrl/q0e3toqTAJdkIVA==, tarball: https://registry.npmmirror.com/@swc/types/-/types-0.1.12.tgz}
+ resolution: {integrity: sha512-wBJA+SdtkbFhHjTMYH+dEH1y4VpfGdAc2Kw/LK09i9bXd/K6j6PkDcFCEzb6iVfZMkPRrl/q0e3toqTAJdkIVA==}
'@sxzz/popperjs-es@2.11.7':
resolution: {integrity: sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==, tarball: https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz}
@@ -1781,7 +1784,7 @@ packages:
resolution: {integrity: sha512-1CQjuSrgbv1/dhmcfQ83eVyYbvGyqhTvb2Opxr0QCV+iJ4J6/J+XWQ3Om59WiwCd1MN3rDUHasx5XRrpUtewYQ==, tarball: https://registry.npmmirror.com/@types/video.js/-/video.js-7.3.58.tgz}
'@types/web-bluetooth@0.0.16':
- resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==, tarball: https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz}
+ resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==}
'@types/web-bluetooth@0.0.20':
resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==}
@@ -2127,19 +2130,19 @@ packages:
resolution: {integrity: sha512-/1vjTol8SXnx6xewDEKfS0Ra//ncg4Hb0DaZiwKf7drgfMsKFExQ+FnnENcN6efPen+1kIzhLQoGSy0eDUVOMg==}
'@vueuse/core@9.13.0':
- resolution: {integrity: sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==, tarball: https://registry.npmmirror.com/@vueuse/core/-/core-9.13.0.tgz}
+ resolution: {integrity: sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==}
'@vueuse/metadata@10.9.0':
resolution: {integrity: sha512-iddNbg3yZM0X7qFY2sAotomgdHK7YJ6sKUvQqbvwnf7TmaVPxS4EJydcNsVejNdS8iWCtDk+fYXr7E32nyTnGA==}
'@vueuse/metadata@9.13.0':
- resolution: {integrity: sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==, tarball: https://registry.npmmirror.com/@vueuse/metadata/-/metadata-9.13.0.tgz}
+ resolution: {integrity: sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==}
'@vueuse/shared@10.9.0':
resolution: {integrity: sha512-Uud2IWncmAfJvRaFYzv5OHDli+FbOzxiVEQdLCKQKLyhz94PIyFC3CHcH7EDMwIn8NPtD06+PNbC/PiO0LGLtw==}
'@vueuse/shared@9.13.0':
- resolution: {integrity: sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==, tarball: https://registry.npmmirror.com/@vueuse/shared/-/shared-9.13.0.tgz}
+ resolution: {integrity: sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==}
'@wangeditor/basic-modules@1.1.7':
resolution: {integrity: sha512-cY9CPkLJaqF05STqfpZKWG4LpxTMeGSIIF1fHvfm/mz+JXatCagjdkbxdikOuKYlxDdeqvOeBmsUBItufDLXZg==}
@@ -2347,7 +2350,7 @@ packages:
engines: {node: '>=8'}
async-validator@4.2.5:
- resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==, tarball: https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz}
+ resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==}
async@3.2.5:
resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==}
@@ -2432,11 +2435,11 @@ packages:
bpmn-js-token-simulation@0.10.0:
resolution: {integrity: sha512-QuZQ/KVXKt9Vl+XENyOBoTW2Aw+uKjuBlKdCJL6El7AyM7DkJ5bZkSYURshId1SkBDdYg2mJ1flSmsrhGuSfwg==}
- bpmn-js@8.9.0:
- resolution: {integrity: sha512-cthSxiJUpEHspiUKiL0YA8/mRCYngNKwALWieLKPtFo42n+vWTFgmxnASNRwhxpPEbSXjYuTah1lZ0lSyLWPpw==}
+ bpmn-js@8.10.0:
+ resolution: {integrity: sha512-NozeOi01qL0ZdVq8+5hWZcikyEvgrP1yzCBqlhSufJdHFsnEMBCwn2bJJ0B/6JgX+IBwy1sk/Uw+Ds8rQ8vfrw==, tarball: https://registry.npmmirror.com/bpmn-js/-/bpmn-js-8.10.0.tgz}
bpmn-moddle@7.1.3:
- resolution: {integrity: sha512-ZcBfw0NSOdYTSXFKEn7MOXHItz7VfLZTrFYKO8cK6V8ZzGjCcdiLIOiw7Lctw1PJsihhLiZQS8Htj2xKf+NwCg==}
+ resolution: {integrity: sha512-ZcBfw0NSOdYTSXFKEn7MOXHItz7VfLZTrFYKO8cK6V8ZzGjCcdiLIOiw7Lctw1PJsihhLiZQS8Htj2xKf+NwCg==, tarball: https://registry.npmmirror.com/bpmn-moddle/-/bpmn-moddle-7.1.3.tgz}
brace-expansion@1.1.11:
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
@@ -2733,7 +2736,7 @@ packages:
engines: {node: '>= 6'}
css.escape@1.5.1:
- resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==}
+ resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==, tarball: https://registry.npmmirror.com/css.escape/-/css.escape-1.5.1.tgz}
cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
@@ -2968,7 +2971,7 @@ packages:
resolution: {integrity: sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==}
diagram-js-direct-editing@1.8.0:
- resolution: {integrity: sha512-B4Xj+PJfgBjbPEzT3uZQEkZI5xHFB0Izc+7BhDFuHidzrEMzQKZrFGdA3PqfWhReHf3dp+iB6Tt11G9eGNjKMw==}
+ resolution: {integrity: sha512-B4Xj+PJfgBjbPEzT3uZQEkZI5xHFB0Izc+7BhDFuHidzrEMzQKZrFGdA3PqfWhReHf3dp+iB6Tt11G9eGNjKMw==, tarball: https://registry.npmmirror.com/diagram-js-direct-editing/-/diagram-js-direct-editing-1.8.0.tgz}
diagram-js: '*'
@@ -2976,10 +2979,10 @@ packages:
resolution: {integrity: sha512-LF9BiwjbOPpZd0ez5VSlYRbdbEA59YQX43bWvNDp1rLMv0xwZ5yIg4oaYDK82nIQ0kH1tjvoQRpNevMTCgQVyw==}
diagram-js@7.9.0:
- resolution: {integrity: sha512-o1yUtX5TXV1pmpevP55gxU/AEG6nCidOXGs/HLuxNXG0zMZ3jQta7kMqRxTK93rNw/XuHmP1eMOwdvdJ2RP5qA==}
+ resolution: {integrity: sha512-o1yUtX5TXV1pmpevP55gxU/AEG6nCidOXGs/HLuxNXG0zMZ3jQta7kMqRxTK93rNw/XuHmP1eMOwdvdJ2RP5qA==, tarball: https://registry.npmmirror.com/diagram-js/-/diagram-js-7.9.0.tgz}
didi@5.2.1:
- resolution: {integrity: sha512-IKNnajUlD4lWMy/Q9Emkk7H1qnzREgY4UyE3IhmOi/9IKua0JYtYldk928bOdt1yNxN8EiOy1sqtSozEYsmjCg==}
+ resolution: {integrity: sha512-IKNnajUlD4lWMy/Q9Emkk7H1qnzREgY4UyE3IhmOi/9IKua0JYtYldk928bOdt1yNxN8EiOy1sqtSozEYsmjCg==, tarball: https://registry.npmmirror.com/didi/-/didi-5.2.1.tgz}
didi@9.0.2:
resolution: {integrity: sha512-q2+aj+lnJcUweV7A9pdUrwFr4LHVmRPwTmQLtHPFz4aT7IBoryN6Iy+jmFku+oIzr5ebBkvtBCOb87+dJhb7bg==}
@@ -3075,7 +3078,7 @@ packages:
resolution: {integrity: sha512-9ItEpeu15hW5m8jKdriL+BQrgwDTXEL9pn4SkillWFu73ZNNNQ2BKKLS+ZHv2vC9UkNhosAeyfxOf/5OSeTCPA==}
element-plus@2.8.4:
- resolution: {integrity: sha512-ZlVAdUOoJliv4kW3ntWnnSHMT+u/Os7mXJjk2xzOlqNeHaI2/ozlF+R58ZCEak8ZnDi6+5A2viWEYRsq64IuiA==, tarball: https://registry.npmmirror.com/element-plus/-/element-plus-2.8.4.tgz}
+ resolution: {integrity: sha512-ZlVAdUOoJliv4kW3ntWnnSHMT+u/Os7mXJjk2xzOlqNeHaI2/ozlF+R58ZCEak8ZnDi6+5A2viWEYRsq64IuiA==}
vue: ^3.2.0
@@ -3160,7 +3163,7 @@ packages:
engines: {node: '>=6'}
escape-html@1.0.3:
- resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==, tarball: https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz}
+ resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
escape-string-regexp@1.0.5:
resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
@@ -4019,7 +4022,7 @@ packages:
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
lodash-unified@1.0.3:
- resolution: {integrity: sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==, tarball: https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz}
+ resolution: {integrity: sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==}
'@types/lodash-es': '*'
lodash: '*'
@@ -4158,7 +4161,7 @@ packages:
resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
memoize-one@6.0.0:
- resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==, tarball: https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz}
+ resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==}
meow@12.1.1:
resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==}
@@ -4207,7 +4210,7 @@ packages:
engines: {node: '>=12'}
min-dash@3.8.1:
- resolution: {integrity: sha512-evumdlmIlg9mbRVPbC4F5FuRhNmcMS5pvuBUbqb1G9v09Ro0ImPEgz5n3khir83lFok1inKqVDjnKEg3GpDxQg==}
+ resolution: {integrity: sha512-evumdlmIlg9mbRVPbC4F5FuRhNmcMS5pvuBUbqb1G9v09Ro0ImPEgz5n3khir83lFok1inKqVDjnKEg3GpDxQg==, tarball: https://registry.npmmirror.com/min-dash/-/min-dash-3.8.1.tgz}
min-dash@4.2.1:
resolution: {integrity: sha512-to+unsToePnm7cUeR9TrMzFlETHd/UXmU+ELTRfWZj5XGT41KF6X3L233o3E/GdEs3sk2Tbw/lOLD1avmWkg8A==}
@@ -4260,10 +4263,10 @@ packages:
resolution: {integrity: sha512-vLgaHvaeunuOXHSmEbZ9izxPx3USsk8KCQ8iC+aTlp5sKRSoZvwhHh5L9VbKSaVC6sJDqbyohIS76E2VmHIPAA==}
moddle-xml@9.0.6:
- resolution: {integrity: sha512-tl0reHpsY/aKlLGhXeFlQWlYAQHFxTkFqC8tq8jXRYpQSnLVw13T6swMaourLd7EXqHdWsc+5ggsB+fEep6xZQ==}
+ resolution: {integrity: sha512-tl0reHpsY/aKlLGhXeFlQWlYAQHFxTkFqC8tq8jXRYpQSnLVw13T6swMaourLd7EXqHdWsc+5ggsB+fEep6xZQ==, tarball: https://registry.npmmirror.com/moddle-xml/-/moddle-xml-9.0.6.tgz}
moddle@5.0.4:
- resolution: {integrity: sha512-Kjb+hjuzO+YlojNGxEUXvdhLYTHTtAABDlDcJTtTcn5MbJF9Zkv4I1Fyvp3Ypmfgg1EfHDZ3PsCQTuML9JD6wg==}
+ resolution: {integrity: sha512-Kjb+hjuzO+YlojNGxEUXvdhLYTHTtAABDlDcJTtTcn5MbJF9Zkv4I1Fyvp3Ypmfgg1EfHDZ3PsCQTuML9JD6wg==, tarball: https://registry.npmmirror.com/moddle/-/moddle-5.0.4.tgz}
mpd-parser@0.22.1:
resolution: {integrity: sha512-fwBebvpyPUU8bOzvhX0VQZgSohncbgYwUyJJoTSNpmy7ccD2ryiCvM7oRkn/xQH5cv73/xU7rJSNCLjdGFor0Q==}
@@ -4329,7 +4332,7 @@ packages:
engines: {node: '>=0.10.0'}
normalize-wheel-es@1.2.0:
- resolution: {integrity: sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==, tarball: https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz}
+ resolution: {integrity: sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==}
npm-run-path@4.0.1:
resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
@@ -4364,7 +4367,7 @@ packages:
engines: {node: '>= 0.4'}
object-refs@0.3.0:
- resolution: {integrity: sha512-eP0ywuoWOaDoiake/6kTJlPJhs+k0qNm4nYRzXLNHj6vh+5M3i9R1epJTdxIPGlhWc4fNRQ7a6XJNCX+/L4FOQ==}
+ resolution: {integrity: sha512-eP0ywuoWOaDoiake/6kTJlPJhs+k0qNm4nYRzXLNHj6vh+5M3i9R1epJTdxIPGlhWc4fNRQ7a6XJNCX+/L4FOQ==, tarball: https://registry.npmmirror.com/object-refs/-/object-refs-0.3.0.tgz}
object-visit@1.0.1:
resolution: {integrity: sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==}
@@ -4491,8 +4494,8 @@ packages:
picocolors@1.0.0:
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
- picocolors@1.1.0:
- resolution: {integrity: sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==, tarball: https://registry.npmmirror.com/picocolors/-/picocolors-1.1.0.tgz}
+ picocolors@1.1.1:
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==, tarball: https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz}
picomatch@2.3.1:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
@@ -4852,7 +4855,7 @@ packages:
resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==}
saxen@8.1.2:
- resolution: {integrity: sha512-xUOiiFbc3Ow7p8KMxwsGICPx46ZQvy3+qfNVhrkwfz3Vvq45eGt98Ft5IQaA1R/7Tb5B5MKh9fUR9x3c3nDTxw==}
+ resolution: {integrity: sha512-xUOiiFbc3Ow7p8KMxwsGICPx46ZQvy3+qfNVhrkwfz3Vvq45eGt98Ft5IQaA1R/7Tb5B5MKh9fUR9x3c3nDTxw==, tarball: https://registry.npmmirror.com/saxen/-/saxen-8.1.2.tgz}
scroll-into-view-if-needed@2.2.31:
resolution: {integrity: sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==}
@@ -4956,6 +4959,9 @@ packages:
sortablejs@1.14.0:
resolution: {integrity: sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==}
+ sortablejs@1.15.3:
+ resolution: {integrity: sha512-zdK3/kwwAK1cJgy1rwl1YtNTbRmc8qW/+vgXf75A7NHag5of4pyI6uK86ktmQETyWRH7IGaE73uZOOBcGxgqZg==}
source-map-js@1.2.0:
resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==}
@@ -4980,7 +4986,7 @@ packages:
source-map@0.6.1:
- resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==, tarball: https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz}
+ resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
split-string@3.1.0:
@@ -5167,7 +5173,7 @@ packages:
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
tiny-svg@2.2.4:
- resolution: {integrity: sha512-NOi39lBknf4UdDEahNkbEAJnzhu1ZcN2j75IS2vLRmIhsfxdZpTChfLKBcN1ShplVmPIXJAIafk6YY5/Aa80lQ==}
+ resolution: {integrity: sha512-NOi39lBknf4UdDEahNkbEAJnzhu1ZcN2j75IS2vLRmIhsfxdZpTChfLKBcN1ShplVmPIXJAIafk6YY5/Aa80lQ==, tarball: https://registry.npmmirror.com/tiny-svg/-/tiny-svg-2.2.4.tgz}
tiny-svg@3.0.1:
resolution: {integrity: sha512-P8T4iwiW1t95vpHVHqrD36Brn7TqFYCPSHIWk9WLJtYK1X4aDd+5cgqcAADIWSjf1/i5idKnpCh9mim8hEdRBg==}
@@ -5376,7 +5382,7 @@ packages:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
uuid@10.0.0:
- resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==, tarball: https://registry.npmmirror.com/uuid/-/uuid-10.0.0.tgz}
+ resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
vary@1.1.2:
@@ -5426,7 +5432,7 @@ packages:
vite: '>=2.0.0'
vite-plugin-top-level-await@1.4.4:
- resolution: {integrity: sha512-QyxQbvcMkgt+kDb12m2P8Ed35Sp6nXP+l8ptGrnHV9zgYDUpraO0CPdlqLSeBqvY2DToR52nutDG7mIHuysdiw==, tarball: https://registry.npmmirror.com/vite-plugin-top-level-await/-/vite-plugin-top-level-await-1.4.4.tgz}
+ resolution: {integrity: sha512-QyxQbvcMkgt+kDb12m2P8Ed35Sp6nXP+l8ptGrnHV9zgYDUpraO0CPdlqLSeBqvY2DToR52nutDG7mIHuysdiw==}
vite: '>=2.8'
@@ -5812,11 +5818,11 @@ snapshots:
'@babel/helper-string-parser@7.24.1': {}
- '@babel/helper-string-parser@7.25.7': {}
+ '@babel/helper-string-parser@7.25.9': {}
'@babel/helper-validator-identifier@7.22.20': {}
- '@babel/helper-validator-identifier@7.25.7': {}
+ '@babel/helper-validator-identifier@7.25.9': {}
'@babel/helper-validator-option@7.23.5': {}
@@ -5845,9 +5851,9 @@ snapshots:
dependencies:
'@babel/types': 7.24.0
- '@babel/types': 7.25.8
+ '@babel/types': 7.26.0
'@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.24.4(@babel/core@7.24.4)':
@@ -6418,11 +6424,10 @@ snapshots:
'@babel/helper-validator-identifier': 7.22.20
to-fast-properties: 2.0.0
- '@babel/helper-string-parser': 7.25.7
- '@babel/helper-validator-identifier': 7.25.7
- to-fast-properties: 2.0.0
+ '@babel/helper-string-parser': 7.25.9
+ '@babel/helper-validator-identifier': 7.25.9
@@ -6702,7 +6707,7 @@ snapshots:
'@form-create/utils': 3.2.0
@@ -6712,15 +6717,15 @@ snapshots:
wangeditor: 4.7.15
- '@form-create/core@3.2.11(vue@3.5.12(typescript@5.3.3))':
+ '@form-create/core@3.2.13(vue@3.5.12(typescript@5.3.3))':
vue: 3.5.12(typescript@5.3.3)
- '@form-create/designer@3.2.7(vue@3.5.12(typescript@5.3.3))':
+ '@form-create/designer@3.2.8(vue@3.5.12(typescript@5.3.3))':
'@form-create/component-wangeditor': 3.1.20
- '@form-create/element-ui': 3.2.11(vue@3.5.12(typescript@5.3.3))
+ '@form-create/element-ui': 3.2.13(vue@3.5.12(typescript@5.3.3))
codemirror: 6.65.7
element-plus: 2.8.4(vue@3.5.12(typescript@5.3.3))
@@ -6729,7 +6734,7 @@ snapshots:
transitivePeerDependencies:
- '@vue/composition-api'
- '@form-create/element-ui@3.2.11(vue@3.5.12(typescript@5.3.3))':
+ '@form-create/element-ui@3.2.13(vue@3.5.12(typescript@5.3.3))':
'@form-create/component-elm-checkbox': 3.2.8
'@form-create/component-elm-frame': 3.2.0
@@ -6737,9 +6742,9 @@ snapshots:
'@form-create/component-elm-radio': 3.2.8
'@form-create/component-elm-select': 3.2.0
'@form-create/component-elm-tree': 3.2.9
- '@form-create/component-elm-upload': 3.2.9
+ '@form-create/component-elm-upload': 3.2.13
'@form-create/component-subform': 3.1.34
- '@form-create/core': 3.2.11(vue@3.5.12(typescript@5.3.3))
+ '@form-create/core': 3.2.13(vue@3.5.12(typescript@5.3.3))
@@ -7674,7 +7679,7 @@ snapshots:
'@vue/compiler-core@3.5.12':
- '@babel/parser': 7.25.8
+ '@babel/parser': 7.26.2
'@vue/shared': 3.5.12
entities: 4.5.0
estree-walker: 2.0.2
@@ -7704,7 +7709,7 @@ snapshots:
'@vue/compiler-sfc@3.5.12':
'@vue/compiler-core': 3.5.12
'@vue/compiler-dom': 3.5.12
'@vue/compiler-ssr': 3.5.12
@@ -8109,11 +8114,11 @@ snapshots:
boolbase@1.0.0: {}
- bpmn-js-properties-panel@0.46.0(bpmn-js@8.9.0):
+ bpmn-js-properties-panel@0.46.0(bpmn-js@8.10.0):
'@bpmn-io/element-templates-validator': 0.2.0
'@bpmn-io/extract-process-variables': 0.4.5
- bpmn-js: 8.9.0
+ bpmn-js: 8.10.0
ids: 1.0.5
inherits: 2.0.4
lodash: 4.17.21
@@ -8128,7 +8133,7 @@ snapshots:
min-dom: 0.2.0
svg.js: 2.7.1
bpmn-moddle: 7.1.3
css.escape: 1.5.1
@@ -10377,7 +10382,7 @@ snapshots:
picocolors@1.0.0: {}
- picocolors@1.1.0: {}
+ picocolors@1.1.1: {}
picomatch@2.3.1: {}
@@ -10463,7 +10468,7 @@ snapshots:
postcss@8.4.47:
nanoid: 3.3.7
- picocolors: 1.1.0
+ picocolors: 1.1.1
source-map-js: 1.2.1
posthtml-parser@0.2.1:
@@ -10862,6 +10867,8 @@ snapshots:
sortablejs@1.14.0: {}
+ sortablejs@1.15.3: {}
source-map-js@1.2.0: {}
source-map-js@1.2.1: {}
@@ -11328,7 +11335,7 @@ snapshots:
- rollup
- unplugin-vue-components@0.25.2(@babel/parser@7.25.8)(rollup@4.17.1)(vue@3.5.12(typescript@5.3.3)):
+ unplugin-vue-components@0.25.2(@babel/parser@7.26.2)(rollup@4.17.1)(vue@3.5.12(typescript@5.3.3)):
'@antfu/utils': 0.7.7
'@rollup/pluginutils': 5.1.0(rollup@4.17.1)
@@ -11342,7 +11349,7 @@ snapshots:
unplugin: 1.10.1
optionalDependencies:
- supports-color
@@ -1,8 +0,0 @@
-import request from '@/config/axios'
-
-export const getActivityList = async (params) => {
- return await request.get({
- url: '/bpm/activity/list',
- params
- })
-}
@@ -36,6 +36,16 @@ export const CategoryApi = {
return await request.put({ url: `/bpm/category/update`, data })
},
+ // 批量修改流程分类的排序
+ updateCategorySortBatch: async (ids: number[]) => {
+ return await request.put({
+ url: `/bpm/category/update-sort-batch`,
+ params: {
+ ids: ids.join(',')
+ }
+ })
+ },
// 删除流程分类
deleteCategory: async (id: number) => {
return await request.delete({ url: `/bpm/category/delete?id=` + id })
@@ -26,8 +26,8 @@ export type ModelVO = {
bpmnXml: string
}
-export const getModelPage = async (params) => {
- return await request.get({ url: '/bpm/model/page', params })
+export const getModelList = async (name: string | undefined) => {
+ return await request.get({ url: '/bpm/model/list', params: { name } })
export const getModel = async (id: string) => {
@@ -38,6 +38,16 @@ export const updateModel = async (data: ModelVO) => {
return await request.put({ url: '/bpm/model/update', data: data })
+// 批量修改流程分类的排序
+export const updateModelSortBatch = async (ids: number[]) => {
+ url: `/bpm/model/update-sort-batch`,
+}
export const updateModelBpmn = async (data: ModelVO) => {
return await request.put({ url: '/bpm/model/update-bpmn', data: data })
@@ -1,6 +1,6 @@
import request from '@/config/axios'
import { ProcessDefinitionVO } from '@/api/bpm/model'
-import { NodeType } from '@/components/SimpleProcessDesignerV2/src/consts'
+import { NodeType, CandidateStrategy } from '@/components/SimpleProcessDesignerV2/src/consts'
export type Task = {
id: string
name: string
@@ -24,30 +24,30 @@ export type ProcessInstanceVO = {
// 用户信息
export type User = {
- id: number,
- nickname: string,
+ id: number
+ nickname: string
avatar: string
// 审批任务信息
export type ApprovalTaskInfo = {
- ownerUser: User,
- assigneeUser: User,
- status: number,
+ ownerUser: User
+ assigneeUser: User
+ status: number
reason: string
// 审批节点信息
export type ApprovalNodeInfo = {
- id : number
nodeType: NodeType
+ candidateStrategy?: CandidateStrategy
status: number
startTime?: Date
endTime?: Date
- candidateUserList?: User[]
+ candidateUsers?: User[]
tasks: ApprovalTaskInfo[]
@@ -88,12 +88,16 @@ export const getProcessInstanceCopyPage = async (params: any) => {
// 获取审批详情
-export const getApprovalDetail = async (processInstanceId?:string, processDefinitionId?:string) => {
- const param = processInstanceId ? '?processInstanceId='+ processInstanceId : '?processDefinitionId='+ processDefinitionId
- return await request.get({ url: 'bpm/process-instance/get-approval-detail'+ param })
+export const getApprovalDetail = async (params: any) => {
+ return await request.get({ url: 'bpm/process-instance/get-approval-detail' , params })
// 获取表单字段权限
export const getFormFieldsPermission = async (params: any) => {
return await request.get({ url: '/bpm/process-instance/get-form-fields-permission', params })
+// 获取流程实例的 BPMN 模型视图
+export const getProcessInstanceBpmnModelView = async (id: string) => {
+ return await request.get({ url: '/bpm/process-instance/get-bpmn-model-view?id=' + id })
@@ -9,10 +9,10 @@ export enum TaskStatusEnum {
*/
NOT_START = -1,
- /**
+ /**
* 待审批
- WAIT = 0,
+ WAIT = 0,
/**
* 审批中
@@ -26,7 +26,7 @@ export enum TaskStatusEnum {
* 审批不通过
REJECT = 3,
* 已取消
@@ -35,19 +35,10 @@ export enum TaskStatusEnum {
* 已退回
RETURN = 5,
- * 委派中
- */
- DELEGATE = 6,
* 审批通过中
- APPROVING = 7,
-export type TaskVO = {
- id: number
+ APPROVING = 7
export const getTaskTodoPage = async (params: any) => {
@@ -76,12 +67,12 @@ export const getTaskListByProcessInstanceId = async (processInstanceId: string)
})
-// 获取所有可回退的节点
+// 获取所有可退回的节点
export const getTaskListByReturn = async (id: string) => {
return await request.get({ url: '/bpm/task/list-by-return', params: { id } })
-// 回退
+// 退回
export const returnTask = async (data: any) => {
return await request.put({ url: '/bpm/task/return', data })
@@ -106,6 +97,16 @@ export const signDeleteTask = async (data: any) => {
return await request.delete({ url: '/bpm/task/delete-sign', data })
+// 抄送
+export const copyTask = async (data: any) => {
+ return await request.put({ url: '/bpm/task/copy', data })
+// 获取我的待办任务
+export const myTodoTask = async (processInstanceId: string) => {
+ return await request.get({ url: '/bpm/task/my-todo?processInstanceId=' + processInstanceId })
// 获取减签任务列表
export const getChildrenTaskList = async (id: string) => {
return await request.get({ url: '/bpm/task/list-by-parent-task-id?parentTaskId=' + id })
@@ -13,10 +13,11 @@ export interface DeliveryPickUpStoreVO {
latitude: number
longitude: number
+ verifyUserIds: number[] // 绑定用户编号组数
// 查询自提门店列表
-export const getDeliveryPickUpStorePage = async (params) => {
+export const getDeliveryPickUpStorePage = async (params: any) => {
return await request.get({ url: '/trade/delivery/pick-up-store/page', params })
@@ -26,8 +27,8 @@ export const getDeliveryPickUpStore = async (id: number) => {
// 查询自提门店精简列表
-export const getListAllSimple = async (): Promise<DeliveryPickUpStoreVO[]> => {
- return await request.get({ url: '/trade/delivery/pick-up-store/list-all-simple' })
+export const getSimpleDeliveryPickUpStoreList = async (): Promise<DeliveryPickUpStoreVO[]> => {
+ return await request.get({ url: '/trade/delivery/pick-up-store/simple-list' })
// 新增自提门店
@@ -44,3 +45,8 @@ export const updateDeliveryPickUpStore = async (data: DeliveryPickUpStoreVO) =>
export const deleteDeliveryPickUpStore = async (id: number) => {
return await request.delete({ url: '/trade/delivery/pick-up-store/delete?id=' + id })
+// 绑定自提店员
+export const bindStoreStaffId = async (data: any) => {
+ return await request.post({ url: '/trade/delivery/pick-up-store/bind', data })
@@ -8,6 +8,7 @@ export interface AppVO {
remark: string
payNotifyUrl: string
refundNotifyUrl: string
+ transferNotifyUrl: string
merchantId: number
merchantName: string
createTime: Date
@@ -19,6 +20,7 @@ export interface AppPageReqVO extends PageParam {
remark?: string
payNotifyUrl?: string
refundNotifyUrl?: string
+ transferNotifyUrl?: string
merchantName?: string
createTime?: Date[]
@@ -0,0 +1 @@
+<svg t="1731390087280" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4297" width="200" height="200"><path d="M639.9 541.7c76.4-44.2 127.9-126.8 127.9-221.5C767.7 179 653.2 64.5 512 64.5S256.3 179 256.3 320.2c0 89.6 46.1 168.4 115.8 214.1C193.5 593 64.5 761.2 64.5 959.5h63.9c0-211.5 172.1-383.6 383.6-383.6 44.9 0 87.8 8.1 127.9 22.4v-56.6zM320.2 320.2c0-105.8 86-191.8 191.8-191.8s191.8 86 191.8 191.8S617.7 512 512 512s-191.8-86-191.8-191.8zM831.6 767.7V639.9h-63.9v127.8H639.9v63.9h127.8v127.9h63.9V831.6h127.9v-63.9z" fill="#5f6266" p-id="4298"></path></svg>
+<svg t="1729561718271" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8640" width="200" height="200"><path d="M908.5952 920.4224H164.7616a31.0784 31.0784 0 0 1-30.976-30.976c0-17.0496 13.9264-30.976 30.976-30.976h743.8336c17.0496 0 31.0272 13.9264 31.0272 30.976a31.0784 31.0784 0 0 1-31.0272 30.976z m0-123.9552H164.7616a31.0784 31.0784 0 0 1-30.976-30.976v-154.9824c0-51.1488 41.8304-92.9792 92.9792-92.9792h198.3488c-6.1952-37.1712-24.7808-72.8064-51.1488-103.8336a216.576 216.576 0 0 1-54.2208-144.128c0-58.88 23.2448-114.688 66.6112-156.4672C429.7728 71.168 485.5296 51.0976 545.9968 52.6848c111.5648 4.608 206.08 100.6592 207.616 212.2752 1.536 55.808-20.1216 110.0288-57.344 151.8592-26.3168 27.904-41.8304 61.952-48.0256 100.7104h198.3488c51.2 0 93.0304 41.8304 93.0304 92.9792v154.9824a31.0784 31.0784 0 0 1-31.0272 30.976z m-712.8064-61.952H877.568v-124.0064a31.0784 31.0784 0 0 0-31.0272-30.976h-232.448a31.0784 31.0784 0 0 1-30.976-31.0272c0-65.024 23.2448-127.0784 66.6624-173.568 27.8528-29.3888 41.8304-68.1472 41.8304-108.4416-1.536-80.5888-68.1984-148.7872-148.7872-151.8592a150.528 150.528 0 0 0-113.152 43.3664 153.6 153.6 0 0 0-48.0256 111.616c0 37.1712 13.9776 74.3424 38.7584 102.2464 44.9536 51.1488 69.7344 113.152 69.7344 176.64a31.0784 31.0784 0 0 1-30.976 31.0272h-232.448a31.0784 31.0784 0 0 0-30.976 30.976v123.9552z" fill="#fff" p-id="8641"></path></svg>
+<svg t="1729585232424" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1602" width="200" height="200"><path d="M925.5 898.9H804.9c-19 0-34.5-15.4-34.5-34.4V761.3c0-19 15.4-34.4 34.5-34.4h34.5V572.2c0-19-15.4-34.4-34.5-34.4H529.2V727h34.5c19 0 34.5 15.4 34.5 34.4v103.2c0 19-15.4 34.4-34.5 34.4H443.1c-19 0-34.5-15.4-34.5-34.4V761.3c0-19 15.4-34.4 34.5-34.4h34.5V537.8H219.1c-19 0-34.5 15.4-34.5 34.4V727h34.5c19 0 34.5 15.4 34.5 34.4v103.2c0 19-15.4 34.4-34.5 34.4H98.5c-19 0-34.5-15.4-34.5-34.4V761.3c0-19 15.4-34.4 34.5-34.4H133V555c0-38 30.9-68.8 68.9-68.8h275.7V297.1h-34.5c-19 0-34.5-15.4-34.5-34.4V159.5c0-19 15.4-34.4 34.5-34.4h120.6c19 0 34.5 15.4 34.5 34.4v103.2c0 19-15.4 34.4-34.5 34.4h-34.5v189.2h292.9c38.1 0 68.9 30.8 68.9 68.8v172h34.5c19 0 34.5 15.4 34.5 34.4v103.2c0 18.8-15.4 34.2-34.5 34.2z m0 0" p-id="1603" fill="#fff"></path></svg>
+<svg t="1729649333541" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1644" width="200" height="200"><path d="M647.888 893.84L491.904 571.52l393.888-393.888-237.904 716.208zM872.32 123.232L459.872 535.68 134.96 380.88 872.32 123.232z m90.72-68.32a23.968 23.968 0 0 0-24.784-5.568L64.08 354.816a23.984 23.984 0 0 0-2.4 44.32l381.392 181.728 187.36 387.088a24.048 24.048 0 0 0 23.152 13.504 24.032 24.032 0 0 0 21.232-16.4L968.96 79.552c2.88-8.672 0.592-18.24-5.92-24.64z" fill="#fff" p-id="1645"></path></svg>
+<svg t="1729585239190" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1755" width="200" height="200"><path d="M901.489435 536.822664v-0.931601l-1.001722-198.240726c-0.100172-19.162936-9.21584-37.474409-25.043042-50.246361-14.024104-11.349507-32.265456-17.60025-51.348255-17.610268l-618.062295-0.18031c-19.142902 0-37.424323 6.280795-51.478478 17.690405-15.827203 12.842072-24.902802 31.2437-24.892785 50.486775v196.798247A114.987635 114.987635 0 1 0 195.295664 536.922836V338.782282c1.15198-1.252152 4.808264-3.596181 10.768509-3.596181l276.725622 0.090155v199.753326a114.987635 114.987635 0 1 0 65.612772 1.412428V335.326342l275.693849 0.080138c6.01033 0 9.626546 2.344029 10.768508 3.596181l1.001722 195.70637a114.987635 114.987635 0 1 0 65.592737 2.113633zM214.979496 645.910158a56.437001 56.437001 0 1 1-56.437001-56.437001 56.507122 56.507122 0 0 1 56.437001 56.437001z m354.689623 0a56.437001 56.437001 0 1 1-56.437001-56.437001 56.507122 56.507122 0 0 1 56.437001 56.437001z m295.507904 56.437001a56.437001 56.437001 0 1 1 56.437001-56.437001 56.507122 56.507122 0 0 1-56.457035 56.437001z" p-id="1756" fill='#fff'></path></svg>
+<svg width="22" height="22" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path fill="#FAFAFA" d="M0 0h22v22H0z"/><circle fill="#919BAE" cx="1" cy="1" r="1"/></g></svg>
+<svg t="1729561814171" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1359" width="200" height="200"><path d="M674.496 603.456c120.256 0 218.176 90.752 221.44 203.84l0.064 5.888v125.888c0 11.52-9.92 20.928-22.144 20.928h-44.352a21.568 21.568 0 0 1-22.144-20.928v-125.888c0-67.712-56.512-123.264-128-125.76l-4.928-0.064H349.568c-71.488 0-130.176 53.504-132.864 121.152l-0.064 4.672v125.888c0 11.52-9.92 20.928-22.144 20.928h-44.352A21.568 21.568 0 0 1 128 939.072v-125.888c0-113.92 95.872-206.528 215.36-209.664l6.208-0.064h324.928zM497.216 128c122.368 0 221.568 93.888 221.568 209.728s-99.2 209.792-221.568 209.792c-122.304 0-221.44-93.952-221.44-209.728C275.712 221.952 374.848 128 497.152 128z m0 83.904c-73.408 0-132.864 56.32-132.864 125.888 0 69.504 59.52 125.824 132.864 125.824 73.408 0 132.928-56.32 132.928-125.824 0-69.504-59.52-125.888-132.928-125.888z" fill="#fff" p-id="1360"></path></svg>
@@ -185,7 +185,6 @@ export const useApiSelect = (option: ApiSelectProps) => {
</el-select>
)
- debugger
return (
<el-select
class="w-1/1"
@@ -16,3 +16,46 @@ export const localeProps = (t, prefix, rules) => {
return rule
+/**
+ * 解析表单组件的 field, title 等字段(递归,如果组件包含子组件)
+ *
+ * @param rule 组件的生成规则 https://www.form-create.com/v3/guide/rule
+ * @param fields 解析后表单组件字段
+ * @param parentTitle 如果是子表单,子表单的标题,默认为空
+ */
+export const parseFormFields = (
+ rule: Record<string, any>,
+ fields: Array<Record<string, any>> = [],
+ parentTitle: string = ''
+) => {
+ const { type, field, $required, title: tempTitle, children } = rule
+ if (field && tempTitle) {
+ let title = tempTitle
+ if (parentTitle) {
+ title = `${parentTitle}.${tempTitle}`
+ let required = false
+ if ($required) {
+ required = true
+ fields.push({
+ field,
+ title,
+ type,
+ required
+ // TODO 子表单 需要处理子表单字段
+ // if (type === 'group' && rule.props?.rule && Array.isArray(rule.props.rule)) {
+ // // 解析子表单的字段
+ // rule.props.rule.forEach((item) => {
+ // parseFields(item, fieldsPermission, title)
+ // })
+ // }
+ if (children && Array.isArray(children)) {
+ children.forEach((rule) => {
+ parseFormFields(rule, fields)
@@ -1,11 +1,12 @@
<template>
<div class="node-handler-wrapper">
- <div class="node-handler" v-if="props.showAdd">
+ <div class="node-handler">
<el-popover
trigger="hover"
v-model:visible="popoverShow"
placement="right-start"
width="auto"
+ v-if="!readonly"
>
<div class="handler-item-wrapper">
<div class="handler-item" @click="addNode(NodeType.USER_TASK_NODE)">
@@ -27,11 +28,17 @@
<div class="handler-item-text">条件分支</div>
</div>
<div class="handler-item" @click="addNode(NodeType.PARALLEL_BRANCH_NODE)">
- <div class="handler-item-icon condition">
+ <div class="handler-item-icon parallel">
<span class="iconfont icon-size icon-parallel"></span>
<div class="handler-item-text">并行分支</div>
+ <div class="handler-item" @click="addNode(NodeType.INCLUSIVE_BRANCH_NODE)">
+ <div class="handler-item-icon inclusive">
+ <span class="iconfont icon-size icon-inclusive"></span>
+ </div>
+ <div class="handler-item-text">包容分支</div>
<template #reference>
<div class="add-icon"><Icon icon="ep:plus" /></div>
@@ -56,23 +63,36 @@ import { generateUUID } from '@/utils'
defineOptions({
name: 'NodeHandler'
-const popoverShow = ref(false)
+const message = useMessage() // 消息弹窗
+const popoverShow = ref(false)
const props = defineProps({
childNode: {
type: Object as () => SimpleFlowNode,
default: null
- showAdd: {
- // 是否显示添加节点
- type: Boolean,
- default: true
+ currentNode: {
+ type: Object as () => SimpleFlowNode,
+ required: true
const emits = defineEmits(['update:childNode'])
+const readonly = inject<Boolean>('readonly') // 是否只读
const addNode = (type: number) => {
+ // 校验:条件分支、包容分支后面,不允许直接添加并行分支
+ if (
+ type === NodeType.PARALLEL_BRANCH_NODE &&
+ [NodeType.CONDITION_BRANCH_NODE, NodeType.INCLUSIVE_BRANCH_NODE].includes(
+ props.currentNode?.type
+ )
+ ) {
+ message.error('条件分支、包容分支后面,不允许直接添加并行分支')
+ return
popoverShow.value = false
if (type === NodeType.USER_TASK_NODE) {
const id = 'Activity_' + generateUUID()
@@ -122,12 +142,11 @@ const addNode = (type: number) => {
childNode: undefined,
conditionType: 1,
defaultFlow: false
{
id: 'Flow_' + generateUUID(),
name: '其它情况',
- showText: '其它情况进入此流程',
+ showText: '未满足其它条件时,将进入此分支',
type: NodeType.CONDITION_NODE,
conditionType: undefined,
@@ -162,6 +181,33 @@ const addNode = (type: number) => {
emits('update:childNode', data)
+ if (type === NodeType.INCLUSIVE_BRANCH_NODE) {
+ const data: SimpleFlowNode = {
+ name: '包容分支',
+ type: NodeType.INCLUSIVE_BRANCH_NODE,
+ id: 'GateWay_' + generateUUID(),
+ childNode: props.childNode,
+ conditionNodes: [
+ {
+ id: 'Flow_' + generateUUID(),
+ name: '包容条件1',
+ showText: '',
+ type: NodeType.CONDITION_NODE,
+ childNode: undefined,
+ defaultFlow: false
+ name: '其它情况',
+ defaultFlow: true
+ ]
+ emits('update:childNode', data)
</script>
@@ -31,6 +31,13 @@
@update:model-value="handleModelValueUpdate"
@find:parent-node="findFromParentNode"
/>
+ <!-- 包容分支节点 -->
+ <InclusiveNode
+ v-if="currentNode && currentNode.type === NodeType.INCLUSIVE_BRANCH_NODE"
+ :flow-node="currentNode"
+ @update:model-value="handleModelValueUpdate"
+ @find:parent-node="findFromParentNode"
+ />
<!-- 递归显示孩子节点 -->
<ProcessNodeTree
v-if="currentNode && currentNode.childNode"
@@ -40,7 +47,10 @@
<!-- 结束节点 -->
- <EndEventNode v-if="currentNode && currentNode.type === NodeType.END_EVENT_NODE" />
+ <EndEventNode
+ v-if="currentNode && currentNode.type === NodeType.END_EVENT_NODE"
</template>
<script setup lang="ts">
import StartUserNode from './nodes/StartUserNode.vue'
@@ -49,6 +59,7 @@ import UserTaskNode from './nodes/UserTaskNode.vue'
import CopyTaskNode from './nodes/CopyTaskNode.vue'
import ExclusiveNode from './nodes/ExclusiveNode.vue'
import ParallelNode from './nodes/ParallelNode.vue'
+import InclusiveNode from './nodes/InclusiveNode.vue'
import { SimpleFlowNode, NodeType } from './consts'
import { useWatchNode } from './node'
@@ -1,23 +1,11 @@
- <div class="simple-flow-canvas" v-loading="loading">
- <div class="simple-flow-container">
- <div class="top-area-container">
- <div class="top-actions">
- <div class="canvas-control">
- <span class="control-scale-group">
- <span class="control-scale-button"> <Icon icon="ep:plus" @click="zoomOut()" /></span>
- <span class="control-scale-label">{{ scaleValue }}%</span>
- <span class="control-scale-button"><Icon icon="ep:minus" @click="zoomIn()" /></span>
- </span>
- </div>
- <el-button type="primary" @click="saveSimpleFlowModel">保存</el-button>
- <!-- <el-button type="primary">全局设置</el-button> -->
- <div class="scale-container" :style="`transform: scale(${scaleValue / 100});`">
- <ProcessNodeTree v-if="processNodeTree" v-model:flow-node="processNodeTree" />
+ <div v-loading="loading" class="overflow-auto">
+ <SimpleProcessModel
+ v-if="processNodeTree"
+ :flow-node="processNodeTree"
+ :readonly="false"
+ @save="saveSimpleFlowModel"
<Dialog v-model="errorDialogVisible" title="保存失败" width="400" :fullscreen="false">
<div class="mb-2">以下节点内容不完善,请修改后保存</div>
<div
@@ -35,7 +23,7 @@
-import ProcessNodeTree from './ProcessNodeTree.vue'
+import SimpleProcessModel from './SimpleProcessModel.vue'
import { updateBpmSimpleModel, getBpmSimpleModel } from '@/api/bpm/simple'
import { SimpleFlowNode, NodeType, NodeId, NODE_DEFAULT_TEXT } from './consts'
import { getModel } from '@/api/bpm/model'
@@ -50,14 +38,16 @@ import * as UserGroupApi from '@/api/bpm/userGroup'
name: 'SimpleProcessDesigner'
-const router = useRouter() // 路由
+const emits = defineEmits(['success']) // 保存成功事件
modelId: {
type: String,
required: true
-const loading = ref(true)
+const loading = ref(false)
const formFields = ref<string[]>([])
const formType = ref(20)
const roleOptions = ref<RoleApi.RoleVO[]>([]) // 角色列表
@@ -79,28 +69,26 @@ const message = useMessage() // 国际化
const processNodeTree = ref<SimpleFlowNode | undefined>()
const errorDialogVisible = ref(false)
let errorNodes: SimpleFlowNode[] = []
-const saveSimpleFlowModel = async () => {
- if (!props.modelId) {
- message.error('缺少模型 modelId 编号')
+const saveSimpleFlowModel = async (simpleModelNode: SimpleFlowNode) => {
+ if (!simpleModelNode) {
+ message.error('模型数据为空')
return
- errorNodes = []
- validateNode(processNodeTree.value, errorNodes)
- if (errorNodes.length > 0) {
- errorDialogVisible.value = true
- return
- }
- const data = {
- id: props.modelId,
- simpleModel: processNodeTree.value
- const result = await updateBpmSimpleModel(data)
- if (result) {
- message.success('修改成功')
- close()
- } else {
- message.alert('修改失败')
+ try {
+ loading.value = true
+ const data = {
+ id: props.modelId,
+ simpleModel: simpleModelNode
+ const result = await updateBpmSimpleModel(data)
+ if (result) {
+ message.success('修改成功')
+ emits('success')
+ } else {
+ message.alert('修改失败')
+ } finally {
+ loading.value = false
// 校验节点设置。 暂时以 showText 为空 未节点错误配置
@@ -111,58 +99,37 @@ const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNo
if (type == NodeType.START_USER_NODE) {
+ // 发起人节点暂时不用校验,直接校验孩子节点
validateNode(node.childNode, errorNodes)
- if (type === NodeType.USER_TASK_NODE) {
- if (!showText) {
- errorNodes.push(node)
- validateNode(node.childNode, errorNodes)
- if (type === NodeType.COPY_TASK_NODE) {
- if (type === NodeType.CONDITION_NODE) {
+ type === NodeType.USER_TASK_NODE ||
+ type === NodeType.COPY_TASK_NODE ||
+ type === NodeType.CONDITION_NODE
if (!showText) {
errorNodes.push(node)
- if (type == NodeType.CONDITION_BRANCH_NODE) {
+ type == NodeType.CONDITION_BRANCH_NODE ||
+ type == NodeType.PARALLEL_BRANCH_NODE ||
+ type == NodeType.INCLUSIVE_BRANCH_NODE
+ // 分支节点
+ // 1. 先校验各个分支
conditionNodes?.forEach((item) => {
validateNode(item, errorNodes)
+ // 2. 校验孩子节点
-const close = () => {
- router.push({ path: '/bpm/manager/model' })
-let scaleValue = ref(100)
-const MAX_SCALE_VALUE = 200
-const MIN_SCALE_VALUE = 50
-// 放大
-const zoomOut = () => {
- if (scaleValue.value == MAX_SCALE_VALUE) {
- scaleValue.value += 10
-// 缩小
-const zoomIn = () => {
- if (scaleValue.value == MIN_SCALE_VALUE) {
- scaleValue.value -= 10
onMounted(async () => {
try {
loading.value = true
@@ -188,7 +155,7 @@ onMounted(async () => {
// 获取用户组列表
userGroupOptions.value = await UserGroupApi.getUserGroupSimpleList()
- // 获取 SIMPLE 设计器模型
+ //获取 SIMPLE 设计器模型
const result = await getBpmSimpleModel(props.modelId)
if (result) {
processNodeTree.value = result
@@ -0,0 +1,140 @@
+<template>
+ <div class="simple-process-model-container position-relative">
+ <div class="position-absolute top-0px right-0px bg-#fff">
+ <el-row type="flex" justify="end">
+ <el-button-group key="scale-control" size="default">
+ <el-button size="default" :icon="ScaleToOriginal" @click="processReZoom()" />
+ <el-button size="default" :plain="true" :icon="ZoomOut" @click="zoomOut()" />
+ <el-button size="default" class="w-80px"> {{ scaleValue }}% </el-button>
+ <el-button size="default" :plain="true" :icon="ZoomIn" @click="zoomIn()" />
+ </el-button-group>
+ <el-button
+ size="default"
+ class="ml-4px"
+ type="primary"
+ :icon="Select"
+ @click="saveSimpleFlowModel"
+ >保存模型</el-button
+ >
+ </el-row>
+ <div class="simple-process-model" :style="`transform: scale(${scaleValue / 100});`">
+ <ProcessNodeTree v-if="processNodeTree" v-model:flow-node="processNodeTree" />
+ <Dialog v-model="errorDialogVisible" title="保存失败" width="400" :fullscreen="false">
+ <div class="mb-2">以下节点内容不完善,请修改后保存</div>
+ <div
+ class="mb-3 b-rounded-1 bg-gray-100 p-2 line-height-normal"
+ v-for="(item, index) in errorNodes"
+ :key="index"
+ {{ item.name }} : {{ NODE_DEFAULT_TEXT.get(item.type) }}
+ <template #footer>
+ <el-button type="primary" @click="errorDialogVisible = false">知道了</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import ProcessNodeTree from './ProcessNodeTree.vue'
+import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from './consts'
+import { useWatchNode } from './node'
+import { Select, ZoomOut, ZoomIn, ScaleToOriginal } from '@element-plus/icons-vue'
+defineOptions({
+ name: 'SimpleProcessModel'
+})
+const props = defineProps({
+ flowNode: {
+ readonly: {
+ type: Boolean,
+ required: false,
+ default: true
+const emits = defineEmits<{
+ 'save': [node: SimpleFlowNode | undefined]
+}>()
+const processNodeTree = useWatchNode(props)
+provide('readonly', props.readonly)
+let scaleValue = ref(100)
+const MAX_SCALE_VALUE = 200
+const MIN_SCALE_VALUE = 50
+// 放大
+const zoomIn = () => {
+ if (scaleValue.value == MAX_SCALE_VALUE) {
+ scaleValue.value += 10
+// 缩小
+const zoomOut = () => {
+ if (scaleValue.value == MIN_SCALE_VALUE) {
+ scaleValue.value -= 10
+const processReZoom = () => {
+ scaleValue.value = 100
+const errorDialogVisible = ref(false)
+let errorNodes: SimpleFlowNode[] = []
+const saveSimpleFlowModel = async () => {
+ errorNodes = []
+ validateNode(processNodeTree.value, errorNodes)
+ if (errorNodes.length > 0) {
+ errorDialogVisible.value = true
+ emits('save', processNodeTree.value)
+// 校验节点设置。 暂时以 showText 为空 未节点错误配置
+const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNode[]) => {
+ if (node) {
+ const { type, showText, conditionNodes } = node
+ if (type == NodeType.END_EVENT_NODE) {
+ if (type == NodeType.START_USER_NODE) {
+ validateNode(node.childNode, errorNodes)
+ if (!showText) {
+ errorNodes.push(node)
+ conditionNodes?.forEach((item) => {
+ validateNode(item, errorNodes)
+</script>
+<style lang="scss" scoped></style>
@@ -0,0 +1,48 @@
+ <SimpleProcessModel :flow-node="simpleModel" :readonly="true" />
+import { SimpleFlowNode } from './consts'
+ name: 'SimpleProcessViewer'
+ // 流程任务
+ tasks: {
+ type: Array,
+ default: () => [] as any[]
+ // 流程实例
+ processInstance: {
+ type: Object,
+ default: () => undefined
+const approveTasks = ref<any[]>(props.tasks)
+const currentProcessInstance = ref(props.processInstance)
+const simpleModel = useWatchNode(props)
+watch(
+ () => props.tasks,
+ (newValue) => {
+ approveTasks.value = newValue
+)
+ () => props.processInstance,
+ currentProcessInstance.value = newValue
+provide('tasks', approveTasks)
+provide('processInstance', currentProcessInstance)
+p
// @ts-ignore
import { DictDataVO } from '@/api/system/dict/types'
+import { TaskStatusEnum } from '@/api/bpm/task'
* 节点类型
@@ -79,7 +79,7 @@ export interface SimpleFlowNode {
// 审批按钮设置
buttonsSetting?: any[]
// 表单权限
- fieldsPermission?: Array<Record<string, string>>
+ fieldsPermission?: Array<Record<string, any>>
// 审批任务超时处理
timeoutHandler?: TimeoutHandler
// 审批任务拒绝处理
@@ -96,7 +96,8 @@ export interface SimpleFlowNode {
conditionGroups?: ConditionGroup
// 是否默认的条件
defaultFlow?: boolean
+ // 活动的状态,用于前端节点状态展示
+ activityStatus?: TaskStatusEnum
// 候选人策略枚举 ( 用于审批节点。抄送节点 )
export enum CandidateStrategy {
@@ -144,6 +145,14 @@ export enum CandidateStrategy {
* 指定用户组
USER_GROUP = 40,
+ * 表单内用户字段
+ FORM_USER = 50,
+ * 表单内部门负责人
+ FORM_DEPT_LEADER = 51,
* 流程表达式
@@ -178,7 +187,7 @@ export enum ApproveMethodType {
export type RejectHandler = {
// 审批拒绝类型
type: RejectHandlerType
- // 回退节点 Id
+ // 退回节点 Id
returnNodeId?: string
@@ -360,9 +369,13 @@ export enum OperationButtonType {
ADD_SIGN = 5,
- * 回退
+ * 退回
+ RETURN = 6,
+ * 抄送
- RETURN = 6
+ COPY = 7
@@ -419,6 +432,8 @@ export const CANDIDATE_STRATEGY: DictDataVO[] = [
{ label: '发起人部门负责人', value: CandidateStrategy.START_USER_DEPT_LEADER },
{ label: '发起人连续部门负责人', value: CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER },
{ label: '用户组', value: CandidateStrategy.USER_GROUP },
+ { label: '表单内用户字段', value: CandidateStrategy.FORM_USER },
+ { label: '表单内部门负责人', value: CandidateStrategy.FORM_DEPT_LEADER },
{ label: '流程表达式', value: CandidateStrategy.EXPRESSION }
]
// 审批节点 的审批类型
@@ -503,16 +518,17 @@ OPERATION_BUTTON_NAME.set(OperationButtonType.REJECT, '拒绝')
OPERATION_BUTTON_NAME.set(OperationButtonType.TRANSFER, '转办')
OPERATION_BUTTON_NAME.set(OperationButtonType.DELEGATE, '委派')
OPERATION_BUTTON_NAME.set(OperationButtonType.ADD_SIGN, '加签')
-OPERATION_BUTTON_NAME.set(OperationButtonType.RETURN, '回退')
+OPERATION_BUTTON_NAME.set(OperationButtonType.RETURN, '退回')
+OPERATION_BUTTON_NAME.set(OperationButtonType.COPY, '抄送')
// 默认的按钮权限设置
export const DEFAULT_BUTTON_SETTING: ButtonSetting[] = [
{ id: OperationButtonType.APPROVE, displayName: '通过', enable: true },
{ id: OperationButtonType.REJECT, displayName: '拒绝', enable: true },
- { id: OperationButtonType.TRANSFER, displayName: '转办', enable: false },
- { id: OperationButtonType.DELEGATE, displayName: '委派', enable: false },
- { id: OperationButtonType.ADD_SIGN, displayName: '加签', enable: false },
- { id: OperationButtonType.RETURN, displayName: '回退', enable: false }
+ { id: OperationButtonType.TRANSFER, displayName: '转办', enable: true },
+ { id: OperationButtonType.DELEGATE, displayName: '委派', enable: true },
+ { id: OperationButtonType.ADD_SIGN, displayName: '加签', enable: true },
+ { id: OperationButtonType.RETURN, displayName: '退回', enable: true }
// 发起人的按钮权限。暂时定死,不可以编辑
@@ -522,7 +538,7 @@ export const START_USER_BUTTON_SETTING: ButtonSetting[] = [
{ id: OperationButtonType.TRANSFER, displayName: '转办', enable: false },
{ id: OperationButtonType.DELEGATE, displayName: '委派', enable: false },
{ id: OperationButtonType.ADD_SIGN, displayName: '加签', enable: false },
+ { id: OperationButtonType.RETURN, displayName: '退回', enable: false }
export const MULTI_LEVEL_DEPT: DictDataVO = [
@@ -542,3 +558,13 @@ export const MULTI_LEVEL_DEPT: DictDataVO = [
{ label: '第 14 级部门', value: 14 },
{ label: '第 15 级部门', value: 15 }
+ * 流程实例的变量枚举
+export enum ProcessVariableEnum {
+ * 发起用户 ID
+ START_USER_ID = 'PROCESS_START_USER_ID'
@@ -1,4 +1,5 @@
import SimpleProcessDesigner from './SimpleProcessDesigner.vue'
+import SimpleProcessViewer from './SimpleProcessViewer.vue'
import '../theme/simple-process-designer.scss'
-export { SimpleProcessDesigner }
+export { SimpleProcessDesigner, SimpleProcessViewer}
import { cloneDeep } from 'lodash-es'
import * as RoleApi from '@/api/system/role'
import * as DeptApi from '@/api/system/dept'
import * as PostApi from '@/api/system/post'
@@ -13,8 +14,10 @@ import {
NODE_DEFAULT_NAME,
AssignStartUserHandlerType,
AssignEmptyHandlerType,
- FieldPermissionType
+ FieldPermissionType,
+ ProcessVariableEnum
} from './consts'
+import { parseFormFields } from '@/components/FormCreate/src/utils/index'
export function useWatchNode(props: { flowNode: SimpleFlowNode }): Ref<SimpleFlowNode> {
const node = ref<SimpleFlowNode>(props.flowNode)
watch(
@@ -26,12 +29,30 @@ export function useWatchNode(props: { flowNode: SimpleFlowNode }): Ref<SimpleFlo
return node
+// 解析 formCreate 所有表单字段, 并返回
+const parseFormCreateFields = (formFields?: string[]) => {
+ const result: Array<Record<string, any>> = []
+ if (formFields) {
+ formFields.forEach((fieldStr: string) => {
+ parseFormFields(JSON.parse(fieldStr), result)
+ // 固定添加发起人 ID 字段
+ result.unshift({
+ field: ProcessVariableEnum.START_USER_ID,
+ title: '发起人',
+ type: 'UserSelect',
+ return result
* @description 表单数据权限配置,用于发起人节点 、审批节点、抄送节点
export function useFormFieldsPermission(defaultPermission: FieldPermissionType) {
// 字段权限配置. 需要有 field, title, permissioin 属性
- const fieldsPermissionConfig = ref<Array<Record<string, string>>>([])
+ const fieldsPermissionConfig = ref<Array<Record<string, any>>>([])
const formType = inject<Ref<number>>('formType') // 表单类型
@@ -44,49 +65,26 @@ export function useFormFieldsPermission(defaultPermission: FieldPermissionType)
// 默认的表单权限: 获取表单的所有字段,设置字段默认权限为只读
const getDefaultFieldsPermission = (formFields?: string[]) => {
- const defaultFieldsPermission: Array<Record<string, string>> = []
+ let defaultFieldsPermission: Array<Record<string, any>> = []
if (formFields) {
- formFields.forEach((fieldStr: string) => {
- parseFieldsSetDefaultPermission(JSON.parse(fieldStr), defaultFieldsPermission)
+ defaultFieldsPermission = parseFormCreateFields(formFields).map((item) => {
+ return {
+ field: item.field,
+ title: item.title,
+ permission: defaultPermission
return defaultFieldsPermission
- // 解析字段。赋给默认权限
- const parseFieldsSetDefaultPermission = (
- rule: Record<string, any>,
- fieldsPermission: Array<Record<string, string>>,
- parentTitle: string = ''
- ) => {
- const { /**type,*/ field, title: tempTitle, children } = rule
- if (field && tempTitle) {
- let title = tempTitle
- if (parentTitle) {
- title = `${parentTitle}.${tempTitle}`
- fieldsPermission.push({
- field,
- title,
- permission: defaultPermission
- // TODO 子表单 需要处理子表单字段
- // if (type === 'group' && rule.props?.rule && Array.isArray(rule.props.rule)) {
- // // 解析子表单的字段
- // rule.props.rule.forEach((item) => {
- // parseFieldsSetDefaultPermission(item, fieldsPermission, title)
- // })
- // }
- if (children && Array.isArray(children)) {
- children.forEach((rule) => {
- parseFieldsSetDefaultPermission(rule, fieldsPermission)
+ // 获取表单的所有字段,作为下拉框选项
+ const formFieldOptions = parseFormCreateFields(unref(formFields))
return {
formType,
fieldsPermissionConfig,
+ formFieldOptions,
getNodeConfigFormFields
@@ -94,50 +92,8 @@ export function useFormFieldsPermission(defaultPermission: FieldPermissionType)
* @description 获取表单的字段
export function useFormFields() {
- // 解析后的表单字段
const formFields = inject<Ref<string[]>>('formFields') // 流程表单字段
- const parseFormFields = () => {
- const parsedFormFields: Array<Record<string, string>> = []
- if (formFields) {
- formFields.value.forEach((fieldStr: string) => {
- parseField(JSON.parse(fieldStr), parsedFormFields)
- return parsedFormFields
- // 解析字段。
- const parseField = (
- parsedFormFields: Array<Record<string, string>>,
- const { field, title: tempTitle, children, type } = rule
- parsedFormFields.push({
- type
- parseField(rule, parsedFormFields)
- return parseFormFields()
+ return parseFormCreateFields(unref(formFields))
export type UserTaskFormType = {
@@ -151,6 +107,8 @@ export type UserTaskFormType = {
userGroups?: number[] // 用户组
postIds?: number[] // 岗位
expression?: string // 流程表达式
+ formUser?: string // 表单内用户字段
+ formDept?: string // 表单内部门字段
approveRatio?: number
rejectHandlerType?: RejectHandlerType
@@ -173,6 +131,8 @@ export type CopyTaskFormType = {
userIds?: number[] // 用户
@@ -186,6 +146,7 @@ export function useNodeForm(nodeType: NodeType) {
const deptOptions = inject<Ref<DeptApi.DeptVO[]>>('deptList') // 部门列表
const userGroupOptions = inject<Ref<UserGroupApi.UserGroupVO[]>>('userGroupList') // 用户组列表
const deptTreeOptions = inject('deptTree') // 部门树
+ const formFields = inject<Ref<string[]>>('formFields') // 流程表单字段
const configForm = ref<UserTaskFormType | CopyTaskFormType>()
if (nodeType === NodeType.USER_TASK_NODE) {
configForm.value = {
@@ -281,6 +242,18 @@ export function useNodeForm(nodeType: NodeType) {
+ // 表单内用户字段
+ if (configForm.value?.candidateStrategy === CandidateStrategy.FORM_USER) {
+ const item = formFieldOptions.find((item) => item.field === configForm.value?.formUser)
+ showText = `表单用户:${item?.title}`
+ // 表单内部门负责人
+ if (configForm.value?.candidateStrategy === CandidateStrategy.FORM_DEPT_LEADER) {
+ showText = `表单内部门负责人`
// 发起人自选
if (configForm.value?.candidateStrategy === CandidateStrategy.START_USER_SELECT) {
showText = `发起人自选`
@@ -327,6 +300,9 @@ export function useNodeForm(nodeType: NodeType) {
case CandidateStrategy.USER_GROUP:
candidateParam = configForm.value.userGroups!.join(',')
break
+ case CandidateStrategy.FORM_USER:
+ candidateParam = configForm.value.formUser!
+ break
case CandidateStrategy.EXPRESSION:
candidateParam = configForm.value.expression!
@@ -346,6 +322,13 @@ export function useNodeForm(nodeType: NodeType) {
candidateParam = deptIds.concat('|' + configForm.value.deptLevel + '')
+ // 表单内部门的负责人
+ case CandidateStrategy.FORM_DEPT_LEADER: {
+ // 候选人参数格式: | 分隔 。左边为表单内部门字段。 右边为部门层级
+ const deptFieldOnForm = configForm.value.formDept!
+ candidateParam = deptFieldOnForm.concat('|' + configForm.value.deptLevel + '')
default:
@@ -375,6 +358,9 @@ export function useNodeForm(nodeType: NodeType) {
configForm.value.userGroups = candidateParam.split(',').map((item) => +item)
+ configForm.value.formUser = candidateParam
configForm.value.expression = candidateParam
@@ -395,6 +381,14 @@ export function useNodeForm(nodeType: NodeType) {
configForm.value.deptLevel = +paramArray[1]
+ // 表单内的部门负责人
+ // 候选人参数格式: | 分隔 。左边为表单内的部门字段。 右边为部门层级
+ const paramArray = candidateParam.split('|')
+ configForm.value.formDept = paramArray[0]
+ configForm.value.deptLevel = +paramArray[1]
@@ -476,3 +470,26 @@ export function useNodeName2(node: Ref<SimpleFlowNode>, nodeType: NodeType) {
blurEvent
+ * @description 根据节点任务状态,获取节点任务状态样式
+export function useTaskStatusClass(taskStatus: TaskStatusEnum | undefined): string {
+ if (!taskStatus) {
+ return ''
+ if (taskStatus === TaskStatusEnum.APPROVE) {
+ return 'status-pass'
+ if (taskStatus === TaskStatusEnum.RUNNING) {
+ return 'status-running'
+ if (taskStatus === TaskStatusEnum.REJECT) {
+ return 'status-reject'
+ if (taskStatus === TaskStatusEnum.CANCEL) {
+ return 'status-cancel'
@@ -26,7 +26,7 @@
<div>
- <div class="mb-3 font-size-16px" v-if="currentNode.defaultFlow">其它条件不满足进入此分支(该分支不可编辑和删除)</div>
+ <div class="mb-3 font-size-16px" v-if="currentNode.defaultFlow">未满足其它条件时,将进入此分支(该分支不可编辑和删除)</div>
<div v-else>
<el-form
ref="formRef"
@@ -60,7 +60,8 @@
<el-form-item
v-if="
configForm.candidateStrategy == CandidateStrategy.DEPT_MEMBER ||
- configForm.candidateStrategy == CandidateStrategy.DEPT_LEADER
+ configForm.candidateStrategy == CandidateStrategy.DEPT_LEADER ||
+ configForm.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER
"
label="指定部门"
prop="deptIds"
@@ -122,7 +123,57 @@
</el-form-item>
+ <el-form-item
+ v-if="configForm.candidateStrategy === CandidateStrategy.FORM_USER"
+ label="表单内用户字段"
+ prop="formUser"
+ <el-select v-model="configForm.formUser" clearable style="width: 100%">
+ <el-option
+ v-for="(item, idx) in userFieldOnFormOptions"
+ :key="idx"
+ :label="item.title"
+ :value="item.field"
+ :disabled ="!item.required"
+ </el-select>
+ </el-form-item>
+ v-if="configForm.candidateStrategy === CandidateStrategy.FORM_DEPT_LEADER"
+ label="表单内部门字段"
+ prop="formDept"
+ <el-select v-model="configForm.formDept" clearable style="width: 100%">
+ v-for="(item, idx) in deptFieldOnFormOptions"
+ v-if="
+ configForm.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER ||
+ configForm.candidateStrategy == CandidateStrategy.START_USER_DEPT_LEADER ||
+ configForm.candidateStrategy ==
+ CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER ||
+ configForm.candidateStrategy == CandidateStrategy.FORM_DEPT_LEADER
+ "
+ :label="deptLevelLabel!"
+ prop="deptLevel"
+ span="24"
+ <el-select v-model="configForm.deptLevel" clearable>
+ v-for="(item, index) in MULTI_LEVEL_DEPT"
+ :label="item.label"
+ :value="item.value"
v-if="configForm.candidateStrategy === CandidateStrategy.EXPRESSION"
label="流程表达式"
@@ -201,7 +252,8 @@ import {
CandidateStrategy,
NodeType,
CANDIDATE_STRATEGY,
+ MULTI_LEVEL_DEPT
} from '../consts'
import {
useWatchNode,
@@ -221,6 +273,15 @@ const props = defineProps({
+const deptLevelLabel = computed(() => {
+ let label = '部门负责人来源'
+ if (configForm.value.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER) {
+ label = label + '(指定部门向上)'
+ label = label + '(发起人部门向上)'
+ return label
// 抽屉配置
const { settingVisible, closeDrawer, openDrawer } = useDrawer()
// 当前节点
@@ -230,9 +291,16 @@ const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.COPY_
// 激活的 Tab 标签页
const activeTabName = ref('user')
// 表单字段权限配置
-const { formType, fieldsPermissionConfig, getNodeConfigFormFields } = useFormFieldsPermission(
- FieldPermissionType.READ
-)
+const { formType, fieldsPermissionConfig, formFieldOptions, getNodeConfigFormFields } =
+ useFormFieldsPermission(FieldPermissionType.READ)
+// 表单内用户字段选项, 必须是必填和用户选择器
+const userFieldOnFormOptions = computed(() => {
+ return formFieldOptions.filter((item) => item.type === 'UserSelect')
+// 表单内部门字段选项, 必须是必填和部门选择器
+const deptFieldOnFormOptions = computed(() => {
+ return formFieldOptions.filter((item) => item.type === 'DeptSelect')
// 抄送人表单配置
const formRef = ref() // 表单 Ref
// 表单校验规则
@@ -243,6 +311,8 @@ const formRules = reactive({
deptIds: [{ required: true, message: '部门不能为空', trigger: 'change' }],
userGroups: [{ required: true, message: '用户组不能为空', trigger: 'change' }],
postIds: [{ required: true, message: '岗位不能为空', trigger: 'change' }],
+ formUser: [{ required: true, message: '表单内用户字段不能为空', trigger: 'change' }],
+ formDept: [{ required: true, message: '表单内部门字段不能为空', trigger: 'change' }],
expression: [{ required: true, message: '流程表达式不能为空', trigger: 'blur' }]
@@ -260,11 +330,7 @@ const {
const configForm = tempConfigForm as Ref<CopyTaskFormType>
// 抄送人策略, 去掉发起人自选 和 发起人自己
const copyUserStrategies = computed(() => {
- return CANDIDATE_STRATEGY.filter(
- (item) =>
- item.value !== CandidateStrategy.START_USER_SELECT &&
- item.value !== CandidateStrategy.START_USER
- )
+ return CANDIDATE_STRATEGY.filter((item) => item.value !== CandidateStrategy.START_USER)
// 改变抄送人设置策略
const changeCandidateStrategy = () => {
@@ -274,6 +340,7 @@ const changeCandidateStrategy = () => {
configForm.value.postIds = []
configForm.value.userGroups = []
configForm.value.deptLevel = 1
+ configForm.value.formUser = ''
// 保存配置
const saveConfig = async () => {
@@ -119,7 +119,6 @@ const saveConfig = async () => {
currentNode.value.fieldsPermission = fieldsPermissionConfig.value
// 设置发起人的按钮权限
currentNode.value.buttonsSetting = START_USER_BUTTON_SETTING
- console.log('currentNode.value.buttonsSetting==>', currentNode.value.buttonsSetting)
settingVisible.value = false
return true
@@ -56,7 +56,6 @@
</el-radio>
</el-radio-group>
v-if="configForm.candidateStrategy == CandidateStrategy.ROLE"
label="指定角色"
@@ -94,25 +93,6 @@
show-checkbox
- <el-form-item
- v-if="
- configForm.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER ||
- configForm.candidateStrategy == CandidateStrategy.START_USER_DEPT_LEADER ||
- configForm.candidateStrategy == CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER
- "
- :label="deptLevelLabel!"
- prop="deptLevel"
- span="24"
- >
- <el-select v-model="configForm.deptLevel" clearable>
- <el-option
- v-for="(item, index) in MULTI_LEVEL_DEPT"
- :key="index"
- :label="item.label"
- :value="item.value"
- />
- </el-select>
- </el-form-item>
v-if="configForm.candidateStrategy == CandidateStrategy.POST"
label="指定岗位"
@@ -134,13 +114,7 @@
prop="userIds"
span="24"
- <el-select
- v-model="configForm.userIds"
- clearable
- multiple
- style="width: 100%"
- @change="changedCandidateUsers"
+ <el-select v-model="configForm.userIds" clearable multiple style="width: 100%">
<el-option
v-for="item in userOptions"
:key="item.id"
@@ -163,6 +137,57 @@
<!-- TODO @jason:后续要支持选择已经存好的表达式 -->
@@ -184,14 +209,7 @@
:key="index"
class="flex items-center"
- <el-radio
- :label="item.value"
- :disabled="
- item.value !== ApproveMethodType.RANDOM_SELECT_ONE_APPROVE &&
- notAllowedMultiApprovers
+ <el-radio :value="item.value" :label="item.value">
{{ item.label }}
<el-form-item prop="approveRatio">
@@ -481,6 +499,8 @@ const deptLevelLabel = computed(() => {
let label = '部门负责人来源'
if (configForm.value.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER) {
label = label + '(指定部门向上)'
+ } else if (configForm.value.candidateStrategy == CandidateStrategy.FORM_DEPT_LEADER) {
+ label = label + '(表单内部门向上)'
} else {
label = label + '(发起人部门向上)'
@@ -495,9 +515,16 @@ const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.USER_
// 表单字段权限设置
// 操作按钮设置
const { buttonsSetting, btnDisplayNameEdit, changeBtnDisplayName, btnDisplayNameBlurEvent } =
useButtonsSetting()
@@ -511,6 +538,8 @@ const formRules = reactive({
roleIds: [{ required: true, message: '角色不能为空', trigger: 'change' }],
expression: [{ required: true, message: '流程表达式不能为空', trigger: 'blur' }],
approveMethod: [{ required: true, message: '多人审批方式不能为空', trigger: 'change' }],
@@ -537,8 +566,7 @@ const {
getShowText
} = useNodeForm(NodeType.USER_TASK_NODE)
const configForm = tempConfigForm as Ref<UserTaskFormType>
-// 不允许多人审批
-const notAllowedMultiApprovers = ref(false)
// 改变审批人设置策略
configForm.value.userIds = []
@@ -547,30 +575,11 @@ const changeCandidateStrategy = () => {
+ configForm.value.formDept = ''
configForm.value.approveMethod = ApproveMethodType.SEQUENTIAL_APPROVE
- if (
- configForm.value.candidateStrategy === CandidateStrategy.START_USER ||
- configForm.value.candidateStrategy === CandidateStrategy.USER
- ) {
- notAllowedMultiApprovers.value = true
- notAllowedMultiApprovers.value = false
-// 改变审批候选人
-const changedCandidateUsers = () => {
- configForm.value.userIds &&
- configForm.value.userIds?.length <= 1 &&
- configForm.value.approveMethod = ApproveMethodType.RANDOM_SELECT_ONE_APPROVE
- configForm.value.rejectHandlerType = RejectHandlerType.FINISH_PROCESS
// 审批方式改变
const approveMethodChanged = () => {
configForm.value.rejectHandlerType = RejectHandlerType.FINISH_PROCESS
@@ -579,7 +588,7 @@ const approveMethodChanged = () => {
formRef.value.clearValidate('approveRatio')
-// 审批拒绝 可回退的节点
+// 审批拒绝 可退回的节点
const returnTaskList = ref<SimpleFlowNode[]>([])
// 审批人超时未处理设置
const {
@@ -666,11 +675,6 @@ const showUserTaskNodeConfig = (node: SimpleFlowNode) => {
configForm.value.candidateStrategy = node.candidateStrategy!
// 解析候选人参数
parseCandidateParam(node.candidateStrategy!, node?.candidateParam)
- if (configForm.value.userIds && configForm.value.userIds.length > 1) {
// 2.2 设置审批方式
configForm.value.approveMethod = node.approveMethod!
if (node.approveMethod == ApproveMethodType.APPROVE_BY_RATIO) {
@@ -1,11 +1,17 @@
<div class="node-wrapper">
<div class="node-container">
- <div class="node-box" :class="{ 'node-config-error': !currentNode.showText }">
+ class="node-box"
+ :class="[
+ { 'node-config-error': !currentNode.showText },
+ `${useTaskStatusClass(currentNode?.activityStatus)}`
+ ]"
<div class="node-title-container">
<div class="node-title-icon copy-task"><span class="iconfont icon-copy"></span></div>
<input
- v-if="showInput"
+ v-if="!readonly && showInput"
type="text"
class="editable-title-input"
@blur="blurEvent()"
@@ -24,9 +30,9 @@
<div class="node-text" v-else>
{{ NODE_DEFAULT_TEXT.get(NodeType.COPY_TASK_NODE) }}
- <Icon icon="ep:arrow-right-bold" />
+ <Icon v-if="!readonly" icon="ep:arrow-right-bold" />
- <div class="node-toolbar">
+ <div v-if="!readonly" class="node-toolbar">
<div class="toolbar-icon"
><Icon color="#0089ff" icon="ep:circle-close-filled" :size="18" @click="deleteNode"
/></div>
@@ -34,15 +40,23 @@
<!-- 传递子节点给添加节点组件。会在子节点前面添加节点 -->
- <NodeHandler v-if="currentNode" v-model:child-node="currentNode.childNode" />
+ <NodeHandler
+ v-if="currentNode"
+ v-model:child-node="currentNode.childNode"
+ :current-node="currentNode"
- <CopyTaskNodeConfig v-if="currentNode" ref="nodeSetting" :flow-node="currentNode" />
+ <CopyTaskNodeConfig
+ v-if="!readonly && currentNode"
+ ref="nodeSetting"
import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
import NodeHandler from '../NodeHandler.vue'
-import { useNodeName2, useWatchNode } from '../node'
+import { useNodeName2, useWatchNode, useTaskStatusClass } from '../node'
import CopyTaskNodeConfig from '../nodes-config/CopyTaskNodeConfig.vue'
name: 'CopyTaskNode'
@@ -57,7 +71,8 @@ const props = defineProps({
const emits = defineEmits<{
'update:flowNode': [node: SimpleFlowNode | undefined]
}>()
+// 是否只读
+const readonly = inject<Boolean>('readonly')
// 监控节点的变化
const currentNode = useWatchNode(props)
// 节点名称编辑
@@ -66,6 +81,9 @@ const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.
const nodeSetting = ref()
// 打开节点配置
const openNodeConfig = () => {
+ if (readonly) {
nodeSetting.value.showCopyTaskNodeConfig(currentNode.value)
nodeSetting.value.openDrawer()
@@ -1,13 +1,102 @@
<div class="end-node-wrapper">
- <div class="end-node-box">
+ <div class="end-node-box cursor-pointer" :class="`${useTaskStatusClass(currentNode?.activityStatus)}`" @click="nodeClick">
<span class="node-fixed-name" title="结束">结束</span>
+ <el-dialog title="审批信息" v-model="dialogVisible" width="1000px" append-to-body>
+ <el-row>
+ <el-table
+ :data="processInstanceInfos"
+ size="small"
+ border
+ header-cell-class-name="table-header-gray"
+ <el-table-column
+ label="序号"
+ header-align="center"
+ align="center"
+ type="index"
+ width="50"
+ label="发起人"
+ prop="assigneeUser.nickname"
+ min-width="100"
+ <el-table-column label="部门" min-width="100" align="center">
+ <template #default="scope">
+ {{ scope.row.assigneeUser?.deptName || scope.row.ownerUser?.deptName }}
+ </el-table-column>
+ :formatter="dateFormatter"
+ label="开始时间"
+ prop="createTime"
+ min-width="140"
+ label="结束时间"
+ prop="endTime"
+ <el-table-column align="center" label="审批状态" prop="status" min-width="90">
+ <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="scope.row.status" />
+ <el-table-column align="center" label="耗时" prop="durationInMillis" width="100">
+ {{ formatPast2(scope.row.durationInMillis) }}
+ </el-table>
+ </el-dialog>
+import { SimpleFlowNode } from '../consts'
+import { useWatchNode, useTaskStatusClass } from '../node'
+import { dateFormatter, formatPast2 } from '@/utils/formatTime'
+import { DICT_TYPE } from '@/utils/dict'
name: 'EndEventNode'
+ default: () => null
+// 监控节点变化
+const currentNode = useWatchNode(props)
+const processInstance = inject<Ref<any>>('processInstance')
+// 审批信息的弹窗显示,用于只读模式
+const dialogVisible = ref(false) // 弹窗可见性
+const processInstanceInfos = ref<any[]>([]) // 流程的审批信息
+const nodeClick = () => {
+ if(processInstance && processInstance.value){
+ processInstanceInfos.value = [
+ assigneeUser: processInstance.value.startUser,
+ createTime: processInstance.value.startTime,
+ endTime: processInstance.value.endTime,
+ status: processInstance.value.status,
+ durationInMillis: processInstance.value.durationInMillis
+ dialogVisible.value = true
<style lang="scss" scoped></style>
@@ -1,7 +1,17 @@
<div class="branch-node-wrapper">
<div class="branch-node-container">
- <div class="branch-node-add" @click="addCondition">添加条件</div>
+ v-if="readonly"
+ class="branch-node-readonly"
+ :class="`${useTaskStatusClass(currentNode?.activityStatus)}`"
+ <span class="iconfont icon-exclusive icon-size condition"></span>
+ <el-button v-else class="branch-node-add" color="#67c23a" @click="addCondition" plain
+ >添加条件</el-button
class="branch-node-item"
v-for="(item, index) in currentNode.conditionNodes"
@@ -17,9 +27,15 @@
- <div class="node-box" :class="{ 'node-config-error': !item.showText }">
+ { 'node-config-error': !item.showText },
+ `${useTaskStatusClass(item.activityStatus)}`
<div class="branch-node-title-container">
- <div v-if="showInputs[index]">
+ <div v-if="!readonly && showInputs[index]">
class="input-max-width editable-title-input"
@@ -39,7 +55,10 @@
{{ NODE_DEFAULT_TEXT.get(NodeType.CONDITION_NODE) }}
- <div class="node-toolbar" v-if="index + 1 !== currentNode.conditionNodes?.length">
+ class="node-toolbar"
+ v-if="!readonly && index + 1 !== currentNode.conditionNodes?.length"
<div class="toolbar-icon">
<Icon
color="#0089ff"
@@ -65,7 +84,7 @@
<Icon icon="ep:arrow-right" />
- <NodeHandler v-model:child-node="item.childNode" />
+ <NodeHandler v-model:child-node="item.childNode" :current-node="item" />
<ConditionNodeConfig :node-index="index" :condition-node="item" :ref="item.id" />
@@ -78,7 +97,11 @@
@@ -87,6 +110,7 @@ import NodeHandler from '../NodeHandler.vue'
import ProcessNodeTree from '../ProcessNodeTree.vue'
import { getDefaultConditionNodeName } from '../utils'
+import { useTaskStatusClass } from '../node'
import { generateUUID } from '@/utils'
import ConditionNodeConfig from '../nodes-config/ConditionNodeConfig.vue'
const { proxy } = getCurrentInstance() as any
@@ -94,10 +118,6 @@ defineOptions({
name: 'ExclusiveNode'
- // parentNode : {
- // type: Object as () => SimpleFlowNode,
- // required: true
- // },
flowNode: {
@@ -113,10 +133,9 @@ const emits = defineEmits<{
nodeType: number
const currentNode = ref<SimpleFlowNode>(props.flowNode)
-// const conditionNodes = computed(() => currentNode.value.conditionNodes);
() => props.flowNode,
(newValue) => {
@@ -139,6 +158,9 @@ const clickEvent = (index: number) => {
const conditionNodeConfig = (nodeId: string) => {
const conditionNode = proxy.$refs[nodeId][0]
conditionNode.open()
@@ -193,7 +215,7 @@ const recursiveFindParentNode = (
node: SimpleFlowNode,
) => {
- if (!node || node.type === NodeType.START_EVENT_NODE) {
+ if (!node || node.type === NodeType.START_USER_NODE) {
if (node.type === nodeType) {
@@ -0,0 +1,233 @@
+ <div class="branch-node-wrapper">
+ <div class="branch-node-container">
+ <span class="iconfont icon-inclusive icon-size inclusive"></span>
+ <el-button v-else class="branch-node-add" color="#345da2" @click="addCondition" plain
+ class="branch-node-item"
+ v-for="(item, index) in currentNode.conditionNodes"
+ <template v-if="index == 0">
+ <div class="branch-line-first-top"> </div>
+ <div class="branch-line-first-bottom"></div>
+ <template v-if="index + 1 == currentNode.conditionNodes?.length">
+ <div class="branch-line-last-top"></div>
+ <div class="branch-line-last-bottom"></div>
+ <div class="node-wrapper">
+ <div class="node-container">
+ <div class="branch-node-title-container">
+ <div v-if="showInputs[index]">
+ <input
+ type="text"
+ class="editable-title-input"
+ @blur="blurEvent(index)"
+ v-mountedFocus
+ v-model="item.name"
+ <div v-else class="branch-title" @click="clickEvent(index)"> {{ item.name }} </div>
+ <div class="branch-node-content" @click="conditionNodeConfig(item.id)">
+ <div class="branch-node-text" :title="item.showText" v-if="item.showText">
+ {{ item.showText }}
+ <div class="branch-node-text" v-else>
+ {{ NODE_DEFAULT_TEXT.get(NodeType.CONDITION_NODE) }}
+ <div class="toolbar-icon">
+ <Icon
+ color="#0089ff"
+ icon="ep:circle-close-filled"
+ :size="18"
+ @click="deleteCondition(index)"
+ class="branch-node-move move-node-left"
+ v-if="!readonly && index != 0 && index + 1 !== currentNode.conditionNodes?.length"
+ @click="moveNode(index, -1)"
+ <Icon icon="ep:arrow-left" />
+ class="branch-node-move move-node-right"
+ !readonly &&
+ currentNode.conditionNodes &&
+ index < currentNode.conditionNodes.length - 2
+ @click="moveNode(index, 1)"
+ <Icon icon="ep:arrow-right" />
+ <ConditionNodeConfig :node-index="index" :condition-node="item" :ref="item.id" />
+ <!-- 递归显示子节点 -->
+ <ProcessNodeTree
+ v-if="item && item.childNode"
+ :parent-node="item"
+ v-model:flow-node="item.childNode"
+ @find:recursive-find-parent-node="recursiveFindParentNode"
+import NodeHandler from '../NodeHandler.vue'
+import ProcessNodeTree from '../ProcessNodeTree.vue'
+import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
+import { getDefaultInclusiveConditionNodeName } from '../utils'
+import { generateUUID } from '@/utils'
+import ConditionNodeConfig from '../nodes-config/ConditionNodeConfig.vue'
+const { proxy } = getCurrentInstance() as any
+ name: 'InclusiveNode'
+// 定义事件,更新父组件
+ 'update:modelValue': [node: SimpleFlowNode | undefined]
+ 'find:parentNode': [nodeList: SimpleFlowNode[], nodeType: number]
+ 'find:recursiveFindParentNode': [
+ nodeList: SimpleFlowNode[],
+ curentNode: SimpleFlowNode,
+ nodeType: number
+const currentNode = ref<SimpleFlowNode>(props.flowNode)
+ () => props.flowNode,
+ currentNode.value = newValue
+const showInputs = ref<boolean[]>([])
+// 失去焦点
+const blurEvent = (index: number) => {
+ showInputs.value[index] = false
+ const conditionNode = currentNode.value.conditionNodes?.at(index) as SimpleFlowNode
+ conditionNode.name =
+ conditionNode.name || getDefaultInclusiveConditionNodeName(index, conditionNode.defaultFlow)
+// 点击条件名称
+const clickEvent = (index: number) => {
+ showInputs.value[index] = true
+const conditionNodeConfig = (nodeId: string) => {
+ const conditionNode = proxy.$refs[nodeId][0]
+ conditionNode.open()
+// 新增条件
+const addCondition = () => {
+ const conditionNodes = currentNode.value.conditionNodes
+ if (conditionNodes) {
+ const len = conditionNodes.length
+ let lastIndex = len - 1
+ const conditionData: SimpleFlowNode = {
+ name: '包容条件' + len,
+ conditionNodes: [],
+ conditionType: 1,
+ conditionNodes.splice(lastIndex, 0, conditionData)
+// 删除条件
+const deleteCondition = (index: number) => {
+ conditionNodes.splice(index, 1)
+ if (conditionNodes.length == 1) {
+ const childNode = currentNode.value.childNode
+ // 更新此节点为后续孩子节点
+ emits('update:modelValue', childNode)
+// 移动节点
+const moveNode = (index: number, to: number) => {
+ // -1 :向左 1: 向右
+ if (currentNode.value.conditionNodes) {
+ currentNode.value.conditionNodes[index] = currentNode.value.conditionNodes.splice(
+ index + to,
+ 1,
+ currentNode.value.conditionNodes[index]
+ )[0]
+// 递归从父节点中查询匹配的节点
+const recursiveFindParentNode = (
+ node: SimpleFlowNode,
+ if (node.type === nodeType) {
+ nodeList.push(node)
+ // 条件节点 (NodeType.CONDITION_NODE) 比较特殊。需要调用其父节点条件分支节点(NodeType.INCLUSIVE_BRANCH_NODE) 继续查找
+ emits('find:parentNode', nodeList, nodeType)
@@ -1,7 +1,16 @@
- <div class="branch-node-add" @click="addCondition">添加分支</div>
+ <span class="iconfont icon-parallel icon-size parallel"></span>
+ <el-button v-else class="branch-node-add" color="#626aef" @click="addCondition" plain
+ >添加分支</el-button
@@ -17,7 +26,7 @@
- <div class="node-box">
+ <div class="node-box" :class="`${useTaskStatusClass(item.activityStatus)}`">
<div v-if="showInputs[index]">
@@ -39,7 +48,7 @@
@@ -49,20 +58,8 @@
- <!-- <div
- class="branch-node-move move-node-left"
- v-if="index != 0 && index + 1 !== currentNode.conditionNodes?.length" @click="moveNode(index, -1)">
- <Icon icon="ep:arrow-left" />
- </div> -->
- class="branch-node-move move-node-right"
- v-if="currentNode.conditionNodes && index < currentNode.conditionNodes.length - 2"
- @click="moveNode(index, 1)">
- <Icon icon="ep:arrow-right" />
<!-- 递归显示子节点 -->
@@ -74,7 +71,11 @@
@@ -82,8 +83,8 @@
name: 'ParallelNode'
@@ -106,6 +107,8 @@ const emits = defineEmits<{
@@ -169,7 +172,7 @@ const recursiveFindParentNode = (
@@ -1,7 +1,13 @@
<div class="node-title-icon start-user"
><span class="iconfont icon-start-user"></span
@@ -19,27 +25,88 @@
{{ currentNode.name }}
- <div class="node-content" @click="openNodeConfig">
+ <div class="node-content" @click="nodeClick">
<div class="node-text" :title="currentNode.showText" v-if="currentNode.showText">
{{ currentNode.showText }}
{{ NODE_DEFAULT_TEXT.get(NodeType.START_USER_NODE) }}
+ <Icon icon="ep:arrow-right-bold" v-if="!readonly" />
- <StartUserNodeConfig v-if="currentNode" ref="nodeSetting" :flow-node="currentNode" />
+ <StartUserNodeConfig v-if="!readonly && currentNode" ref="nodeSetting" :flow-node="currentNode" />
+ <!-- 审批记录 -->
+ <el-dialog
+ :title="dialogTitle || '审批记录'"
+ v-model="dialogVisible"
+ width="1000px"
+ append-to-body
+ <el-table :data="selectTasks" size="small" border header-cell-class-name="table-header-gray">
+ <el-table-column label="审批人" min-width="100" align="center">
+ {{ scope.row.assigneeUser?.nickname || scope.row.ownerUser?.nickname }}
+ <dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="scope.row.status" />
+ <el-table-column align="center" label="审批建议" prop="reason" min-width="120" />
-import { useWatchNode, useNodeName2 } from '../node'
+import { useWatchNode, useNodeName2, useTaskStatusClass } from '../node'
import { SimpleFlowNode, NODE_DEFAULT_TEXT, NodeType } from '../consts'
import StartUserNodeConfig from '../nodes-config/StartUserNodeConfig.vue'
name: 'StartEventNode'
@@ -49,6 +116,8 @@ const props = defineProps({
default: () => null
+const tasks = inject<Ref<any[]>>('tasks')
// 定义事件,更新父组件。
'update:modelValue': [node: SimpleFlowNode | undefined]
@@ -59,11 +128,27 @@ const currentNode = useWatchNode(props)
const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.START_USER_NODE)
-// 打开节点配置
-const openNodeConfig = () => {
- // 把当前节点传递给配置组件
- nodeSetting.value.showStartUserNodeConfig(currentNode.value)
- nodeSetting.value.openDrawer()
+//
+ // 只读模式,弹窗显示任务信息
+ if (tasks && tasks.value) {
+ dialogTitle.value = currentNode.value.name
+ selectTasks.value = tasks.value.filter(
+ (item: any) => item?.taskDefinitionKey === currentNode.value.id
+ // 编辑模式,打开节点配置、把当前节点传递给配置组件
+ nodeSetting.value.showStartUserNodeConfig(currentNode.value)
+ nodeSetting.value.openDrawer()
+// 任务的弹窗显示,用于只读模式
+const dialogTitle = ref<string | undefined>(undefined) // 弹窗标题
+const selectTasks = ref<any[] | undefined>([]) // 选中的任务数组
<div class="node-title-icon user-task"><span class="iconfont icon-approve"></span></div>
@@ -17,23 +23,27 @@
{{ NODE_DEFAULT_TEXT.get(NodeType.USER_TASK_NODE) }}
<UserTaskNodeConfig
@@ -42,12 +52,69 @@
:flow-node="currentNode"
@find:return-task-nodes="findReturnTaskNodes"
import UserTaskNodeConfig from '../nodes-config/UserTaskNodeConfig.vue'
name: 'UserTaskNode'
@@ -61,22 +128,36 @@ const emits = defineEmits<{
'find:parentNode': [nodeList: SimpleFlowNode[], nodeType: NodeType]
// 监控节点变化
- nodeSetting.value.showUserTaskNodeConfig(currentNode.value)
+ nodeSetting.value.showUserTaskNodeConfig(currentNode.value)
const deleteNode = () => {
emits('update:flowNode', currentNode.value.childNode)
// 查找可以驳回用户节点
const findReturnTaskNodes = (
matchNodeList: SimpleFlowNode[] // 匹配的节点
@@ -84,5 +165,10 @@ const findReturnTaskNodes = (
// 从父节点查找
emits('find:parentNode', matchNodeList, NodeType.USER_TASK_NODE)
@@ -8,6 +8,14 @@ export const getDefaultConditionNodeName = (index: number, defaultFlow: boolean
return '条件' + (index + 1)
+// 获取包容分支条件节点默认的名称
+export const getDefaultInclusiveConditionNodeName = (index: number, defaultFlow: boolean | undefined): string => {
+ if (defaultFlow) {
+ return '其它情况'
+ return '包容条件' + (index + 1)
export const convertTimeUnit = (strTimeUnit: string) => {
if (strTimeUnit === 'M') {
return TimeUnitType.MINUTE
@@ -1,512 +1,3 @@
-.simple-flow-canvas {
- position: absolute;
- inset: 0;
- z-index: 1;
- overflow: auto;
- background-color: #fafafa;
- user-select: none;
- .simple-flow-container {
- position: relative;
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- .top-area-container {
- position: sticky;
- width: 100%;
- height: 42px;
- // padding: 4px 0;
- background-color: #fff;
- justify-content: flex-end;
- .top-actions {
- margin: 4px;
- margin-right: 8px;
- .canvas-control {
- font-size: 16px;
- .control-scale-group {
- display: inline-flex;
- .control-scale-button {
- width: 28px;
- height: 28px;
- padding: 2px;
- text-align: center;
- cursor: pointer;
- .control-scale-label {
- margin: 0 4px;
- font-size: 14px;
- .scale-container {
- margin-top: 16px;
- transform-origin: 50% 0 0;
- transform: scale(1);
- transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
- // 节点容器 定义节点宽度
- .node-container {
- width: 200px;
- // 节点
- .node-box {
- min-height: 70px;
- padding: 5px 10px 8px;
- border: 2px solid transparent;
- // border-color: #0089ff;
- border-radius: 8px;
- box-shadow: 0 1px 4px 0 rgba(10, 30, 65, 0.16);
- transition: all 0.1s cubic-bezier(0.645, 0.045, 0.355, 1);
- &:hover {
- border-color: #0089ff;
- .node-toolbar {
- opacity: 1;
- .branch-node-move {
- // 普通节点标题
- .node-title-container {
- padding: 4px;
- border-radius: 4px 4px 0 0;
- .node-title-icon {
- &.user-task {
- color: #ff943e;
- &.copy-task {
- color: #3296fa;
- &.start-user {
- color: #676565;
- .node-title {
- margin-left: 4px;
- font-weight: 600;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- color: #1f1f1f;
- line-height: 18px;
- border-bottom: 1px dashed #f60;
- // 条件节点标题
- .branch-node-title-container {
- padding: 4px 0;
- justify-content: space-between;
- .input-max-width {
- max-width: 115px !important;
- .branch-title {
- font-size: 13px;
- color: #f60;
- border-bottom: 1px dashed #000;
- .branch-priority {
- min-width: 50px;
- .node-content {
- min-height: 32px;
- padding: 4px 8px;
- margin-top: 4px;
- line-height: 32px;
- color: #111f2c;
- background: rgba(0, 0, 0, 0.03);
- border-radius: 4px;
- .node-text {
- display: -webkit-box;
- line-height: 24px;
- word-break: break-all;
- -webkit-line-clamp: 2; /* 这将限制文本显示为两行 */
- -webkit-box-orient: vertical;
- //条件节点内容
- .branch-node-content {
- .branch-node-text {
- // 节点操作 :删除
- opacity: 0;
- top: -20px;
- right: 0px;
- .toolbar-icon {
- vertical-align: middle;
- // 条件节点左右移动
- width: 10px;
- display: none;
- height: 100%;
- .move-node-left {
- left: -2px;
- top: 0px;
- background: rgba(126, 134, 142, 0.08);
- border-top-left-radius: 8px;
- border-bottom-left-radius: 8px;
- .move-node-right {
- right: -2px;
- border-top-right-radius: 6px;
- border-bottom-right-radius: 6px;
- .node-config-error {
- border-color: #ff5219 !important;
- // 普通节点包装
- .node-wrapper {
- // 节点连线处理
- .node-handler-wrapper {
- height: 70px;
- &::before {
- top: 0;
- right: 0;
- left: 0;
- // bottom: 5px;
- bottom: 0px;
- z-index: 0;
- width: 2px;
- // height: calc(100% - 5px);
- margin: auto;
- background-color: #dedede;
- content: '';
- .node-handler {
- .add-icon {
- top: -5px;
- width: 25px;
- height: 25px;
- color: #fff;
- background-color: #0089ff;
- border-radius: 50%;
- transform: scale(1.1);
- .node-handler-arrow {
- bottom: 0;
- left: 50%;
- transform: translateX(-50%);
- // 条件节点包装
- .branch-node-wrapper {
- .branch-node-container {
- width: 4px;
- transform: translate(-50%);
- .branch-node-add {
- top: -18px;
- height: 36px;
- padding: 0 10px;
- font-size: 12px;
- line-height: 36px;
- color: #222;
- background: #fff;
- border: 2px solid #dedede;
- border-radius: 18px;
- transform-origin: center center;
- transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
- .branch-node-item {
- min-width: 280px;
- padding: 40px 40px 0;
- background: transparent;
- border-top: 2px solid #dedede;
- border-bottom: 2px solid #dedede;
- // 覆盖条件节点第一个节点左上角的线
- .branch-line-first-top {
- left: -1px;
- width: 50%;
- height: 7px;
- // 覆盖条件节点第一个节点左下角的线
- .branch-line-first-bottom {
- bottom: -5px;
- // 覆盖条件节点最后一个节点右上角的线
- .branch-line-last-top {
- right: -1px;
- // 覆盖条件节点最后一个节点右下角的线
- .branch-line-last-bottom {
- .node-fixed-name {
- display: inline-block;
- width: auto;
- padding: 0 4px;
- // 开始节点包装
- .start-node-wrapper {
- .start-node-container {
- .start-node-box {
- width: 90px;
- padding: 3px 4px;
- color: #212121;
- // background: #2c2c2c;
- background: #fafafa;
- border-radius: 30px;
- box-shadow: 0 1px 5px 0 rgba(10, 30, 65, 0.08);
- box-sizing: border-box;
- // 结束节点包装
- .end-node-wrapper {
- margin-bottom: 16px;
- .end-node-box {
- width: 80px;
- // background: #6e6e6e;
- // 可编辑的 title 输入框
- .editable-title-input {
- height: 20px;
- max-width: 145px;
- line-height: 20px;
- border: 1px solid #d9d9d9;
- transition: all 0.3s;
- &:focus {
- border-color: #40a9ff;
- outline: 0;
- box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
// 配置节点头部
.config-header {
display: flex;
@@ -626,16 +117,17 @@
cursor: pointer;
.handler-item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
.handler-item-icon {
- height: 80px;
+ width: 60px;
+ height: 60px;
background: #fff;
border: 1px solid #e2e2e2;
border-radius: 50%;
user-select: none;
text-align: center;
@@ -645,8 +137,8 @@
.icon-size {
- font-size: 35px;
- line-height: 80px;
+ font-size: 25px;
+ line-height: 60px;
@@ -658,13 +150,557 @@
.condition {
- color: #15bc83;
+ color: #67c23a;
+ .parallel {
+ color: #626aef;
+ .inclusive {
+ color: #345da2;
.handler-item-text {
margin-top: 4px;
width: 80px;
+ font-size: 13px;
+// Simple 流程模型样式
+.simple-process-model-container {
+ height: 100%;
+ padding-top: 32px;
+ background-color: #fafafa;
+ .simple-process-model {
+ justify-content: center;
+ transform-origin: 50% 0 0;
+ overflow: auto;
+ transform: scale(1);
+ transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+ background: url(@/assets/svgs/bpm/simple-process-bg.svg) 0 0 repeat;
+ // 节点容器 定义节点宽度
+ .node-container {
+ width: 200px;
+ // 节点
+ .node-box {
+ position: relative;
+ min-height: 70px;
+ padding: 5px 10px 8px;
+ cursor: pointer;
+ background-color: #fff;
+ border: 2px solid transparent;
+ border-radius: 8px;
+ box-shadow: 0 1px 4px 0 rgb(10 30 65 / 16%);
+ transition: all 0.1s cubic-bezier(0.645, 0.045, 0.355, 1);
+ &.status-pass {
+ background-color: #a9da90;
+ border-color: #67c23a;
+ &.status-pass:hover {
+ &.status-running {
+ background-color: #e7f0fe;
+ border-color: #5a9cf8;
+ &.status-running:hover {
+ &.status-reject {
+ background-color: #f6e5e5;
+ border-color: #e47470;
+ &.status-reject:hover {
+ &:hover {
+ border-color: #0089ff;
+ .node-toolbar {
+ opacity: 1;
+ .branch-node-move {
+ // 普通节点标题
+ .node-title-container {
+ padding: 4px;
+ border-radius: 4px 4px 0 0;
+ .node-title-icon {
+ &.user-task {
+ color: #ff943e;
+ &.copy-task {
+ color: #3296fa;
+ &.start-user {
+ color: #676565;
+ .node-title {
+ margin-left: 4px;
+ overflow: hidden;
+ font-size: 14px;
+ font-weight: 600;
+ line-height: 18px;
+ color: #1f1f1f;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ border-bottom: 1px dashed #f60;
+ // 条件节点标题
+ .branch-node-title-container {
+ padding: 4px 0;
+ justify-content: space-between;
+ .input-max-width {
+ max-width: 115px !important;
+ .branch-title {
+ color: #f60;
+ border-bottom: 1px dashed #000;
+ .branch-priority {
+ min-width: 50px;
+ font-size: 12px;
+ .node-content {
+ min-height: 32px;
+ padding: 4px 8px;
+ margin-top: 4px;
+ line-height: 32px;
+ color: #111f2c;
+ background: rgb(0 0 0 / 3%);
+ border-radius: 4px;
+ .node-text {
+ display: -webkit-box;
+ line-height: 24px;
+ word-break: break-all;
+ -webkit-line-clamp: 2; /* 这将限制文本显示为两行 */
+ -webkit-box-orient: vertical;
+ //条件节点内容
+ .branch-node-content {
+ .branch-node-text {
+ // 节点操作 :删除
+ position: absolute;
+ top: -20px;
+ right: 0;
+ opacity: 0;
+ .toolbar-icon {
+ text-align: center;
+ vertical-align: middle;
+ // 条件节点左右移动
+ display: none;
+ width: 10px;
+ .move-node-left {
+ top: 0;
+ left: -2px;
+ background: rgb(126 134 142 / 8%);
+ border-bottom-left-radius: 8px;
+ border-top-left-radius: 8px;
+ .move-node-right {
+ right: -2px;
+ border-top-right-radius: 6px;
+ border-bottom-right-radius: 6px;
+ .node-config-error {
+ border-color: #ff5219 !important;
+ // 普通节点包装
+ .node-wrapper {
+ // 节点连线处理
+ .node-handler-wrapper {
+ height: 70px;
+ user-select: none;
+ &::before {
+ z-index: 0;
+ width: 2px;
+ margin: auto;
+ background-color: #dedede;
+ content: '';
+ .node-handler {
+ .add-icon {
+ top: -5px;
+ width: 25px;
+ height: 25px;
+ color: #fff;
+ background-color: #0089ff;
+ border-radius: 50%;
+ transform: scale(1.1);
+ .node-handler-arrow {
+ bottom: 0;
+ left: 50%;
+ transform: translateX(-50%);
+ // 条件节点包装
+ .branch-node-wrapper {
+ margin-top: 16px;
+ .branch-node-container {
+ width: 4px;
+ transform: translate(-50%);
+ .branch-node-add {
+ top: -18px;
+ z-index: 1;
+ height: 36px;
+ padding: 0 10px;
+ line-height: 36px;
+ border: 2px solid #dedede;
+ border-radius: 18px;
+ transform-origin: center center;
+ .branch-node-readonly {
+ width: 36px;
+ background-color: #e9f4e2;
+ border-color: #6bb63c;
+ .icon-size {
+ font-size: 22px;
+ &.condition {
+ &.parallel {
+ &.inclusive {
+ .branch-node-item {
+ min-width: 280px;
+ padding: 40px 40px 0;
+ background: transparent;
+ border-top: 2px solid #dedede;
+ border-bottom: 2px solid #dedede;
+ inset: 0;
+ // 覆盖条件节点第一个节点左上角的线
+ .branch-line-first-top {
+ left: -1px;
+ width: 50%;
+ height: 7px;
+ // 覆盖条件节点第一个节点左下角的线
+ .branch-line-first-bottom {
+ bottom: -5px;
+ // 覆盖条件节点最后一个节点右上角的线
+ .branch-line-last-top {
+ right: -1px;
+ // 覆盖条件节点最后一个节点右下角的线
+ .branch-line-last-bottom {
+ .node-fixed-name {
+ display: inline-block;
+ width: auto;
+ padding: 0 4px;
+ // 开始节点包装
+ .start-node-wrapper {
+ .start-node-container {
+ .start-node-box {
+ width: 90px;
+ padding: 3px 4px;
+ color: #212121;
+ background: #fafafa;
+ border-radius: 30px;
+ box-shadow: 0 1px 5px 0 rgb(10 30 65 / 8%);
+ box-sizing: border-box;
+ // 结束节点包装
+ .end-node-wrapper {
+ margin-bottom: 16px;
+ .end-node-box {
+ width: 80px;
+ border: 2px solid #fafafa;
+ &.status-cancel {
+ background-color: #eaeaeb;
+ border-color: #919398;
+ &.status-cancel:hover {
+ // 可编辑的 title 输入框
+ .editable-title-input {
+ height: 20px;
+ max-width: 145px;
+ line-height: 20px;
+ border: 1px solid #d9d9d9;
+ transition: all 0.3s;
+ &:focus {
+ border-color: #40a9ff;
+ outline: 0;
+ box-shadow: 0 0 0 2px rgb(24 144 255 / 20%);
@@ -0,0 +1,152 @@
+ <Dialog v-model="dialogVisible" title="人员选择" width="800">
+ <el-row class="gap2" v-loading="formLoading">
+ <el-col :span="6">
+ <ContentWrap class="h-1/1">
+ <el-tree
+ ref="treeRef"
+ :data="deptTree"
+ :expand-on-click-node="false"
+ :props="defaultProps"
+ default-expand-all
+ highlight-current
+ node-key="id"
+ @node-click="handleNodeClick"
+ </ContentWrap>
+ </el-col>
+ <el-col :span="17">
+ <el-transfer
+ v-model="selectedUserIdList"
+ :titles="['未选', '已选']"
+ filterable
+ filter-placeholder="搜索成员"
+ :data="transferUserList"
+ :props="{ label: 'nickname', key: 'id' }"
+ :disabled="formLoading || !selectedUserIdList?.length"
+ @click="submitForm"
+ 确 定
+ </el-button>
+ <el-button @click="dialogVisible = false">取 消</el-button>
+<script lang="ts" setup>
+import { defaultProps, findTreeNode, handleTree } from '@/utils/tree'
+import * as DeptApi from '@/api/system/dept'
+import * as UserApi from '@/api/system/user'
+defineOptions({ name: 'UserSelectForm' })
+const emit = defineEmits<{
+ confirm: [id: any, userList: any[]]
+const { t } = useI18n() // 国际
+const deptTree = ref<Tree[]>([]) // 部门树形结构化
+const userList = ref<UserApi.UserVO[]>([]) // 所有用户列表
+const filteredUserList = ref<UserApi.UserVO[]>([]) // 当前部门过滤后的用户列表
+const selectedUserIdList: any = ref([]) // 选中的用户列表
+const dialogVisible = ref(false) // 弹窗的是否展示
+const formLoading = ref(false) // 表单的加载中
+const activityId = ref()
+/** 计算属性:合并已选择的用户和当前部门过滤后的用户 */
+const transferUserList = computed(() => {
+ // 1.1 获取所有已选择的用户
+ const selectedUsers = userList.value.filter((user: any) =>
+ selectedUserIdList.value.includes(user.id)
+ // 1.2 获取当前部门过滤后的未选择用户
+ const filteredUnselectedUsers = filteredUserList.value.filter(
+ (user: any) => !selectedUserIdList.value.includes(user.id)
+ // 2. 合并并去重
+ return [...selectedUsers, ...filteredUnselectedUsers]
+/** 打开弹窗 */
+const open = async (id: number, selectedList?: any[]) => {
+ activityId.value = id
+ resetForm()
+ // 加载部门、用户列表
+ deptTree.value = handleTree(await DeptApi.getSimpleDeptList())
+ userList.value = await UserApi.getSimpleUserList()
+ // 初始状态下,过滤列表等于所有用户列表
+ filteredUserList.value = [...userList.value]
+ selectedUserIdList.value = selectedList?.map((item: any) => item.id) || []
+/** 获取部门过滤后的用户列表 */
+const getUserList = async (deptId?: number) => {
+ formLoading.value = true
+ // @ts-ignore
+ // TODO @芋艿:替换到 simple List 暂不支持 deptId 过滤
+ // TODO @Zqqq:这个,可以使用前端过滤么?通过 deptList 获取到 deptId 子节点,然后去 userList
+ const data = await UserApi.getUserPage({ pageSize: 100, pageNo: 1, deptId })
+ // 更新过滤后的用户列表
+ filteredUserList.value = data.list
+ formLoading.value = false
+/** 提交选择 */
+const submitForm = async () => {
+ message.success(t('common.updateSuccess'))
+ dialogVisible.value = false
+ // 从所有用户列表中筛选出已选择的用户
+ const emitUserList = userList.value.filter((user: any) =>
+ // 发送操作成功的事件
+ emit('confirm', activityId.value, emitUserList)
+/** 重置表单 */
+const resetForm = () => {
+ deptTree.value = []
+ userList.value = []
+ filteredUserList.value = []
+ selectedUserIdList.value = []
+/** 处理部门被点击 */
+const handleNodeClick = (row: { [key: string]: any }) => {
+ getUserList(row.id)
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+<style lang="scss" scoped>
+:deep() {
+ .el-transfer {
+ .el-transfer__buttons {
+ display: flex !important;
+ flex-direction: column-reverse;
+ gap: 20px;
+ .el-transfer__button:nth-child(2) {
+ margin: 0;
+</style>
@@ -1,664 +1,379 @@
- <div class="my-process-designer">
- <div class="my-process-designer__container">
- <div class="my-process-designer__canvas" style="height: 760px" ref="bpmnCanvas"></div>
+ <div class="process-viewer">
+ <div style="height: 100%" ref="processCanvas" v-show="!isLoading"> </div>
+ <!-- 自定义箭头样式,用于已完成状态下流程连线箭头 -->
+ <defs ref="customDefs">
+ <marker
+ id="sequenceflow-end-white-success"
+ viewBox="0 0 20 20"
+ refX="11"
+ refY="10"
+ markerWidth="10"
+ markerHeight="10"
+ orient="auto"
+ <path
+ class="success-arrow"
+ d="M 1 5 L 11 10 L 1 15 Z"
+ style="stroke-width: 1px; stroke-linecap: round; stroke-dasharray: 10000, 1"
+ </marker>
+ id="conditional-flow-marker-white-success"
+ refX="-1"
+ class="success-conditional"
+ d="M 0 10 L 8 6 L 16 10 L 8 14 Z"
+ </defs>
+ <el-dialog :title="dialogTitle || '审批记录'" v-model="dialogVisible" width="1000px">
+ :data="selectTasks"
+ label="审批人"
+ v-if="selectActivityType === 'bpmn:UserTask'"
+ v-else
+ label="审批建议"
+ prop="reason"
+ min-width="120"
+ <!-- Zoom:放大、缩小 -->
+ <div style="position: absolute; top: 0; left: 0; width: 100%">
+ :plain="true"
+ :disabled="defaultZoom <= 0.3"
+ :icon="ZoomOut"
+ @click="processZoomOut()"
+ <el-button size="default" style="width: 90px">
+ {{ Math.floor(defaultZoom * 10 * 10) + '%' }}
+ :disabled="defaultZoom >= 3.9"
+ :icon="ZoomIn"
+ @click="processZoomIn()"
<script lang="ts" setup>
+import '../theme/index.scss'
import BpmnViewer from 'bpmn-js/lib/Viewer'
-import DefaultEmptyXML from './plugins/defaultEmpty'
-import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-import { formatDate } from '@/utils/formatTime'
-import { isEmpty } from '@/utils/is'
-defineOptions({ name: 'MyProcessViewer' })
+import MoveCanvasModule from 'diagram-js/lib/navigation/movecanvas'
+import { ZoomOut, ZoomIn, ScaleToOriginal } from '@element-plus/icons-vue'
+import { BpmProcessInstanceStatus } from '@/utils/constants'
- value: {
- // BPMN XML 字符串
- type: String,
- default: ''
- },
- prefix: {
- // 使用哪个引擎
+ xml: {
- default: 'camunda'
- activityData: {
- // 活动的数据。传递时,可高亮流程
- type: Array,
- default: () => []
- processInstanceData: {
- // 流程实例的数据。传递时,可展示流程发起人等信息
+ view: {
type: Object,
- default: () => {}
- taskData: {
- // 任务实例的数据。传递时,可展示 UserTask 审核相关的信息
+ require: true
-provide('configGlobal', props)
-const emit = defineEmits(['destroy'])
+const processCanvas = ref()
+const bpmnViewer = ref<BpmnViewer | null>(null)
+const customDefs = ref()
+const defaultZoom = ref(1) // 默认缩放比例
+const isLoading = ref(false) // 是否加载中
-let bpmnModeler
+const processInstance = ref<any>({}) // 流程实例
+const tasks = ref([]) // 流程任务
-const xml = ref('')
-const activityLists = ref<any[]>([])
-const processInstance = ref<any>(undefined)
-const taskList = ref<any[]>([])
-const bpmnCanvas = ref()
-// const element = ref()
-const elementOverlayIds = ref<any>(null)
-const overlays = ref<any>(null)
+const selectActivityType = ref<string | undefined>(undefined) // 选中 Task 的活动编号
+const selectTasks = ref<any[]>([]) // 选中的任务数组
-const initBpmnModeler = () => {
- if (bpmnModeler) return
- bpmnModeler = new BpmnViewer({
- container: bpmnCanvas.value,
- bpmnRenderer: {}
+/** Zoom:恢复 */
+ defaultZoom.value = 1
+ bpmnViewer.value?.get('canvas').zoom('fit-viewport', 'auto')
-/* 创建新的流程图 */
-const createNewDiagram = async (xml) => {
- // 将字符串转换成图显示出来
- let newId = `Process_${new Date().getTime()}`
- let newName = `业务流程_${new Date().getTime()}`
- let xmlString = xml || DefaultEmptyXML(newId, newName, props.prefix)
- try {
- let { warnings } = await bpmnModeler.importXML(xmlString)
- if (warnings && warnings.length) {
- warnings.forEach((warn) => console.warn(warn))
- // 高亮流程图
- await highlightDiagram()
- const canvas = bpmnModeler.get('canvas')
- canvas.zoom('fit-viewport', 'auto')
- } catch (e) {
- console.error(e)
- // console.error(`[Process Designer Warn]: ${e?.message || e}`);
+/** Zoom:放大 */
+const processZoomIn = (zoomStep = 0.1) => {
+ let newZoom = Math.floor(defaultZoom.value * 100 + zoomStep * 100) / 100
+ if (newZoom > 4) {
+ throw new Error('[Process Designer Warn ]: The zoom ratio cannot be greater than 4')
+ defaultZoom.value = newZoom
+ bpmnViewer.value?.get('canvas').zoom(defaultZoom.value)
-/* 高亮流程图 */
-// TODO 芋艿:如果多个 endActivity 的话,目前的逻辑可能有一定的问题。https://www.jdon.com/workflow/multi-events.html
-const highlightDiagram = async () => {
- const activityList = activityLists.value
- if (activityList.length === 0) {
- // 参考自 https://gitee.com/tony2y/RuoYi-flowable/blob/master/ruoyi-ui/src/components/Process/index.vue#L222 实现
- // 再次基础上,增加不同审批结果的颜色等等
- let canvas = bpmnModeler.get('canvas')
- let todoActivity: any = activityList.find((m: any) => !m.endTime) // 找到待办的任务
- let endActivity: any = activityList[activityList.length - 1] // 获得最后一个任务
- let findProcessTask = false //是否已经高亮了进行中的任务
- //进行中高亮之后的任务 key 集合,用于过滤掉 taskList 进行中后面的任务,避免进行中后面的数据 Hover 还有数据
- let removeTaskDefinitionKeyList = []
- // debugger
- bpmnModeler.getDefinitions().rootElements[0].flowElements?.forEach((n: any) => {
- let activity: any = activityList.find((m: any) => m.key === n.id) // 找到对应的活动
- if (!activity) {
- if (n.$type === 'bpmn:UserTask') {
- // 用户任务
- // 处理用户任务的高亮
- const task: any = taskList.value.find((m: any) => m.id === activity.taskId) // 找到活动对应的 taskId
- if (!task) {
- // 进行中的任务已经高亮过了,则不高亮后面的任务了
- if (findProcessTask) {
- removeTaskDefinitionKeyList.push(n.id)
- // 高亮任务
- canvas.addMarker(n.id, getResultCss(task.status))
- //标记是否高亮了进行中任务
- if (task.status === 1) {
- findProcessTask = true
- // 如果非通过,就不走后面的线条了
- if (task.status !== 2) {
- // 处理 outgoing 出线
- const outgoing = getActivityOutgoing(activity)
- outgoing?.forEach((nn: any) => {
- let targetActivity: any = activityList.find((m: any) => m.key === nn.targetRef.id)
- // 如果目标活动存在,则根据该活动是否结束,进行【bpmn:SequenceFlow】连线的高亮设置
- if (targetActivity) {
- canvas.addMarker(nn.id, targetActivity.endTime ? 'highlight' : 'highlight-todo')
- } else if (nn.targetRef.$type === 'bpmn:ExclusiveGateway') {
- // TODO 芋艿:这个流程,暂时没走到过
- canvas.addMarker(nn.id, activity.endTime ? 'highlight' : 'highlight-todo')
- canvas.addMarker(nn.targetRef.id, activity.endTime ? 'highlight' : 'highlight-todo')
- } else if (nn.targetRef.$type === 'bpmn:EndEvent') {
- if (!todoActivity && endActivity.key === n.id) {
- canvas.addMarker(nn.id, 'highlight')
- canvas.addMarker(nn.targetRef.id, 'highlight')
- if (!activity.endTime) {
- canvas.addMarker(nn.id, 'highlight-todo')
- canvas.addMarker(nn.targetRef.id, 'highlight-todo')
- } else if (n.$type === 'bpmn:ExclusiveGateway') {
- // 排它网关
- // 设置【bpmn:ExclusiveGateway】排它网关的高亮
- canvas.addMarker(n.id, getActivityHighlightCss(activity))
- // 查找需要高亮的连线
- let matchNN: any = undefined
- let matchActivity: any = undefined
- n.outgoing?.forEach((nn: any) => {
- let targetActivity = activityList.find((m: any) => m.key === nn.targetRef.id)
- if (!targetActivity) {
- // 特殊判断 endEvent 类型的原因,ExclusiveGateway 可能后续连有 2 个路径:
- // 1. 一个是 UserTask => EndEvent
- // 2. 一个是 EndEvent
- // 在选择路径 1 时,其实 EndEvent 可能也存在,导致 1 和 2 都高亮,显然是不正确的。
- // 所以,在 matchActivity 为 EndEvent 时,需要进行覆盖~~
- if (!matchActivity || matchActivity.type === 'endEvent') {
- matchNN = nn
- matchActivity = targetActivity
- if (matchNN && matchActivity) {
- canvas.addMarker(matchNN.id, getActivityHighlightCss(matchActivity))
- } else if (n.$type === 'bpmn:ParallelGateway') {
- // 并行网关
- // 设置【bpmn:ParallelGateway】并行网关的高亮
- // 获得连线是否有指向目标。如果有,则进行高亮
- const targetActivity = activityList.find((m: any) => m.key === nn.targetRef.id)
- canvas.addMarker(nn.id, getActivityHighlightCss(targetActivity)) // 高亮【bpmn:SequenceFlow】连线
- // 高亮【...】目标。其中 ... 可以是 bpm:UserTask、也可以是其它的。当然,如果是 bpm:UserTask 的话,其实不做高亮也没问题,因为上面有逻辑做了这块。
- canvas.addMarker(nn.targetRef.id, getActivityHighlightCss(targetActivity))
- } else if (n.$type === 'bpmn:StartEvent') {
- // 开始节点
- canvas.addMarker(n.id, 'highlight')
- n.outgoing?.forEach((nn) => {
- // outgoing 例如说【bpmn:SequenceFlow】连线
- canvas.addMarker(nn.id, 'highlight') // 高亮【bpmn:SequenceFlow】连线
- canvas.addMarker(n.id, 'highlight') // 高亮【bpmn:StartEvent】开始节点(自己)
- } else if (n.$type === 'bpmn:EndEvent') {
- // 结束节点
- if (!processInstance.value || processInstance.value.status === 1) {
- canvas.addMarker(n.id, getResultCss(processInstance.value.status))
- } else if (n.$type === 'bpmn:ServiceTask') {
- //服务任务
- if (activity.startTime > 0 && activity.endTime === 0) {
- //进入执行,标识进行色
- canvas.addMarker(n.id, getResultCss(1))
- if (activity.endTime > 0) {
- // 执行完成,节点标识完成色, 所有outgoing标识完成色。
- canvas.addMarker(n.id, getResultCss(2))
- outgoing?.forEach((out) => {
- canvas.addMarker(out.id, getResultCss(2))
- } else if (n.$type === 'bpmn:SequenceFlow') {
- let targetActivity = activityList.find((m: any) => m.key === n.targetRef.id)
- canvas.addMarker(n.id, getActivityHighlightCss(targetActivity))
- if (!isEmpty(removeTaskDefinitionKeyList)) {
- taskList.value = taskList.value.filter(
- (item) => !removeTaskDefinitionKeyList.includes(item.taskDefinitionKey)
+/** Zoom:缩小 */
+const processZoomOut = (zoomStep = 0.1) => {
+ let newZoom = Math.floor(defaultZoom.value * 100 - zoomStep * 100) / 100
+ if (newZoom < 0.2) {
+ throw new Error('[Process Designer Warn ]: The zoom ratio cannot be less than 0.2')
-const getActivityHighlightCss = (activity) => {
- return activity.endTime ? 'highlight' : 'highlight-todo'
-const getResultCss = (status) => {
- if (status === 1) {
- // 审批中
- return 'highlight-todo'
- } else if (status === 2) {
- // 已通过
- return 'highlight'
- } else if (status === 3) {
- // 不通过
- return 'highlight-reject'
- } else if (status === 4) {
- // 已取消
- return 'highlight-cancel'
- } else if (status === 5) {
- // 退回
- return 'highlight-return'
- } else if (status === 6) {
- // 委派
- } else if (status === 7) {
- // 审批通过中
- } else if (status === 0) {
- // 待审批
+/** 流程图预览清空 */
+const clearViewer = () => {
+ if (processCanvas.value) {
+ processCanvas.value.innerHTML = ''
+ if (bpmnViewer.value) {
+ bpmnViewer.value.destroy()
- return ''
+ bpmnViewer.value = null
-const getActivityOutgoing = (activity) => {
- // 如果有 outgoing,则直接使用它
- if (activity.outgoing && activity.outgoing.length > 0) {
- return activity.outgoing
+/** 添加自定义箭头 */
+// TODO 芋艿:自定义箭头不生效,有点奇怪!!!!相关的 marker-end、marker-start 暂时也注释了!!!
+const addCustomDefs = () => {
+ if (!bpmnViewer.value) {
- // 如果没有,则遍历获得起点为它的【bpmn:SequenceFlow】节点们。原因是:bpmn-js 的 UserTask 拿不到 outgoing
- const flowElements = bpmnModeler.getDefinitions().rootElements[0].flowElements
- const outgoing: any[] = []
- flowElements.forEach((item: any) => {
- if (item.$type !== 'bpmn:SequenceFlow') {
- if (item.sourceRef.id === activity.key) {
- outgoing.push(item)
- return outgoing
+ const canvas = bpmnViewer.value?.get('canvas')
+ const svg = canvas?._svg
+ svg.appendChild(customDefs.value)
-const initModelListeners = () => {
- const EventBus = bpmnModeler.get('eventBus')
- // 注册需要的监听事件
- EventBus.on('element.hover', function (eventObj) {
- let element = eventObj ? eventObj.element : null
- elementHover(element)
- EventBus.on('element.out', function (eventObj) {
- elementOut(element)
-// 流程图的元素被 hover
-const elementHover = (element) => {
- element.value = element
- !elementOverlayIds.value && (elementOverlayIds.value = {})
- !overlays.value && (overlays.value = bpmnModeler.get('overlays'))
- // 展示信息
- // console.log(activityLists.value, 'activityLists.value')
- // console.log(element.value, 'element.value')
- const activity = activityLists.value.find((m) => m.key === element.value.id)
- // console.log(activity, 'activityactivityactivityactivity')
+/** 节点选中 */
+const onSelectElement = (element: any) => {
+ // 清空原选中
+ selectActivityType.value = undefined
+ dialogTitle.value = undefined
+ if (!element || !processInstance.value?.id) {
- if (!elementOverlayIds.value[element.value.id] && element.value.type !== 'bpmn:Process') {
- let html = `<div class="element-overlays">
- <p>Elemet id: ${element.value.id}</p>
- <p>Elemet type: ${element.value.type}</p>
- </div>` // 默认值
- if (element.value.type === 'bpmn:StartEvent' && processInstance.value) {
- html = `<p>发起人:${processInstance.value.startUser.nickname}</p>
- <p>部门:${processInstance.value.startUser.deptName}</p>
- <p>创建时间:${formatDate(processInstance.value.createTime)}`
- } else if (element.value.type === 'bpmn:UserTask') {
- let task = taskList.value.find((m) => m.id === activity.taskId) // 找到活动对应的 taskId
- let optionData = getIntDictOptions(DICT_TYPE.BPM_TASK_STATUS)
- let dataResult = ''
- optionData.forEach((element) => {
- if (element.value == task.status) {
- dataResult = element.label
- html = `<p>审批人:${task.assigneeUser.nickname}</p>
- <p>部门:${task.assigneeUser.deptName}</p>
- <p>结果:${dataResult}</p>
- <p>创建时间:${formatDate(task.createTime)}</p>`
- // html = `<p>审批人:${task.assigneeUser.nickname}</p>
- // <p>部门:${task.assigneeUser.deptName}</p>
- // <p>结果:${getIntDictOptions(
- // DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT,
- // task.status
- // )}</p>
- // <p>创建时间:${formatDate(task.createTime)}</p>`
- if (task.endTime) {
- html += `<p>结束时间:${formatDate(task.endTime)}</p>`
- if (task.reason) {
- html += `<p>审批建议:${task.reason}</p>`
- } else if (element.value.type === 'bpmn:ServiceTask' && processInstance.value) {
- if (activity.startTime > 0) {
- html = `<p>创建时间:${formatDate(activity.startTime)}</p>`
- html += `<p>结束时间:${formatDate(activity.endTime)}</p>`
- console.log(html)
- } else if (element.value.type === 'bpmn:EndEvent' && processInstance.value) {
- if (element.value == processInstance.value.status) {
- html = `<p>结果:${dataResult}</p>`
- // html = `<p>结果:${getIntDictOptions(
- // processInstance.value.status
- // )}</p>`
- if (processInstance.value.endTime) {
- html += `<p>结束时间:${formatDate(processInstance.value.endTime)}</p>`
+ // UserTask 的情况
+ const activityType = element.type
+ selectActivityType.value = activityType
+ if (activityType === 'bpmn:UserTask') {
+ dialogTitle.value = element.businessObject ? element.businessObject.name : undefined
+ selectTasks.value = tasks.value.filter((item: any) => item?.taskDefinitionKey === element.id)
+ } else if (activityType === 'bpmn:EndEvent' || activityType === 'bpmn:StartEvent') {
+ dialogTitle.value = '审批信息'
+ selectTasks.value = [
- // console.log(html, 'html111111111111111')
- elementOverlayIds.value[element.value.id] = toRaw(overlays.value)?.add(element.value, {
- position: { left: 0, bottom: 0 },
- html: `<div class="element-overlays">${html}</div>`
-// 流程图的元素被 out
-const elementOut = (element) => {
- toRaw(overlays.value).remove({ element })
- elementOverlayIds.value[element.id] = null
+/** 初始化 BPMN 视图 */
+const importXML = async (xml: string) => {
+ // 清空流程图
+ clearViewer()
-onMounted(() => {
- xml.value = props.value
- activityLists.value = props.activityData
- // 初始化
- initBpmnModeler()
- createNewDiagram(xml.value)
- // 初始模型的监听器
- initModelListeners()
-})
+ // 初始化流程图
+ if (xml != null && xml !== '') {
+ bpmnViewer.value = new BpmnViewer({
+ additionalModules: [MoveCanvasModule],
+ container: processCanvas.value
+ // 增加点击事件
+ bpmnViewer.value.on('element.click', ({ element }) => {
+ onSelectElement(element)
-onBeforeUnmount(() => {
- // this.$once('hook:beforeDestroy', () => {
- if (bpmnModeler) bpmnModeler.destroy()
- emit('destroy', bpmnModeler)
- bpmnModeler = null
+ // 初始化 BPMN 视图
+ isLoading.value = true
+ await bpmnViewer.value.importXML(xml)
+ // 自定义成功的箭头
+ addCustomDefs()
+ } catch (e) {
+ isLoading.value = false
+ // 高亮流程
+ setProcessStatus(props.view)
-watch(
- () => props.value,
- (newValue) => {
- xml.value = newValue
+/** 高亮流程 */
+const setProcessStatus = (view: any) => {
+ // 设置相关变量
+ if (!view || !view.processInstance) {
- () => props.activityData,
- (newActivityData) => {
- activityLists.value = newActivityData
+ processInstance.value = view.processInstance
+ tasks.value = view.tasks
+ if (isLoading.value || !bpmnViewer.value) {
- () => props.processInstanceData,
- (newProcessInstanceData) => {
- processInstance.value = newProcessInstanceData
+ const {
+ unfinishedTaskActivityIds,
+ finishedTaskActivityIds,
+ finishedSequenceFlowActivityIds,
+ rejectedTaskActivityIds
+ } = view
+ const canvas = bpmnViewer.value.get('canvas')
+ const elementRegistry = bpmnViewer.value.get('elementRegistry')
+ // 已完成节点
+ if (Array.isArray(finishedSequenceFlowActivityIds)) {
+ finishedSequenceFlowActivityIds.forEach((item: any) => {
+ if (item != null) {
+ canvas.addMarker(item, 'success')
+ const element = elementRegistry.get(item)
+ const conditionExpression = element.businessObject.conditionExpression
+ if (conditionExpression) {
+ canvas.addMarker(item, 'condition-expression')
- () => props.taskData,
- (newTaskListData) => {
- taskList.value = newTaskListData
+ if (Array.isArray(finishedTaskActivityIds)) {
+ finishedTaskActivityIds.forEach((item: any) => canvas.addMarker(item, 'success'))
-</script>
-<style lang="scss">
-/** 处理中 */
-.highlight-todo.djs-connection > .djs-visual > path {
- stroke: #1890ff !important;
- stroke-dasharray: 4px !important;
- fill-opacity: 0.2 !important;
-.highlight-todo.djs-shape .djs-visual > :nth-child(1) {
- fill: #1890ff !important;
-:deep(.highlight-todo.djs-connection > .djs-visual > path) {
- marker-end: url('#sequenceflow-end-_E7DFDF-_E7DFDF-803g1kf6zwzmcig1y2ulm5egr');
-:deep(.highlight-todo.djs-shape .djs-visual > :nth-child(1)) {
-/** 通过 */
-.highlight.djs-shape .djs-visual > :nth-child(1) {
- fill: green !important;
- stroke: green !important;
-.highlight.djs-shape .djs-visual > :nth-child(2) {
-.highlight.djs-shape .djs-visual > path {
-.highlight.djs-connection > .djs-visual > path {
-.highlight:not(.djs-connection) .djs-visual > :nth-child(1) {
- fill: green !important; /* color elements as green */
-:deep(.highlight.djs-shape .djs-visual > :nth-child(1)) {
-:deep(.highlight.djs-shape .djs-visual > :nth-child(2)) {
-:deep(.highlight.djs-shape .djs-visual > path) {
-:deep(.highlight.djs-connection > .djs-visual > path) {
-.djs-element.highlight > .djs-visual > path {
-/** 不通过 */
-.highlight-reject.djs-shape .djs-visual > :nth-child(1) {
- fill: red !important;
- stroke: red !important;
-.highlight-reject.djs-shape .djs-visual > :nth-child(2) {
-.highlight-reject.djs-shape .djs-visual > path {
-.highlight-reject.djs-connection > .djs-visual > path {
- marker-end: url(#sequenceflow-end-white-success) !important;
-.highlight-reject:not(.djs-connection) .djs-visual > :nth-child(1) {
- fill: red !important; /* color elements as green */
-:deep(.highlight-reject.djs-shape .djs-visual > :nth-child(1)) {
-:deep(.highlight-reject.djs-shape .djs-visual > :nth-child(2)) {
-:deep(.highlight-reject.djs-shape .djs-visual > path) {
-:deep(.highlight-reject.djs-connection > .djs-visual > path) {
-/** 已取消 */
-.highlight-cancel.djs-shape .djs-visual > :nth-child(1) {
- fill: grey !important;
- stroke: grey !important;
-.highlight-cancel.djs-shape .djs-visual > :nth-child(2) {
-.highlight-cancel.djs-shape .djs-visual > path {
-.highlight-cancel.djs-connection > .djs-visual > path {
-.highlight-cancel:not(.djs-connection) .djs-visual > :nth-child(1) {
- fill: grey !important; /* color elements as green */
-:deep(.highlight-cancel.djs-shape .djs-visual > :nth-child(1)) {
-:deep(.highlight-cancel.djs-shape .djs-visual > :nth-child(2)) {
-:deep(.highlight-cancel.djs-shape .djs-visual > path) {
-:deep(.highlight-cancel.djs-connection > .djs-visual > path) {
-/** 回退 */
-.highlight-return.djs-shape .djs-visual > :nth-child(1) {
- fill: #e6a23c !important;
- stroke: #e6a23c !important;
-.highlight-return.djs-shape .djs-visual > :nth-child(2) {
-.highlight-return.djs-shape .djs-visual > path {
-.highlight-return.djs-connection > .djs-visual > path {
+ // 未完成节点
+ if (Array.isArray(unfinishedTaskActivityIds)) {
+ unfinishedTaskActivityIds.forEach((item: any) => canvas.addMarker(item, 'primary'))
-.highlight-return:not(.djs-connection) .djs-visual > :nth-child(1) {
- fill: #e6a23c !important; /* color elements as green */
+ // 被拒绝节点
+ if (Array.isArray(rejectedTaskActivityIds)) {
+ rejectedTaskActivityIds.forEach((item: any) => {
+ canvas.addMarker(item, 'danger')
-:deep(.highlight-return.djs-shape .djs-visual > :nth-child(1)) {
+ // 特殊:处理 end 节点的高亮。因为 end 在拒绝、取消时,被后端计算成了 finishedTaskActivityIds 里
+ [BpmProcessInstanceStatus.CANCEL, BpmProcessInstanceStatus.REJECT].includes(
+ processInstance.value.status
+ const endNodes = elementRegistry.filter((element: any) => element.type === 'bpmn:EndEvent')
+ endNodes.forEach((item: any) => {
+ canvas.removeMarker(item.id, 'success')
+ if (processInstance.value.status === BpmProcessInstanceStatus.CANCEL) {
+ canvas.addMarker(item.id, 'cancel')
+ canvas.addMarker(item.id, 'danger')
-:deep(.highlight-return.djs-shape .djs-visual > :nth-child(2)) {
+ () => props.xml,
+ (newXml) => {
+ importXML(newXml)
+ { immediate: true }
-:deep(.highlight-return.djs-shape .djs-visual > path) {
+ () => props.view,
+ (newView) => {
+ setProcessStatus(newView)
-:deep(.highlight-return.djs-connection > .djs-visual > path) {
+/** mounted:初始化 */
+onMounted(() => {
+ importXML(props.xml)
-.element-overlays {
- padding: 8px;
- color: #fafafa;
- background: rgb(0 0 0 / 60%);
-</style>
+/** unmount:销毁 */
+onBeforeUnmount(() => {
@@ -1211,6 +1211,76 @@
"isAttr": true
+ "name": "AssignStartUserHandlerType",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:StartEvent", "bpmn:UserTask"]
+ "properties": [
+ "name": "value",
+ "type": "Integer",
+ "isBody": true
+ "name": "RejectHandlerType",
+ "allowedIn": ["bpmn:UserTask"]
+ "name": "RejectReturnTaskId",
+ "type": "String",
+ "name": "AssignEmptyHandlerType",
+ "name": "AssignEmptyUserIds",
],
"emumerations": []
@@ -1,5 +1,5 @@
- <div class="process-panel__container" :style="{ width: `${width}px` }">
+ <div class="process-panel__container" :style="{ width: `${width}px`,maxHeight: '700px' }">
<el-collapse v-model="activeTab">
<el-collapse-item name="base">
<!-- class="panel-tab__title" -->
@@ -54,6 +54,10 @@
<template #title><Icon icon="ep:promotion" />其他</template>
<element-other-config :id="elementId" />
</el-collapse-item>
+ <el-collapse-item name="customConfig" v-if="elementType.indexOf('Task') !== -1" key="customConfig">
+ <template #title><Icon icon="ep:circle-plus-filled" />自定义配置</template>
+ <element-custom-config :id="elementId" :type="elementType" />
+ </el-collapse-item>
</el-collapse>
@@ -0,0 +1,283 @@
+<!-- UserTask 自定义配置:
+ 1. 审批人与提交人为同一人时
+ 2. 审批人拒绝时
+ 3. 审批人为空时
+-->
+ <div class="panel-tab__content">
+ <el-divider content-position="left">审批人拒绝时</el-divider>
+ <el-form-item prop="rejectHandlerType">
+ <el-radio-group
+ v-model="rejectHandlerType"
+ :disabled="returnTaskList.length === 0"
+ @change="updateRejectHandlerType"
+ <div class="flex-col">
+ <div v-for="(item, index) in REJECT_HANDLER_TYPES" :key="index">
+ <el-radio :key="item.value" :value="item.value" :label="item.label" />
+ </el-radio-group>
+ v-if="rejectHandlerType == RejectHandlerType.RETURN_USER_TASK"
+ label="驳回节点"
+ prop="returnNodeId"
+ <el-select v-model="returnNodeId" clearable style="width: 100%" @change="updateReturnNodeId">
+ v-for="item in returnTaskList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ <el-divider content-position="left">审批人为空时</el-divider>
+ <el-form-item prop="assignEmptyHandlerType">
+ <el-radio-group v-model="assignEmptyHandlerType" @change="updateAssignEmptyHandlerType">
+ <div v-for="(item, index) in ASSIGN_EMPTY_HANDLER_TYPES" :key="index">
+ v-if="assignEmptyHandlerType == AssignEmptyHandlerType.ASSIGN_USER"
+ label="指定用户"
+ prop="assignEmptyHandlerUserIds"
+ <el-select
+ v-model="assignEmptyUserIds"
+ clearable
+ multiple
+ style="width: 100%"
+ @change="updateAssignEmptyUserIds"
+ v-for="item in userOptions"
+ :label="item.nickname"
+ <el-divider content-position="left">审批人与提交人为同一人时</el-divider>
+ <el-radio-group v-model="assignStartUserHandlerType" @change="updateAssignStartUserHandlerType">
+ <div v-for="(item, index) in ASSIGN_START_USER_HANDLER_TYPES" :key="index">
+import {
+ ASSIGN_START_USER_HANDLER_TYPES,
+ RejectHandlerType,
+ REJECT_HANDLER_TYPES,
+ ASSIGN_EMPTY_HANDLER_TYPES,
+ AssignEmptyHandlerType
+} from '@/components/SimpleProcessDesignerV2/src/consts'
+defineOptions({ name: 'ElementCustomConfig' })
+ id: String,
+ type: String
+const prefix = inject('prefix')
+// 审批人与提交人为同一人时
+const assignStartUserHandlerTypeEl = ref()
+const assignStartUserHandlerType = ref()
+// 审批人拒绝时
+const rejectHandlerTypeEl = ref()
+const rejectHandlerType = ref()
+const returnNodeIdEl = ref()
+const returnNodeId = ref()
+const returnTaskList = ref([])
+// 审批人为空时
+const assignEmptyHandlerTypeEl = ref()
+const assignEmptyHandlerType = ref()
+const assignEmptyUserIdsEl = ref()
+const assignEmptyUserIds = ref()
+const elExtensionElements = ref()
+const otherExtensions = ref()
+const bpmnElement = ref()
+const bpmnInstances = () => (window as any)?.bpmnInstances
+const resetCustomConfigList = () => {
+ bpmnElement.value = bpmnInstances().bpmnElement
+ // 获取可回退的列表
+ returnTaskList.value = findAllPredecessorsExcludingStart(
+ bpmnElement.value.id,
+ bpmnInstances().modeler
+ // 获取元素扩展属性 或者 创建扩展属性
+ elExtensionElements.value =
+ bpmnElement.value.businessObject?.extensionElements ??
+ bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] })
+ // 审批人与提交人为同一人时
+ assignStartUserHandlerTypeEl.value =
+ elExtensionElements.value.values?.filter(
+ (ex) => ex.$type === `${prefix}:AssignStartUserHandlerType`
+ )?.[0] || bpmnInstances().moddle.create(`${prefix}:AssignStartUserHandlerType`, { value: 1 })
+ assignStartUserHandlerType.value = assignStartUserHandlerTypeEl.value.value
+ // 审批人拒绝时
+ rejectHandlerTypeEl.value =
+ (ex) => ex.$type === `${prefix}:RejectHandlerType`
+ )?.[0] || bpmnInstances().moddle.create(`${prefix}:RejectHandlerType`, { value: 1 })
+ rejectHandlerType.value = rejectHandlerTypeEl.value.value
+ returnNodeIdEl.value =
+ (ex) => ex.$type === `${prefix}:RejectReturnTaskId`
+ )?.[0] || bpmnInstances().moddle.create(`${prefix}:RejectReturnTaskId`, { value: '' })
+ returnNodeId.value = returnNodeIdEl.value.value
+ // 审批人为空时
+ assignEmptyHandlerTypeEl.value =
+ (ex) => ex.$type === `${prefix}:AssignEmptyHandlerType`
+ )?.[0] || bpmnInstances().moddle.create(`${prefix}:AssignEmptyHandlerType`, { value: 1 })
+ assignEmptyHandlerType.value = assignEmptyHandlerTypeEl.value.value
+ assignEmptyUserIdsEl.value =
+ (ex) => ex.$type === `${prefix}:AssignEmptyUserIds`
+ )?.[0] || bpmnInstances().moddle.create(`${prefix}:AssignEmptyUserIds`, { value: '' })
+ assignEmptyUserIds.value = assignEmptyUserIdsEl.value.value.split(',').map((item) => {
+ // 如果数字超出了最大安全整数范围,则将其作为字符串处理
+ let num = Number(item)
+ return num > Number.MAX_SAFE_INTEGER || num < -Number.MAX_SAFE_INTEGER ? item : num
+ // 保留剩余扩展元素,便于后面更新该元素对应属性
+ otherExtensions.value =
+ (ex) =>
+ ex.$type !== `${prefix}:AssignStartUserHandlerType` &&
+ ex.$type !== `${prefix}:RejectHandlerType` &&
+ ex.$type !== `${prefix}:RejectReturnTaskId` &&
+ ex.$type !== `${prefix}:AssignEmptyHandlerType` &&
+ ex.$type !== `${prefix}:AssignEmptyUserIds`
+ ) ?? []
+ // 更新元素扩展属性,避免后续报错
+ updateElementExtensions()
+const updateAssignStartUserHandlerType = () => {
+ assignStartUserHandlerTypeEl.value.value = assignStartUserHandlerType.value
+const updateRejectHandlerType = () => {
+ rejectHandlerTypeEl.value.value = rejectHandlerType.value
+ returnNodeId.value = returnTaskList.value[0].id
+ returnNodeIdEl.value.value = returnNodeId.value
+const updateReturnNodeId = () => {
+const updateAssignEmptyHandlerType = () => {
+ assignEmptyHandlerTypeEl.value.value = assignEmptyHandlerType.value
+const updateAssignEmptyUserIds = () => {
+ assignEmptyUserIdsEl.value.value = assignEmptyUserIds.value.toString()
+const updateElementExtensions = () => {
+ const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
+ values: [
+ ...otherExtensions.value,
+ assignStartUserHandlerTypeEl.value,
+ rejectHandlerTypeEl.value,
+ returnNodeIdEl.value,
+ assignEmptyHandlerTypeEl.value,
+ assignEmptyUserIdsEl.value
+ bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+ extensionElements: extensions
+ () => props.id,
+ (val) => {
+ val &&
+ val.length &&
+ nextTick(() => {
+ resetCustomConfigList()
+function findAllPredecessorsExcludingStart(elementId, modeler) {
+ const elementRegistry = modeler.get('elementRegistry')
+ const allConnections = elementRegistry.filter((element) => element.type === 'bpmn:SequenceFlow')
+ const predecessors = new Set() // 使用 Set 来避免重复节点
+ // 检查是否是开始事件节点
+ function isStartEvent(element) {
+ return element.type === 'bpmn:StartEvent'
+ function findPredecessorsRecursively(element) {
+ // 获取与当前节点相连的所有连接
+ const incomingConnections = allConnections.filter((connection) => connection.target === element)
+ incomingConnections.forEach((connection) => {
+ const source = connection.source // 获取前置节点
+ // 只添加不是开始事件的前置节点
+ if (!isStartEvent(source)) {
+ predecessors.add(source.businessObject)
+ // 递归查找前置节点
+ findPredecessorsRecursively(source)
+ const targetElement = elementRegistry.get(elementId)
+ if (targetElement) {
+ findPredecessorsRecursively(targetElement)
+ return Array.from(predecessors) // 返回前置节点数组
+const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
+onMounted(async () => {
+ // 获得用户列表
+ userOptions.value = await UserApi.getSimpleUserList()
@@ -268,9 +268,9 @@ const bpmnInstances = () => (window as any)?.bpmnInstances
const resetFormList = () => {
bpmnELement.value = bpmnInstances().bpmnElement
formKey.value = bpmnELement.value.businessObject.formKey
- if (formKey.value?.length > 0) {
- formKey.value = parseInt(formKey.value)
+ // if (formKey.value?.length > 0) {
+ // formKey.value = parseInt(formKey.value)
// 获取元素扩展属性 或者 创建扩展属性
elExtensionElements.value =
bpmnELement.value.businessObject.get('extensionElements') ||
@@ -80,7 +80,7 @@ const resetAttributesList = () => {
otherExtensionList.value = [] // 其他扩展配置
bpmnElementProperties.value =
// bpmnElement.value.businessObject?.extensionElements?.filter((ex) => {
- bpmnElement.value.businessObject?.extensionElements?.values.filter((ex) => {
+ bpmnElement.value.businessObject?.extensionElements?.values?.filter((ex) => {
if (ex.$type !== `${prefix}:Properties`) {
otherExtensionList.value.push(ex)
@@ -1,2 +1,117 @@
@use './process-designer.scss';
@use './process-panel.scss';
+$success-color: #4eb819;
+$primary-color: #409EFF;
+$danger-color: #F56C6C;
+$cancel-color: #909399;
+.process-viewer {
+ border: 1px solid #EFEFEF;
+ background: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PHBhdHRlcm4gaWQ9ImEiIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHBhdGggZD0iTTAgMTBoNDBNMTAgMHY0ME0wIDIwaDQwTTIwIDB2NDBNMCAzMGg0ME0zMCAwdjQwIiBmaWxsPSJub25lIiBzdHJva2U9IiNlMGUwZTAiIG9wYWNpdHk9Ii4yIi8+PHBhdGggZD0iTTQwIDBIMHY0MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZTBlMGUwIi8+PC9wYXR0ZXJuPjwvZGVmcz48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSJ1cmwoI2EpIi8+PC9zdmc+') repeat!important;
+ .success-arrow {
+ fill: $success-color;
+ stroke: $success-color;
+ .success-conditional {
+ fill: white;
+ .success.djs-connection {
+ .djs-visual path {
+ stroke: $success-color!important;
+ //marker-end: url(#sequenceflow-end-white-success)!important;
+ .success.djs-connection.condition-expression {
+ //marker-start: url(#conditional-flow-marker-white-success)!important;
+ .success.djs-shape {
+ .djs-visual rect {
+ fill: $success-color!important;
+ fill-opacity: 0.15!important;
+ .djs-visual polygon {
+ .djs-visual path:nth-child(2) {
+ .djs-visual circle {
+ .primary.djs-shape {
+ stroke: $primary-color!important;
+ fill: $primary-color!important;
+ .danger.djs-shape {
+ stroke: $danger-color!important;
+ fill: $danger-color!important;
+ .cancel.djs-shape {
+ stroke: $cancel-color!important;
+ fill: $cancel-color!important;
+.process-viewer .djs-tooltip-container, .process-viewer .djs-overlay-container, .process-viewer .djs-palette {
@@ -267,9 +267,9 @@ const remainingRouter: AppRouteRecordRaw[] = [
- path: 'manager/simple/workflow/model/edit',
- component: () => import('@/views/bpm/simpleWorkflow/index.vue'),
- name: 'SimpleWorkflowDesignEditor',
+ path: 'manager/simple/model',
+ component: () => import('@/views/bpm/simple/SimpleModelDesign.vue'),
+ name: 'SimpleModelDesign',
meta: {
noCache: true,
hidden: true,
@@ -292,7 +292,6 @@ const remainingRouter: AppRouteRecordRaw[] = [
path: 'process-instance/detail',
- // component: () => import('@/views/bpm/processInstance/detail/index_new.vue'), // TODO 芋艿:新审批界面,已适配 simple 模式,未来会适配 bpmn 模式
component: () => import('@/views/bpm/processInstance/detail/index.vue'),
name: 'BpmProcessInstanceDetail',
@@ -449,3 +449,11 @@ export const BpmModelFormType = {
NORMAL: 10, // 流程表单
CUSTOM: 20 // 业务表单
+export const BpmProcessInstanceStatus = {
+ NOT_START: -1, // 未开始
+ RUNNING: 1, // 审批中
+ APPROVE: 2, // 审批通过
+ REJECT: 3, // 审批不通过
+ CANCEL: 4 // 已取消
@@ -44,6 +44,7 @@ export const setConfAndFields2 = (
value?: object
if (isRef(detailPreview)) {
detailPreview = detailPreview.value
@@ -42,6 +42,7 @@
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import { CategoryApi, CategoryVO } from '@/api/bpm/category'
+import { CommonStatusEnum } from '@/utils/constants'
/** BPM 流程分类 表单 */
defineOptions({ name: 'CategoryForm' })
@@ -57,7 +58,7 @@ const formData = ref({
id: undefined,
name: undefined,
code: undefined,
- status: undefined,
+ status: CommonStatusEnum.ENABLE,
sort: undefined
const formRules = reactive({
@@ -116,7 +117,7 @@ const resetForm = () => {
formRef.value?.resetFields()
@@ -70,13 +70,7 @@
<!-- 弹窗:流程模型图的预览 -->
<Dialog title="流程图" v-model="bpmnDetailVisible" width="800">
- <MyProcessViewer
- key="designer"
- v-model="bpmnXml"
- :value="bpmnXml as any"
- v-bind="bpmnControlForm"
- :prefix="bpmnControlForm.prefix"
+ <MyProcessViewer style="height: 700px" key="designer" :xml="bpmnXml" />
</Dialog>
@@ -118,7 +112,7 @@ const formDetailPreview = ref({
rule: [],
option: {}
-const handleFormDetail = async (row) => {
+const handleFormDetail = async (row: any) => {
if (row.formType == 10) {
// 设置表单
setConfAndFields2(formDetailPreview, row.formConf, row.formFields)
@@ -133,13 +127,13 @@ const handleFormDetail = async (row) => {
/** 流程图的详情按钮操作 */
const bpmnDetailVisible = ref(false)
-const bpmnXml = ref(null)
-const bpmnControlForm = ref({
- prefix: 'flowable'
-const handleBpmnDetail = async (row) => {
- bpmnXml.value = (await DefinitionApi.getProcessDefinition(row.id))?.bpmnXml
+const bpmnXml = ref('')
+const handleBpmnDetail = async (row: any) => {
+ // 设置可见
+ bpmnXml.value = ''
bpmnDetailVisible.value = true
+ // 加载 BPMN XML
+ bpmnXml.value = (await DefinitionApi.getProcessDefinition(row.id))?.bpmnXml
/** 初始化 **/
@@ -64,7 +64,11 @@ const designerConfig = ref({
switchType: [], // 是否可以切换组件类型,或者可以相互切换的字段
autoActive: true, // 是否自动选中拖入的组件
useTemplate: false, // 是否生成vue2语法的模板组件
- formOptions: {}, // 定义表单配置默认值
+ formOptions: {
+ form: {
+ labelWidth: '100px' // 设置默认的 label 宽度为 100px
+ }, // 定义表单配置默认值
fieldReadonly: false, // 配置field是否可以编辑
hiddenDragMenu: false, // 隐藏拖拽操作按钮
hiddenDragBtn: false, // 隐藏拖拽按钮
@@ -143,8 +143,9 @@ const openForm = (id?: number) => {
const toRouter: { name: string; query?: { id: number } } = {
name: 'BpmFormEditor'
+ console.log(typeof id)
// 表单新建的时候id传的是event需要排除
- if (typeof id === 'number') {
+ if (typeof id === 'number' || typeof id === 'string') {
toRouter.query = {
id
@@ -0,0 +1,532 @@
+ <div class="flex items-center h-50px">
+ <!-- 头部:分类名 -->
+ <div class="flex items-center">
+ <el-tooltip content="拖动排序" v-if="isCategorySorting">
+ :size="22"
+ icon="ic:round-drag-indicator"
+ class="ml-10px category-drag-icon cursor-move text-#8a909c"
+ </el-tooltip>
+ <h3 class="ml-20px mr-8px text-18px">{{ categoryInfo.name }}</h3>
+ <div class="color-gray-600 text-16px"> ({{ categoryInfo.modelList?.length || 0 }}) </div>
+ <!-- 头部:操作 -->
+ <div class="flex-1 flex" v-if="!isCategorySorting">
+ v-if="categoryInfo.modelList.length > 0"
+ class="ml-20px flex items-center"
+ 'transition-transform duration-300 cursor-pointer',
+ isExpand ? 'rotate-180' : 'rotate-0'
+ @click="isExpand = !isExpand"
+ <Icon icon="ep:arrow-down-bold" color="#999" />
+ <div class="ml-auto flex items-center" :class="isModelSorting ? 'mr-15px' : 'mr-45px'">
+ <template v-if="!isModelSorting">
+ link
+ type="info"
+ class="mr-20px"
+ @click.stop="handleModelSort"
+ <Icon icon="fa:sort-amount-desc" class="mr-5px" />
+ 排序
+ <el-button v-else link type="info" class="mr-20px" @click.stop="openModelForm('create')">
+ <Icon icon="fa:plus" class="mr-5px" />
+ 新建
+ <el-dropdown
+ @command="(command) => handleCategoryCommand(command, categoryInfo)"
+ placement="bottom"
+ <el-button link type="info">
+ <Icon icon="ep:setting" class="mr-5px" />
+ 分类
+ <template #dropdown>
+ <el-dropdown-menu>
+ <el-dropdown-item command="handleRename"> 重命名 </el-dropdown-item>
+ <el-dropdown-item command="handleDeleteCategory"> 删除该类 </el-dropdown-item>
+ </el-dropdown-menu>
+ </el-dropdown>
+ <template v-else>
+ <el-button @click.stop="handleModelSortCancel"> 取 消 </el-button>
+ <el-button type="primary" @click.stop="handleModelSortSubmit"> 保存排序 </el-button>
+ <!-- 模型列表 -->
+ <el-collapse-transition>
+ <div v-show="isExpand">
+ :class="categoryInfo.name"
+ ref="tableRef"
+ :header-cell-style="{ backgroundColor: isDark ? '' : '#edeff0', paddingLeft: '10px' }"
+ :cell-style="{ paddingLeft: '10px' }"
+ :row-style="{ height: '68px' }"
+ :data="modelList"
+ row-key="id"
+ <el-table-column label="流程名" prop="name" min-width="150">
+ <el-tooltip content="拖动排序" v-if="isModelSorting">
+ class="drag-icon cursor-move text-#8a909c mr-10px"
+ <el-image :src="scope.row.icon" class="h-38px w-38px mr-10px rounded" />
+ {{ scope.row.name }}
+ <el-table-column label="可见范围" prop="startUserIds" min-width="100">
+ <el-text v-if="!scope.row.startUsers || scope.row.startUsers.length === 0">
+ 全部可见
+ </el-text>
+ <el-text v-else-if="scope.row.startUsers.length == 1">
+ {{ scope.row.startUsers[0].nickname }}
+ <el-text v-else>
+ <el-tooltip
+ class="box-item"
+ effect="dark"
+ placement="top"
+ :content="scope.row.startUsers.map((user: any) => user.nickname).join('、')"
+ {{ scope.row.startUsers[0].nickname }}等 {{ scope.row.startUsers.length }} 人可见
+ <el-table-column label="表单信息" prop="formType" min-width="200">
+ v-if="scope.row.formType === BpmModelFormType.NORMAL"
+ @click="handleFormDetail(scope.row)"
+ <span>{{ scope.row.formName }}</span>
+ v-else-if="scope.row.formType === BpmModelFormType.CUSTOM"
+ <span>{{ scope.row.formCustomCreatePath }}</span>
+ <label v-else>暂无表单</label>
+ <el-table-column label="最后发布" prop="deploymentTime" min-width="250">
+ <span v-if="scope.row.processDefinition" class="w-150px">
+ {{ formatDate(scope.row.processDefinition.deploymentTime) }}
+ </span>
+ <el-tag v-if="scope.row.processDefinition">
+ v{{ scope.row.processDefinition.version }}
+ </el-tag>
+ <el-tag v-else type="warning">未部署</el-tag>
+ <el-tag
+ v-if="scope.row.processDefinition?.suspensionState === 2"
+ type="warning"
+ class="ml-10px"
+ 已停用
+ <el-table-column label="操作" width="200" fixed="right">
+ @click="openModelForm('update', scope.row.id)"
+ v-hasPermi="['bpm:model:update']"
+ :disabled="!isManagerUser(scope.row)"
+ 修改
+ class="!ml-5px"
+ @click="handleDesign(scope.row)"
+ 设计
+ @click="handleDeploy(scope.row)"
+ v-hasPermi="['bpm:model:deploy']"
+ 发布
+ class="!align-middle ml-5px"
+ @command="(command) => handleModelCommand(command, scope.row)"
+ v-hasPermi="['bpm:process-definition:query', 'bpm:model:update', 'bpm:model:delete']"
+ <el-button type="primary" link>更多</el-button>
+ <el-dropdown-item
+ command="handleDefinitionList"
+ v-if="checkPermi(['bpm:process-definition:query'])"
+ 历史
+ </el-dropdown-item>
+ command="handleChangeState"
+ v-if="checkPermi(['bpm:model:update']) && scope.row.processDefinition"
+ {{ scope.row.processDefinition.suspensionState === 1 ? '停用' : '启用' }}
+ type="danger"
+ command="handleDelete"
+ v-if="checkPermi(['bpm:model:delete'])"
+ 删除
+ </el-collapse-transition>
+ <!-- 弹窗:重命名分类 -->
+ <Dialog :fullscreen="false" class="rename-dialog" v-model="renameCategoryVisible" width="400">
+ <template #title>
+ <div class="pl-10px font-bold text-18px"> 重命名分类 </div>
+ <div class="px-30px">
+ <el-input v-model="renameCategoryForm.name" />
+ <div class="pr-25px pb-25px">
+ <el-button @click="renameCategoryVisible = false">取 消</el-button>
+ <el-button type="primary" @click="handleRenameConfirm">确 定</el-button>
+ <!-- 表单弹窗:添加流程模型 -->
+ <ModelForm :categoryId="categoryInfo.code" ref="modelFormRef" @success="emit('success')" />
+import ModelForm from './ModelForm.vue'
+import { CategoryApi, CategoryVO } from '@/api/bpm/category'
+import Sortable from 'sortablejs'
+import { propTypes } from '@/utils/propTypes'
+import { formatDate } from '@/utils/formatTime'
+import * as ModelApi from '@/api/bpm/model'
+import * as FormApi from '@/api/bpm/form'
+import { setConfAndFields2 } from '@/utils/formCreate'
+import { BpmModelFormType, BpmModelType } from '@/utils/constants'
+import { checkPermi } from '@/utils/permission'
+import { useUserStoreWithOut } from '@/store/modules/user'
+import { useAppStore } from '@/store/modules/app'
+import { cloneDeep } from 'lodash-es'
+defineOptions({ name: 'BpmModel' })
+ categoryInfo: propTypes.object.def([]), // 分类后的数据
+ isCategorySorting: propTypes.bool.def(false) // 是否分类在排序
+const emit = defineEmits(['success'])
+const { t } = useI18n() // 国际化
+const { push } = useRouter() // 路由
+const userStore = useUserStoreWithOut() // 用户信息缓存
+const isDark = computed(() => useAppStore().getIsDark) // 是否黑暗模式
+const isModelSorting = ref(false) // 是否正处于排序状态
+const originalData: any = ref([]) // 原始数据
+const modelList: any = ref([]) // 模型列表
+const isExpand = ref(false) // 是否处于展开状态
+/** '更多'操作按钮 */
+const handleModelCommand = (command: string, row: any) => {
+ switch (command) {
+ case 'handleDefinitionList':
+ handleDefinitionList(row)
+ case 'handleDelete':
+ handleDelete(row)
+ case 'handleChangeState':
+ handleChangeState(row)
+ default:
+/** '分类'操作按钮 */
+const handleCategoryCommand = async (command: string, row: any) => {
+ case 'handleRename':
+ renameCategoryForm.value = await CategoryApi.getCategory(row.id)
+ renameCategoryVisible.value = true
+ case 'handleDeleteCategory':
+ await handleDeleteCategory()
+/** 删除按钮操作 */
+const handleDelete = async (row: any) => {
+ // 删除的二次确认
+ await message.delConfirm()
+ // 发起删除
+ await ModelApi.deleteModel(row.id)
+ message.success(t('common.delSuccess'))
+ // 刷新列表
+ emit('success')
+ } catch {}
+/** 更新状态操作 */
+const handleChangeState = async (row: any) => {
+ const state = row.processDefinition.suspensionState
+ const newState = state === 1 ? 2 : 1
+ // 修改状态的二次确认
+ const id = row.id
+ debugger
+ const statusState = state === 1 ? '停用' : '启用'
+ const content = '是否确认' + statusState + '流程名字为"' + row.name + '"的数据项?'
+ await message.confirm(content)
+ // 发起修改状态
+ await ModelApi.updateModelState(id, newState)
+ message.success(statusState + '成功')
+/** 设计流程 */
+const handleDesign = (row: any) => {
+ if (row.type == BpmModelType.BPMN) {
+ push({
+ name: 'BpmModelEditor',
+ query: {
+ modelId: row.id
+/** 发布流程 */
+const handleDeploy = async (row: any) => {
+ await message.confirm('是否部署该流程!!')
+ // 发起部署
+ await ModelApi.deployModel(row.id)
+ message.success(t('部署成功'))
+/** 跳转到指定流程定义列表 */
+const handleDefinitionList = (row: any) => {
+ name: 'BpmProcessDefinition',
+ key: row.key
+/** 流程表单的详情按钮操作 */
+const formDetailVisible = ref(false)
+const formDetailPreview = ref({
+ rule: [],
+ option: {}
+ if (row.formType == 10) {
+ // 设置表单
+ const data = await FormApi.getForm(row.formId)
+ setConfAndFields2(formDetailPreview, data.conf, data.fields)
+ // 弹窗打开
+ formDetailVisible.value = true
+ await push({
+ path: row.formCustomCreatePath
+/** 判断是否可以操作 */
+const isManagerUser = (row: any) => {
+ const userId = userStore.getUser.id
+ return row.managerUserIds && row.managerUserIds.includes(userId)
+/** 处理模型的排序 **/
+const handleModelSort = () => {
+ // 保存初始数据
+ originalData.value = cloneDeep(props.categoryInfo.modelList)
+ isModelSorting.value = true
+ initSort()
+/** 处理模型的排序提交 */
+const handleModelSortSubmit = async () => {
+ // 保存排序
+ const ids = modelList.value.map((item: any) => item.id)
+ await ModelApi.updateModelSortBatch(ids)
+ isModelSorting.value = false
+ message.success('排序模型成功')
+/** 处理模型的排序取消 */
+const handleModelSortCancel = () => {
+ // 恢复初始数据
+ modelList.value = cloneDeep(originalData.value)
+/** 创建拖拽实例 */
+const tableRef = ref()
+const initSort = () => {
+ const table = document.querySelector(`.${props.categoryInfo.name} .el-table__body-wrapper tbody`)
+ Sortable.create(table, {
+ group: 'shared',
+ animation: 150,
+ draggable: '.el-table__row',
+ handle: '.drag-icon',
+ // 结束拖动事件
+ onEnd: ({ newDraggableIndex, oldDraggableIndex }) => {
+ if (oldDraggableIndex !== newDraggableIndex) {
+ modelList.value.splice(
+ newDraggableIndex,
+ 0,
+ modelList.value.splice(oldDraggableIndex, 1)[0]
+/** 更新 modelList 模型列表 */
+const updateModeList = () => {
+ modelList.value = cloneDeep(props.categoryInfo.modelList)
+ if (props.categoryInfo.modelList.length > 0) {
+ isExpand.value = true
+/** 重命名弹窗确定 */
+const renameCategoryVisible = ref(false)
+const renameCategoryForm = ref({
+ name: ''
+const handleRenameConfirm = async () => {
+ if (renameCategoryForm.value?.name.length === 0) {
+ return message.warning('请输入名称')
+ // 发起修改
+ await CategoryApi.updateCategory(renameCategoryForm.value as CategoryVO)
+ message.success('重命名成功')
+ renameCategoryVisible.value = false
+/** 删除分类 */
+const handleDeleteCategory = async () => {
+ return message.warning('该分类下仍有流程定义,不允许删除')
+ await message.confirm('确认删除分类吗?')
+ await CategoryApi.deleteCategory(props.categoryInfo.id)
+/** 添加流程模型弹窗 */
+const modelFormRef = ref()
+const openModelForm = (type: string, id?: number) => {
+ modelFormRef.value.open(type, id)
+watch(() => props.categoryInfo.modelList, updateModeList, { immediate: true })
+ () => props.isCategorySorting,
+ if (val) isExpand.value = false
+<style lang="scss">
+.rename-dialog.el-dialog {
+ padding: 0 !important;
+ .el-dialog__header {
+ border-bottom: none;
+ .el-dialog__footer {
+ border-top: none !important;
+ .el-table__cell {
+ border-bottom: none !important;
@@ -155,6 +155,7 @@
import { DICT_TYPE, getBoolDictOptions, getIntDictOptions } from '@/utils/dict'
import { ElMessageBox } from 'element-plus'
import * as ModelApi from '@/api/bpm/model'
@@ -170,7 +171,9 @@ defineOptions({ name: 'ModelForm' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const userStore = useUserStoreWithOut() // 用户信息缓存
+ categoryId: propTypes.number
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
@@ -232,6 +235,9 @@ const open = async (type: string, id?: string) => {
categoryList.value = await CategoryApi.getCategorySimpleList()
// 查询用户列表
userList.value = await UserApi.getSimpleUserList()
+ if (props.categoryId) {
+ formData.value.category = props.categoryId
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
@@ -1,216 +1,94 @@
- <doc-alert title="流程设计器(BPMN)" url="https://doc.iocoder.cn/bpm/model-designer-dingding/" />
- <doc-alert
- title="流程设计器(钉钉、飞书)"
- url="https://doc.iocoder.cn/bpm/model-designer-bpmn/"
- <doc-alert title="选择审批人、发起人自选" url="https://doc.iocoder.cn/bpm/assignee/" />
- <doc-alert title="会签、或签、依次审批" url="https://doc.iocoder.cn/bpm/multi-instance/" />
<ContentWrap>
- <!-- 搜索工作栏 -->
- <el-form
- class="-mb-15px"
- :model="queryParams"
- ref="queryFormRef"
- :inline="true"
- label-width="68px"
- <el-form-item label="流程标识" prop="key">
- <el-input
- v-model="queryParams.key"
- placeholder="请输入流程标识"
- @keyup.enter="handleQuery"
- class="!w-240px"
- <el-form-item label="流程名称" prop="name">
- v-model="queryParams.name"
- placeholder="请输入流程名称"
- <el-form-item label="流程分类" prop="category">
- v-model="queryParams.category"
- placeholder="请选择流程分类"
- v-for="category in categoryList"
- :key="category.code"
- :label="category.name"
- :value="category.code"
- <el-form-item>
- <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
- <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
- <el-button
- type="primary"
- plain
- @click="openForm('create')"
- v-hasPermi="['bpm:model:create']"
- <Icon icon="ep:plus" class="mr-5px" /> 新建
- </el-button>
- </el-form>
- </ContentWrap>
- <!-- 列表 -->
- <ContentWrap>
- <el-table v-loading="loading" :data="list">
- <el-table-column label="流程名称" align="center" prop="name" min-width="200" />
- <el-table-column label="流程图标" align="center" prop="icon" min-width="100">
- <template #default="scope">
- <el-image :src="scope.row.icon" class="h-32px w-32px" />
- </template>
- </el-table-column>
- <el-table-column label="可见范围" align="center" prop="startUserIds" min-width="100">
- <el-text v-if="!scope.row.startUsers || scope.row.startUsers.length === 0">
- 全部可见
- </el-text>
- <el-text v-else-if="scope.row.startUsers.length == 1">
- {{ scope.row.startUsers[0].nickname }}
- <el-text v-else>
- <el-tooltip
- class="box-item"
- effect="dark"
- placement="top"
- :content="scope.row.startUsers.map((user: any) => user.nickname).join('、')"
- {{ scope.row.startUsers[0].nickname }}等 {{ scope.row.startUsers.length }} 人可见
- </el-tooltip>
- <el-table-column label="流程分类" align="center" prop="categoryName" min-width="100" />
- <el-table-column label="表单信息" align="center" prop="formType" min-width="200">
- v-if="scope.row.formType === 10"
- link
- @click="handleFormDetail(scope.row)"
- <span>{{ scope.row.formName }}</span>
- v-else-if="scope.row.formType === 20"
- <span>{{ scope.row.formCustomCreatePath }}</span>
- <label v-else>暂无表单</label>
- <el-table-column label="最后发布" align="center" prop="deploymentTime" min-width="250">
- <span v-if="scope.row.processDefinition">
- {{ formatDate(scope.row.processDefinition.deploymentTime) }}
- <el-tag v-if="scope.row.processDefinition" class="ml-10px">
- v{{ scope.row.processDefinition.version }}
- </el-tag>
- <el-tag v-else type="warning">未部署</el-tag>
- <el-tag
- v-if="scope.row.processDefinition?.suspensionState === 2"
- type="warning"
- class="ml-10px"
- 已停用
- <el-table-column label="操作" align="center" width="200" fixed="right">
- @click="openForm('update', scope.row.id)"
- v-hasPermi="['bpm:model:update']"
- :disabled="!isManagerUser(scope.row)"
- 修改
- class="!ml-5px"
- @click="handleDesign(scope.row)"
+ <div class="flex justify-between pl-20px items-center">
+ <h3 class="font-extrabold">流程模型</h3>
+ <!-- 搜索工作栏 -->
+ <el-form
+ v-if="!isCategorySorting"
+ class="-mb-15px flex mr-10px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ @submit.prevent
+ <el-form-item prop="name" class="ml-auto">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="搜索流程"
+ @keyup.enter="handleQuery"
+ class="!w-240px"
- 设计
- @click="handleDeploy(scope.row)"
- v-hasPermi="['bpm:model:deploy']"
- 发布
+ <template #prefix>
+ <Icon icon="ep:search" class="mx-10px" />
+ </el-input>
+ <!-- 右上角:新建模型、更多操作 -->
+ <el-form-item>
+ <el-button type="primary" @click="openForm('create')" v-hasPermi="['bpm:model:create']">
+ <Icon icon="ep:plus" class="mr-5px" /> 新建模型
</el-button>
- <el-dropdown
- class="!align-middle ml-5px"
- @command="(command) => handleCommand(command, scope.row)"
- v-hasPermi="['bpm:process-definition:query', 'bpm:model:update', 'bpm:model:delete']"
- <el-button type="primary" link>更多</el-button>
+ <el-dropdown @command="(command) => handleCommand(command)" placement="bottom-end">
+ <el-button class="w-30px" plain>
+ <Icon icon="ep:setting" />
<template #dropdown>
<el-dropdown-menu>
- <el-dropdown-item
- command="handleDefinitionList"
- v-if="checkPermi(['bpm:process-definition:query'])"
- 历史
- </el-dropdown-item>
- command="handleChangeState"
- v-if="checkPermi(['bpm:model:update']) && scope.row.processDefinition"
- {{ scope.row.processDefinition.suspensionState === 1 ? '停用' : '启用' }}
+ <el-dropdown-item command="handleCategoryAdd">
+ <Icon icon="ep:circle-plus" :size="13" class="mr-5px" />
+ 新建分类
</el-dropdown-item>
- type="danger"
- command="handleDelete"
- v-if="checkPermi(['bpm:model:delete'])"
- 删除
+ <el-dropdown-item command="handleCategorySort">
+ <Icon icon="fa:sort-amount-desc" :size="13" class="mr-5px" />
+ 分类排序
</el-dropdown-menu>
</el-dropdown>
+ </el-form>
+ <div class="mr-20px" v-else>
+ <el-button @click="handleCategorySortCancel"> 取 消 </el-button>
+ <el-button type="primary" @click="handleCategorySortSubmit"> 保存排序 </el-button>
+ <el-divider />
+ <!-- 按照分类,展示其所属的模型列表 -->
+ <div class="px-15px">
+ <draggable
+ :disabled="!isCategorySorting"
+ v-model="categoryGroup"
+ item-key="id"
+ :animation="400"
+ <template #item="{ element }">
+ <ContentWrap
+ class="rounded-lg transition-all duration-300 ease-in-out hover:shadow-xl"
+ v-loading="loading"
+ :body-style="{ padding: 0 }"
+ :key="element.id"
+ <CategoryDraggableModel
+ :isCategorySorting="isCategorySorting"
+ :categoryInfo="element"
+ @success="getList"
- </el-table>
- <!-- 分页 -->
- <Pagination
- :total="total"
- v-model:page="queryParams.pageNo"
- v-model:limit="queryParams.pageSize"
- @pagination="getList"
+ </draggable>
</ContentWrap>
<!-- 表单弹窗:添加/修改流程 -->
<ModelForm ref="formRef" @success="getList" />
+ <!-- 表单弹窗:添加分类 -->
+ <CategoryForm ref="categoryFormRef" @success="getList" />
<!-- 弹窗:表单详情 -->
<Dialog title="表单详情" v-model="formDetailVisible" width="800">
<form-create :rule="formDetailPreview.rule" :option="formDetailPreview.option" />
@@ -218,187 +96,126 @@
+import draggable from 'vuedraggable'
+import { CategoryApi } from '@/api/bpm/category'
-import * as FormApi from '@/api/bpm/form'
import ModelForm from './ModelForm.vue'
-import { setConfAndFields2 } from '@/utils/formCreate'
-import { CategoryApi } from '@/api/bpm/category'
-import { BpmModelType } from '@/utils/constants'
-import { checkPermi } from '@/utils/permission'
-import { useUserStoreWithOut } from '@/store/modules/user'
+import CategoryForm from '../category/CategoryForm.vue'
+import CategoryDraggableModel from './CategoryDraggableModel.vue'
defineOptions({ name: 'BpmModel' })
-const { t } = useI18n() // 国际化
-const { push } = useRouter() // 路由
-const userStore = useUserStoreWithOut() // 用户信息缓存
const loading = ref(true) // 列表的加载中
-const total = ref(0) // 列表的总页数
-const list = ref([]) // 列表的数据
+const isCategorySorting = ref(false) // 是否 category 正处于排序状态
const queryParams = reactive({
- pageNo: 1,
- pageSize: 10,
- key: undefined,
- name: undefined,
- category: undefined
+ name: undefined
const queryFormRef = ref() // 搜索的表单
-const categoryList = ref([]) // 流程分类列表
-/** 查询列表 */
-const getList = async () => {
- loading.value = true
- const data = await ModelApi.getModelPage(queryParams)
- list.value = data.list
- total.value = data.total
- } finally {
- loading.value = false
+const categoryGroup: any = ref([]) // 按照 category 分组的数据
/** 搜索按钮操作 */
const handleQuery = () => {
- queryParams.pageNo = 1
getList()
-/** 重置按钮操作 */
-const resetQuery = () => {
- queryFormRef.value.resetFields()
- handleQuery()
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
-/** '更多'操作按钮 */
-const handleCommand = (command: string, row: any) => {
+/** 右上角设置按钮 */
+const handleCommand = (command: string) => {
switch (command) {
- case 'handleDefinitionList':
- handleDefinitionList(row)
+ case 'handleCategoryAdd':
+ handleCategoryAdd()
- case 'handleDelete':
- handleDelete(row)
- break
- case 'handleChangeState':
- handleChangeState(row)
+ case 'handleCategorySort':
+ handleCategorySort()
-/** 添加/修改操作 */
-const formRef = ref()
-const openForm = (type: string, id?: number) => {
- formRef.value.open(type, id)
+/** 新建分类 */
+const categoryFormRef = ref()
+const handleCategoryAdd = () => {
+ categoryFormRef.value.open('create')
-/** 删除按钮操作 */
-const handleDelete = async (row: any) => {
- // 删除的二次确认
- await message.delConfirm()
- // 发起删除
- await ModelApi.deleteModel(row.id)
- message.success(t('common.delSuccess'))
- // 刷新列表
- await getList()
- } catch {}
+/** 分类排序的提交 */
+const handleCategorySort = () => {
+ originalData.value = cloneDeep(categoryGroup.value)
+ isCategorySorting.value = true
-/** 更新状态操作 */
-const handleChangeState = async (row: any) => {
- const state = row.processDefinition.suspensionState
- const newState = state === 1 ? 2 : 1
- // 修改状态的二次确认
- const id = row.id
- const statusState = state === 1 ? '停用' : '启用'
- const content = '是否确认' + statusState + '流程名字为"' + row.name + '"的数据项?'
- await message.confirm(content)
- // 发起修改状态
- await ModelApi.updateModelState(id, newState)
- message.success(statusState + '成功')
+/** 分类排序的取消 */
+const handleCategorySortCancel = () => {
+ categoryGroup.value = cloneDeep(originalData.value)
+ isCategorySorting.value = false
-/** 设计流程 */
-const handleDesign = (row: any) => {
- if (row.type == BpmModelType.BPMN) {
- push({
- name: 'BpmModelEditor',
- query: {
- modelId: row.id
+/** 分类排序的保存 */
+const handleCategorySortSubmit = async () => {
+ const ids = categoryGroup.value.map((item: any) => item.id)
+ await CategoryApi.updateCategorySortBatch(ids)
+ message.success('排序分类成功')
+ await getList()
-/** 发布流程 */
-const handleDeploy = async (row: any) => {
+/** 加载数据 */
+const getList = async () => {
- await message.confirm('是否部署该流程!!')
- // 发起部署
- await ModelApi.deployModel(row.id)
- message.success(t('部署成功'))
-/** 跳转到指定流程定义列表 */
-const handleDefinitionList = (row) => {
- name: 'BpmProcessDefinition',
- key: row.key
-/** 流程表单的详情按钮操作 */
-const formDetailVisible = ref(false)
-const formDetailPreview = ref({
- rule: [],
- option: {}
-const handleFormDetail = async (row: any) => {
- if (row.formType == 10) {
- // 设置表单
- const data = await FormApi.getForm(row.formId)
- setConfAndFields2(formDetailPreview, data.conf, data.fields)
- // 弹窗打开
- formDetailVisible.value = true
- await push({
- path: row.formCustomCreatePath
+ // 查询模型 + 分裂的列表
+ const modelList = await ModelApi.getModelList(queryParams.name)
+ const categoryList = await CategoryApi.getCategorySimpleList()
+ // 按照 category 聚合
+ // 注意:必须一次性赋值给 categoryGroup,否则每次操作后,列表会重新渲染,滚动条的位置会偏离!!!
+ categoryGroup.value = categoryList.map((category: any) => ({
+ ...category,
+ modelList: modelList.filter((model: any) => model.categoryName == category.name)
+ }))
-/** 判断是否可以操作 */
-const isManagerUser = (row: any) => {
- const userId = userStore.getUser.id
- return row.managerUserIds && row.managerUserIds.includes(userId)
-onMounted(async () => {
- // 查询流程分类列表
- categoryList.value = await CategoryApi.getCategorySimpleList()
+ getList()
+ .el-table--fit .el-table__inner-wrapper:before {
+ height: 0;
+ .el-card {
+ .el-form--inline .el-form-item {
+ margin-right: 10px;
+ .el-divider--horizontal {
+ margin-top: 6px;
@@ -0,0 +1,404 @@
+ <doc-alert title="流程设计器(BPMN)" url="https://doc.iocoder.cn/bpm/model-designer-dingding/" />
+ <doc-alert
+ title="流程设计器(钉钉、飞书)"
+ url="https://doc.iocoder.cn/bpm/model-designer-bpmn/"
+ <doc-alert title="选择审批人、发起人自选" url="https://doc.iocoder.cn/bpm/assignee/" />
+ <doc-alert title="会签、或签、依次审批" url="https://doc.iocoder.cn/bpm/multi-instance/" />
+ <ContentWrap>
+ class="-mb-15px"
+ <el-form-item label="流程标识" prop="key">
+ v-model="queryParams.key"
+ placeholder="请输入流程标识"
+ <el-form-item label="流程名称" prop="name">
+ placeholder="请输入流程名称"
+ <el-form-item label="流程分类" prop="category">
+ v-model="queryParams.category"
+ placeholder="请选择流程分类"
+ v-for="category in categoryList"
+ :key="category.code"
+ :label="category.name"
+ :value="category.code"
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['bpm:model:create']"
+ <Icon icon="ep:plus" class="mr-5px" /> 新建
+ <!-- 列表 -->
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="流程名称" align="center" prop="name" min-width="200" />
+ <el-table-column label="流程图标" align="center" prop="icon" min-width="100">
+ <el-image :src="scope.row.icon" class="h-32px w-32px" />
+ <el-table-column label="可见范围" align="center" prop="startUserIds" min-width="100">
+ <el-table-column label="流程分类" align="center" prop="categoryName" min-width="100" />
+ <el-table-column label="表单信息" align="center" prop="formType" min-width="200">
+ v-if="scope.row.formType === 10"
+ v-else-if="scope.row.formType === 20"
+ <el-table-column label="最后发布" align="center" prop="deploymentTime" min-width="250">
+ <span v-if="scope.row.processDefinition">
+ <el-tag v-if="scope.row.processDefinition" class="ml-10px">
+ <el-table-column label="操作" align="center" width="200" fixed="right">
+ @click="openForm('update', scope.row.id)"
+ @command="(command) => handleCommand(command, scope.row)"
+ <!-- 分页 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ <!-- 表单弹窗:添加/修改流程 -->
+ <ModelForm ref="formRef" @success="getList" />
+ <!-- 弹窗:表单详情 -->
+ <Dialog title="表单详情" v-model="formDetailVisible" width="800">
+ <form-create :rule="formDetailPreview.rule" :option="formDetailPreview.option" />
+import { BpmModelType } from '@/utils/constants'
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ key: undefined,
+ name: undefined,
+ category: undefined
+const queryFormRef = ref() // 搜索的表单
+const categoryList = ref([]) // 流程分类列表
+/** 查询列表 */
+ const data = await ModelApi.getModelList(queryParams)
+ list.value = data.list
+ total.value = data.total
+/** 搜索按钮操作 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+/** 重置按钮操作 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+const handleCommand = (command: string, row: any) => {
+const handleDefinitionList = (row) => {
+/** 初始化 **/
+ // 查询流程分类列表
+ categoryList.value = await CategoryApi.getCategorySimpleList()
@@ -0,0 +1,259 @@
+ <ContentWrap :bodyStyle="{ padding: '10px 20px 0' }">
+ <div class="processInstance-wrap-main">
+ <el-scrollbar>
+ <div class="text-#878c93 h-15px">流程:{{ selectProcessDefinition.name }}</div>
+ <el-divider class="!my-8px" />
+ <!-- 中间主要内容 tab 栏 -->
+ <el-tabs v-model="activeTab">
+ <!-- 表单信息 -->
+ <el-tab-pane label="表单填写" name="form">
+ <div class="form-scroll-area">
+ <form-create
+ :rule="detailForm.rule"
+ v-model:api="fApi"
+ v-model="detailForm.value"
+ :option="detailForm.option"
+ @submit="submitForm"
+ <el-col :span="6" :offset="1">
+ <!-- 流程时间线 -->
+ <ProcessInstanceTimeline
+ ref="timelineRef"
+ :activity-nodes="activityNodes"
+ :show-status-icon="false"
+ @select-user-confirm="selectUserConfirm"
+ </el-scrollbar>
+ </el-tab-pane>
+ <!-- 流程图 -->
+ <el-tab-pane label="流程图" name="diagram">
+ <!-- BPMN 流程图预览 -->
+ <ProcessInstanceBpmnViewer
+ :bpmn-xml="bpmnXML"
+ v-if="BpmModelType.BPMN === selectProcessDefinition.modelType"
+ <!-- Simple 流程图预览 -->
+ <ProcessInstanceSimpleViewer
+ :simple-json="simpleJson"
+ v-if="BpmModelType.SIMPLE === selectProcessDefinition.modelType"
+ </el-tabs>
+ <!-- 底部操作栏 -->
+ <div class="b-t-solid border-t-1px border-[var(--el-border-color)]">
+ <!-- 操作栏按钮 -->
+ v-if="activeTab === 'form'"
+ class="h-50px bottom-10 text-14px flex items-center color-#32373c dark:color-#fff font-bold btn-container"
+ <el-button plain type="success" @click="submitForm">
+ <Icon icon="ep:select" /> 发起
+ <el-button plain type="danger" @click="handleCancel">
+ <Icon icon="ep:close" /> 取消
+import { decodeFields, setConfAndFields2 } from '@/utils/formCreate'
+import { CandidateStrategy } from '@/components/SimpleProcessDesignerV2/src/consts'
+import ProcessInstanceBpmnViewer from '../detail/ProcessInstanceBpmnViewer.vue'
+import ProcessInstanceSimpleViewer from '../detail/ProcessInstanceSimpleViewer.vue'
+import ProcessInstanceTimeline from '../detail/ProcessInstanceTimeline.vue'
+import type { ApiAttrs } from '@form-create/element-ui/types/config'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import * as ProcessInstanceApi from '@/api/bpm/processInstance'
+import * as DefinitionApi from '@/api/bpm/definition'
+import { ApprovalNodeInfo } from '@/api/bpm/processInstance'
+defineOptions({ name: 'ProcessDefinitionDetail' })
+const props = defineProps<{
+ selectProcessDefinition: any
+const emit = defineEmits(['cancel'])
+const { push, currentRoute } = useRouter() // 路由
+const { delView } = useTagsViewStore() // 视图操作
+const detailForm: any = ref({
+ option: {},
+ value: {}
+}) // 流程表单详情
+const fApi = ref<ApiAttrs>()
+// 指定审批人
+const startUserSelectTasks: any = ref([]) // 发起人需要选择审批人或抄送人的任务列表
+const startUserSelectAssignees = ref({}) // 发起人选择审批人的数据
+const bpmnXML: any = ref(null) // BPMN 数据
+const simpleJson = ref<string | undefined>() // Simple 设计器数据 json 格式
+const activeTab = ref('form') // 当前的 Tab
+const activityNodes = ref<ProcessInstanceApi.ApprovalNodeInfo[]>([]) // 审批节点信息
+/** 设置表单信息、获取流程图数据 **/
+const initProcessInfo = async (row: any, formVariables?: any) => {
+ // 重置指定审批人
+ startUserSelectTasks.value = []
+ startUserSelectAssignees.value = {}
+ // 情况一:流程表单
+ // 注意:需要从 formVariables 中,移除不在 row.formFields 的值。
+ // 原因是:后端返回的 formVariables 里面,会有一些非表单的信息。例如说,某个流程节点的审批人。
+ // 这样,就可能导致一个流程被审批不通过后,重新发起时,会直接后端报错!!!
+ const allowedFields = decodeFields(row.formFields).map((fieldObj: any) => fieldObj.field)
+ for (const key in formVariables) {
+ if (!allowedFields.includes(key)) {
+ delete formVariables[key]
+ setConfAndFields2(detailForm, row.formConf, row.formFields, formVariables)
+ await nextTick()
+ fApi.value?.btn.show(false) // 隐藏提交按钮
+ // 获取流程审批信息
+ await getApprovalDetail(row)
+ // 加载流程图
+ const processDefinitionDetail = await DefinitionApi.getProcessDefinition(row.id)
+ if (processDefinitionDetail) {
+ bpmnXML.value = processDefinitionDetail.bpmnXml
+ simpleJson.value = processDefinitionDetail.simpleModel
+ // 情况二:业务表单
+ } else if (row.formCustomCreatePath) {
+ // 这里暂时无需加载流程图,因为跳出到另外个 Tab;
+/** 获取审批详情 */
+const getApprovalDetail = async (row: any) => {
+ const data = await ProcessInstanceApi.getApprovalDetail({ processDefinitionId: row.id })
+ if (!data) {
+ message.error('查询不到审批详情信息!')
+ // 获取发起人自选的任务
+ startUserSelectTasks.value = data.activityNodes?.filter(
+ (node: ApprovalNodeInfo) => CandidateStrategy.START_USER_SELECT === node.candidateStrategy
+ if (startUserSelectTasks.value?.length > 0) {
+ for (const node of startUserSelectTasks.value) {
+ startUserSelectAssignees.value[node.id] = []
+ // 获取审批节点,显示 Timeline 的数据
+ activityNodes.value = data.activityNodes
+/** 提交按钮 */
+ if (!fApi.value || !props.selectProcessDefinition) {
+ // 如果有指定审批人,需要校验
+ for (const userTask of startUserSelectTasks.value) {
+ Array.isArray(startUserSelectAssignees.value[userTask.id]) &&
+ startUserSelectAssignees.value[userTask.id].length === 0
+ return message.warning(`请选择${userTask.name}的候选人`)
+ // 提交请求
+ fApi.value.btn.loading(true)
+ await ProcessInstanceApi.createProcessInstance({
+ processDefinitionId: props.selectProcessDefinition.id,
+ variables: detailForm.value.value,
+ startUserSelectAssignees: startUserSelectAssignees.value
+ // 提示
+ message.success('发起流程成功')
+ // 跳转回去
+ delView(unref(currentRoute))
+ name: 'BpmProcessInstanceMy'
+ fApi.value.btn.loading(false)
+/** 取消发起审批 */
+const handleCancel = () => {
+ emit('cancel')
+/** 选择发起人 */
+const selectUserConfirm = (id: string, userList: any[]) => {
+ startUserSelectAssignees.value[id] = userList?.map((item: any) => item.id)
+defineExpose({ initProcessInfo })
+$wrap-padding-height: 20px;
+$wrap-margin-height: 15px;
+$button-height: 51px;
+$process-header-height: 105px;
+.processInstance-wrap-main {
+ height: calc(
+ 100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px
+ );
+ max-height: calc(
+ .form-scroll-area {
+ 100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px -
+ $process-header-height - 40px
+.form-box {
+ :deep(.el-card) {
+ border: none;
@@ -1,133 +1,115 @@
- <doc-alert title="流程发起、取消、重新发起" url="https://doc.iocoder.cn/bpm/process-instance/" />
<!-- 第一步,通过流程定义的列表,选择对应的流程 -->
- <ContentWrap v-if="!selectProcessDefinition" v-loading="loading">
- <el-tabs tab-position="left" v-model="categoryActive">
- <el-tab-pane
- :name="category.code"
- <el-row :gutter="20">
- <el-col
- :lg="6"
- :sm="12"
- :xs="24"
- v-for="definition in categoryProcessDefinitionList"
- :key="definition.id"
- <el-card
- shadow="hover"
- class="mb-20px cursor-pointer"
- @click="handleSelect(definition)"
+ <template v-if="!selectProcessDefinition">
+ v-model="searchName"
+ class="!w-50% mb-15px"
+ @input="handleQuery"
+ @clear="handleQuery"
+ <Icon icon="ep:search" />
+ :class="{ 'process-definition-container': filteredProcessDefinitionList?.length }"
+ class="position-relative pb-20px h-700px"
+ <el-row v-if="filteredProcessDefinitionList?.length" :gutter="20" class="!flex-nowrap">
+ <el-col :span="5">
+ <div class="flex flex-col">
+ v-for="category in availableCategories"
+ class="flex items-center p-10px cursor-pointer text-14px rounded-md"
+ :class="categoryActive.code === category.code ? 'text-#3e7bff bg-#e8eeff' : ''"
+ @click="handleCategoryClick(category)"
- <template #default>
- <div class="flex">
- <el-image :src="definition.icon" class="w-32px h-32px" />
- <el-text class="!ml-10px" size="large">{{ definition.name }}</el-text>
- </el-card>
- </el-col>
- </el-row>
- </el-tab-pane>
- </el-tabs>
- <!-- 第二步,填写表单,进行流程的提交 -->
- <ContentWrap v-else>
- <el-card class="box-card">
- <div class="clearfix">
- <span class="el-icon-document">申请信息【{{ selectProcessDefinition.name }}】</span>
- <el-button style="float: right" type="primary" @click="selectProcessDefinition = undefined">
- <Icon icon="ep:delete" /> 选择其它流程
- <el-col :span="16" :offset="6" style="margin-top: 20px">
- <form-create
- :rule="detailForm.rule"
- v-model:api="fApi"
- v-model="detailForm.value"
- :option="detailForm.option"
- @submit="submitForm"
- <template #type-startUserSelect>
- <el-col :span="24">
- <el-card class="mb-10px">
- <template #header>指定审批人</template>
- :model="startUserSelectAssignees"
- :rules="startUserSelectAssigneesFormRules"
- ref="startUserSelectAssigneesFormRef"
+ {{ category.name }}
+ <el-col :span="19">
+ <el-scrollbar ref="scrollWrapper" height="700" @scroll="handleScroll">
+ class="mb-20px pl-10px"
+ v-for="(definitions, categoryCode) in processDefinitionGroup"
+ :key="categoryCode"
+ :ref="`category-${categoryCode}`"
+ <h3 class="text-18px font-bold mb-10px mt-5px">
+ {{ getCategoryName(categoryCode as any) }}
+ </h3>
+ <div class="grid grid-cols-3 gap3">
+ v-for="definition in definitions"
+ :key="definition.id"
+ :content="definition.description"
+ :disabled="!definition.description || definition.description.trim().length === 0"
- v-for="userTask in startUserSelectTasks"
- :key="userTask.id"
- :label="`任务【${userTask.name}】`"
- :prop="userTask.id"
+ <el-card
+ shadow="hover"
+ class="cursor-pointer definition-item-card"
+ @click="handleSelect(definition)"
- v-model="startUserSelectAssignees[userTask.id]"
- placeholder="请选择审批人"
- v-for="user in userList"
- :key="user.id"
- :label="user.nickname"
- :value="user.id"
- </form-create>
- <!-- 流程图预览 -->
- <ProcessInstanceBpmnViewer :bpmn-xml="bpmnXML as any" />
+ <template #default>
+ <div class="flex">
+ <el-image :src="definition.icon" class="w-32px h-32px" />
+ <el-text class="!ml-10px" size="large">{{ definition.name }}</el-text>
+ </el-card>
+ <el-empty class="!py-200px" :image-size="200" description="没有找到搜索结果" v-else />
+ <!-- 第二步,填写表单,进行流程的提交 -->
+ <ProcessDefinitionDetail
+ ref="processDefinitionDetailRef"
+ :selectProcessDefinition="selectProcessDefinition"
+ @cancel="selectProcessDefinition = undefined"
import * as DefinitionApi from '@/api/bpm/definition'
import * as ProcessInstanceApi from '@/api/bpm/processInstance'
-import type { ApiAttrs } from '@form-create/element-ui/types/config'
-import ProcessInstanceBpmnViewer from '../detail/ProcessInstanceBpmnViewer.vue'
-import { useTagsViewStore } from '@/store/modules/tagsView'
-import * as UserApi from '@/api/system/user'
+import ProcessDefinitionDetail from './ProcessDefinitionDetail.vue'
+import { groupBy } from 'lodash-es'
defineOptions({ name: 'BpmProcessInstanceCreate' })
const route = useRoute() // 路由
-const { push, currentRoute } = useRouter() // 路由
const message = useMessage() // 消息
-const { delView } = useTagsViewStore() // 视图操作
-const processInstanceId = route.query.processInstanceId
+const searchName = ref('') // 当前搜索关键字
+const processInstanceId: any = route.query.processInstanceId // 流程实例编号。场景:重新发起时
const loading = ref(true) // 加载中
-const categoryList = ref([]) // 分类的列表
-const categoryActive = ref('') // 选中的分类
+const categoryList: any = ref([]) // 分类的列表
+const categoryActive: any = ref({}) // 选中的分类
const processDefinitionList = ref([]) // 流程定义的列表
/** 查询列表 */
const getList = async () => {
- // 流程分类
- if (categoryList.value.length > 0) {
- categoryActive.value = categoryList.value[0].code
- // 流程定义
- processDefinitionList.value = await DefinitionApi.getProcessDefinitionList({
- suspensionState: 1
+ // 所有流程分类数据
+ await getCategoryList()
+ // 所有流程定义数据
+ await getProcessDefinitionList()
// 如果 processInstanceId 非空,说明是重新发起
if (processInstanceId?.length > 0) {
@@ -137,7 +119,7 @@ const getList = async () => {
const processDefinition = processDefinitionList.value.find(
- (item) => item.key == processInstance.processDefinition?.key
+ (item: any) => item.key == processInstance.processDefinition?.key
if (!processDefinition) {
message.error('重新发起流程失败,原因:流程定义不存在')
@@ -150,108 +132,168 @@ const getList = async () => {
-/** 选中分类对应的流程定义列表 */
-const categoryProcessDefinitionList = computed(() => {
- return processDefinitionList.value.filter((item) => item.category == categoryActive.value)
+/** 获取所有流程分类数据 */
+const getCategoryList = async () => {
+ // 流程分类
+/** 获取所有流程定义数据 */
+const getProcessDefinitionList = async () => {
+ // 流程定义
+ processDefinitionList.value = await DefinitionApi.getProcessDefinitionList({
+ suspensionState: 1
+ // 初始化过滤列表为全部流程定义
+ filteredProcessDefinitionList.value = processDefinitionList.value
+ // 在获取完所有数据后,设置第一个有效分类为激活状态
+ if (availableCategories.value.length > 0 && !categoryActive.value?.code) {
+ categoryActive.value = availableCategories.value[0]
+/** 搜索流程 */
+const filteredProcessDefinitionList = ref([]) // 用于存储搜索过滤后的流程定义
+ if (searchName.value.trim()) {
+ // 如果有搜索关键字,进行过滤
+ filteredProcessDefinitionList.value = processDefinitionList.value.filter(
+ (definition: any) => definition.name.toLowerCase().includes(searchName.value.toLowerCase()) // 假设搜索依据是流程定义的名称
+ // 如果没有搜索关键字,恢复所有数据
+/** 流程定义的分组 */
+const processDefinitionGroup: any = computed(() => {
+ if (!processDefinitionList.value?.length) {
+ return {}
+ const grouped = groupBy(filteredProcessDefinitionList.value, 'category')
+ // 按照 categoryList 的顺序重新组织数据
+ const orderedGroup = {}
+ categoryList.value.forEach((category: any) => {
+ if (grouped[category.code]) {
+ orderedGroup[category.code] = grouped[category.code]
+ return orderedGroup
+/** 左侧分类切换 */
+const handleCategoryClick = (category: any) => {
+ categoryActive.value = category
+ const categoryRef = proxy.$refs[`category-${category.code}`] // 获取点击分类对应的 DOM 元素
+ if (categoryRef?.length) {
+ const scrollWrapper = proxy.$refs.scrollWrapper // 获取右侧滚动容器
+ const categoryOffsetTop = categoryRef[0].offsetTop
+ // 滚动到对应位置
+ scrollWrapper.scrollTo({ top: categoryOffsetTop, behavior: 'smooth' })
+/** 通过分类 code 获取对应的名称 */
+const getCategoryName = (categoryCode: string) => {
+ return categoryList.value?.find((ctg: any) => ctg.code === categoryCode)?.name
// ========== 表单相关 ==========
-const fApi = ref<ApiAttrs>()
-const detailForm = ref({
- option: {},
- value: {}
-}) // 流程表单详情
const selectProcessDefinition = ref() // 选择的流程定义
-// 指定审批人
-const bpmnXML = ref(null) // BPMN 数据
-const startUserSelectTasks = ref([]) // 发起人需要选择审批人的用户任务列表
-const startUserSelectAssignees = ref({}) // 发起人选择审批人的数据
-const startUserSelectAssigneesFormRef = ref() // 发起人选择审批人的表单 Ref
-const startUserSelectAssigneesFormRules = ref({}) // 发起人选择审批人的表单 Rules
-const userList = ref<any[]>([]) // 用户列表
+const processDefinitionDetailRef = ref()
/** 处理选择流程的按钮操作 **/
-const handleSelect = async (row, formVariables) => {
+const handleSelect = async (row, formVariables?) => {
// 设置选择的流程
selectProcessDefinition.value = row
+ // 初始化流程定义详情
+ processDefinitionDetailRef.value?.initProcessInfo(row, formVariables)
- // 重置指定审批人
- startUserSelectTasks.value = []
- startUserSelectAssignees.value = {}
- startUserSelectAssigneesFormRules.value = {}
- // 情况一:流程表单
- setConfAndFields2(detailForm, row.formConf, row.formFields, formVariables)
- // 加载流程图
- const processDefinitionDetail = await DefinitionApi.getProcessDefinition(row.id)
- if (processDefinitionDetail) {
- bpmnXML.value = processDefinitionDetail.bpmnXml
- startUserSelectTasks.value = processDefinitionDetail.startUserSelectTasks
+/** 处理滚动事件,和左侧分类联动 */
+const handleScroll = (e: any) => {
+ // 直接使用事件对象获取滚动位置
+ const scrollTop = e.scrollTop
- // 设置指定审批人
- if (startUserSelectTasks.value?.length > 0) {
- detailForm.value.rule.push({
- type: 'startUserSelect',
- props: {
- title: '指定审批人'
- // 设置校验规则
- for (const userTask of startUserSelectTasks.value) {
- startUserSelectAssignees.value[userTask.id] = []
- startUserSelectAssigneesFormRules.value[userTask.id] = [
- { required: true, message: '请选择审批人', trigger: 'blur' }
- ]
+ // 获取所有分类区域的位置信息
+ const categoryPositions = categoryList.value
+ .map((category: CategoryVO) => {
+ const categoryRef = proxy.$refs[`category-${category.code}`]
+ if (categoryRef?.[0]) {
+ code: category.code,
+ offsetTop: categoryRef[0].offsetTop,
+ height: categoryRef[0].offsetHeight
- // 加载用户列表
- userList.value = await UserApi.getSimpleUserList()
- // 情况二:业务表单
- } else if (row.formCustomCreatePath) {
+ return null
- // 这里暂时无需加载流程图,因为跳出到另外个 Tab;
+ .filter(Boolean)
-/** 提交按钮 */
-const submitForm = async (formData) => {
- if (!fApi.value || !selectProcessDefinition.value) {
- // 如果有指定审批人,需要校验
- await startUserSelectAssigneesFormRef.value.validate()
+ // 查找当前滚动位置对应的分类
+ let currentCategory = categoryPositions[0]
+ for (const position of categoryPositions) {
+ // 为了更好的用户体验,可以添加一个缓冲区域(比如 50px)
+ if (scrollTop >= position.offsetTop - 50) {
+ currentCategory = position
- // 提交请求
- fApi.value.btn.loading(true)
- await ProcessInstanceApi.createProcessInstance({
- processDefinitionId: selectProcessDefinition.value.id,
- variables: formData,
- startUserSelectAssignees: startUserSelectAssignees.value
- // 提示
- message.success('发起流程成功')
- // 跳转回去
- delView(unref(currentRoute))
- name: 'BpmProcessInstanceMy'
- fApi.value.btn.loading(false)
+ // 更新当前 active 的分类
+ if (currentCategory && categoryActive.value.code !== currentCategory.code) {
+ categoryActive.value = categoryList.value.find(
+ (c: CategoryVO) => c.code === currentCategory.code
+/** 过滤出有流程的分类列表。目的:只展示有流程的分类 */
+const availableCategories = computed(() => {
+ if (!categoryList.value?.length || !processDefinitionGroup.value) {
+ return []
+ // 获取所有有流程的分类代码
+ const availableCategoryCodes = Object.keys(processDefinitionGroup.value)
+ // 过滤出有流程的分类
+ return categoryList.value.filter((category: CategoryVO) =>
+ availableCategoryCodes.includes(category.code)
/** 初始化 */
onMounted(() => {
+.process-definition-container::before {
+ border-left: 1px solid #e6e6e6;
+ left: 20.8%;
+ .definition-item-card {
+ .el-card__body {
+ padding: 14px;
@@ -0,0 +1,267 @@
+ <doc-alert title="流程发起、取消、重新发起" url="https://doc.iocoder.cn/bpm/process-instance/" />
+ <!-- 第一步,通过流程定义的列表,选择对应的流程 -->
+ <ContentWrap v-if="!selectProcessDefinition" v-loading="loading">
+ <el-tabs tab-position="left" v-model="categoryActive">
+ <el-tab-pane
+ :name="category.code"
+ <el-row :gutter="20">
+ <el-col
+ :lg="6"
+ :sm="12"
+ :xs="24"
+ v-for="definition in categoryProcessDefinitionList"
+ class="mb-20px cursor-pointer"
+ <ContentWrap v-else>
+ <el-card class="box-card">
+ <div class="clearfix">
+ <span class="el-icon-document">申请信息【{{ selectProcessDefinition.name }}】</span>
+ <el-button style="float: right" type="primary" @click="selectProcessDefinition = undefined">
+ <Icon icon="ep:delete" /> 选择其它流程
+ <el-col :span="16" :offset="6" style="margin-top: 20px">
+ <template #type-startUserSelect>
+ <el-col :span="24">
+ <el-card class="mb-10px">
+ <template #header>指定审批人</template>
+ :model="startUserSelectAssignees"
+ :rules="startUserSelectAssigneesFormRules"
+ ref="startUserSelectAssigneesFormRef"
+ v-for="userTask in startUserSelectTasks"
+ :key="userTask.id"
+ :label="`任务【${userTask.name}】`"
+ :prop="userTask.id"
+ v-model="startUserSelectAssignees[userTask.id]"
+ placeholder="请选择审批人"
+ v-for="user in userList"
+ :key="user.id"
+ :label="user.nickname"
+ :value="user.id"
+ </form-create>
+ <!-- 流程图预览 -->
+ <ProcessInstanceBpmnViewer :bpmn-xml="bpmnXML as any" />
+defineOptions({ name: 'BpmProcessInstanceCreate' })
+const route = useRoute() // 路由
+const message = useMessage() // 消息
+const processInstanceId = route.query.processInstanceId
+const loading = ref(true) // 加载中
+const categoryList = ref([]) // 分类的列表
+const categoryActive = ref('') // 选中的分类
+const processDefinitionList = ref([]) // 流程定义的列表
+ if (categoryList.value.length > 0) {
+ categoryActive.value = categoryList.value[0].code
+ // 如果 processInstanceId 非空,说明是重新发起
+ if (processInstanceId?.length > 0) {
+ const processInstance = await ProcessInstanceApi.getProcessInstance(processInstanceId)
+ if (!processInstance) {
+ message.error('重新发起流程失败,原因:流程实例不存在')
+ const processDefinition = processDefinitionList.value.find(
+ (item) => item.key == processInstance.processDefinition?.key
+ if (!processDefinition) {
+ message.error('重新发起流程失败,原因:流程定义不存在')
+ await handleSelect(processDefinition, processInstance.formVariables)
+/** 选中分类对应的流程定义列表 */
+const categoryProcessDefinitionList = computed(() => {
+ return processDefinitionList.value.filter((item) => item.category == categoryActive.value)
+// ========== 表单相关 ==========
+const detailForm = ref({
+const selectProcessDefinition = ref() // 选择的流程定义
+const bpmnXML = ref(null) // BPMN 数据
+const startUserSelectTasks = ref([]) // 发起人需要选择审批人的用户任务列表
+const startUserSelectAssigneesFormRef = ref() // 发起人选择审批人的表单 Ref
+const startUserSelectAssigneesFormRules = ref({}) // 发起人选择审批人的表单 Rules
+const userList = ref<any[]>([]) // 用户列表
+/** 处理选择流程的按钮操作 **/
+const handleSelect = async (row, formVariables) => {
+ // 设置选择的流程
+ selectProcessDefinition.value = row
+ startUserSelectAssigneesFormRules.value = {}
+ startUserSelectTasks.value = processDefinitionDetail.startUserSelectTasks
+ // 设置指定审批人
+ detailForm.value.rule.push({
+ type: 'startUserSelect',
+ props: {
+ title: '指定审批人'
+ // 设置校验规则
+ startUserSelectAssignees.value[userTask.id] = []
+ startUserSelectAssigneesFormRules.value[userTask.id] = [
+ { required: true, message: '请选择审批人', trigger: 'blur' }
+ // 加载用户列表
+const submitForm = async (formData) => {
+ if (!fApi.value || !selectProcessDefinition.value) {
+ await startUserSelectAssigneesFormRef.value.validate()
+ processDefinitionId: selectProcessDefinition.value.id,
+ variables: formData,
+/** 初始化 */
@@ -1,54 +1,61 @@
<el-card v-loading="loading" class="box-card">
- <template #header>
- <span class="el-icon-picture-outline">流程图</span>
- :activityData="activityList"
- :processInstanceData="processInstance"
- :taskData="tasks"
- :value="bpmnXml"
+ <MyProcessViewer key="designer" :xml="view.bpmnXml" :view="view" class="process-viewer" />
</el-card>
import { propTypes } from '@/utils/propTypes'
import { MyProcessViewer } from '@/components/bpmnProcessDesigner/package'
-import * as ActivityApi from '@/api/bpm/activity'
defineOptions({ name: 'BpmProcessInstanceBpmnViewer' })
- loading: propTypes.bool, // 是否加载中
- id: propTypes.string, // 流程实例的编号
- processInstance: propTypes.any, // 流程实例的信息
- tasks: propTypes.array, // 流程任务的数组
- bpmnXml: propTypes.string // BPMN XML
+ loading: propTypes.bool.def(false), // 是否加载中
+ bpmnXml: propTypes.string, // BPMN XML
+ modelView: propTypes.object
-const activityList = ref([]) // 任务列表
+const view = ref({
+ bpmnXml: ''
+}) // BPMN 流程图数据
/** 只有 loading 完成时,才去加载流程列表 */
- () => props.loading,
- async (value) => {
- if (value && props.id) {
- activityList.value = await ActivityApi.getActivityList({
- processInstanceId: props.id
+ () => props.modelView,
+ async (newModelView) => {
+ // 加载最新
+ if (newModelView) {
+ //@ts-ignore
+ view.value = newModelView
+/** 监听 bpmnXml */
+ () => props.bpmnXml,
+ (value) => {
+ view.value.bpmnXml = value
-<style>
.box-card {
width: 100%;
- margin-bottom: 20px;
+ margin-bottom: 0;
+ :deep(.el-card__body) {
+ padding: 0;
+ :deep(.process-viewer) {
+ height: 100% !important;
+ min-height: 100%;
+ width: 100%;
</style>
@@ -0,0 +1,168 @@
+ <div v-loading="loading" class="process-viewer-container">
+ <SimpleProcessViewer
+ :flow-node="simpleModel"
+ :tasks="tasks"
+ :process-instance="processInstance"
+ class="process-viewer"
+import { SimpleFlowNode, NodeType } from '@/components/SimpleProcessDesignerV2/src/consts'
+import { SimpleProcessViewer } from '@/components/SimpleProcessDesignerV2/src/'
+defineOptions({ name: 'BpmProcessInstanceSimpleViewer' })
+ modelView: propTypes.object,
+ simpleJson: propTypes.string // Simple 模型结构数据 (json 格式)
+const simpleModel = ref()
+// 用户任务
+const tasks = ref([])
+// 流程实例
+const processInstance = ref()
+/** 监控模型视图 包括任务列表、进行中的活动节点编号等 */
+ tasks.value = newModelView.tasks
+ processInstance.value = newModelView.processInstance
+ // 已经拒绝的活动节点编号集合,只包括 UserTask
+ const rejectedTaskActivityIds: string[] = newModelView.rejectedTaskActivityIds
+ // 进行中的活动节点编号集合, 只包括 UserTask
+ const unfinishedTaskActivityIds: string[] = newModelView.unfinishedTaskActivityIds
+ // 已经完成的活动节点编号集合, 包括 UserTask、Gateway 等
+ const finishedActivityIds: string[] = newModelView.finishedTaskActivityIds
+ // 已经完成的连线节点编号集合,只包括 SequenceFlow
+ const finishedSequenceFlowActivityIds: string[] = newModelView.finishedSequenceFlowActivityIds
+ setSimpleModelNodeTaskStatus(
+ newModelView.simpleModel,
+ newModelView.processInstance.status,
+ rejectedTaskActivityIds,
+ finishedActivityIds,
+ finishedSequenceFlowActivityIds
+ simpleModel.value = newModelView.simpleModel
+/** 监控模型结构数据 */
+ () => props.simpleJson,
+ async (value) => {
+ if (value) {
+ simpleModel.value = JSON.parse(value)
+const setSimpleModelNodeTaskStatus = (
+ simpleModel: SimpleFlowNode | undefined,
+ processStatus: number,
+ rejectedTaskActivityIds: string[],
+ unfinishedTaskActivityIds: string[],
+ finishedActivityIds: string[],
+ finishedSequenceFlowActivityIds: string[]
+ if (!simpleModel) {
+ // 结束节点
+ if (simpleModel.type === NodeType.END_EVENT_NODE) {
+ if (finishedActivityIds.includes(simpleModel.id)) {
+ simpleModel.activityStatus = processStatus
+ simpleModel.activityStatus = TaskStatusEnum.NOT_START
+ // 审批节点
+ simpleModel.type === NodeType.START_USER_NODE ||
+ simpleModel.type === NodeType.USER_TASK_NODE
+ if (rejectedTaskActivityIds.includes(simpleModel.id)) {
+ simpleModel.activityStatus = TaskStatusEnum.REJECT
+ } else if (unfinishedTaskActivityIds.includes(simpleModel.id)) {
+ simpleModel.activityStatus = TaskStatusEnum.RUNNING
+ } else if (finishedActivityIds.includes(simpleModel.id)) {
+ simpleModel.activityStatus = TaskStatusEnum.APPROVE
+ // TODO 是不是还缺一个 cancel 的状态
+ // 抄送节点
+ if (simpleModel.type === NodeType.COPY_TASK_NODE) {
+ // 抄送节点 只有通过和未执行状态
+ // 条件节点 对应 SequenceFlow
+ if (simpleModel.type === NodeType.CONDITION_NODE) {
+ // 条件节点。只有通过和未执行状态
+ if (finishedSequenceFlowActivityIds.includes(simpleModel.id)) {
+ // 网关节点
+ simpleModel.type === NodeType.CONDITION_BRANCH_NODE ||
+ simpleModel.type === NodeType.PARALLEL_BRANCH_NODE ||
+ simpleModel.type === NodeType.INCLUSIVE_BRANCH_NODE
+ // 网关节点。只有通过和未执行状态
+ simpleModel.conditionNodes?.forEach((node) => {
+ node,
+ processStatus,
+ simpleModel.childNode,
+.process-viewer-container {
@@ -1,85 +1,50 @@
- <el-card v-loading="loading" class="box-card">
- <span class="el-icon-picture-outline">审批记录</span>
- <el-col :offset="3" :span="17">
- <div class="block">
- <el-timeline>
- <el-timeline-item
- v-if="processInstance.endTime"
- :type="getProcessInstanceTimelineItemType(processInstance)"
- <p style="font-weight: 700">
- 结束流程:在 {{ formatDate(processInstance?.endTime) }} 结束
- <dict-tag
- :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS"
- :value="processInstance.status"
- </p>
- </el-timeline-item>
- v-for="(item, index) in tasks"
- :type="getTaskTimelineItemType(item)"
- 审批任务:{{ item.name }}
- <dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="item.status" />
- v-if="!isEmpty(item.children)"
- @click="openChildrenTask(item)"
- size="small"
- <Icon icon="ep:memo" /> 子任务
- v-if="item.formId > 0"
- @click="handleFormDetail(item)"
- <Icon icon="ep:document" /> 查看表单
- <el-card :body-style="{ padding: '10px' }">
- <label v-if="item.assigneeUser" style="margin-right: 30px; font-weight: normal">
- 审批人:{{ item.assigneeUser.nickname }}
- <el-tag size="small" type="info">{{ item.assigneeUser.deptName }}</el-tag>
- </label>
- <label v-if="item.createTime" style="font-weight: normal">创建时间:</label>
- <label style="font-weight: normal; color: #8a909c">
- {{ formatDate(item?.createTime) }}
- <label v-if="item.endTime" style="margin-left: 30px; font-weight: normal">
- 审批时间:
- <label v-if="item.endTime" style="font-weight: normal; color: #8a909c">
- {{ formatDate(item?.endTime) }}
- <label v-if="item.durationInMillis" style="margin-left: 30px; font-weight: normal">
- 耗时:
- <label v-if="item.durationInMillis" style="font-weight: normal; color: #8a909c">
- {{ formatPast2(item?.durationInMillis) }}
- <p v-if="item.reason"> 审批建议:{{ item.reason }} </p>
- <el-timeline-item type="success">
- 发起流程:【{{ processInstance.startUser?.nickname }}】在
- {{ formatDate(processInstance?.startTime) }} 发起【 {{ processInstance.name }} 】流程
- </el-timeline>
+ <el-table :data="tasks" border header-cell-class-name="table-header-gray">
+ <el-table-column label="审批节点" prop="name" min-width="120" align="center" />
+ <el-table-column align="center" label="审批建议" prop="reason" min-width="200">
+ {{ scope.row.reason }}
+ v-if="scope.row.formId > 0"
+ <Icon icon="ep:document" /> 查看表单
+ <el-table-column align="center" label="耗时" prop="durationInMillis" min-width="100">
- <!-- 弹窗:子任务 -->
- <TaskSignList ref="taskSignListRef" @success="refresh" />
<!-- 弹窗:表单 -->
<Dialog title="表单详情" v-model="taskFormVisible" width="600">
<form-create
@@ -91,61 +56,20 @@
-import { formatDate, formatPast2 } from '@/utils/formatTime'
import { DICT_TYPE } from '@/utils/dict'
-import TaskSignList from './dialog/TaskSignList.vue'
import type { ApiAttrs } from '@form-create/element-ui/types/config'
import { setConfAndFields2 } from '@/utils/formCreate'
+import * as TaskApi from '@/api/bpm/task'
defineOptions({ name: 'BpmProcessInstanceTaskList' })
-defineProps({
- processInstance: propTypes.object, // 流程实例
- tasks: propTypes.arrayOf(propTypes.object) // 流程任务的数组
+ id: propTypes.string // 流程实例的编号
-/** 获得流程实例对应的颜色 */
-const getProcessInstanceTimelineItemType = (item: any) => {
- if (item.status === 2) {
- return 'success'
- if (item.status === 3) {
- return 'danger'
- if (item.status === 4) {
- return 'warning'
-/** 获得任务对应的颜色 */
-const getTaskTimelineItemType = (item: any) => {
- if ([0, 1, 6, 7].includes(item.status)) {
- return 'primary'
- return 'info'
- if (item.status === 5) {
-/** 子任务 */
-const taskSignListRef = ref()
-const openChildrenTask = (item: any) => {
- taskSignListRef.value.open(item)
+const tasks = ref([]) // 流程任务的数组
/** 查看表单 */
const fApi = ref<ApiAttrs>() // form-create 的 API 操作类
@@ -155,7 +79,7 @@ const taskForm = ref({
value: {}
}) // 流程任务的表单详情
const taskFormVisible = ref(false)
setConfAndFields2(taskForm, row.formConf, row.formFields, row.formVariables)
// 弹窗打开
@@ -167,9 +91,13 @@ const handleFormDetail = async (row) => {
fApi.value?.fapi?.disabled(true)
-/** 刷新数据 */
-const emit = defineEmits(['refresh']) // 定义 success 事件,用于操作成功后的回调
-const refresh = () => {
- emit('refresh')
+/** 只有 loading 完成时,才去加载流程列表 */
+ () => props.loading,
+ tasks.value = await TaskApi.getTaskListByProcessInstanceId(props.id)
@@ -3,155 +3,189 @@
<el-timeline class="pt-20px">
<!-- 遍历每个审批节点 -->
<el-timeline-item
- v-for="(activity, index) in approveNodes"
+ v-for="(activity, index) in activityNodes"
size="large"
:icon="getApprovalNodeIcon(activity.status, activity.nodeType)"
:color="getApprovalNodeColor(activity.status)"
- <div class="flex flex-col items-start">
- <div class="font-bold"> {{ activity.name }}</div>
- <div class="flex items-center mt-1">
- <!-- 情况一:遍历每个审批节点下的【进行中】task 任务 -->
- <div v-for="(task, idx) in activity.tasks" :key="idx" class="flex items-center">
- <div class="flex items-center flex-col pr-2">
- <div class="position-relative" v-if="task.assigneeUser || task.ownerUser">
- <!-- 信息:头像 -->
- <el-avatar
- :size="36"
- v-if="task.assigneeUser && task.assigneeUser.avatar"
- :src="task.assigneeUser.avatar"
- <el-avatar v-else-if="task.assigneeUser && task.assigneeUser.nickname">
- {{ task.assigneeUser.nickname.substring(0, 1) }}
- </el-avatar>
- v-else-if="task.ownerUser && task.ownerUser.avatar"
- :src="task.ownerUser.avatar"
- <el-avatar v-else-if="task.ownerUser && task.ownerUser.nickname">
- {{ task.ownerUser.nickname.substring(0, 1) }}
- <!-- 信息:任务 ICON -->
- <div
- class="position-absolute top-26px left-26px bg-#fff rounded-full flex items-center p-2px"
- <Icon
- :size="12"
- :icon="statusIconMap2[task.status]?.icon"
- :color="statusIconMap2[task.status]?.color"
- <div class="flex flex-col mt-1">
- <!-- 信息:昵称 -->
- v-if="task.assigneeUser && task.assigneeUser.nickname"
- class="text-10px text-align-center"
- {{ task.assigneeUser.nickname }}
- v-else-if="task.ownerUser && task.ownerUser.nickname"
- {{ task.ownerUser.nickname }}
- <!-- TODO @jason:审批意见,要展示哈。 -->
- <!-- <div v-if="task.reason" :title="task.reason" class="text-13px text-truncate w-150px mt-1"> 审批意见: {{ task.reason }}</div> -->
+ <template #dot>
+ class="position-absolute left--10px top--6px rounded-full border border-solid border-#dedede w-30px h-30px flex justify-center items-center bg-#3f73f7 p-5px"
+ <img class="w-full h-full" :src="getApprovalNodeImg(activity.nodeType)" alt="" />
+ v-if="showStatusIcon"
+ class="position-absolute top-17px left-17px rounded-full flex items-center p-1px border-2 border-white border-solid"
+ :style="{ backgroundColor: getApprovalNodeColor(activity.status) }"
+ <el-icon :size="11" color="#fff">
+ <component :is="getApprovalNodeIcon(activity.status, activity.nodeType)" />
+ </el-icon>
- <!-- 情况二:遍历每个审批节点下的【候选的】task 任务。例如说,1)依次审批,2)未来的审批任务等 -->
+ <div class="flex flex-col items-start gap2" :id="`activity-task-${activity.id}`">
+ <!-- 第一行:节点名称、时间 -->
+ <div class="flex w-full">
+ <div class="font-bold"> {{ activity.name }}</div>
+ <!-- 信息:时间 -->
+ v-if="activity.status !== TaskStatusEnum.NOT_START"
+ class="text-#a5a5a5 text-13px mt-1 ml-auto"
+ {{ getApprovalNodeTime(activity) }}
+ <!-- 需要自定义选择审批人 -->
+ class="flex flex-wrap gap2 items-center"
+ isEmpty(activity.tasks) &&
+ isEmpty(activity.candidateUsers) &&
+ CandidateStrategy.START_USER_SELECT === activity.candidateStrategy
+ <!-- && activity.nodeType === NodeType.USER_TASK_NODE -->
+ <el-tooltip content="添加用户" placement="left">
+ class="!px-6px"
+ @click="handleSelectUser(activity.id, customApproveUsers[activity.id])"
+ <img class="w-18px text-#ccc" src="@/assets/svgs/bpm/add-user.svg" alt="" />
- v-for="(user, idx1) in activity.candidateUserList"
+ v-for="(user, idx1) in customApproveUsers[activity.id]"
:key="idx1"
- class="flex items-center"
+ class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative"
- <div class="position-relative">
- <el-avatar :size="36" v-if="user.avatar" :src="user.avatar" />
- <el-avatar v-else-if="user.nickname && user.nickname">
- {{ user.nickname.substring(0, 1) }}
+ <el-avatar class="!m-5px" :size="28" v-if="user.avatar" :src="user.avatar" />
+ <el-avatar class="!m-5px" :size="28" v-else>
+ {{ user.nickname.substring(0, 1) }}
+ </el-avatar>
+ {{ user.nickname }}
+ <div v-else class="flex items-center flex-wrap mt-1 gap2">
+ <!-- 情况一:遍历每个审批节点下的【进行中】task 任务 -->
+ <div v-for="(task, idx) in activity.tasks" :key="idx" class="flex flex-col pr-2 gap2">
+ class="position-relative flex flex-wrap gap2"
+ v-if="task.assigneeUser || task.ownerUser"
+ <!-- 信息:头像昵称 -->
+ <template v-if="task.assigneeUser?.avatar || task.assigneeUser?.nickname">
+ <el-avatar
+ class="!m-5px"
+ :size="28"
+ v-if="task.assigneeUser?.avatar"
+ :src="task.assigneeUser?.avatar"
+ {{ task.assigneeUser?.nickname.substring(0, 1) }}
+ {{ task.assigneeUser?.nickname }}
+ <template v-else-if="task.ownerUser?.avatar || task.ownerUser?.nickname">
+ v-if="task.ownerUser?.avatar"
+ :src="task.ownerUser?.avatar"
+ {{ task.ownerUser?.nickname.substring(0, 1) }}
+ {{ task.ownerUser?.nickname }}
<!-- 信息:任务 ICON -->
+ v-if="showStatusIcon && onlyStatusIconShow.includes(task.status)"
+ class="position-absolute top-19px left-23px rounded-full flex items-center p-1px border-2 border-white border-solid"
+ :style="{ backgroundColor: statusIconMap2[task.status]?.color }"
- :icon="statusIconMap2['-1']?.icon"
- :color="statusIconMap2['-1']?.color"
- <div v-if="user.nickname" class="text-10px text-align-center">
- {{ user.nickname }}
+ <Icon :size="11" :icon="statusIconMap2[task.status]?.icon" color="#FFFFFF" />
+ <teleport defer :to="`#activity-task-${activity.id}`">
+ task.reason &&
+ [NodeType.USER_TASK_NODE, NodeType.END_EVENT_NODE].includes(activity.nodeType)
+ class="text-#a5a5a5 text-13px mt-1 w-full bg-#f8f8fa p2 rounded-md"
+ 审批意见:{{ task.reason }}
+ </teleport>
- <!-- 信息:时间 -->
- v-if="activity.status !== TaskStatusEnum.NOT_START"
- class="text-#a5a5a5 text-13px mt-1"
- {{ getApprovalNodeTime(activity) }}
- <!-- <div class="color-#a1a6ae text-12px mb-10px"> {{ activity.assigneeUser.nickname }}</div>
- <div v-if="activity.opinion" class="text-#a5a5a5 text-12px w-100%">
- <div class="mb-5px">审批意见:</div>
+ <!-- 情况二:遍历每个审批节点下的【候选的】task 任务。例如说,1)依次审批,2)未来的审批任务等 -->
- class="w-100% border-1px border-#a5a5a5 border-dashed rounded py-5px px-15px text-#2d2d2d"
+ v-for="(user, idx1) in activity.candidateUsers"
+ :key="idx1"
- {{ activity.opinion }}
+ <!-- 信息:任务 ICON -->
+ class="position-absolute top-20px left-24px rounded-full flex items-center p-1px border-2 border-white border-solid"
+ :style="{ backgroundColor: statusIconMap2['-1']?.color }"
+ <Icon :size="11" :icon="statusIconMap2['-1']?.icon" color="#FFFFFF" />
- <div v-if="activity.createTime" class="text-#a5a5a5 text-13px">
- {{ formatDate(activity.createTime) }}
</el-timeline-item>
</el-timeline>
+ <!-- 用户选择弹窗 -->
+ <UserSelectForm ref="userSelectFormRef" @confirm="handleUserSelectConfirm" />
import { formatDate } from '@/utils/formatTime'
import { TaskStatusEnum } from '@/api/bpm/task'
+import { isEmpty } from '@/utils/is'
import { Check, Close, Loading, Clock, Minus, Delete } from '@element-plus/icons-vue'
+import starterSvg from '@/assets/svgs/bpm/starter.svg'
+import auditorSvg from '@/assets/svgs/bpm/auditor.svg'
+import copySvg from '@/assets/svgs/bpm/copy.svg'
+import conditionSvg from '@/assets/svgs/bpm/condition.svg'
+import parallelSvg from '@/assets/svgs/bpm/parallel.svg'
+import finishSvg from '@/assets/svgs/bpm/finish.svg'
defineOptions({ name: 'BpmProcessInstanceTimeline' })
-const props = defineProps({
- // 流程实例编号
- processInstanceId: {
- required: false,
- // 流程定义编号
- processDefinitionId: {
+withDefaults(
+ defineProps<{
+ activityNodes: ProcessInstanceApi.ApprovalNodeInfo[] // 审批节点信息
+ showStatusIcon?: boolean // 是否显示头像右下角状态图标
+ }>(),
+ showStatusIcon: true // 默认值为 true
// 审批节点
-const approveNodes = ref<ProcessInstanceApi.ApprovalNodeInfo[]>([])
const statusIconMap2 = {
// 未开始
- '-1': { color: '#e5e7ec', icon: 'ep-clock' },
+ '-1': { color: '#909398', icon: 'ep-clock' },
// 待审批
- '0': { color: '#e5e7ec', icon: 'ep:loading' },
+ '0': { color: '#00b32a', icon: 'ep:loading' },
// 审批中
'1': { color: '#448ef7', icon: 'ep:loading' },
// 审批通过
@@ -160,7 +194,7 @@ const statusIconMap2 = {
'3': { color: '#f46b6c', icon: 'fa-solid:times-circle' },
// 取消
'4': { color: '#cccccc', icon: 'ep:delete-filled' },
- // 回退
+ // 退回
'5': { color: '#f46b6c', icon: 'ep:remove-filled' },
// 委派中
'6': { color: '#448ef7', icon: 'ep:loading' },
@@ -170,8 +204,8 @@ const statusIconMap2 = {
const statusIconMap = {
// 审批未开始
- '-1': { color: '#e5e7ec', icon: Clock },
- '0': { color: '#e5e7ec', icon: Clock },
+ '-1': { color: '#909398', icon: Clock },
+ '0': { color: '#00b32a', icon: Clock },
'1': { color: '#448ef7', icon: Loading },
@@ -180,7 +214,7 @@ const statusIconMap = {
'3': { color: '#f46b6c', icon: Close },
// 已取消
'4': { color: '#cccccc', icon: Delete },
'5': { color: '#f46b6c', icon: Minus },
'6': { color: '#448ef7', icon: Loading },
@@ -188,13 +222,27 @@ const statusIconMap = {
'7': { color: '#00b32a', icon: Check }
-/** 获得审批详情 */
-const getApprovalDetail = async () => {
- const data = await ProcessInstanceApi.getApprovalDetail(
- props.processInstanceId,
- props.processDefinitionId
- approveNodes.value = data.approveNodes
+const nodeTypeSvgMap = {
+ [NodeType.END_EVENT_NODE]: { color: '#909398', svg: finishSvg },
+ // 发起人节点
+ [NodeType.START_USER_NODE]: { color: '#909398', svg: starterSvg },
+ // 审批人节点
+ [NodeType.USER_TASK_NODE]: { color: '#ff943e', svg: auditorSvg },
+ // 抄送人节点
+ [NodeType.COPY_TASK_NODE]: { color: '#3296fb', svg: copySvg },
+ // 条件分支节点
+ [NodeType.CONDITION_NODE]: { color: '#14bb83', svg: conditionSvg },
+ // 并行分支节点
+ [NodeType.PARALLEL_BRANCH_NODE]: { color: '#14bb83', svg: parallelSvg }
+// 只有只有状态是 -1、0、1 才展示头像右小角状态小icon
+const onlyStatusIconShow = [-1, 0, 1]
+// timeline时间线上icon图标
+const getApprovalNodeImg = (nodeType: NodeType) => {
+ return nodeTypeSvgMap[nodeType]?.svg
const getApprovalNodeIcon = (taskStatus: number, nodeType: NodeType) => {
@@ -202,7 +250,11 @@ const getApprovalNodeIcon = (taskStatus: number, nodeType: NodeType) => {
return statusIconMap[taskStatus]?.icon
- if (nodeType === NodeType.START_USER_NODE || nodeType === NodeType.USER_TASK_NODE) {
+ nodeType === NodeType.START_USER_NODE ||
+ nodeType === NodeType.USER_TASK_NODE ||
+ nodeType === NodeType.END_EVENT_NODE
@@ -212,22 +264,29 @@ const getApprovalNodeColor = (taskStatus: number) => {
const getApprovalNodeTime = (node: ProcessInstanceApi.ApprovalNodeInfo) => {
+ if (node.nodeType === NodeType.START_USER_NODE && node.startTime) {
+ return `${formatDate(node.startTime)}`
if (node.endTime) {
- return `结束时间:${formatDate(node.endTime)}`
+ return `${formatDate(node.endTime)}`
if (node.startTime) {
- return `创建时间:${formatDate(node.startTime)}`
-/** 重新刷新审批详情 */
- getApprovalDetail()
+// 选择自定义审批人
+const userSelectFormRef = ref()
+const handleSelectUser = (activityId, selectedList) => {
+ userSelectFormRef.value.open(activityId, selectedList)
+ selectUserConfirm: [id: any, userList: any[]]
+const customApproveUsers: any = ref({}) // key:activityId,value:用户列表
+// 选择完成
+const handleUserSelectConfirm = (activityId: string, userList: any[]) => {
+ customApproveUsers.value[activityId] = userList || []
+ emit('selectUserConfirm', activityId, userList)
-defineExpose({ refresh })
- await getApprovalDetail()
@@ -1,89 +0,0 @@
-<template>
- <Dialog v-model="dialogVisible" title="委派任务" width="500">
- ref="formRef"
- v-loading="formLoading"
- :model="formData"
- :rules="formRules"
- label-width="110px"
- <el-form-item label="接收人" prop="delegateUserId">
- <el-select v-model="formData.delegateUserId" clearable style="width: 100%">
- v-for="item in userList"
- :key="item.id"
- :label="item.nickname"
- :value="item.id"
- <el-form-item label="委派理由" prop="reason">
- <el-input v-model="formData.reason" clearable placeholder="请输入委派理由" />
- <template #footer>
- <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
- <el-button @click="dialogVisible = false">取 消</el-button>
- </Dialog>
-</template>
-<script lang="ts" setup>
-import * as TaskApi from '@/api/bpm/task'
-defineOptions({ name: 'BpmTaskDelegateForm' })
-const dialogVisible = ref(false) // 弹窗的是否展示
-const formLoading = ref(false) // 表单的加载中
-const formData = ref({
- id: '',
- delegateUserId: undefined,
- reason: ''
-const formRules = ref({
- delegateUserId: [{ required: true, message: '接收人不能为空', trigger: 'change' }],
- reason: [{ required: true, message: '委派理由不能为空', trigger: 'blur' }]
-const formRef = ref() // 表单 Ref
-/** 打开弹窗 */
-const open = async (id: string) => {
- dialogVisible.value = true
- resetForm()
- formData.value.id = id
- // 获得用户列表
-defineExpose({ open }) // 提供 openModal 方法,用于打开弹窗
-/** 提交表单 */
-const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
-const submitForm = async () => {
- // 校验表单
- if (!formRef) return
- const valid = await formRef.value.validate()
- if (!valid) return
- formLoading.value = true
- await TaskApi.delegateTask(formData.value)
- dialogVisible.value = false
- // 发送操作成功的事件
- emit('success')
- formLoading.value = false
-/** 重置表单 */
-const resetForm = () => {
- formData.value = {
- formRef.value?.resetFields()
@@ -1,90 +0,0 @@
- <Dialog v-model="dialogVisible" title="回退任务" width="500">
- <el-form-item label="退回节点" prop="targetTaskDefinitionKey">
- <el-select v-model="formData.targetTaskDefinitionKey" clearable style="width: 100%">
- v-for="item in returnList"
- :key="item.taskDefinitionKey"
- :label="item.name"
- :value="item.taskDefinitionKey"
- <el-form-item label="回退理由" prop="reason">
- <el-input v-model="formData.reason" clearable placeholder="请输入回退理由" />
-<script lang="ts" name="TaskRollbackDialogForm" setup>
-const message = useMessage() // 消息弹窗
- targetTaskDefinitionKey: undefined,
- targetTaskDefinitionKey: [{ required: true, message: '必须选择回退节点', trigger: 'change' }],
- reason: [{ required: true, message: '回退理由不能为空', trigger: 'blur' }]
-const returnList = ref([] as any)
- returnList.value = await TaskApi.getTaskListByReturn(id)
- if (returnList.value.length === 0) {
- message.warning('当前没有可回退的节点')
- return false
- await TaskApi.returnTask(formData.value)
- message.success('回退成功')
@@ -1,99 +0,0 @@
- <Dialog v-model="dialogVisible" title="加签" width="500">
- <el-form-item label="加签处理人" prop="userIds">
- <el-select v-model="formData.userIds" multiple clearable style="width: 100%">
- <el-form-item label="加签理由" prop="reason">
- <el-input v-model="formData.reason" clearable placeholder="请输入加签理由" />
- <el-button :disabled="formLoading" type="primary" @click="submitForm('before')">
- 向前加签
- <el-button :disabled="formLoading" type="primary" @click="submitForm('after')">
- 向后加签
-defineOptions({ name: 'TaskSignCreateForm' })
- userIds: [],
- type: '',
- userIds: [{ required: true, message: '加签处理人不能为空', trigger: 'change' }],
- reason: [{ required: true, message: '加签理由不能为空', trigger: 'change' }]
-const submitForm = async (type: string) => {
- formData.value.type = type
- await TaskApi.signCreateTask(formData.value)
- message.success('加签成功')
- <Dialog v-model="dialogVisible" title="减签" width="500">
- <el-form-item label="减签任务" prop="id">
- <el-radio-group v-model="formData.id">
- <el-radio-button v-for="item in childrenTaskList" :key="item.id" :value="item.id">
- {{ item.name }}
- ({{ item.assigneeUser?.deptName || item.ownerUser?.deptName }} -
- {{ item.assigneeUser?.nickname || item.ownerUser?.nickname }})
- </el-radio-button>
- </el-radio-group>
- <el-form-item label="减签理由" prop="reason">
- <el-input v-model="formData.reason" clearable placeholder="请输入减签理由" />
-defineOptions({ name: 'TaskSignDeleteForm' })
- id: [{ required: true, message: '必须选择减签任务', trigger: 'change' }],
- reason: [{ required: true, message: '减签理由不能为空', trigger: 'blur' }]
-const childrenTaskList = ref([])
- childrenTaskList.value = await TaskApi.getChildrenTaskList(id)
- if (isEmpty(childrenTaskList.value)) {
- message.warning('当前没有可减签的任务')
- await TaskApi.signDeleteTask(formData.value)
- message.success('减签成功')
@@ -1,106 +0,0 @@
- <el-drawer v-model="drawerVisible" title="子任务" size="880px">
- <!-- 当前任务 -->
- <h4>【{{ parentTask.name }} 】审批人:{{ parentTask?.assigneeUser?.nickname }}</h4>
- style="margin-left: 5px"
- v-if="isSignDeleteButtonVisible(parentTask)"
- @click="handleSignDelete(parentTask)"
- <Icon icon="ep:remove" /> 减签
- <!-- 子任务列表 -->
- <el-table :data="parentTask.children" style="width: 100%" row-key="id" border>
- <el-table-column prop="assigneeUser.nickname" label="审批人" min-width="100">
- {{ scope.row.assigneeUser?.nickname || scope.row.ownerUser?.nickname }}
- <el-table-column prop="assigneeUser.deptName" label="所在部门" min-width="100">
- {{ scope.row.assigneeUser?.deptName || scope.row.ownerUser?.deptName }}
- <el-table-column label="审批状态" prop="status" width="120">
- <dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="scope.row.status" />
- <el-table-column
- label="提交时间"
- align="center"
- prop="createTime"
- width="180"
- :formatter="dateFormatter"
- label="结束时间"
- prop="endTime"
- <el-table-column label="操作" prop="operation" width="90">
- v-if="isSignDeleteButtonVisible(scope.row)"
- @click="handleSignDelete(scope.row)"
- <!-- 减签 -->
- <TaskSignDeleteForm ref="taskSignDeleteFormRef" @success="handleSignDeleteSuccess" />
- </el-drawer>
-import { DICT_TYPE } from '@/utils/dict'
-import { dateFormatter } from '@/utils/formatTime'
-import TaskSignDeleteForm from './TaskSignDeleteForm.vue'
-defineOptions({ name: 'TaskSignList' })
-const drawerVisible = ref(false) // 抽屉的是否展示
-const parentTask = ref({} as any)
-const open = async (task: any) => {
- if (isEmpty(task.children)) {
- message.warning('该任务没有子任务')
- parentTask.value = task
- // 展开抽屉
- drawerVisible.value = true
-/** 发起减签 */
-const taskSignDeleteFormRef = ref()
-const handleSignDelete = (item: any) => {
- taskSignDeleteFormRef.value.open(item.id)
-const handleSignDeleteSuccess = () => {
- // 关闭抽屉
- drawerVisible.value = false
-/** 是否显示减签按钮 */
-const isSignDeleteButtonVisible = (task: any) => {
- return task && task.children && !isEmpty(task.children)
- <Dialog v-model="dialogVisible" title="转派任务" width="500">
- <el-form-item label="新审批人" prop="assigneeUserId">
- <el-select v-model="formData.assigneeUserId" clearable style="width: 100%">
- <el-form-item label="转派理由" prop="reason">
- <el-input v-model="formData.reason" clearable placeholder="请输入转派理由" />
-defineOptions({ name: 'TaskTransferForm' })
- assigneeUserId: undefined,
- assigneeUserId: [{ required: true, message: '新审批人不能为空', trigger: 'change' }],
- reason: [{ required: true, message: '转派理由不能为空', trigger: 'blur' }]
- await TaskApi.transferTask(formData.value)
@@ -1,222 +1,167 @@
- <!-- 审批信息 -->
- v-for="(item, index) in runningTasks"
- v-loading="processInstanceLoading"
- class="box-card"
- <span class="el-icon-picture-outline">审批任务【{{ item.name }}】</span>
- <el-col :offset="6" :span="16">
- :ref="'form' + index"
- :model="auditForms[index]"
- :rules="auditRule"
- label-width="100px"
- <el-form-item v-if="processInstance && processInstance.name" label="流程名">
- {{ processInstance.name }}
- <el-form-item v-if="processInstance && processInstance.startUser" label="流程发起人">
- {{ processInstance?.startUser.nickname }}
- <el-tag size="small" type="info">{{ processInstance?.startUser.deptName }}</el-tag>
- <el-card v-if="runningTasks[index].formId > 0" class="mb-15px !-mt-10px">
- <span class="el-icon-picture-outline">
- 填写表单【{{ runningTasks[index]?.formName }}】
- v-model="approveForms[index].value"
- v-model:api="approveFormFApis[index]"
- :option="approveForms[index].option"
- :rule="approveForms[index].rule"
- <el-form-item label="审批建议" prop="reason">
- v-model="auditForms[index].reason"
- placeholder="请输入审批建议"
- type="textarea"
- <el-form-item label="抄送人" prop="copyUserIds">
- <el-select v-model="auditForms[index].copyUserIds" multiple placeholder="请选择抄送人">
- v-for="itemx in userOptions"
- :key="itemx.id"
- :label="itemx.nickname"
- :value="itemx.id"
- <div style="margin-bottom: 20px; margin-left: 10%; font-size: 14px">
- <!-- TODO @jason:建议搞个 if 来判断,替代现有的 !item.buttonsSetting || item.buttonsSetting[OpsButtonType.APPROVE]?.enable -->
- type="success"
- v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.APPROVE]?.enable"
- @click="handleAudit(item, true)"
- <Icon icon="ep:select" />
- <!-- TODO @jason:这个也是类似哈,搞个方法来生成名字 -->
- {{
- item.buttonsSetting?.[OperationButtonType.APPROVE]?.displayName ||
- OPERATION_BUTTON_NAME.get(OperationButtonType.APPROVE)
- }}
- v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.REJECT]?.enable"
- @click="handleAudit(item, false)"
- <Icon icon="ep:close" />
- item.buttonsSetting?.[OperationButtonType.REJECT].displayName ||
- OPERATION_BUTTON_NAME.get(OperationButtonType.REJECT)
- v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.TRANSFER]?.enable"
- @click="openTaskUpdateAssigneeForm(item.id)"
- <Icon icon="ep:edit" />
- item.buttonsSetting?.[OperationButtonType.TRANSFER]?.displayName ||
- OPERATION_BUTTON_NAME.get(OperationButtonType.TRANSFER)
- v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.DELEGATE]?.enable"
- @click="handleDelegate(item)"
- <Icon icon="ep:position" />
- item.buttonsSetting?.[OperationButtonType.DELEGATE]?.displayName ||
- OPERATION_BUTTON_NAME.get(OperationButtonType.DELEGATE)
- v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.ADD_SIGN]?.enable"
- @click="handleSign(item)"
- <Icon icon="ep:plus" />
- item.buttonsSetting?.[OperationButtonType.ADD_SIGN]?.displayName ||
- OPERATION_BUTTON_NAME.get(OperationButtonType.ADD_SIGN)
- v-if="!item.buttonsSetting || item.buttonsSetting[OperationButtonType.RETURN]?.enable"
- @click="handleBack(item)"
+ <ContentWrap :bodyStyle="{ padding: '10px 20px 0' }" class="position-relative">
+ <img
+ class="position-absolute right-20px"
+ width="150"
+ :src="auditIconsMap[processInstance.status]"
+ alt=""
+ <div class="text-#878c93 h-15px">编号:{{ id }}</div>
+ <div class="flex items-center gap-5 mb-10px h-40px">
+ <div class="text-26px font-bold mb-5px">{{ processInstance.name }}</div>
+ <dict-tag
+ v-if="processInstance.status"
+ :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS"
+ :value="processInstance.status"
+ <div class="flex items-center gap-5 mb-10px text-13px h-35px">
+ class="bg-gray-100 h-35px rounded-3xl flex items-center p-8px gap-2 dark:color-gray-600"
- <Icon icon="ep:back" />
- item.buttonsSetting?.[OperationButtonType.RETURN]?.displayName ||
- OPERATION_BUTTON_NAME.get(OperationButtonType.RETURN)
+ v-if="processInstance?.startUser?.avatar"
+ :src="processInstance?.startUser?.avatar"
+ <el-avatar :size="28" v-else-if="processInstance?.startUser?.nickname">
+ {{ processInstance?.startUser?.nickname.substring(0, 1) }}
+ {{ processInstance?.startUser?.nickname }}
+ <div class="text-#878c93"> {{ formatDate(processInstance.startTime) }} 提交 </div>
- <!-- 申请信息 -->
- <el-card v-loading="processInstanceLoading" class="box-card">
- <span class="el-icon-document">申请信息【{{ processInstance.name }}】</span>
- <!-- 情况一:流程表单 -->
- <el-col v-if="processInstance?.processDefinition?.formType === 10" :offset="6" :span="16">
- <!-- 情况二:业务表单 -->
- <div v-if="processInstance?.processDefinition?.formType === 20">
- <BusinessFormComponent :id="processInstance.businessKey" />
+ <el-tab-pane label="审批详情" name="form">
+ <el-col :span="17" class="!flex !flex-col formCol">
+ v-loading="processInstanceLoading"
+ class="form-box flex flex-col mb-30px flex-1"
+ <!-- 情况一:流程表单 -->
+ <el-col v-if="processDefinition?.formType === 10">
+ <!-- 情况二:业务表单 -->
+ <div v-if="processDefinition?.formType === 20">
+ <BusinessFormComponent :id="processInstance.businessKey" />
+ <el-col :span="7">
+ <!-- 审批记录时间线 -->
+ <ProcessInstanceTimeline :activity-nodes="activityNodes" />
+ v-show="
+ processDefinition.modelType && processDefinition.modelType === BpmModelType.SIMPLE
+ :loading="processInstanceLoading"
+ :model-view="processModelView"
+ processDefinition.modelType && processDefinition.modelType === BpmModelType.BPMN
- <!-- 审批记录 -->
- <ProcessInstanceTaskList
- :loading="tasksLoad"
- :process-instance="processInstance"
- :tasks="tasks"
- @refresh="getTaskList"
+ <!-- 流转记录 -->
+ <el-tab-pane label="流转记录" name="record">
+ <ProcessInstanceTaskList :loading="processInstanceLoading" :id="id" />
- <!-- 高亮流程图 -->
- <ProcessInstanceBpmnViewer
- :id="`${id}`"
- :bpmn-xml="bpmnXml"
- :loading="processInstanceLoading"
+ <!-- 流转评论 TODO 待开发 -->
+ <el-tab-pane label="流转评论" name="comment" v-if="false">
+ <el-scrollbar> 流转评论 </el-scrollbar>
- <!-- 弹窗:转派审批人 -->
- <TaskTransferForm ref="taskTransferFormRef" @success="getDetail" />
- <!-- 弹窗:回退节点 -->
- <TaskReturnForm ref="taskReturnFormRef" @success="getDetail" />
- <!-- 弹窗:委派,将任务委派给别人处理,处理完成后,会重新回到原审批人手中-->
- <TaskDelegateForm ref="taskDelegateForm" @success="getDetail" />
- <!-- 弹窗:加签,当前任务审批人为A,向前加签选了一个C,则需要C先审批,然后再是A审批,向后加签B,A审批完,需要B再审批完,才算完成这个任务节点 -->
- <TaskSignCreateForm ref="taskSignCreateFormRef" @success="getDetail" />
+ <ProcessInstanceOperationButton
+ ref="operationButtonRef"
+ :process-definition="processDefinition"
+ :userOptions="userOptions"
+ @success="refresh"
-import { useUserStore } from '@/store/modules/user'
+import { registerComponent } from '@/utils/routerHelper'
-import * as DefinitionApi from '@/api/bpm/definition'
import ProcessInstanceBpmnViewer from './ProcessInstanceBpmnViewer.vue'
+import ProcessInstanceSimpleViewer from './ProcessInstanceSimpleViewer.vue'
import ProcessInstanceTaskList from './ProcessInstanceTaskList.vue'
-import TaskReturnForm from './dialog/TaskReturnForm.vue'
-import TaskDelegateForm from './dialog/TaskDelegateForm.vue'
-import TaskTransferForm from './dialog/TaskTransferForm.vue'
-import TaskSignCreateForm from './dialog/TaskSignCreateForm.vue'
-import { registerComponent } from '@/utils/routerHelper'
-import {
- OperationButtonType,
- OPERATION_BUTTON_NAME
-} from '@/components/SimpleProcessDesignerV2/src/consts'
+import ProcessInstanceOperationButton from './ProcessInstanceOperationButton.vue'
+import ProcessInstanceTimeline from './ProcessInstanceTimeline.vue'
+import { FieldPermissionType } from '@/components/SimpleProcessDesignerV2/src/consts'
+import runningSvg from '@/assets/svgs/bpm/running.svg'
+import approveSvg from '@/assets/svgs/bpm/approve.svg'
+import rejectSvg from '@/assets/svgs/bpm/reject.svg'
+import cancelSvg from '@/assets/svgs/bpm/cancel.svg'
defineOptions({ name: 'BpmProcessInstanceDetail' })
-const { query } = useRoute() // 查询参数
+ id: string // 流程实例的编号
+ taskId?: string // 任务编号
+ activityId?: string //流程活动编号,用于抄送查看
-const { proxy } = getCurrentInstance() as any
-const userId = useUserStore().getUser.id // 当前登录的编号
-const id = query.id as unknown as string // 流程实例的编号
const processInstanceLoading = ref(false) // 流程实例的加载中
const processInstance = ref<any>({}) // 流程实例
-const bpmnXml = ref('') // BPMN XML
-const tasksLoad = ref(true) // 任务的加载中
-const tasks = ref<any[]>([]) // 任务列表
-// ========== 审批信息 ==========
-const runningTasks = ref<any[]>([]) // 运行中的任务
-const auditForms = ref<any[]>([]) // 审批任务的表单
-const auditRule = reactive({
- reason: [{ required: true, message: '审批建议不能为空', trigger: 'blur' }]
-const approveForms = ref<any[]>([]) // 审批通过时,额外的补充信息
-const approveFormFApis = ref<ApiAttrs[]>([]) // approveForms 的 fAPi
+const processDefinition = ref<any>({}) // 流程定义
+const processModelView = ref<any>({}) // 流程模型视图
+const operationButtonRef = ref() // 操作按钮组件 ref
+const auditIconsMap = {
+ [TaskStatusEnum.RUNNING]: runningSvg,
+ [TaskStatusEnum.APPROVE]: approveSvg,
+ [TaskStatusEnum.REJECT]: rejectSvg,
+ [TaskStatusEnum.CANCEL]: cancelSvg
// ========== 申请信息 ==========
const fApi = ref<ApiAttrs>() //
@@ -226,134 +171,62 @@ const detailForm = ref({
}) // 流程实例的表单详情
-/** 监听 approveFormFApis,实现它对应的 form-create 初始化后,隐藏掉对应的表单提交按钮 */
- () => approveFormFApis.value,
- (value) => {
- value?.forEach((api) => {
- api.btn.show(false)
- api.resetBtn.show(false)
- {
- deep: true
-/** 处理审批通过和不通过的操作 */
-const handleAudit = async (task, pass) => {
- // 1.1 获得对应表单
- const index = runningTasks.value.indexOf(task)
- const auditFormRef = proxy.$refs['form' + index][0]
- // 1.2 校验表单
- const elForm = unref(auditFormRef)
- if (!elForm) return
- let valid = await elForm.validate()
- // 校验申请表单(可编辑字段)
- // TODO @jason:之前这里是 if (!fApi.value) return;针对业务表单的情况下,会导致没办法审核,可能要看下。我这里改了点,看看是不是还有别的地方兼容性
- if (fApi.value) {
- valid = await fApi.value.validate()
- // 2.1 提交审批
- id: task.id,
- reason: auditForms.value[index].reason,
- copyUserIds: auditForms.value[index].copyUserIds
- if (pass) {
- // 审批通过,并且有额外的 approveForm 表单,需要校验 + 拼接到 data 表单里提交
- const formCreateApi = approveFormFApis.value[index]
- if (formCreateApi) {
- await formCreateApi.validate()
- data.variables = approveForms.value[index].value
- // 获取表单可编辑字段的值
- if (fApi.value && task.fieldsPermission !== null) {
- data.variables = getWritableValueOfForm(task.fieldsPermission)
- await TaskApi.approveTask(data)
- message.success('审批通过成功')
- await TaskApi.rejectTask(data)
- message.success('审批不通过成功')
- // 2.2 加载最新数据
- getDetail()
-/** 转派审批人 */
-const taskTransferFormRef = ref()
-const openTaskUpdateAssigneeForm = (id: string) => {
- taskTransferFormRef.value.open(id)
-/** 处理审批退回的操作 */
-const taskDelegateForm = ref()
-const handleDelegate = async (task) => {
- taskDelegateForm.value.open(task.id)
-const taskReturnFormRef = ref()
-const handleBack = async (task: any) => {
- taskReturnFormRef.value.open(task.id)
-/** 处理审批加签的操作 */
-const taskSignCreateFormRef = ref()
-const handleSign = async (task: any) => {
- taskSignCreateFormRef.value.open(task.id)
/** 获得详情 */
-const getDetail = async () => {
- // 1. 获得流程任务列表(审批记录)。 需要先获取任务,表单的权限设置需要根据任务来设置
- await getTaskList()
- // 2. 获得流程实例相关
- getProcessInstance()
+const getDetail = () => {
+ getApprovalDetail()
+ getProcessModelView()
/** 加载流程实例 */
-const BusinessFormComponent = ref(null) // 异步组件
-const getProcessInstance = async () => {
+const BusinessFormComponent = ref<any>(null) // 异步组件
+const getApprovalDetail = async () => {
+ processInstanceLoading.value = true
- processInstanceLoading.value = true
- const data = await ProcessInstanceApi.getProcessInstance(id)
+ const param = {
+ processInstanceId: props.id,
+ activityId: props.activityId,
+ taskId: props.taskId
+ const data = await ProcessInstanceApi.getApprovalDetail(param)
if (!data) {
+ if (!data.processDefinition || !data.processInstance) {
message.error('查询不到流程信息!')
- processInstance.value = data
+ processInstance.value = data.processInstance
+ processDefinition.value = data.processDefinition
// 设置表单信息
- const processDefinition = data.processDefinition
- if (processDefinition.formType === 10) {
+ if (processDefinition.value.formType === 10) {
+ // 获取表单字段权限
+ const formFieldsPermission = data.formFieldsPermission
if (detailForm.value.rule.length > 0) {
- detailForm.value.value = data.formVariables
+ // 避免刷新 form-create 显示不了
+ detailForm.value.value = processInstance.value.formVariables
setConfAndFields2(
detailForm,
- processDefinition.formConf,
- processDefinition.formFields,
- data.formVariables
+ processDefinition.value.formConf,
+ processDefinition.value.formFields,
+ processInstance.value.formVariables
nextTick().then(() => {
fApi.value?.btn.show(false)
fApi.value?.resetBtn.show(false)
fApi.value?.disabled(true)
- // 设置表单权限。后续需要改造成。只处理一个运行中的任务
- if (runningTasks.value.length > 0) {
- const task = runningTasks.value.at(0)
- if (task.fieldsPermission) {
- Object.keys(task.fieldsPermission).forEach((item) => {
- setFieldPermission(item, task.fieldsPermission[item])
+ // 设置表单字段权限
+ if (formFieldsPermission) {
+ Object.keys(data.formFieldsPermission).forEach((item) => {
+ setFieldPermission(item, formFieldsPermission[item])
@@ -361,118 +234,61 @@ const getProcessInstance = async () => {
BusinessFormComponent.value = registerComponent(data.processDefinition.formCustomViewPath)
- bpmnXml.value = (
- await DefinitionApi.getProcessDefinition(processDefinition.id as number)
- )?.bpmnXml
- processInstanceLoading.value = false
-/** 加载任务列表 */
-const getTaskList = async () => {
- runningTasks.value = []
- auditForms.value = []
- approveForms.value = []
- approveFormFApis.value = []
- // 获得未取消的任务
- tasksLoad.value = true
- const data = await TaskApi.getTaskListByProcessInstanceId(id)
- tasks.value = []
- // 1.1 移除已取消的审批
- data.forEach((task) => {
- if (task.status !== 4) {
- tasks.value.push(task)
- // 1.2 排序,将未完成的排在前面,已完成的排在后面;
- tasks.value.sort((a, b) => {
- // 有已完成的情况,按照完成时间倒序
- if (a.endTime && b.endTime) {
- return b.endTime - a.endTime
- } else if (a.endTime) {
- return 1
- } else if (b.endTime) {
- return -1
- // 都是未完成,按照创建时间倒序
- return b.createTime - a.createTime
- // 获得需要自己审批的任务
- loadRunningTask(tasks.value)
+ // 获取待办任务显示操作按钮
+ operationButtonRef.value?.loadTodoTask(data.todoTask)
} finally {
- tasksLoad.value = false
+ processInstanceLoading.value = false
-/**
- * 设置 runningTasks 中的任务
-const loadRunningTask = (tasks) => {
- tasks.forEach((task) => {
- if (!isEmpty(task.children)) {
- loadRunningTask(task.children)
- // 2.1 只有待处理才需要
- if (task.status !== 1 && task.status !== 6) {
- // 2.2 自己不是处理人
- if (!task.assigneeUser || task.assigneeUser.id !== userId) {
- // 2.3 添加到处理任务
- runningTasks.value.push({ ...task })
- auditForms.value.push({
- reason: '',
- copyUserIds: []
- // 2.4 处理 approve 表单
- if (task.formId && task.formConf) {
- const approveForm = {}
- setConfAndFields2(approveForm, task.formConf, task.formFields, task.formVariables)
- approveForms.value.push(approveForm)
- approveForms.value.push({}) // 占位,避免为空
+/** 获取流程模型视图*/
+const getProcessModelView = async () => {
+ if (BpmModelType.BPMN === processDefinition.value?.modelType) {
+ // 重置,解决 BPMN 流程图刷新不会重新渲染问题
+ processModelView.value = {
+ const data = await ProcessInstanceApi.getProcessInstanceBpmnModelView(props.id)
+ if (data) {
+ processModelView.value = data
+// 审批节点信息
+const activityNodes = ref<ProcessInstanceApi.ApprovalNodeInfo[]>([])
* 设置表单权限
const setFieldPermission = (field: string, permission: string) => {
- if (permission === '1') {
+ if (permission === FieldPermissionType.READ) {
fApi.value?.disabled(true, field)
- if (permission === '2') {
+ if (permission === FieldPermissionType.WRITE) {
fApi.value?.disabled(false, field)
- if (permission === '3') {
+ if (permission === FieldPermissionType.NONE) {
fApi.value?.hidden(true, field)
- * 获取可以编辑字段的值
+ * 操作成功后刷新
-const getWritableValueOfForm = (fieldsPermission: Object) => {
- const fieldsValue = {}
- if (fieldsPermission && fApi.value) {
- Object.keys(fieldsPermission).forEach((item) => {
- if (fieldsPermission[item] === '2') {
- fieldsValue[item] = fApi.value.getValue(item)
- return fieldsValue
+const refresh = () => {
+ // 重新获取详情
+ getDetail()
+/** 当前的Tab */
+const activeTab = ref('form')
const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
@@ -481,3 +297,50 @@ onMounted(async () => {
userOptions.value = await UserApi.getSimpleUserList()
+$process-header-height: 194px;
+ :deep(.box-card) {
+ flex: 1;
@@ -1,318 +0,0 @@
- <ContentWrap :bodyStyle="{ padding: '10px 20px' }" class="position-relative">
- <div class="processInstance-wrap-main">
- <el-scrollbar>
- <img
- class="position-absolute right-20px"
- width="150"
- :src="auditIcons[processInstance.status]"
- alt=""
- <div class="text-#878c93 h-15px">编号:{{ id }}</div>
- <el-divider class="!my-8px" />
- <div class="flex items-center gap-5 mb-10px h-40px">
- <div class="text-26px font-bold mb-5px">{{ processInstance.name }}</div>
- <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="processInstance.status" />
- <div class="flex items-center gap-5 mb-10px text-13px h-35px">
- class="bg-gray-100 h-35px rounded-3xl flex items-center p-8px gap-2 dark:color-gray-600"
- <img class="rounded-full h-28px" src="@/assets/imgs/avatar.jpg" alt="" />
- {{ processInstance?.startUser?.nickname }}
- <div class="text-#878c93"> {{ formatDate(processInstance.startTime) }} 提交 </div>
- <el-tabs v-model="activeTab">
- <!-- 表单信息 -->
- <el-tab-pane label="审批详情" name="form">
- <div class="form-scroll-area">
- <el-row :gutter="10">
- <el-col :span="18" class="!flex !flex-col formCol">
- class="form-box flex flex-col mb-30px flex-1"
- v-if="processInstance?.processDefinition?.formType === 10"
- :offset="6"
- :span="16"
- <el-col :span="6">
- <!-- 审批记录时间线 -->
- <ProcessInstanceTimeline ref="timelineRef" :process-instance-id="id" />
- </el-scrollbar>
- <!-- 流程图 -->
- <el-tab-pane label="流程图" name="diagram">
- <!-- 流转记录 -->
- <el-tab-pane label="流转记录" name="record">
- <!-- 流转评论 TODO 待开发 -->
- <el-tab-pane label="流转评论" name="comment"> 流转评论 </el-tab-pane>
- class="b-t-solid border-t-1px border-[var(--el-border-color)]"
- v-if="activeTab === 'form'"
- <!-- 操作栏按钮 -->
- <ProcessInstanceOperationButton
- ref="operationButtonRef"
- :processInstance="processInstance"
- :userOptions="userOptions"
- @success="refresh"
-import * as ProcessInstanceApi from '@/api/bpm/processInstance'
-import ProcessInstanceBpmnViewer from './ProcessInstanceBpmnViewer.vue'
-import ProcessInstanceTaskList from './ProcessInstanceTaskList.vue'
-import ProcessInstanceOperationButton from './ProcessInstanceOperationButton.vue'
-import ProcessInstanceTimeline from './ProcessInstanceTimeline.vue'
-import { FieldPermissionType } from '@/components/SimpleProcessDesignerV2/src/consts'
-import audit1 from '@/assets/svgs/bpm/audit1.svg'
-import audit2 from '@/assets/svgs/bpm/audit2.svg'
-import audit3 from '@/assets/svgs/bpm/audit3.svg'
-defineOptions({ name: 'BpmProcessInstanceDetail' })
-const props = defineProps<{
- id: string // 流程实例的编号
- taskId?: string // 任务编号
- activityId?: string //流程活动编号,用于抄送查看
-}>()
-const processInstanceLoading = ref(false) // 流程实例的加载中
-const processInstance = ref<any>({}) // 流程实例
-const operationButtonRef = ref()
-const timelineRef = ref()
-const auditIcons = {
- 1: audit1,
- 2: audit2,
- 3: audit3
-// ========== 申请信息 ==========
-const fApi = ref<ApiAttrs>() //
-}) // 流程实例的表单详情
-/** 获得详情 */
-const getDetail = () => {
- // 1. 获得流程实例相关
- // 2. 获得流程任务列表(审批记录)
- getTaskList()
-/** 加载流程实例 */
-const BusinessFormComponent = ref<any>(null) // 异步组件
- const data = await ProcessInstanceApi.getProcessInstance(props.id)
- if (!data) {
- message.error('查询不到流程信息!')
- // 设置表单信息
- // 获取表单字段权限
- let fieldsPermission = undefined
- if (props.taskId || props.activityId) {
- fieldsPermission = await ProcessInstanceApi.getFormFieldsPermission({
- processInstanceId: props.id,
- taskId: props.taskId,
- activityId: props.activityId
- setConfAndFields2(
- detailForm,
- nextTick().then(() => {
- fApi.value?.btn.show(false)
- fApi.value?.resetBtn.show(false)
- fApi.value?.disabled(true)
- if (fieldsPermission) {
- setFieldPermission(item, fieldsPermission[item])
- // 注意:data.processDefinition.formCustomViewPath 是组件的全路径,例如说:/crm/contract/detail/index.vue
- BusinessFormComponent.value = registerComponent(data.processDefinition.formCustomViewPath)
- bpmnXml.value = (await DefinitionApi.getProcessDefinition(processDefinition.id))?.bpmnXml
- * 设置表单权限
-const setFieldPermission = (field: string, permission: string) => {
- if (permission === FieldPermissionType.READ) {
- fApi.value?.disabled(true, field)
- if (permission === FieldPermissionType.WRITE) {
- fApi.value?.disabled(false, field)
- if (permission === FieldPermissionType.NONE) {
- fApi.value?.hidden(true, field)
- const data = await TaskApi.getTaskListByProcessInstanceId(props.id)
- data.forEach((task: any) => {
- operationButtonRef.value?.loadRunningTask(tasks.value)
- * 操作成功后刷新
- // 重新获取详情
- // 刷新审批详情 Timeline
- timelineRef.value?.refresh()
-/** 当前的Tab */
-const activeTab = ref('form')
-/** 初始化 */
-const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
- userOptions.value = await UserApi.getSimpleUserList()
-<style lang="scss" scoped>
-$wrap-padding-height: 30px;
-$wrap-margin-height: 15px;
-$button-height: 51px;
-$process-header-height: 194px;
-.processInstance-wrap-main {
- height: calc(
- 100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 45px
- );
- max-height: calc(
- .form-scroll-area {
- 100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 45px -
- $process-header-height - 40px
-.form-box {
- :deep(.el-card) {
- border: none;
@@ -10,7 +10,7 @@
:inline="true"
label-width="68px"
+ <el-form-item label="" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入流程名称"
@@ -19,21 +19,19 @@
class="!w-240px"
- <el-form-item label="所属流程" prop="processDefinitionKey">
- v-model="queryParams.processDefinitionKey"
- placeholder="请输入流程定义的标识"
+ <!-- TODO @ tuituji:style 可以使用 unocss -->
+ <el-form-item label="" prop="category" :style="{ position: 'absolute', right: '130px' }">
+ <!-- TODO @tuituji:应该选择好分类,就触发搜索啦。 -->
v-model="queryParams.category"
placeholder="请选择流程分类"
clearable
+ class="!w-155px"
v-for="category in categoryList"
@@ -43,43 +41,79 @@
- <el-form-item label="流程状态" prop="status">
- v-model="queryParams.status"
- placeholder="请选择流程状态"
- v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS)"
- :key="dict.value"
- :label="dict.label"
- :value="dict.value"
- <el-form-item label="发起时间" prop="createTime">
- <el-date-picker
- v-model="queryParams.createTime"
- value-format="YYYY-MM-DD HH:mm:ss"
- type="daterange"
- start-placeholder="开始日期"
- end-placeholder="结束日期"
- :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
- v-hasPermi="['bpm:process-instance:query']"
- @click="handleCreate(undefined)"
- <Icon icon="ep:plus" class="mr-5px" /> 发起流程
+ <!-- 高级筛选 -->
+ <el-form-item :style="{ position: 'absolute', right: '0px' }">
+ <el-button v-popover="popoverRef" v-click-outside="onClickOutside" :icon="List">
+ 高级筛选
+ <el-popover
+ ref="popoverRef"
+ trigger="click"
+ virtual-triggering
+ persistent
+ :width="400"
+ :show-arrow="false"
+ placement="bottom-end"
+ <el-form-item label="流程发起人" class="bold-label" label-position="top" prop="category">
+ placeholder="请选择流程发起人"
+ class="!w-390px"
+ label="所属流程"
+ class="bold-label"
+ label-position="top"
+ prop="processDefinitionKey"
+ v-model="queryParams.processDefinitionKey"
+ placeholder="请输入流程定义的标识"
+ <el-form-item label="流程状态" class="bold-label" label-position="top" prop="status">
+ v-model="queryParams.status"
+ placeholder="请选择流程状态"
+ v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ <el-form-item label="发起时间" class="bold-label" label-position="top" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="开始日期"
+ end-placeholder="结束日期"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ </el-popover>
+ <!-- TODO @tuituji:这里应该有确认,和取消、清空搜索条件,三个按钮。 -->
</el-form>
@@ -95,6 +129,8 @@
min-width="100"
fixed="left"
+ <!-- TODO @芋艿:摘要 -->
+ <!-- TODO @tuituji:流程状态。可见需求文档里 -->
<el-table-column label="流程状态" prop="status" width="120">
<template #default="scope">
<dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="scope.row.status" />
@@ -114,7 +150,7 @@
width="180"
:formatter="dateFormatter"
- <el-table-column align="center" label="耗时" prop="durationInMillis" width="160">
+ <!--<el-table-column align="center" label="耗时" prop="durationInMillis" width="160">
{{ scope.row.durationInMillis > 0 ? formatPast2(scope.row.durationInMillis) : '-' }}
@@ -126,7 +162,7 @@
</el-table-column>
- <el-table-column label="流程编号" align="center" prop="id" min-width="320px" />
+ -->
<el-table-column label="操作" align="center" fixed="right" width="180">
<el-button
@@ -162,11 +198,13 @@
+// TODO @tuituji:List 改成 <Icon icon="ep:plus" class="mr-5px" /> 类似这种组件哈。
+import { List } from '@element-plus/icons-vue'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-import { dateFormatter, formatPast2 } from '@/utils/formatTime'
+import { dateFormatter } from '@/utils/formatTime'
import { ProcessInstanceVO } from '@/api/bpm/processInstance'
@@ -189,7 +227,7 @@ const queryParams = reactive({
createTime: []
+const categoryList = ref<CategoryVO[]>([]) // 流程分类列表
@@ -222,7 +260,6 @@ const handleCreate = async (row?: ProcessInstanceVO) => {
const processDefinitionDetail = await DefinitionApi.getProcessDefinition(
row.processDefinitionId
if (processDefinitionDetail.formType === 20) {
message.error('重新发起流程失败,原因:该流程使用业务表单,不支持重新发起')
@@ -261,6 +298,15 @@ const handleCancel = async (row) => {
await getList()
+// TODO @tuituji:这个 import 是不是没用哈?
+import { ClickOutside as vClickOutside } from 'element-plus'
+// TODO @tuituji:onClickAdvancedSearch。方法名叫这个,会更好一些哇?打开高级搜索。
+const popoverRef = ref()
+const onClickOutside = () => {
+ unref(popoverRef).popperRef?.delayHide?.()
/** 激活时 **/
onActivated(() => {
@@ -272,3 +318,8 @@ onMounted(async () => {
+<style>
+.bold-label .el-form-item__label {
+ font-weight: bold; /* 将字体加粗 */
@@ -1,13 +1,19 @@
- <SimpleProcessDesigner :model-id="modelId" />
+ <ContentWrap :bodyStyle="{ padding: '20px 16px' }">
+ <SimpleProcessDesigner :model-id="modelId" @success="close" />
import { SimpleProcessDesigner } from '@/components/SimpleProcessDesignerV2/src/'
- name: 'SimpleWorkflowDesignEditor'
+ name: 'SimpleModelDesign'
+const router = useRouter() // 路由
const { query } = useRoute() // 路由的查询
const modelId = query.modelId as string
+const close = () => {
+ router.push({ path: '/bpm/manager/model' })
@@ -45,7 +45,12 @@
<el-table v-loading="loading" :data="list">
<el-table-column align="center" label="流程名" prop="processInstanceName" min-width="180" />
- <el-table-column align="center" label="流程发起人" prop="startUserName" min-width="100" />
+ label="流程发起人"
+ prop="startUser.nickname"
<el-table-column
align="center"
@@ -53,8 +58,11 @@
prop="processInstanceStartTime"
- <el-table-column align="center" label="抄送任务" prop="taskName" min-width="180" />
- <el-table-column align="center" label="抄送人" prop="creatorName" min-width="100" />
+ <el-table-column align="center" label="抄送节点" prop="activityName" min-width="180" />
+ <el-table-column align="center" label="抄送人" min-width="100">
+ <template #default="scope"> {{ scope.row.createUser?.nickname || '系统' }} </template>
+ <el-table-column align="center" label="抄送意见" prop="reason" width="150" />
label="抄送时间"
@@ -49,7 +49,11 @@ const designerConfig = ref({
@@ -26,9 +26,8 @@
v-model="queryParams.pickUpStoreId"
class="!w-280px"
placeholder="全部"
+ @change="handleQuery"
v-for="item in pickUpStoreList"
@@ -73,10 +72,22 @@
<Icon class="mr-5px" icon="ep:refresh" />
重置
- <el-button @click="handlePickup" type="success" plain v-hasPermi="['trade:order:pick-up']">
+ @click="handlePickup"
+ type="success"
+ v-hasPermi="['trade:order:pick-up']"
+ :disabled="isUse"
<Icon class="mr-5px" icon="ep:check" />
核销
+ <el-button type="primary" @click="connectToSerialPort" :disabled="serialPort || isUse">
+ 连接扫描枪
+ <el-button type="danger" @click="cutPort" :disabled="!serialPort || isUse">
+ 断开扫描枪
@@ -216,18 +227,20 @@ import { DeliveryTypeEnum } from '@/utils/constants'
import { TradeOrderSummaryRespVO } from '@/api/mall/trade/order'
import { DeliveryPickUpStoreVO } from '@/api/mall/trade/delivery/pickUpStore'
import OrderPickUpForm from '@/views/mall/trade/order/form/OrderPickUpForm.vue'
+import { ref, onMounted } from 'vue'
+import { useUserStore } from '@/store/modules/user'
+const port = ref('')
+const ports = ref([])
+const reader = ref('')
defineOptions({ name: 'PickUpOrder' })
-// 列表的加载中
-// 列表的总页数
-const total = ref(2)
-// 列表的数据
-const list = ref<TradeOrderApi.OrderVO[]>([])
-// 搜索的表单
-const queryFormRef = ref<FormInstance>()
-// 初始表单参数
+const total = ref(2) // 列表的总页数
+const list = ref<TradeOrderApi.OrderVO[]>([]) // 列表的数据
+const queryFormRef = ref<FormInstance>() // 搜索的表单
const INIT_QUERY_PARAMS = {
// 页数
pageNo: 1,
@@ -238,14 +251,15 @@ const INIT_QUERY_PARAMS = {
// 配送方式
deliveryType: DeliveryTypeEnum.PICK_UP.type,
// 自提门店
- pickUpStoreId: undefined
-// 表单搜索
-const queryParams = ref({ ...INIT_QUERY_PARAMS })
-// 订单搜索类型 queryParam
-const queryType = reactive({ queryParam: 'no' })
-// 订单统计数据
-const summary = ref<TradeOrderSummaryRespVO>()
+ pickUpStoreId: -1
+} // 初始表单参数
+const queryParams = ref({ ...INIT_QUERY_PARAMS }) // 表单搜索
+const queryType = reactive({ queryParam: 'no' }) // 订单搜索类型 queryParam
+const summary = ref<TradeOrderSummaryRespVO>() // 订单统计数据
+const serialPort = ref(false) // 是否连接扫码枪
+const isUse = ref(true) // 是否可核销
// 订单聚合搜索 select 类型配置(动态搜索)
const dynamicSearchList = ref([
@@ -294,13 +308,21 @@ const handleQuery = async () => {
const resetQuery = () => {
queryFormRef.value?.resetFields()
queryParams.value = { ...INIT_QUERY_PARAMS }
+ if (pickUpStoreList.value.length > 0) {
+ queryParams.value.pickUpStoreId = pickUpStoreList.value[0].id
handleQuery()
/** 自提门店精简列表 */
const pickUpStoreList = ref<DeliveryPickUpStoreVO[]>([])
const getPickUpStoreList = async () => {
- pickUpStoreList.value = await PickUpStoreApi.getListAllSimple()
+ pickUpStoreList.value = await PickUpStoreApi.getSimpleDeliveryPickUpStoreList()
+ // 移除自己无法核销的门店
+ const userId = useUserStore().getUser.id
+ pickUpStoreList.value = pickUpStoreList.value.filter((item) =>
+ item.verifyUserIds?.includes(userId)
/** 显示核销表单 */
@@ -309,10 +331,96 @@ const handlePickup = () => {
pickUpForm.value.open()
+/** 连接扫码枪 */
+const connectToSerialPort = async () => {
+ // 判断浏览器支持串口通信
+ 'serial' in navigator &&
+ navigator.serial != null &&
+ typeof navigator.serial === 'object' &&
+ 'requestPort' in navigator.serial
+ // 提示用户选择一个串口
+ port.value = await navigator.serial.requestPort()
+ message.error('浏览器不支持扫码枪连接,请更换浏览器重试')
+ // 获取用户之前授予该网站访问权限的所有串口。
+ ports.value = await navigator.serial.getPorts()
+ // console.log(port.value, ports.value);
+ // console.log(port.value)
+ // 等待串口打开
+ await port.value.open({ baudRate: 9600, dataBits: 8, stopBits: 2 })
+ // console.log(typeof port.value);
+ message.success('成功连接扫码枪')
+ serialPort.value = true
+ // readData(port.value);
+ readData()
+ } catch (error) {
+ // 处理连接串口出错的情况
+ console.log('Error connecting to serial port:', error)
+/** 监听扫码枪输入 */
+const readData = async () => {
+ reader.value = port.value.readable.getReader()
+ let data = '' //扫码数据
+ // 监听来自串口的数据
+ while (true) {
+ const { value, done } = await reader.value.read()
+ if (done) {
+ // 允许稍后关闭串口
+ reader.value.releaseLock()
+ // 获取发送的数据
+ const serialData = new TextDecoder().decode(value)
+ data = `${data}${serialData}`
+ if (serialData.includes('\r')) {
+ //读取结束
+ let codeData = data.replace('\r', '')
+ data = '' //清空下次读取不会叠加
+ console.log(`二维码数据:${codeData}`)
+ //处理拿到数据逻辑
+ pickUpForm.value.open(codeData)
+/** 断开扫码枪 */
+const cutPort = async () => {
+ if (port.value !== '') {
+ await reader.value.cancel()
+ await port.value.close()
+ port.value = ''
+ console.log('断开扫码枪连接')
+ message.success('已成功断开扫码枪连接')
+ serialPort.value = false
+ message.warning('请先连接或打开扫码枪')
- getList()
- getPickUpStoreList()
+ await getPickUpStoreList()
+ if (pickUpStoreList.value.length === 0) {
+ message.error('当前登录人没绑定任何自提点')
+ isUse.value = true
+ // 查询
+ isUse.value = false
<style lang="scss" scoped>
@@ -0,0 +1,143 @@
+ <Dialog :title="dialogTitle" v-model="dialogVisible" width="20%">
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="120px"
+ v-loading="formLoading"
+ <el-form-item label="门店名称" prop="name">
+ <el-input v-model="formData.name" placeholder="请输入门店名称" readonly />
+ <el-form-item label="门店店员" prop="verifyUserIds">
+ <el-button type="primary" @click="storeStaffTableSelect.open()">选择店员</el-button>
+ <!-- 店员列表 -->
+ <ContentWrap v-if="formData.verifyUsers.length > 0">
+ <el-table :data="formData.verifyUsers">
+ <el-table-column label="编号" align="center" prop="id" />
+ label="用户昵称"
+ prop="nickname"
+ :show-overflow-tooltip="true"
+ <el-table-column label="状态" align="center" key="status">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ <el-table-column align="center" label="操作">
+ v-hasPermi="['trade:delivery:pick-up-store:delete']"
+ @click="handleDelete(scope.row.id)"
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+ <!-- 选择员工弹窗 -->
+ <StoreStaffTableSelect ref="storeStaffTableSelect" @change="handleSelect" />
+import * as DeliveryPickUpStoreApi from '@/api/mall/trade/delivery/pickUpStore'
+import StoreStaffTableSelect from './components/StoreStaffTableSelect.vue'
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formData = ref({
+ id: undefined,
+ name: '',
+ verifyUserIds: [],
+ verifyUsers: []
+const formRules = reactive({})
+const formRef = ref() // 表单 Ref
+const storeStaffTableSelect = ref() // 表单 Ref
+const open = async (id: number) => {
+ dialogTitle.value = '绑定自提门店员工'
+ formData.value = await DeliveryPickUpStoreApi.getDeliveryPickUpStore(id)
+/** 提交表单 */
+ // 校验表单
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ id: formData.value.id,
+ verifyUserIds: formData.value.verifyUsers.map((item: any) => item.id)
+ await DeliveryPickUpStoreApi.bindStoreStaffId(data)
+ message.success('绑定成功')
+/** 处理选择员工操作 */
+const handleSelect = (checkedUsers: []) => {
+ formData.value.verifyUsers = checkedUsers
+const handleDelete = async (id: number) => {
+ const index = formData.value.verifyUsers.findIndex((item: any) => {
+ if (item.id == id) {
+ return true
+ formData.value.verifyUsers.splice(index, 1)
+ formData.value = {
+ formRef.value?.resetFields()
@@ -0,0 +1,265 @@
+<!-- TODO 芋艿:这块后续抽个独立的组件出来 -->
+ <Dialog :title="dialogTitle" v-model="dialogVisible" width="60%">
+ <!-- 左侧部门树 -->
+ <el-col :span="4" :xs="24">
+ <DeptTree @node-click="handleDeptNodeClick" />
+ <el-col :span="20" :xs="24">
+ <!-- 搜索 -->
+ <el-form-item label="用户名称" prop="username">
+ v-model="queryParams.username"
+ placeholder="请输入用户名称"
+ <el-form-item label="手机号码" prop="mobile">
+ v-model="queryParams.mobile"
+ placeholder="请输入手机号码"
+ <el-form-item label="状态" prop="status">
+ placeholder="用户状态"
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ <el-form-item label="创建时间" prop="createTime">
+ type="datetimerange"
+ <el-button @click="handleQuery"><Icon icon="ep:search" />搜索</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" />重置</el-button>
+ <el-table-column width="55">
+ <template #header>
+ <el-checkbox
+ v-model="isCheckAll"
+ :indeterminate="isIndeterminate"
+ @change="handleCheckAll"
+ <template #default="{ row }">
+ v-model="checkedStatus[row.id]"
+ @change="(checked: boolean) => handleCheckOne(checked, row, true)"
+ <el-table-column label="用户编号" align="center" key="id" prop="id" />
+ label="用户名称"
+ prop="username"
+ label="部门"
+ key="deptName"
+ prop="deptName"
+ <el-table-column label="手机号码" align="center" prop="mobile" width="120" />
+ <el-table-column label="状态" key="status">
+ label="创建时间"
+ width="180"
+ <el-button type="primary" @click="handleEmitChange">确 定</el-button>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import DeptTree from '@/views/system/user/DeptTree.vue'
+// 是否全选
+const isCheckAll = ref(false)
+// 全选框是否处于中间状态:不是全部选中 && 任意一个选中
+const isIndeterminate = ref(false)
+// 选中的活动
+const checkedUsers = ref([])
+// 选中状态:key为用户ID,value为是否选中
+const checkedStatus = ref<Record<string, boolean>>({})
+const dialogTitle = '选择店员'
+const dialogVisible = ref(false)
+const list = ref([]) // 列表的数
+ username: undefined,
+ mobile: undefined,
+ status: undefined,
+ deptId: undefined,
+ roleId: 5,
+ createTime: []
+ const data = await UserApi.getUserPage(queryParams)
+ queryFormRef.value?.resetFields()
+const handleDeptNodeClick = async (row) => {
+ queryParams.deptId = row.id
+const open = async () => {
+/** 全选/全不选 */
+const handleCheckAll = (checked: boolean) => {
+ isCheckAll.value = checked
+ isIndeterminate.value = false
+ list.value.forEach((combinationActivity) => handleCheckOne(checked, combinationActivity, false))
+ * 选中一行
+ * @param checked 是否选中
+ * @param combinationActivity 活动
+ * @param isCalcCheckAll 是否计算全选
+const handleCheckOne = (checked: boolean, combinationActivity, isCalcCheckAll: boolean) => {
+ if (checked) {
+ checkedUsers.value.push(combinationActivity as never)
+ checkedStatus.value[combinationActivity.id] = true
+ const index = findCheckedIndex(combinationActivity)
+ if (index > -1) {
+ checkedUsers.value.splice(index, 1)
+ checkedStatus.value[combinationActivity.id] = false
+ isCheckAll.value = false
+ // 计算全选框状态
+ if (isCalcCheckAll) {
+ calculateIsCheckAll()
+// 查找活动在已选中活动列表中的索引
+const findCheckedIndex = (user) => checkedUsers.value.findIndex((item) => item.id === user.id)
+// 计算全选框状态
+const calculateIsCheckAll = () => {
+ isCheckAll.value = list.value.every((user) => checkedStatus.value[user.id])
+ // 计算中间状态:不是全部选中 && 任意一个选中
+ isIndeterminate.value =
+ !isCheckAll.value && list.value.some((user) => checkedStatus.value[user.id])
+/** 多选完成 */
+const handleEmitChange = () => {
+ // 关闭弹窗
+ emits('change', [...checkedUsers.value])
+/** 确认选择时的触发事件 */
+ change: [CombinationActivityApi: any]
@@ -93,7 +93,7 @@
prop="createTime"
- <el-table-column align="center" label="操作">
+ <el-table-column align="center" label="操作" min-width="110">
v-hasPermi="['trade:delivery:pick-up-store:update']"
@@ -103,6 +103,14 @@
编辑
+ v-hasPermi="['trade:delivery:pick-up-store:update']"
+ @click="openFormBind(scope.row.id)"
+ 绑定店员
v-hasPermi="['trade:delivery:pick-up-store:delete']"
link
@@ -115,12 +123,16 @@
</el-table>
<!-- 表单弹窗:添加/修改 -->
<DeliveryPickUpStoreForm ref="formRef" @success="getList" />
+ <!-- 表单弹窗:绑定店员 -->
+ <DeliveryPickUpStoreBindForm ref="formBindRef" />
<script lang="ts" name="DeliveryPickUpStore" setup>
import * as DeliveryPickUpStoreApi from '@/api/mall/trade/delivery/pickUpStore'
import DeliveryPickUpStoreForm from './PickUpStoreForm.vue'
+import DeliveryPickUpStoreBindForm from './DeliveryPickUpStoreBindForm.vue'
import { dateFormatter } from '@/utils/formatTime'
@@ -146,6 +158,11 @@ const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
+const formBindRef = ref()
+const openFormBind = (id?: number) => {
+ formBindRef.value.open(id)
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
@@ -13,7 +13,7 @@
<template #footer>
- <el-button type="primary" :disabled="formLoading" @click="getOrderByPickUpVerifyCode">
+ <el-button type="primary" :disabled="formLoading" @click="getOrderByPickUpVerifyCodeClick">
查询
<el-button @click="dialogVisible = false">取 消</el-button>
@@ -52,9 +52,14 @@ const formRef = ref() // 表单 Ref
const orderDetails = ref<OrderVO>({})
/** 打开弹窗 */
-const open = async () => {
+const open = async (pickUpVerifyCode: string) => {
resetForm()
+ if(pickUpVerifyCode != null){
+ formData.value.pickUpVerifyCode = pickUpVerifyCode;
+ await getOrderByPickUpVerifyCode()
+ }else{
@@ -83,18 +88,21 @@ const resetForm = () => {
-/** 查询核销码对应的订单 */
-const getOrderByPickUpVerifyCode = async () => {
+const getOrderByPickUpVerifyCodeClick = async () => {
// 校验表单
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
+/** 查询核销码对应的订单 */
+const getOrderByPickUpVerifyCode = async () => {
formLoading.value = true
const data = await TradeOrderApi.getOrderByPickUpVerifyCode(formData.value.pickUpVerifyCode)
formLoading.value = false
if (data?.deliveryType !== DeliveryTypeEnum.PICK_UP.type) {
- message.error('请输入正确的核销码')
+ message.error('未查询到订单')
if (data?.status !== TradeOrderStatusEnum.UNDELIVERED.status) {
@@ -351,7 +351,7 @@ const deliveryExpressList = ref<DeliveryExpressApi.DeliveryExpressVO[]>([]) //
deliveryExpressList.value = await DeliveryExpressApi.getSimpleDeliveryExpressList()
@@ -273,7 +273,7 @@ const openDetail = (id: number) => {
@@ -30,6 +30,9 @@
<el-form-item label="退款结果的回调地址" prop="refundNotifyUrl">
<el-input v-model="formData.refundNotifyUrl" placeholder="请输入退款结果的回调地址" />
+ <el-form-item label="转账结果的回调地址" prop="transferNotifyUrl">
+ <el-input v-model="formData.transferNotifyUrl" placeholder="请输入转账结果的回调地址" />
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" placeholder="请输入备注" />
@@ -62,7 +65,8 @@ const formData = ref({
status: CommonStatusEnum.ENABLE,
remark: undefined,
orderNotifyUrl: undefined,
- refundNotifyUrl: undefined
+ refundNotifyUrl: undefined,
+ transferNotifyUrl: undefined
name: [{ required: true, message: '应用名不能为空', trigger: 'blur' }],
@@ -126,6 +130,7 @@ const resetForm = () => {
refundNotifyUrl: undefined,
+ transferNotifyUrl: undefined,
appKey: undefined
@@ -257,7 +257,6 @@ const resetForm = (appId, code) => {
const fileBeforeUpload = (file, fileAccept) => {
let format = '.' + file.name.split('.')[1]
if (format !== fileAccept) {
message.error('请上传指定格式"' + fileAccept + '"文件')
return false