225 Commits
Author SHA1 Message Date
YunaiV 1cafde3877 V2.4.1 发布~ 2025-02-09 12:43:11 +08:00
YunaiV 10a698b9ad 【代码评审】全局:TableSelectForm 可能移除,暂时没用到 2025-02-09 08:27:12 +08:00
芋道源码 bffa42aec6 !687 fix: 修复定时任务模块,字段名称错误
Merge pull request !687 from 李恒/master
2025-02-08 23:46:41 +00:00
芋道源码 a39698d6ab !689 fix:修复重置密码接口错误导致管理员忘记密码功能异常
Merge pull request !689 from Baron.Bai/fix-admin-reset-password
2025-02-08 23:45:31 +00:00
YunaiV c6f70cce00 Merge branch 'feature/bpm' of https://gitee.com/yudaocode/yudao-ui-admin-vue3
# Conflicts:
#	src/views/system/menu/index.vue
2025-02-09 07:39:48 +08:00
芋道源码 f4a83c091d Merge pull request #125 from DevDengChao/feat/wechat-pay-channel-form-shortcut
feat: add shortcut to weichat pay
2025-02-09 07:38:12 +08:00
芋道源码 b0d4e39e68 Merge pull request #129 from zws-code/feature/bpm_fix_250206
Feature/bpm fix 250206
2025-02-09 07:02:02 +08:00
芋道源码 d40f1c3150 !686 fix: 代码评审修复
Merge pull request !686 from Lesan/feature/bpm-new
2025-02-08 22:56:32 +00:00
YunaiV 60af2c6e7e 【代码评审】BPM:触发器 - 表单修改 2025-02-09 06:56:04 +08:00
baron.bai e0880db65c fix:修复重置密码接口错误导致管理员忘记密码功能异常 2025-02-08 21:04:10 +08:00
jason e32fc81df6 【功能新增】新增修改表单数据触发器 2025-02-07 22:50:53 +08:00
zws 59842b09c9 fix: 解决 设计器审批节点设置历史数据无监听器报错问题 2025-02-06 09:09:45 +08:00
zws 9c2619851a feat: 设计器审批节点设置 开启下拉过滤方便选择 2025-02-06 09:09:08 +08:00
liheng 25f5b104ce fix: 修复定时任务模块,字段名称错误 2025-02-01 16:42:08 +08:00
LesanOuO 5d2605fcae fix: 代码评审修复 2025-01-27 09:34:15 +08:00
jason 5e801a8e63 fix: 【功能修复】自定义标题。流程发起人等设置问题 2025-01-26 17:35:02 +08:00
jason 62ad543163 Merge branch 'feature/bpm' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into feature/bpm 2025-01-26 14:34:47 +08:00
jason 503a35f44e 【缺陷修复】延迟器节点名称修复问题,延迟器节点高亮问题 2025-01-26 14:34:23 +08:00
YunaiV bd48210d7f 【代码评审】Bpm:数据报表 2025-01-26 14:13:04 +08:00
YunaiV 79a53412d6 【代码评审】Bpm:数据报表 2025-01-26 13:35:07 +08:00
jason f35f62d2f8 Merge branch 'feature/bpm' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into feature/bpm 2025-01-26 13:14:36 +08:00
jason 475385bd43 【功能新增】Http 请求触发器,增加返回值设置 2025-01-26 13:14:16 +08:00
芋道源码 cea84173da !685 feat: BPM-数据报表
Merge pull request !685 from Lesan/feature/bpm-报表
2025-01-26 04:59:47 +00:00
LesanOuO 00dfe00d46 feat: BPM-数据报表 2025-01-26 11:07:16 +08:00
YunaiV 5f5ae354d2 【功能修复】system:菜单管理列表,虚拟 table 的告警问题 2025-01-25 18:03:41 +08:00
YunaiV 410e257a47 【功能优化】Bpm:流程分类维护时,增加 description 2025-01-25 17:57:09 +08:00
YunaiV e726c1dcba 【代码评审】Bpm:数据报表 2025-01-25 17:16:48 +08:00
芋道源码 107b1d3778 !684 feat: BPM-报表
Merge pull request !684 from Lesan/feature/bpm-报表
2025-01-25 09:12:51 +00:00
芋道源码 473d681c11 !683 代码审查修改
Merge pull request !683 from Lesan/feature/bpm-代码审查
2025-01-25 09:02:00 +00:00
LesanOuO 52c393a04b feat: BPM-报表 2025-01-25 13:29:00 +08:00
YunaiV e9a4efbf6a Merge branch 'feature/bpm' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 2025-01-25 10:56:42 +08:00
LesanOuO 092a8acde4 feat: 抄送添加摘要 2025-01-25 10:41:43 +08:00
LesanOuO a88c86d44c feat: 摘要设置代码评审修改 2025-01-25 10:26:56 +08:00
LesanOuO d2954776a5 feat: 更改HttpRequestParam相关命名 2025-01-25 09:42:19 +08:00
YunaiV 7db5ac81ae 【代码评审】Bpm:触发器 2025-01-25 09:31:02 +08:00
jason e3db7d3014 Merge branch 'feature/bpm' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into feature/bpm 2025-01-24 23:36:43 +08:00
jason b7329b0c44 【功能新增】Simple 设计器新增触发器节点 2025-01-24 23:36:26 +08:00
YunaiV a61a405346 【代码评审】Bpm:更多设置-摘要设置 2025-01-24 20:18:36 +08:00
YunaiV 6c504adcbc Merge branch 'feature/bpm' of https://github.com/GoldenZqqq/yudao-ui-admin-vue3 into feature/bpm
# Conflicts:
#	src/views/bpm/model/CategoryDraggableModel.vue
2025-01-24 20:03:40 +08:00
YunaiV 8e8475ffdc 【代码评审】Bpm:更多设置-摘要设置 2025-01-24 19:59:02 +08:00
芋道源码 ab83cee50f !681 feat: BPM-更多设置-摘要设置
Merge pull request !681 from Lesan/feature/bpm-摘要设置
2025-01-24 11:30:14 +00:00
芋道源码 99a44e8c01 !682 fix: 服务任务无法启动
Merge pull request !682 from Lesan/bugfix/bpm-服务任务
2025-01-24 11:25:43 +00:00
LesanOuO 0bbf4c9143 fix: 服务任务无法启动 2025-01-24 13:58:52 +08:00
LesanOuO 9e26e902a7 fix: label拼写错误 2025-01-24 13:19:44 +08:00
LesanOuO 8ff63622ec feat: BPM-更多设置-摘要设置 2025-01-24 13:11:12 +08:00
GoldenZqqq 4319d86978 Revert "fix: 优化Simple设计器接口调用逻辑与去除loading效果"
This reverts commit b3a97b0889.
2025-01-24 11:13:09 +08:00
GoldenZqqq 7f50584a02 perf: 优化流程模型分类组件性能和类型定义 2025-01-24 10:30:57 +08:00
jason 7043dea354 【功能优化】替换 iconfont 图标 2025-01-23 22:12:22 +08:00
jason 1cb06911c7 【功能修复】修复控制台警告问题 2025-01-23 20:52:18 +08:00
YunaiV 3ae486c39f Merge branch 'devlop' of https://gitee.com/puhui999/yudao-ui-admin-vue3 into dev
# Conflicts:
#	src/views/system/menu/index.vue
2025-01-23 19:58:43 +08:00
YunaiV c514c799ff Merge branch 'master' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into dev 2025-01-23 19:43:55 +08:00
YunaiV 58455cd0a4 【代码评审】Bpm:模型列表的优化 2025-01-23 19:40:49 +08:00
YunaiV bf437421bb Merge branch 'feature/bpm_fix_250121' of https://github.com/zws-code/yudao-ui-admin-vue3-fix into feature/bpm 2025-01-23 19:33:21 +08:00
YunaiV 09e406618c 【代码评审】Bpm:更多设置-自定义标题 2025-01-23 19:19:35 +08:00
puhui999 5832fd687f 【代码优化】系统菜单管理使用虚拟列表优化加载速度 2025-01-23 16:32:01 +08:00
芋道源码 a96873ef5d !677 feat: BPM-更多设置-标题设置
Merge pull request !677 from Lesan/feature/bpm-标题设置
2025-01-23 04:57:23 +00:00
芋道源码 951ce1cf50 !675 fix: mall kefu
Merge pull request !675 from puhui999/devlop
2025-01-23 04:26:29 +00:00
LesanOuO 9495c2c63a feat: BPM-更多设置-标题设置 2025-01-23 12:05:52 +08:00
芋道源码 c435e2c9e8 !674 BPM:更多设置-自动去重
Merge pull request !674 from Lesan/feature/bpm-自动去重
2025-01-23 00:16:45 +00:00
芋道源码 b60693036d !671 BPM:更多设置-流程编码
Merge pull request !671 from Lesan/feature/bpm-流程编码
2025-01-23 00:16:04 +00:00
puhui999 c50f59d181 【缺陷修复】mall: 修复当客服消息为空时按回车键会发送空消息的 bug 2025-01-22 18:11:02 +08:00
puhui999 39106843f3 【缺陷修复】mall: 修复客服中心页面接收到消息时,重复调用了多次获取会话接口的问题 2025-01-22 18:10:51 +08:00
LesanOuO 15b0414604 fix: 自动去重枚举变更 2025-01-22 09:51:24 +08:00
DevDengChao 1b8430e2fc feat: add shortcut to weichat pay 2025-01-21 14:47:40 +08:00
芋道源码 051fc134cb !673 BPM-更多设置-自动去重
Merge pull request !673 from Lesan/feature/bpm-自动去重
2025-01-21 01:39:33 +00:00
zws c9c03bed13 perf: 模型列表 权限检查用computed 替代vue指令 减少检查次数 2025-01-21 09:24:12 +08:00
zws d710c9d27e perf: 添加权限Set 替换检查权限数组 提高检查权限性能 2025-01-21 09:23:37 +08:00
Lesan 0215e8d4a8 fix: 使用枚举BpmAutoApproveType 2025-01-21 08:27:06 +08:00
Lesan 0f58e968b1 feat: BPM-更多设置-自动去重 2025-01-21 08:25:09 +08:00
Lesan 3dbe74c1e9 feat: BPM-更多设置-流程编码 2025-01-20 13:36:42 +08:00
YunaiV 187fa08524 【代码评审】Bpm:更多设置-流程编码 2025-01-20 12:40:59 +08:00
YunaiV 0677173856 【代码优化】Bpm:更多设置-流程编码 2025-01-20 12:33:28 +08:00
芋道源码 e6765b7e61 !670 BPM-更多设置-流程编码
Merge pull request !670 from Lesan/feature/bpm-流程编码
2025-01-20 04:30:58 +00:00
GoldenZqqq 3d591912c2 Merge branch 'feature/bpm' of https://github.com/GoldenZqqq/yudao-ui-admin-vue3 into feature/bpm 2025-01-20 11:08:56 +08:00
Lesan 5efdd93789 feat: BPM-更多设置-流程编码 2025-01-20 11:02:20 +08:00
芋道源码 22eb2ccdbf !669 BPM:重命名RouterCondition->RouterSetting
Merge pull request !669 from Lesan/feature/bpm-路由分支
2025-01-20 01:15:59 +00:00
Lesan c3fb2a3ca1 fix: 重命名RouterCondition->RouterSetting 2025-01-20 08:40:00 +08:00
jason b047fd9929 【功能修复】条件节点配置问题修复 2025-01-19 22:42:05 +08:00
jason aeb096a311 冲突修改 Merge branch 'feature/bpm' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into feature/bpm 2025-01-19 20:45:27 +08:00
jason 42de3d9127 【功能优化】条件节点设置优化 2025-01-19 18:40:18 +08:00
YunaiV 2659165220 Merge branch 'dev' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 2025-01-19 18:29:42 +08:00
芋道源码 fef86bcc56 !666 【功能完善】MALL: 模板装修优惠券组件优惠券列表回显
Merge pull request !666 from puhui999/dev
2025-01-19 10:29:32 +00:00
YunaiV 70e6b54642 【代码优化】Bpm:提交人权限的 review 2025-01-19 18:28:45 +08:00
芋道源码 d4432acec3 !665 BPM:审批意见
Merge pull request !665 from Lesan/feature/bpm-审批意见
2025-01-19 10:24:35 +00:00
芋道源码 8330f61eef !667 BPM:更多设置-提交人权限
Merge pull request !667 from Lesan/feature/bpm-提交人权限
2025-01-19 10:23:15 +00:00
Lesan 9694ad0fa3 feat: 提交人权限-允许撤销审批中的申请 2025-01-19 16:43:30 +08:00
puhui999 5551c594ab 【功能完善】MALL: 模板装修优惠券组件优惠券列表回显 2025-01-19 13:45:56 +08:00
puhui999 391a1481c6 【代码优化】MALL: 修复模板装修时组件属性响应式丢失的问题 2025-01-19 13:44:04 +08:00
Lesan 82ee62cb14 feat: 审批意见 2025-01-19 10:28:54 +08:00
Lesan 506d87ec71 feat: 审批意见 2025-01-19 10:15:43 +08:00
YunaiV ced9ad6bba Merge remote-tracking branch 'origin/master' 2025-01-18 19:21:40 +08:00
YunaiV 34081915d4 Merge branch 'dev' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 2025-01-18 19:21:19 +08:00
芋道源码 a2ef21b27f !664 【代码优化】MALL: 修复模板装修时组件属性响应式丢失的问题
Merge pull request !664 from puhui999/dev
2025-01-18 11:16:23 +00:00
YunaiV fea238ef15 【代码优化】Bpm:编辑流程时,“谁可以发起”默认就是“全体” 2025-01-18 19:01:12 +08:00
YunaiV 7349a0d080 【代码优化】Bpm:编辑流程时,“流程管理员”默认就是“指定人员” 2025-01-18 18:51:13 +08:00
YunaiV 39da8118b8 【代码优化】Bpm:移除废弃的 ModelForm.vue 2025-01-18 18:49:18 +08:00
puhui999 625426566c 【代码优化】MALL: 修复模板装修时组件属性响应式丢失的问题 2025-01-18 17:47:10 +08:00
puhui999 ae0cc94da0 【代码优化】MALL: usePropertyForm 替换为 useVModel 实现 modelValue 代理 2025-01-18 16:20:09 +08:00
YunaiV 99790bae63 【功能评审】Bpm:复制流程 2025-01-17 19:49:39 +08:00
YunaiV 6b1c0e7369 Merge branch 'feature/bpm' of https://github.com/yudaocode/yudao-ui-admin-vue3 into feature/bpm 2025-01-17 19:38:28 +08:00
芋道源码 cde758d324 Merge pull request #123 from zws-code/feature/bpm_fix_250116
Feature/bpm fix 250116
2025-01-17 19:38:03 +08:00
芋道源码 43ebd50a42 !658 【功能修复】用户管理:如果没有修改权限,则禁用状态开关
Merge pull request !658 from jinmh/master
2025-01-17 11:37:20 +00:00
YunaiV f484f95de3 【功能评审】Bpm:清理进程 2025-01-17 19:32:57 +08:00
YunaiV b82959118e Merge branch 'feature/bpm' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into feature/bpm 2025-01-17 19:30:22 +08:00
芋道源码 121a6d85ff !661 BPM:流程清理
Merge pull request !661 from Lesan/feature/bpm-流程清理
2025-01-17 11:30:14 +00:00
YunaiV 2486f0d3e0 Merge branch 'feature/bpm-跳过表达式' of https://gitee.com/LesanOuO/yudao-ui-admin-vue3 into feature/bpm
# Conflicts:
#	src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json
2025-01-17 19:14:07 +08:00
YunaiV c8447fe4f7 【功能评审】Bpm:审批签名 2025-01-17 19:12:06 +08:00
芋道源码 e8e357b8a2 !659 BPM:签名功能完善
Merge pull request !659 from Lesan/feature/bpm-n
2025-01-17 11:09:22 +00:00
zws 5c0ee866a0 Merge remote-tracking branch 'refs/remotes/git-origin/feature/bpm' into feature/bpm_fix 2025-01-17 14:22:05 +08:00
Lesan 1af93e502d feat: 模型的流程数据清理功能 2025-01-17 13:59:29 +08:00
Lesan 4e0e8dd220 feat: bpmn跳过表达式 2025-01-17 09:57:45 +08:00
Lesan 9308eceb81 feat: 抽离UserTaskListener组件 2025-01-16 15:25:20 +08:00
Lesan 38578c3e50 feat: 获取分支中的节点 2025-01-16 14:55:32 +08:00
Lesan 69ccd83af3 feat: bpmn设计器添加UserTask签名配置 2025-01-16 14:40:14 +08:00
Lesan 37964e740f feat: 审批签名代码评审 2025-01-16 13:55:15 +08:00
YunaiV 8e5271a6d6 【功能评审】Bpm:审批签名 2025-01-16 13:01:06 +08:00
zws d52e779131 fix: 解决导入模型xml 数据没有保存问题 2025-01-16 10:18:20 +08:00
zws 66d8cf1233 fix: 解决bpmn设计器 初次创建流程key不一致问题 2025-01-16 10:10:46 +08:00
zws 84552a6253 fix: 解决导入模型json 数据没有保存问题 2025-01-16 09:29:34 +08:00
zws 9febee7abe perf: 用download.json 替换模型json下载 2025-01-16 09:29:11 +08:00
zws e44f48c0c4 perf: bpm模型复制优化 2025-01-16 09:27:30 +08:00
zws 2f7200abdb feat: 从vue-element-plus-admin项目 同步TagsView代码 实现设置路由标题功能 2025-01-16 09:26:38 +08:00
zws 9849a040c0 feat: 添加download.json()方法 2025-01-16 09:20:57 +08:00
芋道源码 753e44ccd0 !655 Simple设计器完善及优化
Merge pull request !655 from Lesan/feature/bpm-n
2025-01-15 13:14:58 +00:00
YunaiV e4fab335fd 【功能修复】ERP:采购订单的产品单价缺少为空校验 2025-01-15 21:10:03 +08:00
YunaiV c391ba7689 Merge branch 'master' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into feature/bpm 2025-01-15 20:50:22 +08:00
YunaiV 2b38c02165 Merge branch 'master' of https://github.com/yudaocode/yudao-ui-admin-vue3 2025-01-15 20:46:17 +08:00
芋道源码 d5d667353a Merge pull request #122 from yudaocode/revert-117-master
Revert "fix:Vite CJS Node API deprecated waring"
2025-01-15 20:46:02 +08:00
芋道源码 026b2f2ba5 Revert "fix:Vite CJS Node API deprecated waring" 2025-01-15 20:45:49 +08:00
芋道源码 ece0ccd6df !657 修复表单构建页面,json显示和复制问题,调整highlight废弃的方法
Merge pull request !657 from SamllNorth_Lee/form-build/dev
2025-01-15 12:42:49 +00:00
YunaiV 395c5862fe Merge branch 'master' of https://github.com/yudaocode/yudao-ui-admin-vue3 2025-01-15 20:33:22 +08:00
YunaiV c62833fcf6 Merge branch 'master' of https://github.com/yudaocode/yudao-ui-admin-vue3 into feature/bpm 2025-01-15 20:31:58 +08:00
芋道源码 954f2b6353 Merge pull request #117 from 2234839/master
fix:Vite CJS Node API deprecated waring
2025-01-15 20:31:41 +08:00
YunaiV 5676ac125d Merge branch 'master' of https://github.com/yudaocode/yudao-ui-admin-vue3 into feature/bpm 2025-01-15 20:31:04 +08:00
芋道源码 7e715dd03b Merge pull request #119 from edgexie/patch-1
fix: Update useRenderLayout.tsx add tagsView.value when rendertop
2025-01-15 20:30:44 +08:00
YunaiV 6fba4b7228 【代码评审】BPM:流程编辑 2025-01-15 20:25:07 +08:00
YunaiV d32a9a37bf Merge branch 'feature/bpm' of https://github.com/yudaocode/yudao-ui-admin-vue3 into feature/bpm
# Conflicts:
#	src/views/bpm/simple/SimpleModelDesign.vue
2025-01-15 19:13:48 +08:00
芋道源码 45cf482c74 Merge pull request #121 from zws-code/master_fix_bpm
perf: bpm 流程模型表单代码优化. 去掉大量watch 代码. 用依赖注入替换props 传递流程数据. 增加导入导出
2025-01-15 19:12:47 +08:00
YunaiV f1ac8ba622 Merge branch 'feature/bpm' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into feature/bpm 2025-01-15 19:08:03 +08:00
YunaiV 89c3af5207 Merge branch 'master' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into feature/bpm 2025-01-15 19:07:07 +08:00
zws b117431318 feat: bpm 添加流程模型复制功能 2025-01-15 15:04:23 +08:00
zws 8df285aefb fix: bpm 用onActivated替换onMounted
解决新增模型返回后不刷新新的数据
2025-01-15 14:57:23 +08:00
zws e76eda8af3 fix: 修复组件在keepAlive下刷新不触发onActivated钩子问题 2025-01-15 14:54:23 +08:00
jinmh716 86bb36fc2f 【功能修复】用户管理:如果没有修改权限,则禁用状态开关 2025-01-15 10:22:06 +08:00
lizhixian 0fa31be851 fix:表单构建页面,json显示和生成问题 2025-01-14 17:20:51 +08:00
lizhixian aba4705b8e fix:表单构建页面,json显示和生成问题 2025-01-14 17:17:30 +08:00
zws 4fe158b2ec perf: bpm 修复bpmn设计器不显示问题 2025-01-14 12:00:08 +08:00
zws 99fe894eda Merge remote-tracking branch 'refs/remotes/git-origin/master' into master_fix_bpm 2025-01-14 11:55:46 +08:00
Lesan 42c1618202 fix: 创建UserTask无signEnable字段 2025-01-14 09:12:53 +08:00
zws 3d8f1290c3 perf: bpm 流程模型表单代码优化. 去掉大量watch 代码. 用依赖注入替换props 传递流程数据. 增加导入导出 2025-01-14 08:56:36 +08:00
jason baa4116697 【功能修复】修复审批详情页面,控制台警告问题 2025-01-13 12:59:16 +08:00
GoldenZqqq b3a97b0889 fix: 优化Simple设计器接口调用逻辑与去除loading效果 2025-01-13 11:10:53 +08:00
EchoByte d896996f27 fix: Update useRenderLayout.tsx add tagsView.value when rendertop
if I set tagsview value `false` in app.ts initial. the content will have margin top style.
2025-01-10 16:31:57 +08:00
Lesan dc5aa6d8df feat: 完善监听器表单校验 2025-01-10 14:48:16 +08:00
Lesan 401481662f feat: 补充等式左右值form校验 2025-01-10 14:33:59 +08:00
Lesan 04f4f630ad feat: 完善UserTask审批签名 2025-01-09 13:50:08 +08:00
Lesan dd72d35fe4 feat: ConditionNodeConfig使用Condition组件 2025-01-09 10:16:46 +08:00
Lesan 96d1fcedde fix: ConditionRule后端无type、opName字段 2025-01-09 10:14:50 +08:00
Lesan a1933a781e fix: ConditionRule后端无type、opName字段 2025-01-09 09:50:08 +08:00
Lesan 777564673e fix: 检查所有route->router 2025-01-09 09:19:23 +08:00
Lesan 077dfdcddd Merge remote-tracking branch 'refs/remotes/yudao/feature/bpm' into feature/bpm-n 2025-01-09 09:13:54 +08:00
Lesan 4741f3737f fix: defaultFlowId由后端生成 2025-01-09 09:04:22 +08:00
YunaiV 77938c38ca 【代码评审】BPM:签名组件 2025-01-08 22:18:47 +08:00
芋道源码 d11a82a521 !654 Simple设计器功能完善
Merge pull request !654 from Lesan/feature/bpm-n
2025-01-08 14:12:37 +00:00
Lesan 0c433552f9 feat: Simple设计器-userTask-添加是否需要签名字段 2025-01-08 16:58:14 +08:00
Lesan 5fa2a78e84 feat: 添加监听器请求地址非空校验 2025-01-08 15:03:06 +08:00
Lesan 35a0e69cd4 chore: 部分文件更名 2025-01-08 10:51:47 +08:00
Lesan 2144034e1f fix: 使用ConditionType枚举类型 2025-01-08 10:18:12 +08:00
Lesan 53e6d6aa61 feat: 更换使用条件组件 2025-01-08 10:16:11 +08:00
崮生 7303bcd91a fix prettier 2025-01-08 01:48:48 +00:00
崮生 5e31eb8ebc fix typo 2025-01-08 01:22:44 +00:00
崮生 90fa5ffd81 fix:Vite CJS Node API deprecated waring 2025-01-08 01:18:50 +00:00
Lesan 099b81cb2d feat: 路由分支流程图展示高亮 2025-01-08 08:56:45 +08:00
Lesan e5f6020582 fix: routeGroups命名问题 2025-01-08 08:52:08 +08:00
Lesan a2759680f8 fix: 使用vue3-signature签名组件 2025-01-08 08:31:50 +08:00
YunaiV 72dcb0c1f7 【代码评审】BPM:签名组件 2025-01-07 22:42:24 +08:00
YunaiV 528ebf3ea3 Merge branch 'feature/bpm-n' of https://gitee.com/LesanOuO/yudao-ui-admin-vue3 into feature/bpm
# Conflicts:
#	src/components/SimpleProcessDesignerV2/src/consts.ts
2025-01-07 22:30:18 +08:00
YunaiV 3f9408bfbe 【代码评审】BPM:Simple 设计器 - 路由分支 2025-01-07 22:27:13 +08:00
Lesan 65eee157b9 feat: Simple设计器-监听器-表单使用选择器 2025-01-07 17:02:44 +08:00
Lesan 7683aa2e7d feat: Simple设计器-监听器 2025-01-07 16:33:16 +08:00
Lesan 0da3a9bf7b Merge remote-tracking branch 'yudao/feature/bpm' into feature/bpm-n 2025-01-07 13:34:45 +08:00
芋道源码 eca1c83c56 !648 Simple设计器-路由分支
Merge pull request !648 from Lesan/feature/bpm-路由分支
2025-01-07 04:53:16 +00:00
YunaiV 42a3d2efab 【代码评审】BPM:Simple 设计器 - userTask 监听器 2025-01-07 12:28:27 +08:00
Lesan 0168f9ff9d feat: 签名组件 2025-01-07 10:28:56 +08:00
芋道源码 1ebd37a0e9 !651 Simple设计器-监听器
Merge pull request !651 from Lesan/feature/bpm-监听器
2025-01-07 01:42:28 +00:00
Lesan 739a1c29a2 feat: Simple设计器-监听器 2025-01-06 17:25:59 +08:00
LesanOuO 0a15eca573 feat: 审批人设置一行三个 2025-01-06 12:49:55 +08:00
Lesan 3118d2abd9 feat: Simple设计器-路由分支 2025-01-06 11:22:59 +08:00
芋道源码 e9950929de !647 【修复】修复 npm 流程模拟插件 bpmn-js-token-simulation
Merge pull request !647 from 半栈幼儿员/hotfix/css
2025-01-05 08:40:26 +00:00
preschooler 63c9b8d163 🐞 fix:修复 npm 流程模拟插件 bpmn-js-token-simulation 2025-01-05 15:54:20 +08:00
jason be60018908 Merge branch 'feature/bpm' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into feature/bpm 2025-01-04 15:25:33 +08:00
jason d7e6523dad 【功能优化】延迟器节点的高亮显示 2025-01-04 15:24:38 +08:00
YunaiV 49fc9b5412 Merge branch 'feature/bpm' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 2025-01-04 15:24:09 +08:00
YunaiV 5198b2ee01 【代码优化】BPM:模型列表界面 2025-01-04 15:23:48 +08:00
芋道源码 4aa44abf25 !646 fix: Simple设计器修改不生效
Merge pull request !646 from Lesan/feature/bpm-n
2025-01-04 07:15:25 +00:00
YunaiV 2a9290ce86 【代码优化】支持通过短信重置后台密码 2025-01-04 11:41:40 +08:00
杨宇庆 65924bb921 !617 feat: 支持通过短信重置后台密码
* feat: 支持通过短信重置后台密码
2025-01-04 03:36:45 +00:00
YunaiV 791d59020a 【功能优化】菜单管理:调整宽度,避免过窄 2025-01-04 11:33:55 +08:00
YunaiV 09da6579b6 【功能移除】菜单管理:全部展开/关闭,没有使用场景,所以清理掉 2025-01-04 11:30:14 +08:00
YunaiV ce60f630c4 【功能优化】菜单管理:使用 el-table-v2 解决菜单过多后,存在卡顿的问题 2025-01-04 11:14:33 +08:00
LesanOuO a9b5a7f006 fix: Simple设计器修改不生效 2025-01-04 09:35:44 +08:00
芋道源码 ea133da1d8 !627 菜单管理使用虚拟化树形控件实现
Merge pull request !627 from Jason0z/feat-menu-tree-v2
2025-01-04 01:34:08 +00:00
YunaiV 101ed368c3 Merge branch 'dev' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 2025-01-04 09:33:06 +08:00
芋道源码 48e7e7e5c6 !629 feat: add hiddenSearch function to manage search visibility in RouterSearch component
Merge pull request !629 from AhJindeg/fix/router-serch
2025-01-04 01:32:33 +00:00
YunaiV 452d6024b0 【功能完善】文件列表: 添加复制链接功能 2025-01-04 09:31:28 +08:00
芋道源码 7b7ce96c66 !631 【功能完善】文件列表: 添加复制链接功能
Merge pull request !631 from jinmh/master
2025-01-04 01:30:37 +00:00
YunaiV e508da7ff5 632 【轻量级 PR】:update src/components/Echart/src/Echart.vue. 2025-01-04 09:25:43 +08:00
芋道源码 6b6d2ff806 !632 update src/components/Echart/src/Echart.vue.
Merge pull request !632 from ddlu/N/A
2025-01-04 01:25:13 +00:00
芋道源码 5fce1afe29 !636 修复菜单总是显示无效的问题
Merge pull request !636 from chenting/N/A
2025-01-04 01:15:21 +00:00
芋道源码 69f76705fb !637 升级 element-plus,修复 el-table-v2 滚动条 bug
Merge pull request !637 from chenting/N/A
2025-01-04 01:13:44 +00:00
芋道源码 166e95298b !634 修复菜单显示 Please set titile 的问题
Merge pull request !634 from chenting/master
2025-01-04 01:12:39 +00:00
芋道源码 39486d6696 !644 feat: ✨ add current year calculation to footer component
Merge pull request !644 from AhJindeg/feature/footer-currentYear
2025-01-04 00:24:28 +00:00
YunaiV 98774ecc6d 【代码评审】工作流:延迟器的支持 2025-01-03 20:00:47 +08:00
芋道源码 8bbc5e22d1 !645 Simple设计器-延迟器
Merge pull request !645 from Lesan/feature/bpm-延迟器
2025-01-03 11:19:19 +00:00
Lesan c3dd741d97 Merge branch 'feature/bpm-延迟器' of https://gitee.com/LesanOuO/yudao-ui-admin-vue3 into feature/bpm-延时器 2025-01-03 16:29:16 +08:00
Lesan f470e0d65b feat: Simple设计器-延时器 2025-01-03 16:20:06 +08:00
AhJindeg 700efd7ee2 feat: ✨ add current year calculation to footer component
- 根据"DRY"(Don't Repeat Yourself)原则,使用动态年份代替硬编码,确保年份始终保持最新
- Introduced a new computed property `currentYear` in `Footer.vue` to dynamically display the current year in the copyright notice.
- Updated the copyright span to use `currentYear` instead of a hardcoded year, enhancing maintainability and accuracy.
2025-01-03 15:20:04 +08:00
chenting 739e43a31e 升级 element-plus,修复 el-table-v2 滚动条 bug
Signed-off-by: chenting <1016830869@qq.com>
2025-01-02 08:58:47 +00:00
chenting 7faea8c341 修复菜单总是显示无效的问题
Signed-off-by: chenting <1016830869@qq.com>
2025-01-02 05:55:11 +00:00
LesanOuO 3607b1d468 fix: 当存在重复执行的节点时审批意见展示有误 2025-01-02 12:23:33 +08:00
chenting 2436075337 修复菜单显示 Please set titile 的问题
Signed-off-by: chenting <1016830869@qq.com>
2025-01-02 03:30:26 +00:00
ddlu b9326463c2 update src/components/Echart/src/Echart.vue.
echart 没有显示markLine、markPoint、markArea
如果延后到 vue显示页面加入此三行,线上会出错

Signed-off-by: ddlu <ludd@mail.ustc.edu.cn>
2024-12-28 07:40:57 +00:00
jinmh716 74b62bf05e 【功能完善】文件列表: 添加复制链接功能 2024-12-28 12:59:42 +08:00
AhJindeg d815c60340 feat: add hiddenSearch function to manage search visibility in RouterSearch component 2024-12-26 11:40:25 +08:00
zhuguojian 02fa679d62 feat:菜单列表改为虚拟化树形控件实现 2024-12-24 11:48:24 +08:00
127 changed files with 4333 additions and 2579 deletions

No files matched your search

+4 -3
View File
@@ -1,6 +1,6 @@
{
"name": "yudao-ui-admin-vue3",
"version": "2.4.0-snapshot",
"version": "2.4.1-snapshot",
"description": "基于vue3、vite4、element-plus、typesScript",
"author": "xingyu",
"private": false,
@@ -38,7 +38,7 @@
"animate.css": "^4.1.1",
"axios": "^1.6.8",
"benz-amr-recorder": "^1.1.5",
"bpmn-js-token-simulation": "^0.10.0",
"bpmn-js-token-simulation": "^0.36.0",
"camunda-bpmn-moddle": "^7.0.1",
"cropperjs": "^1.6.1",
"crypto-js": "^4.2.0",
@@ -47,7 +47,7 @@
"driver.js": "^1.3.1",
"echarts": "^5.5.0",
"echarts-wordcloud": "^2.1.0",
"element-plus": "2.8.4",
"element-plus": "2.9.1",
"fast-xml-parser": "^4.3.2",
"highlight.js": "^11.9.0",
"jsencrypt": "^3.3.2",
@@ -73,6 +73,7 @@
"vue-i18n": "9.10.2",
"vue-router": "4.4.5",
"vue-types": "^5.1.1",
"vue3-signature": "^0.2.4",
"vuedraggable": "^4.1.0",
"web-storage-cache": "^1.1.1",
"xml-js": "^1.6.11"
+39 -74
View File
@@ -45,8 +45,8 @@ dependencies:
specifier: ^1.1.5
version: 1.1.5
bpmn-js-token-simulation:
specifier: ^0.10.0
version: 0.10.0
specifier: ^0.36.0
version: 0.36.0
camunda-bpmn-moddle:
specifier: ^7.0.1
version: 7.0.1
@@ -72,8 +72,8 @@ dependencies:
specifier: ^2.1.0
version: 2.1.0(echarts@5.5.1)
element-plus:
specifier: 2.8.4
version: 2.8.4(vue@3.5.12)
specifier: 2.9.1
version: 2.9.1(vue@3.5.12)
fast-xml-parser:
specifier: ^4.3.2
version: 4.5.0
@@ -149,6 +149,9 @@ dependencies:
vue-types:
specifier: ^5.1.1
version: 5.1.3(vue@3.5.12)
vue3-signature:
specifier: ^0.2.4
version: 0.2.4(vue@3.5.12)
vuedraggable:
specifier: ^4.1.0
version: 4.1.0(vue@3.5.12)
@@ -2122,7 +2125,7 @@ packages:
'@form-create/element-ui': 3.2.14(vue@3.5.12)
'@form-create/utils': 3.2.14
codemirror: 6.65.7
element-plus: 2.8.4(vue@3.5.12)
element-plus: 2.9.1(vue@3.5.12)
vue: 3.5.12(typescript@5.3.3)
vuedraggable: 4.1.0(vue@3.5.12)
transitivePeerDependencies:
@@ -4581,12 +4584,14 @@ packages:
min-dom: 4.2.1
dev: true
/bpmn-js-token-simulation@0.10.0:
resolution: {integrity: sha512-QuZQ/KVXKt9Vl+XENyOBoTW2Aw+uKjuBlKdCJL6El7AyM7DkJ5bZkSYURshId1SkBDdYg2mJ1flSmsrhGuSfwg==, tarball: https://registry.npmmirror.com/bpmn-js-token-simulation/-/bpmn-js-token-simulation-0.10.0.tgz}
/bpmn-js-token-simulation@0.36.0:
resolution: {integrity: sha512-vz+RHlbZCev/6dzk6FhJRz8M0aZ1GL7Xrza0ecWqeg4tHbgPozgyOm3tXTz75XdtOwRVVBzmCjcciXQX7A55wQ==, tarball: https://registry.npmmirror.com/bpmn-js-token-simulation/-/bpmn-js-token-simulation-0.36.0.tgz}
engines: {node: '>= 16'}
dependencies:
min-dash: 3.8.1
min-dom: 0.2.0
svg.js: 2.7.1
inherits-browser: 0.1.0
min-dash: 4.2.2
min-dom: 4.2.1
randomcolor: 0.6.2
dev: false
/bpmn-js@17.11.1:
@@ -4927,51 +4932,13 @@ packages:
dot-prop: 5.3.0
dev: true
/component-classes@1.2.6:
resolution: {integrity: sha512-hPFGULxdwugu1QWW3SvVOCUHLzO34+a2J6Wqy0c5ASQkfi9/8nZcBB0ZohaEbXOQlCflMAEMmEWk7u7BVs4koA==, tarball: https://registry.npmmirror.com/component-classes/-/component-classes-1.2.6.tgz}
dependencies:
component-indexof: 0.0.3
dev: false
/component-closest@0.1.4:
resolution: {integrity: sha512-NF9hMj6JKGM5sb6wP/dg7GdJOttaIH9PcTsUNdWcrvu7Kw/5R5swQAFpgaYEHlARrNMyn4Wf7O1PlRej+pt76Q==, tarball: https://registry.npmmirror.com/component-closest/-/component-closest-0.1.4.tgz}
dependencies:
component-matches-selector: 0.1.7
dev: false
/component-delegate@0.2.4:
resolution: {integrity: sha512-OlpcB/6Fi+kXQPh/TfXnSvvmrU04ghz7vcJh/jgLF0Ni+I+E3WGlKJQbBGDa5X+kVUG8WxOgjP+8iWbz902fPg==, tarball: https://registry.npmmirror.com/component-delegate/-/component-delegate-0.2.4.tgz}
dependencies:
component-closest: 0.1.4
component-event: 0.1.4
dev: false
/component-emitter@1.3.1:
resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==, tarball: https://registry.npmmirror.com/component-emitter/-/component-emitter-1.3.1.tgz}
dev: true
/component-event@0.1.4:
resolution: {integrity: sha512-GMwOG8MnUHP1l8DZx1ztFO0SJTFnIzZnBDkXAj8RM2ntV2A6ALlDxgbMY1Fvxlg6WPQ+5IM/a6vg4PEYbjg/Rw==, tarball: https://registry.npmmirror.com/component-event/-/component-event-0.1.4.tgz}
dev: false
/component-event@0.2.1:
resolution: {integrity: sha512-wGA++isMqiDq1jPYeyv2as/Bt/u+3iLW0rEa+8NQ82jAv3TgqMiCM+B2SaBdn2DfLilLjjq736YcezihRYhfxw==, tarball: https://registry.npmmirror.com/component-event/-/component-event-0.2.1.tgz}
/component-indexof@0.0.3:
resolution: {integrity: sha512-puDQKvx/64HZXb4hBwIcvQLaLgux8o1CbWl39s41hrIIZDl1lJiD5jc22gj3RBeGK0ovxALDYpIbyjqDUUl0rw==, tarball: https://registry.npmmirror.com/component-indexof/-/component-indexof-0.0.3.tgz}
dev: false
/component-matches-selector@0.1.7:
resolution: {integrity: sha512-Yb2+pVBvrqkQVpPaDBF0DYXRreBveXJNrpJs9FnFu8PF6/5IIcz5oDZqiH9nB5hbD2/TmFVN5ZCxBzqu7yFFYQ==, tarball: https://registry.npmmirror.com/component-matches-selector/-/component-matches-selector-0.1.7.tgz}
dependencies:
component-query: 0.0.3
global-object: 1.0.0
dev: false
/component-query@0.0.3:
resolution: {integrity: sha512-VgebQseT1hz1Ps7vVp2uaSg+N/gsI5ts3AZUSnN6GMA2M82JH7o+qYifWhmVE/e8w/H48SJuA3nA9uX8zRe95Q==, tarball: https://registry.npmmirror.com/component-query/-/component-query-0.0.3.tgz}
dev: false
/compute-scroll-into-view@1.0.20:
resolution: {integrity: sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==, tarball: https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz}
dev: false
@@ -5521,6 +5488,10 @@ packages:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==, tarball: https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz}
dev: true
/default-passive-events@2.0.0:
resolution: {integrity: sha512-eMtt76GpDVngZQ3ocgvRcNCklUMwID1PaNbCNxfpDXuiOXttSh0HzBbda1HU9SIUsDc02vb7g9+3I5tlqe/qMQ==, tarball: https://registry.npmmirror.com/default-passive-events/-/default-passive-events-2.0.0.tgz}
dev: false
/define-data-property@1.1.4:
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==, tarball: https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.4.tgz}
engines: {node: '>= 0.4'}
@@ -5795,8 +5766,8 @@ packages:
resolution: {integrity: sha512-nz88NNBsD7kQSAGGJyp8hS6xSPtWwqNogA0mjtc2nUYeEf3nURK9qpV18TuBdDmEDgVWotS8Wkzf+V52dSQ/LQ==, tarball: https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.67.tgz}
dev: true
/element-plus@2.8.4(vue@3.5.12):
resolution: {integrity: sha512-ZlVAdUOoJliv4kW3ntWnnSHMT+u/Os7mXJjk2xzOlqNeHaI2/ozlF+R58ZCEak8ZnDi6+5A2viWEYRsq64IuiA==, tarball: https://registry.npmmirror.com/element-plus/-/element-plus-2.8.4.tgz}
/element-plus@2.9.1(vue@3.5.12):
resolution: {integrity: sha512-9Agqf/jt4Ugk7EZ6C5LME71sgkvauPCsnvJN12Xid2XVobjufxMGpRE4L7pS4luJMOmFAH3J0NgYEGZT5r+NDg==, tarball: https://registry.npmmirror.com/element-plus/-/element-plus-2.9.1.tgz}
peerDependencies:
vue: ^3.2.0
dependencies:
@@ -6674,10 +6645,6 @@ packages:
global-prefix: 3.0.0
dev: true
/global-object@1.0.0:
resolution: {integrity: sha512-mSPSkY6UsHv6hgW0V2dfWBWTS8TnPnLx3ECVNoWp6rBI2Bg66VYoqGoTFlH/l7XhAZ/l+StYlntXlt87BEeCcg==, tarball: https://registry.npmmirror.com/global-object/-/global-object-1.0.0.tgz}
dev: false
/global-prefix@3.0.0:
resolution: {integrity: sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==, tarball: https://registry.npmmirror.com/global-prefix/-/global-prefix-3.0.0.tgz}
engines: {node: '>=6'}
@@ -7899,10 +7866,6 @@ packages:
engines: {node: '>=18'}
dev: true
/min-dash@3.8.1:
resolution: {integrity: sha512-evumdlmIlg9mbRVPbC4F5FuRhNmcMS5pvuBUbqb1G9v09Ro0ImPEgz5n3khir83lFok1inKqVDjnKEg3GpDxQg==, tarball: https://registry.npmmirror.com/min-dash/-/min-dash-3.8.1.tgz}
dev: false
/min-dash@4.2.2:
resolution: {integrity: sha512-qbhSYUxk6mBaF096B3JOQSumXbKWHenmT97cSpdNzgkWwGjhjhE/KZODCoDNhI2I4C9Cb6R/Q13S4BYkUSXoXQ==, tarball: https://registry.npmmirror.com/min-dash/-/min-dash-4.2.2.tgz}
@@ -7912,18 +7875,6 @@ packages:
dom-walk: 0.1.2
dev: false
/min-dom@0.2.0:
resolution: {integrity: sha512-VmxugbnAcVZGqvepjhOA4d4apmrpX8mMaRS+/jo0dI5Yorzrr4Ru9zc9KVALlY/+XakVCb8iQ+PYXljihQcsNw==, tarball: https://registry.npmmirror.com/min-dom/-/min-dom-0.2.0.tgz}
dependencies:
component-classes: 1.2.6
component-closest: 0.1.4
component-delegate: 0.2.4
component-event: 0.1.4
component-matches-selector: 0.1.7
component-query: 0.0.3
domify: 1.4.2
dev: false
/min-dom@4.2.1:
resolution: {integrity: sha512-TMoL8SEEIhUWYgkj7XMSgxmwSyGI+4fP2KFFGnN3FbHfbGHVdsLYSz8LoIsgPhz4dWRmLvxWWSMgzZMJW5sZuA==, tarball: https://registry.npmmirror.com/min-dom/-/min-dom-4.2.1.tgz}
dependencies:
@@ -8714,6 +8665,10 @@ packages:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==, tarball: https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz}
dev: true
/randomcolor@0.6.2:
resolution: {integrity: sha512-Mn6TbyYpFgwFuQ8KJKqf3bqqY9O1y37/0jgSK/61PUxV4QfIMv0+K2ioq8DfOjkBslcjwSzRfIDEXfzA9aCx7A==, tarball: https://registry.npmmirror.com/randomcolor/-/randomcolor-0.6.2.tgz}
dev: false
/rd@2.0.1:
resolution: {integrity: sha512-/XdKU4UazUZTXFmI0dpABt8jSXPWcEyaGdk340KdHnsEOdkTctlX23aAK7ChQDn39YGNlAJr1M5uvaKt4QnpNw==, tarball: https://registry.npmmirror.com/rd/-/rd-2.0.1.tgz}
dependencies:
@@ -9128,6 +9083,10 @@ packages:
engines: {node: '>=14'}
dev: true
/signature_pad@3.0.0-beta.4:
resolution: {integrity: sha512-cOf2NhVuTiuNqe2X/ycEmizvCDXk0DoemhsEpnkcGnA4kS5iJYTCqZ9As7tFBbsch45Q1EdX61833+6sjJ8rrw==, tarball: https://registry.npmmirror.com/signature_pad/-/signature_pad-3.0.0-beta.4.tgz}
dev: false
/sirv@2.0.4:
resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==, tarball: https://registry.npmmirror.com/sirv/-/sirv-2.0.4.tgz}
engines: {node: '>= 10'}
@@ -9561,10 +9520,6 @@ packages:
resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==, tarball: https://registry.npmmirror.com/svg-tags/-/svg-tags-1.0.0.tgz}
dev: true
/svg.js@2.7.1:
resolution: {integrity: sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA==, tarball: https://registry.npmmirror.com/svg.js/-/svg.js-2.7.1.tgz}
dev: false
/svgo@2.8.0:
resolution: {integrity: sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==, tarball: https://registry.npmmirror.com/svgo/-/svgo-2.8.0.tgz}
engines: {node: '>=10.13.0'}
@@ -10324,6 +10279,16 @@ packages:
vue: 3.5.12(typescript@5.3.3)
dev: false
/vue3-signature@0.2.4(vue@3.5.12):
resolution: {integrity: sha512-XFwwFVK9OG3F085pKIq2SlNVqx32WdFH+TXbGEWc5FfEKpx8oMmZuAwZZ50K/pH2FgmJSE8IRwU9DDhrLpd6iA==, tarball: https://registry.npmmirror.com/vue3-signature/-/vue3-signature-0.2.4.tgz}
peerDependencies:
vue: ^3.2.0
dependencies:
default-passive-events: 2.0.0
signature_pad: 3.0.0-beta.4
vue: 3.5.12(typescript@5.3.3)
dev: false
/vue@3.5.12(typescript@5.3.3):
resolution: {integrity: sha512-CLVZtXtn2ItBIi/zHZ0Sg1Xkb7+PU32bJJ8Bmy7ts3jxXTcbfsEfBivFYYWz1Hur+lalqGAh65Coin0r+HRUfg==, tarball: https://registry.npmmirror.com/vue/-/vue-3.5.12.tgz}
peerDependencies:
+4
View File
@@ -72,3 +72,7 @@ export const deleteModel = async (id: number) => {
export const deployModel = async (id: number) => {
return await request.post({ url: '/bpm/model/deploy?id=' + id })
}
export const cleanModel = async (id: number) => {
return await request.delete({ url: '/bpm/model/clean?id=' + id })
}
+2 -1
View File
@@ -36,6 +36,7 @@ export type ApprovalTaskInfo = {
assigneeUser: User
status: number
reason: string
signPicUrl: string
}
// 审批节点信息
@@ -89,7 +90,7 @@ export const getProcessInstanceCopyPage = async (params: any) => {
// 获取审批详情
export const getApprovalDetail = async (params: any) => {
return await request.get({ url: 'bpm/process-instance/get-approval-detail' , params })
return await request.get({ url: 'bpm/process-instance/get-approval-detail', params })
}
// 获取表单字段权限
+7 -7
View File
@@ -22,11 +22,6 @@ export const register = (data: RegisterVO) => {
return request.post({ url: '/system/auth/register', data })
}
// 刷新访问令牌
export const refreshToken = () => {
return request.post({ url: '/system/auth/refresh-token?refreshToken=' + getRefreshToken() })
}
// 使用租户名,获得租户编号
export const getTenantIdByName = (name: string) => {
return request.get({ url: '/system/tenant/get-id-by-name?name=' + name })
@@ -76,12 +71,17 @@ export const socialAuthRedirect = (type: number, redirectUri: string) => {
})
}
// 获取验证图片以及 token
export const getCode = (data) => {
export const getCode = (data: any) => {
debugger
return request.postOriginal({ url: 'system/captcha/get', data })
}
// 滑动或者点选验证
export const reqCheck = (data) => {
export const reqCheck = (data: any) => {
return request.postOriginal({ url: 'system/captcha/check', data })
}
// 通过短信重置密码
export const smsResetPassword = (data: any) => {
return request.post({ url: '/system/auth/reset-password', data })
}
+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1735905505218" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4277" width="200" height="200" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M561.778 454.929h198.117c0.549 0 0.994 0.444 0.994 1.001v97.553a0.998 0.998 0 0 1-0.994 1.001H463.224a1.005 1.005 0 0 1-1.002-1V207.04c0-0.552 0.444-1 1.002-1h97.552c0.553 0 1.002 0.455 1.002 1v247.89zM512 952.706c-247.424 0-448-200.576-448-448 0-247.423 200.576-448 448-448s448 200.577 448 448c0 247.424-200.576 448-448 448z m0-99.555c192.44 0 348.444-156.004 348.444-348.445 0-192.44-156.003-348.444-348.444-348.444-192.44 0-348.444 156.004-348.444 348.444 0 192.441 156.003 348.445 348.444 348.445z" fill="#3296FA" p-id="4278"></path></svg>

After

Width:  |  Height:  |  Size: 876 B

@@ -51,7 +51,8 @@
</template>
<script setup lang="ts">
import { ComponentStyle, usePropertyForm } from '@/components/DiyEditor/util'
import { ComponentStyle } from '@/components/DiyEditor/util'
import { useVModel } from '@vueuse/core'
/**
* 组件容器属性:目前右边部分
@@ -61,7 +62,7 @@ defineOptions({ name: 'ComponentContainer' })
const props = defineProps<{ modelValue: ComponentStyle }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
const formData = useVModel(props, 'modelValue', emit)
const treeData = [
{
@@ -93,14 +93,14 @@
<script setup lang="ts">
import { CarouselProperty } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util'
import { useVModel } from '@vueuse/core'
// 轮播图属性面板
defineOptions({ name: 'CarouselProperty' })
const props = defineProps<{ modelValue: CarouselProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
const formData = useVModel(props, 'modelValue', emit)
</script>
<style scoped lang="scss"></style>
@@ -68,15 +68,20 @@
</el-form>
</ComponentContainerProperty>
<!-- 优惠券选择 -->
<CouponSelect ref="couponSelectDialog" v-model:multiple-selection="couponList" />
<CouponSelect
ref="couponSelectDialog"
v-model:multiple-selection="couponList"
:take-type="CouponTemplateTakeTypeEnum.USER.type"
@change="handleCouponSelect"
/>
</template>
<script setup lang="ts">
import { CouponCardProperty } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util'
import { useVModel } from '@vueuse/core'
import * as CouponTemplateApi from '@/api/mall/promotion/coupon/couponTemplate'
import { floatToFixed2 } from '@/utils'
import { PromotionDiscountTypeEnum } from '@/utils/constants'
import { CouponTemplateTakeTypeEnum, PromotionDiscountTypeEnum } from '@/utils/constants'
import CouponSelect from '@/views/mall/promotion/coupon/components/CouponSelect.vue'
// 优惠券卡片属性面板
@@ -84,7 +89,7 @@ defineOptions({ name: 'CouponCardProperty' })
const props = defineProps<{ modelValue: CouponCardProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
const formData = useVModel(props, 'modelValue', emit)
// 优惠券列表
const couponList = ref<CouponTemplateApi.CouponTemplateVO[]>([])
@@ -93,10 +98,20 @@ const couponSelectDialog = ref()
const handleAddCoupon = () => {
couponSelectDialog.value.open()
}
const handleCouponSelect = () => {
formData.value.couponIds = couponList.value.map((coupon) => coupon.id)
}
watch(
() => couponList.value,
() => {
formData.value.couponIds = couponList.value.map((coupon) => coupon.id)
() => formData.value.couponIds,
async () => {
if (formData.value.couponIds?.length > 0) {
couponList.value = await CouponTemplateApi.getCouponTemplateList(formData.value.couponIds)
}
},
{
immediate: true,
deep: true
}
)
</script>
@@ -45,12 +45,12 @@
<script setup lang="ts">
import { DividerProperty } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util'
import { useVModel } from '@vueuse/core'
// 导航栏属性面板
defineOptions({ name: 'DividerProperty' })
const props = defineProps<{ modelValue: DividerProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
const formData = useVModel(props, 'modelValue', emit)
//线类型
const BORDER_TYPES = [
@@ -31,14 +31,14 @@
<script setup lang="ts">
import { FloatingActionButtonProperty } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util'
import { useVModel } from '@vueuse/core'
// 悬浮按钮属性面板
defineOptions({ name: 'FloatingActionButtonProperty' })
const props = defineProps<{ modelValue: FloatingActionButtonProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
const formData = useVModel(props, 'modelValue', emit)
</script>
<style scoped lang="scss"></style>
@@ -20,7 +20,7 @@
</template>
<script setup lang="ts">
import { usePropertyForm } from '@/components/DiyEditor/util'
import { useVModel } from '@vueuse/core'
import { HotZoneProperty } from '@/components/DiyEditor/components/mobile/HotZone/config'
import HotZoneEditDialog from './components/HotZoneEditDialog/index.vue'
@@ -29,7 +29,7 @@ defineOptions({ name: 'HotZoneProperty' })
const props = defineProps<{ modelValue: HotZoneProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
const formData = useVModel(props, 'modelValue', emit)
// 热区编辑对话框
const editDialogRef = ref()
@@ -21,14 +21,14 @@
<script setup lang="ts">
import { ImageBarProperty } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util'
import { useVModel } from '@vueuse/core'
// 图片展示属性面板
defineOptions({ name: 'ImageBarProperty' })
const props = defineProps<{ modelValue: ImageBarProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
const formData = useVModel(props, 'modelValue', emit)
</script>
<style scoped lang="scss"></style>
@@ -56,7 +56,7 @@
</template>
<script setup lang="ts">
import { usePropertyForm } from '@/components/DiyEditor/util'
import { useVModel } from '@vueuse/core'
import { MagicCubeProperty } from '@/components/DiyEditor/components/mobile/MagicCube/config'
/** 广告魔方属性面板 */
@@ -64,7 +64,7 @@ defineOptions({ name: 'MagicCubeProperty' })
const props = defineProps<{ modelValue: MagicCubeProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
const formData = useVModel(props, 'modelValue', emit)
// 选中的热区
const selectedHotAreaIndex = ref(-1)
@@ -48,7 +48,7 @@
</template>
<script setup lang="ts">
import { usePropertyForm } from '@/components/DiyEditor/util'
import { useVModel } from '@vueuse/core'
import {
EMPTY_MENU_GRID_ITEM_PROPERTY,
MenuGridProperty
@@ -59,7 +59,7 @@ defineOptions({ name: 'MenuGridProperty' })
const props = defineProps<{ modelValue: MenuGridProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
const formData = useVModel(props, 'modelValue', emit)
</script>
<style scoped lang="scss"></style>
@@ -28,7 +28,7 @@
</template>
<script setup lang="ts">
import { usePropertyForm } from '@/components/DiyEditor/util'
import { useVModel } from '@vueuse/core'
import {
EMPTY_MENU_LIST_ITEM_PROPERTY,
MenuListProperty
@@ -39,7 +39,7 @@ defineOptions({ name: 'MenuListProperty' })
const props = defineProps<{ modelValue: MenuListProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
const formData = useVModel(props, 'modelValue', emit)
</script>
<style scoped lang="scss"></style>
@@ -58,7 +58,7 @@
</template>
<script setup lang="ts">
import { usePropertyForm } from '@/components/DiyEditor/util'
import { useVModel } from '@vueuse/core'
import {
EMPTY_MENU_SWIPER_ITEM_PROPERTY,
MenuSwiperProperty
@@ -70,7 +70,7 @@ defineOptions({ name: 'MenuSwiperProperty' })
const props = defineProps<{ modelValue: MenuSwiperProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
const formData = useVModel(props, 'modelValue', emit)
</script>
<style scoped lang="scss"></style>
@@ -64,17 +64,22 @@
<script lang="ts" setup>
import { NavigationBarCellProperty } from '../config'
import { usePropertyForm } from '@/components/DiyEditor/util'
import { useVModel } from '@vueuse/core'
// 导航栏属性面板
defineOptions({ name: 'NavigationBarCellProperty' })
const props = defineProps<{
modelValue: NavigationBarCellProperty[]
isMp: boolean
}>()
const props = withDefaults(
defineProps<{
modelValue: NavigationBarCellProperty[]
isMp: boolean
}>(),
{
modelValue: () => [],
isMp: true
}
)
const emit = defineEmits(['update:modelValue'])
const { formData: cellList } = usePropertyForm(props.modelValue, emit)
if (!cellList.value) cellList.value = []
const cellList = useVModel(props, 'modelValue', emit)
// 单元格数量:小程序6个(右侧胶囊按钮占了2个),其它平台8个
const cellCount = computed(() => (props.isMp ? 6 : 8))
@@ -4,7 +4,7 @@
<div v-for="(cell, cellIndex) in cellList" :key="cellIndex" :style="getCellStyle(cell)">
<span v-if="cell.type === 'text'">{{ cell.text }}</span>
<img v-else-if="cell.type === 'image'" :src="cell.imgUrl" alt="" class="h-full w-full" />
<SearchBar v-else :property="getSearchProp" />
<SearchBar v-else :property="getSearchProp(cell)" />
</div>
</div>
<img
@@ -51,14 +51,14 @@ const getCellStyle = (cell: NavigationBarCellProperty) => {
} as StyleValue
}
// 获得搜索框属性
const getSearchProp = (cell: NavigationBarCellProperty) => {
const getSearchProp = computed(() => (cell: NavigationBarCellProperty) => {
return {
height: 30,
showScan: false,
placeholder: cell.placeholder,
borderRadius: cell.borderRadius
} as SearchProperty
}
})
</script>
<style lang="scss" scoped>
.navigation-bar {
@@ -66,7 +66,7 @@
<script setup lang="ts">
import { NavigationBarProperty } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util'
import { useVModel } from '@vueuse/core'
import NavigationBarCellProperty from '@/components/DiyEditor/components/mobile/NavigationBar/components/CellProperty.vue'
// 导航栏属性面板
defineOptions({ name: 'NavigationBarProperty' })
@@ -77,7 +77,7 @@ const rules = {
const props = defineProps<{ modelValue: NavigationBarProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
const formData = useVModel(props, 'modelValue', emit)
if (!formData.value._local) {
formData.value._local = { previewMp: true, previewOther: false }
}
@@ -30,7 +30,7 @@
<script setup lang="ts">
import { NoticeBarProperty } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util'
import { useVModel } from '@vueuse/core'
// 通知栏属性面板
defineOptions({ name: 'NoticeBarProperty' })
// 表单校验
@@ -40,7 +40,7 @@ const rules = {
const props = defineProps<{ modelValue: NoticeBarProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
const formData = useVModel(props, 'modelValue', emit)
</script>
<style scoped lang="scss"></style>
@@ -20,7 +20,7 @@
<script setup lang="ts">
import { PageConfigProperty } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util'
import { useVModel } from '@vueuse/core'
// 导航栏属性面板
defineOptions({ name: 'PageConfigProperty' })
// 表单校验
@@ -28,7 +28,7 @@ const rules = {}
const props = defineProps<{ modelValue: PageConfigProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
const formData = useVModel(props, 'modelValue', emit)
</script>
<style scoped lang="scss"></style>
@@ -25,14 +25,14 @@
<script setup lang="ts">
import { PopoverProperty } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util'
import { useVModel } from '@vueuse/core'
// 弹窗广告属性面板
defineOptions({ name: 'PopoverProperty' })
const props = defineProps<{ modelValue: PopoverProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
const formData = useVModel(props, 'modelValue', emit)
</script>
<style scoped lang="scss"></style>
@@ -135,7 +135,7 @@
<script setup lang="ts">
import { ProductCardProperty } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util'
import { useVModel } from '@vueuse/core'
import SpuShowcase from '@/views/mall/product/spu/components/SpuShowcase.vue'
// 商品卡片属性面板
@@ -143,7 +143,7 @@ defineOptions({ name: 'ProductCardProperty' })
const props = defineProps<{ modelValue: ProductCardProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
const formData = useVModel(props, 'modelValue', emit)
</script>
<style scoped lang="scss"></style>
@@ -85,7 +85,7 @@
<script setup lang="ts">
import { ProductListProperty } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util'
import { useVModel } from '@vueuse/core'
import SpuShowcase from '@/views/mall/product/spu/components/SpuShowcase.vue'
// 商品栏属性面板
@@ -93,7 +93,7 @@ defineOptions({ name: 'ProductListProperty' })
const props = defineProps<{ modelValue: ProductListProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
const formData = useVModel(props, 'modelValue', emit)
</script>
<style scoped lang="scss"></style>
@@ -25,7 +25,7 @@
<script setup lang="ts">
import { PromotionArticleProperty } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util'
import { useVModel } from '@vueuse/core'
import * as ArticleApi from '@/api/mall/promotion/article/index'
// 营销文章属性面板
@@ -33,7 +33,7 @@ defineOptions({ name: 'PromotionArticleProperty' })
const props = defineProps<{ modelValue: PromotionArticleProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
const formData = useVModel(props, 'modelValue', emit)
// 文章列表
const articles = ref<ArticleApi.ArticleVO>([])
@@ -140,7 +140,7 @@
<script setup lang="ts">
import { PromotionCombinationProperty } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util'
import { useVModel } from '@vueuse/core'
import * as CombinationActivityApi from '@/api/mall/promotion/combination/combinationActivity'
import { CommonStatusEnum } from '@/utils/constants'
import CombinationShowcase from '@/views/mall/promotion/combination/components/CombinationShowcase.vue'
@@ -150,7 +150,7 @@ defineOptions({ name: 'PromotionCombinationProperty' })
const props = defineProps<{ modelValue: PromotionCombinationProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
const formData = useVModel(props, 'modelValue', emit)
// 活动列表
const activityList = ref<CombinationActivityApi.CombinationActivityVO[]>([])
onMounted(async () => {
@@ -140,7 +140,7 @@
<script lang="ts" setup>
import { PromotionPointProperty } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util'
import { useVModel } from '@vueuse/core'
import PointShowcase from '@/views/mall/promotion/point/components/PointShowcase.vue'
// 秒杀属性面板
@@ -148,7 +148,7 @@ defineOptions({ name: 'PromotionPointProperty' })
const props = defineProps<{ modelValue: PromotionPointProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
const formData = useVModel(props, 'modelValue', emit)
</script>
<style lang="scss" scoped></style>
@@ -140,7 +140,7 @@
<script setup lang="ts">
import { PromotionSeckillProperty } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util'
import { useVModel } from '@vueuse/core'
import * as SeckillActivityApi from '@/api/mall/promotion/seckill/seckillActivity'
import { CommonStatusEnum } from '@/utils/constants'
import SeckillShowcase from '@/views/mall/promotion/seckill/components/SeckillShowcase.vue'
@@ -150,7 +150,7 @@ defineOptions({ name: 'PromotionSeckillProperty' })
const props = defineProps<{ modelValue: PromotionSeckillProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
const formData = useVModel(props, 'modelValue', emit)
// 活动列表
const activityList = ref<SeckillActivityApi.SeckillActivityVO[]>([])
onMounted(async () => {
@@ -59,7 +59,7 @@
</template>
<script setup lang="ts">
import { usePropertyForm } from '@/components/DiyEditor/util'
import { useVModel } from '@vueuse/core'
import { SearchProperty } from '@/components/DiyEditor/components/mobile/SearchBar/config'
/** 搜索框属性面板 */
@@ -67,7 +67,7 @@ defineOptions({ name: 'SearchProperty' })
const props = defineProps<{ modelValue: SearchProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
const formData = useVModel(props, 'modelValue', emit)
</script>
<style scoped lang="scss"></style>
@@ -80,13 +80,13 @@
<script setup lang="ts">
import { TabBarProperty, component, THEME_LIST } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util'
import { useVModel } from '@vueuse/core'
// 底部导航栏
defineOptions({ name: 'TabBarProperty' })
const props = defineProps<{ modelValue: TabBarProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
const formData = useVModel(props, 'modelValue', emit)
// 将数据库的值更新到右侧属性栏
component.property.items = formData.value.items
@@ -101,13 +101,13 @@
</template>
<script lang="ts" setup>
import { TitleBarProperty } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util'
import { useVModel } from '@vueuse/core'
// 导航栏属性面板
defineOptions({ name: 'TitleBarProperty' })
const props = defineProps<{ modelValue: TitleBarProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
const formData = useVModel(props, 'modelValue', emit)
// 表单校验
const rules = {}
@@ -4,14 +4,14 @@
<script setup lang="ts">
import { UserCardProperty } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util'
import { useVModel } from '@vueuse/core'
// 用户卡片属性面板
defineOptions({ name: 'UserCardProperty' })
const props = defineProps<{ modelValue: UserCardProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
const formData = useVModel(props, 'modelValue', emit)
</script>
<style scoped lang="scss"></style>
@@ -4,14 +4,14 @@
<script setup lang="ts">
import { UserCouponProperty } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util'
import { useVModel } from '@vueuse/core'
// 用户卡券属性面板
defineOptions({ name: 'UserCouponProperty' })
const props = defineProps<{ modelValue: UserCouponProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
const formData = useVModel(props, 'modelValue', emit)
</script>
<style scoped lang="scss"></style>
@@ -4,14 +4,14 @@
<script setup lang="ts">
import { UserOrderProperty } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util'
import { useVModel } from '@vueuse/core'
// 用户订单属性面板
defineOptions({ name: 'UserOrderProperty' })
const props = defineProps<{ modelValue: UserOrderProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
const formData = useVModel(props, 'modelValue', emit)
</script>
<style scoped lang="scss"></style>
@@ -4,14 +4,14 @@
<script setup lang="ts">
import { UserWalletProperty } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util'
import { useVModel } from '@vueuse/core'
// 用户资产属性面板
defineOptions({ name: 'UserWalletProperty' })
const props = defineProps<{ modelValue: UserWalletProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
const formData = useVModel(props, 'modelValue', emit)
</script>
<style scoped lang="scss"></style>
@@ -42,14 +42,14 @@
<script setup lang="ts">
import { VideoPlayerProperty } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util'
import { useVModel } from '@vueuse/core'
// 视频播放属性面板
defineOptions({ name: 'VideoPlayerProperty' })
const props = defineProps<{ modelValue: VideoPlayerProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
const formData = useVModel(props, 'modelValue', emit)
</script>
<style scoped lang="scss"></style>
+31 -13
View File
@@ -110,7 +110,7 @@
<el-tag
v-if="showPageConfig"
:effect="selectedComponent?.uid === pageConfigComponent.uid ? 'dark' : 'plain'"
:type="selectedComponent?.uid === pageConfigComponent.uid ? '' : 'info'"
:type="selectedComponent?.uid === pageConfigComponent.uid ? 'primary' : 'info'"
size="large"
@click="handleComponentSelected(pageConfigComponent)"
>
@@ -121,7 +121,7 @@
<el-tag
v-if="component.position === 'fixed'"
:effect="selectedComponent?.uid === component.uid ? 'dark' : 'plain'"
:type="selectedComponent?.uid === component.uid ? '' : 'info'"
:type="selectedComponent?.uid === component.uid ? 'primary' : 'info'"
closable
size="large"
@click="handleComponentSelected(component)"
@@ -191,7 +191,7 @@ import { cloneDeep, includes } from 'lodash-es'
import { component as PAGE_CONFIG_COMPONENT } from '@/components/DiyEditor/components/mobile/PageConfig/config'
import { component as NAVIGATION_BAR_COMPONENT } from './components/mobile/NavigationBar/config'
import { component as TAB_BAR_COMPONENT } from './components/mobile/TabBar/config'
import { isString } from '@/utils/is'
import { isEmpty, isString } from '@/utils/is'
import { DiyComponent, DiyComponentLibrary, PageConfig } from '@/components/DiyEditor/util'
import { componentConfigs } from '@/components/DiyEditor/components/mobile'
import { array, oneOfType } from 'vue-types'
@@ -238,24 +238,42 @@ const props = defineProps({
watch(
() => props.modelValue,
() => {
const modelValue = isString(props.modelValue)
? (JSON.parse(props.modelValue) as PageConfig)
: props.modelValue
pageConfigComponent.value.property = modelValue?.page || PAGE_CONFIG_COMPONENT.property
const modelValue =
isString(props.modelValue) && !isEmpty(props.modelValue)
? (JSON.parse(props.modelValue) as PageConfig)
: props.modelValue
pageConfigComponent.value.property =
(typeof modelValue !== 'string' && modelValue?.page) || PAGE_CONFIG_COMPONENT.property
navigationBarComponent.value.property =
modelValue?.navigationBar || NAVIGATION_BAR_COMPONENT.property
tabBarComponent.value.property = modelValue?.tabBar || TAB_BAR_COMPONENT.property
(typeof modelValue !== 'string' && modelValue?.navigationBar) ||
NAVIGATION_BAR_COMPONENT.property
tabBarComponent.value.property =
(typeof modelValue !== 'string' && modelValue?.tabBar) || TAB_BAR_COMPONENT.property
// 查找对应的页面组件
pageComponents.value = (modelValue?.components || []).map((item) => {
const component = componentConfigs[item.id]
return { ...component, property: item.property }
})
pageComponents.value = ((typeof modelValue !== 'string' && modelValue?.components) || []).map(
(item) => {
const component = componentConfigs[item.id]
return { ...component, property: item.property }
}
)
},
{
immediate: true
}
)
/** 选择组件修改其属性后更新它的配置 */
watch(
selectedComponent,
(val: any) => {
if (!val || selectedComponentIndex.value === -1) {
return
}
pageComponents.value[selectedComponentIndex.value] = selectedComponent.value!
},
{ deep: true }
)
// 保存
const handleSave = () => {
// 发送保存通知
-29
View File
@@ -1,4 +1,3 @@
import { ref, Ref } from 'vue'
import { PageConfigProperty } from '@/components/DiyEditor/components/mobile/PageConfig/config'
import { NavigationBarProperty } from '@/components/DiyEditor/components/mobile/NavigationBar/config'
import { TabBarProperty } from '@/components/DiyEditor/components/mobile/TabBar/config'
@@ -78,34 +77,6 @@ export interface PageConfig {
// 页面组件,只保留组件ID,组件属性
export interface PageComponent extends Pick<DiyComponent<any>, 'id' | 'property'> {}
// 属性表单监听
export function usePropertyForm<T>(modelValue: T, emit: Function): { formData: Ref<T> } {
const formData = ref<T>()
// 监听属性数据变动
watch(
() => modelValue,
() => {
formData.value = modelValue
},
{
deep: true,
immediate: true
}
)
// 监听表单数据变动
watch(
() => formData.value,
() => {
emit('update:modelValue', formData.value)
},
{
deep: true
}
)
return { formData } as { formData: Ref<T> }
}
// 页面组件库
export const PAGE_LIBS = [
{
+11 -4
View File
@@ -13,9 +13,16 @@
class="mb-4px flex flex-col gap-4px border border-gray-2 border-rounded rounded border-solid p-8px"
>
<!-- 操作按钮区 -->
<div class="m--8px m-b-4px flex flex-row items-center justify-between p-8px" style="background-color: var(--app-content-bg-color);">
<div
class="m--8px m-b-4px flex flex-row items-center justify-between p-8px"
style="background-color: var(--app-content-bg-color)"
>
<el-tooltip content="拖动排序">
<Icon icon="ic:round-drag-indicator" class="drag-icon cursor-move" style="color: #8a909c;" />
<Icon
icon="ic:round-drag-indicator"
class="drag-icon cursor-move"
style="color: #8a909c"
/>
</el-tooltip>
<el-tooltip content="删除">
<Icon
@@ -47,7 +54,7 @@
<script setup lang="ts">
// 拖拽组件
import VueDraggable from 'vuedraggable'
import { usePropertyForm } from '@/components/DiyEditor/util'
import { useVModel } from '@vueuse/core'
import { any, array } from 'vue-types'
import { propTypes } from '@/utils/propTypes'
import { cloneDeep } from 'lodash-es'
@@ -66,7 +73,7 @@ const props = defineProps({
})
// 定义事件
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
const formData = useVModel(props, 'modelValue', emit)
// 处理添加
const handleAdd = () => formData.value.push(cloneDeep(props.emptyItem || {}))
+4
View File
@@ -9,6 +9,10 @@ import { useAppStore } from '@/store/modules/app'
import { isString } from '@/utils/is'
import { useDesign } from '@/hooks/web/useDesign'
import 'echarts/lib/component/markPoint'
import 'echarts/lib/component/markLine'
import 'echarts/lib/component/markArea'
defineOptions({ name: 'EChart' })
const { getPrefixCls, variables } = useDesign()
+4 -28
View File
@@ -1,7 +1,7 @@
<template>
<el-input v-model="valueRef" v-bind="$attrs">
<el-input v-model="modelValue" v-bind="$attrs">
<template #append>
<el-color-picker v-model="colorRef" :predefine="PREDEFINE_COLORS" />
<el-color-picker v-model="color" :predefine="PREDEFINE_COLORS" />
</template>
</el-input>
</template>
@@ -9,6 +9,7 @@
<script lang="ts" setup>
import { propTypes } from '@/utils/propTypes'
import { PREDEFINE_COLORS } from '@/utils/color'
import { useVModels } from '@vueuse/core'
/**
* 带颜色选择器输入框
@@ -19,33 +20,8 @@ const props = defineProps({
modelValue: propTypes.string.def('').isRequired,
color: propTypes.string.def('').isRequired
})
watch(
() => props.modelValue,
(val: string) => {
if (val === unref(valueRef)) return
valueRef.value = val
}
)
const emit = defineEmits(['update:modelValue', 'update:color'])
// 输入框的值
const valueRef = ref(props.modelValue)
watch(
() => valueRef.value,
(val: string) => {
emit('update:modelValue', val)
}
)
// 颜色
const colorRef = ref(props.color)
watch(
() => colorRef.value,
(val: string) => {
emit('update:color', val)
}
)
const { modelValue, color } = useVModels(props, emit)
</script>
<style scoped lang="scss">
:deep(.el-input-group__append) {
+5
View File
@@ -79,9 +79,14 @@ function remoteMethod(data) {
function handleChange(path) {
router.push({ path })
hiddenSearch()
hiddenTopSearch()
}
function hiddenSearch() {
showSearch.value = false
}
function hiddenTopSearch() {
showTopSearch.value = false
}
@@ -39,7 +39,25 @@
</div>
<div class="handler-item-text">包容分支</div>
</div>
</div>
<div class="handler-item" @click="addNode(NodeType.DELAY_TIMER_NODE)">
<div class="handler-item-icon delay">
<span class="iconfont icon-size icon-delay"></span>
</div>
<div class="handler-item-text">延迟器</div>
</div>
<div class="handler-item" @click="addNode(NodeType.ROUTER_BRANCH_NODE)">
<div class="handler-item-icon router">
<span class="iconfont icon-size icon-router"></span>
</div>
<div class="handler-item-text">路由分支</div>
</div>
<div class="handler-item" @click="addNode(NodeType.TRIGGER_NODE)">
<div class="handler-item-icon trigger">
<span class="iconfont icon-size icon-trigger"></span>
</div>
<div class="handler-item-text">触发器</div>
</div>
</div>
<template #reference>
<div class="add-icon"><Icon icon="ep:plus" /></div>
</template>
@@ -53,12 +71,14 @@ import {
ApproveMethodType,
AssignEmptyHandlerType,
AssignStartUserHandlerType,
ConditionType,
NODE_DEFAULT_NAME,
NodeType,
RejectHandlerType,
SimpleFlowNode
SimpleFlowNode,
DEFAULT_CONDITION_GROUP_VALUE
} from './consts'
import { generateUUID } from '@/utils'
import {generateUUID} from '@/utils'
defineOptions({
name: 'NodeHandler'
@@ -113,7 +133,16 @@ const addNode = (type: number) => {
type: AssignEmptyHandlerType.APPROVE
},
assignStartUserHandlerType: AssignStartUserHandlerType.START_USER_AUDIT,
childNode: props.childNode
childNode: props.childNode,
taskCreateListener: {
enable: false
},
taskAssignListener: {
enable: false
},
taskCompleteListener: {
enable: false
}
}
emits('update:childNode', data)
}
@@ -140,8 +169,11 @@ const addNode = (type: number) => {
showText: '',
type: NodeType.CONDITION_NODE,
childNode: undefined,
conditionType: 1,
defaultFlow: false
conditionSetting: {
defaultFlow: false,
conditionType: ConditionType.RULE,
conditionGroups: DEFAULT_CONDITION_GROUP_VALUE
}
},
{
id: 'Flow_' + generateUUID(),
@@ -149,8 +181,9 @@ const addNode = (type: number) => {
showText: '未满足其它条件时,将进入此分支',
type: NodeType.CONDITION_NODE,
childNode: undefined,
conditionType: undefined,
defaultFlow: true
conditionSetting: {
defaultFlow: true
}
}
]
}
@@ -194,7 +227,11 @@ const addNode = (type: number) => {
showText: '',
type: NodeType.CONDITION_NODE,
childNode: undefined,
defaultFlow: false
conditionSetting: {
defaultFlow: false,
conditionType: ConditionType.RULE,
conditionGroups: DEFAULT_CONDITION_GROUP_VALUE
}
},
{
id: 'Flow_' + generateUUID(),
@@ -202,12 +239,44 @@ const addNode = (type: number) => {
showText: '未满足其它条件时,将进入此分支',
type: NodeType.CONDITION_NODE,
childNode: undefined,
defaultFlow: true
conditionSetting: {
defaultFlow: true
}
}
]
}
emits('update:childNode', data)
}
if (type === NodeType.DELAY_TIMER_NODE) {
const data: SimpleFlowNode = {
id: 'Activity_' + generateUUID(),
name: NODE_DEFAULT_NAME.get(NodeType.DELAY_TIMER_NODE) as string,
showText: '',
type: NodeType.DELAY_TIMER_NODE,
childNode: props.childNode
}
emits('update:childNode', data)
}
if (type === NodeType.ROUTER_BRANCH_NODE) {
const data: SimpleFlowNode = {
id: 'GateWay_' + generateUUID(),
name: NODE_DEFAULT_NAME.get(NodeType.ROUTER_BRANCH_NODE) as string,
showText: '',
type: NodeType.ROUTER_BRANCH_NODE,
childNode: props.childNode
}
emits('update:childNode', data)
}
if (type === NodeType.TRIGGER_NODE) {
const data: SimpleFlowNode = {
id: 'Activity_' + generateUUID(),
name: NODE_DEFAULT_NAME.get(NodeType.TRIGGER_NODE) as string,
showText: '',
type: NodeType.TRIGGER_NODE,
childNode: props.childNode
}
emits('update:childNode', data)
}
}
</script>
@@ -38,6 +38,24 @@
@update:model-value="handleModelValueUpdate"
@find:parent-node="findFromParentNode"
/>
<!-- 延迟器节点 -->
<DelayTimerNode
v-if="currentNode && currentNode.type === NodeType.DELAY_TIMER_NODE"
:flow-node="currentNode"
@update:flow-node="handleModelValueUpdate"
/>
<!-- 路由分支节点 -->
<RouterNode
v-if="currentNode && currentNode.type === NodeType.ROUTER_BRANCH_NODE"
:flow-node="currentNode"
@update:flow-node="handleModelValueUpdate"
/>
<!-- 触发器节点 -->
<TriggerNode
v-if="currentNode && currentNode.type === NodeType.TRIGGER_NODE"
:flow-node="currentNode"
@update:flow-node="handleModelValueUpdate"
/>
<!-- 递归显示孩子节点 -->
<ProcessNodeTree
v-if="currentNode && currentNode.childNode"
@@ -60,6 +78,9 @@ 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 DelayTimerNode from './nodes/DelayTimerNode.vue'
import RouterNode from './nodes/RouterNode.vue'
import TriggerNode from './nodes/TriggerNode.vue'
import { SimpleFlowNode, NodeType } from './consts'
import { useWatchNode } from './node'
defineOptions({
@@ -56,16 +56,13 @@ const props = defineProps({
required: false
},
// 可发起流程的人员编号
startUserIds : {
startUserIds: {
type: Array,
required: false
},
value: {
type: [String, Object],
required: false
}
})
const processData = inject('processData') as Ref
const loading = ref(false)
const formFields = ref<string[]>([])
const formType = ref(20)
@@ -76,9 +73,6 @@ const deptOptions = ref<DeptApi.DeptVO[]>([]) // 部门列表
const deptTreeOptions = ref()
const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]) // 用户组列表
// 添加当前值的引用
const currentValue = ref<SimpleFlowNode | undefined>()
provide('formFields', formFields)
provide('formType', formType)
provide('roleList', roleOptions)
@@ -88,9 +82,11 @@ provide('deptList', deptOptions)
provide('userGroupList', userGroupOptions)
provide('deptTree', deptTreeOptions)
provide('startUserIds', props.startUserIds)
provide('tasks', [])
provide('processInstance', {})
const message = useMessage() // 国际化
const processNodeTree = ref<SimpleFlowNode | undefined>()
provide('processNodeTree', processNodeTree)
const errorDialogVisible = ref(false)
let errorNodes: SimpleFlowNode[] = []
@@ -112,70 +108,13 @@ const updateModel = () => {
}
}
// 加载流程数据
const loadProcessData = async (data: any) => {
try {
if (data) {
const parsedData = typeof data === 'string' ? JSON.parse(data) : data
processNodeTree.value = parsedData
currentValue.value = parsedData
// 确保数据加载后刷新视图
await nextTick()
if (simpleProcessModelRef.value?.refresh) {
await simpleProcessModelRef.value.refresh()
}
}
} catch (error) {
console.error('加载流程数据失败:', error)
}
}
// 监听属性变化
watch(
() => props.value,
async (newValue, oldValue) => {
if (newValue && newValue !== oldValue) {
await loadProcessData(newValue)
}
},
{ immediate: true, deep: true }
)
// 监听流程节点树变化,自动保存
watch(
() => processNodeTree.value,
async (newValue, oldValue) => {
if (newValue && oldValue && JSON.stringify(newValue) !== JSON.stringify(oldValue)) {
await saveSimpleFlowModel(newValue)
}
},
{ deep: true }
)
const saveSimpleFlowModel = async (simpleModelNode: SimpleFlowNode) => {
if (!simpleModelNode) {
return
}
// 校验节点
errorNodes = []
validateNode(simpleModelNode, errorNodes)
if (errorNodes.length > 0) {
errorDialogVisible.value = true
return
}
try {
if (props.modelId) {
// 编辑模式
const data = {
id: props.modelId,
simpleModel: simpleModelNode
}
await updateBpmSimpleModel(data)
}
// 无论是编辑还是新建模式,都更新当前值并触发事件
currentValue.value = simpleModelNode
processData.value = simpleModelNode
emits('success', simpleModelNode)
} catch (error) {
console.error('保存失败:', error)
@@ -246,18 +185,9 @@ onMounted(async () => {
deptTreeOptions.value = handleTree(deptOptions.value as DeptApi.DeptVO[], 'id')
// 获取用户组列表
userGroupOptions.value = await UserGroupApi.getUserGroupSimpleList()
// 加载流程数据
if (props.modelId) {
// 获取 SIMPLE 设计器模型
const result = await getBpmSimpleModel(props.modelId)
if (result) {
await loadProcessData(result)
} else {
updateModel()
}
} else if (props.value) {
await loadProcessData(props.value)
if (processData.value) {
processNodeTree.value = processData?.value
} else {
updateModel()
}
@@ -268,38 +198,5 @@ onMounted(async () => {
const simpleProcessModelRef = ref()
/** 获取当前流程数据 */
const getCurrentFlowData = async () => {
try {
if (simpleProcessModelRef.value) {
const data = await simpleProcessModelRef.value.getCurrentFlowData()
if (data) {
currentValue.value = data
return data
}
}
return currentValue.value
} catch (error) {
console.error('获取流程数据失败:', error)
return currentValue.value
}
}
// 刷新方法
const refresh = async () => {
try {
if (currentValue.value) {
await loadProcessData(currentValue.value)
}
} catch (error) {
console.error('刷新失败:', error)
}
}
defineExpose({
getCurrentFlowData,
updateModel,
loadProcessData,
refresh
})
defineExpose({})
</script>
@@ -3,6 +3,22 @@
<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 v-if="!readonly" size="default" @click="exportJson">
<Icon icon="ep:download" /> 导出
</el-button>
<el-button v-if="!readonly" size="default" @click="importJson">
<Icon icon="ep:upload" />导入
</el-button>
<!-- 用于打开本地文件-->
<input
v-if="!readonly"
type="file"
id="files"
ref="refFile"
style="display: none"
accept=".json"
@change="importLocalFile"
/>
<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>
@@ -34,6 +50,8 @@ import ProcessNodeTree from './ProcessNodeTree.vue'
import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from './consts'
import { useWatchNode } from './node'
import { ZoomOut, ZoomIn, ScaleToOriginal } from '@element-plus/icons-vue'
import { isString } from '@/utils/is'
import download from '@/utils/download'
defineOptions({
name: 'SimpleProcessModel'
@@ -52,7 +70,7 @@ const props = defineProps({
})
const emits = defineEmits<{
'save': [node: SimpleFlowNode | undefined]
save: [node: SimpleFlowNode | undefined]
}>()
const processNodeTree = useWatchNode(props)
@@ -85,6 +103,16 @@ const processReZoom = () => {
const errorDialogVisible = ref(false)
let errorNodes: SimpleFlowNode[] = []
const saveSimpleFlowModel = async () => {
errorNodes = []
validateNode(processNodeTree.value, errorNodes)
if (errorNodes.length > 0) {
errorDialogVisible.value = true
return
}
emits('save', processNodeTree.value)
}
// 校验节点设置。 暂时以 showText 为空 未节点错误配置
const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNode[]) => {
if (node) {
@@ -143,6 +171,28 @@ const getCurrentFlowData = async () => {
defineExpose({
getCurrentFlowData
})
/** 导出 JSON */
const exportJson = () => {
download.json(new Blob([JSON.stringify(processNodeTree.value)]), 'model.json')
}
/** 导入 JSON */
const refFile = ref()
const importJson = () => {
refFile.value.click()
}
const importLocalFile = () => {
const file = refFile.value.files[0]
const reader = new FileReader()
reader.readAsText(file)
reader.onload = function () {
if (isString(this.result)) {
processNodeTree.value = JSON.parse(this.result)
emits('save', processNodeTree.value)
}
}
}
</script>
<style lang="scss" scoped></style>
@@ -23,6 +23,16 @@ export enum NodeType {
*/
COPY_TASK_NODE = 12,
/**
* 延迟器节点
*/
DELAY_TIMER_NODE = 14,
/**
* 触发器节点
*/
TRIGGER_NODE = 15,
/**
* 条件节点
*/
@@ -39,7 +49,11 @@ export enum NodeType {
/**
* 包容分支节点 (对应包容网关)
*/
INCLUSIVE_BRANCH_NODE = 53
INCLUSIVE_BRANCH_NODE = 53,
/**
* 路由分支节点
*/
ROUTER_BRANCH_NODE = 54
}
export enum NodeId {
@@ -88,16 +102,27 @@ export interface SimpleFlowNode {
assignEmptyHandler?: AssignEmptyHandler
// 审批节点的审批人与发起人相同时,对应的处理类型
assignStartUserHandlerType?: number
// 条件类型
conditionType?: ConditionType
// 条件表达式
conditionExpression?: string
// 条件组
conditionGroups?: ConditionGroup
// 是否默认的条件
defaultFlow?: boolean
// 创建任务监听器
taskCreateListener?: ListenerHandler
// 创建任务监听器
taskAssignListener?: ListenerHandler
// 创建任务监听器
taskCompleteListener?: ListenerHandler
// 条件设置
conditionSetting?: ConditionSetting
// 活动的状态,用于前端节点状态展示
activityStatus?: TaskStatusEnum
// 延迟设置
delaySetting?: DelaySetting
// 路由分支
routerGroups?: RouterSetting[]
defaultFlowId?: string
// 签名
signEnable?: boolean
// 审批意见
reasonRequire?: boolean
// 触发器设置
triggerSetting?: TriggerSetting
}
// 候选人策略枚举 ( 用于审批节点。抄送节点 )
export enum CandidateStrategy {
@@ -215,6 +240,41 @@ export type AssignEmptyHandler = {
userIds?: number[]
}
/**
* 监听器的结构定义
*/
export type ListenerHandler = {
enable: boolean
path?: string
header?: HttpRequestParam[]
body?: HttpRequestParam[]
}
export type HttpRequestParam = {
key: string
type: number
value: string
}
export enum BpmHttpRequestParamTypeEnum {
/**
* 固定值
*/
FIXED_VALUE = 1,
/**
* 表单
*/
FROM_FORM = 2
}
export const BPM_HTTP_REQUEST_PARAM_TYPES = [
{
value: 1,
label: '固定值'
},
{
value: 2,
label: '表单'
}
]
// 审批拒绝类型枚举
export enum RejectHandlerType {
/**
@@ -308,6 +368,20 @@ export enum TimeUnitType {
DAY = 3
}
/**
* 条件节点设置结构定义,用于条件节点
*/
export type ConditionSetting = {
// 条件类型
conditionType?: ConditionType
// 条件表达式
conditionExpression?: string
// 条件组
conditionGroups?: ConditionGroup
// 是否默认的条件
defaultFlow?: boolean
}
// 条件配置类型 ( 用于条件节点配置 )
export enum ConditionType {
/**
@@ -382,8 +456,6 @@ export enum OperationButtonType {
* 条件规则结构定义
*/
export type ConditionRule = {
type: number
opName: string
opCode: string
leftSide: string
rightSide: string
@@ -398,6 +470,24 @@ export type ConditionGroup = {
// 条件数组
conditions: Condition[]
}
/**
* 条件组默认值
*/
export const DEFAULT_CONDITION_GROUP_VALUE = {
and: true,
conditions: [
{
and: true,
rules: [
{
opCode: '==',
leftSide: '',
rightSide: ''
}
]
}
]
}
/**
* 条件结构定义
@@ -413,12 +503,18 @@ NODE_DEFAULT_TEXT.set(NodeType.USER_TASK_NODE, '请配置审批人')
NODE_DEFAULT_TEXT.set(NodeType.COPY_TASK_NODE, '请配置抄送人')
NODE_DEFAULT_TEXT.set(NodeType.CONDITION_NODE, '请设置条件')
NODE_DEFAULT_TEXT.set(NodeType.START_USER_NODE, '请设置发起人')
NODE_DEFAULT_TEXT.set(NodeType.DELAY_TIMER_NODE, '请设置延迟器')
NODE_DEFAULT_TEXT.set(NodeType.ROUTER_BRANCH_NODE, '请设置路由节点')
NODE_DEFAULT_TEXT.set(NodeType.TRIGGER_NODE, '请设置触发器')
export const NODE_DEFAULT_NAME = new Map<number, string>()
NODE_DEFAULT_NAME.set(NodeType.USER_TASK_NODE, '审批人')
NODE_DEFAULT_NAME.set(NodeType.COPY_TASK_NODE, '抄送人')
NODE_DEFAULT_NAME.set(NodeType.CONDITION_NODE, '条件')
NODE_DEFAULT_NAME.set(NodeType.START_USER_NODE, '发起人')
NODE_DEFAULT_NAME.set(NodeType.DELAY_TIMER_NODE, '延迟器')
NODE_DEFAULT_NAME.set(NodeType.ROUTER_BRANCH_NODE, '路由分支')
NODE_DEFAULT_NAME.set(NodeType.TRIGGER_NODE, '触发器')
// 候选人策略。暂时不从字典中取。 后续可能调整。控制显示顺序
export const CANDIDATE_STRATEGY: DictDataVO[] = [
@@ -451,8 +547,8 @@ export const APPROVE_METHODS: DictDataVO[] = [
]
export const CONDITION_CONFIG_TYPES: DictDataVO[] = [
{ label: '条件表达式', value: ConditionType.EXPRESSION },
{ label: '条件规则', value: ConditionType.RULE }
{ label: '条件规则', value: ConditionType.RULE },
{ label: '条件表达式', value: ConditionType.EXPRESSION }
]
// 时间单位类型
@@ -566,5 +662,101 @@ export enum ProcessVariableEnum {
/**
* 发起用户 ID
*/
START_USER_ID = 'PROCESS_START_USER_ID'
START_USER_ID = 'PROCESS_START_USER_ID',
/**
* 发起时间
*/
START_TIME = 'PROCESS_START_TIME',
/**
* 流程定义名称
*/
PROCESS_DEFINITION_NAME = 'PROCESS_DEFINITION_NAME'
}
/**
* 延迟设置
*/
export type DelaySetting = {
// 延迟类型
delayType: number
// 延迟时间表达式
delayTime: string
}
/**
* 延迟类型
*/
export enum DelayTypeEnum {
/**
* 固定时长
*/
FIXED_TIME_DURATION = 1,
/**
* 固定日期时间
*/
FIXED_DATE_TIME = 2
}
export const DELAY_TYPE = [
{ label: '固定时长', value: DelayTypeEnum.FIXED_TIME_DURATION },
{ label: '固定日期', value: DelayTypeEnum.FIXED_DATE_TIME }
]
/**
* 路由分支结构定义
*/
export type RouterSetting = {
nodeId: string
conditionType: ConditionType
conditionExpression: string
conditionGroups: ConditionGroup
}
// ==================== 触发器相关定义 ====================
/**
* 触发器节点结构定义
*/
export type TriggerSetting = {
type: TriggerTypeEnum
httpRequestSetting?: HttpRequestTriggerSetting
normalFormSetting?: NormalFormTriggerSetting
}
/**
* 触发器类型枚举
*/
export enum TriggerTypeEnum {
/**
* 发送 HTTP 请求触发器
*/
HTTP_REQUEST = 1,
/**
* 更新流程表单触发器
*/
UPDATE_NORMAL_FORM = 2 // TODO @jason:FORM_UPDATE?
}
/**
* HTTP 请求触发器结构定义
*/
export type HttpRequestTriggerSetting = {
// 请求 URL
url: string
// 请求头参数设置
header?: HttpRequestParam[]
// 请求体参数设置
body?: HttpRequestParam[]
// 请求响应设置
response?: Record<string, string>[]
}
/**
* 流程表单触发器配置结构定义
*/
export type NormalFormTriggerSetting = {
// 更新表单字段
updateFormFields?: Record<string, any>
}
export const TRIGGER_TYPES: DictDataVO[] = [
{ label: 'HTTP 请求', value: TriggerTypeEnum.HTTP_REQUEST },
{ label: '修改表单数据', value: TriggerTypeEnum.UPDATE_NORMAL_FORM }
]
@@ -1,4 +1,3 @@
import { cloneDeep } from 'lodash-es'
import { TaskStatusEnum } from '@/api/bpm/task'
import * as RoleApi from '@/api/system/role'
import * as DeptApi from '@/api/system/dept'
@@ -14,9 +13,12 @@ import {
NODE_DEFAULT_NAME,
AssignStartUserHandlerType,
AssignEmptyHandlerType,
FieldPermissionType
FieldPermissionType,
HttpRequestParam,
ProcessVariableEnum
} from './consts'
import { parseFormFields } from '@/components/FormCreate/src/utils/index'
import { parseFormFields } from '@/components/FormCreate/src/utils'
export function useWatchNode(props: { flowNode: SimpleFlowNode }): Ref<SimpleFlowNode> {
const node = ref<SimpleFlowNode>(props.flowNode)
watch(
@@ -46,9 +48,9 @@ export function useFormFieldsPermission(defaultPermission: FieldPermissionType)
// 字段权限配置. 需要有 field, title, permissioin 属性
const fieldsPermissionConfig = ref<Array<Record<string, any>>>([])
const formType = inject<Ref<number>>('formType') // 表单类型
const formType = inject<Ref<number | undefined>>('formType', ref()) // 表单类型
const formFields = inject<Ref<string[]>>('formFields') // 流程表单字段
const formFields = inject<Ref<string[]>>('formFields', ref([])) // 流程表单字段
const getNodeConfigFormFields = (nodeFormFields?: Array<Record<string, string>>) => {
nodeFormFields = toRaw(nodeFormFields)
@@ -104,16 +106,32 @@ export function useFormFieldsPermission(defaultPermission: FieldPermissionType)
getNodeConfigFormFields
}
}
/**
* @description 获取表单的字段
* @description 获取流程表单的字段
*/
export function useFormFields() {
const formFields = inject<Ref<string[]>>('formFields') // 流程表单字段
const formFields = inject<Ref<string[]>>('formFields', ref([])) // 流程表单字段
return parseFormCreateFields(unref(formFields))
}
// TODO @芋艿:后续需要把各种类似 useFormFieldsPermission 的逻辑,抽成一个通用方法。
/**
* @description 获取流程表单的字段和发起人字段
*/
export function useFormFieldsAndStartUser() {
const injectFormFields = inject<Ref<string[]>>('formFields', ref([])) // 流程表单字段
const formFields = parseFormCreateFields(unref(injectFormFields))
// 添加发起人
formFields.unshift({
field: ProcessVariableEnum.START_USER_ID,
title: '发起人',
required: true
})
return formFields
}
export type UserTaskFormType = {
//candidateParamArray: any[]
candidateStrategy: CandidateStrategy
approveMethod: ApproveMethodType
roleIds?: number[] // 角色
@@ -136,10 +154,29 @@ export type UserTaskFormType = {
timeDuration?: number
maxRemindCount?: number
buttonsSetting: any[]
taskCreateListenerEnable?: boolean
taskCreateListenerPath?: string
taskCreateListener?: {
header: HttpRequestParam[]
body: HttpRequestParam[]
}
taskAssignListenerEnable?: boolean
taskAssignListenerPath?: string
taskAssignListener?: {
header: HttpRequestParam[]
body: HttpRequestParam[]
}
taskCompleteListenerEnable?: boolean
taskCompleteListenerPath?: string
taskCompleteListener?: {
header: HttpRequestParam[]
body: HttpRequestParam[]
}
signEnable: boolean
reasonRequire: boolean
}
export type CopyTaskFormType = {
// candidateParamArray: any[]
candidateStrategy: CandidateStrategy
roleIds?: number[] // 角色
deptIds?: number[] // 部门
@@ -156,13 +193,13 @@ export type CopyTaskFormType = {
* @description 节点表单数据。 用于审批节点、抄送节点
*/
export function useNodeForm(nodeType: NodeType) {
const roleOptions = inject<Ref<RoleApi.RoleVO[]>>('roleList') // 角色列表
const postOptions = inject<Ref<PostApi.PostVO[]>>('postList') // 岗位列表
const userOptions = inject<Ref<UserApi.UserVO[]>>('userList') // 用户列表
const deptOptions = inject<Ref<DeptApi.DeptVO[]>>('deptList') // 部门列表
const userGroupOptions = inject<Ref<UserGroupApi.UserGroupVO[]>>('userGroupList') // 用户组列表
const deptTreeOptions = inject('deptTree') // 部门树
const formFields = inject<Ref<string[]>>('formFields') // 流程表单字段
const roleOptions = inject<Ref<RoleApi.RoleVO[]>>('roleList', ref([])) // 角色列表
const postOptions = inject<Ref<PostApi.PostVO[]>>('postList', ref([])) // 岗位列表
const userOptions = inject<Ref<UserApi.UserVO[]>>('userList', ref([])) // 用户列表
const deptOptions = inject<Ref<DeptApi.DeptVO[]>>('deptList', ref([])) // 部门列表
const userGroupOptions = inject<Ref<UserGroupApi.UserGroupVO[]>>('userGroupList', ref([])) // 用户组列表
const deptTreeOptions = inject('deptTree', ref()) // 部门树
const formFields = inject<Ref<string[]>>('formFields', ref([])) // 流程表单字段
const configForm = ref<UserTaskFormType | CopyTaskFormType>()
if (nodeType === NodeType.USER_TASK_NODE) {
configForm.value = {
@@ -26,121 +26,11 @@
</div>
</template>
<div>
<div class="mb-3 font-size-16px" v-if="currentNode.defaultFlow"
<div class="mb-3 font-size-16px" v-if="currentNode.conditionSetting?.defaultFlow"
>未满足其它条件时,将进入此分支(该分支不可编辑和删除)</div
>
<div v-else>
<el-form ref="formRef" :model="currentNode" :rules="formRules" label-position="top">
<el-form-item label="配置方式" prop="conditionType">
<el-radio-group v-model="currentNode.conditionType" @change="changeConditionType">
<el-radio
v-for="(dict, index) in conditionConfigTypes"
:key="index"
:value="dict.value"
:label="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
v-if="currentNode.conditionType === 1"
label="条件表达式"
prop="conditionExpression"
>
<el-input
type="textarea"
v-model="currentNode.conditionExpression"
clearable
style="width: 100%"
/>
</el-form-item>
<el-form-item v-if="currentNode.conditionType === 2" label="条件规则">
<div class="condition-group-tool">
<div class="flex items-center">
<div class="mr-4">条件组关系</div>
<el-switch
v-model="conditionGroups.and"
inline-prompt
active-text="且"
inactive-text="或"
/>
</div>
</div>
<el-space direction="vertical" :spacer="conditionGroups.and ? '且' : '或'">
<el-card
class="condition-group"
style="width: 530px"
v-for="(condition, cIdx) in conditionGroups.conditions"
:key="cIdx"
>
<div class="condition-group-delete" v-if="conditionGroups.conditions.length > 1">
<Icon
color="#0089ff"
icon="ep:circle-close-filled"
:size="18"
@click="deleteConditionGroup(cIdx)"
/>
</div>
<template #header>
<div class="flex items-center justify-between">
<div>条件组</div>
<div class="flex">
<div class="mr-4">规则关系</div>
<el-switch
v-model="condition.and"
inline-prompt
active-text="且"
inactive-text="或"
/>
</div>
</div>
</template>
<div class="flex pt-2" v-for="(rule, rIdx) in condition.rules" :key="rIdx">
<div class="mr-2">
<el-select style="width: 160px" v-model="rule.leftSide">
<el-option
v-for="(item, index) in fieldOptions"
:key="index"
:label="item.title"
:value="item.field"
:disabled="!item.required"
/>
</el-select>
</div>
<div class="mr-2">
<el-select v-model="rule.opCode" style="width: 100px">
<el-option
v-for="item in COMPARISON_OPERATORS"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</div>
<div class="mr-2">
<el-input v-model="rule.rightSide" style="width: 160px" />
</div>
<div class="mr-1 flex items-center" v-if="condition.rules.length > 1">
<Icon
icon="ep:delete"
:size="18"
@click="deleteConditionRule(condition, rIdx)"
/>
</div>
<div class="flex items-center">
<Icon icon="ep:plus" :size="18" @click="addConditionRule(condition, rIdx)" />
</div>
</div>
</el-card>
</el-space>
<div title="添加条件组" class="mt-4 cursor-pointer">
<Icon color="#0089ff" icon="ep:plus" :size="24" @click="addConditionGroup" />
</div>
</el-form-item>
</el-form>
<Condition ref="conditionRef" v-model="condition" />
</div>
</div>
<template #footer>
@@ -155,33 +45,16 @@
<script setup lang="ts">
import {
SimpleFlowNode,
CONDITION_CONFIG_TYPES,
ConditionType,
COMPARISON_OPERATORS,
ConditionGroup,
Condition,
ConditionRule,
ProcessVariableEnum
} from '../consts'
import { getDefaultConditionNodeName } from '../utils'
import { useFormFields } from '../node'
import { BpmModelFormType } from '@/utils/constants'
import { useFormFieldsAndStartUser } from '../node'
import Condition from './components/Condition.vue'
const message = useMessage() // 消息弹窗
defineOptions({
name: 'ConditionNodeConfig'
})
const formType = inject<Ref<number>>('formType') // 表单类型
const conditionConfigTypes = computed(() => {
return CONDITION_CONFIG_TYPES.filter((item) => {
// 业务表单暂时去掉条件规则选项
if (formType?.value === BpmModelFormType.CUSTOM && item.value === ConditionType.RULE) {
return false
} else {
return true
}
})
})
const props = defineProps({
conditionNode: {
type: Object as () => SimpleFlowNode,
@@ -193,12 +66,10 @@ const props = defineProps({
}
})
const settingVisible = ref(false)
const currentNode = ref<SimpleFlowNode>(props.conditionNode)
const condition = ref<any>()
const open = () => {
if (currentNode.value.conditionType === ConditionType.RULE) {
if (currentNode.value.conditionGroups) {
conditionGroups.value = currentNode.value.conditionGroups
}
}
condition.value = currentNode.value.conditionSetting
settingVisible.value = true
}
@@ -219,10 +90,10 @@ const blurEvent = () => {
showInput.value = false
currentNode.value.name =
currentNode.value.name ||
getDefaultConditionNodeName(props.nodeIndex, currentNode.value?.defaultFlow)
getDefaultConditionNodeName(props.nodeIndex, currentNode.value?.conditionSetting?.defaultFlow)
}
const currentNode = ref<SimpleFlowNode>(props.conditionNode)
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
@@ -239,31 +110,27 @@ const handleClose = async (done: (cancel?: boolean) => void) => {
done()
}
}
// 表单校验规则
const formRules = reactive({
conditionType: [{ required: true, message: '配置方式不能为空', trigger: 'blur' }],
conditionExpression: [{ required: true, message: '条件表达式不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
const conditionRef = ref()
// 保存配置
const saveConfig = async () => {
if (!currentNode.value.defaultFlow) {
if (!currentNode.value.conditionSetting?.defaultFlow) {
// 校验表单
if (!formRef) return false
const valid = await formRef.value.validate()
const valid = await conditionRef.value.validate()
if (!valid) return false
const showText = getShowText()
if (!showText) {
return false
}
currentNode.value.showText = showText
if (currentNode.value.conditionType === ConditionType.EXPRESSION) {
currentNode.value.conditionGroups = undefined
currentNode.value.conditionSetting!.conditionType = condition.value?.conditionType
if (currentNode.value.conditionSetting?.conditionType === ConditionType.EXPRESSION) {
currentNode.value.conditionSetting.conditionGroups = undefined
currentNode.value.conditionSetting.conditionExpression = condition.value?.conditionExpression
}
if (currentNode.value.conditionType === ConditionType.RULE) {
currentNode.value.conditionExpression = undefined
currentNode.value.conditionGroups = conditionGroups.value
if (currentNode.value.conditionSetting!.conditionType === ConditionType.RULE) {
currentNode.value.conditionSetting!.conditionExpression = undefined
currentNode.value.conditionSetting!.conditionGroups = condition.value?.conditionGroups
}
}
settingVisible.value = false
@@ -271,16 +138,16 @@ const saveConfig = async () => {
}
const getShowText = (): string => {
let showText = ''
if (currentNode.value.conditionType === ConditionType.EXPRESSION) {
if (currentNode.value.conditionExpression) {
showText = `表达式:${currentNode.value.conditionExpression}`
if (condition.value?.conditionType === ConditionType.EXPRESSION) {
if (condition.value.conditionExpression) {
showText = `表达式:${condition.value.conditionExpression}`
}
}
if (currentNode.value.conditionType === ConditionType.RULE) {
if (condition.value?.conditionType === ConditionType.RULE) {
// 条件组是否为与关系
const groupAnd = conditionGroups.value.and
const groupAnd = condition.value.conditionGroups?.and
let warningMesg: undefined | string = undefined
const conditionGroup = conditionGroups.value.conditions.map((item) => {
const conditionGroup = condition.value.conditionGroups?.conditions.map((item) => {
return (
'(' +
item.rules
@@ -303,85 +170,17 @@ const getShowText = (): string => {
message.warning(warningMesg)
showText = ''
} else {
showText = conditionGroup.join(groupAnd ? ' 且 ' : ' 或 ')
showText = conditionGroup!.join(groupAnd ? ' 且 ' : ' 或 ')
}
}
return showText
}
// 改变条件配置方式
const changeConditionType = () => {}
const conditionGroups = ref<ConditionGroup>({
and: true,
conditions: [
{
and: true,
rules: [
{
type: 1,
opName: '等于',
opCode: '==',
leftSide: '',
rightSide: ''
}
]
}
]
})
// 添加条件组
const addConditionGroup = () => {
const condition = {
and: true,
rules: [
{
type: 1,
opName: '等于',
opCode: '==',
leftSide: '',
rightSide: ''
}
]
}
conditionGroups.value.conditions.push(condition)
}
// 删除条件组
const deleteConditionGroup = (idx: number) => {
conditionGroups.value.conditions.splice(idx, 1)
}
// 添加条件规则
const addConditionRule = (condition: Condition, idx: number) => {
const rule: ConditionRule = {
type: 1,
opName: '等于',
opCode: '==',
leftSide: '',
rightSide: ''
}
condition.rules.splice(idx + 1, 0, rule)
}
const deleteConditionRule = (condition: Condition, idx: number) => {
condition.rules.splice(idx, 1)
}
const fieldsInfo = useFormFields()
/** 条件规则可选择的表单字段 */
const fieldOptions = computed(() => {
const fieldsCopy = fieldsInfo.slice()
// 固定添加发起人 ID 字段
fieldsCopy.unshift({
field: ProcessVariableEnum.START_USER_ID,
title: '发起人',
required: true
})
return fieldsCopy
})
// 流程表单字段和发起人字段
const fieldOptions = useFormFieldsAndStartUser()
/** 获取字段名称 */
const getFieldTitle = (field: string) => {
const item = fieldOptions.value.find((item) => item.field === field)
const item = fieldOptions.find((item) => item.field === field)
return item?.title
}
@@ -0,0 +1,190 @@
<template>
<el-drawer
:append-to-body="true"
v-model="settingVisible"
:show-close="false"
:size="550"
:before-close="saveConfig"
>
<template #header>
<div class="config-header">
<input
v-if="showInput"
type="text"
class="config-editable-input"
@blur="blurEvent()"
v-mountedFocus
v-model="nodeName"
:placeholder="nodeName"
/>
<div v-else class="node-name">
{{ nodeName }} <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" />
</div>
<div class="divide-line"></div>
</div>
</template>
<div>
<el-form ref="formRef" :model="configForm" label-position="top" :rules="formRules">
<el-form-item label="延迟时间" prop="delayType">
<el-radio-group v-model="configForm.delayType">
<el-radio-button
v-for="item in DELAY_TYPE"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-radio-group>
</el-form-item>
<el-form-item v-if="configForm.delayType === DelayTypeEnum.FIXED_TIME_DURATION">
<el-form-item prop="timeDuration">
<el-input-number
class="mr-2"
:style="{ width: '100px' }"
v-model="configForm.timeDuration"
:min="1"
controls-position="right"
/>
</el-form-item>
<el-select v-model="configForm.timeUnit" class="mr-2" :style="{ width: '100px' }">
<el-option
v-for="item in TIME_UNIT_TYPES"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-text>后进入下一节点</el-text>
</el-form-item>
<el-form-item v-if="configForm.delayType === DelayTypeEnum.FIXED_DATE_TIME" prop="dateTime">
<el-date-picker
class="mr-2"
v-model="configForm.dateTime"
type="datetime"
placeholder="请选择日期和时间"
value-format="YYYY-MM-DDTHH:mm:ss"
/>
<el-text>后进入下一节点</el-text>
</el-form-item>
</el-form>
</div>
<template #footer>
<el-divider />
<div>
<el-button type="primary" @click="saveConfig">确 定</el-button>
<el-button @click="closeDrawer">取 消</el-button>
</div>
</template>
</el-drawer>
</template>
<script setup lang="ts">
import {
SimpleFlowNode,
NodeType,
TIME_UNIT_TYPES,
TimeUnitType,
DelayTypeEnum,
DELAY_TYPE
} from '../consts'
import { useWatchNode, useDrawer, useNodeName } from '../node'
import { convertTimeUnit } from '../utils'
defineOptions({
name: 'DelayTimerNodeConfig'
})
const props = defineProps({
flowNode: {
type: Object as () => SimpleFlowNode,
required: true
}
})
// 抽屉配置
const { settingVisible, closeDrawer, openDrawer } = useDrawer()
// 当前节点
const currentNode = useWatchNode(props)
// 节点名称
const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.DELAY_TIMER_NODE)
// 抄送人表单配置
const formRef = ref() // 表单 Ref
// 表单校验规则
const formRules = reactive({
delayType: [{ required: true, message: '延迟时间不能为空', trigger: 'change' }],
timeDuration: [{ required: true, message: '延迟时间不能为空', trigger: 'change' }],
dateTime: [{ required: true, message: '延迟时间不能为空', trigger: 'change' }]
})
// 配置表单数据
const configForm = ref({
delayType: DelayTypeEnum.FIXED_TIME_DURATION,
timeDuration: 1,
timeUnit: TimeUnitType.HOUR,
dateTime: ''
})
// 保存配置
const saveConfig = async () => {
if (!formRef) return false
const valid = await formRef.value.validate()
if (!valid) return false
const showText = getShowText()
if (!showText) return false
currentNode.value.name = nodeName.value!
currentNode.value.showText = showText
if (configForm.value.delayType === DelayTypeEnum.FIXED_TIME_DURATION) {
currentNode.value.delaySetting = {
delayType: configForm.value.delayType,
delayTime: getIsoTimeDuration()
}
}
if (configForm.value.delayType === DelayTypeEnum.FIXED_DATE_TIME) {
currentNode.value.delaySetting = {
delayType: configForm.value.delayType,
delayTime: configForm.value.dateTime
}
}
settingVisible.value = false
return true
}
const getShowText = (): string => {
let showText = ''
if (configForm.value.delayType === DelayTypeEnum.FIXED_TIME_DURATION) {
showText = `延迟${configForm.value.timeDuration}${TIME_UNIT_TYPES.find((item) => item.value === configForm.value.timeUnit).label}`
}
if (configForm.value.delayType === DelayTypeEnum.FIXED_DATE_TIME) {
showText = `延迟至${configForm.value.dateTime.replace('T', ' ')}`
}
return showText
}
const getIsoTimeDuration = () => {
let strTimeDuration = 'PT'
if (configForm.value.timeUnit === TimeUnitType.MINUTE) {
strTimeDuration += configForm.value.timeDuration + 'M'
}
if (configForm.value.timeUnit === TimeUnitType.HOUR) {
strTimeDuration += configForm.value.timeDuration + 'H'
}
if (configForm.value.timeUnit === TimeUnitType.DAY) {
strTimeDuration += configForm.value.timeDuration + 'D'
}
return strTimeDuration
}
// 显示延迟器节点配置, 由父组件传过来
const showDelayTimerNodeConfig = (node: SimpleFlowNode) => {
nodeName.value = node.name
if (node.delaySetting) {
configForm.value.delayType = node.delaySetting.delayType
// 固定时长
if (configForm.value.delayType === DelayTypeEnum.FIXED_TIME_DURATION) {
const strTimeDuration = node.delaySetting.delayTime
let parseTime = strTimeDuration.slice(2, strTimeDuration.length - 1)
let parseTimeUnit = strTimeDuration.slice(strTimeDuration.length - 1)
configForm.value.timeDuration = parseInt(parseTime)
configForm.value.timeUnit = convertTimeUnit(parseTimeUnit)
}
// 固定日期时间
if (configForm.value.delayType === DelayTypeEnum.FIXED_DATE_TIME) {
configForm.value.dateTime = node.delaySetting.delayTime
}
}
}
defineExpose({ openDrawer, showDelayTimerNodeConfig }) // 暴露方法给父组件
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,201 @@
<template>
<el-drawer
:append-to-body="true"
v-model="settingVisible"
:show-close="false"
:size="630"
:before-close="saveConfig"
>
<template #header>
<div class="config-header">
<input
v-if="showInput"
type="text"
class="config-editable-input"
@blur="blurEvent()"
v-mountedFocus
v-model="nodeName"
:placeholder="nodeName"
/>
<div v-else class="node-name">
{{ nodeName }} <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" />
</div>
<div class="divide-line"></div>
</div>
</template>
<div>
<el-form label-position="top">
<el-card class="mb-15px" v-for="(item, index) in routerGroups" :key="index">
<template #header>
<div class="flex flex-items-center">
<el-text size="large">路由{{ index + 1 }}</el-text>
<el-select class="ml-15px" v-model="item.nodeId" style="width: 180px">
<el-option
v-for="node in nodeOptions"
:key="node.value"
:label="node.label"
:value="node.value"
/>
</el-select>
<el-button class="mla" type="danger" link @click="deleteRouterGroup(index)">
删除
</el-button>
</div>
</template>
<Condition
:ref="($event) => (conditionRef[index] = $event)"
v-model="routerGroups[index]"
/>
</el-card>
</el-form>
<el-button class="w-1/1" type="primary" :icon="Plus" @click="addRouterGroup">
新增路由分支
</el-button>
</div>
<template #footer>
<el-divider />
<div>
<el-button type="primary" @click="saveConfig">确 定</el-button>
<el-button @click="closeDrawer">取 消</el-button>
</div>
</template>
</el-drawer>
</template>
<script setup lang="ts">
import { Plus } from '@element-plus/icons-vue'
import { SimpleFlowNode, NodeType, ConditionType, RouterSetting } from '../consts'
import { useWatchNode, useDrawer, useNodeName } from '../node'
import Condition from './components/Condition.vue'
defineOptions({
name: 'RouterNodeConfig'
})
const message = useMessage() // 消息弹窗
const props = defineProps({
flowNode: {
type: Object as () => SimpleFlowNode,
required: true
}
})
const processNodeTree = inject<Ref<SimpleFlowNode>>('processNodeTree')
// 抽屉配置
const { settingVisible, closeDrawer, openDrawer } = useDrawer()
// 当前节点
const currentNode = useWatchNode(props)
// 节点名称
const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.ROUTER_BRANCH_NODE)
const routerGroups = ref<RouterSetting[]>([])
const nodeOptions = ref<any>([])
const conditionRef = ref([])
/** 保存配置 */
const saveConfig = async () => {
// 校验表单
let valid = true
for (const item of conditionRef.value) {
if (item && !(await item.validate())) {
valid = false
}
}
if (!valid) return false
const showText = getShowText()
if (!showText) return false
currentNode.value.name = nodeName.value!
currentNode.value.showText = showText
currentNode.value.routerGroups = routerGroups.value
settingVisible.value = false
return true
}
// 显示路由分支节点配置, 由父组件传过来
const showRouteNodeConfig = (node: SimpleFlowNode) => {
getRouterNode(processNodeTree?.value)
routerGroups.value = []
nodeName.value = node.name
if (node.routerGroups) {
routerGroups.value = node.routerGroups
}
}
const getShowText = () => {
if (!routerGroups.value || !Array.isArray(routerGroups.value) || routerGroups.value.length <= 0) {
message.warning('请配置路由!')
return ''
}
for (const route of routerGroups.value) {
if (!route.nodeId || !route.conditionType) {
message.warning('请完善路由配置项!')
return ''
}
if (route.conditionType === ConditionType.EXPRESSION && !route.conditionExpression) {
message.warning('请完善路由配置项!')
return ''
}
if (route.conditionType === ConditionType.RULE) {
for (const condition of route.conditionGroups.conditions) {
for (const rule of condition.rules) {
if (!rule.leftSide || !rule.rightSide) {
message.warning('请完善路由配置项!')
return ''
}
}
}
}
}
return `${routerGroups.value.length}条路由分支`
}
const addRouterGroup = () => {
routerGroups.value.push({
nodeId: '',
conditionType: ConditionType.RULE,
conditionExpression: '',
conditionGroups: {
and: true,
conditions: [
{
and: true,
rules: [
{
opCode: '==',
leftSide: '',
rightSide: ''
}
]
}
]
}
})
}
const deleteRouterGroup = (index: number) => {
routerGroups.value.splice(index, 1)
}
// 递归获取所有节点
const getRouterNode = (node) => {
// TODO 最好还需要满足以下要求
// 并行分支、包容分支内部节点不能跳转到外部节点
// 条件分支节点可以向上跳转到外部节点
while (true) {
if (!node) break
if (node.type !== NodeType.ROUTER_BRANCH_NODE && node.type !== NodeType.CONDITION_NODE) {
nodeOptions.value.push({
label: node.name,
value: node.id
})
}
if (!node.childNode || node.type === NodeType.END_EVENT_NODE) {
break
}
if (node.conditionNodes && node.conditionNodes.length) {
node.conditionNodes.forEach((item) => {
getRouterNode(item)
})
}
node = node.childNode
}
}
defineExpose({ openDrawer, showRouteNodeConfig }) // 暴露方法给父组件
</script>
@@ -0,0 +1,334 @@
<template>
<el-drawer
:append-to-body="true"
v-model="settingVisible"
:show-close="false"
:size="550"
:before-close="saveConfig"
>
<template #header>
<div class="config-header">
<input
v-if="showInput"
type="text"
class="config-editable-input"
@blur="blurEvent()"
v-mountedFocus
v-model="nodeName"
:placeholder="nodeName"
/>
<div v-else class="node-name">
{{ nodeName }} <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" />
</div>
<div class="divide-line"></div>
</div>
</template>
<div>
<el-form ref="formRef" :model="configForm" label-position="top" :rules="formRules">
<el-form-item label="触发器类型" prop="type">
<el-select v-model="configForm.type">
<el-option
v-for="(item, index) in TRIGGER_TYPES"
:key="index"
:value="item.value"
:label="item.label"
/>
</el-select>
</el-form-item>
<!-- HTTP 请求触发器 -->
<div
v-if="configForm.type === TriggerTypeEnum.HTTP_REQUEST && configForm.httpRequestSetting"
>
<el-form-item>
<el-alert
title="仅支持 POST 请求,以请求体方式接收参数"
type="warning"
show-icon
:closable="false"
/>
</el-form-item>
<!-- 请求地址-->
<el-form-item label="请求地址" prop="httpRequestSetting.url">
<el-input v-model="configForm.httpRequestSetting.url" />
</el-form-item>
<!-- 请求头,请求体设置-->
<HttpRequestParamSetting
:header="configForm.httpRequestSetting.header"
:body="configForm.httpRequestSetting.body"
:bind="'httpRequestSetting'"
/>
<!-- 返回值设置-->
<el-form-item label="返回值">
<el-alert
title="通过请求返回值, 可以修改流程表单的值"
type="warning"
show-icon
:closable="false"
/>
</el-form-item>
<el-form-item>
<div
class="flex pt-2"
v-for="(item, index) in configForm.httpRequestSetting.response"
:key="index"
>
<div class="mr-2">
<el-form-item
:prop="`httpRequestSetting.response.${index}.key`"
:rules="{
required: true,
message: '表单字段不能为空',
trigger: 'blur'
}"
>
<el-select class="w-160px!" v-model="item.key" placeholder="请选择表单字段">
<el-option
v-for="(field, fIdx) in formFields"
:key="fIdx"
:label="field.title"
:value="field.field"
:disabled="!field.required"
/>
</el-select>
</el-form-item>
</div>
<div class="mr-2">
<el-form-item
:prop="`httpRequestSetting.response.${index}.value`"
:rules="{
required: true,
message: '请求返回字段不能为空',
trigger: 'blur'
}"
>
<el-input class="w-160px" v-model="item.value" placeholder="请求返回字段" />
</el-form-item>
</div>
<div class="mr-1 pt-1 cursor-pointer">
<Icon
icon="ep:delete"
:size="18"
@click="deleteHttpResponseSetting(configForm.httpRequestSetting.response!, index)"
/>
</div>
</div>
<el-button
type="primary"
text
@click="addHttpResponseSetting(configForm.httpRequestSetting.response!)"
>
<Icon icon="ep:plus" class="mr-5px" />添加一行
</el-button>
</el-form-item>
</div>
<div
v-if="
configForm.type === TriggerTypeEnum.UPDATE_NORMAL_FORM && configForm.normalFormSetting
"
>
<el-divider content-position="left">修改表单设置</el-divider>
<div
class="flex items-center"
v-for="key in Object.keys(configForm.normalFormSetting.updateFormFields!)"
:key="key"
>
<div class="mr-2 flex items-center">
<el-form-item>
<el-select
class="w-160px!"
:model-value="key"
@update:model-value="(newKey) => updateFormFieldKey(key, newKey)"
placeholder="请选择表单字段"
:disabled="key !== ''"
>
<el-option
v-for="(field, fIdx) in optionalUpdateFormFields"
:key="fIdx"
:label="field.title"
:value="field.field"
:disabled="field.disabled"
/>
</el-select>
</el-form-item>
</div>
<div class="mx-2"><el-form-item>的值设置为</el-form-item></div>
<div class="mr-2">
<el-form-item
:prop="`normalFormSetting.updateFormFields.${key}`"
:rules="{
required: true,
message: '值不能为空',
trigger: 'blur'
}"
>
<el-input
class="w-160px"
v-model="configForm.normalFormSetting.updateFormFields![key]"
placeholder="请输入"
:disabled="!key"
/>
</el-form-item>
</div>
<div class="mr-1 pt-1 cursor-pointer">
<el-form-item>
<Icon icon="ep:delete" :size="18" @click="deleteFormFieldSetting(key)" />
</el-form-item>
</div>
</div>
<el-button type="primary" text @click="addFormFieldSetting()">
<Icon icon="ep:plus" class="mr-5px" />添加修改字段
</el-button>
</div>
</el-form>
</div>
<template #footer>
<el-divider />
<div>
<el-button type="primary" @click="saveConfig">确 定</el-button>
<el-button @click="closeDrawer">取 消</el-button>
</div>
</template>
</el-drawer>
</template>
<script setup lang="ts">
import { SimpleFlowNode, NodeType, TriggerSetting, TRIGGER_TYPES, TriggerTypeEnum } from '../consts'
import { useWatchNode, useDrawer, useNodeName, useFormFields } from '../node'
import HttpRequestParamSetting from './components/HttpRequestParamSetting.vue'
defineOptions({
name: 'TriggerNodeConfig'
})
const props = defineProps({
flowNode: {
type: Object as () => SimpleFlowNode,
required: true
}
})
const message = useMessage() // 消息弹窗
// 抽屉配置
const { settingVisible, closeDrawer, openDrawer } = useDrawer()
// 当前节点
const currentNode = useWatchNode(props)
// 节点名称
const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.TRIGGER_NODE)
// 触发器表单配置
const formRef = ref() // 表单 Ref
// 表单校验规则
const formRules = reactive({
type: [{ required: true, message: '触发器类型不能为空', trigger: 'change' }],
'httpRequestSetting.url': [{ required: true, message: '请求地址不能为空', trigger: 'blur' }]
})
// 触发器配置表单数据
const configForm = ref<TriggerSetting>({
type: TriggerTypeEnum.HTTP_REQUEST,
httpRequestSetting: {
url: '',
header: [],
body: [],
response: []
},
normalFormSetting: { updateFormFields: {} }
})
// 流程表单字段
const formFields = useFormFields()
// 可选的修改的表单字段
const optionalUpdateFormFields = computed(() => {
const usedFields = Object.keys(configForm.value.normalFormSetting?.updateFormFields || {})
return formFields.map((field) => ({
title: field.title,
field: field.field,
disabled: usedFields.includes(field.field)
}))
})
const updateFormFieldKey = (oldKey: string, newKey: string) => {
if (!configForm.value.normalFormSetting?.updateFormFields) return
const value = configForm.value.normalFormSetting.updateFormFields[oldKey]
delete configForm.value.normalFormSetting.updateFormFields[oldKey]
configForm.value.normalFormSetting.updateFormFields[newKey] = value
}
/** 添加 HTTP 请求返回值设置项*/
const addHttpResponseSetting = (responseSetting: Record<string, string>[]) => {
responseSetting.push({
key: '',
value: ''
})
}
/** 删除 HTTP 请求返回值设置项 */
const deleteHttpResponseSetting = (responseSetting: Record<string, string>[], index: number) => {
responseSetting.splice(index, 1)
}
/** 添加修改表单设置项 */
const addFormFieldSetting = () => {
if (configForm.value.normalFormSetting!.updateFormFields === undefined) {
configForm.value.normalFormSetting!.updateFormFields = {}
}
configForm.value.normalFormSetting!.updateFormFields[''] = undefined
}
/** 删除修改表单设置项 */
const deleteFormFieldSetting = (key: string) => {
if (!configForm.value.normalFormSetting?.updateFormFields) return
delete configForm.value.normalFormSetting.updateFormFields[key]
}
/** 保存配置 */
const saveConfig = async () => {
if (!formRef) return false
const valid = await formRef.value.validate()
if (!valid) return false
const showText = getShowText()
if (!showText) return false
currentNode.value.name = nodeName.value!
currentNode.value.showText = showText
if (configForm.value.type === TriggerTypeEnum.HTTP_REQUEST) {
configForm.value.normalFormSetting = undefined
}
if (configForm.value.type === TriggerTypeEnum.UPDATE_NORMAL_FORM) {
configForm.value.httpRequestSetting = undefined
}
currentNode.value.triggerSetting = configForm.value
settingVisible.value = false
return true
}
/** 获取节点展示内容 */
const getShowText = (): string => {
let showText = ''
if (configForm.value.type === TriggerTypeEnum.HTTP_REQUEST) {
showText = `${configForm.value.httpRequestSetting?.url}`
} else if (configForm.value.type === TriggerTypeEnum.UPDATE_NORMAL_FORM) {
const updatefields = Object.keys(configForm.value.normalFormSetting?.updateFormFields || {})
if (updatefields.length === 0) {
message.warning('请设置修改表单字段')
} else {
showText = '修改表单数据'
}
}
return showText
}
/** 显示触发器节点配置, 由父组件传过来 */
const showTriggerNodeConfig = (node: SimpleFlowNode) => {
nodeName.value = node.name
if (node.triggerSetting) {
configForm.value = {
type: node.triggerSetting.type,
httpRequestSetting: node.triggerSetting.httpRequestSetting || {
url: '',
header: [],
body: [],
response: []
},
normalFormSetting: node.triggerSetting.normalFormSetting || { updateFormFields: {} }
}
}
}
defineExpose({ openDrawer, showTriggerNodeConfig }) // 暴露方法给父组件
</script>
<style lang="scss" scoped></style>
@@ -3,7 +3,7 @@
:append-to-body="true"
v-model="settingVisible"
:show-close="false"
:size="550"
:size="580"
:before-close="saveConfig"
class="justify-start"
>
@@ -19,7 +19,8 @@
:placeholder="nodeName"
/>
<div v-else class="node-name">
{{ nodeName }} <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" />
{{ nodeName }}
<Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" />
</div>
<div class="divide-line"></div>
</div>
@@ -46,14 +47,13 @@
v-model="configForm.candidateStrategy"
@change="changeCandidateStrategy"
>
<el-radio
v-for="(dict, index) in CANDIDATE_STRATEGY"
:key="index"
:value="dict.value"
:label="dict.value"
>
{{ dict.label }}
</el-radio>
<el-row>
<el-col v-for="(dict, index) in CANDIDATE_STRATEGY" :key="index" :span="8">
<el-radio :value="dict.value" :label="dict.value">
{{ dict.label }}
</el-radio>
</el-col>
</el-row>
</el-radio-group>
</el-form-item>
<el-form-item
@@ -61,7 +61,7 @@
label="指定角色"
prop="roleIds"
>
<el-select v-model="configForm.roleIds" clearable multiple style="width: 100%">
<el-select filterable v-model="configForm.roleIds" clearable multiple style="width: 100%">
<el-option
v-for="item in roleOptions"
:key="item.id"
@@ -99,7 +99,7 @@
prop="postIds"
span="24"
>
<el-select v-model="configForm.postIds" clearable multiple style="width: 100%">
<el-select filterable v-model="configForm.postIds" clearable multiple style="width: 100%">
<el-option
v-for="item in postOptions"
:key="item.id"
@@ -114,7 +114,7 @@
prop="userIds"
span="24"
>
<el-select v-model="configForm.userIds" clearable multiple style="width: 100%">
<el-select filterable v-model="configForm.userIds" clearable multiple style="width: 100%">
<el-option
v-for="item in userOptions"
:key="item.id"
@@ -128,7 +128,7 @@
label="指定用户组"
prop="userGroups"
>
<el-select v-model="configForm.userGroups" clearable multiple style="width: 100%">
<el-select filterable v-model="configForm.userGroups" clearable multiple style="width: 100%">
<el-option
v-for="item in userGroupOptions"
:key="item.id"
@@ -142,13 +142,13 @@
label="表单内用户字段"
prop="formUser"
>
<el-select v-model="configForm.formUser" clearable style="width: 100%">
<el-select filterable 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"
:disabled="!item.required"
/>
</el-select>
</el-form-item>
@@ -157,13 +157,13 @@
label="表单内部门字段"
prop="formDept"
>
<el-select v-model="configForm.formDept" clearable style="width: 100%">
<el-select filterable v-model="configForm.formDept" clearable style="width: 100%">
<el-option
v-for="(item, idx) in deptFieldOnFormOptions"
:key="idx"
:label="item.title"
:value="item.field"
:disabled ="!item.required"
:disabled="!item.required"
/>
</el-select>
</el-form-item>
@@ -179,7 +179,7 @@
prop="deptLevel"
span="24"
>
<el-select v-model="configForm.deptLevel" clearable>
<el-select filterable v-model="configForm.deptLevel" clearable>
<el-option
v-for="(item, index) in MULTI_LEVEL_DEPT"
:key="index"
@@ -245,7 +245,7 @@
label="驳回节点"
prop="returnNodeId"
>
<el-select v-model="configForm.returnNodeId" clearable style="width: 100%">
<el-select filterable v-model="configForm.returnNodeId" clearable style="width: 100%">
<el-option
v-for="item in returnTaskList"
:key="item.id"
@@ -293,6 +293,7 @@
/>
</el-form-item>
<el-select
filterable
v-model="timeUnit"
class="mr-2"
:style="{ width: '100px' }"
@@ -332,6 +333,7 @@
span="24"
>
<el-select
filterable
v-model="configForm.assignEmptyHandlerUserIds"
clearable
multiple
@@ -356,6 +358,16 @@
</div>
</el-radio-group>
</el-form-item>
<el-divider content-position="left">是否需要签名</el-divider>
<el-form-item prop="signEnable">
<el-switch v-model="configForm.signEnable" active-text="是" inactive-text="否" />
</el-form-item>
<el-divider content-position="left">审批意见</el-divider>
<el-form-item prop="reasonRequire">
<el-switch v-model="configForm.reasonRequire" active-text="必填" inactive-text="非必填" />
</el-form-item>
</el-form>
</div>
</el-tab-pane>
@@ -435,6 +447,9 @@
</div>
</div>
</el-tab-pane>
<el-tab-pane label="监听器" name="listener">
<UserTaskListener ref="userTaskListenerRef" v-model="configForm" :form-field-options="formFieldOptions" />
</el-tab-pane>
</el-tabs>
<template #footer>
<el-divider />
@@ -484,6 +499,7 @@ import {
import { defaultProps } from '@/utils/tree'
import { cloneDeep } from 'lodash-es'
import { convertTimeUnit, getApproveTypeText } from '../utils'
import UserTaskListener from './components/UserTaskListener.vue'
defineOptions({
name: 'UserTaskNodeConfig'
})
@@ -609,9 +625,11 @@ const {
cTimeoutMaxRemindCount
} = useTimeoutHandler()
const userTaskListenerRef = ref()
// 保存配置
const saveConfig = async () => {
activeTabName.value = 'user'
// activeTabName.value = 'user'
// 设置审批节点名称
currentNode.value.name = nodeName.value!
// 设置审批类型
@@ -624,7 +642,8 @@ const saveConfig = async () => {
}
if (!formRef) return false
const valid = await formRef.value.validate()
if (!userTaskListenerRef) return false
const valid = (await formRef.value.validate()) && (await userTaskListenerRef.value.validate())
if (!valid) return false
const showText = getShowText()
if (!showText) return false
@@ -663,6 +682,31 @@ const saveConfig = async () => {
currentNode.value.fieldsPermission = fieldsPermissionConfig.value
// 设置按钮权限
currentNode.value.buttonsSetting = buttonsSetting.value
// 创建任务监听器
currentNode.value.taskCreateListener = {
enable: configForm.value.taskCreateListenerEnable ?? false,
path: configForm.value.taskCreateListenerPath,
header: configForm.value.taskCreateListener?.header,
body: configForm.value.taskCreateListener?.body
}
// 指派任务监听器
currentNode.value.taskAssignListener = {
enable: configForm.value.taskAssignListenerEnable ?? false,
path: configForm.value.taskAssignListenerPath,
header: configForm.value.taskAssignListener?.header,
body: configForm.value.taskAssignListener?.body
}
// 完成任务监听器
currentNode.value.taskCompleteListener = {
enable: configForm.value.taskCompleteListenerEnable ?? false,
path: configForm.value.taskCompleteListenerPath,
header: configForm.value.taskCompleteListener?.header,
body: configForm.value.taskCompleteListener?.body
}
// 签名
currentNode.value.signEnable = configForm.value.signEnable
// 审批意见
currentNode.value.reasonRequire = configForm.value.reasonRequire
currentNode.value.showText = showText
settingVisible.value = false
@@ -714,6 +758,32 @@ const showUserTaskNodeConfig = (node: SimpleFlowNode) => {
buttonsSetting.value = cloneDeep(node.buttonsSetting) || DEFAULT_BUTTON_SETTING
// 4. 表单字段权限配置
getNodeConfigFormFields(node.fieldsPermission)
// 5. 监听器
// 5.1 创建任务
configForm.value.taskCreateListenerEnable = node.taskCreateListener?.enable
configForm.value.taskCreateListenerPath = node.taskCreateListener?.path
configForm.value.taskCreateListener = {
header: node.taskCreateListener?.header ?? [],
body: node.taskCreateListener?.body ?? []
}
// 5.2 指派任务
configForm.value.taskAssignListenerEnable = node.taskAssignListener?.enable
configForm.value.taskAssignListenerPath = node.taskAssignListener?.path
configForm.value.taskAssignListener = {
header: node.taskAssignListener?.header ?? [],
body: node.taskAssignListener?.body ?? []
}
// 5.3 完成任务
configForm.value.taskCompleteListenerEnable = node.taskCompleteListener?.enable
configForm.value.taskCompleteListenerPath = node.taskCompleteListener?.path
configForm.value.taskCompleteListener = {
header: node.taskCompleteListener?.header ?? [],
body: node.taskCompleteListener?.body ?? []
}
// 6. 签名
configForm.value.signEnable = node?.signEnable ?? false
// 7. 审批意见
configForm.value.reasonRequire = node?.reasonRequire ?? false
}
defineExpose({ openDrawer, showUserTaskNodeConfig }) // 暴露方法给父组件
@@ -0,0 +1,264 @@
<template>
<el-form ref="formRef" :model="condition" :rules="formRules" label-position="top">
<el-form-item label="配置方式" prop="conditionType">
<el-radio-group v-model="condition.conditionType" @change="changeConditionType">
<el-radio
v-for="(dict, indexConditionType) in conditionConfigTypes"
:key="indexConditionType"
:value="dict.value"
:label="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="condition.conditionType === ConditionType.RULE && condition.conditionGroups" label="条件规则">
<div class="condition-group-tool">
<div class="flex items-center">
<div class="mr-4">条件组关系</div>
<el-switch
v-model="condition.conditionGroups.and"
inline-prompt
active-text="且"
inactive-text="或"
/>
</div>
</div>
<el-space direction="vertical" :spacer="condition.conditionGroups.and ? '且' : '或'">
<el-card
class="condition-group"
style="width: 530px"
v-for="(equation, cIdx) in condition.conditionGroups.conditions"
:key="cIdx"
>
<div
class="condition-group-delete"
v-if="condition.conditionGroups.conditions.length > 1"
>
<Icon
color="#0089ff"
icon="ep:circle-close-filled"
:size="18"
@click="deleteConditionGroup(condition.conditionGroups.conditions, cIdx)"
/>
</div>
<template #header>
<div class="flex items-center justify-between">
<div>条件组</div>
<div class="flex">
<div class="mr-4">规则关系</div>
<el-switch
v-model="equation.and"
inline-prompt
active-text="且"
inactive-text="或"
/>
</div>
</div>
</template>
<div class="flex pt-2" v-for="(rule, rIdx) in equation.rules" :key="rIdx">
<div class="mr-2">
<el-form-item
:prop="`conditionGroups.conditions.${cIdx}.rules.${rIdx}.leftSide`"
:rules="{
required: true,
message: '左值不能为空',
trigger: 'change'
}"
>
<el-select style="width: 160px" v-model="rule.leftSide">
<el-option
v-for="(field, fIdx) in fieldOptions"
:key="fIdx"
:label="field.title"
:value="field.field"
:disabled="!field.required"
/>
</el-select>
</el-form-item>
</div>
<div class="mr-2">
<el-select v-model="rule.opCode" style="width: 100px">
<el-option
v-for="operator in COMPARISON_OPERATORS"
:key="operator.value"
:label="operator.label"
:value="operator.value"
/>
</el-select>
</div>
<div class="mr-2">
<el-form-item
:prop="`conditionGroups.conditions.${cIdx}.rules.${rIdx}.rightSide`"
:rules="{
required: true,
message: '右值不能为空',
trigger: 'blur'
}"
>
<el-input v-model="rule.rightSide" style="width: 160px" />
</el-form-item>
</div>
<div class="mr-1 flex items-center" v-if="equation.rules.length > 1">
<Icon icon="ep:delete" :size="18" @click="deleteConditionRule(equation, rIdx)" />
</div>
<div class="flex items-center">
<Icon icon="ep:plus" :size="18" @click="addConditionRule(equation, rIdx)" />
</div>
</div>
</el-card>
</el-space>
<div title="添加条件组" class="mt-4 cursor-pointer">
<Icon
color="#0089ff"
icon="ep:plus"
:size="24"
@click="addConditionGroup(condition.conditionGroups?.conditions)"
/>
</div>
</el-form-item>
<el-form-item
v-if="condition.conditionType === ConditionType.EXPRESSION"
label="条件表达式"
prop="conditionExpression"
>
<el-input
type="textarea"
v-model="condition.conditionExpression"
clearable
style="width: 100%"
/>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import {
COMPARISON_OPERATORS,
CONDITION_CONFIG_TYPES,
ConditionType,
DEFAULT_CONDITION_GROUP_VALUE
} from '../../consts'
import { BpmModelFormType } from '@/utils/constants'
import { useFormFieldsAndStartUser } from '../../node'
const props = defineProps({
modelValue: {
type: Object,
required: true
}
})
const emit = defineEmits(['update:modelValue'])
const condition = computed({
get() {
return props.modelValue
},
set(newValue) {
emit('update:modelValue', newValue)
}
})
const formType = inject<Ref<number>>('formType') // 表单类型
const conditionConfigTypes = computed(() => {
return CONDITION_CONFIG_TYPES.filter((item) => {
// 业务表单暂时去掉条件规则选项
if (formType?.value === BpmModelFormType.CUSTOM && item.value === ConditionType.RULE) {
return false
} else {
return true
}
})
})
/** 条件规则可选择的表单字段 */
const fieldOptions = useFormFieldsAndStartUser()
// 表单校验规则
const formRules = reactive({
conditionType: [{ required: true, message: '配置方式不能为空', trigger: 'blur' }],
conditionExpression: [{ required: true, message: '条件表达式不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
/** 切换条件配置方式 */
const changeConditionType = () => {
if (condition.value.conditionType === ConditionType.RULE) {
if (!condition.value.conditionGroups) {
condition.value.conditionGroups = DEFAULT_CONDITION_GROUP_VALUE
}
}
}
const deleteConditionGroup = (conditions, index) => {
conditions.splice(index, 1)
}
const deleteConditionRule = (condition, index) => {
condition.rules.splice(index, 1)
}
const addConditionRule = (condition, index) => {
const rule = {
opCode: '==',
leftSide: '',
rightSide: ''
}
condition.rules.splice(index + 1, 0, rule)
}
const addConditionGroup = (conditions) => {
const condition = {
and: true,
rules: [
{
opCode: '==',
leftSide: '',
rightSide: ''
}
]
}
conditions.push(condition)
}
const validate = async () => {
if (!formRef) return false
return await formRef.value.validate()
}
defineExpose({ validate })
</script>
<style lang="scss" scoped>
.condition-group-tool {
display: flex;
justify-content: space-between;
width: 500px;
margin-bottom: 20px;
}
.condition-group {
position: relative;
&:hover {
border-color: #0089ff;
.condition-group-delete {
opacity: 1;
}
}
.condition-group-delete {
position: absolute;
top: 0;
left: 0;
display: flex;
cursor: pointer;
opacity: 0;
}
}
::v-deep(.el-card__header) {
padding: 8px var(--el-card-padding);
border-bottom: 1px solid var(--el-card-border-color);
box-sizing: border-box;
}
</style>
@@ -0,0 +1,184 @@
<template>
<el-form-item label="请求头">
<div class="flex pt-2" v-for="(item, index) in props.header" :key="index">
<div class="mr-2">
<el-form-item
:prop="`${bind}.header.${index}.key`"
:rules="{
required: true,
message: '参数名不能为空',
trigger: 'blur'
}"
>
<el-input class="w-160px" v-model="item.key" />
</el-form-item>
</div>
<div class="mr-2">
<el-select class="w-100px!" v-model="item.type">
<el-option
v-for="types in BPM_HTTP_REQUEST_PARAM_TYPES"
:key="types.value"
:label="types.label"
:value="types.value"
/>
</el-select>
</div>
<div class="mr-2">
<el-form-item
:prop="`${bind}.header.${index}.value`"
:rules="{
required: true,
message: '参数值不能为空',
trigger: 'blur'
}"
>
<el-input
v-if="item.type === BpmHttpRequestParamTypeEnum.FIXED_VALUE"
class="w-160px"
v-model="item.value"
/>
</el-form-item>
<el-form-item
:prop="`${bind}.header.${index}.value`"
:rules="{
required: true,
message: '参数值不能为空',
trigger: 'change'
}"
>
<el-select
v-if="item.type === BpmHttpRequestParamTypeEnum.FROM_FORM"
class="w-160px!"
v-model="item.value"
>
<el-option
v-for="(field, fIdx) in formFieldOptions"
:key="fIdx"
:label="field.title"
:value="field.field"
:disabled="!field.required"
/>
</el-select>
</el-form-item>
</div>
<div class="mr-1 flex items-center">
<Icon icon="ep:delete" :size="18" @click="deleteHttpRequestParam(props.header, index)" />
</div>
</div>
<el-button type="primary" text @click="addHttpRequestParam(props.header)">
<Icon icon="ep:plus" class="mr-5px" />添加一行
</el-button>
</el-form-item>
<el-form-item label="请求体">
<div class="flex pt-2" v-for="(item, index) in props.body" :key="index">
<div class="mr-2">
<el-form-item
:prop="`${bind}.body.${index}.key`"
:rules="{
required: true,
message: '参数名不能为空',
trigger: 'blur'
}"
>
<el-input class="w-160px" v-model="item.key" />
</el-form-item>
</div>
<div class="mr-2">
<el-select class="w-100px!" v-model="item.type">
<el-option
v-for="types in BPM_HTTP_REQUEST_PARAM_TYPES"
:key="types.value"
:label="types.label"
:value="types.value"
/>
</el-select>
</div>
<div class="mr-2">
<el-form-item
:prop="`${bind}.body.${index}.value`"
:rules="{
required: true,
message: '参数值不能为空',
trigger: 'blur'
}"
>
<el-input
v-if="item.type === BpmHttpRequestParamTypeEnum.FIXED_VALUE"
class="w-160px"
v-model="item.value"
/>
</el-form-item>
<el-form-item
:prop="`${bind}.body.${index}.value`"
:rules="{
required: true,
message: '参数值不能为空',
trigger: 'change'
}"
>
<el-select
v-if="item.type === BpmHttpRequestParamTypeEnum.FROM_FORM"
class="w-160px!"
v-model="item.value"
>
<el-option
v-for="(field, fIdx) in formFieldOptions"
:key="fIdx"
:label="field.title"
:value="field.field"
:disabled="!field.required"
/>
</el-select>
</el-form-item>
</div>
<div class="mr-1 flex items-center">
<Icon icon="ep:delete" :size="18" @click="deleteHttpRequestParam(props.body, index)" />
</div>
</div>
<el-button type="primary" text @click="addHttpRequestParam(props.body)">
<Icon icon="ep:plus" class="mr-5px" />添加一行
</el-button>
</el-form-item>
</template>
<script setup lang="ts">
import { HttpRequestParam, BPM_HTTP_REQUEST_PARAM_TYPES, BpmHttpRequestParamTypeEnum } from '../../consts'
import { useFormFieldsAndStartUser } from '../../node'
defineOptions({
name: 'HttpRequestParamSetting'
})
const props = defineProps({
header: {
type: Array as () => HttpRequestParam[],
required: false,
default: () => []
},
body: {
type: Array as () => HttpRequestParam[],
required: false,
default: () => []
},
bind: {
type: String,
required: true
}
})
// 流程表单字段,发起人字段
const formFieldOptions = useFormFieldsAndStartUser()
/** 添加请求配置项 */
const addHttpRequestParam = (arr: HttpRequestParam[]) => {
arr.push({
key: '',
type: BpmHttpRequestParamTypeEnum.FIXED_VALUE,
value: ''
})
}
/** 删除请求配置项 */
const deleteHttpRequestParam = (arr: HttpRequestParam[], index: number) => {
arr.splice(index, 1)
}
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,88 @@
<template>
<el-form ref="listenerFormRef" :model="configForm" label-position="top">
<div v-for="(listener, listenerIdx) in taskListener" :key="listenerIdx">
<el-divider content-position="left">
<el-text tag="b" size="large">{{ listener.name }}</el-text>
</el-divider>
<el-form-item>
<el-switch
v-model="configForm[`task${listener.type}ListenerEnable`]"
active-text="开启"
inactive-text="关闭"
/>
</el-form-item>
<div v-if="configForm[`task${listener.type}ListenerEnable`]">
<el-form-item>
<el-alert
title="仅支持 POST 请求,以请求体方式接收参数"
type="warning"
show-icon
:closable="false"
/>
</el-form-item>
<el-form-item
label="请求地址"
:prop="`task${listener.type}ListenerPath`"
:rules="{
required: true,
message: '请求地址不能为空',
trigger: 'blur'
}"
>
<el-input v-model="configForm[`task${listener.type}ListenerPath`]" />
</el-form-item>
<HttpRequestParamSetting
:header="configForm[`task${listener.type}Listener`].header"
:body="configForm[`task${listener.type}Listener`].body"
:bind="`task${listener.type}Listener`"
/>
</div>
</div>
</el-form>
</template>
<script setup lang="ts">
import HttpRequestParamSetting from './HttpRequestParamSetting.vue'
const props = defineProps({
modelValue: {
type: Object,
required: true
},
formFieldOptions: {
type: Object,
required: true
}
})
const emit = defineEmits(['update:modelValue'])
const listenerFormRef = ref()
const configForm = computed({
get() {
return props.modelValue
},
set(newValue) {
emit('update:modelValue', newValue)
}
})
const taskListener = ref([
{
name: '创建任务',
type: 'Create'
},
{
name: '指派任务执行人员',
type: 'Assign'
},
{
name: '完成任务',
type: 'Complete'
}
])
const validate = async () => {
if (!listenerFormRef) return false
return await listenerFormRef.value.validate()
}
defineExpose({ validate })
</script>
@@ -0,0 +1,97 @@
<template>
<div class="node-wrapper">
<div class="node-container">
<div
class="node-box"
:class="[
{ 'node-config-error': !currentNode.showText },
`${useTaskStatusClass(currentNode?.activityStatus)}`
]"
>
<div class="node-title-container">
<div class="node-title-icon delay-node"><span class="iconfont icon-delay"></span></div>
<input
v-if="!readonly && showInput"
type="text"
class="editable-title-input"
@blur="blurEvent()"
v-mountedFocus
v-model="currentNode.name"
:placeholder="currentNode.name"
/>
<div v-else class="node-title" @click="clickTitle">
{{ currentNode.name }}
</div>
</div>
<div class="node-content" @click="openNodeConfig">
<div class="node-text" :title="currentNode.showText" v-if="currentNode.showText">
{{ currentNode.showText }}
</div>
<div class="node-text" v-else>
{{ NODE_DEFAULT_TEXT.get(NodeType.DELAY_TIMER_NODE) }}
</div>
<Icon v-if="!readonly" icon="ep:arrow-right-bold" />
</div>
<div v-if="!readonly" class="node-toolbar">
<div class="toolbar-icon"
><Icon color="#0089ff" icon="ep:circle-close-filled" :size="18" @click="deleteNode"
/></div>
</div>
</div>
<!-- 传递子节点给添加节点组件。会在子节点前面添加节点 -->
<NodeHandler
v-if="currentNode"
v-model:child-node="currentNode.childNode"
:current-node="currentNode"
/>
</div>
<DelayTimerNodeConfig
v-if="!readonly && currentNode"
ref="nodeSetting"
:flow-node="currentNode"
/>
</div>
</template>
<script setup lang="ts">
import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
import NodeHandler from '../NodeHandler.vue'
import { useNodeName2, useWatchNode, useTaskStatusClass } from '../node'
import DelayTimerNodeConfig from '../nodes-config/DelayTimerNodeConfig.vue'
defineOptions({
name: 'DelayTimerNode'
})
const props = defineProps({
flowNode: {
type: Object as () => SimpleFlowNode,
required: true
}
})
// 定义事件,更新父组件。
const emits = defineEmits<{
'update:flowNode': [node: SimpleFlowNode | undefined]
}>()
// 是否只读
const readonly = inject<Boolean>('readonly')
// 监控节点的变化
const currentNode = useWatchNode(props)
// 节点名称编辑
const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.DELAY_TIMER_NODE)
const nodeSetting = ref()
// 打开节点配置
const openNodeConfig = () => {
if (readonly) {
return
}
nodeSetting.value.showDelayTimerNodeConfig(currentNode.value)
nodeSetting.value.openDrawer()
}
// 删除节点。更新当前节点为孩子节点
const deleteNode = () => {
emits('update:flowNode', currentNode.value.childNode)
}
</script>
<style lang="scss" scoped></style>
@@ -77,7 +77,7 @@ const props = defineProps({
const currentNode = useWatchNode(props)
// 是否只读
const readonly = inject<Boolean>('readonly')
const processInstance = inject<Ref<any>>('processInstance')
const processInstance = inject<Ref<any>>('processInstance', ref({}))
// 审批信息的弹窗显示,用于只读模式
const dialogVisible = ref(false) // 弹窗可见性
const processInstanceInfos = ref<any[]>([]) // 流程的审批信息
@@ -108,7 +108,7 @@
<script setup lang="ts">
import NodeHandler from '../NodeHandler.vue'
import ProcessNodeTree from '../ProcessNodeTree.vue'
import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
import { SimpleFlowNode, NodeType, ConditionType, DEFAULT_CONDITION_GROUP_VALUE, NODE_DEFAULT_TEXT } from '../consts'
import { getDefaultConditionNodeName } from '../utils'
import { useTaskStatusClass } from '../node'
import { generateUUID } from '@/utils'
@@ -149,7 +149,7 @@ const blurEvent = (index: number) => {
showInputs.value[index] = false
const conditionNode = currentNode.value.conditionNodes?.at(index) as SimpleFlowNode
conditionNode.name =
conditionNode.name || getDefaultConditionNodeName(index, conditionNode.defaultFlow)
conditionNode.name || getDefaultConditionNodeName(index, conditionNode.conditionSetting?.defaultFlow)
}
// 点击条件名称
@@ -178,8 +178,11 @@ const addCondition = () => {
type: NodeType.CONDITION_NODE,
childNode: undefined,
conditionNodes: [],
conditionType: 1,
defaultFlow: false
conditionSetting: {
defaultFlow: false,
conditionType: ConditionType.RULE,
conditionGroups: DEFAULT_CONDITION_GROUP_VALUE
}
}
conditionNodes.splice(lastIndex, 0, conditionData)
}
@@ -34,7 +34,7 @@
]"
>
<div class="branch-node-title-container">
<div v-if="showInputs[index]">
<div v-if="!readonly && showInputs[index]">
<input
type="text"
class="editable-title-input"
@@ -110,7 +110,7 @@
<script setup lang="ts">
import NodeHandler from '../NodeHandler.vue'
import ProcessNodeTree from '../ProcessNodeTree.vue'
import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
import { SimpleFlowNode, NodeType, ConditionType, DEFAULT_CONDITION_GROUP_VALUE, NODE_DEFAULT_TEXT } from '../consts'
import { useTaskStatusClass } from '../node'
import { getDefaultInclusiveConditionNodeName } from '../utils'
import { generateUUID } from '@/utils'
@@ -153,7 +153,7 @@ 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)
conditionNode.name || getDefaultInclusiveConditionNodeName(index, conditionNode.conditionSetting?.defaultFlow)
}
// 点击条件名称
@@ -182,8 +182,11 @@ const addCondition = () => {
type: NodeType.CONDITION_NODE,
childNode: undefined,
conditionNodes: [],
conditionType: 1,
defaultFlow: false
conditionSetting: {
defaultFlow: false,
conditionType: ConditionType.RULE,
conditionGroups: DEFAULT_CONDITION_GROUP_VALUE
}
}
conditionNodes.splice(lastIndex, 0, conditionData)
}
@@ -0,0 +1,97 @@
<template>
<div class="node-wrapper">
<div class="node-container">
<div
class="node-box"
:class="[
{ 'node-config-error': !currentNode.showText },
`${useTaskStatusClass(currentNode?.activityStatus)}`
]"
>
<div class="node-title-container">
<div class="node-title-icon router-node">
<span class="iconfont icon-router"></span>
</div>
<input
v-if="!readonly && showInput"
type="text"
class="editable-title-input"
@blur="blurEvent()"
v-mountedFocus
v-model="currentNode.name"
:placeholder="currentNode.name"
/>
<div v-else class="node-title" @click="clickTitle">
{{ currentNode.name }}
</div>
</div>
<div class="node-content" @click="openNodeConfig">
<div class="node-text" :title="currentNode.showText" v-if="currentNode.showText">
{{ currentNode.showText }}
</div>
<div class="node-text" v-else>
{{ NODE_DEFAULT_TEXT.get(NodeType.ROUTER_BRANCH_NODE) }}
</div>
<Icon v-if="!readonly" icon="ep:arrow-right-bold" />
</div>
<div v-if="!readonly" class="node-toolbar">
<div class="toolbar-icon"
><Icon color="#0089ff" icon="ep:circle-close-filled" :size="18" @click="deleteNode"
/></div>
</div>
</div>
<!-- 传递子节点给添加节点组件。会在子节点前面添加节点 -->
<NodeHandler
v-if="currentNode"
v-model:child-node="currentNode.childNode"
:current-node="currentNode"
/>
</div>
<RouterNodeConfig v-if="!readonly && currentNode" ref="nodeSetting" :flow-node="currentNode" />
</div>
</template>
<script setup lang="ts">
import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
import NodeHandler from '../NodeHandler.vue'
import { useNodeName2, useWatchNode, useTaskStatusClass } from '../node'
import RouterNodeConfig from '../nodes-config/RouterNodeConfig.vue'
defineOptions({
name: 'RouterNode'
})
const props = defineProps({
flowNode: {
type: Object as () => SimpleFlowNode,
required: true
}
})
// 定义事件,更新父组件
const emits = defineEmits<{
'update:flowNode': [node: SimpleFlowNode | undefined]
}>()
// 是否只读
const readonly = inject<Boolean>('readonly')
// 监控节点的变化
const currentNode = useWatchNode(props)
// 节点名称编辑
const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.ROUTER_BRANCH_NODE)
const nodeSetting = ref()
// 打开节点配置
const openNodeConfig = () => {
if (readonly) {
return
}
nodeSetting.value.showRouteNodeConfig(currentNode.value)
nodeSetting.value.openDrawer()
}
// 删除节点。更新当前节点为孩子节点
const deleteNode = () => {
emits('update:flowNode', currentNode.value.childNode)
}
</script>
<style lang="scss" scoped></style>
@@ -13,7 +13,7 @@
><span class="iconfont icon-start-user"></span
></div>
<input
v-if="showInput"
v-if="!readonly && showInput"
type="text"
class="editable-title-input"
@blur="blurEvent()"
@@ -117,7 +117,7 @@ const props = defineProps({
}
})
const readonly = inject<Boolean>('readonly') // 是否只读
const tasks = inject<Ref<any[]>>('tasks')
const tasks = inject<Ref<any[]>>('tasks', ref([]))
// 定义事件,更新父组件。
const emits = defineEmits<{
'update:modelValue': [node: SimpleFlowNode | undefined]
@@ -0,0 +1,97 @@
<template>
<div class="node-wrapper">
<div class="node-container">
<div
class="node-box"
:class="[
{ 'node-config-error': !currentNode.showText },
`${useTaskStatusClass(currentNode?.activityStatus)}`
]"
>
<div class="node-title-container">
<div class="node-title-icon trigger-node">
<span class="iconfont icon-trigger"></span>
</div>
<input
v-if="!readonly && showInput"
type="text"
class="editable-title-input"
@blur="blurEvent()"
v-mountedFocus
v-model="currentNode.name"
:placeholder="currentNode.name"
/>
<div v-else class="node-title" @click="clickTitle">
{{ currentNode.name }}
</div>
</div>
<div class="node-content" @click="openNodeConfig">
<div class="node-text" :title="currentNode.showText" v-if="currentNode.showText">
{{ currentNode.showText }}
</div>
<div class="node-text" v-else>
{{ NODE_DEFAULT_TEXT.get(NodeType.TRIGGER_NODE) }}
</div>
<Icon v-if="!readonly" icon="ep:arrow-right-bold" />
</div>
<div v-if="!readonly" class="node-toolbar">
<div class="toolbar-icon"
><Icon color="#0089ff" icon="ep:circle-close-filled" :size="18" @click="deleteNode"
/></div>
</div>
</div>
<!-- 传递子节点给添加节点组件。会在子节点前面添加节点 -->
<NodeHandler
v-if="currentNode"
v-model:child-node="currentNode.childNode"
:current-node="currentNode"
/>
</div>
<TriggerNodeConfig v-if="!readonly && currentNode" ref="nodeSetting" :flow-node="currentNode" />
</div>
</template>
<script setup lang="ts">
import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
import NodeHandler from '../NodeHandler.vue'
import { useNodeName2, useWatchNode, useTaskStatusClass } from '../node'
import TriggerNodeConfig from '../nodes-config/TriggerNodeConfig.vue'
defineOptions({
name: 'TriggerNode'
})
const props = defineProps({
flowNode: {
type: Object as () => SimpleFlowNode,
required: true
}
})
// 定义事件,更新父组件
const emits = defineEmits<{
'update:flowNode': [node: SimpleFlowNode | undefined]
}>()
// 是否只读
const readonly = inject<Boolean>('readonly')
// 监控节点的变化
const currentNode = useWatchNode(props)
// 节点名称编辑
const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.TRIGGER_NODE)
const nodeSetting = ref()
// 打开节点配置
const openNodeConfig = () => {
if (readonly) {
return
}
nodeSetting.value.showTriggerNodeConfig(currentNode.value)
nodeSetting.value.openDrawer()
}
// 删除节点。更新当前节点为孩子节点
const deleteNode = () => {
emits('update:flowNode', currentNode.value.childNode)
}
</script>
<style lang="scss" scoped></style>
@@ -131,7 +131,7 @@ const emits = defineEmits<{
// 是否只读
const readonly = inject<Boolean>('readonly')
const tasks = inject<Ref<any[]>>('tasks')
const tasks = inject<Ref<any[]>>('tasks', ref([]))
// 监控节点变化
const currentNode = useWatchNode(props)
// 节点名称编辑
@@ -113,18 +113,21 @@
// 节点连线气泡卡片样式
.handler-item-wrapper {
width: 320px;
display: flex;
flex-wrap: wrap;
cursor: pointer;
.handler-item {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 12px;
}
.handler-item-icon {
width: 60px;
height: 60px;
width: 50px;
height: 50px;
background: #fff;
border: 1px solid #e2e2e2;
border-radius: 50%;
@@ -138,13 +141,14 @@
.icon-size {
font-size: 25px;
line-height: 60px;
line-height: 50px;
}
}
.approve {
color: #ff943e;
}
.copy {
color: #3296fa;
}
@@ -161,6 +165,18 @@
color: #345da2;
}
.delay {
color: #e47470;
}
.trigger {
color: #3373d2;
}
.router {
color: #ca3a31
}
.handler-item-text {
margin-top: 4px;
width: 80px;
@@ -266,6 +282,18 @@
&.start-user {
color: #676565;
}
&.delay-node {
color: #e47470;
}
&.trigger-node {
color: #3373d2;
}
&.router-node {
color: #ca3a31
}
}
.node-title {
@@ -711,45 +739,56 @@
// iconfont 样式
@font-face {
font-family: 'iconfont'; /* Project id 4495938 */
src:
url('iconfont.woff2?t=1724339470412') format('woff2'),
url('iconfont.woff?t=1724339470412') format('woff'),
url('iconfont.ttf?t=1724339470412') format('truetype');
font-family: "iconfont"; /* Project id 4495938 */
src: url('iconfont.woff2?t=1737639517142') format('woff2'),
url('iconfont.woff?t=1737639517142') format('woff'),
url('iconfont.ttf?t=1737639517142') format('truetype');
}
.iconfont {
font-family: 'iconfont' !important;
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-trigger:before {
content: "\e6d3";
}
.icon-router:before {
content: "\e6b2";
}
.icon-delay:before {
content: "\e600";
}
.icon-start-user:before {
content: '\e679';
content: "\e679";
}
.icon-inclusive:before {
content: '\e602';
content: "\e602";
}
.icon-copy:before {
content: '\e7eb';
content: "\e7eb";
}
.icon-handle:before {
content: '\e61c';
content: "\e61c";
}
.icon-exclusive:before {
content: '\e717';
content: "\e717";
}
.icon-approve:before {
content: '\e715';
content: "\e715";
}
.icon-parallel:before {
content: '\e688';
content: "\e688";
}
@@ -1,4 +1,5 @@
<!-- 列表选择通用组件,参考 ProductList 组件使用 -->
<!-- TODO 芋艿:可能会移除 -->
<template>
<Dialog v-model="dialogVisible" :appendToBody="true" :scroll="true" :title="title" width="60%">
<el-table
@@ -308,28 +308,6 @@ const props = defineProps({
}
})
// 监听value变化,重新加载流程图
watch(
() => props.value,
(newValue) => {
if (newValue && bpmnModeler) {
createNewDiagram(newValue)
}
},
{ immediate: true }
)
// 监听processId和processName变化
watch(
[() => props.processId, () => props.processName],
([newId, newName]) => {
if (newId && newName && !props.value) {
createNewDiagram(null)
}
},
{ immediate: true }
)
provide('configGlobal', props)
let bpmnModeler: any = null
const defaultZoom = ref(1)
@@ -480,6 +458,7 @@ const initModelListeners = () => {
emit('commandStack-changed', event)
emit('input', xml)
emit('change', xml)
emit('save', xml)
} catch (e: any) {
console.error(`[Process Designer Warn]: ${e.message || e}`)
}
@@ -568,6 +547,7 @@ const importLocalFile = () => {
reader.onload = function () {
let xmlStr = this.result
createNewDiagram(xmlStr)
emit('save', xmlStr)
}
}
/* ------------------------------------------------ refs methods ------------------------------------------------------ */
@@ -1438,6 +1438,45 @@
"isBody": true
}
]
},
{
"name": "SignEnable",
"superClass": ["Element"],
"meta": {
"allowedIn": ["bpmn:UserTask"]
},
"properties": [
{
"name": "value",
"type": "Boolean",
"isBody": true
}
]
},
{
"name": "SkipExpression",
"extends": ["bpmn:UserTask"],
"properties": [
{
"name": "skipExpression",
"isAttr": true,
"type": "String"
}
]
},
{
"name": "ReasonRequire",
"superClass": ["Element"],
"meta": {
"allowedIn": ["bpmn:UserTask"]
},
"properties": [
{
"name": "value",
"type": "Boolean",
"isBody": true
}
]
}
],
"emumerations": []
@@ -1,5 +1,5 @@
<template>
<div class="process-panel__container" :style="{ width: `${width}px` }">
<div class="process-panel__container" :style="{ width: `${width}px`, maxHeight: '600px' }">
<el-collapse v-model="activeTab" v-if="isReady">
<el-collapse-item name="base">
<!-- class="panel-tab__title" -->
@@ -152,6 +152,9 @@ watch(
handleKeyUpdate(props.model.key)
handleNameUpdate(props.model.name)
}
},
{
immediate: true
}
)
@@ -5,6 +5,7 @@
4. 操作按钮
5. 字段权限
6. 审批类型
7. 是否需要签名
-->
<template>
<div>
@@ -161,6 +162,16 @@
</el-radio-group>
</div>
</div>
<el-divider content-position="left">是否需要签名</el-divider>
<el-form-item prop="signEnable">
<el-switch v-model="signEnable.value" active-text="是" inactive-text="否" />
</el-form-item>
<el-divider content-position="left">审批意见</el-divider>
<el-form-item prop="reasonRequire">
<el-switch v-model="reasonRequire.value" active-text="必填" inactive-text="非必填" />
</el-form-item>
</div>
</template>
@@ -218,6 +229,12 @@ const { formType, fieldsPermissionConfig, getNodeConfigFormFields } = useFormFie
// 审批类型
const approveType = ref({ value: ApproveType.USER })
// 是否需要签名
const signEnable = ref({ value: false })
// 审批意见
const reasonRequire = ref({ value: false })
const elExtensionElements = ref()
const otherExtensions = ref()
const bpmnElement = ref()
@@ -311,6 +328,16 @@ const resetCustomConfigList = () => {
})
}
// 是否需要签名
signEnable.value =
elExtensionElements.value.values?.filter((ex) => ex.$type === `${prefix}:SignEnable`)?.[0] ||
bpmnInstances().moddle.create(`${prefix}:SignEnable`, { value: false })
// 审批意见
reasonRequire.value =
elExtensionElements.value.values?.filter((ex) => ex.$type === `${prefix}:ReasonRequire`)?.[0] ||
bpmnInstances().moddle.create(`${prefix}:ReasonRequire`, { value: false })
// 保留剩余扩展元素,便于后面更新该元素对应属性
otherExtensions.value =
elExtensionElements.value.values?.filter(
@@ -322,7 +349,9 @@ const resetCustomConfigList = () => {
ex.$type !== `${prefix}:AssignEmptyUserIds` &&
ex.$type !== `${prefix}:ButtonsSetting` &&
ex.$type !== `${prefix}:FieldsPermission` &&
ex.$type !== `${prefix}:ApproveType`
ex.$type !== `${prefix}:ApproveType` &&
ex.$type !== `${prefix}:SignEnable` &&
ex.$type !== `${prefix}:ReasonRequire`
) ?? []
// 更新元素扩展属性,避免后续报错
@@ -373,7 +402,9 @@ const updateElementExtensions = () => {
assignEmptyUserIdsEl.value,
approveType.value,
...buttonsSettingEl.value,
...fieldsPermissionEl.value
...fieldsPermissionEl.value,
signEnable.value,
reasonRequire.value
]
})
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
@@ -1,6 +1,10 @@
<template>
<div class="panel-tab__content">
<el-radio-group v-model="approveMethod" @change="onApproveMethodChange">
<el-radio-group
v-if="type === 'UserTask'"
v-model="approveMethod"
@change="onApproveMethodChange"
>
<div class="flex-col">
<div v-for="(item, index) in APPROVE_METHODS" :key="index">
<el-radio :value="item.value" :label="item.value">
@@ -23,6 +27,9 @@
</div>
</div>
</el-radio-group>
<div v-else>
除了UserTask以外节点的多实例待实现
</div>
<!-- 与Simple设计器配置合并,保留以前的代码 -->
<el-form label-width="90px" style="display: none">
<el-form-item label="快捷配置">
@@ -301,19 +308,21 @@ const approveMethod = ref()
const approveRatio = ref(100)
const otherExtensions = ref()
const getElementLoopNew = () => {
const extensionElements =
bpmnElement.value.businessObject?.extensionElements ??
bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] })
approveMethod.value = extensionElements.values.filter(
(ex) => ex.$type === `${prefix}:ApproveMethod`
)?.[0]?.value
if (props.type === 'UserTask') {
const extensionElements =
bpmnElement.value.businessObject?.extensionElements ??
bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] })
approveMethod.value = extensionElements.values.filter(
(ex) => ex.$type === `${prefix}:ApproveMethod`
)?.[0]?.value
otherExtensions.value =
extensionElements.values.filter((ex) => ex.$type !== `${prefix}:ApproveMethod`) ?? []
otherExtensions.value =
extensionElements.values.filter((ex) => ex.$type !== `${prefix}:ApproveMethod`) ?? []
if (!approveMethod.value) {
approveMethod.value = ApproveMethodType.SEQUENTIAL_APPROVE
updateLoopCharacteristics()
if (!approveMethod.value) {
approveMethod.value = ApproveMethodType.SEQUENTIAL_APPROVE
updateLoopCharacteristics()
}
}
}
const onApproveMethodChange = () => {
@@ -192,6 +192,16 @@
<!-- 选择弹窗 -->
<ProcessExpressionDialog ref="processExpressionDialogRef" @select="selectProcessExpression" />
</el-form-item>
<el-form-item label="跳过表达式" prop="skipExpression">
<el-input
type="textarea"
v-model="userTaskForm.skipExpression"
clearable
style="width: 100%"
@change="updateSkipExpression"
/>
</el-form-item>
</el-form>
</template>
@@ -220,7 +230,8 @@ const props = defineProps({
const prefix = inject('prefix')
const userTaskForm = ref({
candidateStrategy: undefined, // 分配规则
candidateParam: [] // 分配选项
candidateParam: [], // 分配选项
skipExpression: '' // 跳过表达式
})
const bpmnElement = ref()
const bpmnInstances = () => (window as any)?.bpmnInstances
@@ -311,6 +322,13 @@ const resetTaskForm = () => {
(ex) => ex.$type !== `${prefix}:CandidateStrategy` && ex.$type !== `${prefix}:CandidateParam`
) ?? []
// 跳过表达式
if (businessObject.skipExpression != undefined) {
userTaskForm.value.skipExpression = businessObject.skipExpression
} else {
userTaskForm.value.skipExpression = ''
}
// 改用通过extensionElements来存储数据
return
if (businessObject.candidateStrategy != undefined) {
@@ -390,6 +408,18 @@ const updateElementTask = () => {
})
}
const updateSkipExpression = () => {
if (userTaskForm.value.skipExpression && userTaskForm.value.skipExpression !== '') {
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
skipExpression: userTaskForm.value.skipExpression
})
} else {
bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
skipExpression: null
})
}
}
// 打开监听器弹窗
const processExpressionDialogRef = ref()
const openProcessExpressionDialog = async () => {
@@ -1,6 +1,4 @@
@use 'bpmn-js-token-simulation/assets/css/bpmn-js-token-simulation.css';
@use 'bpmn-js-token-simulation/assets/css/font-awesome.min.css';
@use 'bpmn-js-token-simulation/assets/css/normalize.css';
// 边框被 token-simulation 样式覆盖了
.djs-palette {
@@ -97,12 +95,12 @@
box-sizing: border-box;
}
}
svg {
width: 100%;
height: 100%;
min-height: 100%;
overflow: hidden;
}
// svg {
// width: 100%;
// height: 100%;
// min-height: 100%;
// overflow: hidden;
// }
}
}
+13 -10
View File
@@ -1,22 +1,15 @@
import type { App } from 'vue'
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
import { useUserStore } from '@/store/modules/user'
const { t } = useI18n() // 国际化
/** 判断权限的指令 directive */
export function hasPermi(app: App<Element>) {
app.directive('hasPermi', (el, binding) => {
const { wsCache } = useCache()
const { value } = binding
const all_permission = '*:*:*'
const userInfo = wsCache.get(CACHE_KEY.USER)
const permissions = userInfo?.permissions || []
if (value && value instanceof Array && value.length > 0) {
const permissionFlag = value
const hasPermissions = permissions.some((permission: string) => {
return all_permission === permission || permissionFlag.includes(permission)
})
const hasPermissions = hasPermission(value)
if (!hasPermissions) {
el.parentNode && el.parentNode.removeChild(el)
@@ -26,3 +19,13 @@ export function hasPermi(app: App<Element>) {
}
})
}
/** 判断权限的方法 function */
const userStore = useUserStore()
const all_permission = '*:*:*'
export const hasPermission = (permission: string[]) => {
return (
userStore.permissions.has(all_permission) ||
permission.some((permission) => userStore.permissions.has(permission))
)
}
+4 -1
View File
@@ -12,6 +12,9 @@ const prefixCls = getPrefixCls('footer')
const appStore = useAppStore()
const title = computed(() => appStore.getTitle)
// 添加当前年份计算属性
const currentYear = computed(() => new Date().getFullYear())
</script>
<template>
@@ -19,6 +22,6 @@ const title = computed(() => appStore.getTitle)
:class="prefixCls"
class="h-[var(--app-footer-height)] bg-[var(--app-content-bg-color)] text-center leading-[var(--app-footer-height)] text-[var(--el-text-color-placeholder)] dark:bg-[var(--el-bg-color)] overflow-hidden"
>
<span class="text-14px">Copyright ©2022-{{ title }}</span>
<span class="text-14px">Copyright ©{{ currentYear }} {{ title }}</span>
</div>
</template>
+60 -48
View File
@@ -12,6 +12,10 @@ import { useDesign } from '@/hooks/web/useDesign'
import { useTemplateRefsList } from '@vueuse/core'
import { ElScrollbar } from 'element-plus'
import { useScrollTo } from '@/hooks/event/useScrollTo'
import { useTagsView } from '@/hooks/web/useTagsView'
import { cloneDeep } from 'lodash-es'
defineOptions({ name: 'TagsView' })
const { getPrefixCls } = useDesign()
@@ -19,7 +23,9 @@ const prefixCls = getPrefixCls('tags-view')
const { t } = useI18n()
const { currentRoute, push, replace } = useRouter()
const { currentRoute, push } = useRouter()
const { closeAll, closeLeft, closeRight, closeOther, closeCurrent, refreshPage } = useTagsView()
const permissionStore = usePermissionStore()
@@ -31,6 +37,10 @@ const visitedViews = computed(() => tagsViewStore.getVisitedViews)
const affixTagArr = ref<RouteLocationNormalizedLoaded[]>([])
const selectedTag = computed(() => tagsViewStore.getSelectedTag)
const setSelectTag = tagsViewStore.setSelectedTag
const appStore = useAppStore()
const tagsViewImmerse = computed(() => appStore.getTagsViewImmerse)
@@ -45,66 +55,30 @@ const initTags = () => {
for (const tag of unref(affixTagArr)) {
// Must have tag name
if (tag.name) {
tagsViewStore.addVisitedView(tag)
tagsViewStore.addVisitedView(cloneDeep(tag))
}
}
}
const selectedTag = ref<RouteLocationNormalizedLoaded>()
// 新增tag
const addTags = () => {
const { name } = unref(currentRoute)
if (name) {
selectedTag.value = unref(currentRoute)
setSelectTag(unref(currentRoute))
tagsViewStore.addView(unref(currentRoute))
}
return false
}
// 关闭选中的tag
const closeSelectedTag = (view: RouteLocationNormalizedLoaded) => {
if (view?.meta?.affix) return
tagsViewStore.delView(view)
if (isActive(view)) {
toLastView()
}
}
// 关闭全部
const closeAllTags = () => {
tagsViewStore.delAllViews()
toLastView()
}
// 关闭其它
const closeOthersTags = () => {
tagsViewStore.delOthersViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
}
// 重新加载
const refreshSelectedTag = async (view?: RouteLocationNormalizedLoaded) => {
if (!view) return
tagsViewStore.delCachedView()
const { path, query } = view
await nextTick()
replace({
path: '/redirect' + path,
query: query
closeCurrent(view, () => {
if (isActive(view)) {
toLastView()
}
})
}
// 关闭左侧
const closeLeftTags = () => {
tagsViewStore.delLeftViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
}
// 关闭右侧
const closeRightTags = () => {
tagsViewStore.delRightViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
}
// 跳转到最后一个
// 去最后一个
const toLastView = () => {
const visitedViews = tagsViewStore.getVisitedViews
const latestView = visitedViews.slice(-1)[0]
@@ -118,11 +92,38 @@ const toLastView = () => {
addTags()
return
}
// TODO: You can set another route
push('/')
// You can set another route
push(permissionStore.getAddRouters[0].path)
}
}
// 关闭全部
const closeAllTags = () => {
closeAll(() => {
toLastView()
})
}
// 关闭其它
const closeOthersTags = () => {
closeOther()
}
// 重新加载
const refreshSelectedTag = async (view?: RouteLocationNormalizedLoaded) => {
refreshPage(view)
}
// 关闭左侧
const closeLeftTags = () => {
closeLeft()
}
// 关闭右侧
const closeRightTags = () => {
closeRight()
}
// 滚动到选中的tag
const moveToCurrentTag = async () => {
await nextTick()
@@ -209,13 +210,14 @@ const isActive = (route: RouteLocationNormalizedLoaded): boolean => {
// 所有右键菜单组件的元素
const itemRefs = useTemplateRefsList<ComponentRef<typeof ContextMenu & ContextMenuExpose>>()
// 右键菜单装填改变的时候
// 右键菜单状态改变的时候
const visibleChange = (visible: boolean, tagItem: RouteLocationNormalizedLoaded) => {
if (visible) {
for (const v of unref(itemRefs)) {
const elDropdownMenuRef = v.elDropdownMenuRef
if (tagItem.fullPath !== v.tagItem.fullPath) {
elDropdownMenuRef?.handleClose()
setSelectTag(tagItem)
}
}
}
@@ -243,7 +245,17 @@ const move = (to: number) => {
start()
}
onMounted(() => {
const canShowIcon = (item: RouteLocationNormalizedLoaded) => {
if (
(item?.matched?.[1]?.meta?.icon && unref(tagsViewIcon)) ||
(item?.meta?.affix && unref(tagsViewIcon) && item?.meta?.icon)
) {
return true
}
return false
}
onBeforeMount(() => {
initTags()
addTags()
})
+1 -1
View File
@@ -197,7 +197,7 @@ export const useRenderLayout = () => {
`${prefixCls}-content-scrollbar`,
{
'!h-[calc(100%-var(--tags-view-height))] mt-[calc(var(--tags-view-height))]':
fixedHeader.value
fixedHeader.value && tagsView.value
}
]}
>
+4 -1
View File
@@ -140,7 +140,10 @@ export default {
btnQRCode: 'QR code sign in',
qrcode: 'Scan the QR code to log in',
btnRegister: 'Sign up',
SmsSendMsg: 'code has been sent'
SmsSendMsg: 'code has been sent',
resetPassword: "Reset Password",
resetPasswordSuccess: "Reset Password Success",
invalidTenantName:"Invalid Tenant Name"
},
captcha: {
verification: 'Please complete security verification',
+4 -1
View File
@@ -141,7 +141,10 @@ export default {
btnQRCode: '二维码登录',
qrcode: '扫描二维码登录',
btnRegister: '注册',
SmsSendMsg: '验证码已发送'
SmsSendMsg: '验证码已发送',
resetPassword: "重置密码",
resetPasswordSuccess: "重置密码成功",
invalidTenantName: "无效的租户名称"
},
captcha: {
verification: '请完成安全验证',
+14 -1
View File
@@ -307,6 +307,18 @@ const remainingRouter: AppRouteRecordRaw[] = [
activityId: route.query.activityId
})
},
{
path: 'process-instance/report',
component: () => import('@/views/bpm/processInstance/report/index.vue'),
name: 'BpmProcessInstanceReport',
meta: {
noCache: true,
hidden: true,
canTo: true,
title: '数据报表',
activeMenu: '/bpm/manager/model'
}
},
{
path: 'oa/leave/create',
component: () => import('@/views/bpm/oa/leave/create.vue'),
@@ -344,7 +356,8 @@ const remainingRouter: AppRouteRecordRaw[] = [
}
},
{
path: 'manager/model/update/:id',
// TODO @zws:1)建议,在加一个路由。然后标题是“复制流程”,这样体验会好点;2)复制出来的数据,在名字前面,加“副本 ”,和钉钉保持一致!
path: 'manager/model/:type/:id',
component: () => import('@/views/bpm/model/form/index.vue'),
name: 'BpmModelUpdate',
meta: {
+24 -2
View File
@@ -4,16 +4,19 @@ import { getRawRoute } from '@/utils/routerHelper'
import { defineStore } from 'pinia'
import { store } from '../index'
import { findIndex } from '@/utils'
import { useUserStoreWithOut } from './user'
export interface TagsViewState {
visitedViews: RouteLocationNormalizedLoaded[]
cachedViews: Set<string>
selectedTag?: RouteLocationNormalizedLoaded
}
export const useTagsViewStore = defineStore('tagsView', {
state: (): TagsViewState => ({
visitedViews: [],
cachedViews: new Set()
cachedViews: new Set(),
selectedTag: undefined
}),
getters: {
getVisitedViews(): RouteLocationNormalizedLoaded[] {
@@ -21,6 +24,9 @@ export const useTagsViewStore = defineStore('tagsView', {
},
getCachedViews(): string[] {
return Array.from(this.cachedViews)
},
getSelectedTag(): RouteLocationNormalizedLoaded | undefined {
return this.selectedTag
}
},
actions: {
@@ -98,8 +104,12 @@ export const useTagsViewStore = defineStore('tagsView', {
},
// 删除所有tag
delAllVisitedViews() {
const userStore = useUserStoreWithOut()
// const affixTags = this.visitedViews.filter((tag) => tag.meta.affix)
this.visitedViews = []
this.visitedViews = userStore.getUser
? this.visitedViews.filter((tag) => tag?.meta?.affix)
: []
},
// 删除其他
delOthersViews(view: RouteLocationNormalizedLoaded) {
@@ -145,6 +155,18 @@ export const useTagsViewStore = defineStore('tagsView', {
break
}
}
},
// 设置当前选中的 tag
setSelectedTag(tag: RouteLocationNormalizedLoaded) {
this.selectedTag = tag
},
setTitle(title: string, path?: string) {
for (const v of this.visitedViews) {
if (v.path === (path ?? this.selectedTag?.path)) {
v.meta.title = title
break
}
}
}
},
persist: false
+5 -5
View File
@@ -15,7 +15,7 @@ interface UserVO {
interface UserInfoVO {
// USER 缓存
permissions: string[]
permissions: Set<string>
roles: string[]
isSetUser: boolean
user: UserVO
@@ -23,7 +23,7 @@ interface UserInfoVO {
export const useUserStore = defineStore('admin-user', {
state: (): UserInfoVO => ({
permissions: [],
permissions: new Set<string>(),
roles: [],
isSetUser: false,
user: {
@@ -34,7 +34,7 @@ export const useUserStore = defineStore('admin-user', {
}
}),
getters: {
getPermissions(): string[] {
getPermissions(): Set<string> {
return this.permissions
},
getRoles(): string[] {
@@ -57,7 +57,7 @@ export const useUserStore = defineStore('admin-user', {
if (!userInfo) {
userInfo = await getInfo()
}
this.permissions = userInfo.permissions
this.permissions = new Set(userInfo.permissions)
this.roles = userInfo.roles
this.user = userInfo.user
this.isSetUser = true
@@ -85,7 +85,7 @@ export const useUserStore = defineStore('admin-user', {
this.resetState()
},
resetState() {
this.permissions = []
this.permissions = new Set<string>()
this.roles = []
this.isSetUser = false
this.user = {
+6
View File
@@ -457,3 +457,9 @@ export const BpmProcessInstanceStatus = {
REJECT: 3, // 审批不通过
CANCEL: 4 // 已取消
}
export const BpmAutoApproveType = {
NONE: 0, // 不自动通过
APPROVE_ALL: 1, // 仅审批一次,后续重复的审批节点均自动通过
APPROVE_SEQUENT: 2, // 仅针对连续审批的节点自动通过
}
+29
View File
@@ -33,6 +33,10 @@ const download = {
markdown: (data: Blob, fileName: string) => {
download0(data, fileName, 'text/markdown')
},
// 下载 Json 方法
json: (data: Blob, fileName: string) => {
download0(data, fileName, 'application/json')
},
// 下载图片(允许跨域)
image: ({
url,
@@ -65,6 +69,31 @@ const download = {
a.download = 'image.png'
a.click()
}
},
base64ToFile: (base64: any, fileName: string) => {
// 将base64按照 , 进行分割 将前缀 与后续内容分隔开
const data = base64.split(',')
// 利用正则表达式 从前缀中获取图片的类型信息(image/png、image/jpeg、image/webp等)
const type = data[0].match(/:(.*?);/)[1]
// 从图片的类型信息中 获取具体的文件格式后缀(png、jpeg、webp)
const suffix = type.split('/')[1]
// 使用atob()对base64数据进行解码 结果是一个文件数据流 以字符串的格式输出
const bstr = window.atob(data[1])
// 获取解码结果字符串的长度
let n = bstr.length
// 根据解码结果字符串的长度创建一个等长的整形数字数组
// 但在创建时 所有元素初始值都为 0
const u8arr = new Uint8Array(n)
// 将整形数组的每个元素填充为解码结果字符串对应位置字符的UTF-16 编码单元
while (n--) {
// charCodeAt():获取给定索引处字符对应的 UTF-16 代码单元
u8arr[n] = bstr.charCodeAt(n)
}
// 将File文件对象返回给方法的调用者
return new File([u8arr], `${fileName}.${suffix}`, {
type: type
})
}
}
+4 -15
View File
@@ -1,4 +1,6 @@
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
import {hasPermission} from "@/directives/permission/hasPermi";
const { t } = useI18n() // 国际化
@@ -7,21 +9,8 @@ const { t } = useI18n() // 国际化
* @param {Array} value 校验值
* @returns {Boolean}
*/
export function checkPermi(value: string[]) {
if (value && value instanceof Array && value.length > 0) {
const { wsCache } = useCache()
const permissionDatas = value
const all_permission = '*:*:*'
const userInfo = wsCache.get(CACHE_KEY.USER)
const permissions = userInfo?.permissions || []
const hasPermission = permissions.some((permission: string) => {
return all_permission === permission || permissionDatas.includes(permission)
})
return !!hasPermission
} else {
console.error(t('permission.hasPermission'))
return false
}
export function checkPermi(permission: string[]) {
return hasPermission(permission)
}
/**
+1 -2
View File
@@ -73,7 +73,7 @@ export const generateRoute = (routes: AppCustomRouteRecordRaw[]): AppRouteRecord
noCache: !route.keepAlive,
alwaysShow:
route.children &&
route.children.length === 1 &&
route.children.length > 0 &&
(route.alwaysShow !== undefined ? route.alwaysShow : true)
} as any
// 特殊逻辑:如果后端配置的 MenuDO.component 包含 ?,则表示需要传递参数
@@ -100,7 +100,6 @@ export const generateRoute = (routes: AppCustomRouteRecordRaw[]): AppRouteRecord
//处理顶级非目录路由
if (!route.children && route.parentId == 0 && route.component) {
data.component = Layout
data.meta = {}
data.name = toCamelCase(route.path, true) + 'Parent'
data.redirect = ''
meta.alwaysShow = true
+3 -1
View File
@@ -59,6 +59,8 @@
<RegisterForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
<!-- 三方登录 -->
<SSOLoginVue class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
<!-- 忘记密码 -->
<ForgetPasswordForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
</div>
</Transition>
</div>
@@ -73,7 +75,7 @@ import { useAppStore } from '@/store/modules/app'
import { ThemeSwitch } from '@/layout/components/ThemeSwitch'
import { LocaleDropdown } from '@/layout/components/LocaleDropdown'
import { LoginForm, MobileForm, QrCodeForm, RegisterForm, SSOLoginVue } from './components'
import { LoginForm, MobileForm, QrCodeForm, RegisterForm, SSOLoginVue, ForgetPasswordForm } from './components'
defineOptions({ name: 'Login' })
@@ -0,0 +1,278 @@
<template>
<el-form
v-show="getShow"
ref="formSmsResetPassword"
:model="resetPasswordData"
:rules="rules"
class="login-form"
label-position="top"
label-width="120px"
size="large"
>
<el-row style="margin-right: -10px; margin-left: -10px">
<!-- 租户名 -->
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item>
<LoginFormTitle style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item v-if="resetPasswordData.tenantEnable === 'true'" prop="tenantName">
<el-input
v-model="resetPasswordData.tenantName"
:placeholder="t('login.tenantNamePlaceholder')"
:prefix-icon="iconHouse"
type="primary"
link
/>
</el-form-item>
</el-col>
<!-- 手机号 -->
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item prop="mobile">
<el-input
v-model="resetPasswordData.mobile"
:placeholder="t('login.mobileNumberPlaceholder')"
:prefix-icon="iconCellphone"
/>
</el-form-item>
</el-col>
<Verify
ref="verify"
:captchaType="captchaType"
:imgSize="{ width: '400px', height: '200px' }"
mode="pop"
@success="getSmsCode"
/>
<!-- 验证码 -->
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item prop="code">
<el-row :gutter="5" justify="space-between" style="width: 100%">
<el-col :span="24">
<el-input
v-model="resetPasswordData.code"
:placeholder="t('login.codePlaceholder')"
:prefix-icon="iconCircleCheck"
>
<template #append>
<span
v-if="mobileCodeTimer <= 0"
class="getMobileCode"
style="cursor: pointer"
@click="getCode"
>
{{ t('login.getSmsCode') }}
</span>
<span v-if="mobileCodeTimer > 0" class="getMobileCode" style="cursor: pointer">
{{ mobileCodeTimer }}秒后可重新获取
</span>
</template>
</el-input>
<!-- </el-button> -->
</el-col>
</el-row>
</el-form-item>
</el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item prop="password">
<InputPassword
v-model="resetPasswordData.password"
:placeholder="t('login.passwordPlaceholder')"
style="width: 100%"
strength="true"
/>
</el-form-item>
</el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item prop="check_password">
<InputPassword
v-model="resetPasswordData.check_password"
:placeholder="t('login.checkPassword')"
style="width: 100%"
strength="true"
/>
</el-form-item>
</el-col>
<!-- 登录按钮 / 返回按钮 -->
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item>
<XButton
:loading="loginLoading"
:title="t('login.resetPassword')"
class="w-[100%]"
type="primary"
@click="resetPassword()"
/>
</el-form-item>
</el-col>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item>
<XButton
:loading="loginLoading"
:title="t('login.backLogin')"
class="w-[100%]"
@click="handleBackLogin()"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script lang="ts" setup>
import type { RouteLocationNormalizedLoaded } from 'vue-router'
import { useIcon } from '@/hooks/web/useIcon'
import { sendSmsCode, smsResetPassword } from '@/api/login'
import LoginFormTitle from './LoginFormTitle.vue'
import { LoginStateEnum, useFormValid, useLoginState } from './useLogin'
import { ElLoading } from 'element-plus'
import * as authUtil from '@/utils/auth'
import * as LoginApi from '@/api/login'
defineOptions({ name: 'ForgetPasswordForm' })
const verify = ref()
const { t } = useI18n()
const message = useMessage()
const { currentRoute, push } = useRouter()
const formSmsResetPassword = ref()
const loginLoading = ref(false)
const iconHouse = useIcon({ icon: 'ep:house' })
const iconCellphone = useIcon({ icon: 'ep:cellphone' })
const iconCircleCheck = useIcon({ icon: 'ep:circle-check' })
const { validForm } = useFormValid(formSmsResetPassword)
const { handleBackLogin, getLoginState, setLoginState } = useLoginState()
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.RESET_PASSWORD)
const captchaType = ref('blockPuzzle') // blockPuzzle 滑块 clickWord 点击文字
const validatePass2 = (rule, value, callback) => {
if (value === '') {
callback(new Error('请再次输入密码'))
} else if (value !== resetPasswordData.password) {
callback(new Error('两次输入密码不一致!'))
} else {
callback()
}
}
const rules = {
tenantName: [{ required: true, min: 2, max: 20, trigger: 'blur', message: '长度为4到16位' }],
mobile: [{ required: true, min: 11, max: 11, trigger: 'blur', message: '手机号长度为11位' }],
password: [
{
required: true,
min: 4,
max: 16,
validator: validatePass2,
trigger: 'blur',
message: '密码长度为4到16位'
}
],
check_password: [{ required: true, validator: validatePass2, trigger: 'blur' }],
code: [required]
}
const resetPasswordData = reactive({
captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE,
tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE,
tenantName: '',
username: '',
password: '',
check_password: '',
mobile: '',
code: ''
})
const smsVO = reactive({
tenantName: '',
mobile: '',
captchaVerification: '',
scene: 23
})
const mobileCodeTimer = ref(0)
const redirect = ref<string>('')
// 获取验证码
const getCode = async () => {
// 情况一,未开启:则直接发送验证码
if (resetPasswordData.captchaEnable === 'false') {
await getSmsCode({})
} else {
// 情况二,已开启:则展示验证码;只有完成验证码的情况,才进行发送验证码
// 弹出验证码
verify.value.show()
}
}
const getSmsCode = async (params) => {
if (resetPasswordData.tenantEnable === 'true') {
await getTenantId()
}
smsVO.captchaVerification = params.captchaVerification
smsVO.mobile = resetPasswordData.mobile
await sendSmsCode(smsVO).then(async () => {
message.success(t('login.SmsSendMsg'))
// 设置倒计时
mobileCodeTimer.value = 60
let msgTimer = setInterval(() => {
mobileCodeTimer.value = mobileCodeTimer.value - 1
if (mobileCodeTimer.value <= 0) {
clearInterval(msgTimer)
}
}, 1000)
})
}
watch(
() => currentRoute.value,
(route: RouteLocationNormalizedLoaded) => {
redirect.value = route?.query?.redirect as string
},
{
immediate: true
}
)
const getTenantId = async () => {
if (resetPasswordData.tenantEnable === 'true') {
const res = await LoginApi.getTenantIdByName(resetPasswordData.tenantName)
if (res == null) {
message.error(t('login.invalidTenantName'))
throw t('login.invalidTenantName')
}
authUtil.setTenantId(res)
}
}
// 重置密码
const resetPassword = async () => {
const data = await validForm()
if (!data) return
await getTenantId()
loginLoading.value = true
await smsResetPassword(resetPasswordData)
.then(async () => {
message.success(t('login.resetPasswordSuccess'))
setLoginState(LoginStateEnum.LOGIN)
})
.catch(() => {})
.finally(() => {
loginLoading.value = false
setTimeout(() => {
const loadingInstance = ElLoading.service()
loadingInstance.close()
}, 400)
})
}
</script>
<style lang="scss" scoped>
:deep(.anticon) {
&:hover {
color: var(--el-color-primary) !important;
}
}
.smsbtn {
margin-top: 33px;
}
</style>
+7 -1
View File
@@ -59,7 +59,13 @@
</el-checkbox>
</el-col>
<el-col :offset="6" :span="12">
<el-link style="float: right" type="primary">{{ t('login.forgetPassword') }}</el-link>
<el-link
style="float: right"
type="primary"
@click="setLoginState(LoginStateEnum.RESET_PASSWORD)"
>
{{ t('login.forgetPassword') }}
</el-link>
</el-col>
</el-row>
</el-form-item>
+2 -1
View File
@@ -4,5 +4,6 @@ import LoginFormTitle from './LoginFormTitle.vue'
import RegisterForm from './RegisterForm.vue'
import QrCodeForm from './QrCodeForm.vue'
import SSOLoginVue from './SSOLogin.vue'
import ForgetPasswordForm from './ForgetPasswordForm.vue'
export { LoginForm, MobileForm, LoginFormTitle, RegisterForm, QrCodeForm, SSOLoginVue }
export { LoginForm, MobileForm, LoginFormTitle, RegisterForm, QrCodeForm, SSOLoginVue, ForgetPasswordForm }
+5
View File
@@ -13,6 +13,9 @@
<el-form-item label="分类标志" prop="code">
<el-input v-model="formData.code" placeholder="请输入分类标志" />
</el-form-item>
<el-form-item label="分类描述" prop="description">
<el-input v-model="formData.description" type="textarea" placeholder="请输入分类描述" />
</el-form-item>
<el-form-item label="分类状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio
@@ -58,6 +61,7 @@ const formData = ref({
id: undefined,
name: undefined,
code: undefined,
description: undefined,
status: CommonStatusEnum.ENABLE,
sort: undefined
})
@@ -117,6 +121,7 @@ const resetForm = () => {
id: undefined,
name: undefined,
code: undefined,
description: undefined,
status: CommonStatusEnum.ENABLE,
sort: undefined
}
+178 -74
View File
@@ -1,5 +1,5 @@
<template>
<div class="flex items-center h-50px">
<div class="flex items-center h-50px" v-memo="[categoryInfo.name, isCategorySorting]">
<!-- 头部:分类名 -->
<div class="flex items-center">
<el-tooltip content="拖动排序" v-if="isCategorySorting">
@@ -13,7 +13,7 @@
<div class="color-gray-600 text-16px"> ({{ categoryInfo.modelList?.length || 0 }}) </div>
</div>
<!-- 头部:操作 -->
<div class="flex-1 flex" v-if="!isCategorySorting">
<div class="flex-1 flex" v-show="!isCategorySorting">
<div
v-if="categoryInfo.modelList.length > 0"
class="ml-20px flex items-center"
@@ -64,20 +64,22 @@
</div>
</div>
</div>
<!-- 模型列表 -->
<el-collapse-transition>
<div v-show="isExpand">
<el-table
v-if="modelList && modelList.length > 0"
: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"
:header-cell-style="tableHeaderStyle"
:cell-style="tableCellStyle"
:row-style="{ height: '68px' }"
>
<el-table-column label="流程名" prop="name" min-width="150">
<template #default="scope">
<template #default="{ row }">
<div class="flex items-center">
<el-tooltip content="拖动排序" v-if="isModelSorting">
<Icon
@@ -85,32 +87,30 @@
class="drag-icon cursor-move text-#8a909c mr-10px"
/>
</el-tooltip>
<el-image :src="scope.row.icon" class="h-38px w-38px mr-10px rounded" />
{{ scope.row.name }}
<el-image v-if="row.icon" :src="row.icon" class="h-38px w-38px mr-10px rounded" />
{{ row.name }}
</div>
</template>
</el-table-column>
<el-table-column label="可见范围" prop="startUserIds" min-width="100">
<template #default="scope">
<el-text v-if="!scope.row.startUsers || scope.row.startUsers.length === 0">
全部可见
</el-text>
<el-text v-else-if="scope.row.startUsers.length == 1">
{{ scope.row.startUsers[0].nickname }}
<el-table-column label="可见范围" prop="startUserIds" min-width="150">
<template #default="{ row }">
<el-text v-if="!row.startUsers?.length"> 全部可见 </el-text>
<el-text v-else-if="row.startUsers.length === 1">
{{ row.startUsers[0].nickname }}
</el-text>
<el-text v-else>
<el-tooltip
class="box-item"
effect="dark"
placement="top"
:content="scope.row.startUsers.map((user: any) => user.nickname).join('、')"
:content="row.startUsers.map((user: any) => user.nickname).join('、')"
>
{{ scope.row.startUsers[0].nickname }}等 {{ scope.row.startUsers.length }} 人可见
{{ row.startUsers[0].nickname }}等 {{ row.startUsers.length }} 人可见
</el-tooltip>
</el-text>
</template>
</el-table-column>
<el-table-column label="表单信息" prop="formType" min-width="200">
<el-table-column label="表单信息" prop="formType" min-width="150">
<template #default="scope">
<el-button
v-if="scope.row.formType === BpmModelFormType.NORMAL"
@@ -157,27 +157,26 @@
link
type="primary"
@click="openModelForm('update', scope.row.id)"
v-hasPermi="['bpm:model:update']"
v-if="hasPermiUpdate"
:disabled="!isManagerUser(scope.row)"
>
修改
</el-button>
<el-button
link
class="!ml-5px"
type="primary"
@click="handleDesign(scope.row)"
v-hasPermi="['bpm:model:update']"
@click="openModelForm('copy', scope.row.id)"
v-if="hasPermiUpdate"
:disabled="!isManagerUser(scope.row)"
>
设计
复制
</el-button>
<el-button
link
class="!ml-5px"
type="primary"
@click="handleDeploy(scope.row)"
v-hasPermi="['bpm:model:deploy']"
v-if="hasPermiDeploy"
:disabled="!isManagerUser(scope.row)"
>
发布
@@ -185,28 +184,43 @@
<el-dropdown
class="!align-middle ml-5px"
@command="(command) => handleModelCommand(command, scope.row)"
v-hasPermi="['bpm:process-definition:query', 'bpm:model:update', 'bpm:model:delete']"
v-if="hasPermiMore"
>
<el-button type="primary" link>更多</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
command="handleDefinitionList"
v-if="checkPermi(['bpm:process-definition:query'])"
>
<el-dropdown-item command="handleDefinitionList" v-if="hasPermiPdQuery">
历史
</el-dropdown-item>
<el-dropdown-item
command="handleReport"
v-if="
checkPermi(['bpm:process-instance:manager-query']) &&
scope.row.processDefinition
"
:disabled="!isManagerUser(scope.row)"
>
报表
</el-dropdown-item>
<el-dropdown-item
command="handleChangeState"
v-if="checkPermi(['bpm:model:update']) && scope.row.processDefinition"
v-if="hasPermiUpdate && scope.row.processDefinition"
:disabled="!isManagerUser(scope.row)"
>
{{ scope.row.processDefinition.suspensionState === 1 ? '停用' : '启用' }}
</el-dropdown-item>
<el-dropdown-item
type="danger"
command="handleClean"
v-if="checkPermi(['bpm:model:clean'])"
:disabled="!isManagerUser(scope.row)"
>
清理
</el-dropdown-item>
<el-dropdown-item
type="danger"
command="handleDelete"
v-if="checkPermi(['bpm:model:delete'])"
v-if="hasPermiDelete"
:disabled="!isManagerUser(scope.row)"
>
删除
@@ -235,16 +249,11 @@
</div>
</template>
</Dialog>
<!-- 表单弹窗:添加流程模型 -->
<ModelForm :categoryId="categoryInfo.code" ref="modelFormRef" @success="emit('success')" />
</template>
<script lang="ts" setup>
import ModelForm from './ModelForm.vue'
import { CategoryApi, CategoryVO } from '@/api/bpm/category'
import Sortable from 'sortablejs'
import { propTypes } from '@/utils/propTypes'
import { formatDate } from '@/utils/formatTime'
import * as ModelApi from '@/api/bpm/model'
import * as FormApi from '@/api/bpm/form'
@@ -253,26 +262,89 @@ import { BpmModelFormType } 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'
import { cloneDeep, isEqual } from 'lodash-es'
import { useTagsView } from '@/hooks/web/useTagsView'
import { useDebounceFn } from '@vueuse/core'
defineOptions({ name: 'BpmModel' })
const props = defineProps({
categoryInfo: propTypes.object.def([]), // 分类后的数据
isCategorySorting: propTypes.bool.def(false) // 是否分类在排序
})
// 优化 Props 类型定义
interface UserInfo {
nickname: string
[key: string]: any
}
interface ProcessDefinition {
deploymentTime: string
version: number
suspensionState: number
}
interface ModelInfo {
id: number
name: string
icon?: string
startUsers?: UserInfo[]
processDefinition?: ProcessDefinition
formType?: number
formId?: number
formName?: string
formCustomCreatePath?: string
managerUserIds?: number[]
[key: string]: any
}
interface CategoryInfoProps {
id: number
name: string
modelList: ModelInfo[]
}
const props = defineProps<{
categoryInfo: CategoryInfoProps
isCategorySorting: boolean
}>()
const emit = defineEmits(['success'])
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const { push } = useRouter() // 路由
const userStore = useUserStoreWithOut() // 用户信息缓存
const isDark = computed(() => useAppStore().getIsDark) // 是否黑暗模式
const router = useRouter() // 路由
const isModelSorting = ref(false) // 是否正处于排序状态
const originalData: any = ref([]) // 原始数据
const modelList: any = ref([]) // 模型列表
const originalData = ref<ModelInfo[]>([]) // 原始数据
const modelList = ref<ModelInfo[]>([]) // 模型列表
const isExpand = ref(false) // 是否处于展开状态
// 使用 computed 优化表格样式计算
const tableHeaderStyle = computed(() => ({
backgroundColor: isDark.value ? '' : '#edeff0',
paddingLeft: '10px'
}))
const tableCellStyle = computed(() => ({
paddingLeft: '10px'
}))
/** 权限校验:通过 computed 解决列表的卡顿问题 */
const hasPermiUpdate = computed(() => {
return checkPermi(['bpm:model:update'])
})
const hasPermiDelete = computed(() => {
return checkPermi(['bpm:model:delete'])
})
const hasPermiDeploy = computed(() => {
return checkPermi(['bpm:model:deploy'])
})
const hasPermiMore = computed(() => {
return checkPermi(['bpm:process-definition:query', 'bpm:model:update', 'bpm:model:delete'])
})
const hasPermiPdQuery = computed(() => {
return checkPermi(['bpm:process-definition:query'])
})
/** '更多'操作按钮 */
const handleModelCommand = (command: string, row: any) => {
switch (command) {
@@ -285,6 +357,18 @@ const handleModelCommand = (command: string, row: any) => {
case 'handleChangeState':
handleChangeState(row)
break
case 'handleClean':
handleClean(row)
break
case 'handleReport':
router.push({
name: 'BpmProcessInstanceReport',
query: {
processDefinitionId: row.processDefinition.id,
processDefinitionKey: row.key
}
})
break
default:
break
}
@@ -318,6 +402,19 @@ const handleDelete = async (row: any) => {
} catch {}
}
/** 清理按钮操作 */
const handleClean = async (row: any) => {
try {
// 清理的二次确认
await message.confirm('是否确认清理流程名字为"' + row.name + '"的数据项?')
// 发起清理
await ModelApi.cleanModel(row.id)
message.success('清理成功')
// 刷新列表
emit('success')
} catch {}
}
/** 更新状态操作 */
const handleChangeState = async (row: any) => {
const state = row.processDefinition.suspensionState
@@ -337,14 +434,6 @@ const handleChangeState = async (row: any) => {
} catch {}
}
/** 设计流程 */
const handleDesign = (row: any) => {
push({
name: 'BpmModelUpdate',
params: { id: row.id }
})
}
/** 发布流程 */
const handleDeploy = async (row: any) => {
try {
@@ -422,14 +511,15 @@ const handleModelSortCancel = () => {
/** 创建拖拽实例 */
const tableRef = ref()
const initSort = () => {
const initSort = useDebounceFn(() => {
const table = document.querySelector(`.${props.categoryInfo.name} .el-table__body-wrapper tbody`)
if (!table) return
Sortable.create(table, {
group: 'shared',
animation: 150,
draggable: '.el-table__row',
handle: '.drag-icon',
// 结束拖动事件
onEnd: ({ newDraggableIndex, oldDraggableIndex }) => {
if (oldDraggableIndex !== newDraggableIndex) {
modelList.value.splice(
@@ -440,15 +530,18 @@ const initSort = () => {
}
}
})
}
}, 200)
/** 更新 modelList 模型列表 */
const updateModeList = () => {
modelList.value = cloneDeep(props.categoryInfo.modelList)
if (props.categoryInfo.modelList.length > 0) {
isExpand.value = true
const updateModeList = useDebounceFn(() => {
const newModelList = props.categoryInfo.modelList
if (!isEqual(modelList.value, newModelList)) {
modelList.value = cloneDeep(newModelList)
if (newModelList?.length > 0) {
isExpand.value = true
}
}
}
}, 100)
/** 重命名弹窗确定 */
const renameCategoryVisible = ref(false)
@@ -483,26 +576,31 @@ const handleDeleteCategory = async () => {
}
/** 添加流程模型弹窗 */
const modelFormRef = ref()
const openModelForm = (type: string, id?: number) => {
const tagsView = useTagsView()
const openModelForm = async (type: string, id?: number) => {
if (type === 'create') {
push({ name: 'BpmModelCreate' })
await push({ name: 'BpmModelCreate' })
} else {
push({
await push({
name: 'BpmModelUpdate',
params: { id }
params: { id, type }
})
// 设置标题
if (type === 'copy') {
tagsView.setTitle('复制流程')
}
}
}
watch(() => props.categoryInfo.modelList, updateModeList, { immediate: true })
watch(
() => props.isCategorySorting,
(val) => {
if (val) isExpand.value = false
},
{ immediate: true }
)
watchEffect(() => {
if (props.categoryInfo?.modelList) {
updateModeList()
}
if (props.isCategorySorting) {
isExpand.value = false
}
})
</script>
<style lang="scss">
@@ -519,10 +617,16 @@ watch(
}
</style>
<style lang="scss" scoped>
:deep() {
.el-table__cell {
.category-draggable-model {
:deep(.el-table__cell) {
overflow: hidden;
border-bottom: none !important;
}
// 优化表格渲染性能
:deep(.el-table__body) {
will-change: transform;
transform: translateZ(0);
}
}
</style>
-440
View File
@@ -1,440 +0,0 @@
<template>
<Dialog v-model="dialogVisible" :title="dialogTitle" width="600">
<el-form
ref="formRef"
v-loading="formLoading"
:model="formData"
:rules="formRules"
label-width="110px"
>
<el-form-item label="流程标识" prop="key">
<el-input v-model="formData.key" :disabled="!!formData.id" placeholder="请输入流标标识" />
<el-tooltip
v-if="!formData.id"
class="item"
content="新建后,流程标识不可修改!"
effect="light"
placement="top"
>
<i class="el-icon-question" style="padding-left: 5px"></i>
</el-tooltip>
<el-tooltip v-else class="item" content="流程标识不可修改!" effect="light" placement="top">
<i class="el-icon-question" style="padding-left: 5px"></i>
</el-tooltip>
</el-form-item>
<el-form-item label="流程名称" prop="name">
<el-input
v-model="formData.name"
:disabled="!!formData.id"
clearable
placeholder="请输入流程名称"
/>
</el-form-item>
<el-form-item label="流程分类" prop="category">
<el-select
v-model="formData.category"
clearable
placeholder="请选择流程分类"
style="width: 100%"
>
<el-option
v-for="category in categoryList"
:key="category.code"
:label="category.name"
:value="category.code"
/>
</el-select>
</el-form-item>
<el-form-item label="流程图标" prop="icon">
<UploadImg v-model="formData.icon" :limit="1" height="64px" width="64px" />
</el-form-item>
<el-form-item label="流程描述" prop="description">
<el-input v-model="formData.description" clearable type="textarea" />
</el-form-item>
<el-form-item label="流程类型" prop="type">
<el-radio-group v-model="formData.type">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_TYPE)"
:key="dict.value"
:label="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="表单类型" prop="formType">
<el-radio-group v-model="formData.formType">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_FORM_TYPE)"
:key="dict.value"
:label="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="formData.formType === 10" label="流程表单" prop="formId">
<el-select v-model="formData.formId" clearable style="width: 100%">
<el-option v-for="form in formList" :key="form.id" :label="form.name" :value="form.id" />
</el-select>
</el-form-item>
<el-form-item
v-if="formData.formType === 20"
label="表单提交路由"
prop="formCustomCreatePath"
>
<el-input
v-model="formData.formCustomCreatePath"
placeholder="请输入表单提交路由"
style="width: 330px"
/>
<el-tooltip
class="item"
content="自定义表单的提交路径,使用 Vue 的路由地址,例如说:bpm/oa/leave/create.vue"
effect="light"
placement="top"
>
<i class="el-icon-question" style="padding-left: 5px"></i>
</el-tooltip>
</el-form-item>
<el-form-item v-if="formData.formType === 20" label="表单查看地址" prop="formCustomViewPath">
<el-input
v-model="formData.formCustomViewPath"
placeholder="请输入表单查看的组件地址"
style="width: 330px"
/>
<el-tooltip
class="item"
content="自定义表单的查看组件地址,使用 Vue 的组件地址,例如说:bpm/oa/leave/detail.vue"
effect="light"
placement="top"
>
<i class="el-icon-question" style="padding-left: 5px"></i>
</el-tooltip>
</el-form-item>
<el-form-item label="是否可见" prop="visible">
<el-radio-group v-model="formData.visible">
<el-radio
v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
:key="dict.value as string"
:label="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="谁可以发起" prop="startUserType">
<el-select
v-model="formData.startUserType"
placeholder="请选择谁可以发起"
@change="handleStartUserTypeChange"
>
<el-option label="全员" :value="0" />
<el-option label="指定人员" :value="1" />
<el-option label="均不可提交" :value="2" />
</el-select>
<div v-if="formData.startUserType === 1" class="mt-2 flex flex-wrap gap-2">
<div
v-for="user in selectedStartUsers"
:key="user.id"
class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative"
>
<el-avatar class="!m-5px" :size="28" v-if="user.avatar" :src="user.avatar" />
<el-avatar class="!m-5px" :size="28" v-else>
{{ user.nickname.substring(0, 1) }}
</el-avatar>
{{ user.nickname }}
<Icon
icon="ep:close"
class="ml-2 cursor-pointer hover:text-red-500"
@click="handleRemoveStartUser(user)"
/>
</div>
<el-button type="primary" link @click="openStartUserSelect">
<Icon icon="ep:plus" />选择人员
</el-button>
</div>
</el-form-item>
<el-form-item label="流程管理员" prop="managerUserType">
<el-select
v-model="formData.managerUserType"
placeholder="请选择流程管理员"
@change="handleManagerUserTypeChange"
>
<el-option label="全员" :value="0" />
<el-option label="指定人员" :value="1" />
<el-option label="均不可提交" :value="2" />
</el-select>
<div v-if="formData.managerUserType === 1" class="mt-2 flex flex-wrap gap-2">
<div
v-for="user in selectedManagerUsers"
:key="user.id"
class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative"
>
<el-avatar class="!m-5px" :size="28" v-if="user.avatar" :src="user.avatar" />
<el-avatar class="!m-5px" :size="28" v-else>
{{ user.nickname.substring(0, 1) }}
</el-avatar>
{{ user.nickname }}
<Icon
icon="ep:close"
class="ml-2 cursor-pointer hover:text-red-500"
@click="handleRemoveManagerUser(user)"
/>
</div>
<el-button type="primary" link @click="openManagerUserSelect">
<Icon icon="ep:plus" />选择人员
</el-button>
</div>
</el-form-item>
</el-form>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
<el-button @click="dialogVisible = false">取 消</el-button>
</template>
</Dialog>
<UserSelectForm ref="userSelectFormRef" @confirm="handleUserSelectConfirm" />
</template>
<script lang="ts" setup>
import { propTypes } from '@/utils/propTypes'
import { DICT_TYPE, getBoolDictOptions, getIntDictOptions } from '@/utils/dict'
import { ElMessageBox } from 'element-plus'
import * as ModelApi from '@/api/bpm/model'
import * as FormApi from '@/api/bpm/form'
import { CategoryApi, CategoryVO } from '@/api/bpm/category'
import { BpmModelFormType, BpmModelType } from '@/utils/constants'
import { UserVO } from '@/api/system/user'
import * as UserApi from '@/api/system/user'
import { useUserStoreWithOut } from '@/store/modules/user'
import { FormVO } from '@/api/bpm/form'
defineOptions({ name: 'ModelForm' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const userStore = useUserStoreWithOut() // 用户信息缓存
const props = defineProps({
categoryId: propTypes.number
})
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
const formType = ref('') // 表单的类型:create - 新增;update - 修改
const formData: any = ref({
id: undefined,
name: '',
key: '',
category: undefined,
icon: undefined,
description: '',
type: BpmModelType.BPMN,
formType: BpmModelFormType.NORMAL,
formId: '',
formCustomCreatePath: '',
formCustomViewPath: '',
visible: true,
startUserType: undefined,
managerUserType: undefined,
startUserIds: [],
managerUserIds: []
})
const formRules = reactive({
name: [{ required: true, message: '流程名称不能为空', trigger: 'blur' }],
key: [{ required: true, message: '流程标识不能为空', trigger: 'blur' }],
category: [{ required: true, message: '流程分类不能为空', trigger: 'blur' }],
icon: [{ required: true, message: '流程图标不能为空', trigger: 'blur' }],
type: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }],
formType: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }],
formId: [{ required: true, message: '流程表单不能为空', trigger: 'blur' }],
formCustomCreatePath: [{ required: true, message: '表单提交路由不能为空', trigger: 'blur' }],
formCustomViewPath: [{ required: true, message: '表单查看地址不能为空', trigger: 'blur' }],
visible: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }],
managerUserIds: [{ required: true, message: '流程管理员不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
const formList = ref<FormVO[]>([]) // 流程表单的下拉框的数据
const categoryList = ref<CategoryVO[]>([]) // 流程分类列表
const userList = ref<UserVO[]>([]) // 用户列表
const selectedStartUsers = ref<UserVO[]>([]) // 已选择的发起人列表
const selectedManagerUsers = ref<UserVO[]>([]) // 已选择的管理员列表
const userSelectFormRef = ref() // 用户选择弹窗 ref
const currentSelectType = ref<'start' | 'manager'>('start') // 当前选择的是发起人还是管理员
/** 打开弹窗 */
const open = async (type: string, id?: string) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
// 修改时,设置数据
if (id) {
formLoading.value = true
try {
formData.value = await ModelApi.getModel(id)
} finally {
formLoading.value = false
}
// 加载数据时,根据已有的用户ID列表初始化已选用户
if (formData.value.startUserIds?.length) {
formData.value.startUserType = 1
selectedStartUsers.value = userList.value.filter((user) =>
formData.value.startUserIds.includes(user.id)
)
}
if (formData.value.managerUserIds?.length) {
formData.value.managerUserType = 1
selectedManagerUsers.value = userList.value.filter((user) =>
formData.value.managerUserIds.includes(user.id)
)
}
} else {
formData.value.managerUserIds.push(userStore.getUser.id)
}
// 获得流程表单的下拉框的数据
formList.value = await FormApi.getFormSimpleList()
// 查询流程分类列表
categoryList.value = await CategoryApi.getCategorySimpleList()
// 查询用户列表
userList.value = await UserApi.getSimpleUserList()
if (props.categoryId) {
formData.value.category = props.categoryId
}
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
// 提交请求
formLoading.value = true
try {
const data = formData.value as unknown as ModelApi.ModelVO
if (formType.value === 'create') {
await ModelApi.createModel(data)
// 提示,引导用户做后续的操作
await ElMessageBox.alert(
'<strong>新建模型成功!</strong>后续需要执行如下 2 个步骤:' +
'<div>1. 点击【设计流程】按钮,绘制流程图</div>' +
'<div>2. 点击【发布流程】按钮,完成流程的最终发布</div>' +
'另外,每次流程修改后,都需要点击【发布流程】按钮,才能正式生效!!!',
'重要提示',
{
dangerouslyUseHTMLString: true,
type: 'success'
}
)
} else {
await ModelApi.updateModel(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
name: '',
key: '',
category: undefined,
icon: undefined,
description: '',
type: BpmModelType.BPMN,
formType: BpmModelFormType.NORMAL,
formId: '',
formCustomCreatePath: '',
formCustomViewPath: '',
visible: true,
startUserType: undefined,
managerUserType: undefined,
startUserIds: [],
managerUserIds: []
}
formRef.value?.resetFields()
selectedStartUsers.value = []
selectedManagerUsers.value = []
}
/** 处理发起人类型变化 */
const handleStartUserTypeChange = (value: number) => {
if (value !== 1) {
selectedStartUsers.value = []
formData.value.startUserIds = []
}
}
/** 处理管理员类型变化 */
const handleManagerUserTypeChange = (value: number) => {
if (value !== 1) {
selectedManagerUsers.value = []
formData.value.managerUserIds = []
}
}
/** 打开发起人选择 */
const openStartUserSelect = () => {
currentSelectType.value = 'start'
userSelectFormRef.value.open(0, selectedStartUsers.value)
}
/** 打开管理员选择 */
const openManagerUserSelect = () => {
currentSelectType.value = 'manager'
userSelectFormRef.value.open(0, selectedManagerUsers.value)
}
/** 处理用户选择确认 */
const handleUserSelectConfirm = (_, users: UserVO[]) => {
if (currentSelectType.value === 'start') {
selectedStartUsers.value = users
formData.value.startUserIds = users.map((u) => u.id)
} else {
selectedManagerUsers.value = users
formData.value.managerUserIds = users.map((u) => u.id)
}
}
/** 移除发起人 */
const handleRemoveStartUser = (user: UserVO) => {
selectedStartUsers.value = selectedStartUsers.value.filter((u) => u.id !== user.id)
formData.value.startUserIds = formData.value.startUserIds.filter((id: number) => id !== user.id)
}
/** 移除管理员 */
const handleRemoveManagerUser = (user: UserVO) => {
selectedManagerUsers.value = selectedManagerUsers.value.filter((u) => u.id !== user.id)
formData.value.managerUserIds = formData.value.managerUserIds.filter(
(id: number) => id !== user.id
)
}
</script>
<style lang="scss" scoped>
.bg-gray-100 {
background-color: #f5f7fa;
transition: all 0.3s;
&:hover {
background-color: #e6e8eb;
}
.ep-close {
font-size: 14px;
color: #909399;
transition: color 0.3s;
&:hover {
color: #f56c6c;
}
}
}
</style>
+15 -187
View File
@@ -12,10 +12,12 @@
:additionalModel="controlForm.additionalModel"
:model="model"
@save="save"
:process-id="modelKey"
:process-name="modelName"
/>
<!-- 流程属性器,负责编辑每个流程节点的属性 -->
<MyProcessPenal
v-if="isModelerReady && modeler"
v-if="modeler"
key="penal"
:bpmnModeler="modeler"
:prefix="controlForm.prefix"
@@ -37,8 +39,8 @@ defineOptions({ name: 'BpmModelEditor' })
const props = defineProps<{
modelId?: string
modelKey?: string
modelName?: string
modelKey: string
modelName: string
value?: string
}>()
@@ -51,10 +53,13 @@ const formType = ref(20)
provide('formFields', formFields)
provide('formType', formType)
const xmlString = ref<string>('') // BPMN XML
// 注入流程数据
const xmlString = inject('processData') as Ref
// 注入模型数据
const modelData = inject('modelData') as Ref
const modeler = shallowRef() // BPMN Modeler
const processDesigner = ref()
const isModelerReady = ref(false)
const controlForm = ref({
simulation: true,
labelEditing: false,
@@ -65,154 +70,26 @@ const controlForm = ref({
})
const model = ref<ModelApi.ModelVO>() // 流程模型的信息
// 初始化 bpmnInstances
const initBpmnInstances = () => {
if (!modeler.value) return false
try {
const instances = {
modeler: modeler.value,
modeling: modeler.value.get('modeling'),
moddle: modeler.value.get('moddle'),
eventBus: modeler.value.get('eventBus'),
bpmnFactory: modeler.value.get('bpmnFactory'),
elementFactory: modeler.value.get('elementFactory'),
elementRegistry: modeler.value.get('elementRegistry'),
replace: modeler.value.get('replace'),
selection: modeler.value.get('selection')
}
// 检查所有实例是否都存在
return Object.values(instances).every((instance) => instance)
} catch (error) {
console.error('初始化 bpmnInstances 失败:', error)
return false
}
}
/** 初始化 modeler */
const initModeler = async (item) => {
try {
modeler.value = item
// 等待 modeler 初始化完成
await nextTick()
// 确保 modeler 的所有实例都已经准备好
if (initBpmnInstances()) {
isModelerReady.value = true
emit('init-finished')
// 初始化完成后,设置初始值
if (props.modelId) {
// 编辑模式
const data = await ModelApi.getModel(props.modelId)
model.value = {
...data,
bpmnXml: undefined // 清空 bpmnXml 属性
}
xmlString.value = data.bpmnXml || getDefaultBpmnXml(data.key, data.name)
} else if (props.modelKey && props.modelName) {
// 新建模式
xmlString.value = props.value || getDefaultBpmnXml(props.modelKey, props.modelName)
model.value = {
key: props.modelKey,
name: props.modelName
} as ModelApi.ModelVO
}
// 导入XML并刷新视图
await nextTick()
try {
await modeler.value.importXML(xmlString.value)
if (processDesigner.value?.refresh) {
processDesigner.value.refresh()
}
} catch (error) {
console.error('导入XML失败:', error)
}
} else {
console.error('modeler 实例未完全初始化')
}
} catch (error) {
console.error('初始化 modeler 失败:', error)
}
}
/** 获取默认的BPMN XML */
const getDefaultBpmnXml = (key: string, name: string) => {
return `<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:xsd="http://www.w3.org/2001/XMLSchema" targetNamespace="http://www.activiti.org/processdef">
<process id="${key}" name="${name}" isExecutable="true" />
<bpmndi:BPMNDiagram id="BPMNDiagram">
<bpmndi:BPMNPlane id="${key}_di" bpmnElement="${key}" />
</bpmndi:BPMNDiagram>
</definitions>`
const initModeler = async (item: any) => {
//先初始化模型数据
model.value = modelData.value
modeler.value = item
}
/** 添加/修改模型 */
const save = async (bpmnXml: string) => {
try {
xmlString.value = bpmnXml
if (props.modelId) {
// 编辑模式
const data = {
...model.value,
bpmnXml: bpmnXml
} as unknown as ModelApi.ModelVO
await ModelApi.updateModelBpmn(data)
emit('success')
} else {
// 新建模式,直接返回XML
emit('success', bpmnXml)
}
emit('success', bpmnXml)
} catch (error) {
console.error('保存失败:', error)
message.error('保存失败')
}
}
// 监听 key、name 和 value 的变化
watch(
[() => props.modelKey, () => props.modelName, () => props.value],
async ([newKey, newName, newValue]) => {
if (!props.modelId && isModelerReady.value) {
let shouldRefresh = false
if (newKey && newName) {
const newXml = newValue || getDefaultBpmnXml(newKey, newName)
if (newXml !== xmlString.value) {
xmlString.value = newXml
shouldRefresh = true
}
model.value = {
...model.value,
key: newKey,
name: newName
} as ModelApi.ModelVO
} else if (newValue && newValue !== xmlString.value) {
xmlString.value = newValue
shouldRefresh = true
}
if (shouldRefresh) {
// 确保更新后重新渲染
await nextTick()
if (processDesigner.value?.refresh) {
try {
await modeler.value?.importXML(xmlString.value)
processDesigner.value.refresh()
} catch (error) {
console.error('导入XML失败:', error)
}
}
}
}
},
{ deep: true }
)
// 在组件卸载时清理
onBeforeUnmount(() => {
isModelerReady.value = false
modeler.value = null
// 清理全局实例
const w = window as any
@@ -220,55 +97,6 @@ onBeforeUnmount(() => {
w.bpmnInstances = null
}
})
/** 获取 XML 字符串 */
const saveXML = async () => {
if (!modeler.value) {
return { xml: xmlString.value }
}
try {
const result = await modeler.value.saveXML({ format: true })
xmlString.value = result.xml
return result
} catch (error) {
console.error('获取XML失败:', error)
return { xml: xmlString.value }
}
}
/** 获取SVG字符串 */
const saveSVG = async () => {
if (!modeler.value) {
return { svg: undefined }
}
try {
return await modeler.value.saveSVG()
} catch (error) {
console.error('获取SVG失败:', error)
return { svg: undefined }
}
}
/** 刷新视图 */
const refresh = async () => {
if (processDesigner.value?.refresh && modeler.value) {
try {
await modeler.value.importXML(xmlString.value)
processDesigner.value.refresh()
} catch (error) {
console.error('刷新视图失败:', error)
}
}
}
// 暴露必要的属性和方法给父组件
defineExpose({
modeler,
isModelerReady,
saveXML,
saveSVG,
refresh
})
</script>
<style lang="scss">
.process-panel__container {
+25 -53
View File
@@ -62,7 +62,7 @@
<el-radio-group v-model="modelData.visible">
<el-radio
v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
:key="dict.value"
:key="dict.value as string"
:value="dict.value"
>
{{ dict.label }}
@@ -77,7 +77,6 @@
>
<el-option label="全员" :value="0" />
<el-option label="指定人员" :value="1" />
<el-option label="均不可提交" :value="2" />
</el-select>
<div v-if="modelData.startUserType === 1" class="mt-2 flex flex-wrap gap-2">
<div
@@ -97,21 +96,12 @@
/>
</div>
<el-button type="primary" link @click="openStartUserSelect">
<Icon icon="ep:plus" />选择人员
<Icon icon="ep:plus" /> 选择人员
</el-button>
</div>
</el-form-item>
<el-form-item label="流程管理员" prop="managerUserType" class="mb-20px">
<el-select
v-model="modelData.managerUserType"
placeholder="请选择流程管理员"
@change="handleManagerUserTypeChange"
>
<el-option label="全员" :value="0" />
<el-option label="指定人员" :value="1" />
<el-option label="均不可提交" :value="2" />
</el-select>
<div v-if="modelData.managerUserType === 1" class="mt-2 flex flex-wrap gap-2">
<el-form-item label="流程管理员" prop="managerUserIds" class="mb-20px">
<div class="flex flex-wrap gap-2">
<div
v-for="user in selectedManagerUsers"
:key="user.id"
@@ -142,14 +132,11 @@
<script lang="ts" setup>
import { DICT_TYPE, getBoolDictOptions, getIntDictOptions } from '@/utils/dict'
import { UserVO } from '@/api/system/user'
import { CategoryVO } from '@/api/bpm/category'
const props = defineProps({
modelValue: {
type: Object,
required: true
},
categoryList: {
type: Array,
type: Array as PropType<CategoryVO[]>,
required: true
},
userList: {
@@ -158,8 +145,6 @@ const props = defineProps({
}
})
const emit = defineEmits(['update:modelValue'])
const formRef = ref()
const selectedStartUsers = ref<UserVO[]>([])
const selectedManagerUsers = ref<UserVO[]>([])
@@ -177,27 +162,30 @@ const rules = {
}
// 创建本地数据副本
const modelData = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
const modelData = defineModel<any>()
// 初始化选中的用户
watch(
() => props.modelValue,
() => modelData.value,
(newVal) => {
if (newVal.startUserIds?.length) {
selectedStartUsers.value = props.userList.filter((user: UserVO) =>
newVal.startUserIds.includes(user.id)
) as UserVO[]
} else {
selectedStartUsers.value = []
}
if (newVal.managerUserIds?.length) {
selectedManagerUsers.value = props.userList.filter((user: UserVO) =>
newVal.managerUserIds.includes(user.id)
) as UserVO[]
} else {
selectedManagerUsers.value = []
}
},
{ immediate: true }
{
immediate: true
}
)
/** 打开发起人选择 */
@@ -215,58 +203,42 @@ const openManagerUserSelect = () => {
/** 处理用户选择确认 */
const handleUserSelectConfirm = (_, users: UserVO[]) => {
if (currentSelectType.value === 'start') {
selectedStartUsers.value = users
emit('update:modelValue', {
modelData.value = {
...modelData.value,
startUserIds: users.map((u) => u.id)
})
}
} else {
selectedManagerUsers.value = users
emit('update:modelValue', {
modelData.value = {
...modelData.value,
managerUserIds: users.map((u) => u.id)
})
}
}
}
/** 处理发起人类型变化 */
const handleStartUserTypeChange = (value: number) => {
if (value !== 1) {
selectedStartUsers.value = []
emit('update:modelValue', {
modelData.value = {
...modelData.value,
startUserIds: []
})
}
}
/** 处理管理员类型变化 */
const handleManagerUserTypeChange = (value: number) => {
if (value !== 1) {
selectedManagerUsers.value = []
emit('update:modelValue', {
...modelData.value,
managerUserIds: []
})
}
}
}
/** 移除发起人 */
const handleRemoveStartUser = (user: UserVO) => {
selectedStartUsers.value = selectedStartUsers.value.filter((u) => u.id !== user.id)
emit('update:modelValue', {
modelData.value = {
...modelData.value,
startUserIds: modelData.value.startUserIds.filter((id: number) => id !== user.id)
})
}
}
/** 移除管理员 */
const handleRemoveManagerUser = (user: UserVO) => {
selectedManagerUsers.value = selectedManagerUsers.value.filter((u) => u.id !== user.id)
emit('update:modelValue', {
modelData.value = {
...modelData.value,
managerUserIds: modelData.value.managerUserIds.filter((id: number) => id !== user.id)
})
}
}
/** 表单校验 */
Loaded 100 of 127 files, more files were not shown because too many files have changed in this diff. Show more