389 Commits
Author SHA1 Message Date
YunaiV 4b2785bd41 【版本发布】2.3.0 发布~ 2024-10-07 15:14:21 +08:00
YunaiV 7b1b9084c7 【依赖升级】element-plus from 2.8.0 to 2.8.4
【打包优化】移除 --max_old_space_size=8192 内存配置,不会出现内存溢出
2024-10-07 15:12:52 +08:00
YunaiV 4105636741 【功能修复】工作流:暂时切回老的审批详情,预计 V2.4.0 会切到新的审批界面 2024-10-06 16:54:45 +08:00
芋道源码 d136128650 !559 余额权限修改
Merge pull request !559 from 骄傲的熊/master
2024-10-06 08:52:17 +00:00
骄傲的熊 1d9532ca5a 余额权限修改 2024-10-04 23:18:02 +08:00
芋道源码 b94235adf3 !452 BPM 的草稿 PR(非 BPM 开发成员,不用关注)
Merge pull request !452 from 芋道源码/feature/bpm
2024-10-04 09:46:09 +00:00
YunaiV 619491b4d0 【代码评审】工作流:新审批界面 2024-10-04 16:16:33 +08:00
YunaiV dd4c4817e4 【功能修复】工作流:新建的流程,部分数据的 NPE 问题 2024-10-04 11:47:45 +08:00
YunaiV 926eb70fe5 【功能优化】工作流:流程模型的定义,增加谁可以发起、谁可以管理的字段 CRUD 2024-10-03 20:05:18 +08:00
YunaiV dc0e947334 【功能优化】工作流:模型定义的列表,进行一定的简化,为后续把流程分类、流程定义融合在一个 vue 界面做准备。这样,和钉钉的界面更容易统一! 2024-10-03 17:13:13 +08:00
YunaiV 28f768f3b2 Merge branch 'master' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into feature/bpm 2024-10-03 11:28:45 +08:00
jason 145eabbf42 【工作流】- 新审批界面按钮权限 2024-10-02 16:23:55 +08:00
YunaiV 2d245d1fe7 【功能优化】积木报表:使用 refreshToken 认证,解决无法刷新访问令牌的问题 2024-10-02 14:15:13 +08:00
YunaiV 86c33e1f68 【功能优化】WebSocket 使用 refreshToken 认证,解决无法刷新访问令牌的问题 2024-10-02 14:11:02 +08:00
jason 2d16816f3f 【工作流】- 对接新获取表单字段权限接口 2024-10-02 09:49:07 +08:00
jason 27d24cd02d 【工作流】- 流程实例新界面修改 2024-10-01 23:35:11 +08:00
YunaiV 40512afb49 【错别字修改】优惠卷 => 优惠劵 2024-10-01 20:42:59 +08:00
芋道源码 53d1cb8fff !542 IOT client 同步
Merge pull request !542 from 芋道源码/feature/iot
2024-10-01 11:29:46 +00:00
YunaiV 6d641177b8 【代码评审】IoT:产品、设备、物模型的代码 2024-10-01 19:29:24 +08:00
芋道源码 9394a7cd60 !556 处理vite-plugin-top-level-await打包报错
Merge pull request !556 from OlyLis/master
2024-10-01 11:08:32 +00:00
YunaiV f84c10cdea 【功能修改】商城:限时折扣,多乘以了 100 优惠金额 2024-10-01 10:45:56 +08:00
YunaiV 79c3ba090f 【功能修改】工作流:流程实例列表,使用 processDefinitionKey 检索 2024-09-30 19:45:41 +08:00
安浩浩 4b2800f723 【修改功能】 IOT 物模型新增功能优化 2024-09-30 12:19:55 +08:00
YunaiV 887936ccf0 Merge branch 'dev' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 2024-09-30 09:05:53 +08:00
安浩浩 81a710c02d 【新增功能】 IOT 产品管理,物模型新增 2024-09-29 21:58:03 +08:00
cherishsince 4a2eddbd3b 【增加】dataset 创建数据清洗页面 (30%) 2024-09-29 18:04:06 +08:00
cherishsince 3f6e3c4168 【增加】dataset 创建选择数据源页面 2024-09-29 17:09:51 +08:00
cherishsince f36b01575f 【增加】dataset 列表页面 2024-09-29 17:09:37 +08:00
芋道源码 95bb19dc0b !558 【功能完善】商城: 完善积分商城装修
Merge pull request !558 from puhui999/dev-crm
2024-09-28 12:42:20 +00:00
芋道源码 fd907ddf3b !557 【功能优化】商城相关优化
Merge pull request !557 from 卢越/master
2024-09-28 12:33:48 +00:00
puhui999 d9f4336717 【功能完善】商城: 完善积分商城装修 2024-09-28 14:02:13 +08:00
puhui999 a132f07d04 【功能完善】商城: 完善积分商城装修 2024-09-28 13:40:51 +08:00
puhui999 1f8e419f78 【代码优化】商城: 完善积分商城 2024-09-28 10:00:46 +08:00
卢越 4204c31442 【功能修复】商城:底部导航栏装修,切换页面后图标变成默认的问题 2024-09-27 19:41:44 +08:00
卢越 cf4ed96f28 【功能优化】商城:客服页面优化 2024-09-26 16:04:52 +08:00
卢越 db98b376ff 【功能优化】商城:客服页面优化 2024-09-26 15:37:12 +08:00
YunaiV 7a6c69495d Merge branch 'master' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into dev 2024-09-25 09:26:41 +08:00
YunaiV 989e30810a 【功能修复】商城:diy 店铺装修的 fen2yuan 地址不正确 2024-09-25 09:01:44 +08:00
安浩浩 0e3ebbd2b8 【优化功能】 IOT 产品管理,Topic 类列表展示问题 2024-09-24 23:18:42 +08:00
YunaiV 26b025aca0 【代码优化】全局:移除 VITE_UPLOAD_URL 环境变量,使用 getUploadUrl 方法替代 2024-09-24 20:54:12 +08:00
YunaiV 74e04279c2 【功能优化】支付:查询支付订单时,增加 sync 主动轮询,解决支付宝、微信存在延迟的问题 2024-09-24 09:05:08 +08:00
安浩浩 6aaf611291 【新增】 IOT 产品管理,Topic 类列表 2024-09-23 23:34:21 +08:00
duxz 96593dac52 fix: 处理vite-plugin-top-level-await打包报错问题
Error: [vite-plugin-top-level-await] missing field `ctxt`
2024-09-23 14:50:56 +08:00
YunaiV da45ee034c 【代码评审】商城:积分商品的管理 2024-09-23 09:17:58 +08:00
芋道源码 4872f18dc6 !555 【新增】商城: 积分商城
Merge pull request !555 from puhui999/dev-crm
2024-09-23 01:07:39 +00:00
puhui999 a45c8e6d2d Merge remote-tracking branch 'yudao/dev' into dev-crm 2024-09-22 23:29:41 +08:00
puhui999 360cce10c7 【新增】商城: 积分商城 2024-09-22 23:29:22 +08:00
安浩浩 93a0789e34 【新增】 IOT 设备管理,设备详情 2024-09-22 15:42:20 +08:00
安浩浩 63a0e5dc3d 【优化】 IOT 设备管理 2024-09-22 13:17:12 +08:00
jason 4680a7dcfd 仿钉钉流程设计器, 条件节点修改 2024-09-21 11:01:56 +08:00
安浩浩 b69a784e86 【新增】 IOT 设备管理 2024-09-21 09:15:27 +08:00
jason 4a2a7b760a 【问题修复】仿钉钉设计器,条件节点,表单字段不显示 2024-09-20 22:11:19 +08:00
YunaiV 0db57da901 【功能修复】CRM:合同的成员不支持修改的问题 2024-09-17 18:51:11 +08:00
YunaiV 8f231bc296 【修复】修复菜单的路由地址为链接时不带参数的问题 2024-09-17 15:44:09 +08:00
芋道源码 859f25fee3 !550 【修复】修复菜单的路由地址为链接时不带参数的问题
Merge pull request !550 from kuxiao/master
2024-09-17 07:42:20 +00:00
YunaiV fe33517ef0 【优化】全局:rvest.vs-code-prettier-eslint 转成 esbenp.prettier-vscode 2024-09-15 21:10:00 +08:00
YunaiV f7217a5c83 【功能修复】商城:修复满减送批量合并的错误部分 2024-09-15 21:06:21 +08:00
芋道源码 1a8c04d763 !553 批量优化或修改BUG
Merge pull request !553 from 痴货/develop-mall
2024-09-15 12:58:14 +00:00
芋道源码 8dafe26b2b !552 【代码优化】商城: 限时折扣
Merge pull request !552 from puhui999/dev-crm
2024-09-15 12:54:05 +00:00
痴货 f069f809c2 Merge remote-tracking branch 'refs/remotes/update/dev' into develop-mall 2024-09-15 20:40:30 +08:00
YunaiV 8a225e66b7 【功能修复】商城:满减送的条件类型的满足价格不对的问题 2024-09-15 20:22:34 +08:00
痴货 f086b9c32b 修复指定发放优惠券显示 2024-09-15 16:37:35 +08:00
痴货 fc0de84d78 装修页面获取优惠券只获取直接领取的 2024-09-15 16:36:05 +08:00
痴货 c697824b09 修改添加商品商品简介、修改装修主页商品价格显示 2024-09-15 16:33:55 +08:00
痴货 456f3d210a 【功能优化】优化限时满减的金额数据库保存为分 2024-09-15 16:31:23 +08:00
痴货 4d25dda7fd 【优化】满减送列表页面添加活动范围字段、修改状态为COMMON_STATUS 2024-09-15 16:27:12 +08:00
puhui999 370851ac37 【代码优化】商城: 限时折扣 2024-09-15 14:06:35 +08:00
痴货 fbed29100f 【BUG】满减满足多少元需要元转分 2024-09-14 20:30:16 +08:00
痴货 870a1c44e8 【添加】列表添加优惠类型字段 2024-09-14 20:29:38 +08:00
DESKTOP-ELFHO53\zqx d8fb70628d 【修复】修复菜单的路由地址为链接时不带参数的问题 2024-09-13 13:59:15 +08:00
YunaiV 48e78c38c8 【功能优化】框架:UploadImgs 的 zIndex 9999999,避免遮挡问题 2024-09-12 19:51:49 +08:00
YunaiV 0974ece199 【功能优化】公众号:未配置公众号账号的情况下,自动跳转账号管理的菜单 2024-09-12 09:42:47 +08:00
YunaiV 9d0e77b624 【功能修复】商城:指定发卷、新人卷,支持无限发放的兜底 2024-09-11 20:55:38 +08:00
YunaiV 8d116599b1 Merge remote-tracking branch 'origin/dev' into dev 2024-09-11 12:30:45 +08:00
YunaiV e1502d15a2 【缺陷修复】商城:去掉 Number(params.propertyId) 的 Number,解决对雪花算法的兼容性 2024-09-11 12:30:36 +08:00
芋道源码 823530bbc9 !547 【代码优化】商城: 满减活动范围支持分类多选
Merge pull request !547 from puhui999/dev-crm
2024-09-10 13:09:06 +00:00
芋道源码 7577fc45c4 !548 fix: 修复字典选择器数据类型不生效问题
Merge pull request !548 from OlyLis/master
2024-09-10 13:07:38 +00:00
YunaiV cb6e716ce2 【代码优化】注册的逻辑实现 2024-09-10 21:04:46 +08:00
YunaiV b1e3337cb4 Merge branch 'dev' of https://gitee.com/scholarli/yudao-ui-admin-vue3 into dev
# Conflicts:
#	src/views/Login/components/RegisterForm.vue
2024-09-10 20:58:40 +08:00
duxz 65e1c7385f fix: 修复字典选择器数据类型不生效问题 2024-09-10 15:32:44 +08:00
puhui999 1a0ff0215d 【代码优化】商城: 满减活动范围支持分类多选 2024-09-10 15:11:57 +08:00
芋道源码 0e2000550a !546 【代码优化】商城: 满减送活动
Merge pull request !546 from puhui999/dev-crm
2024-09-10 04:52:07 +00:00
puhui999 4fcc33ec60 【代码优化】商城: 满减活动增加关闭操作 2024-09-10 12:25:14 +08:00
scholar b2dd170884 实现注册功能 2024-09-10 11:35:49 +08:00
YunaiV 5a3ae2ab7a 【代码优化】商城: 限时折扣活动新增 spu 保存 fix 2024-09-09 09:37:39 +08:00
YunaiV f07c74d83c Merge remote-tracking branch 'origin/dev' into dev 2024-09-09 09:30:56 +08:00
芋道源码 1bd853bb32 !545 【代码优化】商城: 限时折扣活动
Merge pull request !545 from puhui999/dev-crm
2024-09-09 01:30:21 +00:00
YunaiV 6f47069df8 【功能优化】商城:秒杀装修重构 2024-09-09 09:20:42 +08:00
芋道源码 5ccca7ffa7 !543 【功能优化】商城:秒杀装修重构
Merge pull request !543 from 卢越/master
2024-09-09 00:15:30 +00:00
puhui999 cb34733318 Merge remote-tracking branch 'yudao/dev' into dev-crm 2024-09-08 22:43:57 +08:00
puhui999 ed54cfe519 【代码优化】商城: 限时折扣活动新增 spu 保存 fix 2024-09-08 22:40:47 +08:00
puhui999 7bb482a829 【代码优化】商城: 限时折扣活动 sku 自动计算对应的满减/折扣 2024-09-08 22:29:35 +08:00
puhui999 2e207ddef9 【代码优化】商城: spu 详情价格回显错误 fix 2024-09-08 21:19:48 +08:00
YunaiV 90656ddb51 【功能修复】商城:店铺装修时,“悬浮按钮”默认不选中 2024-09-08 19:13:32 +08:00
芋道源码 9c6b5eb600 !544 【修复】修复路由二级路径VITE_BASE_PATH支持
Merge pull request !544 from 半栈幼儿员/hotfix/router
2024-09-08 10:43:41 +00:00
preschooler 25dff95fed 🐞 fix:修复路由二级路径 2024-09-08 14:26:22 +08:00
卢越 d5f1e50495 【功能优化】商城:秒杀装修重构 2024-09-08 11:15:53 +08:00
YunaiV 85c0da5aec Merge remote-tracking branch 'origin/feature/iot' into feature/iot
# Conflicts:
#	src/components/DictTag/src/DictTag.vue
2024-09-07 20:43:47 +08:00
YunaiV daddf79683 Merge branch 'master' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into feature/iot
# Conflicts:
#	src/router/modules/remaining.ts
2024-09-07 20:43:14 +08:00
安浩浩 e6d83c1bd0 修改:IOT 产品管理 发布产品和产品详情 2024-09-07 19:40:53 +08:00
YunaiV 3c34602483 【功能修复】商城:店铺装修时,“悬浮按钮”默认不选中 2024-09-07 19:14:34 +08:00
YunaiV ef56489d46 【功能修复】商城:发送商品显示不全问题和打包后客服聊天表情缺失问题 2024-09-07 19:04:07 +08:00
芋道源码 d12c4dc258 !541 【功能修复】商城:发送商品显示不全问题和打包后客服聊天表情缺失问题
Merge pull request !541 from Lcp/pr
2024-09-07 11:02:19 +00:00
Lcp 9faad6424e 【功能修复】商城:发送商品显示不全问题和打包后客服聊天表情缺失问题 2024-09-07 17:26:15 +08:00
YunaiV fb19dd2476 Merge branch 'master' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into feature/bpm
# Conflicts:
#	src/views/bpm/model/ModelForm.vue
#	src/views/bpm/model/index.vue
2024-09-07 16:56:02 +08:00
YunaiV e5ec820127 Merge remote-tracking branch 'origin/master' 2024-09-07 16:54:20 +08:00
YunaiV 3e55015d77 Merge branch 'dev' of https://gitee.com/yudaocode/yudao-ui-admin-vue3
# Conflicts:
#	src/components/DiyEditor/components/mobile/PromotionCombination/index.vue
#	src/views/ai/mindmap/manager/index.vue
2024-09-07 16:53:40 +08:00
安浩浩 e5de6a5cbd 修改:IOT 产品管理 修改禁用字段 2024-09-07 16:38:36 +08:00
芋道源码 beaf533a14 !540 【功能修复】修复新创建用户未上传头像时,头像不显示的bug。 修复布局配置中标签页图标key错误的bug
Merge pull request !540 from kuxiao/pass20240907
2024-09-07 08:15:14 +00:00
zqx 307743b7d0 【功能修复】修复新创建用户未上传头像时,头像不显示的bug。
【功能修复】修复布局配置中标签页图标key错误的bug
2024-09-07 15:38:39 +08:00
安浩浩 4aea2eaed5 新增:IOT 产品管理 2024-09-07 09:46:52 +08:00
芋道源码 b5c4da073a !539 【功能修复】修复拼团装修的价格显示
Merge pull request !539 from 卢越/master
2024-09-06 14:32:34 +00:00
卢越 3dc02f7282 【功能修复】修复拼团的价格显示 2024-09-06 22:23:15 +08:00
YunaiV 7d60b5a619 【代码优化】商城:拼团装修重构 2024-09-06 20:55:08 +08:00
芋道源码 a2091df2df !537 【功能优化】拼团装修重构
Merge pull request !537 from 卢越/master
2024-09-06 12:39:44 +00:00
scholar 2205526a79 优化代码实现。 2024-09-06 16:14:59 +08:00
YunaiV fd6fd9e520 【代码优化】SYSTEM:快速开关菜单的按钮 2024-09-06 09:20:32 +08:00
芋道源码 15ed1cf5c2 Merge pull request #54 from DevDengChao/feat/menu-status-switch
feat: 新增快速开关菜单的按钮
2024-09-06 09:15:27 +08:00
芋道源码 d7e0b771ef Merge pull request #84 from GoldenZqqq/bpm-optimization
工作流审批页面样式优化
2024-09-06 09:13:13 +08:00
芋道源码 52f804e204 Merge pull request #81 from ion1ze/master
bugfix: 移动端浅色模式下标题语言颜色异常
2024-09-06 09:12:07 +08:00
芋道源码 7ba29c97aa !535 feat: ✨ add closeOnPressEscape to ElMessageBox
Merge pull request !535 from AhJindeg/hotfix/handleAuthorized
2024-09-05 12:10:23 +00:00
卢越 4f941df55c 【代码优化】代码格式化 2024-09-05 15:15:19 +08:00
卢越 4870231b61 【代码优化】代码格式化 2024-09-05 12:46:58 +08:00
卢越 40fe63a3fb 【代码优化】拼团活动和SPU详情分开查询 2024-09-05 10:55:57 +08:00
卢越 03356576b3 【功能优化】拼团组件默认为单列大图 2024-09-04 23:08:01 +08:00
卢越 10c95406d8 【代码优化】接口地址修正 2024-09-04 22:58:16 +08:00
卢越 757f10cbe6 【功能优化】现在每次点击装修组件都刷新右侧属性窗口 2024-09-04 17:40:10 +08:00
卢越 19789fabf5 【代码优化】拼团卡片组件名 2024-09-04 10:39:57 +08:00
卢越 c486dfd87a 【功能优化】重构拼团装修(仍需优化) 2024-09-03 15:52:38 +08:00
YunaiV c4fda86747 【功能修复】商城:店铺装修的价格展示,没有 / 100 在管理后台的预览时 2024-09-03 15:31:15 +08:00
AhJindeg f750e3b41b feat: ✨ add closeOnPressEscape to ElMessageBox 2024-09-03 09:28:17 +08:00
GoldenZqqq 428f976034 chore: 删除多余添加的元素类 2024-09-03 09:06:19 +08:00
芋道源码 8362665f2b !533 fix: delete href.includes('login?redirect=')
Merge pull request !533 from AhJindeg/hotfix/handleAuthorized
2024-09-02 14:45:09 +00:00
YunaiV 18880c71db Merge branch 'master' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 2024-09-02 22:31:00 +08:00
芋道源码 a2b378b131 !534 【代码优化】商城: 满减送活动
Merge pull request !534 from puhui999/dev-crm
2024-09-02 14:30:47 +00:00
puhui999 dcc1a3e9d5 【代码优化】商城: 满减送活动 2024-09-02 17:21:10 +08:00
puhui999 9329978f6d 【代码优化】】商城: 满减送活动提示弱化一点 2024-09-02 17:05:01 +08:00
puhui999 9aa7706722 【代码优化】】商城: 满减送活动 2024-09-02 16:53:55 +08:00
GoldenZqqq a9623e3001 feat: 审批记录时间线抽取为单独组件;时间线整体样式优化、展示逻辑TODO 2024-09-02 11:34:03 +08:00
AhJindeg 8547b28d63 fix: 🐞 delete href.includes('login?redirect=') 2024-09-02 10:42:41 +08:00
GoldenZqqq 7151695103 fix: 修复进入审核页面后跳转至其他页面报错 2024-09-02 09:54:46 +08:00
GoldenZqqq fb68dc2bfa feat: 优化操作按钮组件命名;操作按钮固定在整个页面下方逻辑完成 2024-09-02 09:35:37 +08:00
YunaiV 0ceba5a441 【功能修复】商城:发送优惠劵的权限标识是 promotion:coupon:send 2024-09-01 09:42:01 +08:00
芋道源码 5502911333 !532 【修复】暗黑主题下沉浸标签预选内容颜色
Merge pull request !532 from 半栈幼儿员/hotfix/tagsview
2024-09-01 01:31:42 +00:00
芋道源码 632dea08c0 !518 【修复】修复el-input、el-select、el-date-picker宽度(先等等、全局影响)
Merge pull request !518 from 半栈幼儿员/hotfix/input-width
2024-08-31 10:38:01 +00:00
preschooler 8c0380506c 🐞 fix:修复el-input、el-select、el-date-picker宽度 2024-08-31 17:39:48 +08:00
YunaiV 395c9c3dca 【优化】添加全局box-sizing: border-box;(先等等、全局影响) 2024-08-31 09:35:29 +08:00
YunaiV 24270f4e9d Merge branch 'hotfix/box-sizing' of https://gitee.com/preschooler/platform-admin-ui 2024-08-31 09:26:44 +08:00
YunaiV 8cb59315da 【代码优化】商城:优惠劵的描述字段新增 2024-08-31 09:13:05 +08:00
YunaiV 15bd591c1b 【代码优化】商城:优惠劵的描述字段新增 2024-08-31 09:07:18 +08:00
YunaiV 7414a044a6 【代码评审】工作流:审批详情的优化 2024-08-30 23:02:57 +08:00
YunaiV 612fa4ab92 Merge branch 'master' of https://github.com/yudaocode/yudao-ui-admin-vue3 2024-08-30 22:36:37 +08:00
芋道源码 324217a8bb Merge pull request #82 from GoldenZqqq/perf/bpm-optimization
feat: 将工作流状态svg放到同一文件夹下;将工作流审批页面操作栏抽取为单独组件
2024-08-30 22:34:31 +08:00
preschooler b67ed77030 🐞 fix:暗黑主题下沉浸标签预选内容颜色 2024-08-27 11:23:05 +08:00
YunaiV a13e8b77c9 Merge branch 'master' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into dev 2024-08-27 09:00:11 +08:00
芋道源码 0112e7203e !530 【功能优化】商城优惠券新增描述字段
Merge pull request !530 from 卢越/master
2024-08-27 00:59:41 +00:00
卢越 beb7f60bc5 【功能优化】商城优惠券新增描述字段 2024-08-27 06:18:07 +08:00
YunaiV c4f7aa760b 【代码评审】商城:限时折扣活动 2024-08-26 22:09:56 +08:00
YunaiV af1bec2a8c Merge remote-tracking branch 'origin/dev' into dev 2024-08-26 21:58:24 +08:00
YunaiV c3b210d2b5 Merge branch 'master' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into dev 2024-08-26 21:58:12 +08:00
芋道源码 2434911064 !529 【修改】修改限时折扣的一些问题
Merge pull request !529 from 痴货/dev
2024-08-26 13:57:35 +00:00
芋道源码 35c1551fad !528 【代码优化】商城: 完善代码评审提到的问题
Merge pull request !528 from puhui999/dev-crm
2024-08-26 10:49:01 +00:00
YunaiV 898593696c 【功能新增】工作流:BPM Model 增加 type 标记是 BPMN 设计器,还是 SIMPLE 钉钉设计器 2024-08-26 18:38:24 +08:00
痴货 a06a50967e 【修改】修改限时折扣:①【优化】商品允许选择多个;②【优化】选择后的 SKU,需要后面加个【删除】按钮;③【bug】展示的金额,貌似不对,大了 100 倍;④【优化】“优惠类型”,是每个 SKU 可以自定义已设置的 2024-08-26 17:45:33 +08:00
puhui999 e1ccf52389 【代码优化】】商城: 完善代码评审提到的问题 2024-08-26 16:30:35 +08:00
YunaiV ce0f6edba6 【功能简化】工作流:移除“导入”流程的功能,原生 bpmn 设计器,已经提供导入 2024-08-26 13:07:33 +08:00
GoldenZqqq 927196c5d1 feat: 将工作流状态svg放到同一文件夹下;将工作流审批页面操作栏抽取为单独组件 2024-08-26 10:58:50 +08:00
芋道源码 1911ed9934 !527 【功能优化】商城客服聊天相关优化
Merge pull request !527 from 卢越/master
2024-08-25 11:20:17 +00:00
卢越 075cf0c160 【功能优化】商城客服适配黑暗模式,点击卡片进入订单或者商品详情页 2024-08-25 19:08:33 +08:00
卢越 dd4dbb0a5c 【功能修复】商城客服聊天列表无法删除的问题 2024-08-25 19:07:10 +08:00
preschooler ca3e47f673 🐞 fix:删除scrollbar__view高度限制 2024-08-25 16:49:26 +08:00
YunaiV b99700455c 【功能修复】report 缺少 ContentWrap 报错 2024-08-25 10:41:40 +08:00
YunaiV 216d8c00b2 【功能修复】report 缺少 ContentWrap 报错 2024-08-25 10:36:55 +08:00
芋道源码 c832af77e8 !526 【页面优化】商城装修小组件适配黑暗模式
Merge pull request !526 from 卢越/master
2024-08-24 10:39:07 +00:00
YunaiV 8f748edd96 【代码优化】工作流:调整多人审批的文案 2024-08-24 18:08:10 +08:00
卢越 295976407b 【页面优化】商城装修右侧操作区适配黑暗模式 2024-08-24 17:32:54 +08:00
卢越 69dc5c1a23 【页面优化】商城装修小组件适配黑暗模式 2024-08-24 17:09:26 +08:00
爱摸鱼的阿恒 de5cda9ef4 bugfix: 移动端浅色模式下标题语言颜色异常 2024-08-24 11:38:19 +08:00
YunaiV c32e03b002 【代码评审】工作流:审批详情的优化 2024-08-23 20:23:23 +08:00
YunaiV f1ffd6e6b8 Merge branch 'master' of https://github.com/yudaocode/yudao-ui-admin-vue3 2024-08-23 20:19:16 +08:00
芋道源码 535921b0d0 Merge pull request #80 from GoldenZqqq/Refactor_Bpm
重构工作流审批页面
2024-08-23 20:18:57 +08:00
芋道源码 fd9d6e5818 !517 【修复】修复标签栏高度(被box-sizing和scrollbar__view的height: 99%干扰)
Merge pull request !517 from 半栈幼儿员/hotfix/tags-view
2024-08-23 11:29:31 +00:00
芋道源码 6cd826d56d !509 【优化】优化权限选择框
Merge pull request !509 from 半栈幼儿员/hotfix/permission-form
2024-08-23 11:27:12 +00:00
芋道源码 c820b5a574 !510 【修复】修复时间格式错误
Merge pull request !510 from 半栈幼儿员/hotfix/date-format
2024-08-23 11:25:53 +00:00
芋道源码 24e1e2f26c !519 【优化】优化垂直菜单可用宽度
Merge pull request !519 from 半栈幼儿员/hotfix/menu
2024-08-23 11:23:33 +00:00
芋道源码 be2ca75ef0 !513 【修复】修复top布局模式下主容器高度计算方式、工具栏多出1px、页脚多出1px
Merge pull request !513 from 半栈幼儿员/hotfix/layout
2024-08-23 11:21:29 +00:00
芋道源码 21621fd2d2 !508 【优化】优化iframe布局
Merge pull request !508 from 半栈幼儿员/hotfix/iframe
2024-08-23 11:16:05 +00:00
芋道源码 5bf1de5481 !522 fix: Fixed the clearable invalid issue of IconSelect component for MenuForm page
Merge pull request !522 from AhJindeg/hotfix/IconSelect
2024-08-23 11:13:43 +00:00
芋道源码 7ff082a930 !524 【页面优化】适配订单详情和售后详情的黑暗模式
Merge pull request !524 from 卢越/master
2024-08-23 11:12:25 +00:00
YunaiV 3eeb55749d 【代码评审】商城:满减送活动的实现 2024-08-23 19:08:06 +08:00
芋道源码 f2f39c3a91 !525 【新增】MALL: 满减送活动赠送优惠券(100%)
Merge pull request !525 from puhui999/dev-crm
2024-08-23 10:29:29 +00:00
YunaiV a02712ca95 【代码评审】商城:满减送活动的实现 2024-08-23 18:28:37 +08:00
puhui999 d81ca6e8e3 【代码优化】】MALL: 修复满减送活动规则优惠券回显逻辑 2024-08-23 17:18:11 +08:00
puhui999 e9510f50f8 Merge remote-tracking branch 'yudao/dev' into dev-crm 2024-08-23 17:02:16 +08:00
puhui999 299fec7bdd 【代码优化】上传组件 TODO fix 2024-08-23 17:01:23 +08:00
puhui999 f996d00e92 【新增】MALL: 满减送活动赠送优惠券(100%) 2024-08-23 16:57:36 +08:00
卢越 dd841c2c06 【页面优化】订单售后详情适配黑暗模式 2024-08-23 16:36:43 +08:00
puhui999 a08684089d 【新增】MALL: 满减送活动赠送优惠券(80%) 2024-08-23 16:22:46 +08:00
AhJindeg d407bc3c41 fix: 🐞 Fixed the clearable invalid issue of IconSelect component for MenuForm page
- 修复 MenuForm 页面 IconSelect 组件可清除无效问题
2024-08-23 10:32:45 +08:00
preschooler 8150b9b139 🐞 fix:修复时间格式错误 2024-08-23 10:01:07 +08:00
GoldenZqqq f5c093b3d8 feat: 底部操作栏固定fixed在el-card底部 2024-08-23 09:44:38 +08:00
YunaiV 814f9ba9c6 Merge branch 'master' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into dev
# Conflicts:
#	src/views/mall/promotion/rewardActivity/RewardForm.vue
2024-08-23 09:08:16 +08:00
芋道源码 f11df67441 !521 商城满减送活动 TODO 完善
Merge pull request !521 from puhui999/dev-crm
2024-08-23 01:06:52 +00:00
芋道源码 0f420bf39b !516 【页面优化】优化商城部分页面的黑暗模式适配问题
Merge pull request !516 from 卢越/master
2024-08-23 01:04:42 +00:00
芋道源码 31150eaa76 !512 【优化】规范style位置
Merge pull request !512 from 半栈幼儿员/hotfix/login
2024-08-23 01:01:30 +00:00
芋道源码 800ebf174f !511 【优化】el-radio的label属性修改为value以兼容下个版本
Merge pull request !511 from 半栈幼儿员/hotfix/el-radio
2024-08-23 01:00:10 +00:00
芋道源码 484826a5ad !520 【优化】el-checkbox的label属性修改为value以兼容下个版本
Merge pull request !520 from 半栈幼儿员/hotfix/el-checkbox
2024-08-23 00:57:53 +00:00
jason e75f954495 代码优化, bug 修复 2024-08-22 23:23:06 +08:00
puhui999 10cb91ad59 【新增】MALL: 满减送活动赠送优惠券(50%) 2024-08-22 18:19:54 +08:00
puhui999 25b23881b0 【代码优化】MALL: 满减送活动范围重构 2024-08-22 17:25:10 +08:00
GoldenZqqq e294520cf9 fix: 兼容dark模式文字颜色显示不清问题 2024-08-22 17:21:30 +08:00
GoldenZqqq 287421df7a fix: 工作流审批页面根据当前任务状态动态展示右上角图标;优化是否展示操作按钮栏逻辑、修复报错 2024-08-22 17:01:15 +08:00
puhui999 45fc9957af 【代码优化】MALL: 满减送表单重构 2024-08-22 16:39:52 +08:00
GoldenZqqq 9e695c0e69 fix: 时间显示格式修复;popover内通过按钮颜色与外部保持一致 2024-08-22 15:52:03 +08:00
preschooler 70efcadc72 🎈 perf:el-checkbox的label属性修改为value以兼容下个版本 2024-08-22 15:49:28 +08:00
GoldenZqqq 8b35ee53a8 feat: 删除测试数据、工作流审批页面右侧时间线使用接口返回的真实数据 2024-08-22 15:33:39 +08:00
GoldenZqqq 5968fc5fbf feat: 审批通过/拒绝以popover的形式来填写审批意见等信息并进行二次确认 2024-08-22 15:00:17 +08:00
preschooler 64168a9f39 🎈 perf:优化垂直菜单可用宽度 2024-08-22 14:31:53 +08:00
GoldenZqqq 38e2063f1f feat: 工作流审批页面下方按钮样式风格调整以及整体数据逻辑变更 2024-08-22 13:35:33 +08:00
preschooler ca6b61f553 🎈 perf:优化图标选择弹窗宽度 2024-08-22 13:31:44 +08:00
puhui999 c45a5228e1 【代码优化】MALL: 会员修改余额 2024-08-22 11:27:51 +08:00
preschooler 8c28b8d2d9 🐞 fix:修复top布局模式下主容器高度计算方式、cutMenu布局模式下tagsview位置计算方式、工具栏多出1px、页脚多出1px 2024-08-22 10:32:28 +08:00
GoldenZqqq 4bddfbf95d feat: 审批页面头部添加编号、流程名、发起人等信息展示 2024-08-22 10:02:40 +08:00
卢越 f61f0fa2e8 【页面优化】优化商城相关页面的黑暗模式适配问题,临时优化客服的页面 2024-08-22 09:47:17 +08:00
preschooler 5fad001468 🐞 fix:添加全局box-sizing: border-box;,此模式在布局中最为合理,且总会有组件全局修改box-sizing导致中途影响布局(如:进入流程模型页面导致标签栏高度、主容器宽度问题);可能轻微影响某些布局,发现后修复即可。 2024-08-22 01:49:01 +08:00
preschooler 8283c5d5ec 🌈 style:规范style位置 2024-08-22 01:47:32 +08:00
preschooler 5cdfed5741 🐞 fix:修复标签栏高度(被box-sizing和scrollbar__view的height: 99%干扰) 2024-08-22 01:31:01 +08:00
preschooler de7a9a484c 🎈 perf:el-radio的label属性修改为value以兼容下个版本 2024-08-22 01:18:36 +08:00
preschooler 98a95e6e88 🎈 perf:优化权限选择框 2024-08-22 01:07:27 +08:00
preschooler 6586ba43a8 🎈 perf:优化iframe布局 2024-08-22 00:45:15 +08:00
芋道源码 323e6197b1 !507 回退 'Pull Request !505 : 【修复】el-input、el-select、el-date-picker宽度;el-radio的label属性兼容下个版本;优化iframe布局'
Merge pull request !507 from 芋道源码/revert-merge-505-master
2024-08-21 13:41:46 +00:00
芋道源码 1d07068806 回退 'Pull Request !505 : 【修复】el-input、el-select、el-date-picker宽度;el-radio的label属性兼容下个版本;优化iframe布局' 2024-08-21 13:41:32 +00:00
芋道源码 ce5d8d3780 !505 【修复】el-input、el-select、el-date-picker宽度;el-radio的label属性兼容下个版本;优化iframe布局
Merge pull request !505 from 半栈幼儿员/hotfix
2024-08-21 13:36:17 +00:00
jason eb79ee1b77 仿钉钉流程设计器- 新增发起人节点,去掉开始节点 2024-08-21 21:00:38 +08:00
GoldenZqqq d22a2933b6 feat: 样式优化 2024-08-21 16:57:59 +08:00
preschooler 273ae198a6 🐞 fix:修复邮件模块搜索栏元素宽度 2024-08-21 16:36:29 +08:00
preschooler a4d19cc085 🎈 perf:优化图标选择弹窗宽度 2024-08-21 16:36:19 +08:00
preschooler 1c9d40fc54 🎈 perf:优化权限选择框 2024-08-21 16:36:12 +08:00
preschooler edc50b6f00 🐞 fix:修复时间格式错误 2024-08-21 16:36:07 +08:00
GoldenZqqq b47c8f0ba0 refactor: 工作流审批页面布局与样式重构 2024-08-21 15:03:17 +08:00
preschooler eb9a9257de 🎈 perf:优化iframe布局 2024-08-21 13:46:43 +08:00
preschooler 65f1863327 🐞 fix:修复表单中el-date-picker宽度 2024-08-20 22:40:22 +08:00
preschooler d3dc508171 🐞 fix:修复搜索工具栏中el-input、el-select、el-date-picker宽度 2024-08-20 22:40:17 +08:00
preschooler dea9ef8cd1 🎈 perf:el-radio的label属性修改为value以兼容下个版本 2024-08-20 22:40:13 +08:00
preschooler 30605fba76 🌈 style:微调 2024-08-20 22:40:06 +08:00
YunaiV 5dc11f844d 【代码评审】会员:会员详情的 TODO 2024-08-19 22:15:00 +08:00
芋道源码 eecfb90ab6 !504 完善一些 MALL TODO
Merge pull request !504 from puhui999/dev-crm
2024-08-19 11:37:38 +00:00
puhui999 43ad93cd20 【代码优化】MALL: 会员修改余额 2024-08-19 18:04:46 +08:00
puhui999 ffdd3325dd 【代码优化】分支合并 2024-08-19 16:38:15 +08:00
芋道源码 fc5e37dd21 Merge pull request #78 from GoldenZqqq/Feature_MemberDetail
会员详情-余额tab页面开发
2024-08-19 13:07:35 +08:00
puhui999 231bdd1dd0 【新增】MALL: 会员列表增加修改余额操作 2024-08-19 11:54:29 +08:00
puhui999 b9c2d311ba 【新增】MALL: 会员详情增加售后管理界面 2024-08-19 10:50:10 +08:00
puhui999 ab7064037b 【新增】MALL: 会员详情增加钱包流水界面 2024-08-19 10:23:08 +08:00
puhui999 8bbfaf2aeb Merge remote-tracking branch 'refs/remotes/yudao/dev' into dev-crm 2024-08-19 09:59:32 +08:00
puhui999 b931fbc500 【代码优化】MALL: 活动商品价格展示分转元 2024-08-19 09:59:02 +08:00
GoldenZqqqq 687c96bb0e feat: 会员详情-余额tab页面初始化;将查询会员账户信息接口调用逻辑抽取到父组件,方便余额tab页面使用 2024-08-18 21:42:00 +08:00
芋道源码 e2754b3ede !496 更新element-plus依赖,修复Bug
Merge pull request !496 from kuxiao/master
2024-08-18 09:36:32 +00:00
芋道源码 0791a8aedb !499 【新增】标签页沉浸式效果
Merge pull request !499 from 半栈幼儿员/preschooler
2024-08-18 09:34:36 +00:00
YunaiV 7108cde060 【功能优化】支付:支付应用,增加 appKey 标识,用于不同接入方的标识 2024-08-18 15:51:12 +08:00
YunaiV e01883d58b 【功能优化】商城:支持阿里接口内容加密,同时修复支付宝渠道配置前端校验不生效的问题 #IAKCU4 2024-08-18 15:33:59 +08:00
芋道源码 a9e52b4e98 !498 【功能优化】支持支付宝接口内容加密,及修复支付宝渠道配置前端校验失效问题
Merge pull request !498 from zhaoyu/master
2024-08-18 05:53:48 +00:00
YunaiV 254d1e0322 【功能新增】商城:售后分页查询,支持 userId 过滤 2024-08-18 13:02:21 +08:00
芋道源码 3d56931268 Merge pull request #77 from GoldenZqqq/Feature_MemberDetail
会员详情-收货管理、余额模块页面补充
2024-08-18 11:55:57 +08:00
jason eb7e9397f5 仿钉钉流程设计器- 新增审批类型 2024-08-17 22:49:04 +08:00
jason 996fe3329c Merge branch 'feature/bpm' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into feature/bpm 2024-08-17 20:41:01 +08:00
YunaiV 9d9f7ad7f7 【代码评审】工作流:连续多级部门负责人的实现 2024-08-17 20:37:01 +08:00
jason 92ee6d639d 仿钉钉流程设计器- 连续多级部门负责人支持选多个部门 2024-08-17 18:48:05 +08:00
YunaiV 8b3c294e5c 【功能新增】工作流:支持审批人为空时,根据配置进行自动通过、自动拒绝、指定人审批的配置 2024-08-17 17:03:58 +08:00
jason 6b31a32a2d Merge branch 'feature/bpm' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into feature/bpm 2024-08-17 13:18:49 +08:00
jason ab9ce936a3 仿钉钉流程设计器- 删除第1版,新增连续多级部分负责人策略 2024-08-17 12:19:13 +08:00
YunaiV 0ce9ececc6 【代码重构】将审批超时的 action 统一换成 handlerType,保持一致 2024-08-17 12:09:47 +08:00
YunaiV 2dae231afc 【功能新增】审批节点的审批人与发起人相同时,对应的处理类型的配置 2024-08-17 10:18:50 +08:00
preschooler db75640167 Merge branch 'master' of gitee.com:yudaocode/yudao-ui-admin-vue3 into preschooler 2024-08-16 22:52:30 +08:00
YunaiV 13b630b9ce 【功能修复】修复装修页面的拼团价格和秒杀价格 2024-08-16 20:39:25 +08:00
芋道源码 df90c46a5b !501 【功能修复】商城店铺装修的拼团价格和秒杀价格
Merge pull request !501 from 卢越/master
2024-08-16 12:38:29 +00:00
芋道源码 aadf1373fc !500 update src/views/mall/product/spu/form/ProductAttributes.vue.
Merge pull request !500 from 口口口/N/A
2024-08-16 12:33:16 +00:00
卢越 4130b2fec5 【功能修复】修复装修页面的拼团价格和秒杀价格 2024-08-16 14:42:43 +08:00
卢越 3b445d031f 【功能修复】修复装修页面的拼团价格和秒杀价格 2024-08-16 13:49:08 +08:00
黄兆琦 61fb71c010 chore: 增加TODO注释 2024-08-16 09:15:59 +08:00
口口口 3f57039f4d update src/views/mall/product/spu/form/ProductAttributes.vue.
添加商品属性问题

Signed-off-by: 口口口 <17975121@qq.com>
2024-08-16 00:56:00 +00:00
preschooler bd88d29340 ✨ feat:新怎标签页沉浸式效果,默认未开启 2024-08-15 22:47:17 +08:00
preschool 16c919ce02 🎈 perf:避免表单值被后续函数修改导致页面异常 2024-08-15 22:46:37 +08:00
tb 6afcfdd50c 【功能优化】支持阿里接口内容加密,同时修复支付宝渠道配置前端校验不生效的问题 #IAKCU4 2024-08-15 22:02:31 +08:00
黄兆琦 bcfb98cb37 feat: 初始化会员详情-售后管理页面 2024-08-15 16:28:35 +08:00
芋道源码 d3f38c690e !495 优化stylelint、优化登录页、等等
Merge pull request !495 from 半栈幼儿员/master
2024-08-14 16:05:44 +00:00
YunaiV 2dc738a6e7 【功能优化】添加商品属性时允许选择已有的属性值,点击「选择」后,获取 value 列表 2024-08-14 21:54:21 +08:00
zqx 4925273c0b 更新element-plus依赖,修复Bug 2024-08-14 19:09:59 +08:00
preschool 8c2ecbd7aa 🎈 perf:优化标签栏图标取值 2024-08-14 17:31:26 +08:00
preschool c40ca1333d Merge branch 'master' of gitee.com:yudaocode/yudao-ui-admin-vue3 2024-08-14 15:32:45 +08:00
preschool cc1d2b194d 🎈 perf:优化动态路由在首次加载时未匹配导致警告、路由不存在时保留错误路径 2024-08-14 15:26:52 +08:00
YunaiV 521ae46cba 【功能优化】添加商品属性时允许选择已有的属性值,点击「选择」后,获取 value 列表 2024-08-14 13:06:23 +08:00
芋道源码 879870ee26 Merge pull request #75 from GoldenZqqq/GoldenZqqq/Bug-Fix
添加商品属性项重复选择校验
2024-08-14 12:54:45 +08:00
黄兆琦 9475ded922 fix: 商品属性值添加暂时恢复name同名校验 2024-08-14 09:41:05 +08:00
黄兆琦 0e9fde1f8a fix: 添加商品属性值时增加校验,如果是已存在的属性发出警告且不提交 2024-08-14 08:57:19 +08:00
YunaiV bcb4fc3034 【功能优化】添加商品属性时允许选择已有的属性值 2024-08-14 00:43:34 +08:00
芋道源码 e7c9ca0c5b Merge pull request #71 from GoldenZqqq/GoldenZqqq/issue47
【功能优化】添加商品属性时允许选择已有的属性值
2024-08-13 22:57:32 +08:00
YunaiV aaa5e146f1 【代码优化】DictTag 的 render 方法的命名,属性的命名 2024-08-13 22:56:58 +08:00
芋道源码 f45c255073 Merge pull request #72 from Mister-Kin/master
fix: failed to render for Boolean type of DictTag
2024-08-13 22:55:58 +08:00
芋道源码 3a1224a2ec Merge pull request #73 from GoldenZqqq/GoldenZqqq/Fix-DictTag
修复使用dictTag组件会出现位置偏移问题
2024-08-13 22:53:49 +08:00
preschool 5a21c0e8ca 🎈 perf:el-tag传入空字符串type导致警告 2024-08-13 17:15:38 +08:00
preschool 1a4c717584 🎈 perf:优化登录窗口高度、登录页入场动画、登录页暗黑模式下分割线和二维码背景色 2024-08-13 16:31:45 +08:00
preschool c49641d988 🎈 perf:移除stylelint已弃用的规则、开启保存时自动stylelint格式化、移除tsconfig报错内容 2024-08-13 13:46:09 +08:00
黄兆琦 2937f3c174 feat: 使用id来对商品属性值重复添加操作进行校验 2024-08-13 13:09:22 +08:00
黄兆琦 82bf084920 fix: DictTag组件根元素改为行内flex布局 2024-08-13 11:24:10 +08:00
Mr. Kin 14192991d9 fix: failed to render for Boolean type of DictTag 2024-08-13 10:28:55 +08:00
黄兆琦 24e69ecf36 chore: 添加TODO注释;以更优雅的方式跳出循环 2024-08-13 10:11:09 +08:00
黄兆琦 372e2215f0 【功能优化】添加商品属性时允许选择已有的属性值
Fixes #47
2024-08-12 16:14:52 +08:00
YunaiV fd13eb7f8f 【代码评审】工作流:快搭部分的 code review 2024-08-11 00:48:07 +08:00
YunaiV 5376eb4051 Merge branch 'master' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into feature/bpm 2024-08-10 19:30:50 +08:00
YunaiV a7c37fa295 【功能修复】绘图作品,一直打开的问题 2024-08-10 19:13:02 +08:00
YunaiV 38c485b2bc 【功能新增】AI 大模型:支持思维导图的管理 2024-08-10 19:07:24 +08:00
芋道源码 ab110f915c !493 fix: 思维导图管理预览会报错
Merge pull request !493 from hhhero/master
2024-08-10 10:56:11 +00:00
hhhero 5f970864a1 fix: 思维导图管理预览会报错 2024-08-10 18:41:21 +08:00
YunaiV b2bdcdcf9c 【功能新增】AI 大模型:支持思维导图的管理 2024-08-10 18:10:47 +08:00
YunaiV 7c73b800fa 【功能新增】AI 大模型:支持思维导图的管理 2024-08-10 17:39:38 +08:00
YunaiV 5ba69ba707 Merge branch 'master' of https://github.com/yudaocode/yudao-ui-admin-vue3 2024-08-08 23:47:54 +08:00
YunaiV de9ec95099 【代码优化】DictTag 的 render 方法的命名,属性的命名 2024-08-08 23:47:23 +08:00
YunaiV f7d31d5c99 Merge branch 'master' of https://github.com/GoldenZqqq/yudao-ui-admin-vue3
# Conflicts:
#	src/components/DictTag/src/DictTag.vue
2024-08-08 23:42:02 +08:00
YunaiV 53cef8b475 【代码优化】echarts 独立 chunk 打包 2024-08-08 09:35:20 +08:00
YunaiV fb08357e8f Merge branch 'master' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into dev
# Conflicts:
#	src/views/ai/mindmap/index/components/Right.vue
#	src/views/ai/mindmap/index/index.vue
2024-08-08 08:55:34 +08:00
芋道源码 485e3fa466 !489 [代码优化]AI: 思维导图代码优化
Merge pull request !489 from hhhero/dev
2024-08-08 00:54:40 +00:00
芋道源码 270f41428f !490 【代码优化】嵌套循环,item命名重复
Merge pull request !490 from 最后的梦想/N/A
2024-08-08 00:51:31 +00:00
芋道源码 1c60204350 !491 [修复]菜单: 默认布局为top时,二级菜单鼠标悬停背景色为黑色,如何修改和一级菜单悬停色保持一致
Merge pull request !491 from hhhero/fix/menu-item-hover-bg
2024-08-07 16:13:27 +00:00
GoldenZqqqq b5eb9e73a7 feat: DictTag组件增加props支持自定义每个tag之间的间隔,默认为5px 2024-08-04 12:14:01 +08:00
GoldenZqqqq 6edd64f13b feat: DictTag组件加字符串分隔符props,更灵活定义split字符串的分隔符 2024-08-04 02:05:16 +08:00
hhhero a6e1e5ffe3 fix: 一级以下菜单悬停色跟一级菜单悬停色保持一致 2024-08-04 00:45:46 +08:00
芋道源码 1d047d8cb2 Merge pull request #69 from gitkmw/master
修复运营模块数据不实时更新的问题
2024-08-03 19:11:00 +08:00
allen e1cf0e4456 修复运营模块数据不实时更新的问题 2024-08-03 17:11:29 +08:00
最后的梦想 3b9d115c59 【代码优化】嵌套循环,item命名重复
Signed-off-by: 最后的梦想 <1971063665@qq.com>
2024-08-02 02:45:52 +00:00
hhhero 8d4c9e9c16 [代码优化]AI: 思维导图 2024-07-30 23:47:40 +08:00
YunaiV ed36d2bfb4 【代码优化】AI:music 的路径修改 2024-07-30 09:23:21 +08:00
YunaiV d5d783af25 【代码优化】AI:image 作品中心的代码 2024-07-30 00:00:44 +08:00
YunaiV 1fe40b61a0 【代码评审】AI:code review 思维导图的逻辑 2024-07-29 22:07:31 +08:00
YunaiV 86880b48f7 Merge remote-tracking branch 'origin/dev' into dev 2024-07-29 21:37:05 +08:00
YunaiV e54fdc8d3f 【代码优化】AI:markdown 组件,增加相关的注释 2024-07-29 21:27:39 +08:00
YunaiV 07adf4c68b 【代码优化】AI:markdown 组件,增加相关的注释 2024-07-29 21:24:03 +08:00
jason 7cb68da5b0 仿钉钉流程设计器- 复杂表单字段解析 2024-07-26 21:17:17 +08:00
黄兆琦 c015b2e881 fix: 修复vue/return-in-computed-property报错 2024-07-26 11:12:43 +08:00
黄兆琦 251a98df5c feat: DictTag组件兼容传入多个字典值展示多个标签的情况 2024-07-26 10:32:13 +08:00
hhhero a4a376b18d Merge remote-tracking branch 'yudao-ui-admin-vue3/dev' into dev
# Conflicts:
#	package.json
2024-07-25 20:42:26 +08:00
cherishsince 677767fd83 【删除】marked 2024-07-24 21:10:26 +08:00
cherishsince 8c5aaa8801 【优化】marked 替换成 markdown-it 2024-07-24 21:10:09 +08:00
jason 3a5c93fa84 仿钉钉流程设计器- 代码优化 2024-07-23 22:39:06 +08:00
jason 2c11228f55 仿钉钉流程设计- bug 修复 2024-07-20 14:50:05 +08:00
hhhero 33b0b5ceb3 [新增]AI: 思维导图 2024-07-19 01:53:38 +08:00
jason 538ad86b03 仿钉钉流程设计器- 操作按钮设置 2024-07-13 15:00:25 +08:00
jason fae712b9d4 仿钉钉流程设计- 表单字段权限设置 2024-06-27 09:32:38 +08:00
YunaiV 6ad29c6a39 Merge branch 'master' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into feature/bpm 2024-06-23 11:29:30 +08:00
YunaiV 64a691a3c7 Merge branch 'dev' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into feature/bpm 2024-06-23 11:29:23 +08:00
jason 88356c7b2a 仿钉钉设计器- 节点增加校验逻辑 2024-06-21 09:59:16 +08:00
jason ea1e51a655 仿钉钉设计器- 审批节点配置结构调整 2024-06-19 23:54:46 +08:00
jason 58fc91b810 仿钉钉设计器-字段权限顺序调整 2024-06-19 20:39:16 +08:00
jason 5ee0a7b946 仿钉钉流程设计器- 简化审批拒绝逻辑 2024-06-18 00:06:52 +08:00
jason d5a7ae1865 仿钉钉流程设计器- 简化多人审批方式 2024-06-14 11:10:25 +08:00
jason cab9553235 仿钉钉流程设计器 -会签按通过比例 2024-06-13 08:24:54 +08:00
jason 632bcb9944 仿钉钉流程设计器- 通过只需一人,拒绝需要全员 2024-06-06 10:05:34 +08:00
jason e8d06c314e 仿钉钉流程设计器- 新增并行分支 2024-06-02 18:18:55 +08:00
jason 94cdb2ed29 仿钉钉流程设计器- 后端结构调整 2024-05-31 09:00:25 +08:00
jason b768632b0c 仿钉钉流程设计器- 后端接口调整 2024-05-27 21:20:18 +08:00
jason 0e7dbbb04d 仿钉钉流程设计器- 审批节点配置新增拒绝处理方式 2024-05-26 11:02:04 +08:00
jason 142b0f7203 仿钉钉流程设计器- 审批节点超时处理 2024-05-23 22:43:22 +08:00
jason 03433278be Merge branch 'feature/bpm' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into feature/bpm 2024-05-20 21:34:19 +08:00
jason f42c6d8375 仿钉钉流程设计器- 审批人设置新增发起人自己策略 2024-05-20 21:32:12 +08:00
DevDengChao afe28d83c9 feat: 新增快速开关菜单的按钮 2024-05-20 11:02:34 +08:00
YunaiV 5ac92cf362 Merge branch 'master' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into feature/bpm 2024-05-19 10:54:11 +08:00
jason 4f24141689 仿钉钉流程设计器- 条件规则问题修复 2024-05-13 20:54:14 +08:00
jason e1fc9039b2 仿钉钉流程设计器- bug 修复 2024-05-13 09:32:32 +08:00
jason 6cf69f04ab 仿钉钉流程设计器- 条件节点,新增条件规则配置 2024-05-13 09:13:23 +08:00
jason 672db903f3 仿钉钉流程设计器- 审批节点,抄送节点增加表单字段权限配置 2024-05-08 20:04:25 +08:00
jason 22af2dc2d9 仿钉钉流程设计器- 优化节点配置 2024-05-07 23:44:01 +08:00
jason e8193a0aa0 仿钉钉流程设计器- 完善条件分支节点 2024-04-30 00:10:17 +08:00
jason 9a2dcf2004 仿钉钉流程设计器- 新增条件分支节点 2024-04-29 09:06:03 +08:00
jason 0d8d043250 仿钉钉流程设计器-修复一些问题 2024-04-28 21:53:34 +08:00
jason ee895d0752 仿钉钉流程设计器-抄送节点改造. 2024-04-27 22:17:04 +08:00
jason 676ed6bd44 仿钉钉流程设计器-前端审批人节点改造. 2024-04-27 10:45:53 +08:00
jason 931417928f 仿钉钉流程设计器-前端重构 2024-04-27 00:35:18 +08:00
jason 2589fbe560 code review 修改 2024-04-18 15:56:49 +08:00
jason 12548b0af2 Merge branch 'feature/bpm' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into feature/bpm 2024-04-14 10:29:06 +08:00
jason cfc0003388 仿钉钉设计流程:增加字段权限设置 2024-04-14 10:26:42 +08:00
YunaiV 4b2ff9f614 bpm:code review 钉钉流程设计器 2024-04-08 22:51:24 +08:00
jason 11b7cf32ca 仿钉钉设计流程:增加包容分支 2024-04-07 22:31:56 +08:00
jason 32d2eb11f0 仿钉钉设计流程:增加并行分支 2024-04-07 15:53:21 +08:00
jason 556cac8138 仿钉钉设计流程:获取保存的模型 2024-04-05 22:54:40 +08:00
jason f79c29d168 仿钉钉设计流程:增加抄送人节点 2024-04-05 13:04:12 +08:00
jason 4f839136c7 仿钉钉设计流程:增加发起人、条件分支节点 2024-04-02 20:56:51 +08:00
jason 253d707131 仿钉钉设计流程-30%: 获取仿钉钉流程模型数据结构 2024-03-29 19:56:45 +08:00
jason 627b822f15 仿钉钉设计流程后端接入-10% 2024-03-27 09:27:14 +08:00
jason cf28e42a26 Merge branch 'feature/bpm' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into feature/bpm 2024-03-24 20:40:59 +08:00
jason 4c4d13cdd0 改造审批设置. 整合系统中的任务的候选人策略 2024-03-24 20:35:53 +08:00
349 changed files with 16405 additions and 5517 deletions

No files matched your search

-2
View File
@@ -8,8 +8,6 @@ VITE_BASE_URL='http://api-dashboard.yudao.iocoder.cn'
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
VITE_UPLOAD_TYPE=server
# 上传路径
VITE_UPLOAD_URL='http://api-dashboard.yudao.iocoder.cn/admin-api/infra/file/upload'
# 接口地址
VITE_API_URL=/admin-api
-2
View File
@@ -8,8 +8,6 @@ VITE_BASE_URL='http://localhost:48080'
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持 S3 服务
VITE_UPLOAD_TYPE=server
# 上传路径
VITE_UPLOAD_URL='http://localhost:48080/admin-api/infra/file/upload'
# 接口地址
VITE_API_URL=/admin-api
-2
View File
@@ -8,8 +8,6 @@ VITE_BASE_URL='http://localhost:48080'
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
VITE_UPLOAD_TYPE=server
# 上传路径
VITE_UPLOAD_URL='http://localhost:48080/admin-api/infra/file/upload'
# 接口地址
VITE_API_URL=/admin-api
-2
View File
@@ -8,8 +8,6 @@ VITE_BASE_URL='http://api-dashboard.yudao.iocoder.cn'
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
VITE_UPLOAD_TYPE=server
# 上传路径
VITE_UPLOAD_URL='http://api-dashboard.yudao.iocoder.cn/admin-api/infra/file/upload'
# 接口地址
VITE_API_URL=/admin-api
-2
View File
@@ -8,8 +8,6 @@ VITE_BASE_URL='http://localhost:48080'
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
VITE_UPLOAD_TYPE=server
# 上传路径
VITE_UPLOAD_URL='http://localhost:48080/admin-api/infra/file/upload'
# 接口地址
VITE_API_URL=/admin-api
+3 -2
View File
@@ -83,10 +83,11 @@
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
"source.fixAll.eslint": "explicit",
"source.fixAll.stylelint": "explicit"
},
"[vue]": {
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"i18n-ally.localesPaths": ["src/locales"],
"i18n-ally.keystyle": "nested",
+8 -8
View File
@@ -1,6 +1,6 @@
{
"name": "yudao-ui-admin-vue3",
"version": "2.2.0-snapshot",
"version": "2.3.0-snapshot",
"description": "基于vue3、vite4、element-plus、typesScript",
"author": "xingyu",
"private": false,
@@ -9,11 +9,11 @@
"dev": "vite --mode env.local",
"dev-server": "vite --mode dev",
"ts:check": "vue-tsc --noEmit",
"build:local": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build",
"build:dev": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode dev",
"build:test": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode test",
"build:stage": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode stage",
"build:prod": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode prod",
"build:local": "node ./node_modules/vite/bin/vite.js build",
"build:dev": "node ./node_modules/vite/bin/vite.js build --mode dev",
"build:test": "node ./node_modules/vite/bin/vite.js build --mode test",
"build:stage": "node ./node_modules/vite/bin/vite.js build --mode stage",
"build:prod": "node ./node_modules/vite/bin/vite.js build --mode prod",
"serve:dev": "vite preview --mode dev",
"serve:prod": "vite preview --mode prod",
"preview": "pnpm build:local && vite preview",
@@ -47,7 +47,7 @@
"driver.js": "^1.3.1",
"echarts": "^5.5.0",
"echarts-wordcloud": "^2.1.0",
"element-plus": "2.7.0",
"element-plus": "2.8.4",
"fast-xml-parser": "^4.3.2",
"highlight.js": "^11.9.0",
"jsencrypt": "^3.3.2",
@@ -130,7 +130,7 @@
"vite-plugin-progress": "^0.0.7",
"vite-plugin-purge-icons": "^0.10.0",
"vite-plugin-svg-icons": "^2.0.1",
"vite-plugin-top-level-await": "^1.3.1",
"vite-plugin-top-level-await": "^1.4.4",
"vue-eslint-parser": "^9.3.2",
"vue-tsc": "^1.8.27"
},
+1266 -1266
View File
File diff suppressed because it is too large. Load diff
-4
View File
@@ -54,8 +54,4 @@ body {
.#{$prefix-cls}-grey-mode {
filter: grayscale(100%);
}
.scrollbar__view {
height: 99%!important;
}
</style>
+22
View File
@@ -1,7 +1,20 @@
import { getAccessToken } from '@/utils/auth'
import { fetchEventSource } from '@microsoft/fetch-event-source'
import { config } from '@/config/axios/config'
import request from '@/config/axios' // AI 思维导图 VO
// AI 思维导图 VO
export interface MindMapVO {
id: number // 编号
userId: number // 用户编号
prompt: string // 生成内容提示
generatedContent: string // 生成的思维导图内容
platform: string // 平台
model: string // 模型
errorMessage: string // 错误信息
}
// AI 思维导图生成 VO
export interface AiMindMapGenerateReqVO {
prompt: string
}
@@ -34,5 +47,14 @@ export const AiMindMapApi = {
onclose: onClose,
signal: ctrl.signal
})
},
// 查询思维导图分页
getMindMapPage: async (params: any) => {
return await request.get({ url: `/ai/mind-map/page`, params })
},
// 删除思维导图
deleteMindMap: async (id: number) => {
return await request.delete({ url: `/ai/mind-map/delete?id=` + id })
}
}
+5 -1
View File
@@ -30,7 +30,7 @@ export const getModelPage = async (params) => {
return await request.get({ url: '/bpm/model/page', params })
}
export const getModel = async (id: number) => {
export const getModel = async (id: string) => {
return await request.get({ url: '/bpm/model/get?id=' + id })
}
@@ -38,6 +38,10 @@ export const updateModel = async (data: ModelVO) => {
return await request.put({ url: '/bpm/model/update', data: data })
}
export const updateModelBpmn = async (data: ModelVO) => {
return await request.put({ url: '/bpm/model/update-bpmn', data: data })
}
// 任务状态修改
export const updateModelState = async (id: number, state: number) => {
const data = {
+41 -1
View File
@@ -1,6 +1,6 @@
import request from '@/config/axios'
import { ProcessDefinitionVO } from '@/api/bpm/model'
import { NodeType } from '@/components/SimpleProcessDesignerV2/src/consts'
export type Task = {
id: string
name: string
@@ -22,6 +22,35 @@ export type ProcessInstanceVO = {
processDefinition?: ProcessDefinitionVO
}
// 用户信息
export type User = {
id: number,
nickname: string,
avatar: string
}
// 审批任务信息
export type ApprovalTaskInfo = {
id: number,
ownerUser: User,
assigneeUser: User,
status: number,
reason: string
}
// 审批节点信息
export type ApprovalNodeInfo = {
id : number
name: string
nodeType: NodeType
status: number
startTime?: Date
endTime?: Date
candidateUserList?: User[]
tasks: ApprovalTaskInfo[]
}
export const getProcessInstanceMyPage = async (params: any) => {
return await request.get({ url: '/bpm/process-instance/my-page', params })
}
@@ -57,3 +86,14 @@ export const getProcessInstance = async (id: string) => {
export const getProcessInstanceCopyPage = async (params: any) => {
return await request.get({ url: '/bpm/process-instance/copy/page', params })
}
// 获取审批详情
export const getApprovalDetail = async (processInstanceId?:string, processDefinitionId?:string) => {
const param = processInstanceId ? '?processInstanceId='+ processInstanceId : '?processDefinitionId='+ processDefinitionId
return await request.get({ url: 'bpm/process-instance/get-approval-detail'+ param })
}
// 获取表单字段权限
export const getFormFieldsPermission = async (params: any) => {
return await request.get({ url: '/bpm/process-instance/get-form-fields-permission', params })
}
+15
View File
@@ -0,0 +1,15 @@
import request from '@/config/axios'
export const updateBpmSimpleModel = async (data) => {
return await request.post({
url: '/bpm/model/simple/update',
data: data
})
}
export const getBpmSimpleModel = async (id) => {
return await request.get({
url: '/bpm/model/simple/get?id=' + id
})
}
+46
View File
@@ -1,5 +1,51 @@
import request from '@/config/axios'
/**
* 任务状态枚举
*/
export enum TaskStatusEnum {
/**
* 未开始
*/
NOT_START = -1,
/**
* 待审批
*/
WAIT = 0,
/**
* 审批中
*/
RUNNING = 1,
/**
* 审批通过
*/
APPROVE = 2,
/**
* 审批不通过
*/
REJECT = 3,
/**
* 已取消
*/
CANCEL = 4,
/**
* 已退回
*/
RETURN = 5,
/**
* 委派中
*/
DELEGATE = 6,
/**
* 审批通过中
*/
APPROVING = 7,
}
export type TaskVO = {
id: number
}
+74
View File
@@ -0,0 +1,74 @@
import request from '@/config/axios'
// IoT 设备 VO
export interface DeviceVO {
id: number // 设备 ID,主键,自增
deviceKey: string // 设备唯一标识符
deviceName: string // 设备名称
productId: number // 产品编号
productKey: string // 产品标识
deviceType: number // 设备类型
nickname: string // 设备备注名称
gatewayId: number // 网关设备 ID
status: number // 设备状态
statusLastUpdateTime: Date // 设备状态最后更新时间
lastOnlineTime: Date // 最后上线时间
lastOfflineTime: Date // 最后离线时间
activeTime: Date // 设备激活时间
createTime: Date // 创建时间
ip: string // 设备的 IP 地址
firmwareVersion: string // 设备的固件版本
deviceSecret: string // 设备密钥,用于设备认证,需安全存储
mqttClientId: string // MQTT 客户端 ID
mqttUsername: string // MQTT 用户名
mqttPassword: string // MQTT 密码
authType: string // 认证类型
latitude: number // 设备位置的纬度
longitude: number // 设备位置的经度
areaId: number // 地区编码
address: string // 设备详细地址
serialNumber: string // 设备序列号
}
export interface DeviceUpdateStatusVO {
id: number // 设备 ID,主键,自增
status: number // 设备状态
}
// 设备 API
export const DeviceApi = {
// 查询设备分页
getDevicePage: async (params: any) => {
return await request.get({ url: `/iot/device/page`, params })
},
// 查询设备详情
getDevice: async (id: number) => {
return await request.get({ url: `/iot/device/get?id=` + id })
},
// 新增设备
createDevice: async (data: DeviceVO) => {
return await request.post({ url: `/iot/device/create`, data })
},
// 修改设备
updateDevice: async (data: DeviceVO) => {
return await request.put({ url: `/iot/device/update`, data })
},
// 修改设备状态
updateDeviceStatus: async (data: DeviceUpdateStatusVO) => {
return await request.put({ url: `/iot/device/update-status`, data })
},
// 删除设备
deleteDevice: async (id: number) => {
return await request.delete({ url: `/iot/device/delete?id=` + id })
},
// 获取设备数量
getDeviceCount: async (productId: number) => {
return await request.get({ url: `/iot/device/count?productId=` + productId })
}
}
+62
View File
@@ -0,0 +1,62 @@
import request from '@/config/axios'
// IoT 产品 VO
export interface ProductVO {
id: number // 产品编号
name: string // 产品名称
productKey: string // 产品标识
protocolId: number // 协议编号
categoryId: number // 产品所属品类标识符
description: string // 产品描述
validateType: number // 数据校验级别
status: number // 产品状态
deviceType: number // 设备类型
netType: number // 联网方式
protocolType: number // 接入网关协议
dataFormat: number // 数据格式
deviceCount: number // 设备数量
createTime: Date // 创建时间
}
// IoT 产品 API
export const ProductApi = {
// 查询产品分页
getProductPage: async (params: any) => {
return await request.get({ url: `/iot/product/page`, params })
},
// 查询产品详情
getProduct: async (id: number) => {
return await request.get({ url: `/iot/product/get?id=` + id })
},
// 新增产品
createProduct: async (data: ProductVO) => {
return await request.post({ url: `/iot/product/create`, data })
},
// 修改产品
updateProduct: async (data: ProductVO) => {
return await request.put({ url: `/iot/product/update`, data })
},
// 删除产品
deleteProduct: async (id: number) => {
return await request.delete({ url: `/iot/product/delete?id=` + id })
},
// 导出产品 Excel
exportProduct: async (params) => {
return await request.download({ url: `/iot/product/export-excel`, params })
},
// 更新产品状态
updateProductStatus: async (id: number, status: number) => {
return await request.put({ url: `/iot/product/update-status?id=` + id + `&status=` + status })
},
// 查询产品(精简)列表
getSimpleProductList() {
return request.get({ url: '/iot/product/list-all-simple' })
}
}
+55
View File
@@ -0,0 +1,55 @@
import request from '@/config/axios'
// IoT 产品物模型 VO
export interface ThinkModelFunctionVO {
id: number // 物模型功能编号
identifier: string // 功能标识
name: string // 功能名称
description: string // 功能描述
productId: number // 产品编号
productKey: string // 产品标识
type: number // 功能类型
property: string // 属性
event: string // 事件
service: string // 服务
}
// IoT 产品物模型 API
export const ThinkModelFunctionApi = {
// 查询产品物模型分页
getThinkModelFunctionPage: async (params: any) => {
return await request.get({ url: `/iot/think-model-function/page`, params })
},
// 获得产品物模型
getThinkModelFunctionListByProductId: async (params: any) => {
return await request.get({
url: `/iot/think-model-function/list-by-product-id`,
params
})
},
// 查询产品物模型详情
getThinkModelFunction: async (id: number) => {
return await request.get({ url: `/iot/think-model-function/get?id=` + id })
},
// 新增产品物模型
createThinkModelFunction: async (data: ThinkModelFunctionVO) => {
return await request.post({ url: `/iot/think-model-function/create`, data })
},
// 修改产品物模型
updateThinkModelFunction: async (data: ThinkModelFunctionVO) => {
return await request.put({ url: `/iot/think-model-function/update`, data })
},
// 删除产品物模型
deleteThinkModelFunction: async (id: number) => {
return await request.delete({ url: `/iot/think-model-function/delete?id=` + id })
},
// 导出产品物模型 Excel
exportThinkModelFunction: async (params) => {
return await request.download({ url: `/iot/think-model-function/export-excel`, params })
}
}
+6 -1
View File
@@ -1,6 +1,6 @@
import request from '@/config/axios'
import { getRefreshToken } from '@/utils/auth'
import type { UserLoginVO } from './types'
import type { RegisterVO, UserLoginVO } from './types'
export interface SmsCodeVO {
mobile: string
@@ -17,6 +17,11 @@ export const login = (data: UserLoginVO) => {
return request.post({ url: '/system/auth/login', data })
}
// 注册
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() })
+7
View File
@@ -29,3 +29,10 @@ export type UserVO = {
loginIp: string
loginDate: string
}
export type RegisterVO = {
tenantName: string
username: string
password: string
captchaVerification: string
}
+10 -14
View File
@@ -24,20 +24,6 @@ export interface PropertyValueVO {
remark?: string
}
/**
* 商品属性值的明细
*/
export interface PropertyValueDetailVO {
/** 属性项的编号 */
propertyId: number // 属性的编号
/** 属性的名称 */
propertyName: string
/** 属性值的编号 */
valueId: number
/** 属性值的名称 */
valueName: string
}
// ------------------------ 属性项 -------------------
// 创建属性项
@@ -65,6 +51,11 @@ export const getPropertyPage = (params: PageParam) => {
return request.get({ url: '/product/property/page', params })
}
// 获得属性项精简列表
export const getPropertySimpleList = (): Promise<PropertyVO[]> => {
return request.get({ url: '/product/property/simple-list' })
}
// ------------------------ 属性值 -------------------
// 获得属性值分页
@@ -91,3 +82,8 @@ export const updatePropertyValue = (data: PropertyValueVO) => {
export const deletePropertyValue = (id: number) => {
return request.delete({ url: `/product/property/value/delete?id=${id}` })
}
// 获得属性值精简列表
export const getPropertyValueSimpleList = (propertyId: number): Promise<PropertyValueVO[]> => {
return request.get({ url: '/product/property/value/simple-list', params: { propertyId } })
}
+2
View File
@@ -50,6 +50,8 @@ export interface Spu {
giveIntegral?: number // 赠送积分
virtualSalesCount?: number // 虚拟销量
price?: number // 商品价格
combinationPrice?: number // 商品拼团价格
seckillPrice?: number // 商品秒杀价格
salesCount?: number // 商品销量
marketPrice?: number // 市场价
costPrice?: number // 成本价
@@ -16,6 +16,7 @@ export interface CombinationActivityVO {
virtualGroup?: number
status?: number
limitDuration?: number
combinationPrice?: number
products: CombinationProductVO[]
}
@@ -36,7 +37,7 @@ export interface SpuExtension extends Spu {
}
// 查询拼团活动列表
export const getCombinationActivityPage = async (params) => {
export const getCombinationActivityPage = async (params: any) => {
return await request.get({ url: '/promotion/combination-activity/page', params })
}
@@ -45,6 +46,11 @@ export const getCombinationActivity = async (id: number) => {
return await request.get({ url: '/promotion/combination-activity/get?id=' + id })
}
// 获得拼团活动列表,基于活动编号数组
export const getCombinationActivityListByIds = (ids: number[]) => {
return request.get({ url: `/promotion/combination-activity/list-by-ids?ids=${ids}` })
}
// 新增拼团活动
export const createCombinationActivity = async (data: CombinationActivityVO) => {
return await request.post({ url: '/promotion/combination-activity/create', data })
@@ -74,7 +74,7 @@ export function getCouponTemplatePage(params: PageParam) {
}
// 获得优惠劵模板分页
export function getCouponTemplateList(ids: number[]) {
export function getCouponTemplateList(ids: number[]): Promise<CouponTemplateVO[]> {
return request.get({
url: `/promotion/coupon-template/list?ids=${ids}`
})
@@ -30,6 +30,6 @@ export const KeFuConversationApi = {
},
// 删除客服会话
deleteConversation: async (id: number) => {
return await request.get({ url: '/promotion/kefu-conversation/delete?id' + id })
return await request.delete({ url: `/promotion/kefu-conversation/delete?id=${id}`})
}
}
+91
View File
@@ -0,0 +1,91 @@
import request from '@/config/axios'
import { Sku, Spu } from '@/api/mall/product/spu' // 积分商城活动 VO
// 积分商城活动 VO
export interface PointActivityVO {
id: number // 积分商城活动编号
spuId: number // 积分商城活动商品
status: number // 活动状态
stock: number // 积分商城活动库存
totalStock: number // 积分商城活动总库存
remark?: string // 备注
sort: number // 排序
createTime: string // 创建时间
products: PointProductVO[] // 积分商城商品
// ========== 商品字段 ==========
spuName: string // 商品名称
picUrl: string // 商品主图
marketPrice: number // 商品市场价,单位:分
//======================= 显示所需兑换积分最少的 sku 信息 =======================
point: number // 兑换积分
price: number // 兑换金额,单位:分
}
// 秒杀活动所需属性
export interface PointProductVO {
id?: number // 积分商城商品编号
activityId?: number // 积分商城活动 id
spuId?: number // 商品 SPU 编号
skuId: number // 商品 SKU 编号
count: number // 可兑换数量
point: number // 兑换积分
price: number // 兑换金额,单位:分
stock: number // 积分商城商品库存
activityStatus?: number // 积分商城商品状态
}
// 扩展 Sku 配置
export type SkuExtension = Sku & {
productConfig: PointProductVO
}
export interface SpuExtension extends Spu {
skus: SkuExtension[] // 重写类型
}
export interface SpuExtension0 extends Spu {
pointStock: number // 积分商城活动库存
pointTotalStock: number // 积分商城活动总库存
point: number // 兑换积分
pointPrice: number // 兑换金额,单位:分
}
// 积分商城活动 API
export const PointActivityApi = {
// 查询积分商城活动分页
getPointActivityPage: async (params: any) => {
return await request.get({ url: `/promotion/point-activity/page`, params })
},
// 查询积分商城活动详情
getPointActivity: async (id: number) => {
return await request.get({ url: `/promotion/point-activity/get?id=` + id })
},
// 查询积分商城活动列表,基于活动编号数组
getPointActivityListByIds: async (ids: number[]) => {
return request.get({ url: `/promotion/point-activity/list-by-ids?ids=${ids}` })
},
// 新增积分商城活动
createPointActivity: async (data: PointActivityVO) => {
return await request.post({ url: `/promotion/point-activity/create`, data })
},
// 修改积分商城活动
updatePointActivity: async (data: PointActivityVO) => {
return await request.put({ url: `/promotion/point-activity/update`, data })
},
// 删除积分商城活动
deletePointActivity: async (id: number) => {
return await request.delete({ url: `/promotion/point-activity/delete?id=` + id })
},
// 关闭秒杀活动
closePointActivity: async (id: number) => {
return await request.put({ url: '/promotion/point-activity/close?id=' + id })
}
}
+21 -11
View File
@@ -1,34 +1,39 @@
import request from '@/config/axios'
export interface DiscountActivityVO {
export interface RewardActivityVO {
id?: number
name?: string
startTime?: Date
endTime?: Date
startAndEndTime?: Date[] // 只前端使用
remark?: string
conditionType?: number
productScope?: number
rules: RewardRule[]
// 如下仅用于表单,不提交
productScopeValues?: number[] // 商品范围:值为品类编号列表、商品编号列表
productCategoryIds?: number[]
productSpuIds?: number[]
rules?: DiscountProductVO[]
}
// 优惠规则
export interface DiscountProductVO {
limit: number
discountPrice: number
freeDelivery: boolean
export interface RewardRule {
limit?: number
discountPrice?: number
freeDelivery?: boolean
point: number
couponIds: number[]
couponCounts: number[]
giveCouponTemplateCounts?: {
[key: number]: number
}
}
// 新增满减送活动
export const createRewardActivity = async (data: DiscountActivityVO) => {
export const createRewardActivity = async (data: RewardActivityVO) => {
return await request.post({ url: '/promotion/reward-activity/create', data })
}
// 更新满减送活动
export const updateRewardActivity = async (data: DiscountActivityVO) => {
export const updateRewardActivity = async (data: RewardActivityVO) => {
return await request.put({ url: '/promotion/reward-activity/update', data })
}
@@ -42,7 +47,12 @@ export const getReward = async (id: number) => {
return await request.get({ url: '/promotion/reward-activity/get?id=' + id })
}
// 删除限时折扣活动
// 删除满减送活动
export const deleteRewardActivity = async (id: number) => {
return await request.delete({ url: '/promotion/reward-activity/delete?id=' + id })
}
// 关闭满减送活动
export const closeRewardActivity = async (id: number) => {
return await request.put({ url: '/promotion/reward-activity/close?id=' + id })
}
@@ -18,12 +18,14 @@ export interface SeckillActivityVO {
singleLimitCount?: number
stock?: number
totalStock?: number
seckillPrice?: number
products?: SeckillProductVO[]
}
// 秒杀活动所需属性
export interface SeckillProductVO {
skuId: number
spuId: number
seckillPrice: number
stock: number
}
@@ -42,6 +44,11 @@ export const getSeckillActivityPage = async (params) => {
return await request.get({ url: '/promotion/seckill-activity/page', params })
}
// 查询秒杀活动列表,基于活动编号数组
export const getSeckillActivityListByIds = (ids: number[]) => {
return request.get({ url: `/promotion/seckill-activity/list-by-ids?ids=${ids}` })
}
// 查询秒杀活动详情
export const getSeckillActivity = async (id: number) => {
return await request.get({ url: '/promotion/seckill-activity/get?id=' + id })
-5
View File
@@ -46,8 +46,3 @@ export const updateUserLevel = async (data: any) => {
export const updateUserPoint = async (data: any) => {
return await request.put({ url: `/member/user/update-point`, data })
}
// 修改会员用户余额
export const updateUserBalance = async (data: any) => {
return await request.put({ url: `/member/user/update-balance`, data })
}
+1
View File
@@ -2,6 +2,7 @@ import request from '@/config/axios'
export interface AppVO {
id: number
appKey: string
name: string
status: number
remark: string
+8 -2
View File
@@ -84,8 +84,14 @@ export const getOrderPage = async (params: OrderPageReqVO) => {
}
// 查询详情支付订单
export const getOrder = async (id: number) => {
return await request.get({ url: '/pay/order/get?id=' + id })
export const getOrder = async (id: number, sync?: boolean) => {
return await request.get({
url: '/pay/order/get',
params: {
id,
sync
}
})
}
// 获得支付订单的明细
+8 -2
View File
@@ -4,6 +4,7 @@ import request from '@/config/axios'
export interface PayWalletUserReqVO {
userId: number
}
/** 钱包 VO */
export interface WalletVO {
id: number
@@ -20,7 +21,12 @@ export const getWallet = async (params: PayWalletUserReqVO) => {
return await request.get<WalletVO>({ url: `/pay/wallet/get`, params })
}
// 查询会员钱包列表
export const getWalletPage = async (params) => {
/** 查询会员钱包列表 */
export const getWalletPage = async (params: any) => {
return await request.get({ url: `/pay/wallet/page`, params })
}
/** 修改会员钱包余额 */
export const updateWalletBalance = async (data: any) => {
return await request.put({ url: `/pay/wallet/update-balance`, 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="1724304256588" class="icon" viewBox="0 0 1300 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1272" xmlns:xlink="http://www.w3.org/1999/xlink" width="253.90625" height="200"><path d="M784.058182 99.258182l10.938182 18.385454-21.294546-2.56-14.196363 16.058182-4.072728-21.061818-19.781818-8.494545 18.734546-10.472728 2.094545-21.294545 15.709091 14.545454 20.945454-4.654545-9.076363 19.549091zM1067.287273 642.443636l-18.501818 10.821819 2.56-21.294546-16.058182-14.196364 21.061818-4.072727 8.494545-19.665454 10.472728 18.734545 21.294545 1.978182-14.661818 15.709091 4.770909 20.945454-19.432727-8.96z" fill="#2196F3" p-id="1273"></path><path d="M1067.287273 642.443636l-18.501818 10.821819 2.56-21.294546-16.058182-14.196364 21.061818-4.072727 8.494545-19.665454 10.472728 18.734545 21.294545 1.978182-14.661818 15.709091 4.770909 20.945454-19.432727-8.96zM571.927273 100.072727l-17.454546-12.567272 20.596364-6.167273 6.516364-20.48 12.218181 17.570909 21.410909-0.116364-12.916363 17.105455 6.749091 20.363636-20.247273-6.981818-17.338182 12.683636 0.465455-21.410909zM991.418182 784.407273l-21.178182 3.490909 10.123636-18.967273-9.774545-18.967273 21.061818 3.723637 15.127273-15.243637 2.909091 21.294546 19.2 9.658182-19.316364 9.309091-3.258182 21.178181-14.894545-15.476363zM427.985455 156.741818L407.272727 151.505455l16.872728-13.265455-1.396364-21.410909 17.803636 11.985454 20.014546-7.912727-5.934546 20.596364 13.730909 16.523636-21.410909 0.814546-11.52 18.152727-7.447272-20.247273zM854.225455 896.465455l-20.712728-5.352728 16.872728-13.265454-1.396364-21.294546 17.803636 11.869091 20.014546-7.912727-5.934546 20.712727 13.730909 16.523637-21.527272 0.814545-11.403637 18.036364-7.447272-20.130909zM562.501818 923.694545l10.821818 18.385455-21.294545-2.56-14.196364 16.058182-4.072727-21.061818-19.665455-8.494546 18.734546-10.356363 1.978182-21.41091 15.709091 14.661819 20.945454-4.770909-8.96 19.54909zM242.734545 420.770909l-18.385454 10.938182 2.56-21.294546-16.058182-14.196363 21.061818-4.189091 8.494546-19.665455 10.356363 18.734546 21.410909 2.094545-14.545454 15.709091 4.654545 20.945455-19.549091-9.076364z" fill="#2196F3" p-id="1274"></path><path d="M242.734545 420.770909l-18.385454 10.938182 2.56-21.294546-16.058182-14.196363 21.061818-4.189091 8.494546-19.665455 10.356363 18.734546 21.410909 2.094545-14.545454 15.709091 4.654545 20.945455-19.549091-9.076364zM700.858182 943.941818l-17.454546-12.450909 20.48-6.283636 6.516364-20.48 12.334545 17.687272 21.41091-0.116363-12.916364 17.105454 6.632727 20.363637-20.247273-7.098182-17.221818 12.683636 0.465455-21.410909zM303.592727 278.807273l-21.178182 3.490909 10.123637-18.967273-9.890909-18.967273 21.178182 3.723637 15.010909-15.243637 2.909091 21.294546 19.2 9.541818-19.316364 9.425455-3.258182 21.178181-14.778182-15.476363z" fill="#2196F3" p-id="1275"></path><path d="M407.272727 90.647273a486.632727 486.632727 0 0 1 504.552728 11.636363l25.018181-14.429091A512 512 0 0 0 139.636364 546.909091l25.018181-14.429091A486.981818 486.981818 0 0 1 407.272727 90.647273zM893.323636 933.352727a486.749091 486.749091 0 0 1-504.669091-11.636363l-24.901818 14.429091A512 512 0 0 0 1161.192727 477.090909l-24.901818 13.963636a486.981818 486.981818 0 0 1-242.967273 442.298182z" fill="#2196F3" p-id="1276"></path><path d="M814.545455 795.927273a327.447273 327.447273 0 0 1-258.21091 29.556363l-29.78909 17.105455A353.163636 353.163636 0 0 0 998.865455 570.181818l-29.789091 17.105455A326.865455 326.865455 0 0 1 814.545455 795.927273zM486.865455 228.072727A327.447273 327.447273 0 0 1 744.727273 198.516364l29.789091-17.105455A353.163636 353.163636 0 0 0 302.545455 453.818182l29.78909-17.105455A326.865455 326.865455 0 0 1 486.865455 228.072727zM1288.378182 374.690909a53.294545 53.294545 0 0 1-14.429091 11.636364L229.469091 989.090909a53.876364 53.876364 0 0 1-73.425455-19.665454L7.214545 710.632727a53.527273 53.527273 0 0 1 19.781819-73.309091L1071.476364 34.909091a53.876364 53.876364 0 0 1 73.425454 19.665454l148.829091 258.327273a53.061818 53.061818 0 0 1 5.352727 40.727273 55.272727 55.272727 0 0 1-10.705454 21.061818zM32.232727 665.716364A28.043636 28.043636 0 0 0 29.323636 698.181818l148.829091 257.978182a28.392727 28.392727 0 0 0 38.516364 10.356364l1044.48-601.949091a28.16 28.16 0 0 0 10.356364-38.516364L1122.676364 67.84a28.276364 28.276364 0 0 0-38.4-10.356364L39.68 659.432727a27.810909 27.810909 0 0 0-7.447273 6.283637z" fill="#2196F3" p-id="1277"></path><path d="M477.090909 500.945455l22.109091 38.283636-15.36 8.843636-13.963636-24.436363-151.272728 87.621818 14.545455 25.134545-15.243636 8.843637-23.272728-39.330909L377.949091 558.545455c-6.050909-4.887273-11.636364-8.843636-15.825455-11.636364l14.894546-12.450909c3.956364 3.141818 9.658182 8.145455 17.105454 14.894545zM459.869091 698.181818l-48.407273 28.043637 7.44727Line truncated

After

Width:  |  Height:  |  Size: 7.4 KiB

+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="1724316565416" class="icon" viewBox="0 0 1300 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1339" xmlns:xlink="http://www.w3.org/1999/xlink" width="253.90625" height="200"><path d="M784.058182 99.258182l10.938182 18.385454-21.294546-2.56-14.196363 16.058182-4.072728-21.061818-19.781818-8.494545 18.734546-10.472728 2.094545-21.294545 15.709091 14.545454 20.945454-4.654545-9.076363 19.549091zM1067.287273 642.443636l-18.501818 10.821819 2.56-21.294546-16.058182-14.196364 21.061818-4.072727 8.494545-19.665454 10.472728 18.734545 21.294545 1.978182-14.661818 15.709091 4.770909 20.945454-19.432727-8.96z" fill="#13C463" p-id="1340"></path><path d="M1067.287273 642.443636l-18.501818 10.821819 2.56-21.294546-16.058182-14.196364 21.061818-4.072727 8.494545-19.665454 10.472728 18.734545 21.294545 1.978182-14.661818 15.709091 4.770909 20.945454-19.432727-8.96zM571.927273 100.072727l-17.454546-12.567272 20.596364-6.167273 6.516364-20.48 12.218181 17.570909 21.410909-0.116364-12.916363 17.105455 6.749091 20.363636-20.247273-6.981818-17.338182 12.683636 0.465455-21.410909zM991.418182 784.407273l-21.178182 3.490909 10.123636-18.967273-9.774545-18.967273 21.061818 3.723637 15.127273-15.243637 2.909091 21.294546 19.2 9.658182-19.316364 9.309091-3.258182 21.178181-14.894545-15.476363zM427.985455 156.741818L407.272727 151.505455l16.872728-13.265455-1.396364-21.410909 17.803636 11.985454 20.014546-7.912727-5.934546 20.596364 13.730909 16.523636-21.410909 0.814546-11.52 18.152727-7.447272-20.247273zM854.225455 896.465455l-20.712728-5.352728 16.872728-13.265454-1.396364-21.294546 17.803636 11.869091 20.014546-7.912727-5.934546 20.712727 13.730909 16.523637-21.527272 0.814545-11.403637 18.036364-7.447272-20.130909zM562.501818 923.694545l10.821818 18.385455-21.294545-2.56-14.196364 16.058182-4.072727-21.061818-19.665455-8.494546 18.734546-10.356363 1.978182-21.41091 15.709091 14.661819 20.945454-4.770909-8.96 19.54909zM242.734545 420.770909l-18.385454 10.938182 2.56-21.294546-16.058182-14.196363 21.061818-4.189091 8.494546-19.665455 10.356363 18.734546 21.410909 2.094545-14.545454 15.709091 4.654545 20.945455-19.549091-9.076364z" fill="#13C463" p-id="1341"></path><path d="M242.734545 420.770909l-18.385454 10.938182 2.56-21.294546-16.058182-14.196363 21.061818-4.189091 8.494546-19.665455 10.356363 18.734546 21.410909 2.094545-14.545454 15.709091 4.654545 20.945455-19.549091-9.076364zM700.858182 943.941818l-17.454546-12.450909 20.48-6.283636 6.516364-20.48 12.334545 17.687272 21.41091-0.116363-12.916364 17.105454 6.632727 20.363637-20.247273-7.098182-17.221818 12.683636 0.465455-21.410909zM303.592727 278.807273l-21.178182 3.490909 10.123637-18.967273-9.890909-18.967273 21.178182 3.723637 15.010909-15.243637 2.909091 21.294546 19.2 9.541818-19.316364 9.425455-3.258182 21.178181-14.778182-15.476363z" fill="#13C463" p-id="1342"></path><path d="M407.272727 90.647273a486.632727 486.632727 0 0 1 504.552728 11.636363l25.018181-14.429091A512 512 0 0 0 139.636364 546.909091l25.018181-14.429091A486.981818 486.981818 0 0 1 407.272727 90.647273zM893.323636 933.352727a486.749091 486.749091 0 0 1-504.669091-11.636363l-24.901818 14.429091A512 512 0 0 0 1161.192727 477.090909l-24.901818 13.963636a486.981818 486.981818 0 0 1-242.967273 442.298182z" fill="#13C463" p-id="1343"></path><path d="M814.545455 795.927273a327.447273 327.447273 0 0 1-258.21091 29.556363l-29.78909 17.105455A353.163636 353.163636 0 0 0 998.865455 570.181818l-29.789091 17.105455A326.865455 326.865455 0 0 1 814.545455 795.927273zM486.865455 228.072727A327.447273 327.447273 0 0 1 744.727273 198.516364l29.789091-17.105455A353.163636 353.163636 0 0 0 302.545455 453.818182l29.78909-17.105455A326.865455 326.865455 0 0 1 486.865455 228.072727zM1288.378182 374.690909a53.294545 53.294545 0 0 1-14.429091 11.636364L229.469091 989.090909a53.876364 53.876364 0 0 1-73.425455-19.665454L7.214545 710.632727a53.527273 53.527273 0 0 1 19.781819-73.309091L1071.476364 34.909091a53.876364 53.876364 0 0 1 73.425454 19.665454l148.829091 258.327273a53.061818 53.061818 0 0 1 5.352727 40.727273 55.272727 55.272727 0 0 1-10.705454 21.061818zM32.232727 665.716364A28.043636 28.043636 0 0 0 29.323636 698.181818l148.829091 257.978182a28.392727 28.392727 0 0 0 38.516364 10.356364l1044.48-601.949091a28.16 28.16 0 0 0 10.356364-38.516364L1122.676364 67.84a28.276364 28.276364 0 0 0-38.4-10.356364L39.68 659.432727a27.810909 27.810909 0 0 0-7.447273 6.283637z" fill="#13C463" p-id="1344"></path><path d="M356.770909 569.250909l22.341818 38.749091-15.476363 8.727273L349.090909 592.64l-153.483636 88.785455 14.778182 25.483636-15.476364 8.96-23.272727-39.912727L256 627.2c-6.283636-4.887273-11.636364-8.843636-16.174545-11.636364L256 602.647273c3.956364 3.141818 9.774545 8.261818 17.338182 15.127272z m-17.338182 199.447273l-49.221818 28.392727 7.563636 13.149091-15.47Line truncated

After

Width:  |  Height:  |  Size: 10 KiB

+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="1724316570161" class="icon" viewBox="0 0 1185 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1505" xmlns:xlink="http://www.w3.org/1999/xlink" width="231.4453125" height="200"><path d="M414.276535 230.004913l-2.443086-31.647244 26.446614 17.351559 29.437984-11.852598-8.143622 30.31685 20.423559 24.221229-31.623055 1.475527-16.722646 26.801386-11.239811-29.760504-30.663559-7.522772zM581.664252 176.902047l13.884472-28.542992 14.206993 28.220473 31.42148 4.321763-22.350614 22.092599 5.684409 31.123149-28.180157-14.513385-27.897953 14.819779 5.28126-31.066709-22.76989-21.689448zM896.507969 672.735748l17.754708 26.398236-31.494047-2.064126-19.560819 24.705008-7.95011-30.502299-29.575055-11.02211 26.744945-16.771024 1.104629-31.526299 24.414741 20.197795 30.268472-8.619338zM777.030551 801.961323l2.112504 31.647244-26.446614-17.682142-29.413795 11.546205 8.466141-30.308787-20.092976-24.221229 31.606929-1.153008 17.045166-26.793323 10.86085 29.704063 30.647433 7.837229zM609.312252 853.451591l-14.198929 28.518803-14.110236-28.542992-31.405355-4.636221 22.673134-22.084535-5.36189-31.12315 27.833449 14.835906 28.188221-14.803654-5.28126 31.066709 22.76989 22.060346zM298.435528 354.828094l-17.448315-26.390173 31.485984 2.394709 19.875275-24.753386 7.611465 30.865134 29.583118 11.288189-27.011024 16.779087-1.419086 31.526299-24.084158-20.504189-30.518425 8.280693zM962.56 91.53915a43.636913 43.636913 0 0 1 59.375874 15.601889l138.627024 236.753638c12.175118 20.447748 5.12 47.208819-15.609953 59.375874L229.13411 938.185575a43.636913 43.636913 0 0 1-59.375874-15.60189L31.12315 685.773606a43.636913 43.636913 0 0 1 15.601889-59.319433z m25.672567 24.108346a13.594205 13.594205 0 0 0-10.441575 1.548095L61.625449 652.054173a13.586142 13.586142 0 0 0-4.853921 18.83515l138.643149 236.793953a13.586142 13.586142 0 0 0 18.843213 4.837795l915.818834-534.915024a13.957039 13.957039 0 0 0 5.160315-18.778708l-138.602834-236.78589a13.594205 13.594205 0 0 0-8.401638-6.393953z" fill="#F5222D" p-id="1506"></path><path d="M395.981606 172.338394c123.670173-72.349228 271.11811-69.462677 388.394331-5.12l29.623433-17.335433a414.574866 414.574866 0 0 0-112.107842-47.071748 429.991307 429.991307 0 0 0-162.009701-10.498016 412.792945 412.792945 0 0 0-158.80063 54.707401 417.856504 417.856504 0 0 0-125.363402 111.922394A426.282331 426.282331 0 0 0 185.206929 405.004094a417.348535 417.348535 0 0 0-13.529701 120.977134l29.623433-17.335433c1.386835-133.958551 70.688252-263.958173 194.672882-336.307401z m397.666772 679.484472c-123.670173 72.365354-271.110047 69.462677-388.394331 5.128063l-29.623433 17.335433a414.679685 414.679685 0 0 0 112.075591 47.087874 429.991307 429.991307 0 0 0 162.009701 10.498016 412.744567 412.744567 0 0 0 158.808692-54.707402 423.145827 423.145827 0 0 0 209.105638-378.976756l-29.623433 17.335434c-1.072378 133.974677-70.712441 263.95011-194.350362 336.307401h-0.008063z" fill="#F5222D" p-id="1507"></path><path d="M478.377323 313.110173a226.271748 226.271748 0 0 1 109.979212-31.219905l45.668788-26.761071c-58.634079-9.13537-118.316346 2.314079-170.612914 32.735748a258.693039 258.693039 0 0 0-111.91433 132.71685l45.67685-26.761071a230.359685 230.359685 0 0 1 81.097575-80.589606l0.104819-0.120945z m232.568945 397.674835a226.328189 226.328189 0 0 1-109.979213 31.227968l-45.668787 26.753008c58.634079 9.13537 118.316346-2.314079 170.612913-32.735748a258.709165 258.709165 0 0 0 111.914331-132.71685l-45.676851 26.761071a225.215496 225.215496 0 0 1-81.097574 80.597669l-0.104819 0.112882zM188.57726 706.938961l-10.062614-17.424126 109.938897-63.471874 9.578835 16.585574 17.093543-9.869102-18.770645-32.509984-63.689575 36.767244c-4.047622-3.918614-7.804976-7.337323-11.272063-10.24l-16.859717 13.747401c3.249386 2.144756 6.595528 4.458835 9.869103 7.038993l-62.173733 35.896441 19.254426 33.348535 17.093543-9.869102zM317.44 781.142677l-19.060913-33.017953 32.679307-18.867401 4.741039 8.216189 17.093543-9.869103-48.474708-83.959937-49.772851 28.736504-7.933984-13.747401-17.432189 10.062614 7.933984 13.747402-49.264882 28.446236 48.764977 84.459842 17.093543-9.869102-5.031307-8.708032 32.171339-18.585196 19.060913 33.017952 17.432189-10.062614z m-12.505701-97.126803l-32.679307 18.867402-8.321008-14.41663 32.679307-18.867402 8.321008 14.41663z m-50.111496 28.930016l-32.171338 18.577134-8.321008-14.41663 32.171338-18.577134 8.321008 14.41663z m16.932284 29.325102l-32.171339 18.577134-8.127496-14.077984 32.171339-18.577134 8.127496 14.077984z m50.111496-28.930016l-32.679307 18.867402-8.127496-14.077984 32.679307-18.867402 8.127496 14.077984z m95.828661 7.684032c11.062425-6.38589 13.368441-15.537386 6.692284-27.099717l-25.05978-43.411149c3.55578-4.289512 7.014803-8.740283 10.48189-13.199118l-9.482079-16.424315c-3.467087 4.458835-6.92611 8.917669-10.48189 13.199118l-17.803086-30.832882 14.755275-8.Line truncated

After

Width:  |  Height:  |  Size: 11 KiB

+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="1724297262365" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1396" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M707.91 103c16.28 0 29.522 13.007 29.897 29.195l0.009 0.706v111.878a29.96 29.96 0 0 1-0.898 7.3l171.177-0.001c16.28 0 29.522 13.007 29.897 29.195l0.008 0.706v637.12c0 16.278-13.01 29.518-29.2 29.893l-0.705 0.008H270.884c-16.28 0-29.522-13.007-29.897-29.195l-0.008-0.706V787.274c0-16.514 13.389-29.9 29.905-29.9 16.28 0 29.522 13.007 29.897 29.194l0.008 0.706v101.924h577.4V311.88h-577.4v88.787c0 16.278-13.009 29.518-29.2 29.893l-0.705 0.008c-16.28 0-29.522-13.008-29.897-29.195l-0.008-0.706V281.979c0-16.278 13.009-29.518 29.2-29.893l0.705-0.008h408.019a29.916 29.916 0 0 1-0.89-6.593l-0.008-0.706v-81.978H132.808v407.113h385.787L408.223 456.982c-11.36-11.624-11.329-30.143-0.066-41.729l0.554-0.555c11.625-11.358 30.147-11.327 41.734-0.066l0.555 0.554 161.028 164.762c11.244 11.504 11.344 29.793 0.362 41.42l-0.55 0.565-161.027 161.849c-11.648 11.707-30.583 11.757-42.292 0.11-11.524-11.461-11.754-29.979-0.657-41.723l0.546-0.563 111.319-111.89H102.905c-16.28 0-29.522-13.007-29.897-29.195l-0.008-0.705V132.9c0-16.278 13.01-29.518 29.2-29.893l0.705-0.008H707.91z" p-id="1397"></path></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+8
View File
@@ -5,6 +5,7 @@ export interface AppLinkGroup {
// 链接列表
links: AppLink[]
}
// APP 链接
export interface AppLink {
// 链接名称
@@ -21,6 +22,8 @@ export const enum APP_LINK_TYPE_ENUM {
ACTIVITY_COMBINATION,
// 秒杀活动
ACTIVITY_SECKILL,
// 积分商城活动
ACTIVITY_POINT,
// 文章详情
ARTICLE_DETAIL,
// 优惠券详情
@@ -130,6 +133,11 @@ export const APP_LINK_GROUP_LIST = [
path: '/pages/activity/seckill/list',
type: APP_LINK_TYPE_ENUM.ACTIVITY_SECKILL
},
{
name: '积分商城活动',
path: '/pages/activity/point/list',
type: APP_LINK_TYPE_ENUM.ACTIVITY_POINT
},
{
name: '签到中心',
path: '/pages/app/sign'
@@ -11,7 +11,7 @@ const prefixCls = getPrefixCls('content-wrap')
defineProps({
title: propTypes.string.def(''),
message: propTypes.string.def(''),
bodyStyle: propTypes.object.def({ padding: '20px' })
bodyStyle: propTypes.object.def({ padding: '10px' })
})
</script>
+33 -33
View File
@@ -548,10 +548,10 @@ const inputChange = () => {
<el-form>
<el-form-item label="类型">
<el-radio-group v-model="cronValue.second.type">
<el-radio-button label="0">任意值</el-radio-button>
<el-radio-button label="1">范围</el-radio-button>
<el-radio-button label="2">间隔</el-radio-button>
<el-radio-button label="3">指定</el-radio-button>
<el-radio-button value="0">任意值</el-radio-button>
<el-radio-button value="1">范围</el-radio-button>
<el-radio-button value="2">间隔</el-radio-button>
<el-radio-button value="3">指定</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item v-if="cronValue.second.type == '1'" label="范围">
@@ -607,10 +607,10 @@ const inputChange = () => {
<el-form>
<el-form-item label="类型">
<el-radio-group v-model="cronValue.minute.type">
<el-radio-button label="0">任意值</el-radio-button>
<el-radio-button label="1">范围</el-radio-button>
<el-radio-button label="2">间隔</el-radio-button>
<el-radio-button label="3">指定</el-radio-button>
<el-radio-button value="0">任意值</el-radio-button>
<el-radio-button value="1">范围</el-radio-button>
<el-radio-button value="2">间隔</el-radio-button>
<el-radio-button value="3">指定</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item v-if="cronValue.minute.type == '1'" label="范围">
@@ -666,10 +666,10 @@ const inputChange = () => {
<el-form>
<el-form-item label="类型">
<el-radio-group v-model="cronValue.hour.type">
<el-radio-button label="0">任意值</el-radio-button>
<el-radio-button label="1">范围</el-radio-button>
<el-radio-button label="2">间隔</el-radio-button>
<el-radio-button label="3">指定</el-radio-button>
<el-radio-button value="0">任意值</el-radio-button>
<el-radio-button value="1">范围</el-radio-button>
<el-radio-button value="2">间隔</el-radio-button>
<el-radio-button value="3">指定</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item v-if="cronValue.hour.type == '1'" label="范围">
@@ -725,12 +725,12 @@ const inputChange = () => {
<el-form>
<el-form-item label="类型">
<el-radio-group v-model="cronValue.day.type">
<el-radio-button label="0">任意值</el-radio-button>
<el-radio-button label="1">范围</el-radio-button>
<el-radio-button label="2">间隔</el-radio-button>
<el-radio-button label="3">指定</el-radio-button>
<el-radio-button label="4">本月最后一天</el-radio-button>
<el-radio-button label="5">不指定</el-radio-button>
<el-radio-button value="0">任意值</el-radio-button>
<el-radio-button value="1">范围</el-radio-button>
<el-radio-button value="2">间隔</el-radio-button>
<el-radio-button value="3">指定</el-radio-button>
<el-radio-button value="4">本月最后一天</el-radio-button>
<el-radio-button value="5">不指定</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item v-if="cronValue.day.type == '1'" label="范围">
@@ -786,10 +786,10 @@ const inputChange = () => {
<el-form>
<el-form-item label="类型">
<el-radio-group v-model="cronValue.month.type">
<el-radio-button label="0">任意值</el-radio-button>
<el-radio-button label="1">范围</el-radio-button>
<el-radio-button label="2">间隔</el-radio-button>
<el-radio-button label="3">指定</el-radio-button>
<el-radio-button value="0">任意值</el-radio-button>
<el-radio-button value="1">范围</el-radio-button>
<el-radio-button value="2">间隔</el-radio-button>
<el-radio-button value="3">指定</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item v-if="cronValue.month.type == '1'" label="范围">
@@ -846,12 +846,12 @@ const inputChange = () => {
<el-form>
<el-form-item label="类型">
<el-radio-group v-model="cronValue.week.type">
<el-radio-button label="0">任意值</el-radio-button>
<el-radio-button label="1">范围</el-radio-button>
<el-radio-button label="2">间隔</el-radio-button>
<el-radio-button label="3">指定</el-radio-button>
<el-radio-button label="4">本月最后一周</el-radio-button>
<el-radio-button label="5">不指定</el-radio-button>
<el-radio-button value="0">任意值</el-radio-button>
<el-radio-button value="1">范围</el-radio-button>
<el-radio-button value="2">间隔</el-radio-button>
<el-radio-button value="3">指定</el-radio-button>
<el-radio-button value="4">本月最后一周</el-radio-button>
<el-radio-button value="5">不指定</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item v-if="cronValue.week.type == '1'" label="范围">
@@ -925,11 +925,11 @@ const inputChange = () => {
<el-form>
<el-form-item label="类型">
<el-radio-group v-model="cronValue.year.type">
<el-radio-button label="-1">忽略</el-radio-button>
<el-radio-button label="0">任意值</el-radio-button>
<el-radio-button label="1">范围</el-radio-button>
<el-radio-button label="2">间隔</el-radio-button>
<el-radio-button label="3">指定</el-radio-button>
<el-radio-button value="-1">忽略</el-radio-button>
<el-radio-button value="0">任意值</el-radio-button>
<el-radio-button value="1">范围</el-radio-button>
<el-radio-button value="2">间隔</el-radio-button>
<el-radio-button value="3">指定</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item v-if="cronValue.year.type == '1'" label="范围">
+60 -30
View File
@@ -1,8 +1,9 @@
<script lang="tsx">
import { defineComponent, PropType, ref } from 'vue'
import { computed, defineComponent, PropType } from 'vue'
import { isHexColor } from '@/utils/color'
import { ElTag } from 'element-plus'
import { DictDataType, getDictOptions } from '@/utils/dict'
import { isArray, isBoolean, isNumber, isString } from '@/utils/is'
export default defineComponent({
name: 'DictTag',
@@ -12,49 +13,78 @@ export default defineComponent({
required: true
},
value: {
type: [String, Number, Boolean] as PropType<string | number | boolean>,
type: [String, Number, Boolean, Array],
required: true
},
// 字符串分隔符 只有当 props.value 传入值为字符串时有效
separator: {
type: String as PropType<string>,
default: ','
},
// 每个 tag 之间的间隔,默认为 5px,参考的 el-row 的 gutter
gutter: {
type: String as PropType<string>,
default: '5px'
}
},
setup(props) {
const dictData = ref<DictDataType>()
const getDictObj = (dictType: string, value: string) => {
const dictOptions = getDictOptions(dictType)
dictOptions.forEach((dict: DictDataType) => {
if (dict.value === value) {
if (dict.colorType + '' === 'default') {
dict.colorType = 'info'
}
dictData.value = dict
}
})
}
const rederDictTag = () => {
const valueArr: any = computed(() => {
// 1. 是 Number 类型和 Boolean 类型的情况
if (isNumber(props.value) || isBoolean(props.value)) {
return [String(props.value)]
}
// 2. 是字符串(进一步判断是否有包含分隔符号 -> props.sepSymbol )
else if (isString(props.value)) {
return props.value.split(props.separator)
}
// 3. 数组
else if (isArray(props.value)) {
return props.value.map(String)
}
return []
})
const renderDictTag = () => {
if (!props.type) {
return null
}
// 解决自定义字典标签值为零时标签不渲染的问题
if (props.value === undefined || props.value === null) {
if (props.value === undefined || props.value === null || props.value === '') {
return null
}
getDictObj(props.type, props.value.toString())
// 添加标签的文字颜色为白色,解决自定义背景颜色时标签文字看不清的问题
const dictOptions = getDictOptions(props.type)
return (
<ElTag
style={dictData.value?.cssClass ? 'color: #fff' : ''}
type={dictData.value?.colorType}
color={
dictData.value?.cssClass && isHexColor(dictData.value?.cssClass)
? dictData.value?.cssClass
: ''
}
disableTransitions={true}
<div
class="dict-tag"
style={{
display: 'inline-flex',
gap: props.gutter,
justifyContent: 'center',
alignItems: 'center'
}}
>
{dictData.value?.label}
</ElTag>
{dictOptions.map((dict: DictDataType) => {
if (valueArr.value.includes(dict.value)) {
if (dict.colorType + '' === 'primary' || dict.colorType + '' === 'default') {
dict.colorType = ''
}
return (
// 添加标签的文字颜色为白色,解决自定义背景颜色时标签文字看不清的问题
<ElTag
style={dict?.cssClass ? 'color: #fff' : ''}
type={dict?.colorType || null}
color={dict?.cssClass && isHexColor(dict?.cssClass) ? dict?.cssClass : ''}
disableTransitions={true}
>
{dict?.label}
</ElTag>
)
}
})}
</div>
)
}
return () => rederDictTag()
return () => renderDictTag()
}
})
</script>
@@ -165,6 +165,7 @@ $toolbar-position: -55px;
width: 80px;
height: 25px;
font-size: 12px;
color: #6a6a6a;
line-height: 25px;
text-align: center;
background: #fff;
@@ -11,8 +11,8 @@
<el-form :model="formData" label-width="80px">
<el-form-item label="组件背景" prop="bgType">
<el-radio-group v-model="formData.bgType">
<el-radio label="color">纯色</el-radio>
<el-radio label="img">图片</el-radio>
<el-radio value="color">纯色</el-radio>
<el-radio value="img">图片</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="选择颜色" prop="bgColor" v-if="formData.bgType === 'color'">
@@ -5,12 +5,12 @@
<el-form-item label="样式" prop="type">
<el-radio-group v-model="formData.type">
<el-tooltip class="item" content="默认" placement="bottom">
<el-radio-button label="default">
<el-radio-button value="default">
<Icon icon="system-uicons:carousel" />
</el-radio-button>
</el-tooltip>
<el-tooltip class="item" content="卡片" placement="bottom">
<el-radio-button label="card">
<el-radio-button value="card">
<Icon icon="ic:round-view-carousel" />
</el-radio-button>
</el-tooltip>
@@ -18,8 +18,8 @@
</el-form-item>
<el-form-item label="指示器" prop="indicator">
<el-radio-group v-model="formData.indicator">
<el-radio label="dot">小圆点</el-radio>
<el-radio label="number">数字</el-radio>
<el-radio value="dot">小圆点</el-radio>
<el-radio value="number">数字</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="是否轮播" prop="autoplay">
@@ -43,8 +43,8 @@
<template #default="{ element }">
<el-form-item label="类型" prop="type" class="m-b-8px!" label-width="40px">
<el-radio-group v-model="element.type">
<el-radio label="img">图片</el-radio>
<el-radio label="video">视频</el-radio>
<el-radio value="img">图片</el-radio>
<el-radio value="video">视频</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
@@ -26,17 +26,17 @@
<el-form-item label="列数" prop="type">
<el-radio-group v-model="formData.columns">
<el-tooltip class="item" content="一列" placement="bottom">
<el-radio-button :label="1">
<el-radio-button :value="1">
<Icon icon="fluent:text-column-one-24-filled" />
</el-radio-button>
</el-tooltip>
<el-tooltip class="item" content="二列" placement="bottom">
<el-radio-button :label="2">
<el-radio-button :value="2">
<Icon icon="fluent:text-column-two-24-filled" />
</el-radio-button>
</el-tooltip>
<el-tooltip class="item" content="三列" placement="bottom">
<el-radio-button :label="3">
<el-radio-button :value="3">
<Icon icon="fluent:text-column-three-24-filled" />
</el-radio-button>
</el-tooltip>
@@ -11,7 +11,7 @@
:key="index"
:content="item.text"
>
<el-radio-button :label="item.type">
<el-radio-button :value="item.type">
<Icon :icon="item.icon" />
</el-radio-button>
</el-tooltip>
@@ -24,12 +24,12 @@
<el-form-item label="左右边距" prop="paddingType">
<el-radio-group v-model="formData!.paddingType">
<el-tooltip content="无边距" placement="top">
<el-radio-button label="none">
<el-radio-button value="none">
<Icon icon="tabler:box-padding" />
</el-radio-button>
</el-tooltip>
<el-tooltip content="左右留边" placement="top">
<el-radio-button label="horizontal">
<el-radio-button value="horizontal">
<Icon icon="vaadin:padding" />
</el-radio-button>
</el-tooltip>
@@ -44,7 +44,7 @@ defineOptions({ name: 'FloatingActionButton' })
defineProps<{ property: FloatingActionButtonProperty }>()
// 是否展开
const expanded = ref(true)
const expanded = ref(false)
// 处理展开/折叠
const handleToggleFab = () => {
expanded.value = !expanded.value
@@ -3,8 +3,8 @@
<el-card header="按钮配置" class="property-group" shadow="never">
<el-form-item label="展开方向" prop="direction">
<el-radio-group v-model="formData.direction">
<el-radio label="vertical">垂直</el-radio>
<el-radio label="horizontal">水平</el-radio>
<el-radio value="vertical">垂直</el-radio>
<el-radio value="horizontal">水平</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="显示文字" prop="showText">
@@ -4,8 +4,8 @@
<el-form label-width="80px" :model="formData" class="m-t-8px">
<el-form-item label="每行数量" prop="column">
<el-radio-group v-model="formData.column">
<el-radio :label="3">3个</el-radio>
<el-radio :label="4">4个</el-radio>
<el-radio :value="3">3个</el-radio>
<el-radio :value="4">4个</el-radio>
</el-radio-group>
</el-form-item>
@@ -4,21 +4,21 @@
<el-form label-width="80px" :model="formData" class="m-t-8px">
<el-form-item label="布局" prop="layout">
<el-radio-group v-model="formData.layout">
<el-radio label="iconText">图标+文字</el-radio>
<el-radio label="icon">仅图标</el-radio>
<el-radio value="iconText">图标+文字</el-radio>
<el-radio value="icon">仅图标</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="行数" prop="row">
<el-radio-group v-model="formData.row">
<el-radio :label="1">1行</el-radio>
<el-radio :label="2">2行</el-radio>
<el-radio :value="1">1行</el-radio>
<el-radio :value="2">2行</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="列数" prop="column">
<el-radio-group v-model="formData.column">
<el-radio :label="3">3列</el-radio>
<el-radio :label="4">4列</el-radio>
<el-radio :label="5">5列</el-radio>
<el-radio :value="3">3列</el-radio>
<el-radio :value="4">4列</el-radio>
<el-radio :value="5">5列</el-radio>
</el-radio-group>
</el-form-item>
@@ -14,9 +14,9 @@
<template v-if="selectedHotAreaIndex === cellIndex">
<el-form-item label="类型" :prop="`cell[${cellIndex}].type`">
<el-radio-group v-model="cell.type">
<el-radio label="text">文字</el-radio>
<el-radio label="image">图片</el-radio>
<el-radio label="search">搜索框</el-radio>
<el-radio value="text">文字</el-radio>
<el-radio value="image">图片</el-radio>
<el-radio value="search">搜索框</el-radio>
</el-radio-group>
</el-form-item>
<!-- 1. 文字 -->
@@ -2,27 +2,27 @@
<el-form label-width="80px" :model="formData" :rules="rules">
<el-form-item label="样式" prop="styleType">
<el-radio-group v-model="formData!.styleType">
<el-radio label="normal">标准</el-radio>
<el-radio value="normal">标准</el-radio>
<el-tooltip
content="沉侵式头部仅支持微信小程序、APP,建议页面第一个组件为图片展示类组件"
placement="top"
>
<el-radio label="inner">沉浸式</el-radio>
<el-radio value="inner">沉浸式</el-radio>
</el-tooltip>
</el-radio-group>
</el-form-item>
<el-form-item label="常驻显示" prop="alwaysShow" v-if="formData.styleType === 'inner'">
<el-radio-group v-model="formData!.alwaysShow">
<el-radio :label="false">关闭</el-radio>
<el-radio :value="false">关闭</el-radio>
<el-tooltip content="常驻显示关闭后,头部小组件将在页面滑动时淡入" placement="top">
<el-radio :label="true">开启</el-radio>
<el-radio :value="true">开启</el-radio>
</el-tooltip>
</el-radio-group>
</el-form-item>
<el-form-item label="背景类型" prop="bgType">
<el-radio-group v-model="formData.bgType">
<el-radio label="color">纯色</el-radio>
<el-radio label="img">图片</el-radio>
<el-radio value="color">纯色</el-radio>
<el-radio value="img">图片</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="背景颜色" prop="bgColor" v-if="formData.bgType === 'color'">
@@ -11,10 +11,10 @@
<el-form-item label="显示次数" :prop="`list[${index}].showType`">
<el-radio-group v-model="element.showType">
<el-tooltip content="只显示一次,下次打开时不显示" placement="bottom">
<el-radio label="once">一次</el-radio>
<el-radio value="once">一次</el-radio>
</el-tooltip>
<el-tooltip content="每次打开时都会显示" placement="bottom">
<el-radio label="always">不限</el-radio>
<el-radio value="always">不限</el-radio>
</el-tooltip>
</el-radio-group>
</el-form-item>
@@ -67,15 +67,15 @@
class="text-16px"
:style="{ color: property.fields.price.color }"
>
¥{{ spu.price }}
¥{{ fenToYuan(spu.price as any) }}
</span>
<!-- 市场价 -->
<span
v-if="property.fields.marketPrice.show && spu.marketPrice"
class="ml-4px text-10px line-through"
:style="{ color: property.fields.marketPrice.color }"
>¥{{ spu.marketPrice }}</span
>
>¥{{ fenToYuan(spu.marketPrice) }}
</span>
</div>
<div class="text-12px">
<!-- 销量 -->
@@ -117,6 +117,7 @@
<script setup lang="ts">
import { ProductCardProperty } from './config'
import * as ProductSpuApi from '@/api/mall/product/spu'
import { fenToYuan } from '../../../../../utils'
/** 商品卡片 */
defineOptions({ name: 'ProductCard' })
@@ -8,17 +8,17 @@
<el-form-item label="布局" prop="type">
<el-radio-group v-model="formData.layoutType">
<el-tooltip class="item" content="单列大图" placement="bottom">
<el-radio-button label="oneColBigImg">
<el-radio-button value="oneColBigImg">
<Icon icon="fluent:text-column-one-24-filled" />
</el-radio-button>
</el-tooltip>
<el-tooltip class="item" content="单列小图" placement="bottom">
<el-radio-button label="oneColSmallImg">
<el-radio-button value="oneColSmallImg">
<Icon icon="fluent:text-column-two-left-24-filled" />
</el-radio-button>
</el-tooltip>
<el-tooltip class="item" content="双列" placement="bottom">
<el-radio-button label="twoCol">
<el-radio-button value="twoCol">
<Icon icon="fluent:text-column-two-24-filled" />
</el-radio-button>
</el-tooltip>
@@ -74,8 +74,8 @@
<el-card header="按钮" class="property-group" shadow="never">
<el-form-item label="按钮类型" prop="btnBuy.type">
<el-radio-group v-model="formData.btnBuy.type">
<el-radio-button label="text">文字</el-radio-button>
<el-radio-button label="img">图片</el-radio-button>
<el-radio-button value="text">文字</el-radio-button>
<el-radio-button value="img">图片</el-radio-button>
</el-radio-group>
</el-form-item>
<template v-if="formData.btnBuy.type === 'text'">
@@ -54,7 +54,7 @@
class="text-12px"
:style="{ color: property.fields.price.color }"
>
¥{{ spu.price }}
¥{{ fenToYuan(spu.price) }}
</span>
</div>
</div>
@@ -65,6 +65,7 @@
<script setup lang="ts">
import { ProductListProperty } from './config'
import * as ProductSpuApi from '@/api/mall/product/spu'
import { fenToYuan } from '@/utils'
/** 商品栏 */
defineOptions({ name: 'ProductList' })
@@ -8,17 +8,17 @@
<el-form-item label="布局" prop="type">
<el-radio-group v-model="formData.layoutType">
<el-tooltip class="item" content="双列" placement="bottom">
<el-radio-button label="twoCol">
<el-radio-button value="twoCol">
<Icon icon="fluent:text-column-two-24-filled" />
</el-radio-button>
</el-tooltip>
<el-tooltip class="item" content="三列" placement="bottom">
<el-radio-button label="threeCol">
<el-radio-button value="threeCol">
<Icon icon="fluent:text-column-three-24-filled" />
</el-radio-button>
</el-tooltip>
<el-tooltip class="item" content="水平滑动" placement="bottom">
<el-radio-button label="horizSwiper">
<el-radio-button value="horizSwiper">
<Icon icon="system-uicons:carousel" />
</el-radio-button>
</el-tooltip>
@@ -3,13 +3,21 @@ import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
/** 拼团属性 */
export interface PromotionCombinationProperty {
// 布局类型:单列 | 三列
layoutType: 'oneCol' | 'threeCol'
layoutType: 'oneColBigImg' | 'oneColSmallImg' | 'twoCol'
// 商品字段
fields: {
// 商品名称
name: PromotionCombinationFieldProperty
// 商品简介
introduction: PromotionCombinationFieldProperty
// 商品价格
price: PromotionCombinationFieldProperty
// 市场价
marketPrice: PromotionCombinationFieldProperty
// 商品销量
salesCount: PromotionCombinationFieldProperty
// 商品库存
stock: PromotionCombinationFieldProperty
}
// 角标
badge: {
@@ -18,6 +26,19 @@ export interface PromotionCombinationProperty {
// 角标图片
imgUrl: string
}
// 按钮
btnBuy: {
// 类型:文字 | 图片
type: 'text' | 'img'
// 文字
text: string
// 文字按钮:背景渐变起始颜色
bgBeginColor: string
// 文字按钮:背景渐变结束颜色
bgEndColor: string
// 图片按钮:图片地址
imgUrl: string
}
// 上圆角
borderRadiusTop: number
// 下圆角
@@ -25,7 +46,7 @@ export interface PromotionCombinationProperty {
// 间距
space: number
// 拼团活动编号
activityId: number
activityIds: number[]
// 组件样式
style: ComponentStyle
}
@@ -44,12 +65,23 @@ export const component = {
name: '拼团',
icon: 'mdi:account-group',
property: {
layoutType: 'oneCol',
layoutType: 'oneColBigImg',
fields: {
name: { show: true, color: '#000' },
price: { show: true, color: '#ff3000' }
introduction: { show: true, color: '#999' },
price: { show: true, color: '#ff3000' },
marketPrice: { show: true, color: '#c4c4c4' },
salesCount: { show: true, color: '#c4c4c4' },
stock: { show: false, color: '#c4c4c4' }
},
badge: { show: false, imgUrl: '' },
btnBuy: {
type: 'text',
text: '去拼团',
bgBeginColor: '#FF6000',
bgEndColor: '#FE832A',
imgUrl: ''
},
borderRadiusTop: 8,
borderRadiusBottom: 8,
space: 8,
@@ -1,125 +1,201 @@
<template>
<el-scrollbar class="z-1 min-h-30px" wrap-class="w-full" ref="containerRef">
<!-- 商品网格 -->
<div :class="`box-content min-h-30px w-full flex flex-row flex-wrap`" ref="containerRef">
<div
class="grid overflow-x-auto"
class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white"
:style="{
gridGap: `${property.space}px`,
gridTemplateColumns,
width: scrollbarWidth
...calculateSpace(index),
...calculateWidth(),
borderTopLeftRadius: `${property.borderRadiusTop}px`,
borderTopRightRadius: `${property.borderRadiusTop}px`,
borderBottomLeftRadius: `${property.borderRadiusBottom}px`,
borderBottomRightRadius: `${property.borderRadiusBottom}px`
}"
v-for="(spu, index) in spuList"
:key="index"
>
<!-- 商品 -->
<!-- 角标 -->
<div v-if="property.badge.show" class="absolute left-0 top-0 z-1 items-center justify-center">
<el-image fit="cover" :src="property.badge.imgUrl" class="h-26px w-38px" />
</div>
<!-- 商品封面图 -->
<div
class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white"
:style="{
borderTopLeftRadius: `${property.borderRadiusTop}px`,
borderTopRightRadius: `${property.borderRadiusTop}px`,
borderBottomLeftRadius: `${property.borderRadiusBottom}px`,
borderBottomRightRadius: `${property.borderRadiusBottom}px`
}"
v-for="(spu, index) in spuList"
:key="index"
:class="[
'h-140px',
{
'w-full': property.layoutType !== 'oneColSmallImg',
'w-140px': property.layoutType === 'oneColSmallImg'
}
]"
>
<!-- 角标 -->
<div
v-if="property.badge.show"
class="absolute left-0 top-0 z-1 items-center justify-center"
>
<el-image fit="cover" :src="property.badge.imgUrl" class="h-26px w-38px" />
</div>
<!-- 商品封面图 -->
<el-image fit="cover" :src="spu.picUrl" :style="{ width: imageSize, height: imageSize }" />
<el-image fit="cover" class="h-full w-full" :src="spu.picUrl" />
</div>
<div
:class="[
' flex flex-col gap-8px p-8px box-border',
{
'w-full': property.layoutType !== 'oneColSmallImg',
'w-[calc(100%-140px-16px)]': property.layoutType === 'oneColSmallImg'
}
]"
>
<!-- 商品名称 -->
<div
v-if="property.fields.name.show"
:class="[
'flex flex-col gap-8px p-8px box-border',
'text-14px ',
{
'w-[calc(100%-64px)]': columns === 2,
'w-full': columns === 3
truncate: property.layoutType !== 'oneColSmallImg',
'overflow-ellipsis line-clamp-2': property.layoutType === 'oneColSmallImg'
}
]"
:style="{ color: property.fields.name.color }"
>
<!-- 商品名称 -->
<div
v-if="property.fields.name.show"
class="truncate text-12px"
:style="{ color: property.fields.name.color }"
{{ spu.name }}
</div>
<!-- 商品简介 -->
<div
v-if="property.fields.introduction.show"
class="truncate text-12px"
:style="{ color: property.fields.introduction.color }"
>
{{ spu.introduction }}
</div>
<div>
<!-- 价格 -->
<span
v-if="property.fields.price.show"
class="text-16px"
:style="{ color: property.fields.price.color }"
>
{{ spu.name }}
</div>
<div>
<!-- 商品价格 -->
<span
v-if="property.fields.price.show"
class="text-12px"
:style="{ color: property.fields.price.color }"
>
¥{{ spu.price }}
</span>
</div>
¥{{ fenToYuan(spu.price || Infinity) }}
</span>
<!-- 市场价 -->
<span
v-if="property.fields.marketPrice.show && spu.marketPrice"
class="ml-4px text-10px line-through"
:style="{ color: property.fields.marketPrice.color }"
>¥{{ fenToYuan(spu.marketPrice) }}</span
>
</div>
<div class="text-12px">
<!-- 销量 -->
<span
v-if="property.fields.salesCount.show"
:style="{ color: property.fields.salesCount.color }"
>
已售{{ (spu.salesCount || 0) + (spu.virtualSalesCount || 0) }}件
</span>
<!-- 库存 -->
<span v-if="property.fields.stock.show" :style="{ color: property.fields.stock.color }">
库存{{ spu.stock || 0 }}
</span>
</div>
</div>
<!-- 购买按钮 -->
<div class="absolute bottom-8px right-8px">
<!-- 文字按钮 -->
<span
v-if="property.btnBuy.type === 'text'"
class="rounded-full p-x-12px p-y-4px text-12px text-white"
:style="{
background: `linear-gradient(to right, ${property.btnBuy.bgBeginColor}, ${property.btnBuy.bgEndColor}`
}"
>
{{ property.btnBuy.text }}
</span>
<!-- 图片按钮 -->
<el-image
v-else
class="h-28px w-28px rounded-full"
fit="cover"
:src="property.btnBuy.imgUrl"
/>
</div>
</div>
</el-scrollbar>
</div>
</template>
<script setup lang="ts">
import { PromotionCombinationProperty } from './config'
import * as ProductSpuApi from '@/api/mall/product/spu'
import * as CombinationActivityApi from '@/api/mall/promotion/combination/combinationActivity'
import { fenToYuan } from '@/utils'
/** 拼团 */
/** 拼团卡片 */
defineOptions({ name: 'PromotionCombination' })
// 定义属性
const props = defineProps<{ property: PromotionCombinationProperty }>()
// 商品列表
const spuList = ref<ProductSpuApi.Spu[]>([])
const spuIdList = ref<number[]>([])
const combinationActivityList = ref<CombinationActivityApi.CombinationActivityVO[]>([])
watch(
() => props.property.activityId,
() => props.property.activityIds,
async () => {
if (!props.property.activityId) return
const activity = await CombinationActivityApi.getCombinationActivity(props.property.activityId)
if (!activity?.spuId) return
spuList.value = [await ProductSpuApi.getSpu(activity.spuId)]
try {
// 新添加的拼团组件,是没有活动ID的
const activityIds = props.property.activityIds
// 检查活动ID的有效性
if (Array.isArray(activityIds) && activityIds.length > 0) {
// 获取拼团活动详情列表
combinationActivityList.value =
await CombinationActivityApi.getCombinationActivityListByIds(activityIds)
// 获取拼团活动的 SPU 详情列表
spuList.value = []
spuIdList.value = combinationActivityList.value
.map((activity) => activity.spuId)
.filter((spuId): spuId is number => typeof spuId === 'number')
if (spuIdList.value.length > 0) {
spuList.value = await ProductSpuApi.getSpuDetailList(spuIdList.value)
}
// 更新 SPU 的最低价格
combinationActivityList.value.forEach((activity) => {
// 匹配spuId
const spu = spuList.value.find((spu) => spu.id === activity.spuId)
if (spu) {
// 赋值活动价格,哪个最便宜就赋值哪个
spu.price = Math.min(activity.combinationPrice || Infinity, spu.price || Infinity)
}
})
}
} catch (error) {
console.error('获取拼团活动细节或 SPU 细节时出错:', error)
}
},
{
immediate: true,
deep: true
}
)
// 手机宽度
const phoneWidth = ref(375)
/**
* 计算商品的间距
* @param index 商品索引
*/
const calculateSpace = (index: number) => {
// 商品的列数
const columns = props.property.layoutType === 'twoCol' ? 2 : 1
// 第一列没有左边距
const marginLeft = index % columns === 0 ? '0' : props.property.space + 'px'
// 第一行没有上边距
const marginTop = index < columns ? '0' : props.property.space + 'px'
return { marginLeft, marginTop }
}
// 容器
const containerRef = ref()
// 商品的列数
const columns = ref(2)
// 滚动条宽度
const scrollbarWidth = ref('100%')
// 商品图大小
const imageSize = ref('0')
// 商品网络列数
const gridTemplateColumns = ref('')
// 计算布局参数
watch(
() => [props.property, phoneWidth, spuList.value.length],
() => {
// 计算列数
columns.value = props.property.layoutType === 'oneCol' ? 1 : 3
// 每列的宽度为:(总宽度 - 间距 * (列数 - 1))/ 列数
const productWidth =
(phoneWidth.value - props.property.space * (columns.value - 1)) / columns.value
// 商品图布局:2列时,左右布局 3列时,上下布局
imageSize.value = columns.value === 2 ? '64px' : `${productWidth}px`
// 指定列数
gridTemplateColumns.value = `repeat(${columns.value}, auto)`
// 不滚动
scrollbarWidth.value = '100%'
},
{ immediate: true, deep: true }
)
onMounted(() => {
// 提取手机宽度
phoneWidth.value = containerRef.value?.wrapRef?.offsetWidth || 375
})
// 计算商品的宽度
const calculateWidth = () => {
let width = '100%'
// 双列时每列的宽度为:(总宽度 - 间距)/ 2
if (props.property.layoutType === 'twoCol') {
width = `${(containerRef.value.offsetWidth - props.property.space) / 2}px`
}
return { width }
}
</script>
<style scoped lang="scss"></style>
@@ -2,30 +2,31 @@
<ComponentContainerProperty v-model="formData.style">
<el-form label-width="80px" :model="formData">
<el-card header="拼团活动" class="property-group" shadow="never">
<el-form-item label="拼团活动" prop="activityId">
<el-select v-model="formData.activityId">
<el-option
v-for="activity in activityList"
:key="activity.id"
:label="activity.name"
:value="activity.id"
/>
</el-select>
</el-form-item>
<CombinationShowcase v-model="formData.activityIds" />
</el-card>
<el-card header="商品样式" class="property-group" shadow="never">
<el-form-item label="布局" prop="type">
<el-radio-group v-model="formData.layoutType">
<el-tooltip class="item" content="单列" placement="bottom">
<el-radio-button label="oneCol">
<el-tooltip class="item" content="单列大图" placement="bottom">
<el-radio-button value="oneColBigImg">
<Icon icon="fluent:text-column-one-24-filled" />
</el-radio-button>
</el-tooltip>
<el-tooltip class="item" content="三列" placement="bottom">
<el-radio-button label="threeCol">
<Icon icon="fluent:text-column-three-24-filled" />
<el-tooltip class="item" content="单列小图" placement="bottom">
<el-radio-button value="oneColSmallImg">
<Icon icon="fluent:text-column-two-left-24-filled" />
</el-radio-button>
</el-tooltip>
<el-tooltip class="item" content="双列" placement="bottom">
<el-radio-button value="twoCol">
<Icon icon="fluent:text-column-two-24-filled" />
</el-radio-button>
</el-tooltip>
<!--<el-tooltip class="item" content="三列" placement="bottom">
<el-radio-button value="threeCol">
<Icon icon="fluent:text-column-three-24-filled" />
</el-radio-button>
</el-tooltip>-->
</el-radio-group>
</el-form-item>
<el-form-item label="商品名称" prop="fields.name.show">
@@ -34,12 +35,36 @@
<el-checkbox v-model="formData.fields.name.show" />
</div>
</el-form-item>
<el-form-item label="商品简介" prop="fields.introduction.show">
<div class="flex gap-8px">
<ColorInput v-model="formData.fields.introduction.color" />
<el-checkbox v-model="formData.fields.introduction.show" />
</div>
</el-form-item>
<el-form-item label="商品价格" prop="fields.price.show">
<div class="flex gap-8px">
<ColorInput v-model="formData.fields.price.color" />
<el-checkbox v-model="formData.fields.price.show" />
</div>
</el-form-item>
<el-form-item label="市场价" prop="fields.marketPrice.show">
<div class="flex gap-8px">
<ColorInput v-model="formData.fields.marketPrice.color" />
<el-checkbox v-model="formData.fields.marketPrice.show" />
</div>
</el-form-item>
<el-form-item label="商品销量" prop="fields.salesCount.show">
<div class="flex gap-8px">
<ColorInput v-model="formData.fields.salesCount.color" />
<el-checkbox v-model="formData.fields.salesCount.show" />
</div>
</el-form-item>
<el-form-item label="商品库存" prop="fields.stock.show">
<div class="flex gap-8px">
<ColorInput v-model="formData.fields.stock.color" />
<el-checkbox v-model="formData.fields.stock.show" />
</div>
</el-form-item>
</el-card>
<el-card header="角标" class="property-group" shadow="never">
<el-form-item label="角标" prop="badge.show">
@@ -47,10 +72,36 @@
</el-form-item>
<el-form-item label="角标" prop="badge.imgUrl" v-if="formData.badge.show">
<UploadImg v-model="formData.badge.imgUrl" height="44px" width="72px">
<template #tip> 建议尺寸:36 * 22 </template>
<template #tip> 建议尺寸:36 * 22</template>
</UploadImg>
</el-form-item>
</el-card>
<el-card header="按钮" class="property-group" shadow="never">
<el-form-item label="按钮类型" prop="btnBuy.type">
<el-radio-group v-model="formData.btnBuy.type">
<el-radio-button value="text">文字</el-radio-button>
<el-radio-button value="img">图片</el-radio-button>
</el-radio-group>
</el-form-item>
<template v-if="formData.btnBuy.type === 'text'">
<el-form-item label="按钮文字" prop="btnBuy.text">
<el-input v-model="formData.btnBuy.text" />
</el-form-item>
<el-form-item label="左侧背景" prop="btnBuy.bgBeginColor">
<ColorInput v-model="formData.btnBuy.bgBeginColor" />
</el-form-item>
<el-form-item label="右侧背景" prop="btnBuy.bgEndColor">
<ColorInput v-model="formData.btnBuy.bgEndColor" />
</el-form-item>
</template>
<template v-else>
<el-form-item label="图片" prop="btnBuy.imgUrl">
<UploadImg v-model="formData.btnBuy.imgUrl" height="56px" width="56px">
<template #tip> 建议尺寸:56 * 56</template>
</UploadImg>
</el-form-item>
</template>
</el-card>
<el-card header="商品样式" class="property-group" shadow="never">
<el-form-item label="上圆角" prop="borderRadiusTop">
<el-slider
@@ -92,6 +143,7 @@ import { PromotionCombinationProperty } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util'
import * as CombinationActivityApi from '@/api/mall/promotion/combination/combinationActivity'
import { CommonStatusEnum } from '@/utils/constants'
import CombinationShowcase from '@/views/mall/promotion/combination/components/CombinationShowcase.vue'
// 拼团属性面板
defineOptions({ name: 'PromotionCombinationProperty' })
@@ -100,7 +152,7 @@ const props = defineProps<{ modelValue: PromotionCombinationProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
// 活动列表
const activityList = ref<CombinationActivityApi.CombinationActivityVO>([])
const activityList = ref<CombinationActivityApi.CombinationActivityVO[]>([])
onMounted(async () => {
const { list } = await CombinationActivityApi.getCombinationActivityPage({
status: CommonStatusEnum.ENABLE
@@ -0,0 +1,96 @@
import {ComponentStyle, DiyComponent} from '@/components/DiyEditor/util'
/** 积分商城属性 */
export interface PromotionPointProperty {
// 布局类型:单列 | 三列
layoutType: 'oneColBigImg' | 'oneColSmallImg' | 'twoCol'
// 商品字段
fields: {
// 商品名称
name: PromotionPointFieldProperty
// 商品简介
introduction: PromotionPointFieldProperty
// 商品价格
price: PromotionPointFieldProperty
// 市场价
marketPrice: PromotionPointFieldProperty
// 商品销量
salesCount: PromotionPointFieldProperty
// 商品库存
stock: PromotionPointFieldProperty
}
// 角标
badge: {
// 是否显示
show: boolean
// 角标图片
imgUrl: string
}
// 按钮
btnBuy: {
// 类型:文字 | 图片
type: 'text' | 'img'
// 文字
text: string
// 文字按钮:背景渐变起始颜色
bgBeginColor: string
// 文字按钮:背景渐变结束颜色
bgEndColor: string
// 图片按钮:图片地址
imgUrl: string
}
// 上圆角
borderRadiusTop: number
// 下圆角
borderRadiusBottom: number
// 间距
space: number
// 秒杀活动编号
activityIds: number[]
// 组件样式
style: ComponentStyle
}
// 商品字段
export interface PromotionPointFieldProperty {
// 是否显示
show: boolean
// 颜色
color: string
}
// 定义组件
export const component = {
id: 'PromotionPoint',
name: '积分商城',
icon: 'ep:present',
property: {
layoutType: 'oneColBigImg',
fields: {
name: { show: true, color: '#000' },
introduction: { show: true, color: '#999' },
price: { show: true, color: '#ff3000' },
marketPrice: { show: true, color: '#c4c4c4' },
salesCount: { show: true, color: '#c4c4c4' },
stock: { show: false, color: '#c4c4c4' }
},
badge: { show: false, imgUrl: '' },
btnBuy: {
type: 'text',
text: '立即兑换',
bgBeginColor: '#FF6000',
bgEndColor: '#FE832A',
imgUrl: ''
},
borderRadiusTop: 8,
borderRadiusBottom: 8,
space: 8,
style: {
bgType: 'color',
bgColor: '',
marginLeft: 8,
marginRight: 8,
marginBottom: 8
} as ComponentStyle
}
} as DiyComponent<PromotionPointProperty>
@@ -0,0 +1,202 @@
<template>
<div ref="containerRef" :class="`box-content min-h-30px w-full flex flex-row flex-wrap`">
<div
v-for="(spu, index) in spuList"
:key="index"
:style="{
...calculateSpace(index),
...calculateWidth(),
borderTopLeftRadius: `${property.borderRadiusTop}px`,
borderTopRightRadius: `${property.borderRadiusTop}px`,
borderBottomLeftRadius: `${property.borderRadiusBottom}px`,
borderBottomRightRadius: `${property.borderRadiusBottom}px`
}"
class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white"
>
<!-- 角标 -->
<div v-if="property.badge.show" class="absolute left-0 top-0 z-1 items-center justify-center">
<el-image :src="property.badge.imgUrl" class="h-26px w-38px" fit="cover" />
</div>
<!-- 商品封面图 -->
<div
:class="[
'h-140px',
{
'w-full': property.layoutType !== 'oneColSmallImg',
'w-140px': property.layoutType === 'oneColSmallImg'
}
]"
>
<el-image :src="spu.picUrl" class="h-full w-full" fit="cover" />
</div>
<div
:class="[
' flex flex-col gap-8px p-8px box-border',
{
'w-full': property.layoutType !== 'oneColSmallImg',
'w-[calc(100%-140px-16px)]': property.layoutType === 'oneColSmallImg'
}
]"
>
<!-- 商品名称 -->
<div
v-if="property.fields.name.show"
:class="[
'text-14px ',
{
truncate: property.layoutType !== 'oneColSmallImg',
'overflow-ellipsis line-clamp-2': property.layoutType === 'oneColSmallImg'
}
]"
:style="{ color: property.fields.name.color }"
>
{{ spu.name }}
</div>
<!-- 商品简介 -->
<div
v-if="property.fields.introduction.show"
:style="{ color: property.fields.introduction.color }"
class="truncate text-12px"
>
{{ spu.introduction }}
</div>
<div>
<!-- 积分 -->
<span
v-if="property.fields.price.show"
:style="{ color: property.fields.price.color }"
class="text-16px"
>
{{ spu.point }}积分
{{ !spu.pointPrice || spu.pointPrice === 0 ? '' : `+${fenToYuan(spu.pointPrice)}元` }}
</span>
<!-- 市场价 -->
<span
v-if="property.fields.marketPrice.show && spu.marketPrice"
:style="{ color: property.fields.marketPrice.color }"
class="ml-4px text-10px line-through"
>
¥{{ fenToYuan(spu.marketPrice) }}
</span>
</div>
<div class="text-12px">
<!-- 销量 -->
<span
v-if="property.fields.salesCount.show"
:style="{ color: property.fields.salesCount.color }"
>
已兑{{ (spu.pointTotalStock || 0) - (spu.pointStock || 0) }}件
</span>
<!-- 库存 -->
<span v-if="property.fields.stock.show" :style="{ color: property.fields.stock.color }">
库存{{ spu.pointTotalStock || 0 }}
</span>
</div>
</div>
<!-- 购买按钮 -->
<div class="absolute bottom-8px right-8px">
<!-- 文字按钮 -->
<span
v-if="property.btnBuy.type === 'text'"
:style="{
background: `linear-gradient(to right, ${property.btnBuy.bgBeginColor}, ${property.btnBuy.bgEndColor}`
}"
class="rounded-full p-x-12px p-y-4px text-12px text-white"
>
{{ property.btnBuy.text }}
</span>
<!-- 图片按钮 -->
<el-image
v-else
:src="property.btnBuy.imgUrl"
class="h-28px w-28px rounded-full"
fit="cover"
/>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { PromotionPointProperty } from './config'
import * as ProductSpuApi from '@/api/mall/product/spu'
import { PointActivityApi, PointActivityVO, SpuExtension0 } from '@/api/mall/promotion/point'
import { fenToYuan } from '@/utils'
/** 积分商城卡片 */
defineOptions({ name: 'PromotionPoint' })
// 定义属性
const props = defineProps<{ property: PromotionPointProperty }>()
// 商品列表
const spuList = ref<SpuExtension0[]>([])
const spuIdList = ref<number[]>([])
const pointActivityList = ref<PointActivityVO[]>([])
watch(
() => props.property.activityIds,
async () => {
try {
// 新添加的积分商城组件,是没有活动ID的
const activityIds = props.property.activityIds
// 检查活动ID的有效性
if (Array.isArray(activityIds) && activityIds.length > 0) {
// 获取积分商城活动详情列表
pointActivityList.value = await PointActivityApi.getPointActivityListByIds(activityIds)
// 获取积分商城活动的 SPU 详情列表
spuList.value = []
spuIdList.value = pointActivityList.value.map((activity) => activity.spuId)
if (spuIdList.value.length > 0) {
spuList.value = await ProductSpuApi.getSpuDetailList(spuIdList.value)
}
// 更新 SPU 的最低兑换积分和所需兑换金额
pointActivityList.value.forEach((activity) => {
// 匹配spuId
const spu = spuList.value.find((spu) => spu.id === activity.spuId)
if (spu) {
spu.pointStock = activity.stock
spu.pointTotalStock = activity.totalStock
spu.point = activity.point
spu.pointPrice = activity.price
}
})
}
} catch (error) {
console.error('获取积分商城活动细节或 SPU 细节时出错:', error)
}
},
{
immediate: true,
deep: true
}
)
/**
* 计算商品的间距
* @param index 商品索引
*/
const calculateSpace = (index: number) => {
// 商品的列数
const columns = props.property.layoutType === 'twoCol' ? 2 : 1
// 第一列没有左边距
const marginLeft = index % columns === 0 ? '0' : props.property.space + 'px'
// 第一行没有上边距
const marginTop = index < columns ? '0' : props.property.space + 'px'
return { marginLeft, marginTop }
}
// 容器
const containerRef = ref()
// 计算商品的宽度
const calculateWidth = () => {
let width = '100%'
// 双列时每列的宽度为:(总宽度 - 间距)/ 2
if (props.property.layoutType === 'twoCol') {
width = `${(containerRef.value.offsetWidth - props.property.space) / 2}px`
}
return { width }
}
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,154 @@
<template>
<ComponentContainerProperty v-model="formData.style">
<el-form :model="formData" label-width="80px">
<el-card class="property-group" header="积分商城活动" shadow="never">
<PointShowcase v-model="formData.activityIds" />
</el-card>
<el-card class="property-group" header="商品样式" shadow="never">
<el-form-item label="布局" prop="type">
<el-radio-group v-model="formData.layoutType">
<el-tooltip class="item" content="单列大图" placement="bottom">
<el-radio-button value="oneColBigImg">
<Icon icon="fluent:text-column-one-24-filled" />
</el-radio-button>
</el-tooltip>
<el-tooltip class="item" content="单列小图" placement="bottom">
<el-radio-button value="oneColSmallImg">
<Icon icon="fluent:text-column-two-left-24-filled" />
</el-radio-button>
</el-tooltip>
<el-tooltip class="item" content="双列" placement="bottom">
<el-radio-button value="twoCol">
<Icon icon="fluent:text-column-two-24-filled" />
</el-radio-button>
</el-tooltip>
<!--<el-tooltip class="item" content="三列" placement="bottom">
<el-radio-button value="threeCol">
<Icon icon="fluent:text-column-three-24-filled" />
</el-radio-button>
</el-tooltip>-->
</el-radio-group>
</el-form-item>
<el-form-item label="商品名称" prop="fields.name.show">
<div class="flex gap-8px">
<ColorInput v-model="formData.fields.name.color" />
<el-checkbox v-model="formData.fields.name.show" />
</div>
</el-form-item>
<el-form-item label="商品简介" prop="fields.introduction.show">
<div class="flex gap-8px">
<ColorInput v-model="formData.fields.introduction.color" />
<el-checkbox v-model="formData.fields.introduction.show" />
</div>
</el-form-item>
<el-form-item label="商品价格" prop="fields.price.show">
<div class="flex gap-8px">
<ColorInput v-model="formData.fields.price.color" />
<el-checkbox v-model="formData.fields.price.show" />
</div>
</el-form-item>
<el-form-item label="市场价" prop="fields.marketPrice.show">
<div class="flex gap-8px">
<ColorInput v-model="formData.fields.marketPrice.color" />
<el-checkbox v-model="formData.fields.marketPrice.show" />
</div>
</el-form-item>
<el-form-item label="商品销量" prop="fields.salesCount.show">
<div class="flex gap-8px">
<ColorInput v-model="formData.fields.salesCount.color" />
<el-checkbox v-model="formData.fields.salesCount.show" />
</div>
</el-form-item>
<el-form-item label="商品库存" prop="fields.stock.show">
<div class="flex gap-8px">
<ColorInput v-model="formData.fields.stock.color" />
<el-checkbox v-model="formData.fields.stock.show" />
</div>
</el-form-item>
</el-card>
<el-card class="property-group" header="角标" shadow="never">
<el-form-item label="角标" prop="badge.show">
<el-switch v-model="formData.badge.show" />
</el-form-item>
<el-form-item v-if="formData.badge.show" label="角标" prop="badge.imgUrl">
<UploadImg v-model="formData.badge.imgUrl" height="44px" width="72px">
<template #tip> 建议尺寸:36 * 22</template>
</UploadImg>
</el-form-item>
</el-card>
<el-card class="property-group" header="按钮" shadow="never">
<el-form-item label="按钮类型" prop="btnBuy.type">
<el-radio-group v-model="formData.btnBuy.type">
<el-radio-button value="text">文字</el-radio-button>
<el-radio-button value="img">图片</el-radio-button>
</el-radio-group>
</el-form-item>
<template v-if="formData.btnBuy.type === 'text'">
<el-form-item label="按钮文字" prop="btnBuy.text">
<el-input v-model="formData.btnBuy.text" />
</el-form-item>
<el-form-item label="左侧背景" prop="btnBuy.bgBeginColor">
<ColorInput v-model="formData.btnBuy.bgBeginColor" />
</el-form-item>
<el-form-item label="右侧背景" prop="btnBuy.bgEndColor">
<ColorInput v-model="formData.btnBuy.bgEndColor" />
</el-form-item>
</template>
<template v-else>
<el-form-item label="图片" prop="btnBuy.imgUrl">
<UploadImg v-model="formData.btnBuy.imgUrl" height="56px" width="56px">
<template #tip> 建议尺寸:56 * 56</template>
</UploadImg>
</el-form-item>
</template>
</el-card>
<el-card class="property-group" header="商品样式" shadow="never">
<el-form-item label="上圆角" prop="borderRadiusTop">
<el-slider
v-model="formData.borderRadiusTop"
:max="100"
:min="0"
:show-input-controls="false"
input-size="small"
show-input
/>
</el-form-item>
<el-form-item label="下圆角" prop="borderRadiusBottom">
<el-slider
v-model="formData.borderRadiusBottom"
:max="100"
:min="0"
:show-input-controls="false"
input-size="small"
show-input
/>
</el-form-item>
<el-form-item label="间隔" prop="space">
<el-slider
v-model="formData.space"
:max="100"
:min="0"
:show-input-controls="false"
input-size="small"
show-input
/>
</el-form-item>
</el-card>
</el-form>
</ComponentContainerProperty>
</template>
<script lang="ts" setup>
import { PromotionPointProperty } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util'
import PointShowcase from '@/views/mall/promotion/point/components/PointShowcase.vue'
// 秒杀属性面板
defineOptions({ name: 'PromotionPointProperty' })
const props = defineProps<{ modelValue: PromotionPointProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
</script>
<style lang="scss" scoped></style>
@@ -3,13 +3,21 @@ import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
/** 秒杀属性 */
export interface PromotionSeckillProperty {
// 布局类型:单列 | 三列
layoutType: 'oneCol' | 'threeCol'
layoutType: 'oneColBigImg' | 'oneColSmallImg' | 'twoCol'
// 商品字段
fields: {
// 商品名称
name: PromotionSeckillFieldProperty
// 商品简介
introduction: PromotionSeckillFieldProperty
// 商品价格
price: PromotionSeckillFieldProperty
// 市场价
marketPrice: PromotionSeckillFieldProperty
// 商品销量
salesCount: PromotionSeckillFieldProperty
// 商品库存
stock: PromotionSeckillFieldProperty
}
// 角标
badge: {
@@ -18,6 +26,19 @@ export interface PromotionSeckillProperty {
// 角标图片
imgUrl: string
}
// 按钮
btnBuy: {
// 类型:文字 | 图片
type: 'text' | 'img'
// 文字
text: string
// 文字按钮:背景渐变起始颜色
bgBeginColor: string
// 文字按钮:背景渐变结束颜色
bgEndColor: string
// 图片按钮:图片地址
imgUrl: string
}
// 上圆角
borderRadiusTop: number
// 下圆角
@@ -25,10 +46,11 @@ export interface PromotionSeckillProperty {
// 间距
space: number
// 秒杀活动编号
activityId: number
activityIds: number[]
// 组件样式
style: ComponentStyle
}
// 商品字段
export interface PromotionSeckillFieldProperty {
// 是否显示
@@ -43,13 +65,23 @@ export const component = {
name: '秒杀',
icon: 'mdi:calendar-time',
property: {
activityId: undefined,
layoutType: 'oneCol',
layoutType: 'oneColBigImg',
fields: {
name: { show: true, color: '#000' },
price: { show: true, color: '#ff3000' }
introduction: { show: true, color: '#999' },
price: { show: true, color: '#ff3000' },
marketPrice: { show: true, color: '#c4c4c4' },
salesCount: { show: true, color: '#c4c4c4' },
stock: { show: false, color: '#c4c4c4' }
},
badge: { show: false, imgUrl: '' },
btnBuy: {
type: 'text',
text: '立即秒杀',
bgBeginColor: '#FF6000',
bgEndColor: '#FE832A',
imgUrl: ''
},
borderRadiusTop: 8,
borderRadiusBottom: 8,
space: 8,
@@ -1,125 +1,201 @@
<template>
<el-scrollbar class="z-1 min-h-30px" wrap-class="w-full" ref="containerRef">
<!-- 商品网格 -->
<div :class="`box-content min-h-30px w-full flex flex-row flex-wrap`" ref="containerRef">
<div
class="grid overflow-x-auto"
class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white"
:style="{
gridGap: `${property.space}px`,
gridTemplateColumns,
width: scrollbarWidth
...calculateSpace(index),
...calculateWidth(),
borderTopLeftRadius: `${property.borderRadiusTop}px`,
borderTopRightRadius: `${property.borderRadiusTop}px`,
borderBottomLeftRadius: `${property.borderRadiusBottom}px`,
borderBottomRightRadius: `${property.borderRadiusBottom}px`
}"
v-for="(spu, index) in spuList"
:key="index"
>
<!-- 商品 -->
<!-- 角标 -->
<div v-if="property.badge.show" class="absolute left-0 top-0 z-1 items-center justify-center">
<el-image fit="cover" :src="property.badge.imgUrl" class="h-26px w-38px" />
</div>
<!-- 商品封面图 -->
<div
class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white"
:style="{
borderTopLeftRadius: `${property.borderRadiusTop}px`,
borderTopRightRadius: `${property.borderRadiusTop}px`,
borderBottomLeftRadius: `${property.borderRadiusBottom}px`,
borderBottomRightRadius: `${property.borderRadiusBottom}px`
}"
v-for="(spu, index) in spuList"
:key="index"
:class="[
'h-140px',
{
'w-full': property.layoutType !== 'oneColSmallImg',
'w-140px': property.layoutType === 'oneColSmallImg'
}
]"
>
<!-- 角标 -->
<div
v-if="property.badge.show"
class="absolute left-0 top-0 z-1 items-center justify-center"
>
<el-image fit="cover" :src="property.badge.imgUrl" class="h-26px w-38px" />
</div>
<!-- 商品封面图 -->
<el-image fit="cover" :src="spu.picUrl" :style="{ width: imageSize, height: imageSize }" />
<el-image fit="cover" class="h-full w-full" :src="spu.picUrl" />
</div>
<div
:class="[
' flex flex-col gap-8px p-8px box-border',
{
'w-full': property.layoutType !== 'oneColSmallImg',
'w-[calc(100%-140px-16px)]': property.layoutType === 'oneColSmallImg'
}
]"
>
<!-- 商品名称 -->
<div
v-if="property.fields.name.show"
:class="[
'flex flex-col gap-8px p-8px box-border',
'text-14px ',
{
'w-[calc(100%-64px)]': columns === 2,
'w-full': columns === 3
truncate: property.layoutType !== 'oneColSmallImg',
'overflow-ellipsis line-clamp-2': property.layoutType === 'oneColSmallImg'
}
]"
:style="{ color: property.fields.name.color }"
>
<!-- 商品名称 -->
<div
v-if="property.fields.name.show"
class="truncate text-12px"
:style="{ color: property.fields.name.color }"
{{ spu.name }}
</div>
<!-- 商品简介 -->
<div
v-if="property.fields.introduction.show"
class="truncate text-12px"
:style="{ color: property.fields.introduction.color }"
>
{{ spu.introduction }}
</div>
<div>
<!-- 价格 -->
<span
v-if="property.fields.price.show"
class="text-16px"
:style="{ color: property.fields.price.color }"
>
{{ spu.name }}
</div>
<div>
<!-- 商品价格 -->
<span
v-if="property.fields.price.show"
class="text-12px"
:style="{ color: property.fields.price.color }"
>
¥{{ spu.price }}
</span>
</div>
¥{{ fenToYuan(spu.price || Infinity) }}
</span>
<!-- 市场价 -->
<span
v-if="property.fields.marketPrice.show && spu.marketPrice"
class="ml-4px text-10px line-through"
:style="{ color: property.fields.marketPrice.color }"
>¥{{ fenToYuan(spu.marketPrice) }}</span
>
</div>
<div class="text-12px">
<!-- 销量 -->
<span
v-if="property.fields.salesCount.show"
:style="{ color: property.fields.salesCount.color }"
>
已售{{ (spu.salesCount || 0) + (spu.virtualSalesCount || 0) }}件
</span>
<!-- 库存 -->
<span v-if="property.fields.stock.show" :style="{ color: property.fields.stock.color }">
库存{{ spu.stock || 0 }}
</span>
</div>
</div>
<!-- 购买按钮 -->
<div class="absolute bottom-8px right-8px">
<!-- 文字按钮 -->
<span
v-if="property.btnBuy.type === 'text'"
class="rounded-full p-x-12px p-y-4px text-12px text-white"
:style="{
background: `linear-gradient(to right, ${property.btnBuy.bgBeginColor}, ${property.btnBuy.bgEndColor}`
}"
>
{{ property.btnBuy.text }}
</span>
<!-- 图片按钮 -->
<el-image
v-else
class="h-28px w-28px rounded-full"
fit="cover"
:src="property.btnBuy.imgUrl"
/>
</div>
</div>
</el-scrollbar>
</div>
</template>
<script setup lang="ts">
import { PromotionSeckillProperty } from './config'
import * as ProductSpuApi from '@/api/mall/product/spu'
import * as SeckillActivityApi from '@/api/mall/promotion/seckill/seckillActivity'
import { fenToYuan } from '@/utils'
/** 秒杀 */
/** 秒杀卡片 */
defineOptions({ name: 'PromotionSeckill' })
// 定义属性
const props = defineProps<{ property: PromotionSeckillProperty }>()
// 商品列表
const spuList = ref<ProductSpuApi.Spu[]>([])
const spuIdList = ref<number[]>([])
const seckillActivityList = ref<SeckillActivityApi.SeckillActivityVO[]>([])
watch(
() => props.property.activityId,
() => props.property.activityIds,
async () => {
if (!props.property.activityId) return
const activity = await SeckillActivityApi.getSeckillActivity(props.property.activityId)
if (!activity?.spuId) return
spuList.value = [await ProductSpuApi.getSpu(activity.spuId)]
try {
// 新添加的秒杀组件,是没有活动ID的
const activityIds = props.property.activityIds
// 检查活动ID的有效性
if (Array.isArray(activityIds) && activityIds.length > 0) {
// 获取秒杀活动详情列表
seckillActivityList.value =
await SeckillActivityApi.getSeckillActivityListByIds(activityIds)
// 获取秒杀活动的 SPU 详情列表
spuList.value = []
spuIdList.value = seckillActivityList.value
.map((activity) => activity.spuId)
.filter((spuId): spuId is number => typeof spuId === 'number')
if (spuIdList.value.length > 0) {
spuList.value = await ProductSpuApi.getSpuDetailList(spuIdList.value)
}
// 更新 SPU 的最低价格
seckillActivityList.value.forEach((activity) => {
// 匹配spuId
const spu = spuList.value.find((spu) => spu.id === activity.spuId)
if (spu) {
// 赋值活动价格,哪个最便宜就赋值哪个
spu.price = Math.min(activity.seckillPrice || Infinity, spu.price || Infinity)
}
})
}
} catch (error) {
console.error('获取秒杀活动细节或 SPU 细节时出错:', error)
}
},
{
immediate: true,
deep: true
}
)
// 手机宽度
const phoneWidth = ref(375)
/**
* 计算商品的间距
* @param index 商品索引
*/
const calculateSpace = (index: number) => {
// 商品的列数
const columns = props.property.layoutType === 'twoCol' ? 2 : 1
// 第一列没有左边距
const marginLeft = index % columns === 0 ? '0' : props.property.space + 'px'
// 第一行没有上边距
const marginTop = index < columns ? '0' : props.property.space + 'px'
return { marginLeft, marginTop }
}
// 容器
const containerRef = ref()
// 商品的列数
const columns = ref(2)
// 滚动条宽度
const scrollbarWidth = ref('100%')
// 商品图大小
const imageSize = ref('0')
// 商品网络列数
const gridTemplateColumns = ref('')
// 计算布局参数
watch(
() => [props.property, phoneWidth, spuList.value.length],
() => {
// 计算列数
columns.value = props.property.layoutType === 'oneCol' ? 1 : 3
// 每列的宽度为:(总宽度 - 间距 * (列数 - 1))/ 列数
const productWidth =
(phoneWidth.value - props.property.space * (columns.value - 1)) / columns.value
// 商品图布局:2列时,左右布局 3列时,上下布局
imageSize.value = columns.value === 2 ? '64px' : `${productWidth}px`
// 指定列数
gridTemplateColumns.value = `repeat(${columns.value}, auto)`
// 不滚动
scrollbarWidth.value = '100%'
},
{ immediate: true, deep: true }
)
onMounted(() => {
// 提取手机宽度
phoneWidth.value = containerRef.value?.wrapRef?.offsetWidth || 375
})
// 计算商品的宽度
const calculateWidth = () => {
let width = '100%'
// 双列时每列的宽度为:(总宽度 - 间距)/ 2
if (props.property.layoutType === 'twoCol') {
width = `${(containerRef.value.offsetWidth - props.property.space) / 2}px`
}
return { width }
}
</script>
<style scoped lang="scss"></style>
@@ -2,30 +2,31 @@
<ComponentContainerProperty v-model="formData.style">
<el-form label-width="80px" :model="formData">
<el-card header="秒杀活动" class="property-group" shadow="never">
<el-form-item label="秒杀活动" prop="activityId">
<el-select v-model="formData.activityId">
<el-option
v-for="activity in activityList"
:key="activity.id"
:label="activity.name"
:value="activity.id"
/>
</el-select>
</el-form-item>
<SeckillShowcase v-model="formData.activityIds" />
</el-card>
<el-card header="商品样式" class="property-group" shadow="never">
<el-form-item label="布局" prop="type">
<el-radio-group v-model="formData.layoutType">
<el-tooltip class="item" content="单列" placement="bottom">
<el-radio-button label="oneCol">
<el-tooltip class="item" content="单列大图" placement="bottom">
<el-radio-button value="oneColBigImg">
<Icon icon="fluent:text-column-one-24-filled" />
</el-radio-button>
</el-tooltip>
<el-tooltip class="item" content="三列" placement="bottom">
<el-radio-button label="threeCol">
<Icon icon="fluent:text-column-three-24-filled" />
<el-tooltip class="item" content="单列小图" placement="bottom">
<el-radio-button value="oneColSmallImg">
<Icon icon="fluent:text-column-two-left-24-filled" />
</el-radio-button>
</el-tooltip>
<el-tooltip class="item" content="双列" placement="bottom">
<el-radio-button value="twoCol">
<Icon icon="fluent:text-column-two-24-filled" />
</el-radio-button>
</el-tooltip>
<!--<el-tooltip class="item" content="三列" placement="bottom">
<el-radio-button value="threeCol">
<Icon icon="fluent:text-column-three-24-filled" />
</el-radio-button>
</el-tooltip>-->
</el-radio-group>
</el-form-item>
<el-form-item label="商品名称" prop="fields.name.show">
@@ -34,12 +35,36 @@
<el-checkbox v-model="formData.fields.name.show" />
</div>
</el-form-item>
<el-form-item label="商品简介" prop="fields.introduction.show">
<div class="flex gap-8px">
<ColorInput v-model="formData.fields.introduction.color" />
<el-checkbox v-model="formData.fields.introduction.show" />
</div>
</el-form-item>
<el-form-item label="商品价格" prop="fields.price.show">
<div class="flex gap-8px">
<ColorInput v-model="formData.fields.price.color" />
<el-checkbox v-model="formData.fields.price.show" />
</div>
</el-form-item>
<el-form-item label="市场价" prop="fields.marketPrice.show">
<div class="flex gap-8px">
<ColorInput v-model="formData.fields.marketPrice.color" />
<el-checkbox v-model="formData.fields.marketPrice.show" />
</div>
</el-form-item>
<el-form-item label="商品销量" prop="fields.salesCount.show">
<div class="flex gap-8px">
<ColorInput v-model="formData.fields.salesCount.color" />
<el-checkbox v-model="formData.fields.salesCount.show" />
</div>
</el-form-item>
<el-form-item label="商品库存" prop="fields.stock.show">
<div class="flex gap-8px">
<ColorInput v-model="formData.fields.stock.color" />
<el-checkbox v-model="formData.fields.stock.show" />
</div>
</el-form-item>
</el-card>
<el-card header="角标" class="property-group" shadow="never">
<el-form-item label="角标" prop="badge.show">
@@ -47,10 +72,36 @@
</el-form-item>
<el-form-item label="角标" prop="badge.imgUrl" v-if="formData.badge.show">
<UploadImg v-model="formData.badge.imgUrl" height="44px" width="72px">
<template #tip> 建议尺寸:36 * 22 </template>
<template #tip> 建议尺寸:36 * 22</template>
</UploadImg>
</el-form-item>
</el-card>
<el-card header="按钮" class="property-group" shadow="never">
<el-form-item label="按钮类型" prop="btnBuy.type">
<el-radio-group v-model="formData.btnBuy.type">
<el-radio-button value="text">文字</el-radio-button>
<el-radio-button value="img">图片</el-radio-button>
</el-radio-group>
</el-form-item>
<template v-if="formData.btnBuy.type === 'text'">
<el-form-item label="按钮文字" prop="btnBuy.text">
<el-input v-model="formData.btnBuy.text" />
</el-form-item>
<el-form-item label="左侧背景" prop="btnBuy.bgBeginColor">
<ColorInput v-model="formData.btnBuy.bgBeginColor" />
</el-form-item>
<el-form-item label="右侧背景" prop="btnBuy.bgEndColor">
<ColorInput v-model="formData.btnBuy.bgEndColor" />
</el-form-item>
</template>
<template v-else>
<el-form-item label="图片" prop="btnBuy.imgUrl">
<UploadImg v-model="formData.btnBuy.imgUrl" height="56px" width="56px">
<template #tip> 建议尺寸:56 * 56</template>
</UploadImg>
</el-form-item>
</template>
</el-card>
<el-card header="商品样式" class="property-group" shadow="never">
<el-form-item label="上圆角" prop="borderRadiusTop">
<el-slider
@@ -92,6 +143,7 @@ import { PromotionSeckillProperty } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util'
import * as SeckillActivityApi from '@/api/mall/promotion/seckill/seckillActivity'
import { CommonStatusEnum } from '@/utils/constants'
import SeckillShowcase from '@/views/mall/promotion/seckill/components/SeckillShowcase.vue'
// 秒杀属性面板
defineOptions({ name: 'PromotionSeckillProperty' })
@@ -100,7 +152,7 @@ const props = defineProps<{ modelValue: PromotionSeckillProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
// 活动列表
const activityList = ref<SeckillActivityApi.SeckillActivityVO>([])
const activityList = ref<SeckillActivityApi.SeckillActivityVO[]>([])
onMounted(async () => {
const { list } = await SeckillActivityApi.getSeckillActivityPage({
status: CommonStatusEnum.ENABLE
@@ -13,12 +13,12 @@
<el-form-item label="框体样式">
<el-radio-group v-model="formData!.borderRadius">
<el-tooltip content="方形" placement="top">
<el-radio-button :label="0">
<el-radio-button :value="0">
<Icon icon="tabler:input-search" />
</el-radio-button>
</el-tooltip>
<el-tooltip content="圆形" placement="top">
<el-radio-button :label="10">
<el-radio-button :value="10">
<Icon icon="iconoir:input-search" />
</el-radio-button>
</el-tooltip>
@@ -30,12 +30,12 @@
<el-form-item label="文本位置" prop="placeholderPosition">
<el-radio-group v-model="formData!.placeholderPosition">
<el-tooltip content="居左" placement="top">
<el-radio-button label="left">
<el-radio-button value="left">
<Icon icon="ant-design:align-left-outlined" />
</el-radio-button>
</el-tooltip>
<el-tooltip content="居中" placement="top">
<el-radio-button label="center">
<el-radio-button value="center">
<Icon icon="ant-design:align-center-outlined" />
</el-radio-button>
</el-tooltip>
@@ -27,8 +27,8 @@
</el-form-item>
<el-form-item label="导航背景">
<el-radio-group v-model="formData!.style.bgType">
<el-radio-button label="color">纯色</el-radio-button>
<el-radio-button label="img">图片</el-radio-button>
<el-radio-button value="color">纯色</el-radio-button>
<el-radio-button value="img">图片</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="选择颜色" v-if="formData!.style.bgType === 'color'">
@@ -79,7 +79,7 @@
</template>
<script setup lang="ts">
import { TabBarProperty, THEME_LIST } from './config'
import { TabBarProperty, component, THEME_LIST } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util'
// 底部导航栏
defineOptions({ name: 'TabBarProperty' })
@@ -88,6 +88,9 @@ const props = defineProps<{ modelValue: TabBarProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
// 将数据库的值更新到右侧属性栏
component.property.items = formData.value.items
// 要的主题
const handleThemeChange = () => {
const theme = THEME_LIST.find((theme) => theme.id === formData.value.theme)
@@ -10,12 +10,12 @@
<el-form-item label="标题位置" prop="textAlign">
<el-radio-group v-model="formData!.textAlign">
<el-tooltip content="居左" placement="top">
<el-radio-button label="left">
<el-radio-button value="left">
<Icon icon="ant-design:align-left-outlined" />
</el-radio-button>
</el-tooltip>
<el-tooltip content="居中" placement="top">
<el-radio-button label="center">
<el-radio-button value="center">
<Icon icon="ant-design:align-center-outlined" />
</el-radio-button>
</el-tooltip>
@@ -88,9 +88,9 @@
<template v-if="formData.more.show">
<el-form-item label="样式" prop="more.type">
<el-radio-group v-model="formData.more.type">
<el-radio label="text">文字</el-radio>
<el-radio label="icon">图标</el-radio>
<el-radio label="all">文字+图标</el-radio>
<el-radio value="text">文字</el-radio>
<el-radio value="icon">图标</el-radio>
<el-radio value="all">文字+图标</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="更多文字" prop="more.text" v-show="formData.more.type !== 'icon'">
+2 -2
View File
@@ -13,9 +13,9 @@
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 bg-gray-1 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);">
<el-tooltip content="拖动排序">
<Icon icon="ic:round-drag-indicator" class="drag-icon cursor-move" />
<Icon icon="ic:round-drag-indicator" class="drag-icon cursor-move" style="color: #8a909c;" />
</el-tooltip>
<el-tooltip content="删除">
<Icon
+3 -2
View File
@@ -7,6 +7,7 @@ import { isNumber } from '@/utils/is'
import { ElMessage } from 'element-plus'
import { useLocaleStore } from '@/store/modules/locale'
import { getAccessToken, getTenantId } from '@/utils/auth'
import { getUploadUrl } from '@/components/UploadFile/src/useUpload'
defineOptions({ name: 'Editor' })
@@ -88,7 +89,7 @@ const editorConfig = computed((): IEditorConfig => {
scroll: true,
MENU_CONF: {
['uploadImage']: {
server: import.meta.env.VITE_UPLOAD_URL,
server: getUploadUrl(),
// 单个文件的最大体积限制,默认为 2M
maxFileSize: 5 * 1024 * 1024,
// 最多可上传几个文件,默认为 100
@@ -136,7 +137,7 @@ const editorConfig = computed((): IEditorConfig => {
}
},
['uploadVideo']: {
server: import.meta.env.VITE_UPLOAD_URL,
server: getUploadUrl(),
// 单个文件的最大体积限制,默认为 10M
maxFileSize: 10 * 1024 * 1024,
// 最多可上传几个文件,默认为 100
@@ -48,7 +48,7 @@ export const useDictSelectRule = () => {
},
{
type: 'select',
field: 'dictValueType',
field: 'valueType',
title: '字典值类型',
value: 'str',
options: [
+24 -9
View File
@@ -7,26 +7,41 @@ const props = defineProps({
src: propTypes.string.def('')
})
const loading = ref(true)
const height = ref('')
const frameRef = ref<HTMLElement | null>(null)
const init = () => {
height.value = document.documentElement.clientHeight - 94.5 + 'px'
loading.value = false
nextTick(() => {
loading.value = true
if (!frameRef.value) return
frameRef.value.onload = () => {
loading.value = false
}
})
}
onMounted(() => {
setTimeout(() => {
init()
}, 300)
init()
})
watch(
() => props.src,
() => {
init()
}
)
</script>
<template>
<div v-loading="loading" :style="'height:' + height">
<div
v-loading="loading"
class="w-full h-[calc(100vh-var(--top-tool-height)-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-2px)]"
>
<iframe
ref="frameRef"
:src="props.src"
frameborder="no"
frameborder="0"
scrolling="auto"
style="width: 100%; height: 100%"
height="100%"
width="100%"
allowfullscreen="true"
webkitallowfullscreen="true"
mozallowfullscreen="true"
></iframe>
</div>
</template>
+14 -4
View File
@@ -11,6 +11,10 @@ const props = defineProps({
modelValue: {
require: false,
type: String
},
clearable: {
require: false,
type: Boolean
}
})
const emit = defineEmits<{ (e: 'update:modelValue', v: string) }>()
@@ -92,6 +96,12 @@ function onCurrentChange(page) {
currentPage.value = page
}
function clearIcon() {
icon.value = ''
emit('update:modelValue', '')
visible.value = false
}
watch(
() => {
return props.modelValue
@@ -115,14 +125,14 @@ watch(
<template>
<div class="selector">
<ElInput v-model="inputValue" @click="visible = !visible">
<ElInput v-model="inputValue" @click="visible = !visible" :clearable="props.clearable" @clear="clearIcon">
<template #append>
<ElPopover
:popper-options="{
placement: 'auto'
}"
:visible="visible"
:width="350"
:width="355"
popper-class="pure-popper"
trigger="click"
>
@@ -147,7 +157,7 @@ watch(
>
<ElDivider border-style="dashed" class="tab-divider" />
<ElScrollbar height="220px">
<ul class="ml-2 flex flex-wrap px-2">
<ul class="ml-2 flex flex-wrap">
<li
v-for="(item, key) in pageList"
:key="key"
@@ -171,7 +181,7 @@ watch(
background
class="h-10 flex items-center justify-center"
layout="prev, pager, next"
small
size="small"
@current-change="onCurrentChange"
/>
</ElPopover>
@@ -1,9 +1,9 @@
<template>
<div class="flex flex-row items-center gap-2">
<el-radio-group v-model="shortcutDays" @change="handleShortcutDaysChange">
<el-radio-button :label="1">昨天</el-radio-button>
<el-radio-button :label="7">最近7天</el-radio-button>
<el-radio-button :label="30">最近30天</el-radio-button>
<el-radio-button :value="1">昨天</el-radio-button>
<el-radio-button :value="7">最近7天</el-radio-button>
<el-radio-button :value="30">最近30天</el-radio-button>
</el-radio-group>
<el-date-picker
v-model="times"
@@ -1,237 +0,0 @@
/* stylelint-disable order/properties-order */
<template>
<div class="add-node-btn-box">
<div class="add-node-btn">
<el-popover placement="right-start" v-model="visible" width="auto">
<div class="add-node-popover-body">
<a class="add-node-popover-item approver" @click="addType(1)">
<div class="item-wrapper">
<span class="iconfont"></span>
</div>
<p>审批人</p>
</a>
<a class="add-node-popover-item notifier" @click="addType(2)">
<div class="item-wrapper">
<span class="iconfont"></span>
</div>
<p>抄送人</p>
</a>
<a class="add-node-popover-item condition" @click="addType(4)">
<div class="item-wrapper">
<span class="iconfont"></span>
</div>
<p>条件分支</p>
</a>
</div>
<template #reference>
<button class="btn" type="button">
<span class="iconfont"></span>
</button>
</template>
</el-popover>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
let props = defineProps({
childNodeP: {
type: Object,
default: () => ({})
}
})
let emits = defineEmits(['update:childNodeP'])
let visible = ref(false)
const addType = (type) => {
visible.value = false
if (type != 4) {
var data
if (type == 1) {
data = {
nodeName: '审核人',
error: true,
type: 1,
settype: 1,
selectMode: 0,
selectRange: 0,
directorLevel: 1,
examineMode: 1,
noHanderAction: 1,
examineEndDirectorLevel: 0,
childNode: props.childNodeP,
nodeUserList: []
}
} else if (type == 2) {
data = {
nodeName: '抄送人',
type: 2,
ccSelfSelectFlag: 1,
childNode: props.childNodeP,
nodeUserList: []
}
}
emits('update:childNodeP', data)
} else {
emits('update:childNodeP', {
nodeName: '路由',
type: 4,
childNode: null,
conditionNodes: [
{
nodeName: '条件1',
error: true,
type: 3,
priorityLevel: 1,
conditionList: [],
nodeUserList: [],
childNode: props.childNodeP
},
{
nodeName: '条件2',
type: 3,
priorityLevel: 2,
conditionList: [],
nodeUserList: [],
childNode: null
}
]
})
}
}
</script>
<style scoped lang="scss">
.add-node-btn-box {
width: 240px;
display: inline-flex;
-ms-flex-negative: 0;
flex-shrink: 0;
-webkit-box-flex: 1;
-ms-flex-positive: 1;
position: relative;
&:before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: -1;
margin: auto;
width: 2px;
height: 100%;
background-color: #cacaca;
}
.add-node-btn {
user-select: none;
width: 240px;
padding: 20px 0 32px;
display: flex;
-webkit-box-pack: center;
justify-content: center;
flex-shrink: 0;
-webkit-box-flex: 1;
flex-grow: 1;
.btn {
outline: none;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
width: 30px;
height: 30px;
background: #3296fa;
border-radius: 50%;
position: relative;
border: none;
line-height: 30px;
-webkit-transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
.iconfont {
color: #fff;
font-size: 16px;
}
&:hover {
transform: scale(1.3);
box-shadow: 0 13px 27px 0 rgba(0, 0, 0, 0.1);
}
&:active {
transform: none;
background: #1e83e9;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
}
}
}
}
.add-node-popover-body {
display: flex;
.add-node-popover-item {
margin-right: 10px;
cursor: pointer;
text-align: center;
flex: 1;
color: #191f25 !important;
.item-wrapper {
user-select: none;
display: inline-block;
width: 80px;
height: 80px;
margin-bottom: 5px;
background: #fff;
border: 1px solid #e2e2e2;
border-radius: 50%;
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
.iconfont {
font-size: 35px;
line-height: 80px;
}
}
&.approver {
.item-wrapper {
color: #ff943e;
}
}
&.notifier {
.item-wrapper {
color: #3296fa;
}
}
&.condition {
.item-wrapper {
color: #15bc83;
}
}
&:hover {
.item-wrapper {
background: #3296fa;
box-shadow: 0 10px 20px 0 rgba(50, 150, 250, 0.4);
}
.iconfont {
color: #fff;
}
}
&:active {
.item-wrapper {
box-shadow: none;
background: #eaeaea;
}
.iconfont {
color: inherit;
}
}
}
}
</style>
@@ -1,297 +0,0 @@
<!-- eslint-disable vue/no-mutating-props -->
<!--
* @Date: 2022-09-21 14:41:53
* @LastEditors: StavinLi 495727881@qq.com
* @LastEditTime: 2023-05-24 15:20:24
* @FilePath: /Workflow-Vue3/src/components/nodeWrap.vue
-->
<template>
<div class="node-wrap" v-if="nodeConfig.type < 3">
<div class="node-wrap-box" :class="(nodeConfig.type == 0 ? 'start-node ' : '') +(isTried && nodeConfig.error ? 'active error' : '')">
<div class="title" :style="`background: rgb(${bgColors[nodeConfig.type]});`">
<span v-if="nodeConfig.type == 0">{{ nodeConfig.nodeName }}</span>
<template v-else>
<span class="iconfont">{{nodeConfig.type == 1?'':''}}</span>
<input
v-if="isInput"
type="text"
class="ant-input editable-title-input"
@blur="blurEvent()"
@focus="$event.currentTarget.select()"
v-focus
v-model="nodeConfig.nodeName"
:placeholder="defaultText"
/>
<span v-else class="editable-title" @click="clickEvent()">{{ nodeConfig.nodeName }}</span>
<i class="anticon anticon-close close" @click="delNode"></i>
</template>
</div>
<div class="content" @click="setPerson">
<div class="text">
<span class="placeholder" v-if="!showText">请选择{{defaultText}}</span>
{{showText}}
</div>
<i class="anticon anticon-right arrow"></i>
</div>
<div class="error_tip" v-if="isTried && nodeConfig.error">
<i class="anticon anticon-exclamation-circle"></i>
</div>
</div>
<addNode v-model:childNodeP="nodeConfig.childNode" />
</div>
<div class="branch-wrap" v-if="nodeConfig.type == 4">
<div class="branch-box-wrap">
<div class="branch-box">
<button class="add-branch" @click="addTerm">添加条件</button>
<div class="col-box" v-for="(item, index) in nodeConfig.conditionNodes" :key="index">
<div class="condition-node">
<div class="condition-node-box">
<div class="auto-judge" :class="isTried && item.error ? 'error active' : ''">
<div class="sort-left" v-if="index != 0" @click="arrTransfer(index, -1)">&lt;</div>
<div class="title-wrapper">
<input
v-if="isInputList[index]"
type="text"
class="ant-input editable-title-input"
@blur="blurEvent(index)"
@focus="$event.currentTarget.select()"
v-model="item.nodeName"
/>
<span v-else class="editable-title" @click="clickEvent(index)">{{ item.nodeName }}</span>
<span class="priority-title" @click="setPerson(item.priorityLevel)">优先级{{ item.priorityLevel }}</span>
<i class="anticon anticon-close close" @click="delTerm(index)"></i>
</div>
<div class="sort-right" v-if="index != nodeConfig.conditionNodes.length - 1" @click="arrTransfer(index)">&gt;</div>
<div class="content" @click="setPerson(item.priorityLevel)">{{ conditionStr(nodeConfig, index) }}</div>
<div class="error_tip" v-if="isTried && item.error">
<i class="anticon anticon-exclamation-circle"></i>
</div>
</div>
<addNode v-model:childNodeP="item.childNode" />
</div>
</div>
<nodeWrap v-if="item.childNode" v-model:nodeConfig="item.childNode" />
<template v-if="index == 0">
<div class="top-left-cover-line"></div>
<div class="bottom-left-cover-line"></div>
</template>
<template v-if="index == nodeConfig.conditionNodes.length - 1">
<div class="top-right-cover-line"></div>
<div class="bottom-right-cover-line"></div>
</template>
</div>
</div>
<addNode v-model:childNodeP="nodeConfig.childNode" />
</div>
</div>
<nodeWrap v-if="nodeConfig.childNode" v-model:nodeConfig="nodeConfig.childNode" />
</template>
<script setup>
import addNode from './addNode.vue'
import { onMounted, ref, watch, getCurrentInstance, computed } from 'vue'
import {
arrToStr,
conditionStr,
setApproverStr,
copyerStr,
bgColors,
placeholderList
} from './util'
import { useWorkFlowStoreWithOut } from '@/store/modules/simpleWorkflow'
let _uid = getCurrentInstance().uid
let props = defineProps({
nodeConfig: {
type: Object,
default: () => ({})
},
flowPermission: {
type: Object,
// eslint-disable-next-line vue/require-valid-default-prop
default: () => []
}
})
let defaultText = computed(() => {
return placeholderList[props.nodeConfig.type]
})
let showText = computed(() => {
if (props.nodeConfig.type == 0) return arrToStr(props.flowPermission) || '所有人'
if (props.nodeConfig.type == 1) return setApproverStr(props.nodeConfig)
return copyerStr(props.nodeConfig)
})
let isInputList = ref([])
let isInput = ref(false)
const resetConditionNodesErr = () => {
for (var i = 0; i < props.nodeConfig.conditionNodes.length; i++) {
// eslint-disable-next-line vue/no-mutating-props
props.nodeConfig.conditionNodes[i].error =
conditionStr(props.nodeConfig, i) == '请设置条件' &&
i != props.nodeConfig.conditionNodes.length - 1
}
}
onMounted(() => {
if (props.nodeConfig.type == 1) {
// eslint-disable-next-line vue/no-mutating-props
props.nodeConfig.error = !setApproverStr(props.nodeConfig)
} else if (props.nodeConfig.type == 2) {
// eslint-disable-next-line vue/no-mutating-props
props.nodeConfig.error = !copyerStr(props.nodeConfig)
} else if (props.nodeConfig.type == 4) {
resetConditionNodesErr()
}
})
let emits = defineEmits(['update:flowPermission', 'update:nodeConfig'])
let store = useWorkFlowStoreWithOut()
let {
setPromoter,
setApprover,
setCopyer,
setCondition,
setFlowPermission,
setApproverConfig,
setCopyerConfig,
setConditionsConfig
} = store
let isTried = computed(() => store.isTried)
let flowPermission1 = computed(() => store.flowPermission1)
let approverConfig1 = computed(() => store.approverConfig1)
let copyerConfig1 = computed(() => store.copyerConfig1)
let conditionsConfig1 = computed(() => store.conditionsConfig1)
watch(flowPermission1, (flow) => {
if (flow.flag && flow.id === _uid) {
emits('update:flowPermission', flow.value)
}
})
watch(approverConfig1, (approver) => {
if (approver.flag && approver.id === _uid) {
emits('update:nodeConfig', approver.value)
}
})
watch(copyerConfig1, (copyer) => {
if (copyer.flag && copyer.id === _uid) {
emits('update:nodeConfig', copyer.value)
}
})
watch(conditionsConfig1, (condition) => {
if (condition.flag && condition.id === _uid) {
emits('update:nodeConfig', condition.value)
}
})
const clickEvent = (index) => {
if (index || index === 0) {
isInputList.value[index] = true
} else {
isInput.value = true
}
}
const blurEvent = (index) => {
if (index || index === 0) {
isInputList.value[index] = false
// eslint-disable-next-line vue/no-mutating-props
props.nodeConfig.conditionNodes[index].nodeName =
props.nodeConfig.conditionNodes[index].nodeName || '条件'
} else {
isInput.value = false
// eslint-disable-next-line vue/no-mutating-props
props.nodeConfig.nodeName = props.nodeConfig.nodeName || defaultText
}
}
const delNode = () => {
emits('update:nodeConfig', props.nodeConfig.childNode)
}
const addTerm = () => {
let len = props.nodeConfig.conditionNodes.length + 1
// eslint-disable-next-line vue/no-mutating-props
props.nodeConfig.conditionNodes.push({
nodeName: '条件' + len,
type: 3,
priorityLevel: len,
conditionList: [],
nodeUserList: [],
childNode: null
})
resetConditionNodesErr()
emits('update:nodeConfig', props.nodeConfig)
}
const delTerm = (index) => {
// eslint-disable-next-line vue/no-mutating-props
props.nodeConfig.conditionNodes.splice(index, 1)
props.nodeConfig.conditionNodes.map((item, index) => {
item.priorityLevel = index + 1
item.nodeName = `条件${index + 1}`
})
resetConditionNodesErr()
emits('update:nodeConfig', props.nodeConfig)
if (props.nodeConfig.conditionNodes.length == 1) {
if (props.nodeConfig.childNode) {
if (props.nodeConfig.conditionNodes[0].childNode) {
reData(props.nodeConfig.conditionNodes[0].childNode, props.nodeConfig.childNode)
} else {
// eslint-disable-next-line vue/no-mutating-props
props.nodeConfig.conditionNodes[0].childNode = props.nodeConfig.childNode
}
}
emits('update:nodeConfig', props.nodeConfig.conditionNodes[0].childNode)
}
}
const reData = (data, addData) => {
if (!data.childNode) {
data.childNode = addData
} else {
reData(data.childNode, addData)
}
}
const setPerson = (priorityLevel) => {
var { type } = props.nodeConfig
if (type == 0) {
setPromoter(true)
setFlowPermission({
value: props.flowPermission,
flag: false,
id: _uid
})
} else if (type == 1) {
setApprover(true)
setApproverConfig({
value: {
...JSON.parse(JSON.stringify(props.nodeConfig)),
...{ settype: props.nodeConfig.settype ? props.nodeConfig.settype : 1 }
},
flag: false,
id: _uid
})
} else if (type == 2) {
setCopyer(true)
setCopyerConfig({
value: JSON.parse(JSON.stringify(props.nodeConfig)),
flag: false,
id: _uid
})
} else {
setCondition(true)
setConditionsConfig({
value: JSON.parse(JSON.stringify(props.nodeConfig)),
priorityLevel,
flag: false,
id: _uid
})
}
}
const arrTransfer = (index, type = 1) => {
//向左-1,向右1
// eslint-disable-next-line vue/no-mutating-props
props.nodeConfig.conditionNodes[index] = props.nodeConfig.conditionNodes.splice(
index + type,
1,
props.nodeConfig.conditionNodes[index]
)[0]
props.nodeConfig.conditionNodes.map((item, index) => {
item.priorityLevel = index + 1
})
resetConditionNodesErr()
emits('update:nodeConfig', props.nodeConfig)
}
</script>
@@ -1,165 +0,0 @@
/**
* todo
*/
export const arrToStr = (arr?: [{ name: string }]) => {
if (arr) {
return arr
.map((item) => {
return item.name
})
.toString()
}
}
export const setApproverStr = (nodeConfig: any) => {
if (nodeConfig.settype == 1) {
if (nodeConfig.nodeUserList.length == 1) {
return nodeConfig.nodeUserList[0].name
} else if (nodeConfig.nodeUserList.length > 1) {
if (nodeConfig.examineMode == 1) {
return arrToStr(nodeConfig.nodeUserList)
} else if (nodeConfig.examineMode == 2) {
return nodeConfig.nodeUserList.length + '人会签'
}
}
} else if (nodeConfig.settype == 2) {
const level =
nodeConfig.directorLevel == 1 ? '直接主管' : '第' + nodeConfig.directorLevel + '级主管'
if (nodeConfig.examineMode == 1) {
return level
} else if (nodeConfig.examineMode == 2) {
return level + '会签'
}
} else if (nodeConfig.settype == 4) {
if (nodeConfig.selectRange == 1) {
return '发起人自选'
} else {
if (nodeConfig.nodeUserList.length > 0) {
if (nodeConfig.selectRange == 2) {
return '发起人自选'
} else {
return '发起人从' + nodeConfig.nodeUserList[0].name + '中自选'
}
} else {
return ''
}
}
} else if (nodeConfig.settype == 5) {
return '发起人自己'
} else if (nodeConfig.settype == 7) {
return '从直接主管到通讯录中级别最高的第' + nodeConfig.examineEndDirectorLevel + '个层级主管'
}
}
export const copyerStr = (nodeConfig: any) => {
if (nodeConfig.nodeUserList.length != 0) {
return arrToStr(nodeConfig.nodeUserList)
} else {
if (nodeConfig.ccSelfSelectFlag == 1) {
return '发起人自选'
}
}
}
export const conditionStr = (nodeConfig, index) => {
const { conditionList, nodeUserList } = nodeConfig.conditionNodes[index]
if (conditionList.length == 0) {
return index == nodeConfig.conditionNodes.length - 1 &&
nodeConfig.conditionNodes[0].conditionList.length != 0
? '其他条件进入此流程'
: '请设置条件'
} else {
let str = ''
for (let i = 0; i < conditionList.length; i++) {
const {
columnId,
columnType,
showType,
showName,
optType,
zdy1,
opt1,
zdy2,
opt2,
fixedDownBoxValue
} = conditionList[i]
if (columnId == 0) {
if (nodeUserList.length != 0) {
str += '发起人属于:'
str +=
nodeUserList
.map((item) => {
return item.name
})
.join('或') + ' 并且 '
}
}
if (columnType == 'String' && showType == '3') {
if (zdy1) {
str += showName + '属于:' + dealStr(zdy1, JSON.parse(fixedDownBoxValue)) + ' 并且 '
}
}
if (columnType == 'Double') {
if (optType != 6 && zdy1) {
const optTypeStr = ['', '<', '>', '≤', '=', '≥'][optType]
str += `${showName} ${optTypeStr} ${zdy1} 并且 `
} else if (optType == 6 && zdy1 && zdy2) {
str += `${zdy1} ${opt1} ${showName} ${opt2} ${zdy2} 并且 `
}
}
}
return str ? str.substring(0, str.length - 4) : '请设置条件'
}
}
export const dealStr = (str: string, obj) => {
const arr = []
const list = str.split(',')
for (const elem in obj) {
list.map((item) => {
if (item == elem) {
arr.push(obj[elem].value)
}
})
}
return arr.join('或')
}
export const removeEle = (arr, elem, key = 'id') => {
let includesIndex
arr.map((item, index) => {
if (item[key] == elem[key]) {
includesIndex = index
}
})
arr.splice(includesIndex, 1)
}
export const bgColors = ['87, 106, 149', '255, 148, 62', '50, 150, 250']
export const placeholderList = ['发起人', '审核人', '抄送人']
export const setTypes = [
{ value: 1, label: '指定成员' },
{ value: 2, label: '主管' },
{ value: 4, label: '发起人自选' },
{ value: 5, label: '发起人自己' },
{ value: 7, label: '连续多级主管' }
]
export const selectModes = [
{ value: 1, label: '选一个人' },
{ value: 2, label: '选多个人' }
]
export const selectRanges = [
{ value: 1, label: '全公司' },
{ value: 2, label: '指定成员' },
{ value: 3, label: '指定角色' }
]
export const optTypes = [
{ value: '1', label: '小于' },
{ value: '2', label: '大于' },
{ value: '3', label: '小于等于' },
{ value: '4', label: '等于' },
{ value: '5', label: '大于等于' },
{ value: '6', label: '介于两个数之间' }
]
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,168 @@
<template>
<div class="node-handler-wrapper">
<div class="node-handler" v-if="props.showAdd">
<el-popover
trigger="hover"
v-model:visible="popoverShow"
placement="right-start"
width="auto"
>
<div class="handler-item-wrapper">
<div class="handler-item" @click="addNode(NodeType.USER_TASK_NODE)">
<div class="approve handler-item-icon">
<span class="iconfont icon-approve icon-size"></span>
</div>
<div class="handler-item-text">审批人</div>
</div>
<div class="handler-item" @click="addNode(NodeType.COPY_TASK_NODE)">
<div class="handler-item-icon copy">
<span class="iconfont icon-size icon-copy"></span>
</div>
<div class="handler-item-text">抄送</div>
</div>
<div class="handler-item" @click="addNode(NodeType.CONDITION_BRANCH_NODE)">
<div class="handler-item-icon condition">
<span class="iconfont icon-size icon-exclusive"></span>
</div>
<div class="handler-item-text">条件分支</div>
</div>
<div class="handler-item" @click="addNode(NodeType.PARALLEL_BRANCH_NODE)">
<div class="handler-item-icon condition">
<span class="iconfont icon-size icon-parallel"></span>
</div>
<div class="handler-item-text">并行分支</div>
</div>
</div>
<template #reference>
<div class="add-icon"><Icon icon="ep:plus" /></div>
</template>
</el-popover>
</div>
</div>
</template>
<script setup lang="ts">
import {
ApproveMethodType,
AssignEmptyHandlerType,
AssignStartUserHandlerType,
NODE_DEFAULT_NAME,
NodeType,
RejectHandlerType,
SimpleFlowNode
} from './consts'
import { generateUUID } from '@/utils'
defineOptions({
name: 'NodeHandler'
})
const popoverShow = ref(false)
const props = defineProps({
childNode: {
type: Object as () => SimpleFlowNode,
default: null
},
showAdd: {
// 是否显示添加节点
type: Boolean,
default: true
}
})
const emits = defineEmits(['update:childNode'])
const addNode = (type: number) => {
popoverShow.value = false
if (type === NodeType.USER_TASK_NODE) {
const id = 'Activity_' + generateUUID()
const data: SimpleFlowNode = {
id: id,
name: NODE_DEFAULT_NAME.get(NodeType.USER_TASK_NODE) as string,
showText: '',
type: NodeType.USER_TASK_NODE,
approveMethod: ApproveMethodType.SEQUENTIAL_APPROVE,
// 超时处理
rejectHandler: {
type: RejectHandlerType.FINISH_PROCESS
},
timeoutHandler: {
enable: false
},
assignEmptyHandler: {
type: AssignEmptyHandlerType.APPROVE
},
assignStartUserHandlerType: AssignStartUserHandlerType.START_USER_AUDIT,
childNode: props.childNode
}
emits('update:childNode', data)
}
if (type === NodeType.COPY_TASK_NODE) {
const data: SimpleFlowNode = {
id: 'Activity_' + generateUUID(),
name: NODE_DEFAULT_NAME.get(NodeType.COPY_TASK_NODE) as string,
showText: '',
type: NodeType.COPY_TASK_NODE,
childNode: props.childNode
}
emits('update:childNode', data)
}
if (type === NodeType.CONDITION_BRANCH_NODE) {
const data: SimpleFlowNode = {
name: '条件分支',
type: NodeType.CONDITION_BRANCH_NODE,
id: 'GateWay_' + generateUUID(),
childNode: props.childNode,
conditionNodes: [
{
id: 'Flow_' + generateUUID(),
name: '条件1',
showText: '',
type: NodeType.CONDITION_NODE,
childNode: undefined,
conditionType: 1,
defaultFlow: false
},
{
id: 'Flow_' + generateUUID(),
name: '其它情况',
showText: '其它情况进入此流程',
type: NodeType.CONDITION_NODE,
childNode: undefined,
conditionType: undefined,
defaultFlow: true
}
]
}
emits('update:childNode', data)
}
if (type === NodeType.PARALLEL_BRANCH_NODE) {
const data: SimpleFlowNode = {
name: '并行分支',
type: NodeType.PARALLEL_BRANCH_NODE,
id: 'GateWay_' + generateUUID(),
childNode: props.childNode,
conditionNodes: [
{
id: 'Flow_' + generateUUID(),
name: '并行1',
showText: '无需配置条件同时执行',
type: NodeType.CONDITION_NODE,
childNode: undefined
},
{
id: 'Flow_' + generateUUID(),
name: '并行2',
showText: '无需配置条件同时执行',
type: NodeType.CONDITION_NODE,
childNode: undefined
}
]
}
emits('update:childNode', data)
}
}
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,107 @@
<template>
<!-- 发起人节点 -->
<StartUserNode
v-if="currentNode && currentNode.type === NodeType.START_USER_NODE"
:flow-node="currentNode"
/>
<!-- 审批节点 -->
<UserTaskNode
v-if="currentNode && currentNode.type === NodeType.USER_TASK_NODE"
:flow-node="currentNode"
@update:flow-node="handleModelValueUpdate"
@find:parent-node="findFromParentNode"
/>
<!-- 抄送节点 -->
<CopyTaskNode
v-if="currentNode && currentNode.type === NodeType.COPY_TASK_NODE"
:flow-node="currentNode"
@update:flow-node="handleModelValueUpdate"
/>
<!-- 条件节点 -->
<ExclusiveNode
v-if="currentNode && currentNode.type === NodeType.CONDITION_BRANCH_NODE"
:flow-node="currentNode"
@update:model-value="handleModelValueUpdate"
@find:parent-node="findFromParentNode"
/>
<!-- 并行节点 -->
<ParallelNode
v-if="currentNode && currentNode.type === NodeType.PARALLEL_BRANCH_NODE"
:flow-node="currentNode"
@update:model-value="handleModelValueUpdate"
@find:parent-node="findFromParentNode"
/>
<!-- 递归显示孩子节点 -->
<ProcessNodeTree
v-if="currentNode && currentNode.childNode"
v-model:flow-node="currentNode.childNode"
:parent-node="currentNode"
@find:recursive-find-parent-node="recursiveFindParentNode"
/>
<!-- 结束节点 -->
<EndEventNode v-if="currentNode && currentNode.type === NodeType.END_EVENT_NODE" />
</template>
<script setup lang="ts">
import StartUserNode from './nodes/StartUserNode.vue'
import EndEventNode from './nodes/EndEventNode.vue'
import UserTaskNode from './nodes/UserTaskNode.vue'
import CopyTaskNode from './nodes/CopyTaskNode.vue'
import ExclusiveNode from './nodes/ExclusiveNode.vue'
import ParallelNode from './nodes/ParallelNode.vue'
import { SimpleFlowNode, NodeType } from './consts'
import { useWatchNode } from './node'
defineOptions({
name: 'ProcessNodeTree'
})
const props = defineProps({
parentNode: {
type: Object as () => SimpleFlowNode,
default: () => null
},
flowNode: {
type: Object as () => SimpleFlowNode,
default: () => null
}
})
const emits = defineEmits<{
'update:flowNode': [node: SimpleFlowNode | undefined]
'find:recursiveFindParentNode': [
nodeList: SimpleFlowNode[],
curentNode: SimpleFlowNode,
nodeType: number
]
}>()
const currentNode = useWatchNode(props)
// 用于删除节点
const handleModelValueUpdate = (updateValue) => {
emits('update:flowNode', updateValue)
}
const findFromParentNode = (nodeList: SimpleFlowNode[], nodeType: number) => {
emits('find:recursiveFindParentNode', nodeList, props.parentNode, nodeType)
}
// 递归从父节点中查询匹配的节点
const recursiveFindParentNode = (
nodeList: SimpleFlowNode[],
findNode: SimpleFlowNode,
nodeType: number
) => {
if (!findNode) {
return
}
if (findNode.type === NodeType.START_USER_NODE) {
nodeList.push(findNode)
return
}
if (findNode.type === nodeType) {
nodeList.push(findNode)
}
emits('find:recursiveFindParentNode', nodeList, props.parentNode, nodeType)
}
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,212 @@
<template>
<div class="simple-flow-canvas" v-loading="loading">
<div class="simple-flow-container">
<div class="top-area-container">
<div class="top-actions">
<div class="canvas-control">
<span class="control-scale-group">
<span class="control-scale-button"> <Icon icon="ep:plus" @click="zoomOut()" /></span>
<span class="control-scale-label">{{ scaleValue }}%</span>
<span class="control-scale-button"><Icon icon="ep:minus" @click="zoomIn()" /></span>
</span>
</div>
<el-button type="primary" @click="saveSimpleFlowModel">保存</el-button>
<!-- <el-button type="primary">全局设置</el-button> -->
</div>
</div>
<div class="scale-container" :style="`transform: scale(${scaleValue / 100});`">
<ProcessNodeTree v-if="processNodeTree" v-model:flow-node="processNodeTree" />
</div>
</div>
<Dialog v-model="errorDialogVisible" title="保存失败" width="400" :fullscreen="false">
<div class="mb-2">以下节点内容不完善,请修改后保存</div>
<div
class="mb-3 b-rounded-1 bg-gray-100 p-2 line-height-normal"
v-for="(item, index) in errorNodes"
:key="index"
>
{{ item.name }} : {{ NODE_DEFAULT_TEXT.get(item.type) }}
</div>
<template #footer>
<el-button type="primary" @click="errorDialogVisible = false">知道了</el-button>
</template>
</Dialog>
</div>
</template>
<script setup lang="ts">
import ProcessNodeTree from './ProcessNodeTree.vue'
import { updateBpmSimpleModel, getBpmSimpleModel } from '@/api/bpm/simple'
import { SimpleFlowNode, NodeType, NodeId, NODE_DEFAULT_TEXT } from './consts'
import { getModel } from '@/api/bpm/model'
import { getForm, FormVO } from '@/api/bpm/form'
import { handleTree } from '@/utils/tree'
import * as RoleApi from '@/api/system/role'
import * as DeptApi from '@/api/system/dept'
import * as PostApi from '@/api/system/post'
import * as UserApi from '@/api/system/user'
import * as UserGroupApi from '@/api/bpm/userGroup'
defineOptions({
name: 'SimpleProcessDesigner'
})
const router = useRouter() // 路由
const props = defineProps({
modelId: {
type: String,
required: true
}
})
const loading = ref(true)
const formFields = ref<string[]>([])
const formType = ref(20)
const roleOptions = ref<RoleApi.RoleVO[]>([]) // 角色列表
const postOptions = ref<PostApi.PostVO[]>([]) // 岗位列表
const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
const deptOptions = ref<DeptApi.DeptVO[]>([]) // 部门列表
const deptTreeOptions = ref()
const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]) // 用户组列表
provide('formFields', formFields)
provide('formType', formType)
provide('roleList', roleOptions)
provide('postList', postOptions)
provide('userList', userOptions)
provide('deptList', deptOptions)
provide('userGroupList', userGroupOptions)
provide('deptTree', deptTreeOptions)
const message = useMessage() // 国际化
const processNodeTree = ref<SimpleFlowNode | undefined>()
const errorDialogVisible = ref(false)
let errorNodes: SimpleFlowNode[] = []
const saveSimpleFlowModel = async () => {
if (!props.modelId) {
message.error('缺少模型 modelId 编号')
return
}
errorNodes = []
validateNode(processNodeTree.value, errorNodes)
if (errorNodes.length > 0) {
errorDialogVisible.value = true
return
}
const data = {
id: props.modelId,
simpleModel: processNodeTree.value
}
const result = await updateBpmSimpleModel(data)
if (result) {
message.success('修改成功')
close()
} else {
message.alert('修改失败')
}
}
// 校验节点设置。 暂时以 showText 为空 未节点错误配置
const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNode[]) => {
if (node) {
const { type, showText, conditionNodes } = node
if (type == NodeType.END_EVENT_NODE) {
return
}
if (type == NodeType.START_USER_NODE) {
validateNode(node.childNode, errorNodes)
}
if (type === NodeType.USER_TASK_NODE) {
if (!showText) {
errorNodes.push(node)
}
validateNode(node.childNode, errorNodes)
}
if (type === NodeType.COPY_TASK_NODE) {
if (!showText) {
errorNodes.push(node)
}
validateNode(node.childNode, errorNodes)
}
if (type === NodeType.CONDITION_NODE) {
if (!showText) {
errorNodes.push(node)
}
validateNode(node.childNode, errorNodes)
}
if (type == NodeType.CONDITION_BRANCH_NODE) {
conditionNodes?.forEach((item) => {
validateNode(item, errorNodes)
})
validateNode(node.childNode, errorNodes)
}
}
}
const close = () => {
router.push({ path: '/bpm/manager/model' })
}
let scaleValue = ref(100)
const MAX_SCALE_VALUE = 200
const MIN_SCALE_VALUE = 50
// 放大
const zoomOut = () => {
if (scaleValue.value == MAX_SCALE_VALUE) {
return
}
scaleValue.value += 10
}
// 缩小
const zoomIn = () => {
if (scaleValue.value == MIN_SCALE_VALUE) {
return
}
scaleValue.value -= 10
}
onMounted(async () => {
try {
loading.value = true
// 获取表单字段
const bpmnModel = await getModel(props.modelId)
if (bpmnModel) {
formType.value = bpmnModel.formType
if (formType.value === 10) {
const bpmnForm = (await getForm(bpmnModel.formId)) as unknown as FormVO
formFields.value = bpmnForm?.fields
}
}
// 获得角色列表
roleOptions.value = await RoleApi.getSimpleRoleList()
// 获得岗位列表
postOptions.value = await PostApi.getSimplePostList()
// 获得用户列表
userOptions.value = await UserApi.getSimpleUserList()
// 获得部门列表
deptOptions.value = await DeptApi.getSimpleDeptList()
deptTreeOptions.value = handleTree(deptOptions.value as DeptApi.DeptVO[], 'id')
// 获取用户组列表
userGroupOptions.value = await UserGroupApi.getUserGroupSimpleList()
// 获取 SIMPLE 设计器模型
const result = await getBpmSimpleModel(props.modelId)
if (result) {
processNodeTree.value = result
} else {
// 初始值
processNodeTree.value = {
name: '发起人',
type: NodeType.START_USER_NODE,
id: NodeId.START_USER_NODE_ID,
childNode: {
id: NodeId.END_EVENT_NODE_ID,
name: '结束',
type: NodeType.END_EVENT_NODE
}
}
}
} finally {
loading.value = false
}
})
</script>
@@ -0,0 +1,544 @@
// @ts-ignore
import { DictDataVO } from '@/api/system/dict/types'
/**
* 节点类型
*/
export enum NodeType {
/**
* 结束节点
*/
END_EVENT_NODE = 1,
/**
* 发起人节点
*/
START_USER_NODE = 10,
/**
* 审批人节点
*/
USER_TASK_NODE = 11,
/**
* 抄送人节点
*/
COPY_TASK_NODE = 12,
/**
* 条件节点
*/
CONDITION_NODE = 50,
/**
* 条件分支节点 (对应排他网关)
*/
CONDITION_BRANCH_NODE = 51,
/**
* 并行分支节点 (对应并行网关)
*/
PARALLEL_BRANCH_NODE = 52,
/**
* 包容分支节点 (对应包容网关)
*/
INCLUSIVE_BRANCH_NODE = 53
}
export enum NodeId {
/**
* 发起人节点 Id
*/
START_USER_NODE_ID = 'StartUserNode',
/**
* 发起人节点 Id
*/
END_EVENT_NODE_ID = 'EndEvent'
}
/**
* 节点结构定义
*/
export interface SimpleFlowNode {
id: string
type: NodeType
name: string
showText?: string
// 孩子节点
childNode?: SimpleFlowNode
// 条件节点
conditionNodes?: SimpleFlowNode[]
// 审批类型
approveType?: ApproveType
// 候选人策略
candidateStrategy?: number
// 候选人参数
candidateParam?: string
// 多人审批方式
approveMethod?: ApproveMethodType
//通过比例
approveRatio?: number
// 审批按钮设置
buttonsSetting?: any[]
// 表单权限
fieldsPermission?: Array<Record<string, string>>
// 审批任务超时处理
timeoutHandler?: TimeoutHandler
// 审批任务拒绝处理
rejectHandler?: RejectHandler
// 审批人为空的处理
assignEmptyHandler?: AssignEmptyHandler
// 审批节点的审批人与发起人相同时,对应的处理类型
assignStartUserHandlerType?: number
// 条件类型
conditionType?: ConditionType
// 条件表达式
conditionExpression?: string
// 条件组
conditionGroups?: ConditionGroup
// 是否默认的条件
defaultFlow?: boolean
}
// 候选人策略枚举 ( 用于审批节点。抄送节点 )
export enum CandidateStrategy {
/**
* 指定角色
*/
ROLE = 10,
/**
* 部门成员
*/
DEPT_MEMBER = 20,
/**
* 部门的负责人
*/
DEPT_LEADER = 21,
/**
* 连续多级部门的负责人
*/
MULTI_LEVEL_DEPT_LEADER = 23,
/**
* 指定岗位
*/
POST = 22,
/**
* 指定用户
*/
USER = 30,
/**
* 发起人自选
*/
START_USER_SELECT = 35,
/**
* 发起人自己
*/
START_USER = 36,
/**
* 发起人部门负责人
*/
START_USER_DEPT_LEADER = 37,
/**
* 发起人连续多级部门的负责人
*/
START_USER_MULTI_LEVEL_DEPT_LEADER = 38,
/**
* 指定用户组
*/
USER_GROUP = 40,
/**
* 流程表达式
*/
EXPRESSION = 60
}
// 多人审批方式类型枚举 ( 用于审批节点 )
export enum ApproveMethodType {
/**
* 随机挑选一人审批
*/
RANDOM_SELECT_ONE_APPROVE = 1,
/**
* 多人会签(按通过比例)
*/
APPROVE_BY_RATIO = 2,
/**
* 多人或签(通过只需一人,拒绝只需一人)
*/
ANY_APPROVE = 3,
/**
* 多人依次审批
*/
SEQUENTIAL_APPROVE = 4
}
/**
* 审批拒绝结构定义
*/
export type RejectHandler = {
// 审批拒绝类型
type: RejectHandlerType
// 回退节点 Id
returnNodeId?: string
}
/**
* 审批超时结构定义
*/
export type TimeoutHandler = {
// 是否开启超时处理
enable: boolean
// 超时执行的动作
type?: number
// 超时时间设置
timeDuration?: string
// 执行动作是自动提醒, 最大提醒次数
maxRemindCount?: number
}
/**
* 审批人为空的结构定义
*/
export type AssignEmptyHandler = {
// 审批人为空的处理类型
type: AssignEmptyHandlerType
// 指定用户的编号数组
userIds?: number[]
}
// 审批拒绝类型枚举
export enum RejectHandlerType {
/**
* 结束流程
*/
FINISH_PROCESS = 1,
/**
* 驳回到指定节点
*/
RETURN_USER_TASK = 2
}
// 用户任务超时处理类型枚举
export enum TimeoutHandlerType {
/**
* 自动提醒
*/
REMINDER = 1,
/**
* 自动同意
*/
APPROVE = 2,
/**
* 自动拒绝
*/
REJECT = 3
}
// 用户任务的审批人为空时,处理类型枚举
export enum AssignEmptyHandlerType {
/**
* 自动通过
*/
APPROVE = 1,
/**
* 自动拒绝
*/
REJECT = 2,
/**
* 指定人员审批
*/
ASSIGN_USER,
/**
* 转交给流程管理员
*/
ASSIGN_ADMIN = 4
}
// 用户任务的审批人与发起人相同时,处理类型枚举
export enum AssignStartUserHandlerType {
/**
* 由发起人对自己审批
*/
START_USER_AUDIT = 1,
/**
* 自动跳过【参考飞书】:1)如果当前节点还有其他审批人,则交由其他审批人进行审批;2)如果当前节点没有其他审批人,则该节点自动通过
*/
SKIP = 2,
/**
* 转交给部门负责人审批
*/
ASSIGN_DEPT_LEADER = 3
}
// 用户任务的审批类型。 【参考飞书】
export enum ApproveType {
/**
* 人工审批
*/
USER = 1,
/**
* 自动通过
*/
AUTO_APPROVE = 2,
/**
* 自动拒绝
*/
AUTO_REJECT = 3
}
// 时间单位枚举
export enum TimeUnitType {
/**
* 分钟
*/
MINUTE = 1,
/**
* 小时
*/
HOUR = 2,
/**
* 天
*/
DAY = 3
}
// 条件配置类型 ( 用于条件节点配置 )
export enum ConditionType {
/**
* 条件表达式
*/
EXPRESSION = 1,
/**
* 条件规则
*/
RULE = 2
}
/**
* 表单权限的枚举
*/
export enum FieldPermissionType {
/**
* 只读
*/
READ = '1',
/**
* 编辑
*/
WRITE = '2',
/**
* 隐藏
*/
NONE = '3'
}
/**
* 操作按钮权限结构定义
*/
export type ButtonSetting = {
id: OperationButtonType
displayName: string
enable: boolean
}
// 操作按钮类型枚举 (用于审批节点)
export enum OperationButtonType {
/**
* 通过
*/
APPROVE = 1,
/**
* 拒绝
*/
REJECT = 2,
/**
* 转办
*/
TRANSFER = 3,
/**
* 委派
*/
DELEGATE = 4,
/**
* 加签
*/
ADD_SIGN = 5,
/**
* 回退
*/
RETURN = 6
}
/**
* 条件规则结构定义
*/
export type ConditionRule = {
type: number
opName: string
opCode: string
leftSide: string
rightSide: string
}
/**
* 条件组结构定义
*/
export type ConditionGroup = {
// 条件组的逻辑关系是否为且
and: boolean
// 条件数组
conditions: Condition[]
}
/**
* 条件结构定义
*/
export type Condition = {
// 条件规则的逻辑关系是否为且
and: boolean
rules: ConditionRule[]
}
export const NODE_DEFAULT_TEXT = new Map<number, string>()
NODE_DEFAULT_TEXT.set(NodeType.USER_TASK_NODE, '请配置审批人')
NODE_DEFAULT_TEXT.set(NodeType.COPY_TASK_NODE, '请配置抄送人')
NODE_DEFAULT_TEXT.set(NodeType.CONDITION_NODE, '请设置条件')
NODE_DEFAULT_TEXT.set(NodeType.START_USER_NODE, '请设置发起人')
export const NODE_DEFAULT_NAME = new Map<number, string>()
NODE_DEFAULT_NAME.set(NodeType.USER_TASK_NODE, '审批人')
NODE_DEFAULT_NAME.set(NodeType.COPY_TASK_NODE, '抄送人')
NODE_DEFAULT_NAME.set(NodeType.CONDITION_NODE, '条件')
NODE_DEFAULT_NAME.set(NodeType.START_USER_NODE, '发起人')
// 候选人策略。暂时不从字典中取。 后续可能调整。控制显示顺序
export const CANDIDATE_STRATEGY: DictDataVO[] = [
{ label: '指定成员', value: CandidateStrategy.USER },
{ label: '指定角色', value: CandidateStrategy.ROLE },
{ label: '部门成员', value: CandidateStrategy.DEPT_MEMBER },
{ label: '部门负责人', value: CandidateStrategy.DEPT_LEADER },
{ label: '连续多级部门负责人', value: CandidateStrategy.MULTI_LEVEL_DEPT_LEADER },
{ label: '发起人自选', value: CandidateStrategy.START_USER_SELECT },
{ label: '发起人本人', value: CandidateStrategy.START_USER },
{ label: '发起人部门负责人', value: CandidateStrategy.START_USER_DEPT_LEADER },
{ label: '发起人连续部门负责人', value: CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER },
{ label: '用户组', value: CandidateStrategy.USER_GROUP },
{ label: '流程表达式', value: CandidateStrategy.EXPRESSION }
]
// 审批节点 的审批类型
export const APPROVE_TYPE: DictDataVO[] = [
{ label: '人工审批', value: ApproveType.USER },
{ label: '自动通过', value: ApproveType.AUTO_APPROVE },
{ label: '自动拒绝', value: ApproveType.AUTO_REJECT }
]
export const APPROVE_METHODS: DictDataVO[] = [
{ label: '按顺序依次审批', value: ApproveMethodType.SEQUENTIAL_APPROVE },
{ label: '会签(可同时审批,至少 % 人必须审批通过)', value: ApproveMethodType.APPROVE_BY_RATIO },
{ label: '或签(可同时审批,有一人通过即可)', value: ApproveMethodType.ANY_APPROVE },
{ label: '随机挑选一人审批', value: ApproveMethodType.RANDOM_SELECT_ONE_APPROVE }
]
export const CONDITION_CONFIG_TYPES: DictDataVO[] = [
{ label: '条件表达式', value: ConditionType.EXPRESSION },
{ label: '条件规则', value: ConditionType.RULE }
]
// 时间单位类型
export const TIME_UNIT_TYPES: DictDataVO[] = [
{ label: '分钟', value: TimeUnitType.MINUTE },
{ label: '小时', value: TimeUnitType.HOUR },
{ label: '天', value: TimeUnitType.DAY }
]
// 超时处理执行动作类型
export const TIMEOUT_HANDLER_TYPES: DictDataVO[] = [
{ label: '自动提醒', value: 1 },
{ label: '自动同意', value: 2 },
{ label: '自动拒绝', value: 3 }
]
export const REJECT_HANDLER_TYPES: DictDataVO[] = [
{ label: '终止流程', value: RejectHandlerType.FINISH_PROCESS },
{ label: '驳回到指定节点', value: RejectHandlerType.RETURN_USER_TASK }
// { label: '结束任务', value: RejectHandlerType.FINISH_TASK }
]
export const ASSIGN_EMPTY_HANDLER_TYPES: DictDataVO[] = [
{ label: '自动通过', value: 1 },
{ label: '自动拒绝', value: 2 },
{ label: '指定成员审批', value: 3 },
{ label: '转交给流程管理员', value: 4 }
]
export const ASSIGN_START_USER_HANDLER_TYPES: DictDataVO[] = [
{ label: '由发起人对自己审批', value: 1 },
{ label: '自动跳过', value: 2 },
{ label: '转交给部门负责人审批', value: 3 }
]
// 比较运算符
export const COMPARISON_OPERATORS: DictDataVO = [
{
value: '==',
label: '等于'
},
{
value: '!=',
label: '不等于'
},
{
value: '>',
label: '大于'
},
{
value: '>=',
label: '大于等于'
},
{
value: '<',
label: '小于'
},
{
value: '<=',
label: '小于等于'
}
]
// 审批操作按钮名称
export const OPERATION_BUTTON_NAME = new Map<number, string>()
OPERATION_BUTTON_NAME.set(OperationButtonType.APPROVE, '通过')
OPERATION_BUTTON_NAME.set(OperationButtonType.REJECT, '拒绝')
OPERATION_BUTTON_NAME.set(OperationButtonType.TRANSFER, '转办')
OPERATION_BUTTON_NAME.set(OperationButtonType.DELEGATE, '委派')
OPERATION_BUTTON_NAME.set(OperationButtonType.ADD_SIGN, '加签')
OPERATION_BUTTON_NAME.set(OperationButtonType.RETURN, '回退')
// 默认的按钮权限设置
export const DEFAULT_BUTTON_SETTING: ButtonSetting[] = [
{ id: OperationButtonType.APPROVE, displayName: '通过', enable: true },
{ id: OperationButtonType.REJECT, displayName: '拒绝', enable: true },
{ id: OperationButtonType.TRANSFER, displayName: '转办', enable: false },
{ id: OperationButtonType.DELEGATE, displayName: '委派', enable: false },
{ id: OperationButtonType.ADD_SIGN, displayName: '加签', enable: false },
{ id: OperationButtonType.RETURN, displayName: '回退', enable: false }
]
// 发起人的按钮权限。暂时定死,不可以编辑
export const START_USER_BUTTON_SETTING: ButtonSetting[] = [
{ id: OperationButtonType.APPROVE, displayName: '提交', enable: true },
{ id: OperationButtonType.REJECT, displayName: '拒绝', enable: false },
{ id: OperationButtonType.TRANSFER, displayName: '转办', enable: false },
{ id: OperationButtonType.DELEGATE, displayName: '委派', enable: false },
{ id: OperationButtonType.ADD_SIGN, displayName: '加签', enable: false },
{ id: OperationButtonType.RETURN, displayName: '回退', enable: false }
]
export const MULTI_LEVEL_DEPT: DictDataVO = [
{ label: '第 1 级部门', value: 1 },
{ label: '第 2 级部门', value: 2 },
{ label: '第 3 级部门', value: 3 },
{ label: '第 4 级部门', value: 4 },
{ label: '第 5 级部门', value: 5 },
{ label: '第 6 级部门', value: 6 },
{ label: '第 7 级部门', value: 7 },
{ label: '第 8 级部门', value: 8 },
{ label: '第 9 级部门', value: 9 },
{ label: '第 10 级部门', value: 10 },
{ label: '第 11 级部门', value: 11 },
{ label: '第 12 级部门', value: 12 },
{ label: '第 13 级部门', value: 13 },
{ label: '第 14 级部门', value: 14 },
{ label: '第 15 级部门', value: 15 }
]
@@ -0,0 +1,4 @@
import SimpleProcessDesigner from './SimpleProcessDesigner.vue'
import '../theme/simple-process-designer.scss'
export { SimpleProcessDesigner }
@@ -0,0 +1,478 @@
import { cloneDeep } from 'lodash-es'
import * as RoleApi from '@/api/system/role'
import * as DeptApi from '@/api/system/dept'
import * as PostApi from '@/api/system/post'
import * as UserApi from '@/api/system/user'
import * as UserGroupApi from '@/api/bpm/userGroup'
import {
SimpleFlowNode,
CandidateStrategy,
NodeType,
ApproveMethodType,
RejectHandlerType,
NODE_DEFAULT_NAME,
AssignStartUserHandlerType,
AssignEmptyHandlerType,
FieldPermissionType
} from './consts'
export function useWatchNode(props: { flowNode: SimpleFlowNode }): Ref<SimpleFlowNode> {
const node = ref<SimpleFlowNode>(props.flowNode)
watch(
() => props.flowNode,
(newValue) => {
node.value = newValue
}
)
return node
}
/**
* @description 表单数据权限配置,用于发起人节点 、审批节点、抄送节点
*/
export function useFormFieldsPermission(defaultPermission: FieldPermissionType) {
// 字段权限配置. 需要有 field, title, permissioin 属性
const fieldsPermissionConfig = ref<Array<Record<string, string>>>([])
const formType = inject<Ref<number>>('formType') // 表单类型
const formFields = inject<Ref<string[]>>('formFields') // 流程表单字段
const getNodeConfigFormFields = (nodeFormFields?: Array<Record<string, string>>) => {
nodeFormFields = toRaw(nodeFormFields)
fieldsPermissionConfig.value =
cloneDeep(nodeFormFields) || getDefaultFieldsPermission(unref(formFields))
}
// 默认的表单权限: 获取表单的所有字段,设置字段默认权限为只读
const getDefaultFieldsPermission = (formFields?: string[]) => {
const defaultFieldsPermission: Array<Record<string, string>> = []
if (formFields) {
formFields.forEach((fieldStr: string) => {
parseFieldsSetDefaultPermission(JSON.parse(fieldStr), defaultFieldsPermission)
})
}
return defaultFieldsPermission
}
// 解析字段。赋给默认权限
const parseFieldsSetDefaultPermission = (
rule: Record<string, any>,
fieldsPermission: Array<Record<string, string>>,
parentTitle: string = ''
) => {
const { /**type,*/ field, title: tempTitle, children } = rule
if (field && tempTitle) {
let title = tempTitle
if (parentTitle) {
title = `${parentTitle}.${tempTitle}`
}
fieldsPermission.push({
field,
title,
permission: defaultPermission
})
// TODO 子表单 需要处理子表单字段
// if (type === 'group' && rule.props?.rule && Array.isArray(rule.props.rule)) {
// // 解析子表单的字段
// rule.props.rule.forEach((item) => {
// parseFieldsSetDefaultPermission(item, fieldsPermission, title)
// })
// }
}
if (children && Array.isArray(children)) {
children.forEach((rule) => {
parseFieldsSetDefaultPermission(rule, fieldsPermission)
})
}
}
return {
formType,
fieldsPermissionConfig,
getNodeConfigFormFields
}
}
/**
* @description 获取表单的字段
*/
export function useFormFields() {
// 解析后的表单字段
const formFields = inject<Ref<string[]>>('formFields') // 流程表单字段
const parseFormFields = () => {
const parsedFormFields: Array<Record<string, string>> = []
if (formFields) {
formFields.value.forEach((fieldStr: string) => {
parseField(JSON.parse(fieldStr), parsedFormFields)
})
}
return parsedFormFields
}
// 解析字段。
const parseField = (
rule: Record<string, any>,
parsedFormFields: Array<Record<string, string>>,
parentTitle: string = ''
) => {
const { field, title: tempTitle, children, type } = rule
if (field && tempTitle) {
let title = tempTitle
if (parentTitle) {
title = `${parentTitle}.${tempTitle}`
}
parsedFormFields.push({
field,
title,
type
})
// TODO 子表单 需要处理子表单字段
// if (type === 'group' && rule.props?.rule && Array.isArray(rule.props.rule)) {
// // 解析子表单的字段
// rule.props.rule.forEach((item) => {
// parseFieldsSetDefaultPermission(item, fieldsPermission, title)
// })
// }
}
if (children && Array.isArray(children)) {
children.forEach((rule) => {
parseField(rule, parsedFormFields)
})
}
}
return parseFormFields()
}
export type UserTaskFormType = {
//candidateParamArray: any[]
candidateStrategy: CandidateStrategy
approveMethod: ApproveMethodType
roleIds?: number[] // 角色
deptIds?: number[] // 部门
deptLevel?: number // 部门层级
userIds?: number[] // 用户
userGroups?: number[] // 用户组
postIds?: number[] // 岗位
expression?: string // 流程表达式
approveRatio?: number
rejectHandlerType?: RejectHandlerType
returnNodeId?: string
timeoutHandlerEnable?: boolean
timeoutHandlerType?: number
assignEmptyHandlerType?: AssignEmptyHandlerType
assignEmptyHandlerUserIds?: number[]
assignStartUserHandlerType?: AssignStartUserHandlerType
timeDuration?: number
maxRemindCount?: number
buttonsSetting: any[]
}
export type CopyTaskFormType = {
// candidateParamArray: any[]
candidateStrategy: CandidateStrategy
roleIds?: number[] // 角色
deptIds?: number[] // 部门
deptLevel?: number // 部门层级
userIds?: number[] // 用户
userGroups?: number[] // 用户组
postIds?: number[] // 岗位
expression?: string // 流程表达式
}
/**
* @description 节点表单数据。 用于审批节点、抄送节点
*/
export function useNodeForm(nodeType: NodeType) {
const roleOptions = inject<Ref<RoleApi.RoleVO[]>>('roleList') // 角色列表
const postOptions = inject<Ref<PostApi.PostVO[]>>('postList') // 岗位列表
const userOptions = inject<Ref<UserApi.UserVO[]>>('userList') // 用户列表
const deptOptions = inject<Ref<DeptApi.DeptVO[]>>('deptList') // 部门列表
const userGroupOptions = inject<Ref<UserGroupApi.UserGroupVO[]>>('userGroupList') // 用户组列表
const deptTreeOptions = inject('deptTree') // 部门树
const configForm = ref<UserTaskFormType | CopyTaskFormType>()
if (nodeType === NodeType.USER_TASK_NODE) {
configForm.value = {
candidateStrategy: CandidateStrategy.USER,
approveMethod: ApproveMethodType.SEQUENTIAL_APPROVE,
approveRatio: 100,
rejectHandlerType: RejectHandlerType.FINISH_PROCESS,
assignStartUserHandlerType: AssignStartUserHandlerType.START_USER_AUDIT,
returnNodeId: '',
timeoutHandlerEnable: false,
timeoutHandlerType: 1,
timeDuration: 6, // 默认 6小时
maxRemindCount: 1, // 默认 提醒 1次
buttonsSetting: []
}
} else {
configForm.value = {
candidateStrategy: CandidateStrategy.USER
}
}
const getShowText = (): string => {
let showText = ''
// 指定成员
if (configForm.value?.candidateStrategy === CandidateStrategy.USER) {
if (configForm.value?.userIds!.length > 0) {
const candidateNames: string[] = []
userOptions?.value.forEach((item) => {
if (configForm.value?.userIds!.includes(item.id)) {
candidateNames.push(item.nickname)
}
})
showText = `指定成员:${candidateNames.join(',')}`
}
}
// 指定角色
if (configForm.value?.candidateStrategy === CandidateStrategy.ROLE) {
if (configForm.value.roleIds!.length > 0) {
const candidateNames: string[] = []
roleOptions?.value.forEach((item) => {
if (configForm.value?.roleIds!.includes(item.id)) {
candidateNames.push(item.name)
}
})
showText = `指定角色:${candidateNames.join(',')}`
}
}
// 指定部门
if (
configForm.value?.candidateStrategy === CandidateStrategy.DEPT_MEMBER ||
configForm.value?.candidateStrategy === CandidateStrategy.DEPT_LEADER ||
configForm.value?.candidateStrategy === CandidateStrategy.MULTI_LEVEL_DEPT_LEADER
) {
if (configForm.value?.deptIds!.length > 0) {
const candidateNames: string[] = []
deptOptions?.value.forEach((item) => {
if (configForm.value?.deptIds!.includes(item.id!)) {
candidateNames.push(item.name)
}
})
if (configForm.value.candidateStrategy === CandidateStrategy.DEPT_MEMBER) {
showText = `部门成员:${candidateNames.join(',')}`
} else if (configForm.value.candidateStrategy === CandidateStrategy.DEPT_LEADER) {
showText = `部门的负责人:${candidateNames.join(',')}`
} else {
showText = `多级部门的负责人:${candidateNames.join(',')}`
}
}
}
// 指定岗位
if (configForm.value?.candidateStrategy === CandidateStrategy.POST) {
if (configForm.value.postIds!.length > 0) {
const candidateNames: string[] = []
postOptions?.value.forEach((item) => {
if (configForm.value?.postIds!.includes(item.id!)) {
candidateNames.push(item.name)
}
})
showText = `指定岗位: ${candidateNames.join(',')}`
}
}
// 指定用户组
if (configForm.value?.candidateStrategy === CandidateStrategy.USER_GROUP) {
if (configForm.value?.userGroups!.length > 0) {
const candidateNames: string[] = []
userGroupOptions?.value.forEach((item) => {
if (configForm.value?.userGroups!.includes(item.id)) {
candidateNames.push(item.name)
}
})
showText = `指定用户组: ${candidateNames.join(',')}`
}
}
// 发起人自选
if (configForm.value?.candidateStrategy === CandidateStrategy.START_USER_SELECT) {
showText = `发起人自选`
}
// 发起人自己
if (configForm.value?.candidateStrategy === CandidateStrategy.START_USER) {
showText = `发起人自己`
}
// 发起人的部门负责人
if (configForm.value?.candidateStrategy === CandidateStrategy.START_USER_DEPT_LEADER) {
showText = `发起人的部门负责人`
}
// 发起人的部门负责人
if (
configForm.value?.candidateStrategy === CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER
) {
showText = `发起人连续部门负责人`
}
// 流程表达式
if (configForm.value?.candidateStrategy === CandidateStrategy.EXPRESSION) {
showText = `流程表达式:${configForm.value.expression}`
}
return showText
}
/**
* 处理候选人参数的赋值
*/
const handleCandidateParam = () => {
let candidateParam: undefined | string = undefined
if (!configForm.value) {
return candidateParam
}
switch (configForm.value.candidateStrategy) {
case CandidateStrategy.USER:
candidateParam = configForm.value.userIds!.join(',')
break
case CandidateStrategy.ROLE:
candidateParam = configForm.value.roleIds!.join(',')
break
case CandidateStrategy.POST:
candidateParam = configForm.value.postIds!.join(',')
break
case CandidateStrategy.USER_GROUP:
candidateParam = configForm.value.userGroups!.join(',')
break
case CandidateStrategy.EXPRESSION:
candidateParam = configForm.value.expression!
break
case CandidateStrategy.DEPT_MEMBER:
case CandidateStrategy.DEPT_LEADER:
candidateParam = configForm.value.deptIds!.join(',')
break
// 发起人部门负责人
case CandidateStrategy.START_USER_DEPT_LEADER:
case CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER:
candidateParam = configForm.value.deptLevel + ''
break
// 指定连续多级部门的负责人
case CandidateStrategy.MULTI_LEVEL_DEPT_LEADER: {
// 候选人参数格式: | 分隔 。左边为部门(多个部门用 , 分隔)。 右边为部门层级
const deptIds = configForm.value.deptIds!.join(',')
candidateParam = deptIds.concat('|' + configForm.value.deptLevel + '')
break
}
default:
break
}
return candidateParam
}
/**
* 解析候选人参数
*/
const parseCandidateParam = (
candidateStrategy: CandidateStrategy,
candidateParam: string | undefined
) => {
if (!configForm.value || !candidateParam) {
return
}
switch (candidateStrategy) {
case CandidateStrategy.USER: {
configForm.value.userIds = candidateParam.split(',').map((item) => +item)
break
}
case CandidateStrategy.ROLE:
configForm.value.roleIds = candidateParam.split(',').map((item) => +item)
break
case CandidateStrategy.POST:
configForm.value.postIds = candidateParam.split(',').map((item) => +item)
break
case CandidateStrategy.USER_GROUP:
configForm.value.userGroups = candidateParam.split(',').map((item) => +item)
break
case CandidateStrategy.EXPRESSION:
configForm.value.expression = candidateParam
break
case CandidateStrategy.DEPT_MEMBER:
case CandidateStrategy.DEPT_LEADER:
configForm.value.deptIds = candidateParam.split(',').map((item) => +item)
break
// 发起人部门负责人
case CandidateStrategy.START_USER_DEPT_LEADER:
case CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER:
configForm.value.deptLevel = +candidateParam
break
// 指定连续多级部门的负责人
case CandidateStrategy.MULTI_LEVEL_DEPT_LEADER: {
// 候选人参数格式: | 分隔 。左边为部门(多个部门用 , 分隔)。 右边为部门层级
const paramArray = candidateParam.split('|')
configForm.value.deptIds = paramArray[0].split(',').map((item) => +item)
configForm.value.deptLevel = +paramArray[1]
break
}
default:
break
}
}
return {
configForm,
roleOptions,
postOptions,
userOptions,
userGroupOptions,
deptTreeOptions,
handleCandidateParam,
parseCandidateParam,
getShowText
}
}
/**
* @description 抽屉配置
*/
export function useDrawer() {
// 抽屉配置是否可见
const settingVisible = ref(false)
// 关闭配置抽屉
const closeDrawer = () => {
settingVisible.value = false
}
// 打开配置抽屉
const openDrawer = () => {
settingVisible.value = true
}
return {
settingVisible,
closeDrawer,
openDrawer
}
}
/**
* @description 节点名称配置
*/
export function useNodeName(nodeType: NodeType) {
// 节点名称
const nodeName = ref<string>()
// 节点名称输入框
const showInput = ref(false)
// 点击节点名称编辑图标
const clickIcon = () => {
showInput.value = true
}
// 节点名称输入框失去焦点
const blurEvent = () => {
showInput.value = false
nodeName.value = nodeName.value || (NODE_DEFAULT_NAME.get(nodeType) as string)
}
return {
nodeName,
showInput,
clickIcon,
blurEvent
}
}
export function useNodeName2(node: Ref<SimpleFlowNode>, nodeType: NodeType) {
// 显示节点名称输入框
const showInput = ref(false)
// 节点名称输入框失去焦点
const blurEvent = () => {
showInput.value = false
node.value.name = node.value.name || (NODE_DEFAULT_NAME.get(nodeType) as string)
}
// 点击节点标题进行输入
const clickTitle = () => {
showInput.value = true
}
return {
showInput,
clickTitle,
blurEvent
}
}
@@ -0,0 +1,419 @@
<template>
<el-drawer
:append-to-body="true"
v-model="settingVisible"
:show-close="false"
:size="588"
:before-close="handleClose"
>
<template #header>
<div class="config-header">
<input
v-if="showInput"
type="text"
class="config-editable-input"
@blur="blurEvent()"
v-mountedFocus
v-model="currentNode.name"
:placeholder="currentNode.name"
/>
<div v-else class="node-name"
>{{ currentNode.name }}
<Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()"
/></div>
<div class="divide-line"></div>
</div>
</template>
<div>
<div class="mb-3 font-size-16px" v-if="currentNode.defaultFlow">其它条件不满足进入此分支(该分支不可编辑和删除)</div>
<div v-else>
<el-form
ref="formRef"
:model="currentNode"
:rules="formRules"
label-position="top"
>
<el-form-item label="配置方式" prop="conditionType">
<el-radio-group
v-model="currentNode.conditionType"
@change="changeConditionType"
>
<el-radio
v-for="(dict, index) in conditionConfigTypes"
:key="index"
:value="dict.value"
:label="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
v-if="currentNode.conditionType === 1"
label="条件表达式"
prop="conditionExpression"
>
<el-input
type="textarea"
v-model="currentNode.conditionExpression"
clearable
style="width: 100%"
/>
</el-form-item>
<el-form-item v-if="currentNode.conditionType === 2" label="条件规则">
<div class="condition-group-tool">
<div class="flex items-center">
<div class="mr-4">条件组关系</div>
<el-switch
v-model="conditionGroups.and"
inline-prompt
active-text="且"
inactive-text="或"
/>
</div>
</div>
<el-space direction="vertical" :spacer="conditionGroups.and ? '且' : '或'">
<el-card
class="condition-group"
style="width: 530px"
v-for="(condition, cIdx) in conditionGroups.conditions"
:key="cIdx"
>
<div class="condition-group-delete" v-if="conditionGroups.conditions.length > 1">
<Icon
color="#0089ff"
icon="ep:circle-close-filled"
:size="18"
@click="deleteConditionGroup(cIdx)"
/>
</div>
<template #header>
<div class="flex items-center justify-between">
<div>条件组</div>
<div class="flex">
<div class="mr-4">规则关系</div>
<el-switch
v-model="condition.and"
inline-prompt
active-text="且"
inactive-text="或"
/>
</div>
</div>
</template>
<div class="flex pt-2" v-for="(rule, rIdx) in condition.rules" :key="rIdx">
<div class="mr-2">
<el-select style="width: 160px" v-model="rule.leftSide">
<el-option
v-for="(item, index) in fieldsInfo"
:key="index"
:label="item.title"
:value="item.field"
/>
</el-select>
</div>
<div class="mr-2">
<el-select v-model="rule.opCode" style="width: 100px">
<el-option
v-for="item in COMPARISON_OPERATORS"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</div>
<div class="mr-2">
<el-input v-model="rule.rightSide" style="width: 160px" />
</div>
<div class="mr-1 flex items-center" v-if="condition.rules.length > 1">
<Icon
icon="ep:delete"
:size="18"
@click="deleteConditionRule(condition, rIdx)"
/>
</div>
<div class="flex items-center">
<Icon icon="ep:plus" :size="18" @click="addConditionRule(condition, rIdx)" />
</div>
</div>
</el-card>
</el-space>
<div title="添加条件组" class="mt-4 cursor-pointer">
<Icon color="#0089ff" icon="ep:plus" :size="24" @click="addConditionGroup" />
</div>
</el-form-item>
</el-form>
</div>
</div>
<template #footer>
<el-divider />
<div>
<el-button type="primary" @click="saveConfig">确 定</el-button>
<el-button @click="closeDrawer">取 消</el-button>
</div>
</template>
</el-drawer>
</template>
<script setup lang="ts">
import {
SimpleFlowNode,
CONDITION_CONFIG_TYPES,
ConditionType,
COMPARISON_OPERATORS,
ConditionGroup,
Condition,
ConditionRule
} from '../consts'
import { getDefaultConditionNodeName } from '../utils'
import { useFormFields } from '../node'
const message = useMessage() // 消息弹窗
defineOptions({
name: 'ConditionNodeConfig'
})
const formType = inject<Ref<number>>('formType') // 表单类型
const conditionConfigTypes = computed(() => {
return CONDITION_CONFIG_TYPES.filter((item) => {
// 业务表单暂时去掉条件规则选项
if (formType?.value !== 10) {
return item.value === ConditionType.RULE
} else {
return true
}
})
})
const props = defineProps({
conditionNode: {
type: Object as () => SimpleFlowNode,
required: true
},
nodeIndex: {
type: Number,
required: true
}
})
const settingVisible = ref(false)
const open = () => {
if (currentNode.value.conditionType === ConditionType.RULE) {
if (currentNode.value.conditionGroups) {
conditionGroups.value = currentNode.value.conditionGroups
}
}
settingVisible.value = true
}
watch(
() => props.conditionNode,
(newValue) => {
currentNode.value = newValue
}
)
// 显示名称输入框
const showInput = ref(false)
const clickIcon = () => {
showInput.value = true
}
// 输入框失去焦点
const blurEvent = () => {
showInput.value = false
currentNode.value.name =
currentNode.value.name ||
getDefaultConditionNodeName(props.nodeIndex, currentNode.value?.defaultFlow)
}
const currentNode = ref<SimpleFlowNode>(props.conditionNode)
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
// 关闭
const closeDrawer = () => {
settingVisible.value = false
}
const handleClose = async (done: (cancel?: boolean) => void) => {
const isSuccess = await saveConfig()
if (!isSuccess) {
done(true) // 传入 true 阻止关闭
} else {
done()
}
}
// 表单校验规则
const formRules = reactive({
conditionType: [{ required: true, message: '配置方式不能为空', trigger: 'blur' }],
conditionExpression: [{ required: true, message: '条件表达式不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
// 保存配置
const saveConfig = async () => {
if (!currentNode.value.defaultFlow) {
// 校验表单
if (!formRef) return false
const valid = await formRef.value.validate()
if (!valid) return false
const showText = getShowText()
if (!showText) {
return false
}
currentNode.value.showText = showText
if (currentNode.value.conditionType === ConditionType.EXPRESSION) {
currentNode.value.conditionGroups = undefined
}
if (currentNode.value.conditionType === ConditionType.RULE) {
currentNode.value.conditionExpression = undefined
currentNode.value.conditionGroups = conditionGroups.value
}
}
settingVisible.value = false
return true
}
const getShowText = (): string => {
let showText = ''
if (currentNode.value.conditionType === ConditionType.EXPRESSION) {
if (currentNode.value.conditionExpression) {
showText = `表达式:${currentNode.value.conditionExpression}`
}
}
if (currentNode.value.conditionType === ConditionType.RULE) {
// 条件组是否为与关系
const groupAnd = conditionGroups.value.and
let warningMesg: undefined | string = undefined
const conditionGroup = conditionGroups.value.conditions.map((item) => {
return (
'(' +
item.rules
.map((rule) => {
if (rule.leftSide && rule.rightSide) {
return (
getFieldTitle(rule.leftSide) + ' ' + getOpName(rule.opCode) + ' ' + rule.rightSide
)
} else {
// 有一条规则不完善。提示错误
warningMesg = '请完善条件规则'
return ''
}
})
.join(item.and ? ' 且 ' : ' 或 ') +
' ) '
)
})
if (warningMesg) {
message.warning(warningMesg)
showText = ''
} else {
showText = conditionGroup.join(groupAnd ? ' 且 ' : ' 或 ')
}
}
return showText
}
// 改变条件配置方式
const changeConditionType = () => {}
const conditionGroups = ref<ConditionGroup>({
and: true,
conditions: [
{
and: true,
rules: [
{
type: 1,
opName: '等于',
opCode: '==',
leftSide: '',
rightSide: ''
}
]
}
]
})
// 添加条件组
const addConditionGroup = () => {
const condition = {
and: true,
rules: [
{
type: 1,
opName: '等于',
opCode: '==',
leftSide: '',
rightSide: ''
}
]
}
conditionGroups.value.conditions.push(condition)
}
// 删除条件组
const deleteConditionGroup = (idx: number) => {
conditionGroups.value.conditions.splice(idx, 1)
}
// 添加条件规则
const addConditionRule = (condition: Condition, idx: number) => {
const rule: ConditionRule = {
type: 1,
opName: '等于',
opCode: '==',
leftSide: '',
rightSide: ''
}
condition.rules.splice(idx + 1, 0, rule)
}
const deleteConditionRule = (condition: Condition, idx: number) => {
condition.rules.splice(idx, 1)
}
const fieldsInfo = useFormFields()
const getFieldTitle = (field: string) => {
const item = fieldsInfo.find((item) => item.field === field)
return item?.title
}
const getOpName = (opCode: string): string => {
const opName = COMPARISON_OPERATORS.find((item) => item.value === opCode)
return opName?.label
}
</script>
<style lang="scss" scoped>
.condition-group-tool {
display: flex;
justify-content: space-between;
width: 500px;
margin-bottom: 20px;
}
.condition-group {
position: relative;
&:hover {
border-color: #0089ff;
.condition-group-delete {
opacity: 1;
}
}
.condition-group-delete {
position: absolute;
top: 0;
left: 0;
display: flex;
cursor: pointer;
opacity: 0;
}
}
::v-deep(.el-card__header) {
padding: 8px var(--el-card-padding);
border-bottom: 1px solid var(--el-card-border-color);
box-sizing: border-box;
}
</style>
@@ -0,0 +1,307 @@
<template>
<el-drawer
:append-to-body="true"
v-model="settingVisible"
:show-close="false"
:size="550"
:before-close="saveConfig"
>
<template #header>
<div class="config-header">
<input
v-if="showInput"
type="text"
class="config-editable-input"
@blur="blurEvent()"
v-mountedFocus
v-model="nodeName"
:placeholder="nodeName"
/>
<div v-else class="node-name">
{{ nodeName }} <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" />
</div>
<div class="divide-line"></div>
</div>
</template>
<el-tabs type="border-card" v-model="activeTabName">
<el-tab-pane label="抄送人" name="user">
<div>
<el-form ref="formRef" :model="configForm" label-position="top" :rules="formRules">
<el-form-item label="抄送人设置" prop="candidateStrategy">
<el-radio-group
v-model="configForm.candidateStrategy"
@change="changeCandidateStrategy"
>
<el-radio
v-for="(dict, index) in copyUserStrategies"
:key="index"
:value="dict.value"
:label="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
v-if="configForm.candidateStrategy == CandidateStrategy.ROLE"
label="指定角色"
prop="roleIds"
>
<el-select v-model="configForm.roleIds" clearable multiple style="width: 100%">
<el-option
v-for="item in roleOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="
configForm.candidateStrategy == CandidateStrategy.DEPT_MEMBER ||
configForm.candidateStrategy == CandidateStrategy.DEPT_LEADER
"
label="指定部门"
prop="deptIds"
span="24"
>
<el-tree-select
ref="treeRef"
v-model="configForm.deptIds"
:data="deptTreeOptions"
:props="defaultProps"
empty-text="加载中,请稍后"
multiple
node-key="id"
style="width: 100%"
show-checkbox
/>
</el-form-item>
<el-form-item
v-if="configForm.candidateStrategy == CandidateStrategy.POST"
label="指定岗位"
prop="postIds"
span="24"
>
<el-select v-model="configForm.postIds" clearable multiple style="width: 100%">
<el-option
v-for="item in postOptions"
:key="item.id"
:label="item.name"
:value="item.id!"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="configForm.candidateStrategy == CandidateStrategy.USER"
label="指定用户"
prop="userIds"
span="24"
>
<el-select v-model="configForm.userIds" clearable multiple style="width: 100%">
<el-option
v-for="item in userOptions"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="configForm.candidateStrategy === CandidateStrategy.USER_GROUP"
label="指定用户组"
prop="userGroups"
>
<el-select v-model="configForm.userGroups" clearable multiple style="width: 100%">
<el-option
v-for="item in userGroupOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="configForm.candidateStrategy === CandidateStrategy.EXPRESSION"
label="流程表达式"
prop="expression"
>
<el-input
type="textarea"
v-model="configForm.expression"
clearable
style="width: 100%"
/>
</el-form-item>
</el-form>
</div>
</el-tab-pane>
<el-tab-pane label="表单字段权限" name="fields" v-if="formType === 10">
<div class="field-setting-pane">
<div class="field-setting-desc">字段权限</div>
<div class="field-permit-title">
<div class="setting-title-label first-title"> 字段名称 </div>
<div class="other-titles">
<span class="setting-title-label">只读</span>
<span class="setting-title-label">可编辑</span>
<span class="setting-title-label">隐藏</span>
</div>
</div>
<div
class="field-setting-item"
v-for="(item, index) in fieldsPermissionConfig"
:key="index"
>
<div class="field-setting-item-label"> {{ item.title }} </div>
<el-radio-group class="field-setting-item-group" v-model="item.permission">
<div class="item-radio-wrap">
<el-radio
:value="FieldPermissionType.READ"
size="large"
:label="FieldPermissionType.WRITE"
><span></span
></el-radio>
</div>
<div class="item-radio-wrap">
<el-radio
:value="FieldPermissionType.WRITE"
size="large"
:label="FieldPermissionType.WRITE"
disabled
><span></span
></el-radio>
</div>
<div class="item-radio-wrap">
<el-radio
:value="FieldPermissionType.NONE"
size="large"
:label="FieldPermissionType.NONE"
><span></span
></el-radio>
</div>
</el-radio-group>
</div>
</div>
</el-tab-pane>
</el-tabs>
<template #footer>
<el-divider />
<div>
<el-button type="primary" @click="saveConfig">确 定</el-button>
<el-button @click="closeDrawer">取 消</el-button>
</div>
</template>
</el-drawer>
</template>
<script setup lang="ts">
import {
SimpleFlowNode,
CandidateStrategy,
NodeType,
CANDIDATE_STRATEGY,
FieldPermissionType
} from '../consts'
import {
useWatchNode,
useDrawer,
useNodeName,
useFormFieldsPermission,
useNodeForm,
CopyTaskFormType
} from '../node'
import { defaultProps } from '@/utils/tree'
defineOptions({
name: 'CopyTaskNodeConfig'
})
const props = defineProps({
flowNode: {
type: Object as () => SimpleFlowNode,
required: true
}
})
// 抽屉配置
const { settingVisible, closeDrawer, openDrawer } = useDrawer()
// 当前节点
const currentNode = useWatchNode(props)
// 节点名称
const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.COPY_TASK_NODE)
// 激活的 Tab 标签页
const activeTabName = ref('user')
// 表单字段权限配置
const { formType, fieldsPermissionConfig, getNodeConfigFormFields } = useFormFieldsPermission(
FieldPermissionType.READ
)
// 抄送人表单配置
const formRef = ref() // 表单 Ref
// 表单校验规则
const formRules = reactive({
candidateStrategy: [{ required: true, message: '抄送人设置不能为空', trigger: 'change' }],
userIds: [{ required: true, message: '用户不能为空', trigger: 'change' }],
roleIds: [{ required: true, message: '角色不能为空', trigger: 'change' }],
deptIds: [{ required: true, message: '部门不能为空', trigger: 'change' }],
userGroups: [{ required: true, message: '用户组不能为空', trigger: 'change' }],
postIds: [{ required: true, message: '岗位不能为空', trigger: 'change' }],
expression: [{ required: true, message: '流程表达式不能为空', trigger: 'blur' }]
})
const {
configForm: tempConfigForm,
roleOptions,
postOptions,
userOptions,
userGroupOptions,
deptTreeOptions,
getShowText,
handleCandidateParam,
parseCandidateParam
} = useNodeForm(NodeType.COPY_TASK_NODE)
const configForm = tempConfigForm as Ref<CopyTaskFormType>
// 抄送人策略, 去掉发起人自选 和 发起人自己
const copyUserStrategies = computed(() => {
return CANDIDATE_STRATEGY.filter(
(item) =>
item.value !== CandidateStrategy.START_USER_SELECT &&
item.value !== CandidateStrategy.START_USER
)
})
// 改变抄送人设置策略
const changeCandidateStrategy = () => {
configForm.value.userIds = []
configForm.value.deptIds = []
configForm.value.roleIds = []
configForm.value.postIds = []
configForm.value.userGroups = []
configForm.value.deptLevel = 1
}
// 保存配置
const saveConfig = async () => {
activeTabName.value = 'user'
if (!formRef) return false
const valid = await formRef.value.validate()
if (!valid) return false
const showText = getShowText()
if (!showText) return false
currentNode.value.name = nodeName.value!
currentNode.value.candidateParam = handleCandidateParam()
currentNode.value.candidateStrategy = configForm.value.candidateStrategy
currentNode.value.showText = showText
currentNode.value.fieldsPermission = fieldsPermissionConfig.value
settingVisible.value = false
return true
}
// 显示抄送节点配置, 由父组件传过来
const showCopyTaskNodeConfig = (node: SimpleFlowNode) => {
nodeName.value = node.name
// 抄送人设置
configForm.value.candidateStrategy = node.candidateStrategy!
parseCandidateParam(node.candidateStrategy!, node?.candidateParam)
// 表单字段权限
getNodeConfigFormFields(node.fieldsPermission)
}
defineExpose({ openDrawer, showCopyTaskNodeConfig }) // 暴露方法给父组件
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,136 @@
<template>
<el-drawer
:append-to-body="true"
v-model="settingVisible"
:show-close="false"
:size="550"
:before-close="saveConfig"
>
<template #header>
<div class="config-header">
<input
v-if="showInput"
type="text"
class="config-editable-input"
@blur="blurEvent()"
v-mountedFocus
v-model="nodeName"
:placeholder="nodeName"
/>
<div v-else class="node-name">
{{ nodeName }} <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" />
</div>
<div class="divide-line"></div>
</div>
</template>
<el-tabs type="border-card" v-model="activeTabName">
<el-tab-pane label="权限" name="user">
<div> 待实现 </div>
</el-tab-pane>
<el-tab-pane label="表单字段权限" name="fields" v-if="formType === 10">
<div class="field-setting-pane">
<div class="field-setting-desc">字段权限</div>
<div class="field-permit-title">
<div class="setting-title-label first-title"> 字段名称 </div>
<div class="other-titles">
<span class="setting-title-label">只读</span>
<span class="setting-title-label">可编辑</span>
<span class="setting-title-label">隐藏</span>
</div>
</div>
<div
class="field-setting-item"
v-for="(item, index) in fieldsPermissionConfig"
:key="index"
>
<div class="field-setting-item-label"> {{ item.title }} </div>
<el-radio-group class="field-setting-item-group" v-model="item.permission">
<div class="item-radio-wrap">
<el-radio
:value="FieldPermissionType.READ"
size="large"
:label="FieldPermissionType.READ"
><span></span
></el-radio>
</div>
<div class="item-radio-wrap">
<el-radio
:value="FieldPermissionType.WRITE"
size="large"
:label="FieldPermissionType.WRITE"
><span></span
></el-radio>
</div>
<div class="item-radio-wrap">
<el-radio
:value="FieldPermissionType.NONE"
size="large"
:label="FieldPermissionType.NONE"
><span></span
></el-radio>
</div>
</el-radio-group>
</div>
</div>
</el-tab-pane>
</el-tabs>
<template #footer>
<el-divider />
<div>
<el-button type="primary" @click="saveConfig">确 定</el-button>
<el-button @click="closeDrawer">取 消</el-button>
</div>
</template>
</el-drawer>
</template>
<script setup lang="ts">
import { SimpleFlowNode, NodeType, FieldPermissionType, START_USER_BUTTON_SETTING } from '../consts'
import { useWatchNode, useDrawer, useNodeName, useFormFieldsPermission } from '../node'
defineOptions({
name: 'StartUserNodeConfig'
})
const props = defineProps({
flowNode: {
type: Object as () => SimpleFlowNode,
required: true
}
})
// 抽屉配置
const { settingVisible, closeDrawer, openDrawer } = useDrawer()
// 当前节点
const currentNode = useWatchNode(props)
// 节点名称
const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.COPY_TASK_NODE)
// 激活的 Tab 标签页
const activeTabName = ref('user')
// 表单字段权限配置
const { formType, fieldsPermissionConfig, getNodeConfigFormFields } = useFormFieldsPermission(
FieldPermissionType.WRITE
)
// 保存配置
const saveConfig = async () => {
activeTabName.value = 'user'
currentNode.value.name = nodeName.value!
// TODO 暂时写死。后续可以显示谁有权限可以发起
currentNode.value.showText = '已设置'
// 设置表单权限
currentNode.value.fieldsPermission = fieldsPermissionConfig.value
// 设置发起人的按钮权限
currentNode.value.buttonsSetting = START_USER_BUTTON_SETTING
console.log('currentNode.value.buttonsSetting==>', currentNode.value.buttonsSetting)
settingVisible.value = false
return true
}
// 显示发起人节点配置, 由父组件传过来
const showStartUserNodeConfig = (node: SimpleFlowNode) => {
nodeName.value = node.name
// 表单字段权限
getNodeConfigFormFields(node.fieldsPermission)
}
defineExpose({ openDrawer, showStartUserNodeConfig }) // 暴露方法给父组件
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,901 @@
<template>
<el-drawer
:append-to-body="true"
v-model="settingVisible"
:show-close="false"
:size="550"
:before-close="saveConfig"
class="justify-start"
>
<template #header>
<div class="config-header">
<input
v-if="showInput"
type="text"
class="config-editable-input"
@blur="blurEvent()"
v-mountedFocus
v-model="nodeName"
:placeholder="nodeName"
/>
<div v-else class="node-name">
{{ nodeName }} <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" />
</div>
<div class="divide-line"></div>
</div>
</template>
<div class="flex flex-items-center mb-3">
<span class="font-size-16px mr-3">审批类型 :</span>
<el-radio-group v-model="approveType">
<el-radio
v-for="(item, index) in APPROVE_TYPE"
:key="index"
:value="item.value"
:label="item.value"
>
{{ item.label }}
</el-radio>
</el-radio-group>
</div>
<el-tabs type="border-card" v-model="activeTabName" v-if="approveType === ApproveType.USER">
<el-tab-pane label="审批人" name="user">
<div>
<el-form ref="formRef" :model="configForm" label-position="top" :rules="formRules">
<el-form-item label="审批人设置" prop="candidateStrategy">
<el-radio-group
v-model="configForm.candidateStrategy"
@change="changeCandidateStrategy"
>
<el-radio
v-for="(dict, index) in CANDIDATE_STRATEGY"
:key="index"
:value="dict.value"
:label="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
v-if="configForm.candidateStrategy == CandidateStrategy.ROLE"
label="指定角色"
prop="roleIds"
>
<el-select v-model="configForm.roleIds" clearable multiple style="width: 100%">
<el-option
v-for="item in roleOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="
configForm.candidateStrategy == CandidateStrategy.DEPT_MEMBER ||
configForm.candidateStrategy == CandidateStrategy.DEPT_LEADER ||
configForm.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER
"
label="指定部门"
prop="deptIds"
span="24"
>
<el-tree-select
ref="treeRef"
v-model="configForm.deptIds"
:data="deptTreeOptions"
:props="defaultProps"
empty-text="加载中,请稍后"
multiple
node-key="id"
:check-strictly="true"
style="width: 100%"
show-checkbox
/>
</el-form-item>
<el-form-item
v-if="
configForm.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER ||
configForm.candidateStrategy == CandidateStrategy.START_USER_DEPT_LEADER ||
configForm.candidateStrategy == CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER
"
:label="deptLevelLabel!"
prop="deptLevel"
span="24"
>
<el-select v-model="configForm.deptLevel" clearable>
<el-option
v-for="(item, index) in MULTI_LEVEL_DEPT"
:key="index"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="configForm.candidateStrategy == CandidateStrategy.POST"
label="指定岗位"
prop="postIds"
span="24"
>
<el-select v-model="configForm.postIds" clearable multiple style="width: 100%">
<el-option
v-for="item in postOptions"
:key="item.id"
:label="item.name"
:value="item.id!"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="configForm.candidateStrategy == CandidateStrategy.USER"
label="指定用户"
prop="userIds"
span="24"
>
<el-select
v-model="configForm.userIds"
clearable
multiple
style="width: 100%"
@change="changedCandidateUsers"
>
<el-option
v-for="item in userOptions"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="configForm.candidateStrategy === CandidateStrategy.USER_GROUP"
label="指定用户组"
prop="userGroups"
>
<el-select v-model="configForm.userGroups" clearable multiple style="width: 100%">
<el-option
v-for="item in userGroupOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<!-- TODO @jason:后续要支持选择已经存好的表达式 -->
<el-form-item
v-if="configForm.candidateStrategy === CandidateStrategy.EXPRESSION"
label="流程表达式"
prop="expression"
>
<el-input
type="textarea"
v-model="configForm.expression"
clearable
style="width: 100%"
/>
</el-form-item>
<el-form-item label="多人审批方式" prop="approveMethod">
<el-radio-group v-model="configForm.approveMethod" @change="approveMethodChanged">
<div class="flex-col">
<div
v-for="(item, index) in APPROVE_METHODS"
:key="index"
class="flex items-center"
>
<el-radio
:value="item.value"
:label="item.value"
:disabled="
item.value !== ApproveMethodType.RANDOM_SELECT_ONE_APPROVE &&
notAllowedMultiApprovers
"
>
{{ item.label }}
</el-radio>
<el-form-item prop="approveRatio">
<el-input-number
v-model="configForm.approveRatio"
:min="10"
:max="100"
:step="10"
size="small"
v-if="
item.value === ApproveMethodType.APPROVE_BY_RATIO &&
configForm.approveMethod === ApproveMethodType.APPROVE_BY_RATIO
"
/>
</el-form-item>
</div>
</div>
</el-radio-group>
</el-form-item>
<el-divider content-position="left">审批人拒绝时</el-divider>
<el-form-item prop="rejectHandlerType">
<el-radio-group v-model="configForm.rejectHandlerType">
<div class="flex-col">
<div v-for="(item, index) in REJECT_HANDLER_TYPES" :key="index">
<el-radio :key="item.value" :value="item.value" :label="item.label" />
</div>
</div>
</el-radio-group>
</el-form-item>
<el-form-item
v-if="configForm.rejectHandlerType == RejectHandlerType.RETURN_USER_TASK"
label="驳回节点"
prop="returnNodeId"
>
<el-select v-model="configForm.returnNodeId" clearable style="width: 100%">
<el-option
v-for="item in returnTaskList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-divider content-position="left">审批人超时未处理时</el-divider>
<el-form-item label="启用开关" prop="timeoutHandlerEnable">
<el-switch
v-model="configForm.timeoutHandlerEnable"
active-text="开启"
inactive-text="关闭"
@change="timeoutHandlerChange"
/>
</el-form-item>
<el-form-item
label="执行动作"
prop="timeoutHandlerType"
v-if="configForm.timeoutHandlerEnable"
>
<el-radio-group
v-model="configForm.timeoutHandlerType"
@change="timeoutHandlerTypeChanged"
>
<el-radio-button
v-for="item in TIMEOUT_HANDLER_TYPES"
:key="item.value"
:value="item.value"
:label="item.label"
/>
</el-radio-group>
</el-form-item>
<el-form-item label="超时时间设置" v-if="configForm.timeoutHandlerEnable">
<span class="mr-2">当超过</span>
<el-form-item prop="timeDuration">
<el-input-number
class="mr-2"
:style="{ width: '100px' }"
v-model="configForm.timeDuration"
:min="1"
controls-position="right"
/>
</el-form-item>
<el-select
v-model="timeUnit"
class="mr-2"
:style="{ width: '100px' }"
@change="timeUnitChange"
>
<el-option
v-for="item in TIME_UNIT_TYPES"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
未处理
</el-form-item>
<el-form-item
label="最大提醒次数"
prop="maxRemindCount"
v-if="configForm.timeoutHandlerEnable && configForm.timeoutHandlerType === 1"
>
<el-input-number v-model="configForm.maxRemindCount" :min="1" :max="10" />
</el-form-item>
<el-divider content-position="left">审批人为空时</el-divider>
<el-form-item prop="assignEmptyHandlerType">
<el-radio-group v-model="configForm.assignEmptyHandlerType">
<div class="flex-col">
<div v-for="(item, index) in ASSIGN_EMPTY_HANDLER_TYPES" :key="index">
<el-radio :key="item.value" :value="item.value" :label="item.label" />
</div>
</div>
</el-radio-group>
</el-form-item>
<el-form-item
v-if="configForm.assignEmptyHandlerType == AssignEmptyHandlerType.ASSIGN_USER"
label="指定用户"
prop="assignEmptyHandlerUserIds"
span="24"
>
<el-select
v-model="configForm.assignEmptyHandlerUserIds"
clearable
multiple
style="width: 100%"
>
<el-option
v-for="item in userOptions"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-divider content-position="left">审批人与提交人为同一人时</el-divider>
<el-form-item prop="assignStartUserHandlerType">
<el-radio-group v-model="configForm.assignStartUserHandlerType">
<div class="flex-col">
<div v-for="(item, index) in ASSIGN_START_USER_HANDLER_TYPES" :key="index">
<el-radio :key="item.value" :value="item.value" :label="item.label" />
</div>
</div>
</el-radio-group>
</el-form-item>
</el-form>
</div>
</el-tab-pane>
<el-tab-pane label="操作按钮设置" name="buttons">
<div class="button-setting-pane">
<div class="button-setting-desc">操作按钮</div>
<div class="button-setting-title">
<div class="button-title-label">操作按钮</div>
<div class="pl-4 button-title-label">显示名称</div>
<div class="button-title-label">启用</div>
</div>
<div class="button-setting-item" v-for="(item, index) in buttonsSetting" :key="index">
<div class="button-setting-item-label"> {{ OPERATION_BUTTON_NAME.get(item.id) }} </div>
<div class="button-setting-item-label">
<input
type="text"
class="editable-title-input"
@blur="btnDisplayNameBlurEvent(index)"
v-mountedFocus
v-model="item.displayName"
:placeholder="item.displayName"
v-if="btnDisplayNameEdit[index]"
/>
<el-button v-else text @click="changeBtnDisplayName(index)"
>{{ item.displayName }} &nbsp;<Icon icon="ep:edit"
/></el-button>
</div>
<div class="button-setting-item-label">
<el-switch v-model="item.enable" />
</div>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="表单字段权限" name="fields" v-if="formType === 10">
<div class="field-setting-pane">
<div class="field-setting-desc">字段权限</div>
<div class="field-permit-title">
<div class="setting-title-label first-title"> 字段名称 </div>
<div class="other-titles">
<span class="setting-title-label">只读</span>
<span class="setting-title-label">可编辑</span>
<span class="setting-title-label">隐藏</span>
</div>
</div>
<div
class="field-setting-item"
v-for="(item, index) in fieldsPermissionConfig"
:key="index"
>
<div class="field-setting-item-label"> {{ item.title }} </div>
<el-radio-group class="field-setting-item-group" v-model="item.permission">
<div class="item-radio-wrap">
<el-radio
:value="FieldPermissionType.READ"
size="large"
:label="FieldPermissionType.READ"
><span></span
></el-radio>
</div>
<div class="item-radio-wrap">
<el-radio
:value="FieldPermissionType.WRITE"
size="large"
:label="FieldPermissionType.WRITE"
><span></span
></el-radio>
</div>
<div class="item-radio-wrap">
<el-radio
:value="FieldPermissionType.NONE"
size="large"
:label="FieldPermissionType.NONE"
><span></span
></el-radio>
</div>
</el-radio-group>
</div>
</div>
</el-tab-pane>
</el-tabs>
<template #footer>
<el-divider />
<div>
<el-button type="primary" @click="saveConfig">确 定</el-button>
<el-button @click="closeDrawer">取 消</el-button>
</div>
</template>
</el-drawer>
</template>
<script setup lang="ts">
import {
SimpleFlowNode,
APPROVE_TYPE,
ApproveType,
APPROVE_METHODS,
CandidateStrategy,
NodeType,
ApproveMethodType,
TimeUnitType,
RejectHandlerType,
TIMEOUT_HANDLER_TYPES,
TIME_UNIT_TYPES,
REJECT_HANDLER_TYPES,
DEFAULT_BUTTON_SETTING,
OPERATION_BUTTON_NAME,
ButtonSetting,
MULTI_LEVEL_DEPT,
CANDIDATE_STRATEGY,
ASSIGN_START_USER_HANDLER_TYPES,
TimeoutHandlerType,
ASSIGN_EMPTY_HANDLER_TYPES,
AssignEmptyHandlerType,
FieldPermissionType
} from '../consts'
import {
useWatchNode,
useNodeName,
useFormFieldsPermission,
useNodeForm,
UserTaskFormType,
useDrawer
} from '../node'
import { defaultProps } from '@/utils/tree'
import { cloneDeep } from 'lodash-es'
import { convertTimeUnit, getApproveTypeText } from '../utils'
defineOptions({
name: 'UserTaskNodeConfig'
})
const props = defineProps({
flowNode: {
type: Object as () => SimpleFlowNode,
required: true
}
})
const emits = defineEmits<{
'find:returnTaskNodes': [nodeList: SimpleFlowNode[]]
}>()
const deptLevelLabel = computed(() => {
let label = '部门负责人来源'
if (configForm.value.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER) {
label = label + '(指定部门向上)'
} else {
label = label + '(发起人部门向上)'
}
return label
})
// 监控节点的变化
const currentNode = useWatchNode(props)
// 抽屉配置
const { settingVisible, closeDrawer, openDrawer } = useDrawer()
// 节点名称配置
const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.USER_TASK_NODE)
// 激活的 Tab 标签页
const activeTabName = ref('user')
// 表单字段权限设置
const { formType, fieldsPermissionConfig, getNodeConfigFormFields } = useFormFieldsPermission(
FieldPermissionType.READ
)
// 操作按钮设置
const { buttonsSetting, btnDisplayNameEdit, changeBtnDisplayName, btnDisplayNameBlurEvent } =
useButtonsSetting()
const approveType = ref(ApproveType.USER)
// 审批人表单设置
const formRef = ref() // 表单 Ref
// 表单校验规则
const formRules = reactive({
candidateStrategy: [{ required: true, message: '审批人设置不能为空', trigger: 'change' }],
userIds: [{ required: true, message: '用户不能为空', trigger: 'change' }],
roleIds: [{ required: true, message: '角色不能为空', trigger: 'change' }],
deptIds: [{ required: true, message: '部门不能为空', trigger: 'change' }],
userGroups: [{ required: true, message: '用户组不能为空', trigger: 'change' }],
postIds: [{ required: true, message: '岗位不能为空', trigger: 'change' }],
expression: [{ required: true, message: '流程表达式不能为空', trigger: 'blur' }],
approveMethod: [{ required: true, message: '多人审批方式不能为空', trigger: 'change' }],
approveRatio: [{ required: true, message: '通过比例不能为空', trigger: 'blur' }],
returnNodeId: [{ required: true, message: '驳回节点不能为空', trigger: 'change' }],
timeoutHandlerEnable: [{ required: true }],
timeoutHandlerType: [{ required: true }],
timeDuration: [{ required: true, message: '超时时间不能为空', trigger: 'blur' }],
maxRemindCount: [{ required: true, message: '提醒次数不能为空', trigger: 'blur' }],
assignEmptyHandlerType: [{ required: true }],
assignEmptyHandlerUserIds: [{ required: true, message: '用户不能为空', trigger: 'change' }],
assignStartUserHandlerType: [{ required: true }]
})
const {
configForm: tempConfigForm,
roleOptions,
postOptions,
userOptions,
userGroupOptions,
deptTreeOptions,
handleCandidateParam,
parseCandidateParam,
getShowText
} = useNodeForm(NodeType.USER_TASK_NODE)
const configForm = tempConfigForm as Ref<UserTaskFormType>
// 不允许多人审批
const notAllowedMultiApprovers = ref(false)
// 改变审批人设置策略
const changeCandidateStrategy = () => {
configForm.value.userIds = []
configForm.value.deptIds = []
configForm.value.roleIds = []
configForm.value.postIds = []
configForm.value.userGroups = []
configForm.value.deptLevel = 1
configForm.value.approveMethod = ApproveMethodType.SEQUENTIAL_APPROVE
if (
configForm.value.candidateStrategy === CandidateStrategy.START_USER ||
configForm.value.candidateStrategy === CandidateStrategy.USER
) {
notAllowedMultiApprovers.value = true
} else {
notAllowedMultiApprovers.value = false
}
}
// 改变审批候选人
const changedCandidateUsers = () => {
if (
configForm.value.userIds &&
configForm.value.userIds?.length <= 1 &&
configForm.value.candidateStrategy === CandidateStrategy.USER
) {
configForm.value.approveMethod = ApproveMethodType.RANDOM_SELECT_ONE_APPROVE
configForm.value.rejectHandlerType = RejectHandlerType.FINISH_PROCESS
notAllowedMultiApprovers.value = true
} else {
notAllowedMultiApprovers.value = false
}
}
// 审批方式改变
const approveMethodChanged = () => {
configForm.value.rejectHandlerType = RejectHandlerType.FINISH_PROCESS
if (configForm.value.approveMethod === ApproveMethodType.APPROVE_BY_RATIO) {
configForm.value.approveRatio = 100
}
formRef.value.clearValidate('approveRatio')
}
// 审批拒绝 可回退的节点
const returnTaskList = ref<SimpleFlowNode[]>([])
// 审批人超时未处理设置
const {
timeoutHandlerChange,
cTimeoutType,
timeoutHandlerTypeChanged,
timeUnit,
timeUnitChange,
isoTimeDuration,
cTimeoutMaxRemindCount
} = useTimeoutHandler()
// 保存配置
const saveConfig = async () => {
activeTabName.value = 'user'
// 设置审批节点名称
currentNode.value.name = nodeName.value!
// 设置审批类型
currentNode.value.approveType = approveType.value
// 如果不是人工审批。返回
if (approveType.value !== ApproveType.USER) {
currentNode.value.showText = getApproveTypeText(approveType.value)
settingVisible.value = false
return true
}
if (!formRef) return false
const valid = await formRef.value.validate()
if (!valid) return false
const showText = getShowText()
if (!showText) return false
currentNode.value.candidateStrategy = configForm.value.candidateStrategy
// 处理 candidateParam 参数
currentNode.value.candidateParam = handleCandidateParam()
// 设置审批方式
currentNode.value.approveMethod = configForm.value.approveMethod
if (configForm.value.approveMethod === ApproveMethodType.APPROVE_BY_RATIO) {
currentNode.value.approveRatio = configForm.value.approveRatio
}
// 设置拒绝处理
currentNode.value.rejectHandler = {
type: configForm.value.rejectHandlerType!,
returnNodeId: configForm.value.returnNodeId
}
// 设置超时处理
currentNode.value.timeoutHandler = {
enable: configForm.value.timeoutHandlerEnable!,
type: cTimeoutType.value,
timeDuration: isoTimeDuration.value,
maxRemindCount: cTimeoutMaxRemindCount.value
}
// 设置审批人为空时
currentNode.value.assignEmptyHandler = {
type: configForm.value.assignEmptyHandlerType!,
userIds:
configForm.value.assignEmptyHandlerType === AssignEmptyHandlerType.ASSIGN_USER
? configForm.value.assignEmptyHandlerUserIds
: undefined
}
// 设置审批人与发起人相同时
currentNode.value.assignStartUserHandlerType = configForm.value.assignStartUserHandlerType
// 设置表单权限
currentNode.value.fieldsPermission = fieldsPermissionConfig.value
// 设置按钮权限
currentNode.value.buttonsSetting = buttonsSetting.value
currentNode.value.showText = showText
settingVisible.value = false
return true
}
// 显示审批节点配置, 由父组件传过来
const showUserTaskNodeConfig = (node: SimpleFlowNode) => {
nodeName.value = node.name
// 1 审批类型
approveType.value = node.approveType ? node.approveType : ApproveType.USER
// 如果审批类型不是人工审批返回
if (approveType.value !== ApproveType.USER) {
return
}
//2.1 审批人设置
configForm.value.candidateStrategy = node.candidateStrategy!
// 解析候选人参数
parseCandidateParam(node.candidateStrategy!, node?.candidateParam)
if (configForm.value.userIds && configForm.value.userIds.length > 1) {
notAllowedMultiApprovers.value = true
} else {
notAllowedMultiApprovers.value = false
}
// 2.2 设置审批方式
configForm.value.approveMethod = node.approveMethod!
if (node.approveMethod == ApproveMethodType.APPROVE_BY_RATIO) {
configForm.value.approveRatio = node.approveRatio!
}
// 2.3 设置审批拒绝处理
configForm.value.rejectHandlerType = node.rejectHandler!.type
configForm.value.returnNodeId = node.rejectHandler?.returnNodeId
const matchNodeList = []
emits('find:returnTaskNodes', matchNodeList)
returnTaskList.value = matchNodeList
// 2.4 设置审批超时处理
configForm.value.timeoutHandlerEnable = node.timeoutHandler!.enable
if (node.timeoutHandler?.enable && node.timeoutHandler?.timeDuration) {
const strTimeDuration = node.timeoutHandler.timeDuration
let parseTime = strTimeDuration.slice(2, strTimeDuration.length - 1)
let parseTimeUnit = strTimeDuration.slice(strTimeDuration.length - 1)
configForm.value.timeDuration = parseInt(parseTime)
timeUnit.value = convertTimeUnit(parseTimeUnit)
}
configForm.value.timeoutHandlerType = node.timeoutHandler?.type
configForm.value.maxRemindCount = node.timeoutHandler?.maxRemindCount
// 2.5 设置审批人为空时
configForm.value.assignEmptyHandlerType = node.assignEmptyHandler?.type
configForm.value.assignEmptyHandlerUserIds = node.assignEmptyHandler?.userIds
// 2.6 设置用户任务的审批人与发起人相同时
configForm.value.assignStartUserHandlerType = node.assignStartUserHandlerType
// 3. 操作按钮设置
buttonsSetting.value = cloneDeep(node.buttonsSetting) || DEFAULT_BUTTON_SETTING
// 4. 表单字段权限配置
getNodeConfigFormFields(node.fieldsPermission)
}
defineExpose({ openDrawer, showUserTaskNodeConfig }) // 暴露方法给父组件
/**
* @description 操作按钮设置
*/
function useButtonsSetting() {
const buttonsSetting = ref<ButtonSetting[]>()
// 操作按钮显示名称可编辑
const btnDisplayNameEdit = ref<boolean[]>([])
const changeBtnDisplayName = (index: number) => {
btnDisplayNameEdit.value[index] = true
}
const btnDisplayNameBlurEvent = (index: number) => {
btnDisplayNameEdit.value[index] = false
const buttonItem = buttonsSetting.value![index]
buttonItem.displayName = buttonItem.displayName || OPERATION_BUTTON_NAME.get(buttonItem.id)!
}
return {
buttonsSetting,
btnDisplayNameEdit,
changeBtnDisplayName,
btnDisplayNameBlurEvent
}
}
/**
* @description 审批人超时未处理配置
*/
function useTimeoutHandler() {
// 时间单位
const timeUnit = ref(TimeUnitType.HOUR)
// 超时开关改变
const timeoutHandlerChange = () => {
if (configForm.value.timeoutHandlerEnable) {
timeUnit.value = 2
configForm.value.timeDuration = 6
configForm.value.timeoutHandlerType = 1
configForm.value.maxRemindCount = 1
}
}
// 超时执行的动作
const cTimeoutType = computed(() => {
if (!configForm.value.timeoutHandlerEnable) {
return undefined
}
return configForm.value.timeoutHandlerType
})
// 超时处理动作改变
const timeoutHandlerTypeChanged = () => {
if (configForm.value.timeoutHandlerType === TimeoutHandlerType.REMINDER) {
configForm.value.maxRemindCount = 1 // 超时提醒次数,默认为1
}
}
// 时间单位改变
const timeUnitChange = () => {
// 分钟,默认是 60 分钟
if (timeUnit.value === TimeUnitType.MINUTE) {
configForm.value.timeDuration = 60
}
// 小时,默认是 6 个小时
if (timeUnit.value === TimeUnitType.HOUR) {
configForm.value.timeDuration = 6
}
// 天, 默认 1天
if (timeUnit.value === TimeUnitType.DAY) {
configForm.value.timeDuration = 1
}
}
// 超时时间的 ISO 表示
const isoTimeDuration = computed(() => {
if (!configForm.value.timeoutHandlerEnable) {
return undefined
}
let strTimeDuration = 'PT'
if (timeUnit.value === TimeUnitType.MINUTE) {
strTimeDuration += configForm.value.timeDuration + 'M'
}
if (timeUnit.value === TimeUnitType.HOUR) {
strTimeDuration += configForm.value.timeDuration + 'H'
}
if (timeUnit.value === TimeUnitType.DAY) {
strTimeDuration += configForm.value.timeDuration + 'D'
}
return strTimeDuration
})
// 超时最大提醒次数
const cTimeoutMaxRemindCount = computed(() => {
if (!configForm.value.timeoutHandlerEnable) {
return undefined
}
if (configForm.value.timeoutHandlerType !== TimeoutHandlerType.REMINDER) {
return undefined
}
return configForm.value.maxRemindCount
})
return {
timeoutHandlerChange,
cTimeoutType,
timeoutHandlerTypeChanged,
timeUnit,
timeUnitChange,
isoTimeDuration,
cTimeoutMaxRemindCount
}
}
</script>
<style lang="scss" scoped>
.button-setting-pane {
display: flex;
flex-direction: column;
font-size: 14px;
.button-setting-desc {
padding-right: 8px;
margin-bottom: 16px;
font-size: 16px;
font-weight: 700;
}
.button-setting-title {
display: flex;
justify-content: space-between;
align-items: center;
height: 45px;
padding-left: 12px;
background-color: #f8fafc0a;
border: 1px solid #1f38581a;
& > :first-child {
width: 100px !important;
text-align: left !important;
}
& > :last-child {
text-align: center !important;
}
.button-title-label {
width: 150px;
font-size: 13px;
font-weight: 700;
color: #000;
text-align: left;
}
}
.button-setting-item {
align-items: center;
display: flex;
justify-content: space-between;
height: 38px;
padding-left: 12px;
border: 1px solid #1f38581a;
border-top: 0;
& > :first-child {
width: 100px !important;
}
& > :last-child {
text-align: center !important;
}
.button-setting-item-label {
width: 150px;
overflow: hidden;
text-align: left;
text-overflow: ellipsis;
white-space: nowrap;
}
.editable-title-input {
height: 24px;
max-width: 130px;
margin-left: 4px;
line-height: 24px;
border: 1px solid #d9d9d9;
border-radius: 4px;
transition: all 0.3s;
&:focus {
border-color: #40a9ff;
outline: 0;
box-shadow: 0 0 0 2px rgb(24 144 255 / 20%);
}
}
}
}
</style>
@@ -0,0 +1,79 @@
<template>
<div class="node-wrapper">
<div class="node-container">
<div class="node-box" :class="{ 'node-config-error': !currentNode.showText }">
<div class="node-title-container">
<div class="node-title-icon copy-task"><span class="iconfont icon-copy"></span></div>
<input
v-if="showInput"
type="text"
class="editable-title-input"
@blur="blurEvent()"
v-mountedFocus
v-model="currentNode.name"
:placeholder="currentNode.name"
/>
<div v-else class="node-title" @click="clickTitle">
{{ currentNode.name }}
</div>
</div>
<div class="node-content" @click="openNodeConfig">
<div class="node-text" :title="currentNode.showText" v-if="currentNode.showText">
{{ currentNode.showText }}
</div>
<div class="node-text" v-else>
{{ NODE_DEFAULT_TEXT.get(NodeType.COPY_TASK_NODE) }}
</div>
<Icon icon="ep:arrow-right-bold" />
</div>
<div 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" />
</div>
<CopyTaskNodeConfig v-if="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 } from '../node'
import CopyTaskNodeConfig from '../nodes-config/CopyTaskNodeConfig.vue'
defineOptions({
name: 'CopyTaskNode'
})
const props = defineProps({
flowNode: {
type: Object as () => SimpleFlowNode,
required: true
}
})
// 定义事件,更新父组件。
const emits = defineEmits<{
'update:flowNode': [node: SimpleFlowNode | undefined]
}>()
// 监控节点的变化
const currentNode = useWatchNode(props)
// 节点名称编辑
const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.COPY_TASK_NODE)
const nodeSetting = ref()
// 打开节点配置
const openNodeConfig = () => {
nodeSetting.value.showCopyTaskNodeConfig(currentNode.value)
nodeSetting.value.openDrawer()
}
// 删除节点。更新当前节点为孩子节点
const deleteNode = () => {
emits('update:flowNode', currentNode.value.childNode)
}
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,13 @@
<template>
<div class="end-node-wrapper">
<div class="end-node-box">
<span class="node-fixed-name" title="结束">结束</span>
</div>
</div>
</template>
<script setup lang="ts">
defineOptions({
name: 'EndEventNode'
})
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,207 @@
<template>
<div class="branch-node-wrapper">
<div class="branch-node-container">
<div class="branch-node-add" @click="addCondition">添加条件</div>
<div
class="branch-node-item"
v-for="(item, index) in currentNode.conditionNodes"
:key="index"
>
<template v-if="index == 0">
<div class="branch-line-first-top"> </div>
<div class="branch-line-first-bottom"></div>
</template>
<template v-if="index + 1 == currentNode.conditionNodes?.length">
<div class="branch-line-last-top"></div>
<div class="branch-line-last-bottom"></div>
</template>
<div class="node-wrapper">
<div class="node-container">
<div class="node-box" :class="{ 'node-config-error': !item.showText }">
<div class="branch-node-title-container">
<div v-if="showInputs[index]">
<input
type="text"
class="input-max-width editable-title-input"
@blur="blurEvent(index)"
v-mountedFocus
v-model="item.name"
/>
</div>
<div v-else class="branch-title" @click="clickEvent(index)"> {{ item.name }} </div>
<div class="branch-priority"> 优先级{{ index + 1 }} </div>
</div>
<div class="branch-node-content" @click="conditionNodeConfig(item.id)">
<div class="branch-node-text" :title="item.showText" v-if="item.showText">
{{ item.showText }}
</div>
<div class="branch-node-text" v-else>
{{ NODE_DEFAULT_TEXT.get(NodeType.CONDITION_NODE) }}
</div>
</div>
<div class="node-toolbar" v-if="index + 1 !== currentNode.conditionNodes?.length">
<div class="toolbar-icon">
<Icon
color="#0089ff"
icon="ep:circle-close-filled"
:size="18"
@click="deleteCondition(index)"
/>
</div>
</div>
<div
class="branch-node-move move-node-left"
v-if="index != 0 && index + 1 !== currentNode.conditionNodes?.length"
@click="moveNode(index, -1)"
>
<Icon icon="ep:arrow-left" />
</div>
<div
class="branch-node-move move-node-right"
v-if="currentNode.conditionNodes && index < currentNode.conditionNodes.length - 2"
@click="moveNode(index, 1)"
>
<Icon icon="ep:arrow-right" />
</div>
</div>
<NodeHandler v-model:child-node="item.childNode" />
</div>
</div>
<ConditionNodeConfig :node-index="index" :condition-node="item" :ref="item.id" />
<!-- 递归显示子节点 -->
<ProcessNodeTree
v-if="item && item.childNode"
:parent-node="item"
v-model:flow-node="item.childNode"
@find:recursive-find-parent-node="recursiveFindParentNode"
/>
</div>
</div>
<NodeHandler v-if="currentNode" v-model:child-node="currentNode.childNode" />
</div>
</template>
<script setup lang="ts">
import NodeHandler from '../NodeHandler.vue'
import ProcessNodeTree from '../ProcessNodeTree.vue'
import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
import { getDefaultConditionNodeName } from '../utils'
import { generateUUID } from '@/utils'
import ConditionNodeConfig from '../nodes-config/ConditionNodeConfig.vue'
const { proxy } = getCurrentInstance() as any
defineOptions({
name: 'ExclusiveNode'
})
const props = defineProps({
// parentNode : {
// type: Object as () => SimpleFlowNode,
// required: true
// },
flowNode: {
type: Object as () => SimpleFlowNode,
required: true
}
})
// 定义事件,更新父组件
const emits = defineEmits<{
'update:modelValue': [node: SimpleFlowNode | undefined]
'find:parentNode': [nodeList: SimpleFlowNode[], nodeType: number]
'find:recursiveFindParentNode': [
nodeList: SimpleFlowNode[],
curentNode: SimpleFlowNode,
nodeType: number
]
}>()
const currentNode = ref<SimpleFlowNode>(props.flowNode)
// const conditionNodes = computed(() => currentNode.value.conditionNodes);
watch(
() => props.flowNode,
(newValue) => {
currentNode.value = newValue
}
)
const showInputs = ref<boolean[]>([])
// 失去焦点
const blurEvent = (index: number) => {
showInputs.value[index] = false
const conditionNode = currentNode.value.conditionNodes?.at(index) as SimpleFlowNode
conditionNode.name =
conditionNode.name || getDefaultConditionNodeName(index, conditionNode.defaultFlow)
}
// 点击条件名称
const clickEvent = (index: number) => {
showInputs.value[index] = true
}
const conditionNodeConfig = (nodeId: string) => {
const conditionNode = proxy.$refs[nodeId][0]
conditionNode.open()
}
// 新增条件
const addCondition = () => {
const conditionNodes = currentNode.value.conditionNodes
if (conditionNodes) {
const len = conditionNodes.length
let lastIndex = len - 1
const conditionData: SimpleFlowNode = {
id: 'Flow_' + generateUUID(),
name: '条件' + len,
showText: '',
type: NodeType.CONDITION_NODE,
childNode: undefined,
conditionNodes: [],
conditionType: 1,
defaultFlow: false
}
conditionNodes.splice(lastIndex, 0, conditionData)
}
}
// 删除条件
const deleteCondition = (index: number) => {
const conditionNodes = currentNode.value.conditionNodes
if (conditionNodes) {
conditionNodes.splice(index, 1)
if (conditionNodes.length == 1) {
const childNode = currentNode.value.childNode
// 更新此节点为后续孩子节点
emits('update:modelValue', childNode)
}
}
}
// 移动节点
const moveNode = (index: number, to: number) => {
// -1 :向左 1: 向右
if (currentNode.value.conditionNodes) {
currentNode.value.conditionNodes[index] = currentNode.value.conditionNodes.splice(
index + to,
1,
currentNode.value.conditionNodes[index]
)[0]
}
}
// 递归从父节点中查询匹配的节点
const recursiveFindParentNode = (
nodeList: SimpleFlowNode[],
node: SimpleFlowNode,
nodeType: number
) => {
if (!node || node.type === NodeType.START_EVENT_NODE) {
return
}
if (node.type === nodeType) {
nodeList.push(node)
}
// 条件节点 (NodeType.CONDITION_NODE) 比较特殊。需要调用其父节点条件分支节点(NodeType.EXCLUSIVE_NODE) 继续查找
emits('find:parentNode', nodeList, nodeType)
}
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,181 @@
<template>
<div class="branch-node-wrapper">
<div class="branch-node-container">
<div class="branch-node-add" @click="addCondition">添加分支</div>
<div
class="branch-node-item"
v-for="(item, index) in currentNode.conditionNodes"
:key="index"
>
<template v-if="index == 0">
<div class="branch-line-first-top"></div>
<div class="branch-line-first-bottom"></div>
</template>
<template v-if="index + 1 == currentNode.conditionNodes?.length">
<div class="branch-line-last-top"></div>
<div class="branch-line-last-bottom"></div>
</template>
<div class="node-wrapper">
<div class="node-container">
<div class="node-box">
<div class="branch-node-title-container">
<div v-if="showInputs[index]">
<input
type="text"
class="input-max-width editable-title-input"
@blur="blurEvent(index)"
v-mountedFocus
v-model="item.name"
/>
</div>
<div v-else class="branch-title" @click="clickEvent(index)"> {{ item.name }} </div>
<div class="branch-priority">无优先级</div>
</div>
<div class="branch-node-content" @click="conditionNodeConfig(item.id)">
<div class="branch-node-text" :title="item.showText" v-if="item.showText">
{{ item.showText }}
</div>
<div class="branch-node-text" v-else>
{{ NODE_DEFAULT_TEXT.get(NodeType.CONDITION_NODE) }}
</div>
</div>
<div class="node-toolbar">
<div class="toolbar-icon">
<Icon
color="#0089ff"
icon="ep:circle-close-filled"
:size="18"
@click="deleteCondition(index)"
/>
</div>
</div>
<!-- <div
class="branch-node-move move-node-left"
v-if="index != 0 && index + 1 !== currentNode.conditionNodes?.length" @click="moveNode(index, -1)">
<Icon icon="ep:arrow-left" />
</div> -->
<!-- <div
class="branch-node-move move-node-right"
v-if="currentNode.conditionNodes && index < currentNode.conditionNodes.length - 2"
@click="moveNode(index, 1)">
<Icon icon="ep:arrow-right" />
</div> -->
</div>
<NodeHandler v-model:child-node="item.childNode" />
</div>
</div>
<!-- 递归显示子节点 -->
<ProcessNodeTree
v-if="item && item.childNode"
:parent-node="item"
v-model:flow-node="item.childNode"
@find:recursive-find-parent-node="recursiveFindParentNode"
/>
</div>
</div>
<NodeHandler v-if="currentNode" v-model:child-node="currentNode.childNode" />
</div>
</template>
<script setup lang="ts">
import NodeHandler from '../NodeHandler.vue'
import ProcessNodeTree from '../ProcessNodeTree.vue'
import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
import { generateUUID } from '@/utils'
const { proxy } = getCurrentInstance() as any
defineOptions({
name: 'ParallelNode'
})
const props = defineProps({
flowNode: {
type: Object as () => SimpleFlowNode,
required: true
}
})
// 定义事件,更新父组件
const emits = defineEmits<{
'update:modelValue': [node: SimpleFlowNode | undefined]
'find:parentNode': [nodeList: SimpleFlowNode[], nodeType: number]
'find:recursiveFindParentNode': [
nodeList: SimpleFlowNode[],
curentNode: SimpleFlowNode,
nodeType: number
]
}>()
const currentNode = ref<SimpleFlowNode>(props.flowNode)
watch(
() => props.flowNode,
(newValue) => {
currentNode.value = newValue
}
)
const showInputs = ref<boolean[]>([])
// 失去焦点
const blurEvent = (index: number) => {
showInputs.value[index] = false
const conditionNode = currentNode.value.conditionNodes?.at(index) as SimpleFlowNode
conditionNode.name = conditionNode.name || `并行${index + 1}`
}
// 点击条件名称
const clickEvent = (index: number) => {
showInputs.value[index] = true
}
const conditionNodeConfig = (nodeId: string) => {
const conditionNode = proxy.$refs[nodeId][0]
conditionNode.open()
}
// 新增条件
const addCondition = () => {
const conditionNodes = currentNode.value.conditionNodes
if (conditionNodes) {
const len = conditionNodes.length
let lastIndex = len - 1
const conditionData: SimpleFlowNode = {
id: 'Flow_' + generateUUID(),
name: '并行' + len,
showText: '无需配置条件同时执行',
type: NodeType.CONDITION_NODE,
childNode: undefined,
conditionNodes: []
}
conditionNodes.splice(lastIndex, 0, conditionData)
}
}
// 删除条件
const deleteCondition = (index: number) => {
const conditionNodes = currentNode.value.conditionNodes
if (conditionNodes) {
conditionNodes.splice(index, 1)
if (conditionNodes.length == 1) {
const childNode = currentNode.value.childNode
// 更新此节点为后续孩子节点
emits('update:modelValue', childNode)
}
}
}
// 递归从父节点中查询匹配的节点
const recursiveFindParentNode = (
nodeList: SimpleFlowNode[],
node: SimpleFlowNode,
nodeType: number
) => {
if (!node || node.type === NodeType.START_EVENT_NODE) {
return
}
if (node.type === nodeType) {
nodeList.push(node)
}
// 条件节点 (NodeType.CONDITION_NODE) 比较特殊。需要调用其父节点并行节点(NodeType.PARALLEL_NODE) 继续查找
emits('find:parentNode', nodeList, nodeType)
}
</script>
@@ -0,0 +1,69 @@
<template>
<div class="node-wrapper">
<div class="node-container">
<div class="node-box" :class="{ 'node-config-error': !currentNode.showText }">
<div class="node-title-container">
<div class="node-title-icon start-user"
><span class="iconfont icon-start-user"></span
></div>
<input
v-if="showInput"
type="text"
class="editable-title-input"
@blur="blurEvent()"
v-mountedFocus
v-model="currentNode.name"
:placeholder="currentNode.name"
/>
<div v-else class="node-title" @click="clickTitle">
{{ currentNode.name }}
</div>
</div>
<div class="node-content" @click="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.START_USER_NODE) }}
</div>
<Icon icon="ep:arrow-right-bold" />
</div>
</div>
<!-- 传递子节点给添加节点组件。会在子节点前面添加节点 -->
<NodeHandler v-if="currentNode" v-model:child-node="currentNode.childNode" />
</div>
</div>
<StartUserNodeConfig v-if="currentNode" ref="nodeSetting" :flow-node="currentNode" />
</template>
<script setup lang="ts">
import NodeHandler from '../NodeHandler.vue'
import { useWatchNode, useNodeName2 } from '../node'
import { SimpleFlowNode, NODE_DEFAULT_TEXT, NodeType } from '../consts'
import StartUserNodeConfig from '../nodes-config/StartUserNodeConfig.vue'
defineOptions({
name: 'StartEventNode'
})
const props = defineProps({
flowNode: {
type: Object as () => SimpleFlowNode,
default: () => null
}
})
// 定义事件,更新父组件。
const emits = defineEmits<{
'update:modelValue': [node: SimpleFlowNode | undefined]
}>()
// 监控节点变化
const currentNode = useWatchNode(props)
// 节点名称编辑
const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.START_USER_NODE)
const nodeSetting = ref()
// 打开节点配置
const openNodeConfig = () => {
// 把当前节点传递给配置组件
nodeSetting.value.showStartUserNodeConfig(currentNode.value)
nodeSetting.value.openDrawer()
}
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,88 @@
<template>
<div class="node-wrapper">
<div class="node-container">
<div class="node-box" :class="{ 'node-config-error': !currentNode.showText }">
<div class="node-title-container">
<div class="node-title-icon user-task"><span class="iconfont icon-approve"></span></div>
<input
v-if="showInput"
type="text"
class="editable-title-input"
@blur="blurEvent()"
v-mountedFocus
v-model="currentNode.name"
:placeholder="currentNode.name"
/>
<div v-else class="node-title" @click="clickTitle">
{{ currentNode.name }}
</div>
</div>
<div class="node-content" @click="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.USER_TASK_NODE) }}
</div>
<Icon icon="ep:arrow-right-bold" />
</div>
<div 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" />
</div>
</div>
<UserTaskNodeConfig
v-if="currentNode"
ref="nodeSetting"
:flow-node="currentNode"
@find:return-task-nodes="findReturnTaskNodes"
/>
</template>
<script setup lang="ts">
import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
import { useWatchNode, useNodeName2 } from '../node'
import NodeHandler from '../NodeHandler.vue'
import UserTaskNodeConfig from '../nodes-config/UserTaskNodeConfig.vue'
defineOptions({
name: 'UserTaskNode'
})
const props = defineProps({
flowNode: {
type: Object as () => SimpleFlowNode,
required: true
}
})
const emits = defineEmits<{
'update:flowNode': [node: SimpleFlowNode | undefined]
'find:parentNode': [nodeList: SimpleFlowNode[], nodeType: NodeType]
}>()
// 监控节点变化
const currentNode = useWatchNode(props)
// 节点名称编辑
const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.START_USER_NODE)
const nodeSetting = ref()
// 打开节点配置
const openNodeConfig = () => {
// 把当前节点传递给配置组件
nodeSetting.value.showUserTaskNodeConfig(currentNode.value)
nodeSetting.value.openDrawer()
}
const deleteNode = () => {
emits('update:flowNode', currentNode.value.childNode)
}
// 查找可以驳回用户节点
const findReturnTaskNodes = (
matchNodeList: SimpleFlowNode[] // 匹配的节点
) => {
// 从父节点查找
emits('find:parentNode', matchNodeList, NodeType.USER_TASK_NODE)
}
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,33 @@
import { TimeUnitType, ApproveType, APPROVE_TYPE } from './consts'
// 获取条件节点默认的名称
export const getDefaultConditionNodeName = (index: number, defaultFlow: boolean | undefined): string => {
if (defaultFlow) {
return '其它情况'
}
return '条件' + (index + 1)
}
export const convertTimeUnit = (strTimeUnit: string) => {
if (strTimeUnit === 'M') {
return TimeUnitType.MINUTE
}
if (strTimeUnit === 'H') {
return TimeUnitType.HOUR
}
if (strTimeUnit === 'D') {
return TimeUnitType.DAY
}
return TimeUnitType.HOUR
}
export const getApproveTypeText = (approveType: ApproveType): string => {
let approveTypeText = ''
APPROVE_TYPE.forEach((item) => {
if (item.value === approveType) {
approveTypeText = item.label
return
}
})
return approveTypeText
}
@@ -0,0 +1,714 @@
.simple-flow-canvas {
position: absolute;
inset: 0;
z-index: 1;
overflow: auto;
background-color: #fafafa;
user-select: none;
.simple-flow-container {
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.top-area-container {
position: sticky;
inset: 0;
display: flex;
width: 100%;
height: 42px;
z-index: 1;
// padding: 4px 0;
background-color: #fff;
justify-content: flex-end;
align-items: center;
.top-actions {
display: flex;
margin: 4px;
margin-right: 8px;
align-items: center;
.canvas-control {
font-size: 16px;
.control-scale-group {
display: inline-flex;
align-items: center;
margin-right: 8px;
.control-scale-button {
display: inline-flex;
width: 28px;
height: 28px;
padding: 2px;
text-align: center;
cursor: pointer;
justify-content: center;
align-items: center;
}
.control-scale-label {
margin: 0 4px;
font-size: 14px;
}
}
}
}
}
.scale-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-top: 16px;
background-color: #fafafa;
transform-origin: 50% 0 0;
transform: scale(1);
transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
// 节点容器 定义节点宽度
.node-container {
width: 200px;
}
// 节点
.node-box {
position: relative;
display: flex;
min-height: 70px;
padding: 5px 10px 8px;
cursor: pointer;
background-color: #fff;
flex-direction: column;
border: 2px solid transparent;
// border-color: #0089ff;
border-radius: 8px;
// border-color: #0089ff;
box-shadow: 0 1px 4px 0 rgba(10, 30, 65, 0.16);
transition: all 0.1s cubic-bezier(0.645, 0.045, 0.355, 1);
&:hover {
border-color: #0089ff;
.node-toolbar {
opacity: 1;
}
.branch-node-move {
display: flex;
}
}
// 普通节点标题
.node-title-container {
display: flex;
padding: 4px;
cursor: pointer;
border-radius: 4px 4px 0 0;
align-items: center;
.node-title-icon {
display: flex;
align-items: center;
&.user-task {
color: #ff943e;
}
&.copy-task {
color: #3296fa;
}
&.start-user {
color: #676565;
}
}
.node-title {
margin-left: 4px;
font-size: 14px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #1f1f1f;
line-height: 18px;
&:hover {
border-bottom: 1px dashed #f60;
}
}
}
// 条件节点标题
.branch-node-title-container {
display: flex;
padding: 4px 0;
cursor: pointer;
border-radius: 4px 4px 0 0;
align-items: center;
justify-content: space-between;
.input-max-width {
max-width: 115px !important;
}
.branch-title {
font-size: 13px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #f60;
&:hover {
border-bottom: 1px dashed #000;
}
}
.branch-priority {
min-width: 50px;
font-size: 13px;
}
}
.node-content {
display: flex;
min-height: 32px;
padding: 4px 8px;
margin-top: 4px;
line-height: 32px;
justify-content: space-between;
align-items: center;
color: #111f2c;
background: rgba(0, 0, 0, 0.03);
border-radius: 4px;
.node-text {
display: -webkit-box;
overflow: hidden;
font-size: 14px;
line-height: 24px;
text-overflow: ellipsis;
word-break: break-all;
-webkit-line-clamp: 2; /* 这将限制文本显示为两行 */
-webkit-box-orient: vertical;
}
}
//条件节点内容
.branch-node-content {
display: flex;
min-height: 32px;
padding: 4px 8px;
margin-top: 4px;
line-height: 32px;
align-items: center;
color: #111f2c;
border-radius: 4px;
.branch-node-text {
overflow: hidden;
font-size: 14px;
line-height: 24px;
text-overflow: ellipsis;
word-break: break-all;
-webkit-line-clamp: 2; /* 这将限制文本显示为两行 */
-webkit-box-orient: vertical;
}
}
// 节点操作 :删除
.node-toolbar {
opacity: 0;
position: absolute;
top: -20px;
right: 0px;
display: flex;
.toolbar-icon {
text-align: center;
vertical-align: middle;
}
}
// 条件节点左右移动
.branch-node-move {
position: absolute;
width: 10px;
cursor: pointer;
display: none;
align-items: center;
height: 100%;
justify-content: center;
}
.move-node-left {
left: -2px;
top: 0px;
background: rgba(126, 134, 142, 0.08);
border-top-left-radius: 8px;
border-bottom-left-radius: 8px;
}
.move-node-right {
right: -2px;
top: 0px;
background: rgba(126, 134, 142, 0.08);
border-top-right-radius: 6px;
border-bottom-right-radius: 6px;
}
}
.node-config-error {
border-color: #ff5219 !important;
}
// 普通节点包装
.node-wrapper {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
// 节点连线处理
.node-handler-wrapper {
position: relative;
display: flex;
height: 70px;
align-items: center;
user-select: none;
justify-content: center;
flex-direction: column;
&::before {
position: absolute;
top: 0;
right: 0;
left: 0;
// bottom: 5px;
bottom: 0px;
z-index: 0;
width: 2px;
height: 100%;
// height: calc(100% - 5px);
margin: auto;
background-color: #dedede;
content: '';
}
.node-handler {
.add-icon {
position: relative;
top: -5px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
width: 25px;
height: 25px;
color: #fff;
background-color: #0089ff;
border-radius: 50%;
&:hover {
transform: scale(1.1);
}
}
}
.node-handler-arrow {
position: absolute;
bottom: 0;
left: 50%;
display: flex;
transform: translateX(-50%);
}
}
// 条件节点包装
.branch-node-wrapper {
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-top: 16px;
.branch-node-container {
position: relative;
display: flex;
&::before {
position: absolute;
height: 100%;
width: 4px;
background-color: #fafafa;
content: '';
left: 50%;
transform: translate(-50%);
}
.branch-node-add {
position: absolute;
top: -18px;
left: 50%;
z-index: 1;
height: 36px;
padding: 0 10px;
font-size: 12px;
line-height: 36px;
color: #222;
cursor: pointer;
background: #fff;
border: 2px solid #dedede;
border-radius: 18px;
transform: translateX(-50%);
transform-origin: center center;
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
}
.branch-node-item {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
min-width: 280px;
padding: 40px 40px 0;
background: transparent;
border-top: 2px solid #dedede;
border-bottom: 2px solid #dedede;
&::before {
position: absolute;
width: 2px;
height: 100%;
margin: auto;
inset: 0;
background-color: #dedede;
content: '';
}
}
// 覆盖条件节点第一个节点左上角的线
.branch-line-first-top {
position: absolute;
top: -5px;
left: -1px;
width: 50%;
height: 7px;
background-color: #fafafa;
content: '';
}
// 覆盖条件节点第一个节点左下角的线
.branch-line-first-bottom {
position: absolute;
bottom: -5px;
left: -1px;
width: 50%;
height: 7px;
background-color: #fafafa;
content: '';
}
// 覆盖条件节点最后一个节点右上角的线
.branch-line-last-top {
position: absolute;
top: -5px;
right: -1px;
width: 50%;
height: 7px;
background-color: #fafafa;
content: '';
}
// 覆盖条件节点最后一个节点右下角的线
.branch-line-last-bottom {
position: absolute;
right: -1px;
bottom: -5px;
width: 50%;
height: 7px;
background-color: #fafafa;
content: '';
}
}
}
.node-fixed-name {
display: inline-block;
width: auto;
padding: 0 4px;
overflow: hidden;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
}
// 开始节点包装
.start-node-wrapper {
position: relative;
margin-top: 16px;
.start-node-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.start-node-box {
display: flex;
justify-content: center;
align-items: center;
width: 90px;
height: 36px;
padding: 3px 4px;
color: #212121;
cursor: pointer;
// background: #2c2c2c;
background: #fafafa;
border-radius: 30px;
box-shadow: 0 1px 5px 0 rgba(10, 30, 65, 0.08);
box-sizing: border-box;
}
}
}
// 结束节点包装
.end-node-wrapper {
margin-bottom: 16px;
.end-node-box {
display: flex;
justify-content: center;
align-items: center;
width: 80px;
height: 36px;
color: #212121;
// background: #6e6e6e;
background: #fafafa;
border-radius: 30px;
box-shadow: 0 1px 5px 0 rgba(10, 30, 65, 0.08);
box-sizing: border-box;
}
}
// 可编辑的 title 输入框
.editable-title-input {
height: 20px;
max-width: 145px;
line-height: 20px;
font-size: 12px;
margin-left: 4px;
border: 1px solid #d9d9d9;
border-radius: 4px;
transition: all 0.3s;
&:focus {
border-color: #40a9ff;
outline: 0;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
}
}
}
}
// 配置节点头部
.config-header {
display: flex;
flex-direction: column;
.node-name {
display: flex;
height: 24px;
line-height: 24px;
font-size: 16px;
cursor: pointer;
align-items: center;
}
.divide-line {
width: 100%;
height: 1px;
margin-top: 16px;
background: #eee;
}
.config-editable-input {
height: 24px;
max-width: 510px;
font-size: 16px;
line-height: 24px;
border: 1px solid #d9d9d9;
border-radius: 4px;
transition: all 0.3s;
&:focus {
border-color: #40a9ff;
outline: 0;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
}
}
// 表单字段权限
.field-setting-pane {
display: flex;
flex-direction: column;
font-size: 14px;
.field-setting-desc {
padding-right: 8px;
margin-bottom: 16px;
font-size: 16px;
font-weight: 700;
}
.field-permit-title {
display: flex;
justify-content: space-between;
align-items: center;
height: 45px;
padding-left: 12px;
line-height: 45px;
background-color: #f8fafc0a;
border: 1px solid #1f38581a;
.first-title {
text-align: left !important;
}
.other-titles {
display: flex;
justify-content: space-between;
}
.setting-title-label {
display: inline-block;
width: 110px;
padding: 5px 0;
font-size: 13px;
font-weight: 700;
color: #000;
text-align: center;
}
}
.field-setting-item {
align-items: center;
display: flex;
justify-content: space-between;
height: 38px;
padding-left: 12px;
border: 1px solid #1f38581a;
border-top: 0;
.field-setting-item-label {
display: inline-block;
width: 110px;
min-height: 16px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: text;
}
.field-setting-item-group {
display: flex;
justify-content: space-between;
.item-radio-wrap {
display: inline-block;
width: 110px;
text-align: center;
}
}
}
}
// 节点连线气泡卡片样式
.handler-item-wrapper {
display: flex;
cursor: pointer;
.handler-item {
margin-right: 8px;
}
.handler-item-icon {
width: 80px;
height: 80px;
background: #fff;
border: 1px solid #e2e2e2;
border-radius: 50%;
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
user-select: none;
text-align: center;
&:hover {
background: #e2e2e2;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
}
.icon-size {
font-size: 35px;
line-height: 80px;
}
}
.approve {
color: #ff943e;
}
.copy {
color: #3296fa;
}
.condition {
color: #15bc83;
}
.handler-item-text {
margin-top: 4px;
width: 80px;
text-align: center;
}
}
// iconfont 样式
@font-face {
font-family: 'iconfont'; /* Project id 4495938 */
src:
url('iconfont.woff2?t=1724339470412') format('woff2'),
url('iconfont.woff?t=1724339470412') format('woff'),
url('iconfont.ttf?t=1724339470412') format('truetype');
}
.iconfont {
font-family: 'iconfont' !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-start-user:before {
content: '\e679';
}
.icon-inclusive:before {
content: '\e602';
}
.icon-copy:before {
content: '\e7eb';
}
.icon-handle:before {
content: '\e61c';
}
.icon-exclusive:before {
content: '\e717';
}
.icon-approve:before {
content: '\e715';
}
.icon-parallel:before {
content: '\e688';
}
+20 -4
View File
@@ -1,5 +1,5 @@
<template>
<div class="upload-file">
<div v-if="!disabled" class="upload-file">
<el-upload
ref="uploadRef"
v-model:file-list="fileList"
@@ -20,11 +20,11 @@
class="upload-file-uploader"
name="file"
>
<el-button v-if="!disabled" type="primary">
<el-button type="primary">
<Icon icon="ep:upload-filled" />
选取文件
</el-button>
<template v-if="isShowTip && !disabled" #tip>
<template v-if="isShowTip" #tip>
<div style="font-size: 8px">
大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
</div>
@@ -32,7 +32,6 @@
格式为 <b style="color: #f56c6c">{{ fileType.join('/') }}</b> 的文件
</div>
</template>
<!-- TODO @puhui999:1)表单展示的时候,位置会偏掉,已发微信;2)disable 的时候,应该把【删除】按钮也隐藏掉? -->
<template #file="row">
<div class="flex items-center">
<span>{{ row.file.name }}</span>
@@ -54,6 +53,18 @@
</template>
</el-upload>
</div>
<!-- 上传操作禁用时 -->
<div v-if="disabled" class="upload-file">
<div v-for="(file, index) in fileList" :key="index" class="flex items-center file-list-item">
<span>{{ file.name }}</span>
<div class="ml-10px">
<el-link :href="file.url" :underline="false" download target="_blank" type="primary">
下载
</el-link>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { propTypes } from '@/utils/propTypes'
@@ -211,4 +222,9 @@ const emitUpdateModelValue = () => {
:deep(.ele-upload-list__item-content-action .el-link) {
margin-right: 10px;
}
.file-list-item {
border: 1px dashed var(--el-border-color-darker);
border-radius: 8px;
}
</style>
+9 -14
View File
@@ -25,7 +25,7 @@
<template #file="{ file }">
<img :src="file.url" class="upload-image" />
<div class="upload-handle" @click.stop>
<div class="handle-icon" @click="handlePictureCardPreview(file)">
<div class="handle-icon" @click="imagePreview(file.url!)">
<Icon icon="ep:zoom-in" />
<span>查看</span>
</div>
@@ -39,16 +39,12 @@
<div class="el-upload__tip">
<slot name="tip"></slot>
</div>
<el-image-viewer
v-if="imgViewVisible"
:url-list="[viewImageUrl]"
@close="imgViewVisible = false"
/>
</div>
</template>
<script lang="ts" setup>
import type { UploadFile, UploadProps, UploadUserFile } from 'element-plus'
import { ElNotification } from 'element-plus'
import { createImageViewer } from '@/components/ImageViewer'
import { propTypes } from '@/utils/propTypes'
import { useUpload } from '@/components/UploadFile/src/useUpload'
@@ -56,6 +52,13 @@ import { useUpload } from '@/components/UploadFile/src/useUpload'
defineOptions({ name: 'UploadImgs' })
const message = useMessage() // 消息弹窗
// 查看图片
const imagePreview = (imgUrl: string) => {
createImageViewer({
zIndex: 9999999,
urlList: [imgUrl]
})
}
type FileTypes =
| 'image/apng'
@@ -178,14 +181,6 @@ const handleExceed = () => {
type: 'warning'
})
}
// 图片预览
const viewImageUrl = ref('')
const imgViewVisible = ref(false)
const handlePictureCardPreview: UploadProps['onPreview'] = (uploadFile) => {
viewImageUrl.value = uploadFile.url!
imgViewVisible.value = true
}
</script>
<style lang="scss" scoped>
Loaded 100 of 349 files, more files were not shown because too many files have changed in this diff. Show more