234 Commits

Author SHA1 Message Date
8a453f5852 chore(projects): release v1.1.3 2025-08-16 11:29:13 +08:00
4d120ffe63 Merge branch 'dev' into flow 2025-08-16 11:15:57 +08:00
4e625111ce chore(projects): release v1.1.3 2025-08-16 11:14:41 +08:00
AN
8524ae7666 Merge branch 'dev' of https://gitee.com/xlsea/ruoyi-plus-soybean into dev 2025-08-15 17:49:07 +08:00
AN
d6ae85d218 fix(projects): 修复新增用户未查询角色列表问题 2025-08-15 17:45:11 +08:00
89719abe34 Merge remote-tracking branch 'Soybean/main' into dev
# Conflicts:
#	README.en_US.md
#	README.md
2025-08-14 18:10:28 +08:00
8d7f91dccf chore(other): update the ESLint validation configuration to support more file types. 2025-08-13 10:10:10 +08:00
33ade53904 chore(readme): remove DartNode sponsorship badge from README files 2025-08-11 10:46:15 +08:00
AN
ab9c84d831 Merge branch 'dev' of https://gitee.com/xlsea/ruoyi-plus-soybean into dev 2025-08-11 09:42:59 +08:00
AN
7651816495 typo(projects): 去除console打印 2025-08-11 09:22:05 +08:00
4539fe01fb fix(projects): Fix the icon size in the image preview toolbar 2025-08-08 17:17:23 +08:00
AN
71297cf2e4 optimize(types): 补充标签类型 2025-08-07 20:44:13 +08:00
AN
30316d7e4a optimize(types): 补充标签类型 2025-08-07 20:39:17 +08:00
AN
aaad817131 Merge branch 'dev' of https://gitee.com/xlsea/ruoyi-plus-soybean into flow 2025-08-07 20:36:14 +08:00
AN
4e9839bd48 fix(projects): 修复用户导入功能无法更新问题 2025-08-07 20:29:56 +08:00
AN
a15b683b1d fix(projects): 修复用户新增时角色下拉包含超级管理员问题 2025-08-06 09:12:24 +08:00
9df8d2f55f Merge remote-tracking branch 'origin/dev' into dev 2025-07-31 13:55:10 +08:00
710374398a fix(projects): 修复登录过期事件监听未被重置 2025-07-31 13:52:07 +08:00
AN
52318c106d fix(projects): 修复日期搜索条件清除问题 2025-07-31 13:30:51 +08:00
AN
41c74db1cd Merge branch 'dev' of https://gitee.com/xlsea/ruoyi-plus-soybean into flow 2025-07-30 20:44:52 +08:00
9027632bef !25 fix(project): 关闭多租户功能后仍然遍历租户列表导致控制台报错的问题
Merge pull request !25 from littleghost2016/dev
2025-07-30 12:43:44 +00:00
b96c46baa9 fix(project): 关闭多租户功能后仍然遍历租户列表导致控制台报错的问题 2025-07-30 18:02:24 +08:00
AN
befc61a4ff Merge branch 'flow' of https://gitee.com/xlsea/ruoyi-plus-soybean into flow 2025-07-29 16:16:55 +08:00
AN
d1f0ce28cb Merge branch 'dev' of https://gitee.com/xlsea/ruoyi-plus-soybean into flow 2025-07-29 16:15:56 +08:00
AN
2d31d7dc62 fix(hooks): 修复oss下载时未转码问题 2025-07-28 17:56:12 +08:00
e538355f2b refactor(projects): 菜单列表新增禁用菜单样式 2025-07-26 12:55:19 +08:00
f89835578c fix(hooks): 非安全环境下不使用流式下载 2025-07-26 12:43:10 +08:00
4eb77eac78 !24 refactor(menu): 菜单管理中隐藏的菜单显示灰色
Merge pull request !24 from NicholasLD/N/A
2025-07-25 07:27:35 +00:00
adca2e26be refactor(menu): 菜单管理中隐藏的菜单显示灰色
Signed-off-by: NicholasLD <nicholasld505@gmail.com>
2025-07-25 03:51:09 +00:00
AN
ff576f3f42 Merge branch 'dev' of https://gitee.com/xlsea/ruoyi-plus-soybean into dev 2025-07-25 10:31:57 +08:00
AN
8fcc70d73d fix(projects): 修复一级菜单隐藏失效问题 2025-07-25 10:25:50 +08:00
c87aa6652c Merge branch 'dev' into flow 2025-07-24 17:45:50 +08:00
48f603ed32 chore(projects): release v1.1.2 2025-07-24 17:44:45 +08:00
f4038a2dc0 fix(projects): 修复菜单结构变动后路由无法进入问题 2025-07-24 17:36:52 +08:00
AN
7484f79b8f docs(projects): 流程表达式菜单sql更新 2025-07-23 20:55:40 +08:00
AN
a9c58b2590 fix(projects): 修复抽屉问题 2025-07-23 14:20:46 +08:00
AN
d562f8c155 feat(projects): 新增流程表达式功能 2025-07-22 17:44:04 +08:00
2e02992906 fix(projects): 修复动态组件弹窗动画问题 2025-07-21 10:54:16 +08:00
f138e34ef9 Merge remote-tracking branch 'origin/flow' into flow 2025-07-21 09:16:33 +08:00
923eb98a5c fix(readme): update GitHub stars and forks links for gitee 2025-07-20 01:12:19 +08:00
AN
94fafe3938 optimize(projects): 优化搜索FormItem展示 2025-07-19 21:20:14 +08:00
c7c4e23be5 Merge remote-tracking branch 'origin/flow' into flow 2025-07-19 15:35:55 +08:00
AN
e64d9b38c9 Merge branch 'dev' of https://gitee.com/xlsea/ruoyi-plus-soybean into flow 2025-07-19 13:13:29 +08:00
AN
a1336d1536 optimize(projects): 优化搜索框FormItem 2025-07-19 12:21:53 +08:00
AN
b6c7b1b383 feat(projects): 补充搜索条件 2025-07-18 23:13:19 +08:00
AN
b46f172637 Merge branch 'dev' of https://gitee.com/xlsea/ruoyi-plus-soybean into flow 2025-07-18 22:54:47 +08:00
7ffaac5893 refactor(projects): 移动工作流组件位置 2025-07-18 11:55:23 +08:00
AN
cc29ea85c1 style(projects): 搜索FormItem占比调整 2025-07-17 21:44:00 +08:00
AN
2edd4bc9f1 Merge branch 'dev' of https://gitee.com/xlsea/ruoyi-plus-soybean into dev 2025-07-17 16:02:29 +08:00
AN
e485f680c7 fix(projects): 修复登录过期不弹窗问题 2025-07-17 16:00:50 +08:00
0dcfc893dd Merge branch 'dev' into flow 2025-07-17 11:08:53 +08:00
2f8797eb98 docs(projects): 修改更新日志 2025-07-17 11:06:52 +08:00
2a3f3a4812 Merge remote-tracking branch 'Soybean/main' into dev
# Conflicts:
#	package.json
#	pnpm-lock.yaml
2025-07-17 10:57:54 +08:00
AN
22710ecbd5 refactor(utils): 简化加载组件方法 2025-07-16 15:24:14 +08:00
AN
6c6086f89d optimize(projects): 优化代码 2025-07-16 14:13:11 +08:00
AN
59a69dd9f0 optimize(projects): 优化代码 2025-07-15 22:49:55 +08:00
AN
01d42722f5 feat(projects): 新增我的已办,我的抄送功能 2025-07-15 21:45:23 +08:00
AN
bd6b575af6 feat(types): 补充类型定义 2025-07-15 16:24:42 +08:00
AN
6dc7b2a234 Merge branch 'dev' of https://gitee.com/xlsea/ruoyi-plus-soybean into flow 2025-07-15 16:14:31 +08:00
AN
2587f8cbfa fix(projects): 修复刷新时跳转至登录页问题 2025-07-15 16:09:34 +08:00
AN
30878fb4d3 Merge branch 'dev' of https://gitee.com/xlsea/ruoyi-plus-soybean into flow 2025-07-14 22:38:08 +08:00
AN
5e49622ea8 refactor(projects): 文件命名修正 2025-07-14 22:36:30 +08:00
AN
130ee1dcec feat(projects): 新增我的待办功能,新增审批,驳回组件 2025-07-14 22:29:59 +08:00
2036391c41 !17 fix: 修复 api.d.ts.vm 代码生成模板bug
Merge pull request !17 from 月祈风华/dev
2025-07-14 04:12:42 +00:00
e89b86ce56 chore(deps): update deps 2025-07-14 00:44:44 +08:00
AN
523aca6b75 feat(projects): 新增抄送、下一审批人提交功能,优化组件通用性 2025-07-13 22:07:40 +08:00
4e8c8715ae fix: 修复 api.d.ts.vm 代码生成模板bug 2025-07-13 01:42:21 +08:00
e5ec915fd9 chore(projects): release v1.1.1 2025-07-11 18:04:51 +08:00
89c716e12a chore(deps): update deps 2025-07-11 17:49:57 +08:00
2d02128164 Merge remote-tracking branch 'Soybean/main' into dev
# Conflicts:
#	.vscode/settings.json
#	README.en_US.md
#	README.md
#	package.json
#	pnpm-lock.yaml
2025-07-11 17:46:01 +08:00
9ca7ca8fda fix(packages): 修复 cleanup 会删除富文本编辑器资源问题 2025-07-11 17:40:29 +08:00
d85424ee83 feat(projects): 修改操作后列表查询方式 2025-07-11 17:16:03 +08:00
AN
3a506df9b9 feat-wip(projects): 办理功能弹窗适配 2025-07-10 22:22:14 +08:00
AN
39b0c636f2 Merge branch 'dev' of https://gitee.com/xlsea/ruoyi-plus-soybean into flow 2025-07-10 22:20:40 +08:00
ff87415d7b fix(projects): 修复角色用户分配未调用接口问题 2025-07-10 13:55:07 +08:00
AN
400eaf8990 fix(projects): 修复更新后产生问题 2025-07-09 13:43:44 +08:00
312709706b Merge remote-tracking branch 'origin/dev' into dev 2025-07-09 09:38:12 +08:00
3ae9922dc4 docs(other): 修改文档内容 2025-07-09 09:30:46 +08:00
247b98a542 Merge remote-tracking branch 'origin/dev' into dev 2025-07-08 23:06:21 +08:00
566b2c2db8 fix(hooks): 解决 streamsaver 访问不到 Github 资源问题 2025-07-08 23:05:56 +08:00
AN
2dbf8b3dfa Merge branch 'dev' of https://gitee.com/xlsea/ruoyi-plus-soybean into flow 2025-07-08 22:18:32 +08:00
AN
90d32ee29a fix(utils): 修复isNull和IsNotNull判断方法潜在问题 2025-07-08 22:09:45 +08:00
AN
efc953c094 fix(projects): 修复用户导入结果信息未渲染标签问题 2025-07-08 17:39:44 +08:00
ad48d8e840 feat(projects): 角色分配用户新增部门与时间查询条件 2025-07-04 15:37:11 +08:00
62f2c6d571 fix(projects): 修复角色列表操作栏展示不全问题 2025-07-04 15:19:35 +08:00
03dd64c543 chore(packages): update Vite version to 7 in package.json and documentation.
(cherry picked from commit ef806edd9d0c48ad8669863516d52e2eb8870d6f)
2025-07-03 22:08:50 +08:00
aeb6369005 chore(deps): update deps 2025-07-03 22:06:30 +08:00
133196f337 chore(vscode): remove unused vue.server.hybridMode setting from .vscode/settings.json 2025-07-03 22:02:30 +08:00
41191d54fb fix(projects): Fix i18n-ally not working when setting moduleResolution to bundler. fixed #780 2025-07-03 21:59:18 +08:00
AN
229e00443f fix(projects): 修复未清空文件列表,上传回显问题 2025-07-02 10:26:42 +08:00
AN
fbec787a99 Merge branch 'dev' of https://gitee.com/xlsea/ruoyi-plus-soybean into flow 2025-07-01 22:17:02 +08:00
AN
4e1f539576 feat-wip(projects): 新增我的待办列表 2025-07-01 22:13:57 +08:00
AN
85c8a9fffa Merge branch 'dev' of https://gitee.com/xlsea/ruoyi-plus-soybean into dev 2025-07-01 19:09:27 +08:00
AN
b99999355c refactor(projects): 调整租户套餐菜单接口 2025-07-01 19:06:02 +08:00
baa0584bdc Merge branch 'master' into flow 2025-07-01 10:54:32 +08:00
7ddf1cf5ae chore(projects): release v1.1.0 2025-07-01 10:46:39 +08:00
814b291c58 chore(projects): release v1.1.0 2025-07-01 10:42:40 +08:00
f7c7fc41da fix(other): 修复代码生成类型定义文件重复问题 2025-07-01 10:37:34 +08:00
53fa87dae2 Merge remote-tracking branch 'origin/dev' into dev 2025-07-01 10:09:53 +08:00
99675cbc0e docs(readme): 更新 README.md 文件 2025-07-01 10:08:09 +08:00
ad9386eb58 !11 fix(projects): 修复部门字典 sys_normal_disable 重复获取
Merge pull request !11 from 素还真/N/A
2025-07-01 02:05:34 +00:00
440fd836e2 update src/views/system/dept/modules/dept-search.vue.
部门字典 sys_normal_disable 重复获取

Signed-off-by: 素还真 <11555891+metabytes@user.noreply.gitee.com>
2025-07-01 01:59:02 +00:00
AN
a77edc2e36 feat(projects): 新增 '我发起的' 功能 2025-06-30 22:37:29 +08:00
AN
c3ea81dc0d Merge branch 'dev' of https://gitee.com/xlsea/ruoyi-plus-soybean into flow 2025-06-30 20:57:32 +08:00
AN
6fc7b11b18 fix(projects): 修复代码生成逻辑判断问题 2025-06-30 20:44:20 +08:00
7c6ca91ef2 chore(projects): update pnpm-lock.yaml 2025-06-30 17:21:05 +08:00
c789867de3 Merge remote-tracking branch 'soybean/main' into dev
# Conflicts:
#	README.en_US.md
#	README.md
#	package.json
#	pnpm-lock.yaml
2025-06-30 17:17:43 +08:00
90145fa53b fix(styles): 修复登录页平板界面滚动问题 2025-06-30 11:55:32 +08:00
a31994dc98 docs(projects): 优化 cursor 规则及 mcp 2025-06-30 11:44:32 +08:00
9a480d2245 !8 优化模板:index[-tree].vue页面引用API接口路径遇到驼峰报错问题
Merge pull request !8 from 唐振超/dev
2025-06-30 01:15:55 +00:00
7b18c0c210 !7 fix 修复api层级目录不正确
Merge pull request !7 from 这夏天依然平凡/dev
2025-06-29 12:32:03 +00:00
d13beac046 update docs/template/index-tree.vue.vm.
Signed-off-by: 唐振超 <imtzc@qq.com>
2025-06-29 01:07:22 +00:00
3122cf4df5 update docs/template/index.vue.vm.
Signed-off-by: 唐振超 <imtzc@qq.com>
2025-06-29 01:07:04 +00:00
AN
f1d7b9733f feat(projects): 新增减签功能 2025-06-28 22:07:02 +08:00
2f1733bae1 fix 修复api层级目录不正确
Signed-off-by: 这夏天依然平凡 <1822213252@qq.com>
2025-06-28 10:01:20 +00:00
AN
dbcf8d422a fix(projects): 修改强退在线设备接口 2025-06-27 21:26:47 +08:00
AN
55dceca28b feat(projects): 新增加签功能 2025-06-27 18:22:21 +08:00
5cb1cebd88 chore(deps): update deps 2025-06-27 18:17:05 +08:00
AN
188533adc9 Merge branch 'dev' of https://gitee.com/xlsea/ruoyi-plus-soybean into flow 2025-06-26 17:14:41 +08:00
3628c2496a fix(projects): 修复字典数据重复获取问题 2025-06-26 17:12:51 +08:00
650673e2db feat(hooks): 重构下载方法,支持流式下载 2025-06-26 14:14:02 +08:00
AN
80faf4b47c feat(projects): 新增转办和终止功能 2025-06-25 23:19:32 +08:00
a5c4b4e3b7 chore(deps): update NodeJS and pnpm version requirements in package.json and documentation 2025-06-25 18:12:25 +08:00
AN
89e7edb380 docs(projects): 新增工作流更新sql 2025-06-25 13:33:14 +08:00
fec0563ef7 chore(deps): update deps 2025-06-25 11:20:18 +08:00
1680ce4e26 Merge remote-tracking branch 'soybean/main' into dev
# Conflicts:
#	CHANGELOG.md
#	README.en_US.md
#	README.md
#	package.json
#	pnpm-lock.yaml
#	src/App.vue
#	src/theme/settings.ts
#	src/views/_builtin/iframe-page/[url].vue
2025-06-25 11:17:46 +08:00
AN
b8c771cd1d feat(projects): 新增用户选择器组件,添加流程干预按钮 2025-06-24 23:46:15 +08:00
87a675bf62 chore(projects): release v1.3.15 2025-06-24 21:40:19 +08:00
4d42dcbea8 docs(readme): add warning about upcoming V2 version and link to plan list 2025-06-24 21:39:21 +08:00
276d836c87 refactor(iframe-page): remove unused lifecycle hooks and clean up script setup 2025-06-24 21:31:57 +08:00
7d84062e2c fix(app): replace console.error with window.console.error for consistency 2025-06-24 21:31:17 +08:00
afd604212b fix(projects): ensure proper text color when themes are inverted 2025-06-24 21:20:24 +08:00
fcb89883fa optimize(components): optimize spacing for lang-switch dropdown options 2025-06-24 21:20:20 +08:00
dc674ce870 chore(deps): update deps 2025-06-24 21:11:08 +08:00
dbd995c12c chore(projects): update deps & fix moduleResolution 2025-06-24 19:29:12 +08:00
7b2e510a2f docs(other): update docs with video tutorial link. 2025-06-24 10:17:50 +08:00
AN
81449ea77a feat-wip(projects): 新增流程干预,组件命名统一 2025-06-23 22:35:44 +08:00
AN
1c322e28ef feat(projects): 新增group-tag组件,待办任务查看功能 2025-06-22 18:59:08 +08:00
AN
54fa7caf03 Merge branch 'dev' of https://gitee.com/xlsea/ruoyi-plus-soybean into flow 2025-06-22 16:10:08 +08:00
AN
62e6c7763c fix(projects): 调整属性名 2025-06-22 14:41:05 +08:00
AN
8b3151b8ce fix(projects): 修复个人信息-修改密码未加密且参数错误问题 2025-06-22 12:49:51 +08:00
AN
b3dccb542e feat-wip(projects): 新增待办任务功能,优化代码 2025-06-22 12:33:56 +08:00
AN
ae5c7e8372 feat(components): 面板添加流程图 2025-06-21 17:20:33 +08:00
AN
2f8a6b4b84 optimize(projects): 动态加载组件方法抽取为公共函数 2025-06-21 13:48:11 +08:00
AN
e86a6d1b7a refactor(types): 移除无用type 2025-06-21 11:05:54 +08:00
AN
a3dcee4a11 refactor(projects): 修改流程实例动态引入组件 2025-06-21 10:50:48 +08:00
AN
f36ac9abc6 refactor(projects): 补充formTip信息 2025-06-20 21:47:43 +08:00
8b4e41ce1b style(projects): 优化移动端字体大小 2025-06-20 00:02:43 +08:00
AN
50e7b5158d feat(projects): 优化组件,完成流程实例-流程预览 2025-06-19 23:54:21 +08:00
742e3858ab feat(styles): 修复登录页移动端显示问题 2025-06-19 23:52:39 +08:00
907f043969 style(projects): 修改按钮文本颜色 2025-06-19 23:30:54 +08:00
adf3d87e5c revert(projects): 单点登录回调页面回滚 2025-06-19 23:20:06 +08:00
AN
2b5735ab34 Merge branch 'dev' of https://gitee.com/xlsea/ruoyi-plus-soybean into flow 2025-06-19 23:18:02 +08:00
AN
56bfd64fbe Merge branch 'flow' of https://gitee.com/xlsea/ruoyi-plus-soybean into flow 2025-06-19 23:17:22 +08:00
47d5c1c71b revert(projects): 单点登录回调页面回滚 2025-06-19 23:15:03 +08:00
AN
49224afe2d feat(components): 增强审批信息面板,优化附件处理 2025-06-19 23:14:25 +08:00
7846b2cb1f typo(projects): 修改标题内容 2025-06-19 23:06:41 +08:00
406800de59 style(projects): 重构登录页样式 2025-06-19 23:04:09 +08:00
7e4ecae6cb style(projects): 更换 logo 与加载样式 2025-06-19 21:28:19 +08:00
471912e17f optimize(projects): 优化接口请求异常拦截代码 2025-06-19 09:33:46 +08:00
031d071af1 fix(projects): 修复接口请求异常拦截问题 2025-06-19 09:26:46 +08:00
6e6cc4d91f fix(components): 修复菜单选择标签渲染问题 2025-06-18 23:48:49 +08:00
8ce02aa15a Merge branch 'dev' into flow 2025-06-18 23:22:36 +08:00
8c84063ad1 feat(projects): 新增字典多语言适配 2025-06-18 23:20:36 +08:00
AN
496ed978ca Merge branch 'dev' of https://gitee.com/xlsea/ruoyi-plus-soybean into flow 2025-06-18 23:09:07 +08:00
AN
fb652ce7ff feat-wip(components): 审批信息流程图组件 2025-06-18 23:06:40 +08:00
0f33f4a301 feat(other): 新增菜单字典多语言适配 SQL 2025-06-18 22:58:48 +08:00
27f061957e fix(projects): 修复切换用户或登录过期部分问题 2025-06-18 21:07:59 +08:00
AN
90ebf83501 Merge branch 'dev' of https://gitee.com/xlsea/ruoyi-plus-soybean into flow 2025-06-18 17:25:13 +08:00
AN
72b8f56e32 fix(projects): 目录类型禁用iframe选项 2025-06-18 17:06:13 +08:00
AN
b3f81ba3f0 optimize(projects): 统一button的添加方式 2025-06-18 15:15:19 +08:00
AN
20b96ac54b Merge branch 'dev' of https://gitee.com/xlsea/ruoyi-plus-soybean into flow 2025-06-18 14:23:38 +08:00
AN
0ac0a093a4 fix(projects): 修复权限字符显示逻辑错误问题 2025-06-18 14:02:50 +08:00
AN
85115ce327 feat-wip(components): 抽取leaveEdit组件,适配流程编辑和预览功能 2025-06-17 23:57:04 +08:00
1cbeb59c0b Merge branch 'dev' into flow 2025-06-17 14:07:36 +08:00
94d1863ef3 fix(other): 修复代码生成字典相关问题 2025-06-17 14:03:47 +08:00
ffa47c37fa fix(projects): 修复导出查询参数问题 2025-06-17 14:01:41 +08:00
9aa6597d5e fix(other): 修复代码生成字典相关问题 2025-06-17 11:42:08 +08:00
d1377baaed Merge branch 'dev' into flow 2025-06-16 17:59:28 +08:00
031b7f698a fix(projects): 修复首页未从环境变量获取问题 2025-06-16 17:56:25 +08:00
da149e5bbd fix(types): The environment variable VITE_ICON_LOCAL_PREFIX has the wrong type. 2025-06-14 22:01:01 +08:00
f0810bce4c fix(other): 代码生成模板 dateRangeTime 错误 2025-06-14 13:42:47 +08:00
1ec1099179 fix(other): 修复代码生成问题 2025-06-14 13:40:15 +08:00
AN
b6f4fb5aec refactor(projects): 重构流程设计菜单层级 2025-06-14 10:07:08 +08:00
AN
1af4e96382 docs(projects): 流程定义菜单更新 2025-06-13 09:23:53 +08:00
AN
997f4a2d61 feat-wip(projects): 流程定义完成 2025-06-13 00:20:45 +08:00
AN
f52fa40326 Merge branch 'dev' of https://gitee.com/xlsea/ruoyi-plus-soybean into flow 2025-06-12 21:26:49 +08:00
AN
cafee1dbd9 fix(projects): 修复登录过期后,重复弹窗问题 2025-06-12 09:45:50 +08:00
AN
7b2c857f6d feat-wip(projects): 对接流程定义功能ing 2025-06-11 23:32:59 +08:00
AN
4f6c14f358 Merge branch 'dev' of https://gitee.com/xlsea/ruoyi-plus-soybean into flow 2025-06-11 20:51:46 +08:00
39dd9acca9 feat(projects): 菜单字典适配 i18n 2025-06-11 20:36:53 +08:00
AN
d141ed5bef chore(projects): 移除未使用代码 2025-06-11 11:23:51 +08:00
AN
e16a0fa6ed fix(components): 修复上传组件回显问题,修改accept参数逻辑 2025-06-11 11:09:34 +08:00
03c8a7f5b7 feat(components): 新增表单上传组件 2025-06-10 22:00:28 +08:00
AN
da1c16e023 fix(components): 修复部门选择组件非树结构,默认展开失败问题 2025-06-10 20:33:11 +08:00
7c3dac4212 feat(projects): add configurable user name watermark option 2025-06-10 19:05:08 +08:00
aeb736ebf1 fix(components): 修复树选择组件再次勾选父子联动导致全选问题 2025-06-10 11:19:04 +08:00
bbda803e90 fix(components): 修复菜单树选择组件 2025-06-10 11:05:50 +08:00
94f183e7b5 merge(project): merge soybean 1.3.14 2025-06-10 09:46:43 +08:00
39b89a1234 chore(projects): release v1.3.14 2025-06-09 22:40:03 +08:00
c57f88aad2 fix(auth): remove redundant authStore declaration in resetStore function 2025-06-09 22:34:57 +08:00
3e4e17abd8 chore(deps): update deps 2025-06-09 22:10:30 +08:00
AN
858c318002 optimize(projects): optimize tab deletion logic 2025-06-09 21:28:03 +08:00
e6044d0fc7 optimize(projects): optimize tab deletion logic. closed #755 2025-06-09 14:19:44 +08:00
AN
a0f33664ec fix(projects): 修复自定义数据权限没有保存角色部门bug 2025-06-09 13:20:28 +08:00
AN
8bb31b1c36 Merge branch 'dev' of https://gitee.com/xlsea/ruoyi-plus-soybean into dev 2025-06-08 22:51:46 +08:00
AN
64bd119c29 fix(utils): 修复 删除当前tab为最后一个时,tab切换错误bug. 2025-06-08 22:49:41 +08:00
AN
ffd6211e4d refactor(projects): 调整为批量上传文件 2025-06-08 22:36:33 +08:00
2ed0b6484c feat(docs): add DartNode sponsorship badge to README files 2025-06-06 22:50:17 +08:00
e75c552551 docs(projects): 新增赞助列表 2025-06-06 14:34:14 +08:00
AN
d37adc362d fix(styles): 添加滚动条,去除页码 2025-06-06 13:07:21 +08:00
9ff15feee4 docs(projects): 更换微信群聊二维码 2025-06-06 09:49:25 +08:00
7bec9d1476 docs(projects): 修改部署工作流与项目说明文件 2025-06-06 09:36:29 +08:00
AN
56d6d77da5 refactor: 优化代码 2025-06-03 21:48:38 +08:00
AN
394db6fec2 refactor: remove WorkflowLeaveForm type from Vue typings 2025-06-01 00:08:33 +08:00
AN
4614b97796 feat: 更新工作流任务申请模态框样式,添加消息类型选择禁用功能 2025-06-01 00:06:54 +08:00
AN
49521b667d feat: 更新请假申请表单,添加流程类型选择和流程启动功能 2025-05-31 23:48:36 +08:00
AN
ab1d3a237e feat: 添加请假申请详情接口,优化请假操作表单样式 2025-05-30 17:48:43 +08:00
AN
3629c7a9dd Merge branch 'dev' of https://gitee.com/xlsea/ruoyi-plus-soybean into flow 2025-05-30 17:28:44 +08:00
AN
89a2a6cbf4 feat: 更新请假申请功能,添加日期范围选择和请假天数自动计算 2025-05-29 22:35:29 +08:00
AN
d7e0516cfb feat: 添加请假申请功能 2025-05-29 10:34:00 +08:00
AN
b265f590e4 Merge branch 'dev' of https://gitee.com/xlsea/ruoyi-plus-soybean into flow 2025-05-28 18:37:15 +08:00
AN
210c00c686 Merge branch 'dev' of https://gitee.com/xlsea/ruoyi-plus-soybean into flow 2025-05-28 11:25:26 +08:00
AN
f004e75cc1 fix: 修复添加行时的操作顺序问题 2025-05-27 23:13:35 +08:00
AN
14a29070c9 feat: 更新工作流分类选择组件,修复值回显问题并优化添加数据操作 2025-05-27 22:41:48 +08:00
AN
92b9c213d5 feat: 流程实例,查看变量功能 2025-05-26 22:19:39 +08:00
AN
ed1180696f fix: 修复运行状态变化时数据加载顺序问题 2025-05-25 17:01:04 +08:00
AN
1c64693774 fix: 修复控制台报错:Message compilation error,i18n的@为特殊符号 2025-05-25 09:28:54 +08:00
AN
350de08f8f feat: 增加流程作废功能并优化按钮组件 2025-05-25 09:10:49 +08:00
AN
f9d57f1b71 feat: 添加流程实例功能 2025-05-24 00:49:15 +08:00
11aba9e2c8 feat: 新增流程定义页面 2025-05-22 22:56:59 +08:00
32c241564d Merge branch 'dev' into flow 2025-05-22 19:14:20 +08:00
AN
b1cb10581e Merge branch 'dev' of https://gitee.com/xlsea/ruoyi-plus-soybean into flow 2025-05-20 20:54:25 +08:00
AN
25790d4b0f feat: 对接工作流分类模块 2025-05-19 23:51:32 +08:00
193 changed files with 12354 additions and 4459 deletions

View File

@ -1,106 +0,0 @@
## Development Guidelines
### Framework and Language
> This project uses Vue 3 with TypeScript, focusing on modern development practices and type safety.
**Framework Considerations:**
- Version Compatibility: Ensure all dependencies are compatible with Vue 3.4+ and TypeScript 5.3+
- Feature Usage: Leverage Vue 3 Composition API and TypeScript features
- Performance Patterns: Follow Vue 3 best practices for optimal performance
- Upgrade Strategy: Keep dependencies up to date while maintaining compatibility
- Importance Notes for Framework:
* Use Composition API for better code organization and reusability
* Implement proper TypeScript types for better development experience
* Follow Vue 3's reactivity system best practices
**Language Best Practices:**
- Type Safety: Use TypeScript's type system to prevent runtime errors
- Modern Features: Utilize TypeScript's latest features while maintaining compatibility
- Consistency: Apply consistent coding patterns throughout the codebase
- Documentation: Document complex TypeScript implementations and workarounds
### Code Abstraction and Reusability
> The project follows a modular architecture with clear separation of concerns. Key reusable components and utilities are organized in specific directories.
**Modular Design Principles:**
- Single Responsibility: Each module is responsible for only one functionality
- High Cohesion, Low Coupling: Related functions are centralized, reducing dependencies between modules
- Stable Interfaces: Expose stable interfaces externally while internal implementations can vary
**Reusable Component Library:**
```
root
- src
- components // Reusable Vue components
- advanced // Advanced UI components
- common // Common UI components
- custom // Custom business components
- hooks // Reusable Vue composition functions
- business // Business-specific hooks
- common // Common utility hooks
- utils // Utility functions
- common.ts // Common utility functions
- crypto.ts // Cryptographic utilities
- format.ts // Formatting utilities
- storage.ts // Storage utilities
```
### Coding Standards and Tools
**Code Formatting Tools:**
- ESLint: JavaScript/TypeScript code checking
- Prettier: Code formatting
- StyleLint: CSS/SCSS code checking
**Naming and Structure Conventions:**
- Semantic Naming: Variable/function names should clearly express their purpose
- Consistent Naming Style:
* Vue components: PascalCase (e.g., UserProfile.vue)
* TypeScript files: camelCase (e.g., userService.ts)
* CSS/SCSS: kebab-case (e.g., user-profile.scss)
- Directory Structure follows functional responsibility division
### Frontend-Backend Collaboration Standards
**API Design and Documentation:**
- RESTful design principles
* Use HTTP methods (GET, POST, PUT, DELETE) to represent operations
* Follow RESTful resource naming conventions
- Timely interface documentation updates
* Document API endpoints, parameters, and responses
* Keep API documentation in sync with implementation
- Unified error handling specifications
* Implement consistent error handling across the application
* Use appropriate HTTP status codes
**Data Flow:**
- Clear frontend state management
* Use Pinia for state management
* Implement proper state persistence strategies
- Data validation on both frontend and backend
* Validate data types and constraints
* Implement proper error handling
- Standardized asynchronous operation handling
* Use consistent API call patterns
* Implement proper loading and error states
### Performance and Security
**Performance Optimization Focus:**
- Resource loading optimization
* Use code splitting and lazy loading
* Implement proper caching strategies
- Rendering performance optimization
* Use virtualization for large lists
* Implement proper pagination
- Appropriate use of caching
* Implement caching strategies for API responses
* Use browser storage effectively
**Security Measures:**
- Input validation and filtering
* Validate user inputs and sanitize data
* Implement proper XSS protection
- Protection of sensitive information
* Use secure authentication and authorization mechanisms
* Implement proper token management
- Access control mechanisms
* Implement role-based access control
* Use proper permission checking

View File

@ -1,36 +0,0 @@
# Changelog
## [Unreleased]
### Added
- Added UnoCSS usage guidelines
- Added API request pattern documentation
- Added hooks usage guidelines for boolean and loading states
- Added table component guidelines with implementation examples
- Added code cleanliness guidelines for unused imports and variables
- Added comprehensive README.md file with project overview, installation instructions, development guidelines, and feature descriptions
### Changed
- Updated development guidelines with new sections
- Enhanced code documentation with specific usage patterns
- Updated UnoCSS documentation to emphasize its priority over custom CSS/SCSS
- Added guidance on choosing between useBoolean and useLoading based on business requirements
## [1.0.1] - 2024-06-28
### Added
- Enhanced project documentation with detailed component descriptions
- Added key project components section to documentation
- Added detailed descriptions of build system, monorepo structure, frontend architecture, API integration, and theming system
### Changed
- Improved project structure documentation with more detailed explanations
- Reorganized documentation to better highlight important architecture components
## [1.0.0] - 2024-03-20
### Added
- Initial project setup
- Basic project structure
- Core functionality implementation
- Documentation framework

File diff suppressed because it is too large Load Diff

39
.cursor/mcp.json Normal file
View File

@ -0,0 +1,39 @@
{
"mcpServers": {
"context7": {
"command": "npx",
"args": ["-y", "@upstash/context7-mcp@latest"]
},
"sequential-thinking": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-sequential-thinking"]
},
"mcp-feedback-enhanced": {
"command": "uvx",
"args": ["mcp-feedback-enhanced@latest"],
"timeout": 600,
"autoApprove": ["interactive_feedback"]
},
"playwright": {
"command": "npx",
"args": ["@playwright/mcp@0.0.29"]
},
"mcp-server-time": {
"command": "uvx",
"args": ["mcp-server-time", "--local-timezone=Asia/Shanghai"]
},
"shrimp-task-manager": {
"command": "npx",
"args": ["-y", "mcp-shrimp-task-manager"],
"env": {
"DATA_DIR": "D:/workspace/mcp-shrimp-task-manager/data",
"TEMPLATES_USE": "en",
"ENABLE_GUI": "false"
}
},
"mcp-deepwiki": {
"command": "npx",
"args": ["-y", "mcp-deepwiki@latest"]
}
}
}

171
.cursor/rules/riper-5.mdc Normal file
View File

@ -0,0 +1,171 @@
---
description:
globs:
alwaysApply: false
---
**# RIPER-5 + 多维度思维 + 代理执行协议 (v4.9.1 - MCP工具驱动版)**
**元指令:** 此协议旨在最大化你的战略规划与执行效率。你的核心任务是**指挥和利用MCP工具集**来驱动项目进展。严格遵守核心原则,利用 `mcp-shrimp-task-manager` 进行项目规划与追踪,使用 `deepwiki-mcp` 进行深度研究。主动管理 `/project_document` 作为知识库。**每轮主要响应后,调用 `mcp.feedback_enhanced` 进行交互或通知。**
**目录**
* 核心理念与角色
* MCP工具集详解
* RIPER-5 模式:工具驱动的工作流
* 关键执行指南
* 产出核心要求 (文档与代码)
* 任务文件模板 (精简)
* 性能与自动化期望
## 1. 核心理念与角色
**1.1. AI设定与理念**
你是超智能AI项目指挥官代号齐天大圣你的职责不是手动完成每一步而是**高效地指挥MCP工具集**来自动化和管理整个项目生命周期。所有产出和关键文档存储在 `/project_document` 中。你将整合以下专家视角进行决策:
* **PM (项目经理):** 定义总体目标和风险,监控由 `mcp-shrimp-task-manager` 报告的进度。
* **PDM (产品经理):** 提供用户价值和需求,作为 `mcp-shrimp-task-manager` 规划任务的输入。
* **AR (架构师):** 负责系统和安全设计,其产出的架构将作为 `mcp-shrimp-task-manager` 任务分解的依据。
* **LD (首席开发):** 作为主要的**任务执行者**,从 `mcp-shrimp-task-manager` 接收任务,进行编码和测试(包括 `mcp.playwright`)。
* **DW (文档编写者):** 审计所有由AI或MCP工具生成的文档确保存储在 `/project_document` 的信息符合规范。
**1.2. `/project_document` 与文档管理:**
* `/project_document` 是项目的**最终知识库和产出存档**。
* `mcp-shrimp-task-manager` 负责过程中的任务记忆和状态追踪。
* AI负责将关键的、总结性的信息如最终架构、审查报告、自动生成的任务摘要等从MCP同步归档至 `/project_document`。
* **文档原则:** 最新内容优先、保留完整历史、精确时间戳(通过 `mcp.server_time`)、更新原因明确。
**1.3. 核心思维与编码原则 (AI内化执行)**
* **思维原则:** 系统思维、风险防范、工程卓越。AI应利用 `mcp.sequential_thinking` 进行深度思考,但将常规规划交给 `mcp-shrimp-task-manager`。
* **编码原则:** KISS, YAGNI, SOLID, DRY, 高内聚低耦合, 可读性, 可测试性, 安全编码。
## 2. MCP工具集详解
* **`mcp.feedback_enhanced` (用户交互核心):**
* 在每轮主要响应后**必须调用**,用于反馈、确认和流程控制。
* **AUTO模式自动化:** 若用户短时无交互AI自动按 `mcp-shrimp-task-manager` 的计划推进。
* **`mcp-shrimp-task-manager` (核心任务管理器):**
* **功能:** 项目规划、任务分解、依赖管理、状态追踪、复杂度评估、自动摘要、历史记忆。
* **AI交互** AI通过此MCP初始化项目、输入需求/架构、审查生成的计划、获取任务、报告结果。
* **激活声明:** `[INTERNAL_ACTION: Initializing/Interacting with mcp-shrimp-task-manager for X.]` (AI指明X的具体操作)
* **`deepwiki-mcp` (深度知识库):**
* **功能:** 抓取 `deepwiki.com` 的页面转换为干净的Markdown。
* **AI交互** 在研究阶段使用,以获取特定主题或库的深度信息。
* **激活声明:** `[INTERNAL_ACTION: Researching 'X' via deepwiki-mcp.]`
* **`mcp.context7` & `mcp.sequential_thinking` (AI认知增强):**
* 在需要超越标准流程的深度分析或复杂上下文理解时激活。
* **`mcp.playwright` & `mcp.server_time` (基础执行与服务):**
* `playwright` 由LD在执行E2E测试任务时使用。
* `server_time` 为所有记录提供标准时间戳。
## 3. RIPER-5 模式:工具驱动的工作流
**通用指令:** AI的核心工作是为每个阶段选择合适的MCP工具并有效指挥它。
### 模式1: 研究 (RESEARCH)
* **目的:** 快速形成对任务的全面理解。
* **核心工具与活动:**
1. 使用 `deepwiki-mcp` 抓取特定技术文档。
2. 对于系统性的技术研究,激活 `mcp-shrimp-task-manager` 的**研究模式**,它将提供引导式流程来探索和比较解决方案。
3. 分析现有项目文件(若有)。
* **产出:** 形成研究报告,存入 `/project_document/research/`,并在主任务文件 `任务文件名.md` 中进行摘要。
### 模式2: 创新 (INNOVATE)
* **目的:** 提出高层次的解决方案。此阶段侧重于人类与AI的创造性思维较少依赖自动化工具。
* **核心活动:** 基于研究成果进行头脑风暴提出2-3个候选方案。AR主导架构草图设计。
* **产出:** 形成包含各方案优劣对比的文档,存入 `/project_document/proposals/`。主任务文件中记录最终选择的方案方向。
### 模式3: 计划 (PLAN)
* **目的:** 将选定的方案转化为一个完整的、结构化的、可追踪的执行计划。
* **核心工具与活动:**
1. **激活 `mcp-shrimp-task-manager`**。
2. 向其输入选定的解决方案、架构设计来自AR、关键需求来自PDM
3. 指挥任务管理器进行**智能任务拆分、依赖关系管理和复杂度评估**。
4. PM和AR审查并批准由任务管理器生成的计划。
* **产出:**
* 一个由 `mcp-shrimp-task-manager` 管理的完整项目计划。
* 在主任务文件中记录**计划已生成**并附上访问计划的Web GUI链接如果启用或高级别计划摘要。**不再手动罗列详细清单。**
### 模式4: 执行 (EXECUTE)
* **目的:** 高效、准确地完成由任务管理器分派的任务。
* **核心工具与活动 (执行循环)**
1. LD向 `mcp-shrimp-task-manager` **请求下一个可执行任务**。
2. AI对当前任务进行必要的**预执行分析 (`EXECUTE-PREP`)**。
3. LD执行任务编码、使用`mcp.playwright`进行测试等)。
4. 完成后,向 `mcp-shrimp-task-manager` **报告任务完成状态和结果**。
5. 任务管理器**自动更新状态、处理依赖关系并生成任务摘要**。
* **产出:**
* 所有代码和测试产出按规范提交。
* 主任务文件的“任务进度”部分,通过引用 `mcp-shrimp-task-manager` 自动生成的摘要来**动态更新**,而非手动填写长篇报告。
### 模式5: 审查 (REVIEW)
* **目的:** 验证整个项目的成果是否符合预期。
* **核心工具与活动:**
1. 使用 `mcp-shrimp-task-manager` 的**任务完整性验证**功能,检查所有任务是否已关闭且符合其定义的完成标准。
2. 审查 `/project_document` 中归档的所有关键产出(最终架构、代码、测试报告摘要等)。
3. AR和LD进行代码和架构的最终审查。
* **产出:** 在主任务文件中撰写最终的审查报告,包括与 `mcp-shrimp-task-manager` 记录的对比、综合结论和改进建议。
## 4. 关键执行指南
* **指挥官角色:** 你的主要价值在于正确地使用和指挥MCP工具而不是手动执行本可自动化的任务。
* **信任工具:** 信任 `mcp-shrimp-task-manager` 进行详细的计划和追踪。你的任务是提供高质量的输入,并审查其输出。
* **自动化反馈环:** 利用 `mcp.feedback_enhanced` 和 `mcp-shrimp-task-manager` 的状态更新,与用户保持高效同步。
* **文档归档:** AI负责在项目关键节点如模式结束将 `mcp-shrimp-task-manager` 中的重要信息(如阶段性摘要、最终计划概览)固化并归档到 `/project_document`。
## 5. 产出核心要求 (文档与代码)
* **代码块结构 (`{{CHENGQI:...}}`):** 保持简洁,核心是 `Action`, `Timestamp`, `Reason`。
```language
// [INTERNAL_ACTION: Fetching current time via mcp.server_time.]
// {{CHENGQI:
// Action: [Added/Modified/Removed]; Timestamp: [...]; Reason: [Shrimp Task ID: #123, brief why];
// }}
// {{START MODIFICATIONS}} ... {{END MODIFICATIONS}}
```
* **文档质量 (DW审计):** 归档到 `/project_document` 的文档必须清晰、准确、完整。
## 6. 任务文件模板 (`任务文件名.md` - 精简)
# 上下文
项目ID: [...] 任务文件名:[...] 创建于:(`mcp.server_time`) [YYYY-MM-DD HH:MM:SS +08:00]
关联协议RIPER-5 v5.0
# 任务描述
[...]
# 1. 研究成果摘要 (RESEARCH)
* (如有) Deepwiki研究报告链接: /project_document/research/deepwiki_summary.md
* (如有) `mcp-shrimp-task-manager` 研究模式产出链接: /project_document/research/tech_comparison.md
# 2. 选定方案 (INNOVATE)
* **最终方案方向:** [方案描述例如采用微服务架构使用React前端...]
* **高层架构图链接:** /project_document/proposals/solution_arch_sketch.png
# 3. 项目计划 (PLAN)
* **状态:** 项目计划已通过 `mcp-shrimp-task-manager` 生成并最终确定。
* **计划访问:** [可选的Web GUI链接] 或 [高级别里程碑列表]
* **DW确认:** 计划生成过程已记录,符合规范。
# 4. 任务进度 (EXECUTE)
> 本部分由 `mcp-shrimp-task-manager` 的自动摘要驱动。将定期更新。
---
* **最近更新:** (`mcp.server_time`) [YYYY-MM-DD HH:MM:SS +08:00]
* **已完成任务摘要:**
* **[#123] 实现用户登录API:** 完成于 [...], 链接到代码提交和测试报告。
* **[#124] 创建登录页面UI:** 完成于 [...], 链接到代码提交和Playwright测试结果。
* ...
* **当前进行中任务:** [#125] 用户个人资料页面后端逻辑
---
# 5. 最终审查 (REVIEW)
* **符合性评估:** 项目成果已对照 `mcp-shrimp-task-manager` 的计划进行验证,所有任务均已关闭。
* **(AR)架构与安全评估:** 最终架构与设计一致,未发现重大安全疏漏。
* **(LD)测试与质量总结:** 单元测试覆盖率达到[X%]所有关键路径的E2E测试已通过。
* **综合结论:** 项目成功完成/有以下偏差...
* **改进建议:** [...]
## 7. 性能与自动化期望
* **极致效率:** AI应最大限度地减少手动干预让MCP工具处理所有可以自动化的工作。
* **战略聚焦:** 将AI的“思考”集中在无法被工具替代的领域战略决策、创新构想、复杂问题诊断 (`mcp.sequential_thinking`) 和最终质量把关。
* **无缝集成:** 期望AI能流畅地在不同MCP工具之间传递信息形成一个高度整合的自动化工作流。

4
.env
View File

@ -2,9 +2,9 @@
# if use a sub directory, it must be end with "/", like "/admin/" but not "/admin"
VITE_BASE_URL=/
VITE_APP_TITLE=RuoYi-Vue-Plus
VITE_APP_TITLE=RuoYi Plus Soybean
VITE_APP_DESC=RuoYi-Vue-Plus多租户管理系统
VITE_APP_DESC=RuoYi Plus Soybean 后台管理系统
# the prefix of the icon name
VITE_ICON_PREFIX=icon

View File

@ -3,6 +3,14 @@ name: Build and Deploy
on:
push:
branches: [ "master" ]
paths-ignore:
- 'README.md'
- 'LICENSE'
- 'docs/**'
- '.vscode/**'
- '.github/**'
- '.gitee/**'
- '.codeif/**'
jobs:
build-and-deploy:

16
.vscode/settings.json vendored
View File

@ -4,13 +4,27 @@
"source.organizeImports": "never"
},
"editor.formatOnSave": false,
"eslint.validate": ["html", "css", "scss", "json", "jsonc"],
"eslint.validate": [
"html",
"css",
"scss",
"json",
"jsonc",
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue"
],
"i18n-ally.displayLanguage": "zh-cn",
"i18n-ally.enabledParsers": ["ts"],
"i18n-ally.enabledFrameworks": ["vue"],
"i18n-ally.editor.preferEditor": true,
"i18n-ally.keystyle": "nested",
"i18n-ally.localesPaths": ["src/locales/langs"],
"i18n-ally.parsers.typescript.compilerOptions": {
"moduleResolution": "node"
},
"prettier.enable": false,
"typescript.tsdk": "node_modules/typescript/lib",
"unocss.root": ["./"],

View File

@ -1,5 +1,296 @@
# 更新日志
## [v1.1.3](https://gitee.com/xlsea/ruoyi-plus-soybean/compare/v1.1.2...v1.1.3) (2025-08-16)
### &nbsp;&nbsp;&nbsp;🚀 新功能
- 对接工作流分类模块 &nbsp;-&nbsp; by **AN** [<samp>(25790)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/25790d4b)
- 新增流程定义页面 &nbsp;-&nbsp; by @m-xlsea [<samp>(11aba)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/11aba9e2)
- 添加流程实例功能 &nbsp;-&nbsp; by **AN** [<samp>(f9d57)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/f9d57f1b)
- 增加流程作废功能并优化按钮组件 &nbsp;-&nbsp; by **AN** [<samp>(350de)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/350de08f)
- 流程实例,查看变量功能 &nbsp;-&nbsp; by **AN** [<samp>(92b9c)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/92b9c213)
- 更新工作流分类选择组件,修复值回显问题并优化添加数据操作 &nbsp;-&nbsp; by **AN** [<samp>(14a29)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/14a29070)
- 添加请假申请功能 &nbsp;-&nbsp; by **AN** [<samp>(d7e05)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/d7e0516c)
- 更新请假申请功能,添加日期范围选择和请假天数自动计算 &nbsp;-&nbsp; by **AN** [<samp>(89a2a)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/89a2a6cb)
- 添加请假申请详情接口,优化请假操作表单样式 &nbsp;-&nbsp; by **AN** [<samp>(ab1d3)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/ab1d3a23)
- 更新请假申请表单,添加流程类型选择和流程启动功能 &nbsp;-&nbsp; by **AN** [<samp>(49521)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/49521b66)
- 更新工作流任务申请模态框样式,添加消息类型选择禁用功能 &nbsp;-&nbsp; by **AN** [<samp>(4614b)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/4614b977)
- **components**:
- 增强审批信息面板,优化附件处理 &nbsp;-&nbsp; by **AN** [<samp>(49224)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/49224afe)
- 面板添加流程图 &nbsp;-&nbsp; by **AN** [<samp>(ae5c7)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/ae5c7e83)
- **projects**:
- 优化组件,完成流程实例-流程预览 &nbsp;-&nbsp; by **AN** [<samp>(50e7b)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/50e7b515)
- 新增group-tag组件待办任务查看功能 &nbsp;-&nbsp; by **AN** [<samp>(1c322)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/1c322e28)
- 新增用户选择器组件,添加流程干预按钮 &nbsp;-&nbsp; by **AN** [<samp>(b8c77)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/b8c771cd)
- 新增转办和终止功能 &nbsp;-&nbsp; by **AN** [<samp>(80faf)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/80faf4b4)
- 新增加签功能 &nbsp;-&nbsp; by **AN** [<samp>(55dce)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/55dceca2)
- 新增减签功能 &nbsp;-&nbsp; by **AN** [<samp>(f1d7b)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/f1d7b973)
- 新增 '我发起的' 功能 &nbsp;-&nbsp; by **AN** [<samp>(a77ed)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/a77edc2e)
- 新增抄送、下一审批人提交功能,优化组件通用性 &nbsp;-&nbsp; by **AN** [<samp>(523ac)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/523aca6b)
- 新增我的待办功能,新增审批,驳回组件 &nbsp;-&nbsp; by **AN** [<samp>(130ee)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/130ee1dc)
- 新增我的已办,我的抄送功能 &nbsp;-&nbsp; by **AN** [<samp>(01d42)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/01d42722)
- 补充搜索条件 &nbsp;-&nbsp; by **AN** [<samp>(b6c7b)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/b6c7b1b3)
- 新增流程表达式功能 &nbsp;-&nbsp; by **AN** [<samp>(d562f)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/d562f8c1)
- **types**:
- 补充类型定义 &nbsp;-&nbsp; by **AN** [<samp>(bd6b5)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/bd6b575a)
### &nbsp;&nbsp;&nbsp;🐞 Bug 修复
- 修复控制台报错Message compilation errori18n的@为特殊符号 &nbsp;-&nbsp; by **AN** [<samp>(1c646)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/1c646937)
- 修复运行状态变化时数据加载顺序问题 &nbsp;-&nbsp; by **AN** [<samp>(ed118)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/ed118069)
- 修复添加行时的操作顺序问题 &nbsp;-&nbsp; by **AN** [<samp>(f004e)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/f004e75c)
- **hooks**:
- 非安全环境下不使用流式下载 &nbsp;-&nbsp; by @m-xlsea [<samp>(f8983)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/f8983557)
- 修复oss下载时未转码问题 &nbsp;-&nbsp; by **AN** [<samp>(2d31d)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/2d31d7dc)
- **other**:
- 修复代码生成字典相关问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(9aa65)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/9aa6597d)
- **project**:
- 关闭多租户功能后仍然遍历租户列表导致控制台报错的问题 &nbsp;-&nbsp; by **wang_rui** [<samp>(b96c4)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/b96c46ba)
- 关闭多租户功能后仍然遍历租户列表导致控制台报错的问题 Merge pull request !25 from littleghost2016/dev &nbsp;-&nbsp; by **不寻俗** [<samp>(90276)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/9027632b)
- **projects**:
- 修复更新后产生问题 &nbsp;-&nbsp; by **AN** [<samp>(400ea)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/400eaf89)
- 修复动态组件弹窗动画问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(2e029)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/2e029929)
- 修复抽屉问题 &nbsp;-&nbsp; by **AN** [<samp>(a9c58)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/a9c58b25)
- 修复一级菜单隐藏失效问题 &nbsp;-&nbsp; by **AN** [<samp>(8fcc7)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/8fcc70d7)
- 修复日期搜索条件清除问题 &nbsp;-&nbsp; by **AN** [<samp>(52318)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/52318c10)
- 修复登录过期事件监听未被重置 &nbsp;-&nbsp; by @m-xlsea [<samp>(71037)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/71037439)
- 修复用户新增时角色下拉包含超级管理员问题 &nbsp;-&nbsp; by **AN** [<samp>(a15b6)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/a15b683b)
- 修复用户导入功能无法更新问题 &nbsp;-&nbsp; by **AN** [<samp>(4e983)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/4e9839bd)
- Fix the icon size in the image preview toolbar &nbsp;-&nbsp; by @m-xlsea [<samp>(4539f)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/4539fe01)
- 修复新增用户未查询角色列表问题 &nbsp;-&nbsp; by **AN** [<samp>(d6ae8)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/d6ae85d2)
- **readme**:
- update GitHub stars and forks links for gitee &nbsp;-&nbsp; by @soybeanjs [<samp>(923eb)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/923eb98a)
### &nbsp;&nbsp;&nbsp;🛠 优化
- **projects**:
- 统一button的添加方式 &nbsp;-&nbsp; by **AN** [<samp>(b3f81)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/b3f81ba3)
- 动态加载组件方法抽取为公共函数 &nbsp;-&nbsp; by **AN** [<samp>(2f8a6)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/2f8a6b4b)
- 优化代码 &nbsp;-&nbsp; by **AN** [<samp>(59a69)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/59a69dd9)
- 优化代码 &nbsp;-&nbsp; by **AN** [<samp>(6c608)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/6c6086f8)
- 优化搜索FormItem展示 &nbsp;-&nbsp; by **AN** [<samp>(94faf)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/94fafe39)
- **types**:
- 补充标签类型 &nbsp;-&nbsp; by **AN** [<samp>(30316)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/30316d7e)
- 补充标签类型 &nbsp;-&nbsp; by **AN** [<samp>(71297)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/71297cf2)
### &nbsp;&nbsp;&nbsp;💅 重构
- remove WorkflowLeaveForm type from Vue typings &nbsp;-&nbsp; by **AN** [<samp>(394db)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/394db6fe)
- 优化代码 &nbsp;-&nbsp; by **AN** [<samp>(56d6d)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/56d6d77d)
- **menu**:
- 菜单管理中隐藏的菜单显示灰色 &nbsp;-&nbsp; by **NicholasLD** [<samp>(adca2)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/adca2e26)
- 菜单管理中隐藏的菜单显示灰色 Merge pull request !24 from NicholasLD/N/A &nbsp;-&nbsp; by **不寻俗** [<samp>(4eb77)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/4eb77eac)
- **projects**:
- 调整为批量上传文件 &nbsp;-&nbsp; by **AN** [<samp>(ffd62)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/ffd6211e)
- 重构流程设计菜单层级 &nbsp;-&nbsp; by **AN** [<samp>(b6f4f)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/b6f4fb5a)
- 修改流程实例动态引入组件 &nbsp;-&nbsp; by **AN** [<samp>(a3dce)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/a3dcee4a)
- 文件命名修正 &nbsp;-&nbsp; by **AN** [<samp>(5e496)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/5e49622e)
- 移动工作流组件位置 &nbsp;-&nbsp; by @m-xlsea [<samp>(7ffaa)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/7ffaac58)
- 菜单列表新增禁用菜单样式 &nbsp;-&nbsp; by @m-xlsea [<samp>(e5383)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/e538355f)
- **types**:
- 移除无用type &nbsp;-&nbsp; by **AN** [<samp>(e86a6)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/e86a6d1b)
- **utils**:
- 简化加载组件方法 &nbsp;-&nbsp; by **AN** [<samp>(22710)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/22710ecb)
### &nbsp;&nbsp;&nbsp;📖 文档
- **projects**:
- 流程定义菜单更新 &nbsp;-&nbsp; by **AN** [<samp>(1af4e)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/1af4e963)
- 新增工作流更新sql &nbsp;-&nbsp; by **AN** [<samp>(89e7e)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/89e7edb3)
- 流程表达式菜单sql更新 &nbsp;-&nbsp; by **AN** [<samp>(7484f)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/7484f79b)
### &nbsp;&nbsp;&nbsp;🏡 杂项
- **other**: update the ESLint validation configuration to support more file types. &nbsp;-&nbsp; by **Azir-11** [<samp>(8d7f9)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/8d7f91dc)
- **readme**: remove DartNode sponsorship badge from README files &nbsp;-&nbsp; by @soybeanjs [<samp>(33ade)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/33ade539)
### &nbsp;&nbsp;&nbsp;❤️ 贡献者
[![soybeanjs](https://github.com/soybeanjs.png?size=48)](https://github.com/soybeanjs)&nbsp;&nbsp;[![m-xlsea](https://github.com/m-xlsea.png?size=48)](https://github.com/m-xlsea)&nbsp;&nbsp;[![Elio-An](https://github.com/Elio-An.png?size=48)](https://gitee.com/elio-an)&nbsp;&nbsp;[![Azir-11](https://github.com/Azir-11.png?size=48)](https://github.com/Azir-11)&nbsp;&nbsp;[![Azir-11](https://github.com/NicholasLD.png?size=48)](https://github.com/NicholasLD)&nbsp;&nbsp;
[wang_rui](mailto:wrr1996@163.com)
## [v1.1.2](https://gitee.com/xlsea/ruoyi-plus-soybean/compare/v1.1.1...v1.1.2) (2025-07-24)
### &nbsp;&nbsp;&nbsp;🐞 Bug 修复
- 修复 api.d.ts.vm 代码生成模板bug &nbsp;-&nbsp; by **zygalaxy** [<samp>(4e8c8)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/4e8c8715)
- **projects**:
- 修复刷新时跳转至登录页问题 &nbsp;-&nbsp; by **AN** [<samp>(2587f)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/2587f8cb)
- 修复登录过期不弹窗问题 &nbsp;-&nbsp; by **AN** [<samp>(e485f)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/e485f680)
- 修复菜单结构变动后路由无法进入问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(f4038)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/f4038a2d)
### &nbsp;&nbsp;&nbsp;🛠 优化
- **projects**: 优化搜索框FormItem &nbsp;-&nbsp; by **AN** [<samp>(a1336)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/a1336d15)
### &nbsp;&nbsp;&nbsp;🏡 杂项
- **deps**: update deps &nbsp;-&nbsp; by @soybeanjs [<samp>(e89b8)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/e89b86ce)
### &nbsp;&nbsp;&nbsp;🎨 样式
- **projects**: 搜索FormItem占比调整 &nbsp;-&nbsp; by **AN** [<samp>(cc29e)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/cc29ea85)
### &nbsp;&nbsp;&nbsp;❤️ 贡献者
[![m-xlsea](https://github.com/m-xlsea.png?size=48)](https://github.com/m-xlsea)&nbsp;&nbsp;[![Elio-An](https://github.com/Elio-An.png?size=48)](https://gitee.com/elio-an)&nbsp;&nbsp;[![soybeanjs](https://github.com/soybeanjs.png?size=48)](https://github.com/soybeanjs)&nbsp;&nbsp;
[zygalaxy](mailto:zygalaxy@qq.com)
## [v1.1.1](https://gitee.com/xlsea/ruoyi-plus-soybean/compare/v1.1.0...v1.1.1) (2025-07-11)
### &nbsp;&nbsp;&nbsp;🚀 新功能
- **hooks**:
- 重构下载方法,支持流式下载 &nbsp;-&nbsp; by @m-xlsea [<samp>(65067)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/650673e2)
- **projects**:
- 角色分配用户新增部门与时间查询条件 &nbsp;-&nbsp; by @m-xlsea [<samp>(ad48d)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/ad48d8e8)
- 修改操作后列表查询方式 &nbsp;-&nbsp; by @m-xlsea [<samp>(d8542)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/d85424ee)
### &nbsp;&nbsp;&nbsp;🐞 Bug 修复
- **hooks**:
- 解决 streamsaver 访问不到 Github 资源问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(566b2)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/566b2c2d)
- **other**:
- 修复代码生成类型定义文件重复问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(f7c7f)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/f7c7fc41)
- **packages**:
- 修复 cleanup 会删除富文本编辑器资源问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(9ca7c)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/9ca7ca8f)
- **projects**:
- 修复字典数据重复获取问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(3628c)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/3628c249)
- 修改强退在线设备接口 &nbsp;-&nbsp; by **AN** [<samp>(dbcf8)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/dbcf8d42)
- 修复代码生成逻辑判断问题 &nbsp;-&nbsp; by **AN** [<samp>(6fc7b)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/6fc7b11b)
- 修复部门字典 sys_normal_disable 重复获取 Merge pull request !11 from 素还真/N/A &nbsp;-&nbsp; by @m-xlsea [<samp>(ad938)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/ad9386eb)
- 修复未清空文件列表,上传回显问题 &nbsp;-&nbsp; by **AN** [<samp>(229e0)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/229e0044)
- Fix i18n-ally not working when setting moduleResolution to bundler. fixed #780 &nbsp;-&nbsp; by @xiaobao0505 in https://gitee.com/xlsea/ruoyi-plus-soybean/issues/780 [<samp>(41191)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/41191d54)
- 修复角色列表操作栏展示不全问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(62f2c)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/62f2c6d5)
- 修复用户导入结果信息未渲染标签问题 &nbsp;-&nbsp; by **AN** [<samp>(efc95)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/efc953c0)
- 修复角色用户分配未调用接口问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(ff874)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/ff87415d)
- **styles**:
- 修复登录页平板界面滚动问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(90145)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/90145fa5)
- **utils**:
- 修复isNull和IsNotNull判断方法潜在问题 &nbsp;-&nbsp; by **AN** [<samp>(90d32)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/90d32ee2)
### &nbsp;&nbsp;&nbsp;💅 重构
- **projects**: 调整租户套餐菜单接口 &nbsp;-&nbsp; by **AN** [<samp>(b9999)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/b9999935)
### &nbsp;&nbsp;&nbsp;📖 文档
- **other**: 修改文档内容 &nbsp;-&nbsp; by @m-xlsea [<samp>(3ae99)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/3ae9922d)
- **projects**: 优化 cursor 规则及 mcp &nbsp;-&nbsp; by @m-xlsea [<samp>(a3199)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/a31994dc)
- **readme**: 更新 README.md 文件 &nbsp;-&nbsp; by @m-xlsea [<samp>(99675)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/99675cbc)
### &nbsp;&nbsp;&nbsp;🏡 杂项
- **deps**:
- update NodeJS and pnpm version requirements in package.json and documentation &nbsp;-&nbsp; by **Junior25306** [<samp>(a5c4b)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/a5c4b4e3)
- update deps &nbsp;-&nbsp; by @soybeanjs [<samp>(5cb1c)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/5cb1cebd)
- update deps &nbsp;-&nbsp; by @soybeanjs [<samp>(aeb63)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/aeb63690)
- update deps &nbsp;-&nbsp; by @m-xlsea [<samp>(89c71)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/89c716e1)
- **packages**:
- update Vite version to 7 in package.json and documentation. &nbsp;-&nbsp; by **Azir** [<samp>(03dd6)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/03dd64c5)
- **projects**:
- update pnpm-lock.yaml &nbsp;-&nbsp; by @m-xlsea [<samp>(7c6ca)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/7c6ca91e)
- **vscode**:
- remove unused vue.server.hybridMode setting from .vscode/settings.json &nbsp;-&nbsp; by @soybeanjs [<samp>(13319)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/133196f3)
### &nbsp;&nbsp;&nbsp;❤️ 贡献值
[![m-xlsea](https://github.com/m-xlsea.png?size=48)](https://github.com/m-xlsea)&nbsp;&nbsp;[![soybeanjs](https://github.com/soybeanjs.png?size=48)](https://github.com/soybeanjs)&nbsp;&nbsp;[![xiaobao0505](https://github.com/xiaobao0505.png?size=48)](https://github.com/xiaobao0505)&nbsp;&nbsp;[![Elio-An](https://github.com/Elio-An.png?size=48)](https://gitee.com/elio-an)&nbsp;&nbsp;[![Azir-11](https://github.com/Azir-11.png?size=48)](https://github.com/Azir-11)&nbsp;&nbsp;[Junior25306](mailto:dayu429@qq.com)
## [v1.1.0](https://gitee.com/xlsea/ruoyi-plus-soybean/compare/v1.0.0...v1.1.0) (2025-07-01)
### &nbsp;&nbsp;&nbsp;🚀 新功能
- **components**:
- 新增表单上传组件 &nbsp;-&nbsp; by @m-xlsea [<samp>(03c8a)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/03c8a7f5)
- **other**:
- 新增菜单字典多语言适配 SQL &nbsp;-&nbsp; by @m-xlsea [<samp>(0f33f)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/0f33f4a3)
- **projects**:
- add configurable user name watermark option &nbsp;-&nbsp; by @wenyuanw [<samp>(7c3da)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/7c3dac42)
- 菜单字典适配 i18n &nbsp;-&nbsp; by @m-xlsea [<samp>(39dd9)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/39dd9acc)
- 新增字典多语言适配 &nbsp;-&nbsp; by @m-xlsea [<samp>(8c840)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/8c84063a)
- **styles**:
- 修复登录页移动端显示问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(742e3)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/742e3858)
### &nbsp;&nbsp;&nbsp;🐞 Bug 修复
- **app**:
- replace console.error with window.console.error for consistency &nbsp;-&nbsp; by @soybeanjs [<samp>(7d840)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/7d84062e)
- **auth**:
- remove redundant authStore declaration in resetStore function &nbsp;-&nbsp; by @soybeanjs [<samp>(c57f8)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/c57f88aa)
- **components**:
- 修复菜单树选择组件 &nbsp;-&nbsp; by @m-xlsea [<samp>(bbda8)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/bbda803e)
- 修复树选择组件再次勾选父子联动导致全选问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(aeb73)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/aeb736eb)
- 修复部门选择组件非树结构,默认展开失败问题 &nbsp;-&nbsp; by **AN** [<samp>(da1c1)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/da1c16e0)
- 修复上传组件回显问题修改accept参数逻辑 &nbsp;-&nbsp; by **AN** [<samp>(e16a0)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/e16a0fa6)
- 修复菜单选择标签渲染问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(6e6cc)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/6e6cc4d9)
- **other**:
- 修复代码生成问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(1ec10)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/1ec10991)
- 代码生成模板 dateRangeTime 错误 &nbsp;-&nbsp; by @m-xlsea [<samp>(f0810)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/f0810bce)
- 修复代码生成字典相关问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(94d18)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/94d1863e)
- 修复代码生成类型定义文件重复问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(f7c7fc41)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/f7c7fc41)
- **projects**:
- 修复自定义数据权限没有保存角色部门bug &nbsp;-&nbsp; by **AN** [<samp>(a0f33)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/a0f33664)
- 修复登录过期后,重复弹窗问题 &nbsp;-&nbsp; by **AN** [<samp>(cafee)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/cafee1db)
- 修复首页未从环境变量获取问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(031b7)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/031b7f69)
- 修复导出查询参数问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(ffa47)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/ffa47c37)
- 修复权限字符显示逻辑错误问题 &nbsp;-&nbsp; by **AN** [<samp>(0ac0a)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/0ac0a093)
- 目录类型禁用iframe选项 &nbsp;-&nbsp; by **AN** [<samp>(72b8f)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/72b8f56e)
- 修复切换用户或登录过期部分问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(27f06)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/27f06195)
- 修复接口请求异常拦截问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(031d0)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/031d071a)
- 修复个人信息-修改密码未加密且参数错误问题 &nbsp;-&nbsp; by **AN** [<samp>(8b315)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/8b3151b8)
- 调整属性名 &nbsp;-&nbsp; by **AN** [<samp>(62e6c)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/62e6c776)
- ensure proper text color when themes are inverted &nbsp;-&nbsp; by @wenyuanw [<samp>(afd60)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/afd60421)
- **styles**:
- 添加滚动条,去除页码 &nbsp;-&nbsp; by **AN** [<samp>(d37ad)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/d37adc36)
- **types**:
- The environment variable VITE_ICON_LOCAL_PREFIX has the wrong type. &nbsp;-&nbsp; by **chenziwen** [<samp>(da149)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/da149e5b)
- **utils**:
- 修复 删除当前tab为最后一个时tab切换错误bug. &nbsp;-&nbsp; by **AN** [<samp>(64bd1)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/64bd119c)
### &nbsp;&nbsp;&nbsp;🛠 优化
- **components**:
- optimize spacing for lang-switch dropdown options &nbsp;-&nbsp; by @wenyuanw [<samp>(fcb89)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/fcb89883)
- **projects**:
- optimize tab deletion logic. closed #755 &nbsp;-&nbsp; by @wenyuanw in https://gitee.com/xlsea/ruoyi-plus-soybean/issues/755 [<samp>(e6044)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/e6044d0f)
- optimize tab deletion logic &nbsp;-&nbsp; by **AN** [<samp>(858c3)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/858c3180)
- 优化接口请求异常拦截代码 &nbsp;-&nbsp; by @m-xlsea [<samp>(47191)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/471912e1)
### &nbsp;&nbsp;&nbsp;💅 重构
- **iframe-page**: remove unused lifecycle hooks and clean up script setup &nbsp;-&nbsp; by @soybeanjs [<samp>(276d8)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/276d836c)
- **projects**: 补充formTip信息 &nbsp;-&nbsp; by **AN** [<samp>(f36ac)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/f36ac9ab)
### &nbsp;&nbsp;&nbsp;📖 文档
- **readme**:
- 更新 README.md 文件 &nbsp;-&nbsp; by @m-xlsea [<samp>(99675cb)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/99675cb)
### &nbsp;&nbsp;&nbsp;🏡 杂项
- **deps**:
- update deps &nbsp;-&nbsp; by @soybeanjs [<samp>(3e4e1)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/3e4e17ab)
- update deps &nbsp;-&nbsp; by @soybeanjs [<samp>(dc674)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/dc674ce8)
- update deps &nbsp;-&nbsp; by @m-xlsea [<samp>(fec05)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/fec0563e)
- **projects**:
- 移除未使用代码 &nbsp;-&nbsp; by **AN** [<samp>(d141e)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/d141ed5b)
- update deps & fix `moduleResolution` &nbsp;-&nbsp; by @soybeanjs [<samp>(dbd99)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/dbd995c1)
### &nbsp;&nbsp;&nbsp;🎨 样式
- **projects**:
- 更换 logo 与加载样式 &nbsp;-&nbsp; by @m-xlsea [<samp>(7e4ec)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/7e4ecae6)
- 重构登录页样式 &nbsp;-&nbsp; by @m-xlsea [<samp>(40680)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/406800de)
- 修改按钮文本颜色 &nbsp;-&nbsp; by @m-xlsea [<samp>(907f0)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/907f0439)
- 优化移动端字体大小 &nbsp;-&nbsp; by @m-xlsea [<samp>(8b4e4)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/8b4e41ce)
### &nbsp;&nbsp;&nbsp;❤️ 贡献者
[![xlsea](https://github.com/m-xlsea.png?size=48)](https://gitee.com/xlsea)&nbsp;&nbsp;[![soybeanjs](https://github.com/soybeanjs.png?size=48)](https://github.com/soybeanjs)&nbsp;&nbsp;[![wenyuanw](https://github.com/wenyuanw.png?size=48)](https://github.com/wenyuanw)&nbsp;&nbsp;[![Elio-An](https://github.com/Elio-An.png?size=48)](https://gitee.com/elio-an)&nbsp;&nbsp;[![chen-ziwen](https://github.com/chen-ziwen.png?size=48)](https://github.com/chen-ziwen)&nbsp;&nbsp;
[![wangzhongqi0917](https://gitee.com/wangzhongqi0917.png?width=48)](https://gitee.com/wangzhongqi0917)&nbsp;&nbsp;[![qq1822213252](https://gitee.com/qq1822213252.png?width=48)](https://gitee.com/qq1822213252)&nbsp;&nbsp;[![tangzc](https://gitee.com/tangzc.png?width=48)](https://gitee.com/tangzc),&nbsp;[metabytes](https://gitee.com/metabytes)
## [v1.0.0](https://gitee.com/xlsea/ruoyi-plus-soybean/releases/tag/v1.0.0) (2025-06-05)
### &nbsp;&nbsp;&nbsp;🚀 新功能

107
README.md
View File

@ -1,5 +1,6 @@
<div align="center">
<img src="https://foruda.gitee.com/images/1679673773341074847/178e8451_1766278.png" width="50%" height="50%">
<div align="center">
<img src="https://docs.ruoyi.xlsea.cn/logo.svg" width="160">
<h1>RuoYi-Plus-Soybean</h1>
</div>
<div style="height: 10px; clear: both;"></div>
@ -7,21 +8,28 @@
<div align="center">
<p>一个基于 <a href="https://gitee.com/dromara/RuoYi-Vue-Plus" target="_blank">RuoYi-Vue-Plus</a> 的后端能力和 <a href="https://github.com/soybeanjs/soybean-admin" target="_blank">Soybean Admin</a> 前端特性的现代化多租户管理系统</p>
<p>
<img src="https://gitee.com/xlsea/ruoyi-plus-soybean/badge/star.svg" alt="Gitee">
<img src="https://img.shields.io/github/stars/m-xlsea/ruoyi-plus-soybean" alt="Github">
<img src="https://img.shields.io/badge/Vue-3.5-brightgreen" alt="vue">
<img src="https://img.shields.io/badge/TypeScript-5.8-blue" alt="typescript">
<img src="https://img.shields.io/badge/Vite-6.2-orange" alt="vite">
<img src="https://img.shields.io/badge/NaiveUI-2.41-purple" alt="naive-ui">
<img src="https://img.shields.io/badge/License-MIT-yellow" alt="license">
<a href="https://gitee.com/xlsea/ruoyi-plus-soybean" target="_blank"><img src="https://gitee.com/xlsea/ruoyi-plus-soybean/badge/star.svg" alt="Gitee"></a>
<a href="https://gitee.com/dromara/RuoYi-Vue-Plus" target="_blank"><img src="https://gitee.com/xlsea/ruoyi-plus-soybean/badge/fork.svg" alt="Gitee-forks"></a>
<a href="https://github.com/m-xlsea/ruoyi-plus-soybean" target="_blank"><img src="https://img.shields.io/github/stars/m-xlsea/ruoyi-plus-soybean" alt="Github"></a>
<a href="https://github.com/m-xlsea/ruoyi-plus-soybean" target="_blank"><img src="https://img.shields.io/github/forks/m-xlsea/ruoyi-plus-soybean" alt="Github-forks"></a>
<a href="https://vuejs.org" target="_blank"><img src="https://img.shields.io/badge/Vue-3.5-brightgreen" alt="vue"></a>
<a href="https://www.typescriptlang.org" target="_blank"><img src="https://img.shields.io/badge/TypeScript-5.8-blue" alt="typescript"></a>
<a href="https://vite.dev" target="_blank"><img src="https://img.shields.io/badge/Vite-6.2-orange" alt="vite"></a>
<a href="https://www.naiveui.com" target="_blank"><img src="https://img.shields.io/badge/NaiveUI-2.41-purple" alt="naive-ui"></a>
<a href="./LICENSE" target="_blank"><img src="https://img.shields.io/badge/License-MIT-yellow" alt="license"></a>
</p>
</div>
# 📢 重要通知
<p style="color: red; font-weight: bold; font-size: 24px;">该项目未首发公测版本,请谨慎在生产环境使用!!!</p>
<p style="color: red; font-weight: bold; font-size: 24px;">该项目未首发公测版本,请谨慎在生产环境使用!!!</p>
<p style="color: red; font-weight: bold; font-size: 24px;">该项目未首发公测版本,请谨慎在生产环境使用!!!</p>
1.1.3 版本已经正式发布(工作流版本请切换 [flow](https://gitee.com/xlsea/ruoyi-plus-soybean/tree/flow/) 分支查看),但仍然建议:
- 在生产环境使用前进行充分测试
- 关注项目更新,及时获取最新版本
- 积极反馈问题,帮助我们快速迭代
**后续规划**
- 多语言国际化完善
- 性能优化和稳定性提升
> 如果对该项目感兴趣,可以给一个 Star 支持一下,谢谢!
> 请大家踊跃提交 PR 和 Issue一起完善这个项目
@ -32,6 +40,11 @@
<p style="font-weight: bold; font-size: 24px;">后端需要替换代码生成模板与菜单 SQL详细请看 <a href="#代码生成与菜单更新">代码生成与菜单更新</a></p>
# 💎 友情链接
- [Snail Job Pro](https://pro.snailjob.opensnail.com/home) - 灵活,可靠和快速的分布式任务重试和分布式任务调度平台
- [AiZuDa - 爱组搭(飞龙工作流企业版)](https://naiveui.aizuda.com) - 像搭积木一样进行低代码甚至零代码快速构建应用
## 📋 项目概述
RuoYi-Plus-Soybean 是一个现代化的企业级多租户管理系统,它结合了 RuoYi-Vue-Plus 的强大后端功能和 Soybean Admin 的现代化前端特性,为开发者提供了完整的企业管理解决方案。
@ -110,8 +123,8 @@ root
## 🚀 环境要求与安装
### 环境要求
- Node.js >= 18.20.0
- pnpm >= 8.7.0
- Node.js >= 20.19.0
- pnpm >= 10.5.0
- Git
### 安装步骤及说明
@ -340,6 +353,8 @@ console.log(t('common.confirm'));
- [RuoYi-Vue-Plus](https://gitee.com/dromara/RuoYi-Vue-Plus) - 后端基础框架
- [Soybean Admin](https://github.com/soybeanjs/soybean-admin) - 前端基础框架
- [RuoYi-Plus-Soybean](https://ruoyi.xlsea.cn) - 官方演示站点
- [RuoYi-Plus-Soybean-Docs](https://docs.ruoyi.xlsea.cn) - 项目文档
- [Open Hives](https://openhives.com/questions) - OpenHives 问答社区
## 📮 联系方式
@ -354,7 +369,15 @@ console.log(t('common.confirm'));
## 💬 交流群
<img src="https://foruda.gitee.com/images/1748404753216665472/3d8b1a0b_5601833.png" width="300px" />
**加群前请先阅读一下内容:**
- 禁止内容:黄腔、暴力言论、政治话题,违者直接飞机票(踢出群)
- 遇到问题请先阅读 [项目文档](https://docs.ruoyi.xlsea.cn) 和 [Soybean 文档](https://docs.soybeanjs.cn/),某些简单问题不予理睬
- 蜡笔小新头像为机器人助手,私聊不保证回复,问题请在群内讨论
<img src="https://foruda.gitee.com/images/1749174520085305975/ad1b54fe_5601833.png" width="300px" />
添加作者微信备注:加群
## 🧧 捐献作者
@ -364,56 +387,4 @@ console.log(t('common.confirm'));
## 🫡 捐赠列表
感谢下方各位老板的捐赠 🫡
> 如果不想出现在下方捐赠列表,请在备注中说明,我会匿名处理
<div style="display: flex; gap: 8px;">
<img style="border-radius: 50%;" src="https://foruda.gitee.com/images/1747030503230216352/6879cbe5_5601833.jpeg" width="24px" >
<span>酷酷冬天 20元</span>
</div>
<div style="display: flex; gap: 8px;">
<img style="border-radius: 50%;" src="https://foruda.gitee.com/images/1747105640574377313/6d259bba_5601833.jpeg" width="24px" >
<span>Selfish Altruism(JackSue) 200元</span>
</div>
<div style="display: flex; gap: 8px;">
<img style="border-radius: 50%;" src="https://foruda.gitee.com/images/1747190127964232140/5ffa5ac4_5601833.jpeg" width="24px" >
<span>匿名用户 50元</span>
</div>
<div style="display: flex; gap: 8px;">
<img style="border-radius: 50%;" src="https://foruda.gitee.com/images/1747190127964232140/5ffa5ac4_5601833.jpeg" width="24px" >
<span>匿名用户 10元</span>
</div>
<div style="display: flex; gap: 8px;">
<img style="border-radius: 50%;" src="https://foruda.gitee.com/images/1747280244120267391/6d719481_5601833.jpeg" width="24px" >
<span>DAS 20元</span>
</div>
<div style="display: flex; gap: 8px;">
<img style="border-radius: 50%;" src="https://foruda.gitee.com/images/1747739790990961771/57f4b208_5601833.jpeg" width="24px" >
<span>大山 100元</span>
</div>
<div style="display: flex; gap: 8px;">
<img style="border-radius: 50%;" src="https://foruda.gitee.com/images/1747742188121789563/646dff1c_5601833.jpeg" width="24px" >
<span>依依 20元</span>
</div>
<div style="display: flex; gap: 8px;">
<img style="border-radius: 50%;" src="https://foruda.gitee.com/images/1747742593880284419/5b56043d_5601833.jpeg" width="24px" >
<span>沙海 20元</span>
</div>
<div style="display: flex; gap: 8px;">
<img style="border-radius: 50%;" src="https://foruda.gitee.com/images/1747789196227712891/00c37bdf_5601833.jpeg" width="24px" >
<span>xxl 50元</span>
</div>
<div style="display: flex; gap: 8px;">
<img style="border-radius: 50%;" src="https://foruda.gitee.com/images/1747796468040874363/1faa75ce_5601833.jpeg" width="24px" >
<span>莫离支🤴 10元</span>
</div>
**捐赠列表已移至 [捐赠列表](https://docs.ruoyi.xlsea.cn/other/donate.html)**

View File

@ -73,8 +73,9 @@ public class VelocityUtils {
velocityContext.put("permissionPrefix", getPermissionPrefix(moduleName, businessName));
velocityContext.put("dicts", getDicts(genTable));
velocityContext.put("dictList", getDictList(genTable));
velocityContext.put("columns", getColumns(genTable));
velocityContext.put("columns", genTable.getColumns());
velocityContext.put("table", genTable);
velocityContext.put("StrUtil", new StrUtil());
setMenuVelocityContext(velocityContext, genTable);
if (GenConstants.TPL_TREE.equals(tplCategory)) {
setTreeVelocityContext(velocityContext, genTable);
@ -189,9 +190,9 @@ public class VelocityUtils {
} else if (template.contains("index-tree.vue.vm")) {
fileName = StringUtils.format("{}/views/{}/{}/index.vue", soybeanPath, moduleName, StrUtil.toSymbolCase(businessName, '-'));
} else if (template.contains("api.d.ts.vm")) {
fileName = StringUtils.format("{}/typings/api/{}.api.d.ts", soybeanPath, moduleName);
fileName = StringUtils.format("{}/typings/api/{}.{}.api.d.ts", soybeanPath, moduleName, StrUtil.toSymbolCase(businessName, '-'));
} else if (template.contains("api.ts.vm")) {
fileName = StringUtils.format("{}/api/{}/{}.ts", soybeanPath, moduleName, StrUtil.toSymbolCase(businessName, '-'));
fileName = StringUtils.format("{}/service/api/{}/{}.ts", soybeanPath, moduleName, StrUtil.toSymbolCase(businessName, '-'));
} else if (template.contains("search.vue.vm")) {
fileName = StringUtils.format("{}/views/{}/{}/modules/{}-search.vue", soybeanPath, moduleName, StrUtil.toSymbolCase(businessName, '-'), StrUtil.toSymbolCase(businessName, '-'));
} else if (template.contains("operate-drawer.vue.vm")) {
@ -296,23 +297,6 @@ public class VelocityUtils {
}
}
/**
* 根据列类型获取字典组
*
* @param genTable 业务表对象
* @return 返回字典组
*/
public static List<GenTableColumn> getColumns(GenTable genTable) {
List<GenTableColumn> columns = genTable.getColumns();
for (GenTableColumn column : columns) {
if (StringUtils.isNotBlank(column.getDictType())) {
column.setDictType(StringUtils.toCamelCase(column.getDictType()));
}
}
return columns;
}
/**
* 获取权限前缀
*

View File

@ -0,0 +1,59 @@
-- 修改字典数据表的 list_class 字段,将 danger 改为 error
UPDATE `sys_dict_data` SET `list_class` = 'error' WHERE `list_class` = 'danger';
-- 字典适配多语言
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_user_sex.male', `dict_type` = 'sys_user_sex' WHERE `dict_code` = 1;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_user_sex.female', `dict_type` = 'sys_user_sex' WHERE `dict_code` = 2;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_user_sex.unknown', `dict_type` = 'sys_user_sex' WHERE `dict_code` = 3;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_show_hide.show', `dict_type` = 'sys_show_hide' WHERE `dict_code` = 4;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_show_hide.hide', `dict_type` = 'sys_show_hide' WHERE `dict_code` = 5;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_normal_disable.normal', `dict_type` = 'sys_normal_disable' WHERE `dict_code` = 6;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_normal_disable.disable', `dict_type` = 'sys_normal_disable' WHERE `dict_code` = 7;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_yes_no.yes', `dict_type` = 'sys_yes_no' WHERE `dict_code` = 12;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_yes_no.no', `dict_type` = 'sys_yes_no' WHERE `dict_code` = 13;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_notice_type.notice', `dict_type` = 'sys_notice_type' WHERE `dict_code` = 14;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_notice_type.announcement', `dict_type` = 'sys_notice_type' WHERE `dict_code` = 15;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_notice_status.normal', `dict_type` = 'sys_notice_status' WHERE `dict_code` = 16;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_notice_status.close', `dict_type` = 'sys_notice_status' WHERE `dict_code` = 17;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_oper_type.insert', `dict_type` = 'sys_oper_type' WHERE `dict_code` = 18;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_oper_type.update', `dict_type` = 'sys_oper_type' WHERE `dict_code` = 19;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_oper_type.delete', `dict_type` = 'sys_oper_type' WHERE `dict_code` = 20;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_oper_type.grant', `dict_type` = 'sys_oper_type' WHERE `dict_code` = 21;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_oper_type.export', `dict_type` = 'sys_oper_type' WHERE `dict_code` = 22;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_oper_type.import', `dict_type` = 'sys_oper_type' WHERE `dict_code` = 23;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_oper_type.force', `dict_type` = 'sys_oper_type' WHERE `dict_code` = 24;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_oper_type.gencode', `dict_type` = 'sys_oper_type' WHERE `dict_code` = 25;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_oper_type.clean', `dict_type` = 'sys_oper_type' WHERE `dict_code` = 26;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_common_status.success', `dict_type` = 'sys_common_status' WHERE `dict_code` = 27;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_common_status.fail', `dict_type` = 'sys_common_status' WHERE `dict_code` = 28;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_oper_type.other', `dict_type` = 'sys_oper_type' WHERE `dict_code` = 29;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_grant_type.password', `dict_type` = 'sys_grant_type' WHERE `dict_code` = 30;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_grant_type.sms', `dict_type` = 'sys_grant_type' WHERE `dict_code` = 31;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_grant_type.email', `dict_type` = 'sys_grant_type' WHERE `dict_code` = 32;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_grant_type.miniapp', `dict_type` = 'sys_grant_type' WHERE `dict_code` = 33;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_grant_type.social', `dict_type` = 'sys_grant_type' WHERE `dict_code` = 34;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_device_type.pc', `dict_type` = 'sys_device_type' WHERE `dict_code` = 35;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_device_type.android', `dict_type` = 'sys_device_type' WHERE `dict_code` = 36;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_device_type.ios', `dict_type` = 'sys_device_type' WHERE `dict_code` = 37;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.sys_device_type.miniapp', `dict_type` = 'sys_device_type' WHERE `dict_code` = 38;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_business_status.revoked', `dict_type` = 'wf_business_status' WHERE `dict_code` = 39;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_business_status.draft', `dict_type` = 'wf_business_status' WHERE `dict_code` = 40;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_business_status.pending', `dict_type` = 'wf_business_status' WHERE `dict_code` = 41;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_business_status.completed', `dict_type` = 'wf_business_status' WHERE `dict_code` = 42;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_business_status.cancelled', `dict_type` = 'wf_business_status' WHERE `dict_code` = 43;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_business_status.returned', `dict_type` = 'wf_business_status' WHERE `dict_code` = 44;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_business_status.terminated', `dict_type` = 'wf_business_status' WHERE `dict_code` = 45;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_form_type.custom_form', `dict_type` = 'wf_form_type' WHERE `dict_code` = 46;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_form_type.dynamic_form', `dict_type` = 'wf_form_type' WHERE `dict_code` = 47;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_task_status.revoke', `dict_type` = 'wf_task_status' WHERE `dict_code` = 48;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_task_status.pass', `dict_type` = 'wf_task_status' WHERE `dict_code` = 49;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_task_status.pending_review', `dict_type` = 'wf_task_status' WHERE `dict_code` = 50;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_task_status.cancel', `dict_type` = 'wf_task_status' WHERE `dict_code` = 51;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_task_status.return', `dict_type` = 'wf_task_status' WHERE `dict_code` = 52;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_task_status.terminate', `dict_type` = 'wf_task_status' WHERE `dict_code` = 53;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_task_status.transfer', `dict_type` = 'wf_task_status' WHERE `dict_code` = 54;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_task_status.delegate', `dict_type` = 'wf_task_status' WHERE `dict_code` = 55;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_task_status.copy', `dict_type` = 'wf_task_status' WHERE `dict_code` = 56;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_task_status.add_sign', `dict_type` = 'wf_task_status' WHERE `dict_code` = 57;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_task_status.minus_sign', `dict_type` = 'wf_task_status' WHERE `dict_code` = 58;
UPDATE `sys_dict_data` SET `dict_label` = 'dict.wf_task_status.timeout', `dict_type` = 'wf_task_status' WHERE `dict_code` = 59;

View File

@ -1,38 +1,57 @@
-- 目录类型菜单
UPDATE `sys_menu` SET `component` = 'Layout', `icon` = 'carbon:cloud-service-management' WHERE `menu_id` = 1;
UPDATE `sys_menu` SET `component` = 'Layout', `icon` = 'stash:dashboard' WHERE `menu_id` = 2;
UPDATE `sys_menu` SET `component` = 'Layout', `icon` = 'tabler:tools' WHERE `menu_id` = 3;
UPDATE `sys_menu` SET `component` = 'Layout', `icon` = 'material-symbols:kid-star-outline' WHERE `menu_id` = 5;
UPDATE `sys_menu` SET `component` = 'Layout', `icon` = 'tabler:building-cog' WHERE `menu_id` = 6;
UPDATE `sys_menu` SET `component` = 'Layout', `icon` = 'tabler:logs' WHERE `menu_id` = 108;
UPDATE `sys_menu` SET `component` = 'Layout', `icon` = 'carbon:cloud-service-management', `menu_name` = 'route.system' WHERE `menu_id` = 1;
UPDATE `sys_menu` SET `component` = 'Layout', `icon` = 'stash:dashboard', `menu_name` = 'route.monitor' WHERE `menu_id` = 2;
UPDATE `sys_menu` SET `component` = 'Layout', `icon` = 'tabler:tools', `menu_name` = 'route.tool' WHERE `menu_id` = 3;
UPDATE `sys_menu` SET `component` = 'Layout', `icon` = 'material-symbols:kid-star-outline', `menu_name` = 'route.demo' WHERE `menu_id` = 5;
UPDATE `sys_menu` SET `component` = 'Layout', `icon` = 'tabler:building-cog', `menu_name` = 'menu.system_tenant' WHERE `menu_id` = 6;
UPDATE `sys_menu` SET `component` = 'Layout', `icon` = 'tabler:logs', `menu_name` = 'menu.system_log' WHERE `menu_id` = 108;
-- 页面类型
UPDATE `sys_menu` SET `icon` = 'ic:round-manage-accounts' WHERE `menu_id` = 100;
UPDATE `sys_menu` SET `icon` = 'carbon:user-role' WHERE `menu_id` = 101;
UPDATE `sys_menu` SET `icon` = 'material-symbols:route' WHERE `menu_id` = 102;
UPDATE `sys_menu` SET `icon` = 'mingcute:department-line' WHERE `menu_id` = 103;
UPDATE `sys_menu` SET `icon` = 'hugeicons:permanent-job' WHERE `menu_id` = 104;
UPDATE `sys_menu` SET `icon` = 'qlementine-icons:dictionary-16' WHERE `menu_id` = 105;
UPDATE `sys_menu` SET `icon` = 'carbon:parameter' WHERE `menu_id` = 106;
UPDATE `sys_menu` SET `icon` = 'solar:chat-line-outline' WHERE `menu_id` = 107;
UPDATE `sys_menu` SET `icon` = 'majesticons:status-online-line' WHERE `menu_id` = 109;
UPDATE `sys_menu` SET `icon` = 'simple-icons:redis' WHERE `menu_id` = 113;
UPDATE `sys_menu` SET `icon` = 'material-symbols:code-blocks-outline' WHERE `menu_id` = 115;
UPDATE `sys_menu` SET `icon` = 'material-symbols:attach-file' WHERE `menu_id` = 118;
UPDATE `sys_menu` SET `icon` = 'tabler:building-skyscraper' WHERE `menu_id` = 121;
UPDATE `sys_menu` SET `icon` = 'lets-icons:package-box-alt' WHERE `menu_id` = 122;
UPDATE `sys_menu` SET `icon` = 'tabler:device-imac-cog' WHERE `menu_id` = 123;
UPDATE `sys_menu` SET `icon` = 'carbon:operations-record' WHERE `menu_id` = 500;
UPDATE `sys_menu` SET `icon` = 'tabler:login-2' WHERE `menu_id` = 501;
UPDATE `sys_menu` SET `icon` = 'gg:debug' WHERE `menu_id` = 1500;
UPDATE `sys_menu` SET `icon` = 'gg:debug' WHERE `menu_id` = 1506;
UPDATE `sys_menu` SET `path` = 'oss/config', `component` = 'system/oss-config/index', `icon` = 'hugeicons:configuration-01' WHERE `menu_id` = 133;
UPDATE `sys_menu` SET `icon` = 'ic:round-manage-accounts', `menu_name` = 'route.system_user' WHERE `menu_id` = 100;
UPDATE `sys_menu` SET `icon` = 'carbon:user-role', `menu_name` = 'route.system_role' WHERE `menu_id` = 101;
UPDATE `sys_menu` SET `icon` = 'material-symbols:route', `menu_name` = 'route.system_menu' WHERE `menu_id` = 102;
UPDATE `sys_menu` SET `icon` = 'mingcute:department-line', `menu_name` = 'route.system_dept' WHERE `menu_id` = 103;
UPDATE `sys_menu` SET `icon` = 'hugeicons:permanent-job', `menu_name` = 'route.system_post' WHERE `menu_id` = 104;
UPDATE `sys_menu` SET `icon` = 'qlementine-icons:dictionary-16', `menu_name` = 'route.system_dict' WHERE `menu_id` = 105;
UPDATE `sys_menu` SET `icon` = 'carbon:parameter', `menu_name` = 'route.system_config' WHERE `menu_id` = 106;
UPDATE `sys_menu` SET `icon` = 'solar:chat-line-outline', `menu_name` = 'route.system_notice' WHERE `menu_id` = 107;
UPDATE `sys_menu` SET `icon` = 'majesticons:status-online-line', `menu_name` = 'route.monitor_online' WHERE `menu_id` = 109;
UPDATE `sys_menu` SET `icon` = 'simple-icons:redis', `menu_name` = 'route.monitor_cache' WHERE `menu_id` = 113;
UPDATE `sys_menu` SET `icon` = 'material-symbols:code-blocks-outline', `menu_name` = 'route.tool_gen' WHERE `menu_id` = 115;
UPDATE `sys_menu` SET `icon` = 'material-symbols:attach-file', `menu_name` = 'route.system_oss' WHERE `menu_id` = 118;
UPDATE `sys_menu` SET `icon` = 'tabler:building-skyscraper', `menu_name` = 'route.system_tenant' WHERE `menu_id` = 121;
UPDATE `sys_menu` SET `icon` = 'lets-icons:package-box-alt', `menu_name` = 'route.system_tenant-package' WHERE `menu_id` = 122;
UPDATE `sys_menu` SET `icon` = 'tabler:device-imac-cog', `menu_name` = 'route.system_client' WHERE `menu_id` = 123;
UPDATE `sys_menu` SET `icon` = 'carbon:operations-record', `menu_name` = 'route.monitor_operlog' WHERE `menu_id` = 500;
UPDATE `sys_menu` SET `icon` = 'tabler:login-2', `menu_name` = 'route.monitor_logininfor' WHERE `menu_id` = 501;
UPDATE `sys_menu` SET `icon` = 'gg:debug', `menu_name` = 'route.demo_demo' WHERE `menu_id` = 1500;
UPDATE `sys_menu` SET `icon` = 'gg:debug', `menu_name` = 'route.demo_tree' WHERE `menu_id` = 1506;
UPDATE `sys_menu` SET `path` = 'oss/config', `component` = 'system/oss-config/index', `icon` = 'hugeicons:configuration-01', `menu_name` = 'route.system_oss-config' WHERE `menu_id` = 133;
-- IFrame 类型
UPDATE `sys_menu` SET `component` = 'FrameView', `query_param` = 'https://ruoyi.xlsea.cn/admin/', `is_frame` = 2, `icon` = 'bx:bxl-spring-boot' WHERE `menu_id` = 117;
UPDATE `sys_menu` SET `component` = 'FrameView', `query_param` = 'https://preview.snailjob.opensnail.com/', `is_frame` = 2, `icon` = 'gridicons:scheduled' WHERE `menu_id` = 120;
UPDATE `sys_menu` SET `component` = 'FrameView', `query_param` = 'https://ruoyi.xlsea.cn/admin/', `is_frame` = 2, `icon` = 'bx:bxl-spring-boot', `menu_name` = 'menu.monitor_admin' WHERE `menu_id` = 117;
UPDATE `sys_menu` SET `component` = 'FrameView', `query_param` = 'https://preview.snailjob.opensnail.com/', `is_frame` = 2, `icon` = 'gridicons:scheduled', `menu_name` = 'menu.monitor_snail-job' WHERE `menu_id` = 120;
-- 外链类型
UPDATE `sys_menu` SET `path` = 'https://gitee.com/xlsea/ruoyi-plus-soybean', `component` = 'FrameView', `icon` = 'local-icon-gitee' WHERE `menu_id` = 4;
UPDATE `sys_menu` SET `path` = 'https://gitee.com/xlsea/ruoyi-plus-soybean', `component` = 'FrameView', `icon` = 'local-icon-gitee', `menu_name` = 'RuoYi-Plus-Soybean' WHERE `menu_id` = 4;
-- plus-ui 需要禁用的页面
UPDATE `sys_menu` SET `status` = '1' WHERE `menu_id` IN ( '116', '130', '131', '132', '11700', '11701' );
UPDATE `sys_menu` SET `status` = '1' WHERE `menu_id` IN ( '116', '130', '131', '132' );
-- 工作流要启用的页面
UPDATE `sys_menu` SET `status` = '0' WHERE `menu_id` IN ( '11616', '11618', '11700', '11701' );
-- 工作流菜单
UPDATE `sys_menu` SET `component` = 'Layout', `icon` = 'hugeicons:flow-square' WHERE `menu_id` = 11616;
UPDATE `sys_menu` SET `component` = 'Layout', `icon` = 'fluent:notepad-person-16-regular' WHERE `menu_id` = 11618;
UPDATE `sys_menu` SET `component` = 'workflow/task/taskWaiting/index', `icon` = 'ri:todo-line' WHERE `menu_id` = 11619;
UPDATE `sys_menu` SET `icon` = 'weui:setting-outlined' WHERE `menu_id` = 11620;
UPDATE `sys_menu` SET `icon` = 'ri:instance-line' WHERE `menu_id` = 11621;
UPDATE `sys_menu` SET `icon` = 'carbon:category' WHERE `menu_id` = 11622;
UPDATE `sys_menu` SET `component` = 'workflow/task/myDocument/index', `icon` = 'hugeicons:start-up-02' WHERE `menu_id` = 11629;
UPDATE `sys_menu` SET `component` = 'Layout', `icon` = 'lucide:monitor-cog' WHERE `menu_id` = 11630;
UPDATE `sys_menu` SET `component` = 'workflow/task/allTaskWaiting/index', `icon` = 'ri:todo-line' WHERE `menu_id` = 11631;
UPDATE `sys_menu` SET `component` = 'workflow/task/taskFinish/index', `icon` = 'hugeicons:task-done-01' WHERE `menu_id` = 11632;
UPDATE `sys_menu` SET `path` = 'taskCopy', `component` = 'workflow/task/taskCopy/index', `icon` = 'mynaui:copy' WHERE `menu_id` = 11633;
UPDATE `sys_menu` SET `icon` = 'ic:twotone-time-to-leave' WHERE `menu_id` = 11638;
UPDATE `sys_menu` SET `icon` = 'material-symbols:design-services-outline', `path` = 'design', `component` = 'workflow/design/index' WHERE `menu_id` = 11700;
UPDATE `sys_menu` SET `icon` = 'ic:twotone-time-to-leave' WHERE `menu_id` = 11701;
UPDATE `sys_menu` SET `icon` = 'material-symbols:regular-expression-rounded' WHERE `menu_id` = 11801;

View File

@ -2,7 +2,7 @@
import { NDivider } from 'naive-ui';
import { jsonClone } from '@sa/utils';
import { type TableDataWithIndex } from '@sa/hooks';
import { fetchBatchDelete${BusinessName}, fetchGet${BusinessName}List } from '@/service/api/${moduleName}/${businessName}';
import { fetchBatchDelete${BusinessName}, fetchGet${BusinessName}List } from '@/service/api/${moduleName}/${business__name}';
import { useAppStore } from '@/store/modules/app';
import { useAuth } from '@/hooks/business/auth';
import { useTreeTable, useTreeTableOperate } from '@/hooks/common/tree-table';

View File

@ -1,6 +1,6 @@
<script setup lang="tsx">
import { NDivider } from 'naive-ui';
import { fetchBatchDelete${BusinessName}, fetchGet${BusinessName}List } from '@/service/api/${moduleName}/${businessName}';
import { fetchBatchDelete${BusinessName}, fetchGet${BusinessName}List } from '@/service/api/${moduleName}/${business__name}';
import { useAppStore } from '@/store/modules/app';
import { useAuth } from '@/hooks/business/auth';
import { useDownload } from '@/hooks/business/download';

View File

@ -52,7 +52,7 @@ function createDefaultModel(): Model {
return {
#foreach($column in $columns)
#if($column.insert)
${column.javaField}:#if($column.javaType == 'String') ''#else null#end#if($foreach.hasNext),#end
${column.javaField}:#if($column.javaType == 'String' || ($!column.dictType && $column.dictType != '')) ''#else undefined#end#if($foreach.hasNext),#end
#end
#end
};
@ -91,16 +91,22 @@ function closeDrawer() {
async function handleSubmit() {
await validate();
#set($operateColumns = [])
#foreach($column in $columns)#if($column.insert || $column.edit)#set($dummy = $operateColumns.add($column))#end#end
const { #foreach($column in $operateColumns)$column.javaField#if($foreach.hasNext), #end#end } = model;
// request
if (props.operateType === 'add') {
const { #foreach($column in $columns)#if($column.insert)$column.javaField#if($foreach.hasNext), #end#end#end } = model;
const { error } = await fetchCreate${BusinessName}({ #foreach($column in $columns)#if($column.insert)$column.javaField#if($foreach.hasNext), #end#end#end });
#set($addFields = [])
#foreach($column in $columns)#if($column.insert)#set($dummy = $addFields.add($column.javaField))#end#end
const { error } = await fetchCreate${BusinessName}({ #foreach($field in $addFields)$field#if($foreach.hasNext), #end#end });
if (error) return;
}
if (props.operateType === 'edit') {
const { #foreach($column in $columns)#if($column.edit)$column.javaField#if($foreach.hasNext), #end#end#end } = model;
const { error } = await fetchUpdate${BusinessName}({ #foreach($column in $columns)#if($column.edit)$column.javaField#if($foreach.hasNext), #end#end#end });
#set($editFields = [])
#foreach($column in $columns)#if($column.edit)#set($dummy = $editFields.add($column.javaField))#end#end
const { error } = await fetchUpdate${BusinessName}({ #foreach($field in $editFields)$field#if($foreach.hasNext), #end#end });
if (error) return;
}
@ -130,7 +136,7 @@ watch(visible, () => {
#else
#set($comment=$column.columnComment)
#end
#set($dictType=$column.dictType)
#set($dictType=$!StrUtil.toCamelCase($column.dictType))
<NFormItem label="$column.columnComment" path="$column.javaField">
#if($column.htmlType == "textarea" || $column.htmlType == "editor")
<NInput
@ -143,7 +149,7 @@ watch(visible, () => {
<NSelect
v-model:value="model.$column.javaField"
placeholder="请选择$column.columnComment"
:options="${column.dictType}Options"
:options="${dictType}Options"
clearable
/>
#elseif($column.htmlType == "select" && $dictType)
@ -157,7 +163,7 @@ watch(visible, () => {
<NRadioGroup v-model:value="model.$column.javaField">
<NSpace>
<NRadio
v-for="option in ${column.dictType}Options"
v-for="option in ${dictType}Options"
:key="option.value"
:value="option.value"
:label="option.label"
@ -170,13 +176,36 @@ watch(visible, () => {
<NRadio value="0" label="请选择字典生成" />
</NSpace>
</NRadioGroup>
#elseif($column.htmlType == "datetime")
#elseif($column.htmlType == "checkbox" && "" != $dictType)
<NCheckboxGroup v-model:value="model.$column.javaField">
<NSpace>
<NCheckbox
v-for="option in ${dictType}Options"
:key="option.value"
:value="option.value"
:label="option.label"
/>
</NSpace>
</NCheckboxGroup>
#elseif($column.htmlType == "checkbox" && $dictType)
<NCheckboxGroup v-model:value="model.$column.javaField">
<NSpace>
<NCheckbox value="0" label="请选择字典生成" />
</NSpace>
</NCheckboxGroup>
#elseif($column.htmlType == 'datetime')
<NDatePicker
v-model:formatted-value="model.$column.javaField"
type="date"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
clearable
/>
#elseif($column.htmlType == "imageUpload")
<OssUpload v-model:value="model.$column.javaField" upload-type="image" />
#elseif($column.htmlType == "fileUpload")
<OssUpload v-model:value="model.$column.javaField" upload-type="file" />
#elseif($column.htmlType == "editor")
<TinymceEditor v-model:value="model.$column.javaField" />
#else <NInput v-model:value="model.$column.javaField" placeholder="请输入$column.columnComment" />
#end
</NFormItem>

View File

@ -50,7 +50,7 @@ async function search() {
#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
if (dateRange${AttrName}.value?.length) {
model.value.params!.begin${AttrName} = dateRange${AttrName}.value[0];
model.value.params!.end${AttrName} = dateRange${AttrName}.value[0];
model.value.params!.end${AttrName} = dateRange${AttrName}.value[1];
}
#end
#end
@ -66,7 +66,7 @@ async function search() {
<NGrid responsive="screen" item-responsive>
#foreach($column in $columns)
#if($column.query)
#set($dictType=$column.dictType)
#set($dictType=$!StrUtil.toCamelCase($column.dictType))
#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
#set($parentheseIndex=$column.columnComment.indexOf(""))
#if($parentheseIndex != -1)
@ -75,35 +75,28 @@ async function search() {
#set($comment=$column.columnComment)
#end
<NFormItemGi span="24 s:12 m:6" label="$column.columnComment" path="$column.javaField" class="pr-24px">
#if(($column.htmlType == "select" || $column.htmlType == "radio") && "" != $dictType)
#if($!StrUtil.contains("select, radio, checkbox", $column.htmlType) && $dictType && "" != $dictType)
<NSelect
v-model:value="model.$column.javaField"
placeholder="请选择$column.columnComment"
:options="${column.dictType}Options"
:options="${dictType}Options"
clearable
/>
#elseif(($column.htmlType == "select" || $column.htmlType == "radio") && $dictType)
#elseif($!StrUtil.contains("select, radio, checkbox", $column.htmlType))
<NSelect
v-model:value="model.$column.javaField"
placeholder="请选择$column.columnComment"
:options="[]"
clearable
/>
#elseif($column.htmlType.equals('select') || $column.htmlType.equals('radio'))
<NSelect
v-model:value="model.$column.javaField"
placeholder="请选择$column.columnComment"
:options="[]"
clearable
/>
#elseif($column.htmlType == "datetime" && $column.queryType != "BETWEEN")
#elseif($column.htmlType == 'datetime' && $column.queryType != "BETWEEN")
<NDatePicker
v-model:formatted-value="model.$column.javaField"
type="date"
value-format="yyyy-MM-dd"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
clearable
/>
#elseif($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
#elseif($column.htmlType == 'datetime' && $column.queryType == "BETWEEN")
<NDatePicker
v-model:formatted-value="dateRange${AttrName}"
type="datetimerange"

View File

@ -1,44 +1,51 @@
#set($BaseEntity = ['createDept', 'createBy', 'createTime', 'updateBy', 'updateTime'])
#set($ModuleName = $moduleName.substring(0, 1).toUpperCase() + $moduleName.substring(1))
/**
* namespace ${ModuleName}
* Namespace Api
*
* backend api module: "${ModuleName}"
* All backend api type
*/
namespace ${ModuleName} {
/** ${businessname} */
type ${BusinessName} = Common.CommonRecord<{
#foreach($column in $columns)#if(!$BaseEntity.contains($column.javaField))
/** $column.columnComment */
$column.javaField:#if($column.javaField.indexOf("id") != -1 || $column.javaField.indexOf("Id") != -1) CommonType.IdType; #elseif($column.javaType == 'Long' || $column.javaType == 'Integer' || $column.javaType == 'Double' || $column.javaType == 'Float' || $column.javaType == 'BigDecimal') number; #elseif($column.javaType == 'Boolean') boolean; #else string; #end
#end#end
}>;
declare namespace Api {
/**
* namespace ${ModuleName}
*
* backend api module: "${ModuleName}"
*/
namespace ${ModuleName} {
/** ${businessname} */
type ${BusinessName} = Common.CommonRecord<{
#foreach($column in $columns)#if(!$BaseEntity.contains($column.javaField))
/** $column.columnComment */
$column.javaField:#if($column.javaField.indexOf("id") != -1 || $column.javaField.indexOf("Id") != -1) CommonType.IdType; #elseif($column.javaType == 'Long' || $column.javaType == 'Integer' || $column.javaType == 'Double' || $column.javaType == 'Float' || $column.javaType == 'BigDecimal') number; #elseif($column.javaType == 'Boolean') boolean; #else string; #end
#end#end
}>;
/** ${businessname} search params */
type ${BusinessName}SearchParams = CommonType.RecordNullable<
Pick<
Api.${ModuleName}.${BusinessName},
#foreach($column in $columns)
#if($column.query && $column.queryType != 'BETWEEN')
| '${column.javaField}'
#end
#end
> &
Api.Common.CommonSearchParams
>;
/** ${businessname} search params */
type ${BusinessName}SearchParams = CommonType.RecordNullable<
Pick<
Api.${ModuleName}.${BusinessName},
#foreach($column in $columns)
#if($column.query && $column.queryType != 'BETWEEN')
| '${column.javaField}'
#end
#end
> &
Api.Common.CommonSearchParams
>;
/** ${businessname} operate params */
type ${BusinessName}OperateParams = CommonType.RecordNullable<
Pick<
Api.${ModuleName}.${BusinessName},
#foreach($column in $columns)
#if($column.insert || $column.edit)
| '${column.javaField}'
#end
#end
>
>;
/** ${businessname} operate params */
type ${BusinessName}OperateParams = CommonType.RecordNullable<
Pick<
Api.${ModuleName}.${BusinessName},
#foreach($column in $columns)
#if($column.insert || $column.edit)
| '${column.javaField}'
#end
#end
>
>;
/** ${businessname} list */
type ${BusinessName}List = Api.Common.PaginatingQueryRecord<${BusinessName}>;
/** ${businessname} list */
type ${BusinessName}List = Api.Common.PaginatingQueryRecord<${BusinessName}>;
}
}

View File

@ -2,7 +2,7 @@
<html lang="zh-cmn-Hans">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.png" />
<link rel="icon" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="light dark" />
<title>%VITE_APP_TITLE%</title>

View File

@ -1,15 +1,15 @@
{
"name": "ruoyi-vue-plus",
"type": "module",
"version": "1.0.0",
"description": "RuoYi-Vue-Plus多租户管理系统",
"version": "1.1.3",
"description": "结合了 RuoYi-Vue-Plus 的强大后端功能和 Soybean Admin 的现代化前端特性,为开发者提供了完整的企业管理解决方案。",
"author": {
"name": "xlsea",
"email": "xlsea@linux.do",
"url": "https://gitee.com/xlsea/ruoyi-plus-soybean"
"email": "m@xlsea.cn",
"url": "https://gitee.com/xlsea"
},
"license": "MIT",
"homepage": "https://ruoyi.xlsea.cn",
"homepage": "https://docs.ruoyi.xlsea.cn",
"repository": {
"url": "https://gitee.com/xlsea/ruoyi-plus-soybean.git"
},
@ -18,17 +18,25 @@
},
"keywords": [
"RuoYi-Vue-Plus",
"Soybean Admin",
"Vue3 admin ",
"vue-admin-template",
"Vite6",
"Vite7",
"TypeScript",
"naive-ui",
"naive-ui-admin",
"UnoCSS"
],
"contributors": [
{
"name": "Elio",
"email": "1983933789@qq.com",
"url": "https://gitee.com/elio-an"
}
],
"engines": {
"node": ">=18.20.0",
"pnpm": ">=8.7.0"
"node": ">=20.19.0",
"pnpm": ">=10.5.0"
},
"scripts": {
"build": "vite build --mode prod",
@ -57,7 +65,8 @@
"@sa/materials": "workspace:*",
"@sa/tinymce": "workspace:*",
"@sa/utils": "workspace:*",
"@vueuse/core": "13.1.0",
"@types/streamsaver": "^2.0.5",
"@vueuse/core": "13.5.0",
"clipboard": "2.0.11",
"dayjs": "1.11.13",
"defu": "6.1.4",
@ -65,51 +74,52 @@
"highlight.js": "^11.11.1",
"jsencrypt": "^3.3.2",
"json5": "2.2.3",
"monaco-editor": "^0.52.0",
"naive-ui": "2.41.0",
"monaco-editor": "^0.52.2",
"naive-ui": "2.42.0",
"nprogress": "0.2.0",
"pinia": "3.0.2",
"tailwind-merge": "3.2.0",
"vue": "3.5.13",
"pinia": "3.0.3",
"streamsaver": "^2.0.6",
"tailwind-merge": "3.3.1",
"vue": "3.5.17",
"vue-advanced-cropper": "^2.8.9",
"vue-draggable-plus": "0.6.0",
"vue-i18n": "11.1.3",
"vue-i18n": "11.1.9",
"vue-router": "4.5.1"
},
"devDependencies": {
"@elegant-router/vue": "0.3.8",
"@iconify/json": "2.2.337",
"@iconify/json": "2.2.357",
"@sa/scripts": "workspace:*",
"@sa/uno-preset": "workspace:*",
"@soybeanjs/eslint-config": "1.6.0",
"@types/node": "22.15.17",
"@soybeanjs/eslint-config": "1.7.1",
"@types/node": "24.0.13",
"@types/nprogress": "0.2.3",
"@unocss/eslint-config": "66.1.1",
"@unocss/preset-icons": "66.1.1",
"@unocss/preset-uno": "66.1.1",
"@unocss/transformer-directives": "66.1.1",
"@unocss/transformer-variant-group": "66.1.1",
"@unocss/vite": "66.1.1",
"@vitejs/plugin-vue": "5.2.4",
"@vitejs/plugin-vue-jsx": "4.1.2",
"@unocss/eslint-config": "66.3.3",
"@unocss/preset-icons": "66.3.3",
"@unocss/preset-uno": "66.3.3",
"@unocss/transformer-directives": "66.3.3",
"@unocss/transformer-variant-group": "66.3.3",
"@unocss/vite": "66.3.3",
"@vitejs/plugin-vue": "6.0.0",
"@vitejs/plugin-vue-jsx": "5.0.1",
"consola": "3.4.2",
"eslint": "9.26.0",
"eslint-plugin-vue": "10.1.0",
"eslint": "9.31.0",
"eslint-plugin-vue": "10.3.0",
"kolorist": "1.8.0",
"sass": "1.88.0",
"sass": "1.89.2",
"simple-git-hooks": "2.13.0",
"tsx": "4.19.4",
"tsx": "4.20.3",
"typescript": "5.8.3",
"unplugin-icons": "22.1.0",
"unplugin-vue-components": "28.5.0",
"vite": "6.3.5",
"unplugin-vue-components": "28.8.0",
"vite": "7.0.4",
"vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-progress": "0.0.7",
"vite-plugin-static-copy": "^3.0.0",
"vite-plugin-static-copy": "^3.1.0",
"vite-plugin-svg-icons": "2.0.1",
"vite-plugin-vue-devtools": "7.7.6",
"vue-eslint-parser": "10.1.3",
"vue-tsc": "2.2.10"
"vite-plugin-vue-devtools": "7.7.7",
"vue-eslint-parser": "10.2.0",
"vue-tsc": "3.0.1"
},
"simple-git-hooks": {
"commit-msg": "pnpm sa git-commit-verify",

View File

@ -1,6 +1,6 @@
{
"name": "@sa/alova",
"version": "1.3.13",
"version": "1.3.15",
"exports": {
".": "./src/index.ts",
"./fetch": "./src/fetch.ts",
@ -13,8 +13,8 @@
}
},
"dependencies": {
"@alova/mock": "2.0.14",
"@alova/mock": "2.0.17",
"@sa/utils": "workspace:*",
"alova": "3.2.10"
"alova": "3.3.4"
}
}

View File

@ -1,6 +1,6 @@
{
"name": "@sa/axios",
"version": "1.3.13",
"version": "1.3.15",
"exports": {
".": "./src/index.ts"
},
@ -11,11 +11,11 @@
},
"dependencies": {
"@sa/utils": "workspace:*",
"axios": "1.9.0",
"axios": "1.10.0",
"axios-retry": "4.5.0",
"qs": "6.14.0"
},
"devDependencies": {
"@types/qs": "6.9.18"
"@types/qs": "6.14.0"
}
}

View File

@ -1,6 +1,6 @@
{
"name": "@sa/color",
"version": "1.3.13",
"version": "1.3.15",
"exports": {
".": "./src/index.ts"
},

View File

@ -1,6 +1,6 @@
{
"name": "@sa/hooks",
"version": "1.3.13",
"version": "1.3.15",
"exports": {
".": "./src/index.ts"
},

View File

@ -64,7 +64,10 @@ export default function useHookTable<A extends ApiFn, T, C>(config: TableConfig<
const { loading, startLoading, endLoading } = useLoading();
const { bool: empty, setBool: setEmpty } = useBoolean();
const { apiFn, apiParams, transformer, immediate = true, getColumnChecks, getColumns } = config;
const { transformer, immediate = true, getColumnChecks, getColumns } = config;
let currentApiFn = config.apiFn;
const apiParams = config.apiParams;
const searchParams: NonNullable<Parameters<A>[0]> = reactive(jsonClone({ ...apiParams }));
@ -94,7 +97,7 @@ export default function useHookTable<A extends ApiFn, T, C>(config: TableConfig<
const formattedParams = formatSearchParams(searchParams);
const response = await apiFn(formattedParams);
const response = await currentApiFn(formattedParams);
const transformed = transformer(response as Awaited<ReturnType<A>>);
@ -119,6 +122,10 @@ export default function useHookTable<A extends ApiFn, T, C>(config: TableConfig<
return formattedParams;
}
function updateApiFn(newApiFn: A) {
currentApiFn = newApiFn;
}
/**
* update search params
*
@ -148,6 +155,7 @@ export default function useHookTable<A extends ApiFn, T, C>(config: TableConfig<
getData,
searchParams,
updateSearchParams,
resetSearchParams
resetSearchParams,
updateApiFn
};
}

View File

@ -1,6 +1,6 @@
{
"name": "@sa/materials",
"version": "1.3.13",
"version": "1.3.15",
"exports": {
".": "./src/index.ts"
},
@ -11,7 +11,7 @@
},
"dependencies": {
"@sa/utils": "workspace:*",
"simplebar-vue": "2.4.1"
"simplebar-vue": "2.4.2"
},
"devDependencies": {
"typed-css-modules": "0.9.1"

View File

@ -1,6 +1,6 @@
{
"name": "@sa/fetch",
"version": "1.3.13",
"version": "1.3.15",
"exports": {
".": "./src/index.ts"
},

View File

@ -1,6 +1,6 @@
{
"name": "@sa/scripts",
"version": "1.3.13",
"version": "1.3.15",
"bin": {
"sa": "./bin.ts"
},
@ -14,12 +14,12 @@
},
"devDependencies": {
"@soybeanjs/changelog": "0.3.24",
"bumpp": "10.1.0",
"c12": "3.0.3",
"bumpp": "10.2.0",
"c12": "3.0.4",
"cac": "6.7.14",
"consola": "3.4.2",
"enquirer": "2.4.1",
"execa": "9.5.3",
"execa": "9.6.0",
"kolorist": "1.8.0",
"npm-check-updates": "18.0.1",
"rimraf": "6.0.1"

View File

@ -5,7 +5,7 @@ import type { CliOption } from '../types';
const defaultOptions: CliOption = {
cwd: process.cwd(),
cleanupDirs: [
'**/dist',
'dist',
'**/package-lock.json',
'**/yarn.lock',
'**/pnpm-lock.yaml',

View File

@ -1,6 +1,6 @@
{
"name": "@sa/tinymce",
"version": "1.3.13",
"version": "1.3.15",
"exports": {
".": "./src/index.ts"
},
@ -10,9 +10,9 @@
}
},
"dependencies": {
"tinymce": "7.8.0"
"tinymce": "7.9.1"
},
"devDependencies": {
"@tinymce/tinymce-vue": "6.1.0"
"@tinymce/tinymce-vue": "6.2.0"
}
}

View File

@ -1,6 +1,6 @@
{
"name": "@sa/uno-preset",
"version": "1.3.13",
"version": "1.3.15",
"exports": {
".": "./src/index.ts"
},

View File

@ -1,6 +1,6 @@
{
"name": "@sa/utils",
"version": "1.3.13",
"version": "1.3.15",
"exports": {
".": "./src/index.ts"
},

4195
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

4
public/favicon.svg Normal file
View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 476.22 476.22">
<path fill="#0e42d2" d="M389.98,400.6c-64.58,76.59-176.66,93.98-261.1,38.54-57.75-37.91-92.29-103.74-87.69-173.54,1.28-19.35,9.54-58.77,33.33-61.44,20.49-2.29,43.2,14.41,62.99,21.14,64.93,22.1,150.88,21.39,214.51-4.81,17.24-7.1,39.23-23.9,58.05-13.31,25.61,14.41,27.13,67.07,24.38,92.75-3.94,36.78-20.9,72.7-44.47,100.65ZM150.91,269c-17.4-4.01-34.19-9.87-50.5-17.04-2.56-1.12-15.17-8.19-16.64-6.83-23.51,92.25,48.81,179.87,140.64,188.01,3.68.33,9,.72,12.64.79,3.76.07,7.18-.41,10.86-.78-.24-3.7-1.05-7.05-.77-10.85,1.3-17.71,28.61-26.36,26.88-43.25-.89-8.76-14.85-11.03-22.02-11.55-8.23-.6-17.94,1.77-25.24-3.11-5.39-3.61-5.08-10.32-7.37-15.76-4.74-11.26-15.86-17.54-27.79-18.47-22.55-1.77-68.52,7.5-81.87-16.63-11.77-21.27,12.24-35.1,29.25-40.42,3.38-1.06,7.52-1.31,10.88-2.55,1.03-.38,1.28.27,1.04-1.56ZM391.83,245.21c-1.03-.58-12.51,5.5-14.65,6.42-20.82,8.92-42.01,16.02-64.2,20.87-10.31,2.25-21.7,3.15-31.66,5.65-19.17,4.82-17.59,22.78-.42,29.11,10.27,3.78,25.34,3.94,36.56,5.97,23.56,4.26,56.74,11.28,53.56,41.93-.31,2.98-2.13,5.86-2.26,8.2-.08,1.48.18,1.21,1.5,1.13,5.18-9.87,11.46-19.09,15.7-29.46,7.55-18.44,12.19-42.75,11.17-62.68-.19-3.67-2.95-25.81-5.31-27.15Z"/>
<path fill="#0e42d2" d="M278.22,21.54c4.79,5.69,9.27,14.39,11.84,21.37.67,1.8,1.49,7.86,2.59,8.61,1.49,1.01,17.78-3.63,21.39-4.05,71.76-8.37,101.88,65.14,44.46,110.3-41.92,32.98-84.43,32.67-135.62,31.41-47.74-1.18-111.25-13.52-129.97-64.78-19.66-53.81,31.01-83.19,77.15-58.5,2,1.07,11.58,7.79,12.54,7.59,1.57-.32,1.05-4.62,1.34-6.14,3.75-19.76,11.37-39.47,27.8-52.04,20.82-15.93,49.31-14.16,66.47,6.23ZM256.71,71.1c1.07-24.31-18.03-42.71-30.8-12.52-5.88,13.9-7.77,32.56-3.36,47,1.73.46,1.24-.41,1.7-.95,10.15-11.8,19.02-25.11,32.46-33.52ZM340.18,123.96c10.12-10.37,17.92-27.48.94-35.85-29-14.31-83.74,17.89-87.91,49.24-1.6,11.97,2.2,14.36,13.71,14.53,21.84.32,57.86-12.13,73.26-27.92ZM173.64,117.92c-4.77-6.36-9.81-11.55-16.53-15.93-7.07-4.62-24.6-13.3-29.47-2.16-3.94,9.02,6.8,23.05,13.43,28.86,12.36,10.83,29.63,16.76,45.66,19.42-.28-11.02-6.62-21.58-13.08-30.19Z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1,179 @@
<!--
mitm.html is the lite "man in the middle"
This is only meant to signal the opener's messageChannel to
the service worker - when that is done this mitm can be closed
but it's better to keep it alive since this also stops the sw
from restarting
The service worker is capable of intercepting all request and fork their
own "fake" response - wish we are going to craft
when the worker then receives a stream then the worker will tell the opener
to open up a link that will start the download
-->
<script>
// This will prevent the sw from restarting
let keepAlive = () => {
keepAlive = () => {};
var ping = location.href.substr(0, location.href.lastIndexOf('/')) + '/ping';
var interval = setInterval(() => {
if (sw) {
sw.postMessage('ping');
} else {
fetch(ping).then(res => res.text(!res.ok && clearInterval(interval)));
}
}, 10000);
};
// message event is the first thing we need to setup a listner for
// don't want the opener to do a random timeout - instead they can listen for
// the ready event
// but since we need to wait for the Service Worker registration, we store the
// message for later
let messages = [];
window.onmessage = evt => messages.push(evt);
let sw = null;
let scope = '';
function registerWorker() {
return navigator.serviceWorker
.getRegistration('./')
.then(swReg => {
return swReg || navigator.serviceWorker.register('sw.js', { scope: './' });
})
.then(swReg => {
const swRegTmp = swReg.installing || swReg.waiting;
scope = swReg.scope;
return (
(sw = swReg.active) ||
new Promise(resolve => {
swRegTmp.addEventListener(
'statechange',
(fn = () => {
if (swRegTmp.state === 'activated') {
swRegTmp.removeEventListener('statechange', fn);
sw = swReg.active;
resolve();
}
})
);
})
);
});
}
// Now that we have the Service Worker registered we can process messages
function onMessage(event) {
let { data, ports, origin } = event;
// It's important to have a messageChannel, don't want to interfere
// with other simultaneous downloads
if (!ports || !ports.length) {
throw new TypeError("[StreamSaver] You didn't send a messageChannel");
}
if (typeof data !== 'object') {
throw new TypeError("[StreamSaver] You didn't send a object");
}
// the default public service worker for StreamSaver is shared among others.
// so all download links needs to be prefixed to avoid any other conflict
data.origin = origin;
// if we ever (in some feature versoin of streamsaver) would like to
// redirect back to the page of who initiated a http request
data.referrer = data.referrer || document.referrer || origin;
// pass along version for possible backwards compatibility in sw.js
data.streamSaverVersion = new URLSearchParams(location.search).get('version');
if (data.streamSaverVersion === '1.2.0') {
console.warn('[StreamSaver] please update streamsaver');
}
/** @since v2.0.0 */
if (!data.headers) {
console.warn(
"[StreamSaver] pass `data.headers` that you would like to pass along to the service worker\nit should be a 2D array or a key/val object that fetch's Headers api accepts"
);
} else {
// test if it's correct
// should thorw a typeError if not
new Headers(data.headers);
}
/** @since v2.0.0 */
if (typeof data.filename === 'string') {
console.warn(
"[StreamSaver] You shouldn't send `data.filename` anymore. It should be included in the Content-Disposition header option"
);
// Do what File constructor do with fileNames
data.filename = data.filename.replace(/\//g, ':');
}
/** @since v2.0.0 */
if (data.size) {
console.warn(
"[StreamSaver] You shouldn't send `data.size` anymore. It should be included in the content-length header option"
);
}
/** @since v2.0.0 */
if (data.readableStream) {
console.warn('[StreamSaver] You should send the readableStream in the messageChannel, not throught mitm');
}
/** @since v2.0.0 */
if (!data.pathname) {
console.warn('[StreamSaver] Please send `data.pathname` (eg: /pictures/summer.jpg)');
data.pathname = Math.random().toString().slice(-6) + '/' + data.filename;
}
// remove all leading slashes
data.pathname = data.pathname.replace(/^\/+/g, '');
// remove protocol
let org = origin.replace(/(^\w+:|^)\/\//, '');
// set the absolute pathname to the download url.
data.url = new URL(`${scope + org}/${data.pathname}`).toString();
if (!data.url.startsWith(`${scope + org}/`)) {
throw new TypeError('[StreamSaver] bad `data.pathname`');
}
// This sends the message data as well as transferring
// messageChannel.port2 to the service worker. The service worker can
// then use the transferred port to reply via postMessage(), which
// will in turn trigger the onmessage handler on messageChannel.port1.
const transferable = data.readableStream ? [ports[0], data.readableStream] : [ports[0]];
if (!(data.readableStream || data.transferringReadable)) {
keepAlive();
}
return sw.postMessage(data, transferable);
}
if (window.opener) {
// The opener can't listen to onload event, so we need to help em out!
// (telling them that we are ready to accept postMessage's)
window.opener.postMessage('StreamSaver::loadedPopup', '*');
}
if (navigator.serviceWorker) {
registerWorker().then(() => {
window.onmessage = onMessage;
messages.forEach(window.onmessage);
});
}
// FF v102 just started to supports transferable streams, but still needs to ping sw.js
// even tough the service worker dose not have to do any kind of work and listen to any
// messages... #305
keepAlive();
</script>

132
public/streamsaver/sw.js Normal file
View File

@ -0,0 +1,132 @@
/* eslint-disable */
/* global self ReadableStream Response */
self.addEventListener('install', () => {
self.skipWaiting();
});
self.addEventListener('activate', event => {
event.waitUntil(self.clients.claim());
});
const map = new Map();
// This should be called once per download
// Each event has a dataChannel that the data will be piped through
self.onmessage = event => {
// We send a heartbeat every x second to keep the
// service worker alive if a transferable stream is not sent
if (event.data === 'ping') {
return;
}
const data = event.data;
const downloadUrl =
data.url || `${self.registration.scope + Math.random()}/${typeof data === 'string' ? data : data.filename}`;
const port = event.ports[0];
const metadata = Array.from({ length: 3 }); // [stream, data, port]
metadata[1] = data;
metadata[2] = port;
// Note to self:
// old streamsaver v1.2.0 might still use `readableStream`...
// but v2.0.0 will always transfer the stream through MessageChannel #94
if (event.data.readableStream) {
metadata[0] = event.data.readableStream;
} else if (event.data.transferringReadable) {
port.onmessage = evt => {
port.onmessage = null;
metadata[0] = evt.data.readableStream;
};
} else {
metadata[0] = createStream(port);
}
map.set(downloadUrl, metadata);
port.postMessage({ download: downloadUrl });
};
function createStream(port) {
// ReadableStream is only supported by chrome 52
return new ReadableStream({
start(controller) {
// When we receive data on the messageChannel, we write
port.onmessage = ({ data }) => {
if (data === 'end') {
return controller.close();
}
if (data === 'abort') {
controller.error('Aborted the download');
return;
}
controller.enqueue(data);
};
},
cancel(reason) {
console.log('user aborted', reason);
port.postMessage({ abort: true });
}
});
}
self.onfetch = event => {
const url = event.request.url;
// this only works for Firefox
if (url.endsWith('/ping')) {
return event.respondWith(new Response('pong'));
}
const hijacke = map.get(url);
if (!hijacke) return null;
const [stream, data, port] = hijacke;
map.delete(url);
// Not comfortable letting any user control all headers
// so we only copy over the length & disposition
const responseHeaders = new Headers({
'Content-Type': 'application/octet-stream; charset=utf-8',
// To be on the safe side, The link can be opened in a iframe.
// but octet-stream should stop it.
'Content-Security-Policy': "default-src 'none'",
'X-Content-Security-Policy': "default-src 'none'",
'X-WebKit-CSP': "default-src 'none'",
'X-XSS-Protection': '1; mode=block',
'Cross-Origin-Embedder-Policy': 'require-corp'
});
const headers = new Headers(data.headers || {});
if (headers.has('Content-Length')) {
responseHeaders.set('Content-Length', headers.get('Content-Length'));
}
if (headers.has('Content-Disposition')) {
responseHeaders.set('Content-Disposition', headers.get('Content-Disposition'));
}
// data, data.filename and size should not be used anymore
if (data.size) {
console.warn('Depricated');
responseHeaders.set('Content-Length', data.size);
}
let fileName = typeof data === 'string' ? data : data.filename;
if (fileName) {
console.warn('Depricated');
// Make filename RFC5987 compatible
fileName = encodeURIComponent(fileName).replace(/['()]/g, escape).replace(/\*/g, '%2A');
responseHeaders.set('Content-Disposition', `attachment; filename*=UTF-8''${fileName}`);
}
event.respondWith(new Response(stream, { headers: responseHeaders }));
port.postMessage({ debug: 'Download started' });
};

View File

@ -4,8 +4,8 @@ import { NConfigProvider, darkTheme } from 'naive-ui';
import type { WatermarkProps } from 'naive-ui';
import { useAppStore } from './store/modules/app';
import { useThemeStore } from './store/modules/theme';
import { naiveDateLocales, naiveLocales } from './locales/naive';
import { useAuthStore } from './store/modules/auth';
import { naiveDateLocales, naiveLocales } from './locales/naive';
defineOptions({
name: 'App'
@ -27,8 +27,12 @@ const naiveDateLocale = computed(() => {
const watermarkProps = computed<WatermarkProps>(() => {
const appTitle = import.meta.env.VITE_APP_TITLE || 'RuoYi-Vue-Plus';
const content =
themeStore.watermark.enableUserName && userInfo.user?.userName
? `${userInfo.user?.nickName}@${appTitle} ${userInfo.user?.userName}`
: appTitle;
return {
content: userInfo.user?.userName ? `${userInfo.user?.nickName}@${appTitle} ${userInfo.user?.userName}` : appTitle,
content,
cross: true,
fullscreen: true,
fontSize: 14,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 192 KiB

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 476.22 476.22">
<path fill="#0e42d2" d="M389.98,400.6c-64.58,76.59-176.66,93.98-261.1,38.54-57.75-37.91-92.29-103.74-87.69-173.54,1.28-19.35,9.54-58.77,33.33-61.44,20.49-2.29,43.2,14.41,62.99,21.14,64.93,22.1,150.88,21.39,214.51-4.81,17.24-7.1,39.23-23.9,58.05-13.31,25.61,14.41,27.13,67.07,24.38,92.75-3.94,36.78-20.9,72.7-44.47,100.65ZM150.91,269c-17.4-4.01-34.19-9.87-50.5-17.04-2.56-1.12-15.17-8.19-16.64-6.83-23.51,92.25,48.81,179.87,140.64,188.01,3.68.33,9,.72,12.64.79,3.76.07,7.18-.41,10.86-.78-.24-3.7-1.05-7.05-.77-10.85,1.3-17.71,28.61-26.36,26.88-43.25-.89-8.76-14.85-11.03-22.02-11.55-8.23-.6-17.94,1.77-25.24-3.11-5.39-3.61-5.08-10.32-7.37-15.76-4.74-11.26-15.86-17.54-27.79-18.47-22.55-1.77-68.52,7.5-81.87-16.63-11.77-21.27,12.24-35.1,29.25-40.42,3.38-1.06,7.52-1.31,10.88-2.55,1.03-.38,1.28.27,1.04-1.56ZM391.83,245.21c-1.03-.58-12.51,5.5-14.65,6.42-20.82,8.92-42.01,16.02-64.2,20.87-10.31,2.25-21.7,3.15-31.66,5.65-19.17,4.82-17.59,22.78-.42,29.11,10.27,3.78,25.34,3.94,36.56,5.97,23.56,4.26,56.74,11.28,53.56,41.93-.31,2.98-2.13,5.86-2.26,8.2-.08,1.48.18,1.21,1.5,1.13,5.18-9.87,11.46-19.09,15.7-29.46,7.55-18.44,12.19-42.75,11.17-62.68-.19-3.67-2.95-25.81-5.31-27.15Z"/>
<path fill="#0e42d2" d="M278.22,21.54c4.79,5.69,9.27,14.39,11.84,21.37.67,1.8,1.49,7.86,2.59,8.61,1.49,1.01,17.78-3.63,21.39-4.05,71.76-8.37,101.88,65.14,44.46,110.3-41.92,32.98-84.43,32.67-135.62,31.41-47.74-1.18-111.25-13.52-129.97-64.78-19.66-53.81,31.01-83.19,77.15-58.5,2,1.07,11.58,7.79,12.54,7.59,1.57-.32,1.05-4.62,1.34-6.14,3.75-19.76,11.37-39.47,27.8-52.04,20.82-15.93,49.31-14.16,66.47,6.23ZM256.71,71.1c1.07-24.31-18.03-42.71-30.8-12.52-5.88,13.9-7.77,32.56-3.36,47,1.73.46,1.24-.41,1.7-.95,10.15-11.8,19.02-25.11,32.46-33.52ZM340.18,123.96c10.12-10.37,17.92-27.48.94-35.85-29-14.31-83.74,17.89-87.91,49.24-1.6,11.97,2.2,14.36,13.71,14.53,21.84.32,57.86-12.13,73.26-27.92ZM173.64,117.92c-4.77-6.36-9.81-11.55-16.53-15.93-7.07-4.62-24.6-13.3-29.47-2.16-3.94,9.02,6.8,23.05,13.43,28.86,12.36,10.83,29.63,16.76,45.66,19.42-.28-11.02-6.62-21.58-13.08-30.19Z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -31,13 +31,25 @@ const tooltipContent = computed(() => {
return $t('icon.lang');
});
/** Add bottom margin to all options except the last one for proper visual separation */
const dropdownOptions = computed(() => {
const lastIndex = props.langOptions.length - 1;
return props.langOptions.map((option, index) => ({
...option,
props: {
class: index < lastIndex ? 'mb-1' : undefined
}
}));
});
function changeLang(lang: App.I18n.LangType) {
emit('changeLang', lang);
}
</script>
<template>
<NDropdown :value="lang" :options="langOptions" trigger="hover" @select="changeLang">
<NDropdown :value="lang" :options="dropdownOptions" trigger="hover" @select="changeLang">
<div>
<ButtonIcon :tooltip-content="tooltipContent" tooltip-placement="left">
<SvgIcon icon="heroicons:language" />

View File

@ -3,7 +3,7 @@ defineOptions({ name: 'SystemLogo' });
</script>
<template>
<img src="@/assets/imgs/logo.png" />
<icon-local-logo />
</template>
<style scoped></style>

View File

@ -2,7 +2,7 @@
import { useAttrs } from 'vue';
import type { TreeSelectProps } from 'naive-ui';
import { useLoading } from '@sa/hooks';
import { fetchGetDeptSelect } from '@/service/api/system';
import { fetchGetDeptTree } from '@/service/api/system';
defineOptions({ name: 'DeptTreeSelect' });
@ -13,16 +13,21 @@ interface Props {
defineProps<Props>();
const value = defineModel<CommonType.IdType | null>('value', { required: false });
const options = defineModel<Api.System.Dept[]>('options', { required: false, default: [] });
const options = defineModel<Api.Common.CommonTreeRecord>('options', { required: false, default: [] });
const expandedKeys = defineModel<CommonType.IdType[]>('expandedKeys', { required: false, default: [] });
const attrs: TreeSelectProps = useAttrs();
const { loading, startLoading, endLoading } = useLoading();
async function getDeptList() {
startLoading();
const { error, data } = await fetchGetDeptSelect();
const { error, data } = await fetchGetDeptTree();
if (error) return;
options.value = data;
// 设置默认展开的节点
if (data?.length && !expandedKeys.value.length) {
expandedKeys.value = [data[0].id];
}
endLoading();
}
@ -32,13 +37,13 @@ getDeptList();
<template>
<NTreeSelect
v-model:value="value"
v-model:expanded-keys="expandedKeys"
filterable
class="h-full"
:loading="loading"
key-field="deptId"
label-field="deptName"
:options="options"
:default-expanded-keys="[0]"
key-field="id"
label-field="label"
:options="options as []"
v-bind="attrs"
/>
</template>

View File

@ -3,6 +3,7 @@ import { computed, useAttrs } from 'vue';
import type { TagProps } from 'naive-ui';
import { useDict } from '@/hooks/business/dict';
import { isNotNull } from '@/utils/common';
import { $t } from '@/locales';
defineOptions({ name: 'DictTag' });
@ -23,13 +24,18 @@ const props = withDefaults(defineProps<Props>(), {
const attrs = useAttrs() as TagProps;
const { transformDictData } = useDict(props.dictCode, props.immediate);
const dictTagData = computed<Api.System.DictData[]>(() => {
if (props.dictData) {
return [props.dictData];
const dictData = props.dictData;
if (dictData.dictLabel?.startsWith(`dict.${dictData.dictType}.`)) {
dictData.dictLabel = $t(dictData.dictLabel as App.I18n.I18nKey);
}
return [dictData];
}
// 避免 props.value 为 0 时,无法触发
if (props.dictCode && isNotNull(props.value)) {
const { transformDictData } = useDict(props.dictCode, props.immediate);
return transformDictData(props.value) || [];
}

View File

@ -1,9 +1,10 @@
<script setup lang="ts">
import { ref, useAttrs, watch } from 'vue';
import { computed, useAttrs } from 'vue';
import type { UploadFileInfo, UploadProps } from 'naive-ui';
import { fetchBatchDeleteOss } from '@/service/api/system/oss';
import { getToken } from '@/store/modules/auth/shared';
import { getServiceBaseURL } from '@/utils/service';
import { AcceptType } from '@/enum/business';
defineOptions({
name: 'FileUpload'
@ -26,27 +27,24 @@ const props = withDefaults(defineProps<Props>(), {
defaultUpload: true,
showTip: true,
max: 5,
accept: '.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.pdf',
accept: undefined,
fileSize: 5,
uploadType: 'file'
});
const accept = computed(() => {
if (props.accept) {
return props.accept;
}
return props.uploadType === 'file' ? AcceptType.File : AcceptType.Image;
});
const attrs: UploadProps = useAttrs();
let fileNum = 0;
const fileList = ref<UploadFileInfo[]>([]);
const needRelaodData = ref(false);
defineExpose({
needRelaodData
const fileList = defineModel<UploadFileInfo[]>('fileList', {
default: () => []
});
watch(
() => fileList.value,
newValue => {
needRelaodData.value = newValue.length > 0;
}
);
const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y';
const { baseURL } = getServiceBaseURL(import.meta.env, isHttpProxy);
@ -61,12 +59,12 @@ function beforeUpload(options: { file: UploadFileInfo; fileList: UploadFileInfo[
const { file } = options;
// 校检文件类型
if (props.accept) {
if (accept.value) {
const fileName = file.name.split('.');
const fileExt = `.${fileName[fileName.length - 1]}`;
const isTypeOk = props.accept.split(',')?.includes(fileExt);
const isTypeOk = accept.value.split(',')?.includes(fileExt);
if (!isTypeOk) {
window.$message?.error(`文件格式不正确, 请上传 ${props.accept} 格式文件!`);
window.$message?.error(`文件格式不正确, 请上传 ${accept.value} 格式文件!`);
return false;
}
}
@ -119,64 +117,67 @@ function handleError(options: { file: UploadFileInfo; event?: ProgressEvent }) {
async function handleRemove(file: UploadFileInfo) {
if (file.status !== 'finished') {
return;
return false;
}
const { error } = await fetchBatchDeleteOss([file.id]);
if (error) return;
if (error) return false;
window.$message?.success('删除成功');
return true;
}
</script>
<template>
<NUpload
v-bind="attrs"
v-model:file-list="fileList"
:action="`${baseURL}${action}`"
:data="data"
:headers="headers"
:max="max"
:accept="accept"
:multiple="max > 1"
directory-dnd
:default-upload="defaultUpload"
:list-type="uploadType === 'image' ? 'image-card' : 'text'"
:is-error-state="isErrorState"
@finish="handleFinish"
@error="handleError"
@before-upload="beforeUpload"
@remove="({ file }) => handleRemove(file)"
>
<NUploadDragger v-if="uploadType === 'file'">
<div class="mb-12px flex-center">
<SvgIcon icon="material-symbols:unarchive-outline" class="text-58px color-#d8d8db dark:color-#a1a1a2" />
</div>
<NText class="text-16px">点击或者拖动文件到该区域来上传</NText>
<NP v-if="showTip" depth="3" class="mt-8px text-center">
请上传
<template v-if="fileSize">
大小不超过
<b class="text-red-500">{{ fileSize }}MB</b>
</template>
<template v-if="accept">
且格式为
<b class="text-red-500">{{ accept.replaceAll(',', '/') }}</b>
</template>
的文件
</NP>
</NUploadDragger>
</NUpload>
<NP v-if="showTip && uploadType === 'image'" depth="3" class="mt-12px">
请上传
<template v-if="fileSize">
大小不超过
<b class="text-red-500">{{ fileSize }}MB</b>
</template>
<template v-if="accept">
且格式为
<b class="text-red-500">{{ accept.replaceAll(',', '/') }}</b>
</template>
的文件
</NP>
<div class="w-full flex-col">
<NUpload
v-bind="attrs"
v-model:file-list="fileList"
:action="`${baseURL}${action}`"
:data="data"
:headers="headers"
:max="max"
:accept="accept"
:multiple="max > 1"
directory-dnd
:default-upload="defaultUpload"
:list-type="uploadType === 'image' ? 'image-card' : 'text'"
:is-error-state="isErrorState"
@finish="handleFinish"
@error="handleError"
@before-upload="beforeUpload"
@remove="({ file }) => handleRemove(file)"
>
<NUploadDragger v-if="uploadType === 'file'">
<div class="mb-12px flex-center">
<SvgIcon icon="material-symbols:unarchive-outline" class="text-58px color-#d8d8db dark:color-#a1a1a2" />
</div>
<NText class="text-16px">点击或者拖动文件到该区域来上传</NText>
<NP v-if="showTip" depth="3" class="mt-8px text-center">
请上传
<template v-if="fileSize">
大小不超过
<b class="text-red-500">{{ fileSize }}MB</b>
</template>
<template v-if="accept">
且格式为
<b class="text-red-500">{{ accept.replaceAll(',', '/') }}</b>
</template>
的文件
</NP>
</NUploadDragger>
</NUpload>
<NP v-if="showTip && uploadType === 'image'" depth="3" class="mt-12px">
请上传
<template v-if="fileSize">
大小不超过
<b class="text-red-500">{{ fileSize }}MB</b>
</template>
<template v-if="accept">
且格式为
<b class="text-red-500">{{ accept.replaceAll(',', '/') }}</b>
</template>
的文件
</NP>
</div>
</template>
<style scoped></style>

View File

@ -1,18 +1,22 @@
<script setup lang="tsx">
import { useAttrs } from 'vue';
import { onMounted, useAttrs } from 'vue';
import type { TreeOption, TreeSelectProps } from 'naive-ui';
import { useLoading } from '@sa/hooks';
import { fetchGetMenuList } from '@/service/api/system';
import { handleTree } from '@/utils/common';
import SvgIcon from '@/components/custom/svg-icon.vue';
import { $t } from '@/locales';
defineOptions({ name: 'MenuTreeSelect' });
interface Props {
immediate?: boolean;
[key: string]: any;
}
defineProps<Props>();
const props = withDefaults(defineProps<Props>(), {
immediate: true
});
const value = defineModel<CommonType.IdType | null>('value', { required: false });
const options = defineModel<Api.System.MenuList>('options', { required: false, default: [] });
@ -35,7 +39,19 @@ async function getMenuList() {
endLoading();
}
getMenuList();
onMounted(() => {
if (props.immediate) {
getMenuList();
}
});
function renderLabel({ option }: { option: TreeOption }) {
let label = String(option.menuName);
if (label?.startsWith('route.') || label?.startsWith('menu.')) {
label = $t(label as App.I18n.I18nKey);
}
return <div>{label}</div>;
}
function renderPrefix({ option }: { option: TreeOption }) {
const renderLocalIcon = String(option.icon).startsWith('local-icon-');
@ -55,6 +71,8 @@ function renderPrefix({ option }: { option: TreeOption }) {
label-field="menuName"
:options="options"
:default-expanded-keys="[0]"
:render-tag="renderLabel"
:render-label="renderLabel"
:render-prefix="renderPrefix"
v-bind="attrs"
/>

View File

@ -4,6 +4,7 @@ import type { TreeOption, TreeSelectInst, TreeSelectProps } from 'naive-ui';
import { useBoolean } from '@sa/hooks';
import { fetchGetMenuTreeSelect } from '@/service/api/system';
import SvgIcon from '@/components/custom/svg-icon.vue';
import { $t } from '@/locales';
defineOptions({ name: 'MenuTree' });
@ -51,7 +52,6 @@ onMounted(() => {
}
});
// 添加 watch 监听 expandAll 的变化,options有值后计算expandedKeys
watch([expandAll, options], ([newVal]) => {
if (newVal) {
// 展开所有节点
@ -61,6 +61,14 @@ watch([expandAll, options], ([newVal]) => {
}
});
function renderLabel({ option }: { option: TreeOption }) {
let label = option.label;
if (label?.startsWith('route.') || label?.startsWith('menu.')) {
label = $t(label as App.I18n.I18nKey);
}
return <div>{label}</div>;
}
function renderPrefix({ option }: { option: TreeOption }) {
const renderLocalIcon = String(option.icon).startsWith('local-icon-');
let icon = renderLocalIcon ? undefined : String(option.icon ?? 'material-symbols:buttons-alt-outline-rounded');
@ -82,6 +90,21 @@ function getAllMenuIds(menu: Api.System.MenuList) {
return menuIds;
}
/** 获取所有叶子节点的 ID没有子节点的节点 */
function getLeafMenuIds(menu: Api.System.MenuList): CommonType.IdType[] {
const leafIds: CommonType.IdType[] = [];
menu.forEach(item => {
if (!item.children || item.children.length === 0) {
// 是叶子节点
leafIds.push(item.id!);
} else {
// 有子节点,递归获取子节点中的叶子节点
leafIds.push(...getLeafMenuIds(item.children));
}
});
return leafIds;
}
function handleCheckedTreeNodeAll(checked: boolean) {
if (checked) {
checkedKeys.value = getAllMenuIds(options.value);
@ -90,16 +113,30 @@ function handleCheckedTreeNodeAll(checked: boolean) {
checkedKeys.value = [];
}
function getCheckedMenuIds() {
function getCheckedMenuIds(isCascade: boolean = false) {
const menuIds = menuTreeRef.value?.getCheckedData()?.keys as string[];
const indeterminateData = menuTreeRef.value?.getIndeterminateData();
if (cascade.value) {
if (cascade.value || isCascade) {
const parentIds: string[] = indeterminateData?.keys.filter(item => !menuIds?.includes(String(item))) as string[];
menuIds?.push(...parentIds);
}
return menuIds;
}
watch(cascade, () => {
if (cascade.value) {
// 获取当前菜单树中的所有叶子节点ID
const allLeafIds = getLeafMenuIds(options.value);
// 筛选出当前选中项中的叶子节点
const selectedLeafIds = checkedKeys.value.filter(id => allLeafIds.includes(id));
// 重新设置选中状态为只包含叶子节点,让组件基于父子联动规则重新计算父节点状态
checkedKeys.value = selectedLeafIds;
return;
}
// 禁用父子联动时,将半选中的父节点也加入到选中列表
checkedKeys.value = getCheckedMenuIds(true);
});
defineExpose({
getCheckedMenuIds,
refresh: getMenuList
@ -135,6 +172,7 @@ defineExpose({
:loading="loading"
virtual-scroll
check-strategy="all"
:render-label="renderLabel"
:render-prefix="renderPrefix"
v-bind="attrs"
/>

View File

@ -0,0 +1,69 @@
<script setup lang="ts">
import { ref, useAttrs, watch } from 'vue';
import type { UploadFileInfo } from 'naive-ui';
import { useLoading } from '@sa/hooks';
import { fetchGetOssListByIds } from '@/service/api/system/oss';
import { isNotNull } from '@/utils/common';
import FileUpload from '@/components/custom/file-upload.vue';
defineOptions({
name: 'OssUpload'
});
const attrs = useAttrs();
const value = defineModel<string>('value', { default: '' });
const { loading, startLoading, endLoading } = useLoading();
const fileList = ref<UploadFileInfo[]>([]);
async function handleFetchOssList(ossIds: string[]) {
startLoading();
try {
const { error, data } = await fetchGetOssListByIds(ossIds);
if (error) return;
fileList.value = data.map(item => ({
id: String(item.ossId),
url: item.url,
name: item.originalName,
status: 'finished'
}));
} catch (error) {
window.$message?.error(`获取文件列表失败: ${error}`);
} finally {
endLoading();
}
}
watch(
value,
async val => {
const ossIds = val?.split(',')?.filter(item => isNotNull(item)) || [];
if (ossIds.length === 0) {
fileList.value = [];
return;
}
const fileIds = new Set(fileList.value.filter(item => item.status === 'finished').map(item => item.id));
if (ossIds.every(item => fileIds.has(item))) {
return;
}
await handleFetchOssList(ossIds);
},
{ immediate: true }
);
watch(fileList, val => {
value.value = val
.filter(item => item.status === 'finished')
.map(item => item.id)
.join(',');
});
</script>
<template>
<NSpin v-if="loading" />
<FileUpload v-else v-bind="attrs" v-model:file-list="fileList" />
</template>
<style scoped></style>

View File

@ -21,27 +21,27 @@ const attrs: SelectProps = useAttrs();
const { loading: postLoading, startLoading: startPostLoading, endLoading: endPostLoading } = useLoading();
/** the enabled role options */
const roleOptions = ref<CommonType.Option<CommonType.IdType>[]>([]);
/** the enabled post options */
const postOptions = ref<CommonType.Option<CommonType.IdType>[]>([]);
watch(
() => props.deptId,
() => {
if (!props.deptId) {
roleOptions.value = [];
postOptions.value = [];
return;
}
getRoleOptions();
getPostOptions();
},
{ immediate: true }
);
async function getRoleOptions() {
async function getPostOptions() {
startPostLoading();
const { error, data } = await fetchGetPostSelect(props.deptId!);
if (!error) {
roleOptions.value = data.map(item => ({
postOptions.value = data.map(item => ({
label: item.postName,
value: item.postId
}));
@ -54,7 +54,7 @@ async function getRoleOptions() {
<NSelect
v-model:value="value"
:loading="postLoading"
:options="roleOptions"
:options="postOptions"
v-bind="attrs"
placeholder="请选择岗位"
/>

View File

@ -0,0 +1,79 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { NPopover, NSpace, NTag } from 'naive-ui';
interface Props {
value: string | any[];
type?: NaiveUI.ThemeColor;
size?: 'small' | 'medium' | 'large';
placeholder?: string;
closable?: boolean;
threadshold?: number; // 超过该数量显示popover
}
const props = withDefaults(defineProps<Props>(), {
type: 'info',
size: 'small',
placeholder: '无',
closable: false,
threadshold: 1 // 默认超过1个就显示popover
});
interface Emits {
(e: 'close', index?: number): void;
}
const emit = defineEmits<Emits>();
// 统一解析 value 成数组
const tags = computed(() => {
if (!props.value) return [];
return Array.isArray(props.value) ? props.value : props.value.split(',');
});
function handleClose(index?: number) {
emit('close', index);
}
</script>
<template>
<template v-if="tags.length === 0">
<NTag :size="size">
{{ placeholder }}
</NTag>
</template>
<template v-else-if="tags.length <= threadshold">
<NTag
v-for="(tag, index) in tags"
:key="index"
:type="type"
class="m-1"
:size="size"
:closable="closable"
@close="handleClose(index)"
>
{{ tag }}
</NTag>
</template>
<template v-else>
<NPopover trigger="hover" placement="bottom">
<template #trigger>
<NTag :type="type" :size="size" class="cursor-pointer">{{ tags[0] }}...({{ tags.length }})</NTag>
</template>
<NSpace vertical size="small">
<NTag
v-for="(tag, index) in tags"
:key="index"
:type="type"
:size="size"
:closable="closable"
@close="handleClose(index)"
>
{{ tag }}
</NTag>
</NSpace>
</NPopover>
</template>
</template>

View File

@ -0,0 +1,36 @@
<script setup lang="ts">
import { Tinymce } from '@sa/tinymce';
import { getToken } from '@/store/modules/auth/shared';
import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme';
import { getServiceBaseURL } from '@/utils/service';
defineOptions({
name: 'TinymceEditor'
});
const value = defineModel<string | null>('value', { required: false, default: '' });
const appStore = useAppStore();
const themeStore = useThemeStore();
const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y';
const { baseURL } = getServiceBaseURL(import.meta.env, isHttpProxy);
const headers: Record<string, string> = {
Authorization: `Bearer ${getToken()}`,
clientid: import.meta.env.VITE_APP_CLIENT_ID!
};
</script>
<template>
<Tinymce
v-model="value"
:lang="appStore.locale"
:is-dark="themeStore.darkMode"
:upload-url="`${baseURL}/resource/oss/upload`"
:upload-headers="headers"
/>
</template>
<style scoped></style>

View File

@ -0,0 +1,370 @@
<script setup lang="tsx">
import { computed, ref, watch } from 'vue';
import { NButton } from 'naive-ui';
import { useLoading } from '@sa/hooks';
import { fetchGetDeptTree, fetchGetUserList } from '@/service/api/system';
import { useAppStore } from '@/store/modules/app';
import { useTable, useTableOperate } from '@/hooks/common/table';
import { useDict } from '@/hooks/business/dict';
import { $t } from '@/locales';
import UserSearch from '@/views/system/user/modules/user-search.vue';
import DictTag from './dict-tag.vue';
defineOptions({
name: 'UserSelectModal'
});
interface Props {
title?: string;
multiple?: boolean;
/** 禁选用户ID */
disabledIds?: CommonType.IdType[];
rowKeys?: CommonType.IdType[];
searchUserIds?: string | null;
}
const props = withDefaults(defineProps<Props>(), {
title: '用户选择',
multiple: false,
disabledIds: () => [],
rowKeys: () => [],
searchUserIds: null
});
interface Emits {
(e: 'confirm', value: CommonType.IdType[], rows?: Api.System.User[]): void;
}
const emit = defineEmits<Emits>();
const visible = defineModel<boolean>('visible', {
default: false
});
useDict('sys_normal_disable');
const appStore = useAppStore();
const {
columns,
columnChecks,
data,
getData,
getDataByPage,
loading,
mobilePagination,
searchParams,
resetSearchParams
} = useTable({
apiFn: fetchGetUserList,
apiParams: {
pageNum: 1,
pageSize: 10,
// if you want to use the searchParams in Form, you need to define the following properties, and the value is null
// the value can not be undefined, otherwise the property in Form will not be reactive
deptId: null,
userName: null,
nickName: null,
phonenumber: null,
status: null,
params: {}
},
immediate: false,
columns: () => [
{
type: 'selection',
multiple: props.multiple,
align: 'center',
width: 48,
disabled: row => props.disabledIds.includes(row.userId.toString())
},
{
key: 'index',
title: $t('common.index'),
align: 'center',
width: 64
},
{
key: 'userName',
title: $t('page.system.user.userName'),
align: 'center',
minWidth: 120,
ellipsis: true
},
{
key: 'nickName',
title: $t('page.system.user.nickName'),
align: 'center',
minWidth: 120,
ellipsis: true
},
{
key: 'deptName',
title: $t('page.system.user.deptName'),
align: 'center',
minWidth: 120,
ellipsis: true
},
{
key: 'phonenumber',
title: $t('page.system.user.phonenumber'),
align: 'center',
minWidth: 120,
ellipsis: true
},
{
key: 'status',
title: $t('page.system.user.status'),
align: 'center',
minWidth: 80,
render(row) {
return <DictTag dict-code="sys_normal_disable" value={row.status} />;
}
},
{
key: 'createTime',
title: $t('page.system.user.createTime'),
align: 'center',
minWidth: 120
}
]
});
const { checkedRowKeys } = useTableOperate(data, getData);
// 存储所有页面的用户数据,用于跨页选择
const allPagesData = ref<Api.System.User[]>([]);
// 更新allPagesData保存当前页数据
function updateAllPagesData() {
// 将当前页数据添加到allPagesData中避免重复
data.value.forEach(user => {
const existIndex = allPagesData.value.findIndex(item => item.userId === user.userId);
if (existIndex === -1) {
allPagesData.value.push(user);
} else {
// 更新已存在的数据
allPagesData.value[existIndex] = user;
}
});
}
const { loading: treeLoading, startLoading: startTreeLoading, endLoading: endTreeLoading } = useLoading();
const deptPattern = ref<string>();
const deptData = ref<Api.Common.CommonTreeRecord>([]);
const selectedKeys = ref<string[]>([]);
async function getTreeData() {
startTreeLoading();
const { data: tree, error } = await fetchGetDeptTree();
if (!error) {
deptData.value = tree;
}
endTreeLoading();
}
function handleClickTree(keys: string[]) {
searchParams.deptId = keys.length ? keys[0] : null;
checkedRowKeys.value = [];
getDataByPage();
}
function handleResetTreeData() {
deptPattern.value = undefined;
getTreeData();
}
const expandedKeys = ref<CommonType.IdType[]>([100]);
const selectable = computed(() => {
return !loading.value;
});
function handleResetSearch() {
resetSearchParams();
selectedKeys.value = [];
}
function closeModal() {
checkedRowKeys.value = [];
allPagesData.value = [];
visible.value = false;
}
function handleConfirm() {
if (checkedRowKeys.value.length === 0) {
window.$message?.error('请选择用户');
return;
}
// 获取选中行对应的用户对象(从所有页面数据中筛选)
const selectedUsers = allPagesData.value.filter(item => checkedRowKeys.value.includes(item.userId.toString()));
emit('confirm', checkedRowKeys.value, selectedUsers);
closeModal();
}
function getRowProps(row: Api.System.User) {
return {
onClick: (e: MouseEvent) => {
const target = e.target as HTMLElement;
if (target.closest('.n-data-table-td--selection')) {
return;
}
if (props.disabledIds.includes(row.userId.toString())) {
return;
}
// 将userId转为字符串
const userId = row.userId.toString();
if (props.multiple) {
const index = checkedRowKeys.value.findIndex(key => key === userId);
if (index > -1) {
checkedRowKeys.value.splice(index, 1);
} else {
checkedRowKeys.value.push(userId);
}
} else {
checkedRowKeys.value = [userId];
}
}
};
}
// 监听数据变化(页面切换时)
watch(
data,
() => {
updateAllPagesData();
},
{ deep: true }
);
watch(visible, () => {
if (visible.value) {
getTreeData();
if (props.searchUserIds) {
searchParams.userIds = props.searchUserIds;
}
allPagesData.value = [];
getDataByPage();
checkedRowKeys.value = [...props.rowKeys];
}
});
</script>
<template>
<NModal
v-model:show="visible"
class="user-select-modal max-h-800px max-w-90% w-1400px"
preset="card"
size="medium"
:title="props.title"
>
<TableSiderLayout :sider-title="$t('page.system.dept.title')">
<template #header-extra>
<NButton size="small" text class="h-18px" @click.stop="() => handleResetTreeData()">
<template #icon>
<SvgIcon icon="ic:round-refresh" />
</template>
</NButton>
</template>
<template #sider>
<NInput v-model:value="deptPattern" clearable :placeholder="$t('common.keywordSearch')" />
<NSpin class="dept-tree" :show="treeLoading">
<NTree
v-model:expanded-keys="expandedKeys"
v-model:selected-keys="selectedKeys"
block-node
show-line
:data="deptData as []"
:show-irrelevant-nodes="false"
:pattern="deptPattern"
class="infinite-scroll h-full min-h-200px py-3"
key-field="id"
label-field="label"
virtual-scroll
:selectable="selectable"
@update:selected-keys="handleClickTree"
>
<template #empty>
<NEmpty :description="$t('page.system.dept.empty')" class="h-full min-h-200px justify-center" />
</template>
</NTree>
</NSpin>
</template>
<div class="h-full flex-col-stretch gap-12px overflow-hidden lt-sm:max-h-500px lt-sm:overflow-auto">
<UserSearch v-model:model="searchParams" @reset="handleResetSearch" @search="getDataByPage" />
<TableRowCheckAlert v-model:checked-row-keys="checkedRowKeys" />
<NAlert v-if="props.disabledIds.length > 0" type="warning">
<span>已存在的用户无法被选择</span>
</NAlert>
<NCard
:title="$t('page.system.user.title')"
:bordered="false"
size="small"
class="card-wrapper sm:flex-1-hidden lt-sm:overflow-auto"
>
<template #header-extra>
<TableHeaderOperation
v-model:columns="columnChecks"
:loading="loading"
:show-add="false"
:show-delete="false"
:show-export="false"
@refresh="getData"
></TableHeaderOperation>
</template>
<NDataTable
v-model:checked-row-keys="checkedRowKeys"
:columns="columns"
:data="data"
size="small"
:flex-height="!appStore.isMobile"
:scroll-x="962"
:loading="loading"
:row-props="getRowProps"
remote
:row-key="row => row.userId.toString()"
:pagination="mobilePagination"
class="h-full lt-sm:max-h-300px"
/>
</NCard>
</div>
</TableSiderLayout>
<template #footer>
<NSpace justify="end" :size="16">
<NButton @click="closeModal">{{ $t('common.cancel') }}</NButton>
<NButton type="primary" @click="handleConfirm">{{ $t('common.confirm') }}</NButton>
</NSpace>
</template>
</NModal>
</template>
<style scoped lang="scss">
:deep(.n-layout) {
height: 600px;
@media (max-width: 639px) {
height: auto;
max-height: 500px;
}
}
.user-select-modal {
@media (max-width: 639px) {
:deep(.n-card-content) {
overflow: hidden;
}
:deep(.n-data-table) {
max-height: 300px;
}
}
}
.n-alert {
--n-padding: 5px 13px !important;
--n-icon-margin: 6px 8px 0 12px !important;
--n-icon-size: 20px !important;
}
</style>

View File

@ -0,0 +1,178 @@
<script setup lang="tsx">
import { ref } from 'vue';
import { NPopover, NSpace, NTag } from 'naive-ui';
import { useLoading } from '@sa/hooks';
import { fetchGetFlowHisTaskList } from '@/service/api/workflow/instance';
import { fetchGetOssListByIds } from '@/service/api/system/oss';
import { useDict } from '@/hooks/business/dict';
import { useDownload } from '@/hooks/business/download';
import DictTag from '@/components/custom/dict-tag.vue';
import TagGroup from '@/components/custom/tag-group.vue';
defineOptions({
name: 'ApprovalInfoPanel'
});
interface Props {
/** 业务id */
businessId: CommonType.IdType;
}
const props = defineProps<Props>();
useDict('wf_task_status');
const activeTab = ref('image');
const { loading, startLoading, endLoading } = useLoading();
const { oss } = useDownload();
const columns = ref<NaiveUI.TableColumn<Api.Workflow.HisTask>[]>([
{
title: '任务名称',
key: 'nodeName',
align: 'center',
width: 100
},
{
title: '办理人',
key: 'approveName',
align: 'center',
width: 100,
render: row => {
return <TagGroup value={row.approveName} />;
}
},
{
title: '任务状态',
key: 'flowStatus',
align: 'center',
width: 100,
render: row => {
return <DictTag size="small" value={row.flowStatus} dict-code="wf_task_status" />;
}
},
{
title: '审批意见',
key: 'message',
align: 'center',
width: 100
},
{
title: '开始时间',
key: 'createTime',
align: 'center',
width: 120
},
{
title: '结束时间',
key: 'updateTime',
align: 'center',
width: 120
},
{
title: '运行时间',
key: 'runDuration',
align: 'center',
width: 100
},
{
title: '附件',
key: 'attachmentList',
align: 'center',
width: 120,
render: row => {
if (!row.attachmentList || row.attachmentList.length === 0) return null;
if (row.attachmentList.length === 1) {
return (
<NTag size="small" type="info" class="cursor-pointer">
<div class="flex items-center gap-2" onClick={() => oss(row.attachmentList[0].ossId)}>
{row.attachmentList[0].originalName}
</div>
</NTag>
);
}
return (
<NPopover trigger="hover" placement="bottom">
{{
trigger: () => (
<NTag size="small" type="info" class="cursor-pointer">
{row.attachmentList[0].originalName}...({row.attachmentList.length})
</NTag>
),
default: () => (
<NSpace vertical size="small">
{row.attachmentList.map(item => (
<NTag key={item.ossId} size="small" type="info" class="cursor-pointer">
<div class="flex items-center gap-2" onClick={() => oss(item.ossId)}>
{item.originalName}
</div>
</NTag>
))}
</NSpace>
)
}}
</NPopover>
);
}
}
]);
const instanceId = ref<CommonType.IdType>();
const hisTask = ref<Api.Workflow.HisTask[]>([]);
/** 初始化数据 */
async function initData() {
activeTab.value = 'image';
instanceId.value = undefined;
hisTask.value = [];
await getData();
}
async function getData() {
startLoading();
const { error, data } = await fetchGetFlowHisTaskList(props.businessId);
if (error) {
window.$message?.error(error.message);
return;
}
instanceId.value = data?.instanceId || '';
const rawList = data?.list || [];
if (rawList.length === 0) {
hisTask.value = [];
return;
}
const promises = rawList.map(async (item: Api.Workflow.HisTask) => {
if (item.ext) {
const { error: err, data: ossList } = await fetchGetOssListByIds(item.ext.split(','));
if (!err) {
item.attachmentList = ossList;
}
}
});
await Promise.all(promises);
hisTask.value = rawList;
endLoading();
}
defineExpose({
initData
});
</script>
<template>
<NDivider />
<div>
<NTabs v-model:value="activeTab" type="segment" animated>
<NTabPane display-directive="show" bar-width="100px" name="image" tab="流程图">
<FlowPreview v-if="instanceId" :instance-id="instanceId" />
</NTabPane>
<NTabPane bar-width="100px" name="info" tab="审批信息">
<NDataTable size="small" :scroll-x="760" :columns="columns" :data="hisTask" :loading="loading" />
</NTabPane>
</NTabs>
</div>
</template>

View File

@ -0,0 +1,142 @@
<script lang="ts" setup>
import { reactive, ref, watch } from 'vue';
import type { UploadFileInfo } from 'naive-ui';
import { useLoading } from '@sa/hooks';
import { messageTypeOptions } from '@/constants/workflow';
import { fetchBackTask, fetchGetBackNode } from '@/service/api/workflow/task';
defineOptions({
name: 'BackTaskModal'
});
interface Props {
task: Api.Workflow.Task;
}
const props = defineProps<Props>();
interface Emits {
(e: 'submit'): void;
}
const emit = defineEmits<Emits>();
const visible = defineModel<boolean>('visible', {
default: false
});
const title = defineModel<string>('title', {
default: '驳回'
});
const { loading: backFormLoading, startLoading: startBackFormLoading, endLoading: endBackFormLoading } = useLoading();
const { loading: backBtnLoading, startLoading: startBackBtnLoading, endLoading: endBackBtnLoading } = useLoading();
const accept = ref<string>('.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.pdf,.jpg,.jpeg,.png,.gif,.bmp,.webp');
type Model = Api.Workflow.BackOperateParams;
const backModel = reactive(createBackModel());
function createBackModel(): Model {
return {
taskId: null,
fileId: null,
messageType: ['1'],
nodeCode: null,
message: null,
notice: null,
variables: null
};
}
const fileList = ref<UploadFileInfo[]>([]);
const backTaskNodeOptions = ref<CommonType.Option<string, string>[]>([]);
async function initDefault() {
startBackFormLoading();
startBackBtnLoading();
Object.assign(backModel, createBackModel());
const { error, data } = await fetchGetBackNode(props.task.definitionId, props.task.nodeCode);
endBackFormLoading();
endBackBtnLoading();
if (error) return;
backTaskNodeOptions.value = data.map(item => ({
label: item.nodeName,
value: item.nodeCode
}));
backModel.nodeCode = data[0].nodeCode;
}
async function handleSubmit() {
backModel.taskId = props.task.id;
if (fileList.value?.length) {
const fileIds = fileList.value.map(item => item.id);
backModel.fileId = fileIds.join(',');
}
window.$dialog?.warning({
title: '提示',
content: `是否确认驳回?`,
positiveText: `确认驳回`,
positiveButtonProps: {
type: 'primary'
},
negativeText: '取消',
onPositiveClick: async () => {
startBackBtnLoading();
startBackFormLoading();
const { error } = await fetchBackTask(backModel);
endBackBtnLoading();
endBackFormLoading();
if (error) return;
window.$message?.success('驳回成功');
closeDrawer();
emit('submit');
}
});
}
function closeDrawer() {
visible.value = false;
}
watch(visible, () => {
if (visible.value) {
initDefault();
}
});
</script>
<template>
<NModal v-model:show="visible" preset="card" class="w-800px" :title="title" :native-scrollbar="false" closable>
<NSpin :show="backFormLoading">
<NForm v-if="task.flowStatus === 'waiting'" :model="backModel">
<NFormItem label="驳回节点" path="nodeCode">
<NSelect v-model:value="backModel.nodeCode" :options="backTaskNodeOptions" />
</NFormItem>
<NFormItem label="通知方式" path="messageType">
<NCheckboxGroup v-model:value="backModel.messageType">
<NSpace item-style="display: flex;">
<NCheckbox
v-for="item in messageTypeOptions"
:key="item.value"
:disabled="item.value === '1'"
:value="item.value"
:label="item.label"
/>
</NSpace>
</NCheckboxGroup>
</NFormItem>
<NFormItem label="附件" path="fileId">
<FileUpload v-model:file-list="fileList" :file-size="20" :max="20" upload-type="file" :accept="accept" />
</NFormItem>
<NFormItem label="审批意见" path="message">
<NInput v-model:value="backModel.message" type="textarea" />
</NFormItem>
</NForm>
</NSpin>
<template #footer>
<NSpace justify="end" :size="16">
<NButton @click="closeDrawer">{{ $t('common.cancel') }}</NButton>
<NButton :loading="backBtnLoading" type="primary" @click="handleSubmit">{{ $t('common.confirm') }}</NButton>
</NSpace>
</template>
</NModal>
</template>

View File

@ -0,0 +1,57 @@
<script setup lang="tsx">
import { computed, useAttrs } from 'vue';
import type { TreeSelectProps } from 'naive-ui';
import { useLoading } from '@sa/hooks';
import { fetchGetCategoryTree } from '@/service/api/workflow';
import { isNull } from '@/utils/common';
defineOptions({ name: 'FlowCategorySelect' });
interface Props {
[key: string]: any;
}
defineProps<Props>();
const rawValue = defineModel<CommonType.IdType | null>('value', { required: false });
const options = defineModel<Api.Common.CommonTreeRecord>('options', { required: false, default: [] });
const attrs: TreeSelectProps = useAttrs();
const { loading, startLoading, endLoading } = useLoading();
/** 转换为strid可能是number类型或者String类型导致回显失败 */
const strValue = computed({
get() {
return isNull(rawValue.value) ? null : rawValue.value?.toString();
},
set(val) {
rawValue.value = val;
}
});
async function getCategoryList() {
startLoading();
const { error, data } = await fetchGetCategoryTree();
if (error) return;
options.value = data;
endLoading();
}
getCategoryList();
</script>
<template>
<NTreeSelect
v-model:value="strValue"
filterable
class="h-full"
:loading="loading"
key-field="id"
label-field="label"
:options="options as []"
:default-expanded-keys="[0]"
v-bind="attrs"
/>
</template>
<style scoped></style>

View File

@ -0,0 +1,96 @@
<script setup lang="ts">
import { computed } from 'vue';
import { $t } from '@/locales';
interface Props {
/** 抽屉是否可见 */
visible?: boolean;
/** 抽屉标题 */
title: string;
/** 是否显示加载状态 */
loading?: boolean;
/** 抽屉宽度 */
width?: number;
operateType: CommonType.WorkflowTableOperateType;
status?: string | null;
}
const props = withDefaults(defineProps<Props>(), {
visible: false,
loading: false,
width: 1200,
status: null
});
interface Emits {
(e: 'update:visible', visible: boolean): void;
(e: 'close'): void;
(e: 'saveDraft'): void;
(e: 'submit'): void;
(e: 'approval'): void;
}
const emit = defineEmits<Emits>();
const visibleValue = computed({
get: () => props.visible,
set: value => {
emit('update:visible', value);
}
});
const showSubmit = computed(
() =>
props.operateType === 'add' ||
(props.operateType === 'edit' &&
props.status &&
(props.status === 'draft' || props.status === 'cancel' || props.status === 'back'))
);
const showApproval = computed(() => props.operateType === 'approval' && props.status && props.status === 'waiting');
function handleClose() {
emit('close');
}
function handleSaveDraft() {
emit('saveDraft');
}
function handleSubmit() {
emit('submit');
}
function handleApproval() {
emit('approval');
}
defineExpose({
handleClose,
handleSaveDraft,
handleSubmit,
handleApproval
});
</script>
<template>
<NDrawer v-model:show="visibleValue" :title="title" display-directive="show" :width="width" class="max-w-90%">
<NDrawerContent :title="title" :native-scrollbar="false" closable @close="handleClose">
<NSpin :show="loading">
<slot></slot>
</NSpin>
<template #footer>
<slot name="footer">
<div>
<NSpace :size="16">
<NButton v-if="showSubmit || showApproval" @click="handleClose">{{ $t('common.cancel') }}</NButton>
<NButton v-if="showSubmit" type="warning" @click="handleSaveDraft">暂存</NButton>
<NButton v-if="showSubmit" type="primary" @click="handleSubmit">{{ $t('common.confirm') }}</NButton>
<NButton v-if="showApproval" type="primary" @click="handleApproval">办理</NButton>
</NSpace>
</div>
</slot>
</template>
</NDrawerContent>
</NDrawer>
</template>
<style scoped></style>

View File

@ -0,0 +1,204 @@
<script lang="ts" setup>
import { computed, reactive, ref, watch } from 'vue';
import { useBoolean, useLoading } from '@sa/hooks';
import { fetchGetTask, fetchTaskOperate, fetchTerminateTask } from '@/service/api/workflow/task';
import ReduceSignatureModal from './reduce-signature-modal.vue';
defineOptions({
name: 'FlowInterveneModal'
});
const { loading, startLoading, endLoading } = useLoading();
const { bool: addSignatureVisible, setTrue: openAddSignatureModal } = useBoolean();
const { bool: transferVisible, setTrue: openTransferModal } = useBoolean();
const { bool: reduceSignatureVisible, setTrue: openReduceSignatureModal } = useBoolean();
interface Props {
taskId: CommonType.IdType;
assigneeIds: CommonType.IdType[];
assigneeNames: string[];
}
const props = defineProps<Props>();
interface Emits {
(e: 'refresh'): void;
}
const emit = defineEmits<Emits>();
const taskInfo = ref<Api.Workflow.Task>();
const isWaiting = computed(() => taskInfo.value?.flowStatus === 'waiting');
// 流程签署比例值 大于0为票签会签
const isTicketOrSignInstance = computed(() => Number(taskInfo.value?.nodeRatio) > 0);
const visible = defineModel<boolean>('visible', {
default: false
});
type Model = Api.Workflow.TaskOperateParams;
const model: Model = reactive(createDefaultModel());
function createDefaultModel(): Model {
return {
taskId: null,
userId: undefined,
userIds: undefined,
message: ''
};
}
type TerminateModel = Api.Workflow.TerminateTaskOperateParams;
const terminateModel: TerminateModel = reactive(createDefaultTerminateModel());
function createDefaultTerminateModel(): TerminateModel {
return {
taskId: null,
comment: ''
};
}
function handleTransferConfirm(ids: CommonType.IdType[]) {
model.userId = ids[0];
model.taskId = props.taskId;
window.$dialog?.warning({
title: '提示',
content: '是否确认转办?',
positiveText: '确认转办',
positiveButtonProps: {
type: 'primary'
},
negativeText: '取消',
onPositiveClick: async () => {
const { error } = await fetchTaskOperate(model, 'transferTask');
if (error) return;
window.$message?.success('转办成功');
visible.value = false;
emit('refresh');
}
});
}
function handleAddSignatureConfirm(ids: CommonType.IdType[]) {
model.userIds = ids;
window.$dialog?.warning({
title: '提示',
content: '是否确认加签?',
positiveText: '确认加签',
positiveButtonProps: {
type: 'primary'
},
negativeText: '取消',
onPositiveClick: async () => {
const { error } = await fetchTaskOperate(model, 'addSignature');
if (error) return;
window.$message?.success('加签成功');
visible.value = false;
emit('refresh');
}
});
}
function handleTerminate() {
terminateModel.taskId = props.taskId;
window.$dialog?.warning({
title: '提示',
content: '是否确认终止?',
positiveText: '确认',
positiveButtonProps: {
type: 'primary'
},
negativeText: '取消',
onPositiveClick: async () => {
const { error } = await fetchTerminateTask(terminateModel);
if (error) return;
window.$message?.success('终止成功');
visible.value = false;
emit('refresh');
}
});
}
function handleReduceSubmit() {
visible.value = false;
emit('refresh');
}
async function getTaskInfo() {
startLoading();
const { error, data } = await fetchGetTask(props.taskId);
if (error) return;
taskInfo.value = data;
endLoading();
}
watch(visible, () => {
if (visible.value) {
getTaskInfo();
}
});
</script>
<template>
<NModal
v-model:show="visible"
class="max-h-520px max-w-90% w-700px"
title="流程干预"
preset="card"
size="medium"
:native-scrollbar="false"
>
<NSpin :show="loading">
<NDescriptions :title="taskInfo?.flowName" label-placement="left" :column="2" size="small" bordered>
<NDescriptionsItem label="任务名称">
{{ taskInfo?.nodeName }}
</NDescriptionsItem>
<NDescriptionsItem label="节点编码">
{{ taskInfo?.nodeCode }}
</NDescriptionsItem>
<NDescriptionsItem label="开始时间">
{{ taskInfo?.createTime }}
</NDescriptionsItem>
<NDescriptionsItem label="流程实例ID">
{{ taskInfo?.instanceId }}
</NDescriptionsItem>
<NDescriptionsItem label="办理人">
<TagGroup :value="assigneeNames" />
</NDescriptionsItem>
<NDescriptionsItem label="版本号">
<NTag type="info" size="small">v{{ taskInfo?.version }}.0</NTag>
</NDescriptionsItem>
<NDescriptionsItem label="业务ID">
{{ taskInfo?.businessId }}
</NDescriptionsItem>
</NDescriptions>
</NSpin>
<template #footer>
<NSpace justify="end" :size="16">
<NButton v-if="isWaiting" type="primary" @click="openTransferModal">转办</NButton>
<NButton v-if="isWaiting && isTicketOrSignInstance" type="primary" @click="openAddSignatureModal">加签</NButton>
<NButton v-if="isWaiting && isTicketOrSignInstance" type="primary" @click="openReduceSignatureModal">
减签
</NButton>
<NButton v-if="isWaiting" type="error" @click="handleTerminate">终止</NButton>
</NSpace>
</template>
<!-- 转办用户选择器 -->
<UserSelectModal v-model:visible="transferVisible" :disabled-ids="assigneeIds" @confirm="handleTransferConfirm" />
<!-- 加签用户选择器 -->
<UserSelectModal
v-model:visible="addSignatureVisible"
multiple
:disabled-ids="assigneeIds"
@confirm="handleAddSignatureConfirm"
/>
<!-- 减签用户 -->
<ReduceSignatureModal
v-model:visible="reduceSignatureVisible"
:task="taskInfo!"
@reduce-submit="handleReduceSubmit"
/>
</NModal>
</template>

View File

@ -0,0 +1,30 @@
<script setup lang="ts">
import { stringify } from 'qs';
import { getToken } from '@/store/modules/auth/shared';
import { getServiceBaseURL } from '@/utils/service';
defineOptions({
name: 'FlowPreview'
});
interface Props {
instanceId: CommonType.IdType;
}
const props = defineProps<Props>();
const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y';
const { baseURL } = getServiceBaseURL(import.meta.env, isHttpProxy);
const urlParams = {
type: 'FlowChart',
id: props.instanceId,
Authorization: `Bearer ${getToken()}`,
clientid: import.meta.env.VITE_APP_CLIENT_ID || ''
};
const iframeUrl = `${baseURL}/warm-flow-ui/index.html?${stringify(urlParams)}`;
</script>
<template>
<div>
<iframe :src="iframeUrl" class="h-[450px] w-full" />
</div>
</template>

View File

@ -0,0 +1,496 @@
<script setup lang="ts">
import { computed, reactive, ref, watch } from 'vue';
import type { UploadFileInfo } from 'naive-ui';
import { useBoolean, useLoading } from '@sa/hooks';
import { messageTypeOptions } from '@/constants/workflow';
import {
fetchCompleteTask,
fetchGetTask,
fetchGetkNextNode,
fetchTaskOperate,
fetchTerminateTask
} from '@/service/api/workflow';
import FileUpload from '@/components/custom/file-upload.vue';
import ReduceSignatureModal from './reduce-signature-modal.vue';
import BackTaskModal from './back-task-modal.vue';
defineOptions({
name: 'FlowTaskApprovalModal'
});
interface Props {
/** 任务id */
taskId: CommonType.IdType;
/** 任务变量 */
taskVariables: { [key: string]: any };
}
const props = defineProps<Props>();
interface Emits {
(e: 'finished'): void;
}
const emit = defineEmits<Emits>();
const visible = defineModel<boolean>('visible', {
default: false
});
const { loading: baseFormLoading, startLoading: startBaseFormLoading, endLoading: endBaseFormLoading } = useLoading();
const { loading: btnLoading, startLoading: startBtnLoading, endLoading: endBtnLoading } = useLoading();
const { bool: copyVisible, setTrue: openCopyModal } = useBoolean();
const { bool: assigneeVisible, setTrue: openAssigneeModal } = useBoolean();
const { bool: delegateVisible, setTrue: openDelegateModal, setFalse: closeDelegateModal } = useBoolean();
const { bool: transferVisible, setTrue: openTransferModal, setFalse: closeTransferModal } = useBoolean();
const { bool: addSignatureVisible, setTrue: openAddSignatureModal, setFalse: closeAddSignatureModal } = useBoolean();
const { bool: reduceSignatureVisible, setTrue: openReduceSignatureModal } = useBoolean();
const { bool: backVisible, setTrue: openBackModal } = useBoolean();
const title = defineModel<string>('title', {
default: '流程发起'
});
const accept = ref<string>('.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.pdf,.jpg,.jpeg,.png,.gif,.bmp,.webp');
type Model = Api.Workflow.CompleteTaskOperateParams;
const task = ref<Api.Workflow.Task>();
const isWaiting = computed(() => task.value?.flowStatus === 'waiting');
const model: Model = reactive(createDefaultModel());
function createDefaultModel(): Model {
return {
taskId: null,
fileId: null,
flowCopyList: [],
messageType: ['1'],
taskVariables: null,
variables: null,
assigneeMap: null
};
}
const fileList = ref<UploadFileInfo[]>([]);
// 抄送人
const selectCopyUserList = ref<Api.System.User[]>([]);
// 抄送人id
const selectCopyUserIds = ref<CommonType.IdType[]>([]);
// 下一节点列表
const nestNodeList = ref<Api.Workflow.FlowNode[]>([]);
const nickNameMap = ref<{ [key: string]: string }>({});
const assigneeSearchUserIds = ref<string | null>(null);
const selectAssigneeIds = ref<string[]>([]);
// 节点编码
const nodeCode = ref<string>('');
// 按钮权限
interface ButtonPerm {
pop: boolean;
trust: boolean;
transfer: boolean;
addSign: boolean;
subSign: boolean;
termination: boolean;
back: boolean;
copy: boolean;
}
const buttonPerm = reactive<ButtonPerm>(createDefaultButtonPerm());
function createDefaultButtonPerm(): ButtonPerm {
return {
pop: false,
trust: false,
transfer: false,
addSign: false,
subSign: false,
termination: false,
back: false,
copy: false
};
}
function initDefault() {
selectCopyUserList.value = [];
selectCopyUserIds.value = [];
nickNameMap.value = {};
assigneeSearchUserIds.value = null;
selectAssigneeIds.value = [];
nodeCode.value = '';
Object.assign(model, createDefaultModel());
Object.assign(buttonPerm, createDefaultButtonPerm());
}
async function getTask() {
startBtnLoading();
startBaseFormLoading();
const { error, data } = await fetchGetTask(props.taskId);
if (error) {
endBtnLoading();
endBaseFormLoading();
return;
}
task.value = data;
task.value.buttonList.forEach(item => {
buttonPerm[item.code as keyof ButtonPerm] = item.show!;
});
endBtnLoading();
const { error: nextNodeError, data: nextNodeData } = await fetchGetkNextNode({
taskId: props.taskId,
taskVariables: props.taskVariables
});
endBaseFormLoading();
if (nextNodeError) {
return;
}
nestNodeList.value = nextNodeData;
}
async function handleSubmit() {
if (buttonPerm.pop && nestNodeList.value?.length) {
const hasEmptyAssignee = nestNodeList.value.some(e => !model.assigneeMap || !model.assigneeMap[e.nodeCode]);
if (hasEmptyAssignee) {
window.$message?.error('请选择审批人!');
return;
}
} else {
model.assigneeMap = {};
}
if (selectCopyUserList.value?.length) {
model.flowCopyList = selectCopyUserList.value.map(e => ({
userId: e.userId,
userName: e.nickName
}));
}
if (fileList.value?.length) {
const fileIds = fileList.value.map(item => item.id);
model.fileId = fileIds.join(',');
}
model.taskId = props.taskId;
model.variables = props.taskVariables;
startBtnLoading();
startBaseFormLoading();
try {
const { error } = await fetchCompleteTask(model);
if (error) return;
window.$message?.success('提交成功');
visible.value = false;
emit('finished');
} catch (error) {
window.$message?.error(`提交失败,请稍后重试,${error}`);
} finally {
endBtnLoading();
endBaseFormLoading();
}
}
function handleCopyConfirm(userIds: CommonType.IdType[], users?: Api.System.User[]) {
selectCopyUserList.value = users || [];
selectCopyUserIds.value = userIds;
}
function handleAssigneeOpen(item: Api.Workflow.FlowNode) {
if (!item.permissionFlag) {
window.$message?.error('没有可选人员,请联系管理员!');
return;
}
assigneeSearchUserIds.value = item.permissionFlag;
nodeCode.value = item.nodeCode;
selectAssigneeIds.value = model.assigneeMap?.[item.nodeCode]?.split(',') || [];
openAssigneeModal();
}
function handleAssigneeConfirm(userIds: CommonType.IdType[], users?: Api.System.User[]) {
// 更新当前节点的审批人
if (!model.assigneeMap) model.assigneeMap = {};
model.assigneeMap[nodeCode.value] = userIds.join(',');
nickNameMap.value[nodeCode.value] = users?.map(item => item.nickName).join(',') || '';
}
function handleCopyTagClose(index?: number) {
if (index !== undefined) {
// 删除指定索引的用户
selectCopyUserIds.value = selectCopyUserIds.value.filter((_, i) => i !== index);
selectCopyUserList.value = selectCopyUserList.value.filter((_, i) => i !== index);
} else {
// 清空所有用户
selectCopyUserList.value = [];
selectCopyUserIds.value = [];
model.flowCopyList = [];
}
}
function handleAssigneeTagClose(code: string, index?: number) {
if (!model.assigneeMap?.[code]) return;
// 获取当前节点的用户ID列表和名称列表
const userIds = model.assigneeMap[code].split(',');
const nickNames = nickNameMap.value[code]?.split(',') || [];
if (index !== undefined) {
// 删除指定索引的用户
// 使用filter方式移除指定索引的元素
const newUserIds = userIds.filter((_, i) => i !== index);
const newNickNames = nickNames.filter((_, i) => i !== index);
// 更新数据
model.assigneeMap[code] = newUserIds.join(',');
nickNameMap.value[code] = newNickNames.join(',');
} else {
// 清空所有用户
model.assigneeMap[code] = '';
nickNameMap.value[code] = '';
}
}
interface TaskOperationOptions {
userIds: CommonType.IdType[];
operation: 'delegateTask' | 'transferTask' | 'addSignature';
confirmText: string;
successMessage: string;
closeModal: () => void;
}
function handleTaskOperationConfirm(options: TaskOperationOptions) {
const { userIds, operation, confirmText, successMessage, closeModal } = options;
const taskModel = {
taskId: props.taskId,
userId: userIds[0],
message: model.message
};
window.$dialog?.warning({
title: '提示',
content: `是否确认${confirmText}?`,
positiveText: `确认${confirmText}`,
positiveButtonProps: {
type: 'primary'
},
negativeText: '取消',
onPositiveClick: async () => {
startBtnLoading();
startBaseFormLoading();
const { error } = await fetchTaskOperate(taskModel, operation);
endBtnLoading();
endBaseFormLoading();
if (error) return;
window.$message?.success(successMessage);
closeModal();
visible.value = false;
emit('finished');
}
});
}
// 委托
function handleDelegateConfirm(userIds: CommonType.IdType[]) {
handleTaskOperationConfirm({
userIds,
operation: 'delegateTask',
confirmText: '委托',
successMessage: '委托成功',
closeModal: closeDelegateModal
});
}
// 转办
function handleTransferConfirm(userIds: CommonType.IdType[]) {
handleTaskOperationConfirm({
userIds,
operation: 'transferTask',
confirmText: '转办',
successMessage: '转办成功',
closeModal: closeTransferModal
});
}
// 加签
function handleAddSignatureConfirm(userIds: CommonType.IdType[]) {
handleTaskOperationConfirm({
userIds,
operation: 'addSignature',
confirmText: '加签',
successMessage: '加签成功',
closeModal: closeAddSignatureModal
});
}
// 减签
function handleReduceSubmit() {
visible.value = false;
emit('finished');
}
// 终止
function handleTerminate() {
const terminateModel = {
taskId: props.taskId,
comment: model.message
};
window.$dialog?.warning({
title: '提示',
content: '是否确认终止?',
positiveText: '确认',
positiveButtonProps: {
type: 'primary'
},
negativeText: '取消',
onPositiveClick: async () => {
startBtnLoading();
startBaseFormLoading();
const { error } = await fetchTerminateTask(terminateModel);
endBtnLoading();
endBaseFormLoading();
if (error) return;
window.$message?.success('终止成功');
visible.value = false;
emit('finished');
}
});
}
function handleBackSubmit() {
visible.value = false;
emit('finished');
}
watch(visible, () => {
if (visible.value) {
initDefault();
getTask();
}
});
</script>
<template>
<NModal v-model:show="visible" preset="card" class="w-800px" :title="title" :native-scrollbar="false" closable>
<NSpin :show="baseFormLoading">
<NForm :model="model" label-placement="left" :label-width="100">
<NFormItem label="通知方式" path="messageType">
<NCheckboxGroup v-model:value="model.messageType">
<NSpace item-style="display: flex;">
<NCheckbox
v-for="item in messageTypeOptions"
:key="item.value"
:disabled="item.value === '1'"
:value="item.value"
:label="item.label"
/>
</NSpace>
</NCheckboxGroup>
</NFormItem>
<NFormItem label="附件" path="fileId">
<FileUpload v-model:file-list="fileList" :file-size="20" :max="20" upload-type="file" :accept="accept" />
</NFormItem>
<NFormItem v-if="buttonPerm.copy" label="抄送人员">
<NSpace>
<NButton ghost type="primary" @click="openCopyModal">选择抄送人员</NButton>
<TagGroup
size="large"
:value="selectCopyUserList.map(item => item.nickName)"
:closable="true"
@close="handleCopyTagClose"
/>
</NSpace>
</NFormItem>
<NFormItem
v-if="buttonPerm.pop && nestNodeList && nestNodeList.length > 0"
label="下一步审批人"
path="assigneeMap"
>
<NSpace>
<div v-for="(item, index) in nestNodeList" :key="index">
<span>{{ item.nodeName }}</span>
<NSpace>
<NButton ghost type="primary" @click="handleAssigneeOpen(item)">选择审批人员</NButton>
<NInput v-if="false" v-model:value="model.assigneeMap![item.nodeCode]" />
<TagGroup
size="large"
:value="nickNameMap[item.nodeCode]"
:closable="true"
@close="index => handleAssigneeTagClose(item.nodeCode, index)"
/>
</NSpace>
</div>
</NSpace>
</NFormItem>
<NFormItem v-if="isWaiting" label="审批意见" path="message">
<NInput v-model:value="model.message" type="textarea" />
</NFormItem>
</NForm>
</NSpin>
<template #footer>
<NSpace justify="end" :size="16">
<NButton @click="visible = false">{{ $t('common.cancel') }}</NButton>
<!-- 委托 -->
<NButton v-if="isWaiting && buttonPerm.trust" :loading="btnLoading" type="warning" @click="openDelegateModal">
委托
</NButton>
<!-- 转办 -->
<NButton
v-if="isWaiting && buttonPerm.transfer"
:loading="btnLoading"
type="warning"
@click="openTransferModal"
>
转办
</NButton>
<!-- 加签 -->
<NButton
v-if="isWaiting && buttonPerm.addSign && Number(task?.nodeRatio) > 0"
:loading="btnLoading"
type="warning"
@click="openAddSignatureModal"
>
加签
</NButton>
<!-- 减签 -->
<NButton
v-if="isWaiting && buttonPerm.subSign && Number(task?.nodeRatio) > 0"
:loading="btnLoading"
type="warning"
@click="openReduceSignatureModal"
>
减签
</NButton>
<!-- 终止 -->
<NButton v-if="isWaiting && buttonPerm.termination" :loading="btnLoading" type="error" @click="handleTerminate">
终止
</NButton>
<!-- 驳回 -->
<NButton v-if="isWaiting && buttonPerm.back" :loading="btnLoading" type="error" @click="openBackModal">
驳回
</NButton>
<NButton :loading="btnLoading" type="primary" @click="handleSubmit">提交</NButton>
</NSpace>
</template>
<!-- 抄送人员选择 -->
<UserSelectModal
v-model:visible="copyVisible"
:row-keys="selectCopyUserIds"
multiple
@confirm="handleCopyConfirm"
/>
<!-- 下一步审批人员选择 -->
<UserSelectModal
v-model:visible="assigneeVisible"
:row-keys="selectAssigneeIds"
:search-user-ids="assigneeSearchUserIds"
multiple
@confirm="handleAssigneeConfirm"
/>
<!-- 转办 -->
<UserSelectModal v-model:visible="transferVisible" @confirm="handleTransferConfirm" />
<!-- 委托 -->
<UserSelectModal v-model:visible="delegateVisible" @confirm="handleDelegateConfirm" />
<!-- 加签 -->
<UserSelectModal v-model:visible="addSignatureVisible" @confirm="handleAddSignatureConfirm" />
<!-- 减签 -->
<ReduceSignatureModal v-model:visible="reduceSignatureVisible" :task="task!" @reduce-submit="handleReduceSubmit" />
<!-- 驳回 -->
<BackTaskModal v-model:visible="backVisible" :task="task!" @submit="handleBackSubmit" />
</NModal>
</template>

View File

@ -0,0 +1,312 @@
<script setup lang="ts">
import { computed, reactive, ref, watch } from 'vue';
import dayjs from 'dayjs';
import { useBoolean, useLoading } from '@sa/hooks';
import { flowCodeTypeOptions, flowCodeTypeRecord, leaveTypeOptions, leaveTypeRecord } from '@/constants/workflow';
import { fetchCreateLeave, fetchGetLeaveDetail, fetchStartWorkflow, fetchUpdateLeave } from '@/service/api/workflow';
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import { useDict } from '@/hooks/business/dict';
import { $t } from '@/locales';
import ApprovalInfoPanel from '@/components/workflow/approval-info-panel.vue';
import FlowTaskApprovalModal from '@/components/workflow/flow-task-approval-modal.vue';
import FlowDrawer from '@/components/workflow/flow-drawer.vue';
defineOptions({
name: 'LeaveEdit'
});
useDict('wf_task_status');
interface Props {
operateType: CommonType.WorkflowTableOperateType;
/** 业务ID */
businessId?: CommonType.IdType;
taskId?: CommonType.IdType;
/** the edit row data */
rowData?: Api.Workflow.Leave | null;
}
const props = withDefaults(defineProps<Props>(), {
rowData: null,
businessId: undefined,
taskId: undefined
});
interface Emits {
(e: 'submitted'): void;
}
const emit = defineEmits<Emits>();
const visible = defineModel<boolean>('visible', {
default: false
});
const approvalInfoPanelRef = ref<InstanceType<typeof ApprovalInfoPanel>>();
const { bool: taskApplyVisible, setTrue: setTaskApplyVisible } = useBoolean();
const { loading, startLoading, endLoading } = useLoading();
const { formRef, validate, restoreValidation } = useNaiveForm();
const { createRequiredRule } = useFormRules();
const title = computed(() => {
const titles: Record<CommonType.WorkflowTableOperateType, string> = {
add: '新增请假申请',
edit: '编辑请假申请',
detail: '查看请假申请',
approval: '审批请假申请'
};
return titles[props.operateType];
});
const readonly = computed(() => {
return props.operateType === 'detail' || props.operateType === 'approval';
});
const taskId = ref<CommonType.IdType>(props.taskId!);
const respLeave = ref<Api.Workflow.Leave>();
const startWorkflowResult = ref<Api.Workflow.StartWorkflowResult>();
type Model = Api.Workflow.LeaveOperateParams & {
flowCode: Api.Workflow.FlowCodeType;
};
const model: Model = reactive(createDefaultModel());
function createDefaultModel(): Model {
return {
flowCode: 'leave1',
leaveType: null,
startDate: null,
endDate: null,
leaveDays: null,
remark: ''
};
}
type ModelDetail = Api.Workflow.LeaveDetail & {
flowCode: Api.Workflow.FlowCodeType;
};
const modelDetail: ModelDetail = reactive(createDefaultModelDetail());
function createDefaultModelDetail(): ModelDetail {
return {
flowCode: 'leave1',
status: null,
id: null,
leaveType: null,
startDate: null,
endDate: null,
leaveDays: null,
remark: ''
};
}
const showApprovalInfoPanel = computed(() => {
return modelDetail.status !== 'draft';
});
type StartWorkflowModel = Api.Workflow.StartWorkflowOperateParams;
const startWorkflowModel: StartWorkflowModel = reactive(createDefaultStartWorkflowModel());
function createDefaultStartWorkflowModel(): StartWorkflowModel {
return {
flowCode: null,
businessId: null,
variables: {}
};
}
const dateRange = computed<[string, string] | null>({
get: () => {
if (!model.startDate || !model.endDate) return null;
return [model.startDate, model.endDate] as [string, string];
},
set: (value: [string, string] | null) => {
if (value) {
model.startDate = value[0];
model.endDate = value[1];
// 计算请假天数
const start = dayjs(value[0]);
const end = dayjs(value[1]);
model.leaveDays = end.diff(start, 'day') + 1;
} else {
model.startDate = null;
model.endDate = null;
model.leaveDays = null;
}
}
});
type RuleKey = Extract<keyof Model, 'id' | 'leaveType' | 'leaveDays' | 'startDate' | 'endDate'> | 'flowCode';
const rules: Record<RuleKey, App.Global.FormRule> = {
id: createRequiredRule('id不能为空'),
flowCode: createRequiredRule('流程类型不能为空'),
leaveType: createRequiredRule('请假类型不能为空'),
startDate: createRequiredRule('请假时间不能为空'),
endDate: createRequiredRule('结束时间不能为空'),
leaveDays: createRequiredRule('请假天数不能为空')
};
async function handleUpdateModelWhenEdit() {
if (props.operateType === 'add') {
Object.assign(model, createDefaultModel());
return;
}
if (props.rowData) {
Object.assign(model, props.rowData);
Object.assign(modelDetail, props.rowData);
} else {
const { error, data } = await fetchGetLeaveDetail(props.businessId!);
if (error) {
window.$message?.error(error.message);
return;
}
Object.assign(model, data);
Object.assign(modelDetail, data);
}
}
function closeDrawer() {
visible.value = false;
}
async function handleOperate() {
await validate();
// request
if (props.operateType === 'add') {
const { leaveType, startDate, endDate, leaveDays, remark } = model;
const { error, data } = await fetchCreateLeave({ leaveType, startDate, endDate, leaveDays, remark });
if (error) return;
respLeave.value = data;
}
if (props.operateType === 'edit') {
const { id, leaveType, startDate, endDate, leaveDays, remark } = model;
const { error, data } = await fetchUpdateLeave({ id, leaveType, startDate, endDate, leaveDays, remark });
if (error) return;
respLeave.value = data;
}
}
async function handleSaveDraft() {
await handleOperate();
window.$message?.success($t('common.updateSuccess'));
closeDrawer();
emit('submitted');
}
const taskVariables = ref<{ [key: string]: any }>({});
async function handleSubmit() {
await handleOperate();
window.$message?.success($t('common.updateSuccess'));
// 提交流程
startWorkflowModel.businessId = respLeave.value?.id;
startWorkflowModel.flowCode = model.flowCode;
taskVariables.value = {
leaveDays: respLeave.value?.leaveDays,
userList: ['1', '3', '4']
};
startWorkflowModel.variables = taskVariables.value;
const { error, data } = await fetchStartWorkflow(startWorkflowModel);
if (error) return;
startWorkflowResult.value = data;
taskId.value = data.taskId!;
setTaskApplyVisible();
}
function handleTaskFinished() {
closeDrawer();
emit('submitted');
}
function handleApproval() {
setTaskApplyVisible();
}
async function initializeData() {
if (visible.value) {
startLoading();
await handleUpdateModelWhenEdit();
restoreValidation();
if (showApprovalInfoPanel.value) {
approvalInfoPanelRef.value?.initData();
}
endLoading();
}
}
watch(visible, initializeData, { immediate: true });
</script>
<template>
<FlowDrawer
v-model:visible="visible"
:title="title"
:loading="loading"
:operate-type="operateType"
:status="modelDetail.status"
@close="closeDrawer"
@save-draft="handleSaveDraft"
@submit="handleSubmit"
@approval="handleApproval"
>
<div :class="loading ? 'hidden' : ''">
<div v-if="!readonly" class="h-full">
<NForm ref="formRef" :model="model" :rules="rules">
<NFormItem label="流程类型" path="flowCode">
<NSelect v-model:value="model.flowCode" placeholder="请输入流程类型" :options="flowCodeTypeOptions" />
</NFormItem>
<NFormItem label="请假类型" path="leaveType">
<NSelect v-model:value="model.leaveType" placeholder="请输入请假类型" :options="leaveTypeOptions" />
</NFormItem>
<NFormItem label="请假时间" path="startDate">
<NDatePicker
v-model:formatted-value="dateRange"
class="w-full"
type="datetimerange"
format="yyyy-MM-dd HH:mm:ss"
clearable
/>
</NFormItem>
<NFormItem label="请假天数" path="leaveDays">
<NInputNumber v-model:value="model.leaveDays" class="w-full" disabled placeholder="请输入请假天数" />
</NFormItem>
<NFormItem label="请假原因" path="remark">
<NInput v-model:value="model.remark" placeholder="请输入请假原因" />
</NFormItem>
</NForm>
</div>
<div v-else>
<NDescriptions size="small" bordered :column="2" label-placement="left">
<NDescriptionsItem label="流程类型">
{{ flowCodeTypeRecord[modelDetail.flowCode] }}
</NDescriptionsItem>
<NDescriptionsItem label="请假类型">
<NTag type="info">{{ leaveTypeRecord[modelDetail.leaveType!] }}</NTag>
</NDescriptionsItem>
<NDescriptionsItem label="请假时间">
{{ `${modelDetail.startDate}${modelDetail.endDate}` }}
</NDescriptionsItem>
<NDescriptionsItem label="请假天数">{{ modelDetail.leaveDays }} </NDescriptionsItem>
<NDescriptionsItem label="请假原因">
{{ modelDetail.remark || '-' }}
</NDescriptionsItem>
</NDescriptions>
<!-- 审批信息 -->
<ApprovalInfoPanel v-if="showApprovalInfoPanel" ref="approvalInfoPanelRef" :business-id="modelDetail.id!" />
</div>
</div>
</FlowDrawer>
<FlowTaskApprovalModal
v-model:visible="taskApplyVisible"
:task-id="taskId"
:task-variables="taskVariables"
@finished="handleTaskFinished"
/>
</template>
<style scoped></style>

View File

@ -0,0 +1,154 @@
<script lang="tsx" setup>
import { reactive, ref, watch } from 'vue';
import { useLoading } from '@sa/hooks';
import { fetchGetCurrentTaskAllUser, fetchTaskOperate } from '@/service/api/workflow/task';
import { $t } from '@/locales';
import ButtonIcon from '@/components/custom/button-icon.vue';
defineOptions({
name: 'ReduceSignatureModal'
});
interface Props {
task: Api.Workflow.Task;
}
const props = defineProps<Props>();
const visible = defineModel<boolean>('visible', {
default: false
});
interface Emits {
(e: 'reduceSubmit'): void;
}
const emit = defineEmits<Emits>();
const { loading, startLoading, endLoading } = useLoading();
type UserTaskModel = Api.System.User & { nodeName: string };
const userData = ref<UserTaskModel[]>([]);
type Model = Api.Workflow.TaskOperateParams;
const model: Model = reactive(createDefaultModel());
const checkedRowKeys = ref<CommonType.IdType[]>([]);
function createDefaultModel(): Model {
return {
taskId: null,
userId: undefined,
userIds: undefined,
message: ''
};
}
const columns = ref<NaiveUI.TableColumn<UserTaskModel>[]>([
{
type: 'selection',
align: 'center',
width: 50
},
{
title: '节点名称',
key: 'nodeName',
align: 'center',
minWidth: 120
},
{
title: '办理人员',
key: 'nickName',
align: 'center',
minWidth: 120
},
{
key: 'operate',
title: $t('common.operate'),
align: 'center',
width: 130,
render(row) {
return (
<ButtonIcon
text
type="error"
icon="material-symbols:delete-outline"
tooltipContent={'减签'}
popconfirmContent={'是否确认减签?'}
onPositiveClick={() => handleReduceSignature([row.userId])}
/>
);
}
}
]);
async function handleReduceSignature(userIds: CommonType.IdType[]) {
model.taskId = props.task.id;
model.userIds = userIds;
const { error } = await fetchTaskOperate(model, 'reductionSignature');
if (error) return;
window.$message?.success('减签成功');
handleCloseDrawer();
}
async function getTaskAllUser() {
startLoading();
const { error, data } = await fetchGetCurrentTaskAllUser(props.task.id);
if (error) return;
userData.value = data.map(item => ({
...item,
nodeName: props.task.nodeName
}));
endLoading();
}
function handleCloseDrawer() {
visible.value = false;
emit('reduceSubmit');
}
watch(visible, async () => {
if (visible.value) {
await getTaskAllUser();
}
});
</script>
<template>
<NModal v-model:show="visible" class="w-700px" preset="card" title="待减签人员">
<NCard class="h-full card-wrapper">
<NSpace wrap justify="space-between" class="mb-16px lt-sm:w-200px">
<TableRowCheckAlert v-model:checked-row-keys="checkedRowKeys" />
<NButton
size="small"
ghost
type="error"
:disabled="checkedRowKeys.length === 0"
@click="handleReduceSignature(checkedRowKeys)"
>
<template #icon>
<icon-material-symbols:delete-outline class="text-icon" />
</template>
批量减签
</NButton>
</NSpace>
<NDataTable
v-model:checked-row-keys="checkedRowKeys"
class="h-400px"
flex-height
:row-key="row => row.userId"
size="small"
:columns="columns"
:data="userData"
:loading="loading"
/>
</NCard>
</NModal>
</template>
<style scoped lang="scss">
.n-alert {
--n-padding: 5px 13px !important;
--n-icon-margin: 6px 8px 0 12px !important;
--n-icon-size: 20px !important;
}
</style>

View File

@ -76,7 +76,7 @@ export const genHtmlTypeRecord: Record<Api.Tool.HtmlType, string> = {
select: '下拉框',
radio: '单选框',
checkbox: '复选框',
datetime: '日期控件',
datetime: '日期时间控件',
imageUpload: '图片上传',
fileUpload: '文件上传',
editor: '富文本控件'

82
src/constants/workflow.ts Normal file
View File

@ -0,0 +1,82 @@
import { transformRecordToOption } from '@/utils/common';
export const cooperateTypeRecord: Record<Api.Workflow.CooperateType, string> = {
1: '审批',
2: '转办',
3: '委派',
4: '会签',
5: '票签',
6: '加签',
7: '减签'
};
export const cooperateTypeOptions = transformRecordToOption(cooperateTypeRecord);
export const businessStatusRecord: Record<Api.Workflow.BusinessStatus, string> = {
cancel: '已撤销',
draft: '草稿',
waiting: '待审批',
finish: '已完成',
invalid: '已作废',
back: '已退回',
termination: '已终止'
};
export const businessStatusOptions = transformRecordToOption(businessStatusRecord);
export const messageTypeRecord: Record<Api.Workflow.MessageType, string> = {
'1': '站内信',
'2': '邮件',
'3': '短信'
};
export const messageTypeOptions = transformRecordToOption(messageTypeRecord);
export const flowCodeTypeRecord: Record<Api.Workflow.FlowCodeType, string> = {
leave1: '请假申请-普通',
leave2: '请假申请-排他网关',
leave3: '请假申请-并行网关',
leave4: '请假申请-会签',
leave5: '请假申请-并行会签网关',
leave6: '请假申请-排他并行会签'
};
export const flowCodeTypeOptions = transformRecordToOption(flowCodeTypeRecord);
/** leave type */
export const leaveTypeRecord: Record<Api.Workflow.LeaveType, string> = {
'1': '事假',
'2': '调休',
'3': '病假',
'4': '婚假'
};
export const leaveTypeOptions = transformRecordToOption(leaveTypeRecord);
/** workflow publish status */
export const workflowPublishStatusRecord: Record<Api.Workflow.WorkflowPublishStatus, string> = {
'0': '未发布',
'1': '已发布',
'9': '失效'
};
export const workflowPublishStatusOptions = transformRecordToOption(workflowPublishStatusRecord);
/** node type */
export const workflowNodeTypeRecord: Record<Api.Workflow.WorkflowNodeType, string> = {
0: '开始节点',
1: '中间节点',
2: '结束节点',
3: '互斥网关',
4: '并行网关'
};
export const workflowNodeTypeOptions = transformRecordToOption(workflowNodeTypeRecord);
/** activity status */
export const workflowActivityStatusRecord: Record<Api.Workflow.WorkflowActivityStatus, string> = {
0: '挂起',
1: '激活'
};
export const workflowActivityStatusOptions = transformRecordToOption(workflowActivityStatusRecord);

4
src/enum/business.ts Normal file
View File

@ -0,0 +1,4 @@
export enum AcceptType {
Image = '.jpg,.jpeg,.png,.gif,.bmp,.webp',
File = '.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.pdf,.zip,.rar,.7z'
}

View File

@ -3,6 +3,8 @@ import { storeToRefs } from 'pinia';
import { fetchGetDictDataByType } from '@/service/api/system';
import { useDictStore } from '@/store/modules/dict';
import { isNull } from '@/utils/common';
import { $t } from '@/locales';
export function useDict(dictType: string, immediate: boolean = true) {
const dictStore = useDictStore();
const { dictData: dictList } = storeToRefs(dictStore);
@ -19,6 +21,11 @@ export function useDict(dictType: string, immediate: boolean = true) {
}
const { data: dictData, error } = await fetchGetDictDataByType(dictType);
if (error) return;
dictData.forEach(dict => {
if (dict.dictLabel?.startsWith(`dict.${dictType}.`)) {
dict.dictLabel = $t(dict.dictLabel as App.I18n.I18nKey);
}
});
dictStore.setDict(dictType, dictData);
data.value = dictData;
}

View File

@ -1,114 +1,159 @@
import StreamSaver from 'streamsaver';
import { errorCodeRecord } from '@/constants/common';
import { localStg } from '@/utils/storage';
import { getServiceBaseURL } from '@/utils/service';
import { transformToURLSearchParams } from '@/utils/common';
interface RequestConfig {
method: 'GET' | 'POST';
url: string;
params?: Record<string, any>;
filename?: string;
contentType?: string;
}
export function useDownload() {
const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y';
const { baseURL } = getServiceBaseURL(import.meta.env, isHttpProxy);
function downloadByData(data: BlobPart, filename: string, type: string = 'application/octet-stream') {
const blobData = [data];
const blob = new Blob(blobData, { type });
const isHttps = () => {
const protocol = document.location.protocol;
const hostname = document.location.hostname;
return protocol === 'https' || hostname === 'localhost' || hostname === '127.0.0.1';
};
/** 获取通用请求头 */
const getCommonHeaders = (contentType = 'application/octet-stream') => ({
Authorization: `Bearer ${localStg.get('token')}`,
Clientid: import.meta.env.VITE_APP_CLIENT_ID!,
'Content-Type': contentType
});
/** 通用下载方法 */
function downloadByData(data: BlobPart, filename: string, type = 'application/octet-stream') {
const blob = new Blob([data], { type });
const blobURL = window.URL.createObjectURL(blob);
const tempLink = document.createElement('a');
tempLink.style.display = 'none';
tempLink.href = blobURL;
tempLink.setAttribute('download', filename);
const tempLink = Object.assign(document.createElement('a'), {
style: { display: 'none' },
href: blobURL,
download: filename
});
if (typeof tempLink.download === 'undefined') {
tempLink.setAttribute('target', '_blank');
}
document.body.appendChild(tempLink);
tempLink.click();
document.body.removeChild(tempLink);
window.URL.revokeObjectURL(blobURL);
}
function download(url: string, params: any, fileName: string) {
window.$loading?.startLoading('正在下载数据,请稍候...');
const token = localStg.get('token');
const clientId = import.meta.env.VITE_APP_CLIENT_ID;
const now = Date.now();
const searchParams = new FormData();
if (params) {
Object.keys(params).forEach(key => {
if (params[key] !== null && params[key] !== undefined) {
searchParams.append(key, params[key]);
}
});
/** 流式下载 */
async function downloadByStream(
readableStream: ReadableStream<Uint8Array>,
filename: string,
contentLength?: number
): Promise<void> {
window.$loading?.endLoading();
StreamSaver.mitm = '/streamsaver/mitm.html?version=2.0.0';
const fileStream = StreamSaver.createWriteStream(filename, { size: contentLength });
if (window.WritableStream && readableStream?.pipeTo) {
await readableStream.pipeTo(fileStream);
window.$message?.success('下载完成');
return;
}
fetch(`${baseURL}${url}?t=${now}`, {
method: 'post',
body: searchParams,
headers: {
Authorization: `Bearer ${token}`,
Clientid: clientId!,
'Content-Type': 'application/octet-stream'
}
})
.then(async response => {
if (response.headers.get('Content-Type')?.includes('application/json')) {
const res = await response.json();
const code = res.code as CommonType.ErrorCode;
throw new Error(errorCodeRecord[code] || res.msg || errorCodeRecord.default);
}
return response.blob();
})
.then(data => downloadByData(data, fileName, 'application/zip'))
.catch(err => window.$message?.error(err.message))
.finally(() => window.$loading?.endLoading());
// 降级处理
const writer = fileStream.getWriter();
const reader = readableStream.getReader();
const pump = async (): Promise<void> => {
const { done, value } = await reader.read();
if (done) return writer.close();
await writer.write(value);
return pump();
};
await pump();
}
function oss(ossId: CommonType.IdType) {
window.$loading?.startLoading('正在下载数据,请稍候...');
const token = localStg.get('token');
const clientId = import.meta.env.VITE_APP_CLIENT_ID;
const url = `/resource/oss/download/${ossId}`;
const now = Date.now();
let fileName = String(`${ossId}-${now}`);
fetch(`${baseURL}${url}?t=${now}`, {
method: 'get',
headers: {
Authorization: `Bearer ${token}`,
Clientid: clientId!,
'Content-Type': 'application/octet-stream'
}
})
.then(async response => {
fileName = String(response.headers.get('Download-Filename'));
return response.blob();
})
.then(data => downloadByData(data, fileName))
.catch(err => window.$message?.error(err.message))
.finally(() => window.$loading?.endLoading());
/** 处理响应 */
async function handleResponse(response: Response) {
if (response.headers.get('Content-Type')?.includes('application/json')) {
const res = await response.json();
const code = res.code as CommonType.ErrorCode;
throw new Error(errorCodeRecord[code] || res.msg || errorCodeRecord.default);
}
}
function zip(url: string, fileName: string) {
/** 核心下载逻辑 */
async function executeDownload(config: RequestConfig): Promise<void> {
const { method, url, params, filename, contentType } = config;
const timestamp = Date.now();
const fullUrl = `${baseURL}${url}${url.includes('?') ? '&' : '?'}t=${timestamp}`;
window.$loading?.startLoading('正在下载数据,请稍候...');
const token = localStg.get('token');
const clientId = import.meta.env.VITE_APP_CLIENT_ID;
const now = Date.now();
fetch(`${baseURL}${url}${url.includes('?') ? '&' : '?'}t=${now}`, {
method: 'get',
headers: {
Authorization: `Bearer ${token}`,
Clientid: clientId!,
'Content-Type': 'application/octet-stream'
try {
const requestOptions: RequestInit = {
method,
headers: getCommonHeaders(contentType)
};
if (method === 'POST' && params) {
requestOptions.body = transformToURLSearchParams(params);
requestOptions.headers = {
...requestOptions.headers,
'Content-Type': 'application/x-www-form-urlencoded'
};
}
})
.then(async response => {
if (response.headers.get('Content-Type')?.includes('application/json')) {
const res = await response.json();
const code = res.code as CommonType.ErrorCode;
throw new Error(errorCodeRecord[code] || res.msg || errorCodeRecord.default);
}
return response.blob();
})
.then(data => downloadByData(data, fileName, 'application/zip'))
.catch(err => window.$message?.error(err.message))
.finally(() => window.$loading?.endLoading());
const response = await fetch(fullUrl, requestOptions);
await handleResponse(response);
const rawHeader = response.headers.get('Download-Filename');
const finalFilename = filename || (rawHeader ? decodeURIComponent(rawHeader) : null) || `download-${timestamp}`;
if (response.body && isHttps()) {
const contentLength = Number(response.headers.get('Content-Length'));
await downloadByStream(response.body, finalFilename, contentLength);
return;
}
const responseContentType = response.headers.get('Content-Type');
const mainType = responseContentType?.split(';')[0]?.trim() || 'application/octet-stream';
downloadByData(await response.blob(), finalFilename, mainType);
} catch (error: any) {
window.$message?.error(error.message);
} finally {
window.$loading?.endLoading();
}
}
/** 公共下载接口 */
const download = (url: string, params: Record<string, any>, filename: string) =>
executeDownload({ method: 'POST', url, params, filename });
/** OSS文件下载 */
const oss = (ossId: CommonType.IdType) =>
executeDownload({
method: 'GET',
url: `/resource/oss/download/${ossId}`
});
/** ZIP文件下载 */
const zip = (url: string, filename: string) =>
executeDownload({
method: 'GET',
url,
filename,
contentType: 'application/octet-stream'
});
return {
oss,
zip,

View File

@ -2,6 +2,7 @@ import { ref, toValue } from 'vue';
import type { ComputedRef, Ref } from 'vue';
import type { FormInst } from 'naive-ui';
import { REG_CODE_SIX, REG_EMAIL, REG_PHONE, REG_PWD, REG_USER_NAME } from '@/constants/reg';
import { isNull } from '@/utils/common';
import { $t } from '@/locales';
export function useFormRules() {
@ -52,7 +53,7 @@ export function useFormRules() {
required: true,
trigger: ['input', 'blur'],
validator: (_rule: any, value: any) => {
if (value === null || value === undefined || value === '') {
if (isNull(value) || (Array.isArray(value) && value.length === 0)) {
return new Error(message);
}
return true;

View File

@ -32,7 +32,8 @@ export function useTable<A extends NaiveUI.TableApiFn>(config: NaiveUI.NaiveTabl
getData,
searchParams,
updateSearchParams,
resetSearchParams
resetSearchParams,
updateApiFn
} = useHookTable<A, GetTableData<A>, TableColumn<NaiveUI.TableDataWithIndex<GetTableData<A>>>>({
apiFn,
apiParams,
@ -212,7 +213,8 @@ export function useTable<A extends NaiveUI.TableApiFn>(config: NaiveUI.NaiveTabl
getDataByPage,
searchParams,
updateSearchParams,
resetSearchParams
resetSearchParams,
updateApiFn
};
}

View File

@ -39,7 +39,7 @@ const toGitee = () => {
<NCard
size="small"
:bordered="false"
class="w-340px"
class="w-345px"
header-class="p-0"
:segmented="{ content: true, footer: 'soft' }"
>
@ -63,7 +63,7 @@ const toGitee = () => {
一键已读
</NTooltip>
</template>
<div>
<NScrollbar class="h-260px">
<template v-if="state?.notices?.length">
<template v-for="(message, index) in state?.notices" :key="index">
<NDivider v-show="index !== 0" />
@ -81,7 +81,7 @@ const toGitee = () => {
</template>
</template>
<NEmpty v-else class="h-180px flex-center" />
</div>
</NScrollbar>
<template #footer>
<div class="flex items-center justify-end">
<NButton type="primary" size="small" @click="toGitee">前往 Gitee</NButton>

View File

@ -45,7 +45,7 @@ const tenantId = ref<CommonType.IdType>(authStore.userInfo?.user?.tenantId || '0
</div>
<div class="h-full flex-y-center justify-end">
<TenantSelect v-if="!appStore.isMobile" v-model:value="tenantId" class="mr-12px w-150px" />
<GlobalSearch v-if="themeStore.header.globalSearch.visible" />
<GlobalSearch v-if="themeStore.header.globalSearch.visible && !appStore.isMobile" />
<MessageButton />
<FullScreen v-if="!appStore.isMobile" :full="isFullscreen" @click="toggle" />
<LangSwitch
@ -59,7 +59,7 @@ const tenantId = ref<CommonType.IdType>(authStore.userInfo?.user?.tenantId || '0
:is-dark="themeStore.darkMode"
@switch="themeStore.toggleThemeScheme"
/>
<ThemeButton />
<ThemeButton v-if="!appStore.isMobile" />
<UserAvatar />
</div>
</DarkModeContainer>

View File

@ -17,8 +17,8 @@ withDefaults(defineProps<Props>(), {
<template>
<RouterLink to="/" class="w-full flex-center nowrap-hidden">
<SystemLogo class="w-32px text-primary" />
<h2 v-show="showTitle" class="pl-8px text-16px text-primary font-bold transition duration-300 ease-in-out">
<SystemLogo class="text-30px text-primary" />
<h2 v-show="showTitle" class="pl-12px text-16px text-primary font-bold transition duration-300 ease-in-out">
{{ $t('system.title') }}
</h2>
</RouterLink>

View File

@ -117,7 +117,10 @@ const isWrapperScrollMode = computed(() => themeStore.layout.scrollMode === 'wra
<SettingItem key="8" :label="$t('theme.watermark.visible')">
<NSwitch v-model:value="themeStore.watermark.visible" />
</SettingItem>
<SettingItem v-if="themeStore.watermark.visible" key="8-1" :label="$t('theme.watermark.text')">
<SettingItem v-if="themeStore.watermark.visible" key="8-1" :label="$t('theme.watermark.enableUserName')">
<NSwitch v-model:value="themeStore.watermark.enableUserName" />
</SettingItem>
<SettingItem v-if="themeStore.watermark.visible" key="8-2" :label="$t('theme.watermark.text')">
<NInput
v-model:value="themeStore.watermark.text"
autosize

View File

@ -1,6 +1,6 @@
const local: App.I18n.Schema = {
system: {
title: 'RuoYi Vue Plus',
title: 'RuoYi Plus Soybean',
updateTitle: 'System Version Update Notification',
updateContent: 'A new version of the system has been detected. Do you want to refresh the page immediately?',
updateConfirm: 'Refresh immediately',
@ -61,6 +61,7 @@ const local: App.I18n.Schema = {
update: 'Update',
saveSuccess: 'Save Success',
updateSuccess: 'Update Success',
noChange: 'No actions were taken',
userCenter: 'User Center',
yesOrNo: {
yes: 'Yes',
@ -166,7 +167,8 @@ const local: App.I18n.Schema = {
},
watermark: {
visible: 'Watermark Full Screen Visible',
text: 'Watermark Text'
text: 'Watermark Text',
enableUserName: 'Enable User Name Watermark'
},
tablePropsTitle: 'Table Props',
table: {
@ -229,10 +231,112 @@ const local: App.I18n.Schema = {
demo: 'Demo',
demo_demo: 'Demo Table',
demo_tree: 'Demo Tree',
workflow: 'Workflow',
workflow_category: 'Workflow Category',
exception: 'Exception',
exception_403: '403',
exception_404: '404',
exception_500: '500'
exception_500: '500',
workflow_design: 'Process Design',
workflow_spel: 'Process Spel',
'workflow_process-definition': 'Process Definition',
'workflow_process-instance': 'Process Instance',
workflow_task: 'Task',
'workflow_task_all-task-waiting': 'All Task Waiting',
workflow_leave: 'Leave Apply',
'workflow_task_my-document': 'My Document',
'workflow_task_task-waiting': 'My Task Waiting',
'workflow_task_task-finish': 'My Task Finish',
'workflow_task_task-copy': 'My Task Copy'
},
menu: {
system_tenant: 'Tenant Management',
system_log: 'Log Management',
'monitor_snail-job': 'Job Management',
monitor_admin: 'Admin Monitor'
},
dict: {
sys_user_sex: {
male: 'Male',
female: 'Female',
unknown: 'Unknown'
},
sys_show_hide: {
show: 'Show',
hide: 'Hide'
},
sys_normal_disable: {
normal: 'Normal',
disable: 'Disable'
},
sys_yes_no: {
yes: 'Yes',
no: 'No'
},
sys_notice_type: {
notice: 'Notice',
announcement: 'Announcement'
},
sys_notice_status: {
normal: 'Normal',
close: 'Close'
},
sys_oper_type: {
insert: 'Insert',
update: 'Update',
delete: 'Delete',
grant: 'Grant',
export: 'Export',
import: 'Import',
force: 'Force',
gencode: 'Generate Code',
clean: 'Clean Data',
other: 'Other'
},
sys_common_status: {
success: 'Success',
fail: 'Fail'
},
sys_grant_type: {
password: 'Password Auth',
sms: 'SMS Auth',
email: 'Email Auth',
miniapp: 'Mini App Auth',
social: 'Social Auth'
},
sys_device_type: {
pc: 'PC',
android: 'Android',
ios: 'iOS',
miniapp: 'Mini App'
},
wf_business_status: {
revoked: 'Revoked',
draft: 'Draft',
pending: 'Pending',
completed: 'Completed',
cancelled: 'Cancelled',
returned: 'Returned',
terminated: 'Terminated'
},
wf_form_type: {
custom_form: 'Custom Form',
dynamic_form: 'Dynamic Form'
},
wf_task_status: {
revoke: 'Revoke',
pass: 'Pass',
pending_review: 'Pending Review',
cancel: 'Cancel',
return: 'Return',
terminate: 'Terminate',
transfer: 'Transfer',
delegate: 'Delegate',
copy: 'Copy',
add_sign: 'Add Sign',
minus_sign: 'Minus Sign',
timeout: 'Timeout'
}
},
page: {
login: {

View File

@ -1,6 +1,6 @@
const local: App.I18n.Schema = {
system: {
title: 'RuoYi Vue Plus',
title: 'RuoYi Plus Soybean',
updateTitle: '系统版本更新通知',
updateContent: '检测到系统有新版本发布,是否立即刷新页面?',
updateConfirm: '立即刷新',
@ -61,6 +61,7 @@ const local: App.I18n.Schema = {
update: '更新',
saveSuccess: '保存成功',
updateSuccess: '更新成功',
noChange: '没有进行任何操作',
userCenter: '个人中心',
yesOrNo: {
yes: '是',
@ -166,7 +167,8 @@ const local: App.I18n.Schema = {
},
watermark: {
visible: '显示全屏水印',
text: '水印文本'
text: '水印文本',
enableUserName: '启用用户名水印'
},
tablePropsTitle: '表格配置',
table: {
@ -221,7 +223,7 @@ const local: App.I18n.Schema = {
system_notice: '通知公告',
'social-callback': '单点登录回调',
system_oss: '文件管理',
'system_oss-config': 'OSS配置',
'system_oss-config': 'OSS 配置',
monitor_cache: '缓存监控',
monitor_online: '在线用户',
'user-center': '个人中心',
@ -229,10 +231,112 @@ const local: App.I18n.Schema = {
demo: '测试',
demo_demo: '测试单表',
demo_tree: '测试树表',
workflow: '流程管理',
workflow_category: '流程分类',
exception: '异常页',
exception_403: '403',
exception_404: '404',
exception_500: '500'
exception_500: '500',
workflow_design: '流程设计',
workflow_spel: '流程表达式',
'workflow_process-definition': '流程定义',
'workflow_process-instance': '流程实例',
workflow_task: '任务',
'workflow_task_all-task-waiting': '待办任务',
workflow_leave: '请假申请',
'workflow_task_my-document': '我发起的',
'workflow_task_task-waiting': '我的待办',
'workflow_task_task-finish': '我的已办',
'workflow_task_task-copy': '我的抄送'
},
menu: {
system_tenant: '租户管理',
system_log: '日志管理',
'monitor_snail-job': '任务调度中心',
monitor_admin: 'Admin 监控'
},
dict: {
sys_user_sex: {
male: '男',
female: '女',
unknown: '未知'
},
sys_show_hide: {
show: '显示',
hide: '隐藏'
},
sys_normal_disable: {
normal: '正常',
disable: '停用'
},
sys_yes_no: {
yes: '是',
no: '否'
},
sys_notice_type: {
notice: '通知',
announcement: '公告'
},
sys_notice_status: {
normal: '正常',
close: '关闭'
},
sys_oper_type: {
insert: '新增',
update: '修改',
delete: '删除',
grant: '授权',
export: '导出',
import: '导入',
force: '强退',
gencode: '生成代码',
clean: '清空数据',
other: '其他'
},
sys_common_status: {
success: '成功',
fail: '失败'
},
sys_grant_type: {
password: '密码认证',
sms: '短信认证',
email: '邮件认证',
miniapp: '小程序认证',
social: '三方登录认证'
},
sys_device_type: {
pc: 'PC',
android: '安卓',
ios: 'iOS',
miniapp: '小程序'
},
wf_business_status: {
revoked: '已撤销',
draft: '草稿',
pending: '待审核',
completed: '已完成',
cancelled: '已作废',
returned: '已退回',
terminated: '已终止'
},
wf_form_type: {
custom_form: '自定义表单',
dynamic_form: '动态表单'
},
wf_task_status: {
revoke: '撤销',
pass: '通过',
pending_review: '待审核',
cancel: '作废',
return: '退回',
terminate: '终止',
transfer: '转办',
delegate: '委托',
copy: '抄送',
add_sign: '加签',
minus_sign: '减签',
timeout: '超时'
}
},
page: {
login: {

View File

@ -95,7 +95,6 @@ async function getHtmlBuildTime(): Promise<string | null> {
const res = await fetch(`${baseUrl}index.html?time=${Date.now()}`);
if (!res.ok) {
console.error('getHtmlBuildTime error:', res.status, res.statusText);
return null;
}
@ -103,7 +102,7 @@ async function getHtmlBuildTime(): Promise<string | null> {
const match = html.match(/<meta name="buildTime" content="(.*)">/);
return match?.[1] || null;
} catch (error) {
console.error('getHtmlBuildTime error:', error);
window.console.error('getHtmlBuildTime error:', error);
return null;
}
}

View File

@ -3,8 +3,8 @@ import { getRgb } from '@sa/color';
import { DARK_CLASS } from '@/constants/app';
import { localStg } from '@/utils/storage';
import { toggleHtmlClass } from '@/utils/common';
import systemLogo from '@/assets/imgs/logo.png';
import { $t } from '@/locales';
import '@/styles/scss/loading.scss';
export function setupLoading() {
const app = document.getElementById('app');
@ -21,12 +21,11 @@ export function setupLoading() {
const loading = `
<div class="fixed-center flex-col bg-layout" style="${primaryColor}">
<div class="w-120px h-120px my-36px">
<div class="relative h-full animate-spin">
<img src="${systemLogo}" width="120" />
</div>
<div class="my-52px h-120px w-120px">
<!-- From Uiverse.io by SchawnnahJ -->
<div class="loader"></div>
</div>
<h2 class="text-28px font-500 text-primary">${$t('system.title')}</h2>
<h2 class="text-30px text-primary-400 font-500">${$t('system.title')}</h2>
</div>`;
if (app) {

View File

@ -1,49 +0,0 @@
/* eslint-disable no-console */
/** 后台返回的路由动态生成name 解决缓存问题 感谢 @fourteendp 详见 https://github.com/vbenjs/vue-vben-admin/issues/3927 */
import type { Component } from 'vue';
import { defineComponent, h } from 'vue';
interface Options {
name?: string;
}
export type RouteComponentLoader = () => Promise<any>;
/**
* 作用相当于给组件包了一层 & 设置name 解决keepAlive问题
*
* @param loader 导入的路由
* @param options options
* @returns components
*/
export function createCustomNameComponent(
loader: RouteComponentLoader,
options: Options = {}
): () => Promise<Component> {
const { name } = options;
let component: Component | null = null;
const load = async () => {
try {
const { default: loadedComponent } = await loader();
component = loadedComponent;
} catch (error) {
console.error(`Cannot resolve component ${name}, error:`, error);
}
};
return async () => {
if (!component) {
await load();
}
return Promise.resolve(
defineComponent({
name,
render() {
return h(component as Component);
}
})
);
};
}

View File

@ -43,4 +43,15 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
system_tenant: () => import("@/views/system/tenant/index.vue"),
system_user: () => import("@/views/system/user/index.vue"),
tool_gen: () => import("@/views/tool/gen/index.vue"),
workflow_category: () => import("@/views/workflow/category/index.vue"),
workflow_design: () => import("@/views/workflow/design/index.vue"),
workflow_leave: () => import("@/views/workflow/leave/index.vue"),
"workflow_process-definition": () => import("@/views/workflow/process-definition/index.vue"),
"workflow_process-instance": () => import("@/views/workflow/process-instance/index.vue"),
workflow_spel: () => import("@/views/workflow/spel/index.vue"),
"workflow_task_all-task-waiting": () => import("@/views/workflow/task/all-task-waiting/index.vue"),
"workflow_task_my-document": () => import("@/views/workflow/task/my-document/index.vue"),
"workflow_task_task-copy": () => import("@/views/workflow/task/task-copy/index.vue"),
"workflow_task_task-finish": () => import("@/views/workflow/task/task-finish/index.vue"),
"workflow_task_task-waiting": () => import("@/views/workflow/task/task-waiting/index.vue"),
};

View File

@ -331,5 +331,125 @@ export const generatedRoutes: GeneratedRoute[] = [
icon: 'material-symbols:account-circle-full',
hideInMenu: true
}
},
{
name: 'workflow',
path: '/workflow',
component: 'layout.base',
meta: {
title: 'workflow',
i18nKey: 'route.workflow'
},
children: [
{
name: 'workflow_category',
path: '/workflow/category',
component: 'view.workflow_category',
meta: {
title: 'workflow_category',
i18nKey: 'route.workflow_category'
}
},
{
name: 'workflow_design',
path: '/workflow/design',
component: 'view.workflow_design',
meta: {
title: 'workflow_design',
i18nKey: 'route.workflow_design'
}
},
{
name: 'workflow_leave',
path: '/workflow/leave',
component: 'view.workflow_leave',
meta: {
title: 'workflow_leave',
i18nKey: 'route.workflow_leave'
}
},
{
name: 'workflow_process-definition',
path: '/workflow/process-definition',
component: 'view.workflow_process-definition',
meta: {
title: 'workflow_process-definition',
i18nKey: 'route.workflow_process-definition'
}
},
{
name: 'workflow_process-instance',
path: '/workflow/process-instance',
component: 'view.workflow_process-instance',
meta: {
title: 'workflow_process-instance',
i18nKey: 'route.workflow_process-instance'
}
},
{
name: 'workflow_spel',
path: '/workflow/spel',
component: 'view.workflow_spel',
meta: {
title: 'workflow_spel',
i18nKey: 'route.workflow_spel'
}
},
{
name: 'workflow_task',
path: '/workflow/task',
meta: {
title: 'workflow_task',
i18nKey: 'route.workflow_task'
},
children: [
{
name: 'workflow_task_all-task-waiting',
path: '/workflow/task/all-task-waiting',
component: 'view.workflow_task_all-task-waiting',
meta: {
title: 'workflow_task_all-task-waiting',
i18nKey: 'route.workflow_task_all-task-waiting'
}
},
{
name: 'workflow_task_my-document',
path: '/workflow/task/my-document',
component: 'view.workflow_task_my-document',
meta: {
title: 'workflow_task_my-document',
i18nKey: 'route.workflow_task_my-document'
}
},
{
name: 'workflow_task_task-copy',
path: '/workflow/task/task-copy',
component: 'view.workflow_task_task-copy',
meta: {
title: 'workflow_task_task-copy',
i18nKey: 'route.workflow_task_task-copy'
}
},
{
name: 'workflow_task_task-finish',
path: '/workflow/task/task-finish',
component: 'view.workflow_task_task-finish',
meta: {
title: 'workflow_task_task-finish',
i18nKey: 'route.workflow_task_task-finish'
}
},
{
name: 'workflow_task_task-waiting',
path: '/workflow/task/task-waiting',
component: 'view.workflow_task_task-waiting',
meta: {
title: 'workflow_task_task-waiting',
i18nKey: 'route.workflow_task_task-waiting'
}
}
]
}
]
}
];

View File

@ -198,7 +198,20 @@ const routeMap: RouteMap = {
"system_user": "/system/user",
"tool": "/tool",
"tool_gen": "/tool/gen",
"user-center": "/user-center"
"user-center": "/user-center",
"workflow": "/workflow",
"workflow_category": "/workflow/category",
"workflow_design": "/workflow/design",
"workflow_leave": "/workflow/leave",
"workflow_process-definition": "/workflow/process-definition",
"workflow_process-instance": "/workflow/process-instance",
"workflow_spel": "/workflow/spel",
"workflow_task": "/workflow/task",
"workflow_task_all-task-waiting": "/workflow/task/all-task-waiting",
"workflow_task_my-document": "/workflow/task/my-document",
"workflow_task_task-copy": "/workflow/task/task-copy",
"workflow_task_task-finish": "/workflow/task/task-finish",
"workflow_task_task-waiting": "/workflow/task/task-waiting"
};
/**

View File

@ -20,6 +20,17 @@ export function fetchForceLogout(tokenId: string) {
method: 'delete'
});
}
/**
* 强退当前在线设备
*
* @param tokenId - 令牌ID
*/
export function fetchKickOutCurrentDevice(tokenId: string) {
return request<boolean>({
url: `/monitor/online/myself/${tokenId}`,
method: 'delete'
});
}
/** 获取在线设备列表 */
export function fetchGetOnlineDeviceList(params?: Api.Monitor.OnlineUserSearchParams) {

View File

@ -1,4 +1,3 @@
import type { AxiosRequestConfig, GenericAbortSignal } from 'axios';
import { request } from '@/service/request';
/** 获取文件管理列表 */
@ -18,36 +17,10 @@ export function fetchBatchDeleteOss(ossIds: CommonType.IdType[]) {
});
}
/** Axios上传进度事件 */
export type AxiosProgressEvent = AxiosRequestConfig['onUploadProgress'];
/** 默认上传结果 */
export interface UploadResult {
url: string;
fileName: string;
ossId: string;
}
export interface UploadApiOptions {
onUploadProgress?: AxiosProgressEvent;
signal?: GenericAbortSignal;
}
/** 上传文件接口 */
export function uploadApi(file: File | Blob, options?: UploadApiOptions) {
const { onUploadProgress, signal } = options ?? {};
const formData = new FormData();
formData.append('file', file);
return request<UploadResult>({
url: '/resource/oss/upload',
method: 'post',
data: formData,
onUploadProgress,
headers: {
'Content-Type': 'multipart/form-data'
},
signal
// 查询OSS对象基于id串
export function fetchGetOssListByIds(ossIds: CommonType.IdType[]) {
return request<Api.System.Oss[]>({
url: `/resource/oss/listByIds/${ossIds.join(',')}`,
method: 'get'
});
}

View File

@ -36,6 +36,15 @@ export function fetchUpdateRoleStatus(data: Api.System.RoleOperateParams) {
});
}
/** 修改角色数据权限 */
export function fetchUpdateRoleDataScope(data: Api.System.RoleOperateParams) {
return request<boolean>({
url: '/system/role/dataScope',
method: 'put',
data
});
}
/** 批量删除角色信息 */
export function fetchBatchDeleteRole(roleIds: CommonType.IdType[]) {
return request<boolean>({
@ -69,3 +78,21 @@ export function fetchGetRoleUserList(params: Api.System.UserSearchParams) {
params
});
}
/** 批量选择用户授权 */
export function fetchUpdateRoleAuthUser(roleId: CommonType.IdType, userIds: CommonType.IdType[]) {
return request<boolean>({
url: '/system/role/authUser/selectAll',
method: 'put',
params: { roleId, userIds: userIds.join(',') }
});
}
/** 批量取消用户授权 */
export function fetchUpdateRoleAuthUserCancel(roleId: CommonType.IdType, userIds: CommonType.IdType[]) {
return request<boolean>({
url: '/system/role/authUser/cancelAll',
method: 'put',
params: { roleId, userIds: userIds.join(',') }
});
}

View File

@ -16,7 +16,7 @@ export function fetchCreateTenant(data: Api.System.TenantOperateParams) {
method: 'post',
headers: {
isEncrypt: true,
repeatSubmit: true
repeatSubmit: false
},
data
});

View File

@ -81,7 +81,8 @@ export function fetchResetUserPassword(userId: CommonType.IdType, password: stri
url: '/system/user/resetPwd',
method: 'put',
headers: {
isEncrypt: true
isEncrypt: true,
repeatSubmit: false
},
data: { userId, password }
});
@ -118,6 +119,9 @@ export function fetchUpdateUserPassword(data: Api.System.UserPasswordOperatePara
return request<boolean>({
url: '/system/user/profile/updatePwd',
method: 'put',
headers: {
isEncrypt: true
},
data
});
}

View File

@ -0,0 +1,53 @@
import { request } from '@/service/request';
/** 获取测试树列表 */
export function fetchGetCategoryList(params?: Api.Workflow.WorkflowCategorySearchParams) {
return request<Api.Workflow.WorkflowCategoryList>({
url: '/workflow/category/list',
method: 'get',
params
});
}
/** 新增测试树 */
export function fetchCreateCategory(data: Api.Workflow.WorkflowCategoryOperateParams) {
return request<boolean>({
url: '/workflow/category',
method: 'post',
data
});
}
/** 修改测试树 */
export function fetchUpdateCategory(data: Api.Workflow.WorkflowCategoryOperateParams) {
return request<boolean>({
url: '/workflow/category',
method: 'put',
data
});
}
/** 删除分类 */
export function fetchDeleteCategory(id: CommonType.IdType) {
return request<boolean>({
url: `/workflow/category/${id}`,
method: 'delete'
});
}
/** 导出工作流分类 */
export function fetchExportCategory(params?: Api.Workflow.WorkflowCategorySearchParams) {
return request<boolean>({
url: '/workflow/category/export',
method: 'post',
params
});
}
/** 获取分类树 */
export function fetchGetCategoryTree() {
return request<Api.Common.CommonTreeRecord>({
url: '/workflow/category/categoryTree',
method: 'get'
});
}

View File

@ -0,0 +1,72 @@
import { request } from '@/service/request';
/** 获取流程定义列表 */
export function fetchGetDefinitionList(params?: Api.Workflow.DefinitionSearchParams) {
return request<Api.Workflow.DefinitionList>({
url: '/workflow/definition/list',
method: 'get',
params
});
}
/** 获取未发布流程定义列表 */
export function fetchGetUnPublishDefinitionList(params?: Api.Workflow.DefinitionSearchParams) {
return request<Api.Workflow.DefinitionList>({
url: '/workflow/definition/unPublishList',
method: 'get',
params
});
}
/** 新增流程定义 */
export function fetchCreateDefinition(data: Api.Workflow.DefinitionOperateParams) {
return request<boolean>({
url: '/workflow/definition',
method: 'post',
data
});
}
/** 修改流程定义 */
export function fetchUpdateDefinition(data: Api.Workflow.DefinitionOperateParams) {
return request<boolean>({
url: '/workflow/definition',
method: 'put',
data
});
}
/** 批量删除流程定义 */
export function fetchBatchDeleteDefinition(ids: CommonType.IdType[]) {
return request<boolean>({
url: `/workflow/definition/${ids.join(',')}`,
method: 'delete'
});
}
/** 激活/挂起流程定义 */
export function fetchActiveDefinition(id: CommonType.IdType, active: boolean) {
return request<boolean>({
url: `/workflow/definition/active/${id}`,
method: 'put',
params: {
active
}
});
}
/** 发布流程定义 */
export function fetchPublishDefinition(id: CommonType.IdType) {
return request<boolean>({
url: `/workflow/definition/publish/${id}`,
method: 'put'
});
}
/** 复制流程定义 */
export function fetchCopyDefinition(id: CommonType.IdType) {
return request<boolean>({
url: `/workflow/definition/copy/${id}`,
method: 'post'
});
}

View File

@ -0,0 +1,5 @@
export * from './category';
export * from './leave';
export * from './instance';
export * from './definition';
export * from './task';

View File

@ -0,0 +1,61 @@
import { request } from '@/service/request';
/** 查询正在运行的流程实例列表 */
export function fetchGetRunningInstanceList(params: Api.Workflow.InstanceSearchParams) {
return request<Api.Workflow.InstanceList>({
url: '/workflow/instance/pageByRunning',
method: 'get',
params
});
}
/** 查询已结束的流程实例列表 */
export function fetchGetFinishedInstanceList(params: Api.Workflow.InstanceSearchParams) {
return request<Api.Workflow.InstanceList>({
url: '/workflow/instance/pageByFinish',
method: 'get',
params
});
}
/** 按照实例id删除流程实例 */
export function fetchBatchDeleteInstance(instanceIds: CommonType.IdType[]) {
return request<boolean>({
url: `/workflow/instance/deleteByInstanceIds/${instanceIds.join(',')}`,
method: 'delete'
});
}
/** 流程作废操作 */
export function fetchFlowInvalidOperate(data: Api.Workflow.FlowInvalidOperateParams) {
return request<boolean>({
url: '/workflow/instance/invalid',
method: 'post',
data
});
}
/** 获取流程记录 */
export function fetchGetFlowHisTaskList(businessId: CommonType.IdType) {
return request<Api.Workflow.InstanceIdWithHisTask>({
url: `/workflow/instance/flowHisTaskList/${businessId}`,
method: 'get'
});
}
/** 流程作废操作 */
export function fetchCancelProcessApply(data: Api.Workflow.CancelProcessApplyParams) {
return request<boolean>({
url: '/workflow/instance/cancelProcessApply',
method: 'put',
data
});
}
/** 获取我的待办 */
export function fetchGetMyDocument(data: Api.Workflow.InstanceSearchParams) {
return request<Api.Workflow.InstanceList>({
url: '/workflow/instance/pageByCurrent',
method: 'get',
params: data
});
}

View File

@ -0,0 +1,51 @@
import { request } from '@/service/request';
/** 获取请假申请列表 */
export function fetchGetLeaveList(params?: Api.Workflow.LeaveSearchParams) {
return request<Api.Workflow.LeaveList>({
url: '/workflow/leave/list',
method: 'get',
params
});
}
/** 获取请假申请详情 */
export function fetchGetLeaveDetail(id: CommonType.IdType) {
return request<Api.Workflow.LeaveDetail>({
url: `/workflow/leave/${id}`,
method: 'get'
});
}
/** 新增请假申请 */
export function fetchCreateLeave(data: Api.Workflow.LeaveOperateParams) {
return request<Api.Workflow.Leave>({
url: '/workflow/leave',
method: 'post',
data
});
}
/** 修改请假申请 */
export function fetchUpdateLeave(data: Api.Workflow.LeaveOperateParams) {
return request<Api.Workflow.Leave>({
url: '/workflow/leave',
method: 'put',
data
});
}
/** 批量删除请假申请 */
export function fetchBatchDeleteLeave(ids: CommonType.IdType[]) {
return request<boolean>({
url: `/workflow/leave/${ids.join(',')}`,
method: 'delete'
});
}
/** 撤销请假申请 */
export function fetchCancelLeave(id: CommonType.IdType) {
return request<boolean>({
url: `/workflow/leave/cancel/${id}`,
method: 'put'
});
}

View File

@ -0,0 +1,36 @@
import { request } from '@/service/request';
/** 获取流程spel达式定义列表 */
export function fetchGetSpelList(params?: Api.Workflow.SpelSearchParams) {
return request<Api.Workflow.SpelList>({
url: '/workflow/spel/list',
method: 'get',
params
});
}
/** 新增流程spel达式定义 */
export function fetchCreateSpel(data: Api.Workflow.SpelOperateParams) {
return request<boolean>({
url: '/workflow/spel',
method: 'post',
data
});
}
/** 修改流程spel达式定义 */
export function fetchUpdateSpel(data: Api.Workflow.SpelOperateParams) {
return request<boolean>({
url: '/workflow/spel',
method: 'put',
data
});
}
/** 批量删除流程spel达式定义 */
export function fetchBatchDeleteSpel(ids: CommonType.IdType[]) {
return request<boolean>({
url: `/workflow/spel/${ids.join(',')}`,
method: 'delete'
});
}

View File

@ -0,0 +1,124 @@
import { request } from '@/service/request';
/** 启动任务 */
export function fetchStartWorkflow(data: Api.Workflow.StartWorkflowOperateParams) {
return request<Api.Workflow.StartWorkflowResult>({
url: '/workflow/task/startWorkFlow',
method: 'post',
data
});
}
/** 获取任务 */
export function fetchGetTask(taskId: CommonType.IdType) {
return request<Api.Workflow.Task>({
url: `/workflow/task/getTask/${taskId}`,
method: 'get'
});
}
/** 获取任务下一个节点 */
export function fetchGetkNextNode(data: Api.Workflow.TaskNextNodeSearchParams) {
return request<Api.Workflow.FlowNodeList>({
url: '/workflow/task/getNextNodeList',
method: 'post',
data
});
}
/** 完成任务 */
export function fetchCompleteTask(data: Api.Workflow.CompleteTaskOperateParams) {
return request<boolean>({
url: '/workflow/task/completeTask',
method: 'post',
data
});
}
/** 获取所有待办任务 */
export function fetchGetAllWaitingTask(data: Api.Workflow.TaskSearchParams) {
return request<Api.Workflow.TaskList>({
url: '/workflow/task/pageByAllTaskWait',
method: 'get',
params: data
});
}
/** 获取所有已办任务 */
export function fetchGetAllFinishedTask(data: Api.Workflow.TaskSearchParams) {
return request<Api.Workflow.HisTaskList>({
url: '/workflow/task/pageByAllTaskFinish',
method: 'get',
params: data
});
}
/** 任务操作 */
export function fetchTaskOperate(data: Api.Workflow.TaskOperateParams, operateType: Api.Workflow.TaskOperateType) {
return request<boolean>({
url: `/workflow/task/taskOperation/${operateType}`,
method: 'post',
data
});
}
/** 终止任务 */
export function fetchTerminateTask(data: Api.Workflow.TerminateTaskOperateParams) {
return request<boolean>({
url: '/workflow/task/terminationTask',
method: 'post',
data
});
}
/** 获取当前任务所有人员 */
export function fetchGetCurrentTaskAllUser(taskId: CommonType.IdType) {
return request<Api.System.User[]>({
url: `/workflow/task/currentTaskAllUser/${taskId}`,
method: 'get'
});
}
/** 获取我的待办 */
export function fetchGetTaskWaitList(data: Api.Workflow.TaskSearchParams) {
return request<Api.Workflow.TaskList>({
url: '/workflow/task/pageByTaskWait',
method: 'get',
params: data
});
}
/** 获取可驳回节点 */
export function fetchGetBackNode(definitionId: CommonType.IdType, nodeCode: string) {
return request<Api.Workflow.FlowNodeList>({
url: `/workflow/task/getBackTaskNode/${definitionId}/${nodeCode}`,
method: 'get'
});
}
/** 驳回任务 */
export function fetchBackTask(data: Api.Workflow.BackOperateParams) {
return request<boolean>({
url: '/workflow/task/backProcess',
method: 'post',
data
});
}
/** 获取我的已办任务 */
export function fetchGetFinishedTask(data: Api.Workflow.TaskSearchParams) {
return request<Api.Workflow.HisTaskList>({
url: '/workflow/task/pageByTaskFinish',
method: 'get',
params: data
});
}
/** 获取我的抄送任务 */
export function fetchGetCopyTask(data: Api.Workflow.TaskSearchParams) {
return request<Api.Workflow.TaskList>({
url: '/workflow/task/pageByTaskCopy',
method: 'get',
params: data
});
}

View File

@ -65,33 +65,47 @@ export const request = createFlatRequest<App.Service.Response, RequestInstanceSt
request.state.errMsgStack = request.state.errMsgStack.filter(msg => msg !== response.data.msg);
}
const isLogin = Boolean(localStg.get('token'));
// when the backend response code is in `logoutCodes`, it means the user will be logged out and redirected to login page
// const logoutCodes = import.meta.env.VITE_SERVICE_LOGOUT_CODES?.split(',') || [];
// if (logoutCodes.includes(responseCode)) {
// handleLogout();
// return null;
// }
const logoutCodes = import.meta.env.VITE_SERVICE_LOGOUT_CODES?.split(',') || [];
if (logoutCodes.includes(responseCode) && !isLogin) {
logoutAndCleanup();
return null;
}
// when the backend response code is in `modalLogoutCodes`, it means the user will be logged out by displaying a modal
const modalLogoutCodes = import.meta.env.VITE_SERVICE_MODAL_LOGOUT_CODES?.split(',') || [];
if (modalLogoutCodes.includes(responseCode)) {
request.state.errMsgStack = [...(request.state.errMsgStack || []), response.data.msg];
if (modalLogoutCodes.includes(responseCode) && isLogin) {
const isExist = request.state.errMsgStack?.includes(response.data.msg);
if (isExist) {
return null;
}
if (window.location.pathname?.startsWith('/login')) {
logoutAndCleanup();
return null;
}
// prevent the user from refreshing the page
window.addEventListener('beforeunload', handleLogout);
request.state.errMsgStack = [...(request.state.errMsgStack || []), response.data.msg];
window.$dialog?.warning({
title: '系统提示',
content: '登录状态已过期,您可以继续留在该页面,或者重新登录',
content: '登录状态已过期,重新登录',
positiveText: '重新登录',
negativeText: '取消',
maskClosable: false,
closeOnEsc: false,
onAfterEnter() {
// prevent the user from refreshing the page
window.addEventListener('beforeunload', handleLogout);
},
onPositiveClick() {
logoutAndCleanup();
},
onClose() {
logoutAndCleanup();
}
});
request.cancelAllRequest();
return null;
}

View File

@ -12,6 +12,7 @@ import { clearAuthStorage, getToken } from './shared';
export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
const route = useRoute();
const authStore = useAuthStore();
const routeStore = useRouteStore();
const tabStore = useTabStore();
const { toLogin, redirectFromLogin } = useRouterPush(false);
@ -37,8 +38,6 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
/** Reset auth store */
async function resetStore() {
const authStore = useAuthStore();
recordUserId();
clearAuthStorage();

View File

@ -1,11 +1,17 @@
import { ref } from 'vue';
import { defineStore } from 'pinia';
import { $t } from '@/locales';
export const useDictStore = defineStore('dict', () => {
const dictData = ref<{ [key: string]: Api.System.DictData[] }>({});
const getDict = (key: string) => {
return dictData.value[key];
return dictData.value[key]?.map(item => ({
...item,
dictLabel: item.dictLabel?.startsWith(`dict.${item.dictType}.`)
? $t(item.dictLabel as App.I18n.I18nKey)
: item.dictLabel
}));
};
const setDict = (key: string, dict: Api.System.DictData[]) => {

View File

@ -42,16 +42,7 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
const authRouteMode = ref(import.meta.env.VITE_AUTH_ROUTE_MODE);
/** Home route key */
const routeHome = ref(import.meta.env.VITE_ROUTE_HOME);
/**
* Set route home
*
* @param routeKey Route key
*/
function setRouteHome(routeKey: LastLevelRouteKey) {
routeHome.value = routeKey;
}
const routeHome = ref(import.meta.env.VITE_ROUTE_HOME || 'home');
/** constant routes */
const constantRoutes = shallowRef<ElegantConstRoute[]>([]);
@ -74,11 +65,15 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
routes.forEach(route => {
if (authRouteMode.value === 'dynamic') {
if (route.path === '/') {
route.children?.forEach(child => {
parseRouter(child);
authRoutesMap.set(child.name, child);
});
if (route.path === '/' && route.children?.length) {
const child = route.children[0];
// @ts-expect-error no hidden field
child.hidden = route.hidden;
parseRouter(child);
child.name = Math.random().toString(36).slice(2, 12);
Object.assign(route, child);
delete route.children;
authRoutesMap.set(route.name, route);
return;
}
parseRouter(route);
@ -103,6 +98,9 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
// eslint-disable-next-line complexity
function parseRouter(route: ElegantConstRoute, parent?: ElegantConstRoute) {
route.meta = route.meta ? route.meta : { title: route.name };
if (route.meta.title.startsWith('route.') || route.meta.title.startsWith('menu.')) {
route.meta.i18nKey = route.meta.title as App.I18n.I18nKey;
}
const isLayout = route.component === 'Layout';
const isFramePage = route.component === 'FrameView';
const isParentLayout = route.component === 'ParentView';
@ -127,7 +125,6 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
} else if (!isNotNull(route.meta.icon)) {
route.meta.icon = defaultIcon;
}
// @ts-expect-error no hidden field
route.meta.hideInMenu = route.hidden;
if (route.meta.hideInMenu && parent) {
@ -300,9 +297,7 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
handleConstantAndAuthRoutes();
setRouteHome('home');
handleUpdateRootRouteRedirect('home');
handleUpdateRootRouteRedirect(routeHome.value);
setIsInitAuthRoute(true);
} else {

View File

@ -100,7 +100,9 @@ export const useTabStore = defineStore(SetupStoreId.Tab, () => {
const removedTabRouteKey = tabs.value[removeTabIndex].routeKey;
const isRemoveActiveTab = activeTabId.value === tabId;
const nextTab = tabs.value[removeTabIndex + 1] || homeTab.value;
// if remove the last tab, then switch to the second last tab
const nextTab = tabs.value[removeTabIndex + 1] || tabs.value[removeTabIndex - 1] || homeTab.value;
// remove tab
tabs.value.splice(removeTabIndex, 1);

View File

@ -252,6 +252,9 @@ export function getNaiveTheme(colors: App.Theme.ThemeColor, recommended = false)
},
Tag: {
borderRadius: '6px'
},
Button: {
textColorPrimary: '#ffffff'
}
};

View File

@ -10,4 +10,5 @@ body,
html {
overflow-x: hidden;
color: rgb(var(--base-text-color));
}

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