130 Commits

Author SHA1 Message Date
9fc63fa294 Merge branch 'dev' into flow 2025-09-10 17:13:07 +08:00
AN
d5bbc37dec fix(projects): 修复新增部门时不显示上级部门问题 2025-09-10 11:48:13 +08:00
AN
2f794c4b73 fix(projects): 修改代码生成功能模块名为驼峰时,路由错误问题 2025-09-10 10:40:23 +08:00
AN
19bf543485 fix(projects): 修改代码生成功能模块名为驼峰时,路由错误问题 2025-09-10 10:31:18 +08:00
AN
34ad79e815 feat(projects): 补充发起流程字段参数 2025-09-08 14:20:00 +08:00
AN
7de471aa68 Merge branch 'dev' of https://gitee.com/xlsea/ruoyi-plus-soybean into flow 2025-09-08 13:59:44 +08:00
AN
378aa869bf style(components): 修改json预览组件样式问题 2025-09-08 13:51:28 +08:00
AN
4a4244b5c4 style(styles): 修复字体样式导致下划线不可见问题 2025-09-05 17:20:55 +08:00
AN
ecad1c3e78 optimize(components): 补充国际化 2025-09-05 14:31:23 +08:00
AN
1eb0a3dac2 feat(projects): 补充Task的业务扩展字段 2025-09-05 13:53:07 +08:00
AN
bc44d2cddc feat(projects): 添加修改办理人,催办功能,优化组件样式 2025-09-05 13:28:47 +08:00
AN
b07729edd1 Merge branch 'dev' of https://gitee.com/xlsea/ruoyi-plus-soybean into flow 2025-09-05 09:29:24 +08:00
9ef0bd416e fix(utils): 修复请求工具响应解密问题 2025-09-04 14:57:12 +08:00
AN
9b60512165 feat(projects): 流程实例添加变量修改功能 2025-09-04 14:09:05 +08:00
AN
f739e1e031 feat(projects): 流程定义添加设计器样式等拓展字段,优化流程分类下拉组件 2025-09-03 14:31:50 +08:00
b70095e5ff Merge branch 'dev' into flow 2025-09-02 15:46:13 +08:00
25ee32074a feat(projects): 路由兼容 activeMenu 选项 2025-09-02 15:10:59 +08:00
8412a8db16 feat(projects): 重构登录页面样式 2025-09-02 14:49:58 +08:00
e230b0da81 Merge remote-tracking branch 'Soybean/main' into dev
# Conflicts:
#	build/config/proxy.ts
#	package.json
#	pnpm-lock.yaml
2025-09-01 15:06:44 +08:00
12b25e0d58 fix(types): fix proxy types 2025-08-28 00:26:29 +08:00
e33f944a74 chore(deps): update deps 2025-08-28 00:26:12 +08:00
7f2f3bd088 feat(utils): 新增本地 Excel 导出工具类 2025-08-18 16:14:29 +08:00
5ef1c5de98 fix(hooks): 修复下载 hooks 错误未处理 2025-08-18 15:24:25 +08:00
90a14e338a fix(components): 修复字典标签会修改字典数据值问题 2025-08-18 14:10:12 +08:00
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
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
41c74db1cd Merge branch 'dev' of https://gitee.com/xlsea/ruoyi-plus-soybean into flow 2025-07-30 20:44:52 +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
c87aa6652c Merge branch 'dev' into flow 2025-07-24 17:45:50 +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
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
0dcfc893dd Merge branch 'dev' into flow 2025-07-17 11:08:53 +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
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
AN
523aca6b75 feat(projects): 新增抄送、下一审批人提交功能,优化组件通用性 2025-07-13 22:07:40 +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
AN
400eaf8990 fix(projects): 修复更新后产生问题 2025-07-09 13:43:44 +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
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
baa0584bdc Merge branch 'master' into flow 2025-07-01 10:54:32 +08: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
f1d7b9733f feat(projects): 新增减签功能 2025-06-28 22:07:02 +08:00
AN
55dceca28b feat(projects): 新增加签功能 2025-06-27 18:22:21 +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
AN
80faf4b47c feat(projects): 新增转办和终止功能 2025-06-25 23:19:32 +08:00
AN
89e7edb380 docs(projects): 新增工作流更新sql 2025-06-25 13:33:14 +08:00
AN
b8c771cd1d feat(projects): 新增用户选择器组件,添加流程干预按钮 2025-06-24 23:46:15 +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
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
50e7b5158d feat(projects): 优化组件,完成流程实例-流程预览 2025-06-19 23:54:21 +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
8ce02aa15a Merge branch 'dev' into flow 2025-06-18 23:22: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
AN
90ebf83501 Merge branch 'dev' of https://gitee.com/xlsea/ruoyi-plus-soybean into flow 2025-06-18 17:25: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
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
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
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
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
AN
ffd6211e4d refactor(projects): 调整为批量上传文件 2025-06-08 22:36:33 +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
93 changed files with 10422 additions and 1346 deletions

View File

@ -15,6 +15,8 @@ VITE_APP_CLIENT_ID=e5cd7e4891bf95d1d19206ce24a7b32e
# 接口加密功能开关(如需关闭 后端也必须对应关闭)
VITE_APP_ENCRYPT=Y
# AES 加密头标识
VITE_HEADER_FLAG=encrypt-key
# 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
VITE_APP_RSA_PUBLIC_KEY='MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
# 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换

View File

@ -12,6 +12,8 @@ VITE_APP_CLIENT_ID=e5cd7e4891bf95d1d19206ce24a7b32e
# 接口加密功能开关(如需关闭 后端也必须对应关闭)
VITE_APP_ENCRYPT=Y
# AES 加密头标识
VITE_HEADER_FLAG=encrypt-key
# 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
VITE_APP_RSA_PUBLIC_KEY='MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
# 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换

View File

@ -12,6 +12,8 @@ VITE_APP_CLIENT_ID=e5cd7e4891bf95d1d19206ce24a7b32e
# 接口加密功能开关(如需关闭 后端也必须对应关闭)
VITE_APP_ENCRYPT=Y
# AES 加密头标识
VITE_HEADER_FLAG=encrypt-key
# 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
VITE_APP_RSA_PUBLIC_KEY='MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
# 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换

13
.vscode/settings.json vendored
View File

@ -4,7 +4,18 @@
"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"],

View File

@ -1,5 +1,114 @@
# 更新日志
## [v1.1.3](https://gitee.com/xlsea/ruoyi-plus-soybean/compare/v1.1.2...v1.1.3) (2025-08-16)
###    🚀 新功能
- 对接工作流分类模块 &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 修复

View File

@ -22,7 +22,7 @@
# 📢 重要通知
1.1.2 版本已经正式发布工作流版本迎来首个版本(请切换 [flow](https://gitee.com/xlsea/ruoyi-plus-soybean/tree/flow/) 分支查看),但仍然建议:
1.1.3 版本已经正式发布工作流版本请切换 [flow](https://gitee.com/xlsea/ruoyi-plus-soybean/tree/flow/) 分支查看),但仍然建议:
- 在生产环境使用前进行充分测试
- 关注项目更新,及时获取最新版本
- 积极反馈问题,帮助我们快速迭代
@ -40,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 的现代化前端特性,为开发者提供了完整的企业管理解决方案。

View File

@ -1,4 +1,4 @@
import type { HttpProxy, ProxyOptions } from 'vite';
import type { ProxyOptions } from 'vite';
import { bgRed, bgYellow, green, lightBlue } from 'kolorist';
import { consola } from 'consola';
import { createServiceConfig } from '../../src/utils/service';
@ -34,7 +34,7 @@ function createProxyItem(item: App.Service.ServiceConfigItem, enableLog: boolean
target: item.baseURL,
changeOrigin: true,
ws: item.ws,
configure: (_proxy: HttpProxy.Server, options: ProxyOptions) => {
configure: (_proxy, options) => {
_proxy.on('proxyReq', (_proxyReq, req, _res) => {
if (!enableLog) return;

View File

@ -10,6 +10,7 @@ import org.apache.velocity.VelocityContext;
import org.dromara.common.core.utils.DateUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.mybatis.enums.DataBaseType;
import org.dromara.common.mybatis.helper.DataBaseHelper;
import org.dromara.generator.constant.GenConstants;
import org.dromara.generator.domain.GenTable;
@ -58,7 +59,7 @@ public class VelocityUtils {
velocityContext.put("functionName", StringUtils.isNotEmpty(functionName) ? functionName : "【请填写功能名称】");
velocityContext.put("ClassName", genTable.getClassName());
velocityContext.put("className", StringUtils.uncapitalize(genTable.getClassName()));
velocityContext.put("moduleName", genTable.getModuleName());
velocityContext.put("moduleName", StrUtil.toSymbolCase(genTable.getModuleName(), '-'));
velocityContext.put("BusinessName", StringUtils.capitalize(genTable.getBusinessName()));
velocityContext.put("businessName", genTable.getBusinessName());
velocityContext.put("business_name", StrUtil.toUnderlineCase(genTable.getBusinessName()));
@ -124,11 +125,12 @@ public class VelocityUtils {
templates.add("vm/java/serviceImpl.java.vm");
templates.add("vm/java/controller.java.vm");
templates.add("vm/xml/mapper.xml.vm");
if (DataBaseHelper.isOracle()) {
DataBaseType dataBaseType = DataBaseHelper.getDataBaseType();
if (dataBaseType.isOracle()) {
templates.add("vm/sql/oracle/sql.vm");
} else if (DataBaseHelper.isPostgerSql()) {
} else if (dataBaseType.isPostgreSql()) {
templates.add("vm/sql/postgres/sql.vm");
} else if (DataBaseHelper.isSqlServer()) {
} else if (dataBaseType.isSqlServer()) {
templates.add("vm/sql/sqlserver/sql.vm");
} else {
templates.add("vm/sql/sql.vm");
@ -163,7 +165,7 @@ public class VelocityUtils {
String javaPath = PROJECT_PATH + "/" + StringUtils.replace(packageName, ".", "/");
String mybatisPath = MYBATIS_PATH + "/" + moduleName;
String soybeanPath = "soy";
String soybeanModuleName = StrUtil.toSymbolCase(moduleName, '-');
if (template.contains("domain.java.vm")) {
fileName = StringUtils.format("{}/domain/{}.java", javaPath, className);
}
@ -186,17 +188,17 @@ public class VelocityUtils {
} else if (template.contains("sql.vm")) {
fileName = businessName + "Menu.sql";
} else if (template.contains("index.vue.vm")) {
fileName = StringUtils.format("{}/views/{}/{}/index.vue", soybeanPath, moduleName, StrUtil.toSymbolCase(businessName, '-'));
fileName = StringUtils.format("{}/views/{}/{}/index.vue", soybeanPath, soybeanModuleName, StrUtil.toSymbolCase(businessName, '-'));
} else if (template.contains("index-tree.vue.vm")) {
fileName = StringUtils.format("{}/views/{}/{}/index.vue", soybeanPath, moduleName, StrUtil.toSymbolCase(businessName, '-'));
fileName = StringUtils.format("{}/views/{}/{}/index.vue", soybeanPath, soybeanModuleName, StrUtil.toSymbolCase(businessName, '-'));
} else if (template.contains("api.d.ts.vm")) {
fileName = StringUtils.format("{}/typings/api/{}.{}.api.d.ts", soybeanPath, moduleName, StrUtil.toSymbolCase(businessName, '-'));
fileName = StringUtils.format("{}/typings/api/{}.{}.api.d.ts", soybeanPath, soybeanModuleName, StrUtil.toSymbolCase(businessName, '-'));
} else if (template.contains("api.ts.vm")) {
fileName = StringUtils.format("{}/service/api/{}/{}.ts", soybeanPath, moduleName, StrUtil.toSymbolCase(businessName, '-'));
fileName = StringUtils.format("{}/service/api/{}/{}.ts", soybeanPath, soybeanModuleName, 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, '-'));
fileName = StringUtils.format("{}/views/{}/{}/modules/{}-search.vue", soybeanPath, soybeanModuleName, StrUtil.toSymbolCase(businessName, '-'), StrUtil.toSymbolCase(businessName, '-'));
} else if (template.contains("operate-drawer.vue.vm")) {
fileName = StringUtils.format("{}/views/{}/{}/modules/{}-operate-drawer.vue", soybeanPath, moduleName, StrUtil.toSymbolCase(businessName, '-'), StrUtil.toSymbolCase(businessName, '-'));
fileName = StringUtils.format("{}/views/{}/{}/modules/{}-operate-drawer.vue", soybeanPath, soybeanModuleName, StrUtil.toSymbolCase(businessName, '-'), StrUtil.toSymbolCase(businessName, '-'));
}
return fileName;
}

View File

@ -35,4 +35,23 @@ UPDATE `sys_menu` SET `component` = 'FrameView', `query_param` = 'https://previe
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

@ -1,7 +1,7 @@
{
"name": "ruoyi-vue-plus",
"type": "module",
"version": "1.1.2",
"version": "1.1.3",
"description": "结合了 RuoYi-Vue-Plus 的强大后端功能和 Soybean Admin 的现代化前端特性,为开发者提供了完整的企业管理解决方案。",
"author": {
"name": "xlsea",
@ -66,11 +66,11 @@
"@sa/tinymce": "workspace:*",
"@sa/utils": "workspace:*",
"@types/streamsaver": "^2.0.5",
"@vueuse/core": "13.5.0",
"@vueuse/core": "13.8.0",
"clipboard": "2.0.11",
"dayjs": "1.11.13",
"dayjs": "1.11.14",
"defu": "6.1.4",
"echarts": "5.6.0",
"echarts": "6.0.0",
"highlight.js": "^11.11.1",
"jsencrypt": "^3.3.2",
"json5": "2.2.3",
@ -80,46 +80,47 @@
"pinia": "3.0.3",
"streamsaver": "^2.0.6",
"tailwind-merge": "3.3.1",
"vue": "3.5.17",
"vue": "3.5.20",
"vue-advanced-cropper": "^2.8.9",
"vue-draggable-plus": "0.6.0",
"vue-i18n": "11.1.9",
"vue-router": "4.5.1"
"vue-i18n": "11.1.11",
"vue-router": "4.5.1",
"xlsx": "0.18.5"
},
"devDependencies": {
"@elegant-router/vue": "0.3.8",
"@iconify/json": "2.2.357",
"@iconify/json": "2.2.378",
"@sa/scripts": "workspace:*",
"@sa/uno-preset": "workspace:*",
"@soybeanjs/eslint-config": "1.7.1",
"@types/node": "24.0.13",
"@types/node": "24.3.0",
"@types/nprogress": "0.2.3",
"@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",
"@unocss/eslint-config": "66.4.2",
"@unocss/preset-icons": "66.4.2",
"@unocss/preset-uno": "66.4.2",
"@unocss/transformer-directives": "66.4.2",
"@unocss/transformer-variant-group": "66.4.2",
"@unocss/vite": "66.4.2",
"@vitejs/plugin-vue": "6.0.1",
"@vitejs/plugin-vue-jsx": "5.1.0",
"consola": "3.4.2",
"eslint": "9.31.0",
"eslint-plugin-vue": "10.3.0",
"eslint": "9.34.0",
"eslint-plugin-vue": "10.4.0",
"kolorist": "1.8.0",
"sass": "1.89.2",
"simple-git-hooks": "2.13.0",
"tsx": "4.20.3",
"typescript": "5.8.3",
"unplugin-icons": "22.1.0",
"unplugin-vue-components": "28.8.0",
"vite": "7.0.4",
"sass": "1.91.0",
"simple-git-hooks": "2.13.1",
"tsx": "4.20.5",
"typescript": "5.9.2",
"unplugin-icons": "22.2.0",
"unplugin-vue-components": "29.0.0",
"vite": "7.1.3",
"vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-progress": "0.0.7",
"vite-plugin-static-copy": "^3.1.0",
"vite-plugin-svg-icons": "2.0.1",
"vite-plugin-vue-devtools": "7.7.7",
"vite-plugin-vue-devtools": "8.0.1",
"vue-eslint-parser": "10.2.0",
"vue-tsc": "3.0.1"
"vue-tsc": "3.0.6"
},
"simple-git-hooks": {
"commit-msg": "pnpm sa git-commit-verify",

View File

@ -11,7 +11,7 @@
},
"dependencies": {
"@sa/utils": "workspace:*",
"axios": "1.10.0",
"axios": "1.11.0",
"axios-retry": "4.5.0",
"qs": "6.14.0"
},

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

@ -14,14 +14,14 @@
},
"devDependencies": {
"@soybeanjs/changelog": "0.3.24",
"bumpp": "10.2.0",
"c12": "3.0.4",
"bumpp": "10.2.3",
"c12": "3.2.0",
"cac": "6.7.14",
"consola": "3.4.2",
"enquirer": "2.4.1",
"execa": "9.6.0",
"kolorist": "1.8.0",
"npm-check-updates": "18.0.1",
"npm-check-updates": "18.0.3",
"rimraf": "6.0.1"
}
}

2421
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,7 @@
<script setup lang="ts">
import { computed, useAttrs } from 'vue';
import type { TagProps } from 'naive-ui';
import { jsonClone } from '@sa/utils';
import { useDict } from '@/hooks/business/dict';
import { isNotNull } from '@/utils/common';
import { $t } from '@/locales';
@ -28,7 +29,7 @@ const { transformDictData } = useDict(props.dictCode, props.immediate);
const dictTagData = computed<Api.System.DictData[]>(() => {
if (props.dictData) {
const dictData = props.dictData;
const dictData = jsonClone(props.dictData);
if (dictData.dictLabel?.startsWith(`dict.${dictData.dictType}.`)) {
dictData.dictLabel = $t(dictData.dictLabel as App.I18n.I18nKey);
}

View File

@ -2,6 +2,7 @@
import { computed } from 'vue';
import hljs from 'highlight.js/lib/core';
import json from 'highlight.js/lib/languages/json';
import { twMerge } from 'tailwind-merge';
hljs.registerLanguage('json', json);
@ -10,15 +11,19 @@ defineOptions({
});
interface Props {
class?: string;
code?: string;
showLineNumbers?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
class: '',
code: '',
showLineNumbers: false
});
const DEFAULT_CLASS = 'max-h-500px';
/** 格式化JSON数据 */
const jsonData = computed<string>(() => {
if (!props.code) return '';
@ -33,9 +38,9 @@ const jsonData = computed<string>(() => {
</script>
<template>
<div class="json-preview">
<NCode :code="jsonData" :hljs="hljs" language="json" :show-line-numbers="showLineNumbers" />
</div>
<NScrollbar :class="twMerge(DEFAULT_CLASS, props.class)">
<NCode :code="jsonData" :hljs="hljs" language="json" :show-line-numbers="showLineNumbers" :word-wrap="true" />
</NScrollbar>
</template>
<style lang="scss">
@ -44,18 +49,4 @@ html[class='dark'] {
background-color: #7c7777;
}
}
.json-preview {
width: 100%;
max-height: 500px;
overflow-y: auto;
@include scrollbar();
.empty-data {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
color: #999;
font-size: 14px;
}
}
</style>

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,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 class="bg-gray-50 p-2" :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

@ -1,61 +1,524 @@
<!-- Copyright By https://github.com/Daymychen/art-design-pro/blob/main/src/components/core/views/login/LoginLeftView.vue -->
<script lang="ts" setup>
import { computed } from 'vue';
import { getPaletteColorByNumber } from '@sa/color';
import { useThemeStore } from '@/store/modules/theme';
defineOptions({ name: 'WaveBg' });
interface Props {
/** Theme color */
themeColor: string;
const themeStore = useThemeStore();
function toggleThemeScheme() {
if (themeStore.darkMode) {
themeStore.setThemeScheme('light');
return;
}
themeStore.setThemeScheme('dark');
}
const props = defineProps<Props>();
const lightColor = computed(() => getPaletteColorByNumber(props.themeColor, 200));
const darkColor = computed(() => getPaletteColorByNumber(props.themeColor, 500));
</script>
<template>
<div class="absolute-lt z-1 size-full overflow-hidden">
<div class="absolute -right-300px -top-900px lt-sm:(-right-100px -top-1170px)">
<svg height="1337" width="1337">
<defs>
<path
id="path-1"
opacity="1"
fill-rule="evenodd"
d="M1337,668.5 C1337,1037.455193874239 1037.455193874239,1337 668.5,1337 C523.6725684305388,1337 337,1236 370.50000000000006,1094 C434.03835568300906,824.6732385973953 6.906089672974592e-14,892.6277623047779 0,668.5000000000001 C0,299.5448061257611 299.5448061257609,1.1368683772161603e-13 668.4999999999999,0 C1037.455193874239,0 1337,299.544806125761 1337,668.5Z"
/>
<linearGradient id="linearGradient-2" x1="0.79" y1="0.62" x2="0.21" y2="0.86">
<stop offset="0" :stop-color="lightColor" stop-opacity="1" />
<stop offset="1" :stop-color="darkColor" stop-opacity="1" />
</linearGradient>
</defs>
<g opacity="1">
<use xlink:href="#path-1" fill="url(#linearGradient-2)" fill-opacity="1" />
</g>
</svg>
<div class="wave-bg">
<!-- 几何装饰元素 -->
<div class="geometric-decorations">
<!-- 基础几何形状 -->
<div class="geo-element circle-outline animate-fade-in-up animate-delay-0s"></div>
<div class="geo-element square-rotated animate-fade-in-left animate-delay-0s"></div>
<div class="geo-element circle-small animate-fade-in-up animate-delay-0.3s"></div>
<div class="geo-element square-bottom-right animate-fade-in-right animate-delay-0s"></div>
<!-- 背景泡泡 -->
<div class="geo-element bg-bubble animate-scale-in animate-delay-0.5s"></div>
<!-- 太阳/月亮 -->
<div
class="geo-element circle-top-right animate-fade-in-down animate-delay-0.5s"
@click="toggleThemeScheme"
></div>
<!-- 装饰点 -->
<div class="geo-element dot dot-top-left animate-bounce-in animate-delay-0s"></div>
<div class="geo-element dot dot-top-right animate-bounce-in animate-delay-0s"></div>
<div class="geo-element dot dot-center-right animate-bounce-in animate-delay-0s"></div>
<!-- 叠加方块组 -->
<div class="squares-group">
<i class="geo-element square square-blue animate-fade-in-left-rotated-blue animate-delay-0.2s"></i>
<i class="geo-element square square-pink animate-fade-in-left-rotated-pink animate-delay-0.4s"></i>
<i class="geo-element square square-purple animate-fade-in-left-no-rotation animate-delay-0.6s"></i>
</div>
<div class="absolute -bottom-400px -left-200px lt-sm:(-bottom-760px -left-100px)">
<svg height="896" width="967.8852157128662">
<defs>
<path
id="path-2"
opacity="1"
fill-rule="evenodd"
d="M896,448 C1142.6325445712241,465.5747656464056 695.2579309733121,896 448,896 C200.74206902668806,896 5.684341886080802e-14,695.2579309733121 0,448.0000000000001 C0,200.74206902668806 200.74206902668791,5.684341886080802e-14 447.99999999999994,0 C695.2579309733121,0 475,418 896,448Z"
/>
<linearGradient id="linearGradient-3" x1="0.5" y1="0" x2="0.5" y2="1">
<stop offset="0" :stop-color="darkColor" stop-opacity="1" />
<stop offset="1" :stop-color="lightColor" stop-opacity="1" />
</linearGradient>
</defs>
<g opacity="1">
<use xlink:href="#path-2" fill="url(#linearGradient-3)" fill-opacity="1" />
</g>
</svg>
</div>
</div>
</template>
<style scoped></style>
<style lang="scss" scoped>
// 颜色变量定义
$primary-light-7: rgb(var(--primary-50-color));
$primary-light-8: rgb(var(--primary-100-color));
$primary-light-9: rgb(var(--primary-200-color));
$primary-base: rgb(var(--primary-color));
$main-bg: rgb(var(--primary-50-color));
// 混合颜色函数
$bg-mix-light-9: color-mix(in srgb, $primary-light-9 100%, $main-bg);
$bg-mix-light-8: color-mix(in srgb, $primary-light-8 80%, $main-bg);
$bg-mix-light-7: color-mix(in srgb, $primary-light-7 80%, $main-bg);
.wave-bg {
.geometric-decorations {
.geo-element {
position: absolute;
opacity: 0;
animation-fill-mode: forwards;
animation-duration: 0.8s;
animation-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
// 动画 mixin
@mixin fadeAnimation($direction: '', $rotation: 0deg) {
from {
opacity: 0;
@if $direction == 'up' {
transform: translateY(30px) rotate($rotation);
} @else if $direction == 'down' {
transform: translateY(-30px) rotate($rotation);
} @else if $direction == 'left' {
transform: translateX(-30px) rotate($rotation);
} @else if $direction == 'right' {
transform: translateX(30px) rotate($rotation);
}
}
to {
opacity: 1;
@if $direction == 'up' or $direction == 'down' {
transform: translateY(0) rotate($rotation);
} @else {
transform: translateX(0) rotate($rotation);
}
}
}
// 动画定义
@keyframes fadeInUp {
@include fadeAnimation('up');
}
@keyframes fadeInDown {
@include fadeAnimation('down');
}
@keyframes fadeInLeft {
@include fadeAnimation('left');
}
@keyframes fadeInLeftRotated {
@include fadeAnimation('left', -25deg);
}
@keyframes fadeInRight {
@include fadeAnimation('right');
}
@keyframes fadeInRightRotated {
@include fadeAnimation('right', 45deg);
}
@keyframes fadeInLeftRotatedBlue {
@include fadeAnimation('left', -10deg);
}
@keyframes fadeInLeftRotatedPink {
@include fadeAnimation('left', 10deg);
}
@keyframes fadeInLeftNoRotation {
@include fadeAnimation('left');
}
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.8);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes bounceIn {
0% {
opacity: 0;
transform: scale(0.3);
}
50% {
opacity: 1;
transform: scale(1.05);
}
70% {
transform: scale(0.9);
}
100% {
opacity: 1;
transform: scale(1);
}
}
@keyframes lineGrow {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideInLeft {
from {
opacity: 0;
transform: translateX(-30px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
// 动画类
.animate-fade-in-up {
animation-name: fadeInUp;
}
.animate-fade-in-down {
animation-name: fadeInDown;
}
.animate-fade-in-left {
animation-name: fadeInLeft;
}
.animate-fade-in-right {
animation-name: fadeInRight;
}
.animate-scale-in {
animation-name: scaleIn;
animation-duration: 1.2s;
}
.animate-bounce-in {
animation-name: bounceIn;
animation-duration: 0.6s;
}
.animate-fade-in-left-rotated-blue {
animation-name: fadeInLeftRotatedBlue;
}
.animate-fade-in-left-rotated-pink {
animation-name: fadeInLeftRotatedPink;
}
.animate-fade-in-left-no-rotation {
animation-name: fadeInLeftNoRotation;
}
// 基础几何形状
.circle-outline {
top: 10%;
left: 25%;
width: 42px;
height: 42px;
border: 2px solid $primary-light-8;
border-radius: 50%;
}
.square-rotated {
top: 50%;
left: 16%;
width: 60px;
height: 60px;
background-color: $bg-mix-light-8;
&.animate-fade-in-left {
animation-name: fadeInLeftRotated;
}
}
.circle-small {
bottom: 26%;
left: 30%;
width: 18px;
height: 18px;
background-color: $primary-light-8;
border-radius: 50%;
}
// 太阳/月亮效果
.circle-top-right {
top: 3%;
right: 3%;
z-index: 100;
width: 50px;
height: 50px;
cursor: pointer;
background: $bg-mix-light-7;
border-radius: 50%;
transition: all 0.3s;
&::after {
position: absolute;
top: 50%;
left: 50%;
width: 100%;
height: 100%;
content: '';
background: linear-gradient(to right, #fcbb04, #fffc00);
border-radius: 50%;
opacity: 0;
transition: all 0.5s;
transform: translate(-50%, -50%);
}
&:hover {
box-shadow: 0 0 36px #fffc00;
&::after {
opacity: 1;
}
}
}
.square-bottom-right {
right: 10%;
bottom: 10%;
width: 50px;
height: 50px;
background-color: $primary-light-8;
&.animate-fade-in-right {
animation-name: fadeInRightRotated;
}
}
// 背景泡泡
.bg-bubble {
top: -120px;
right: -120px;
width: 360px;
height: 360px;
background-color: $bg-mix-light-8;
border-radius: 50%;
}
// 装饰点
.dot {
width: 14px;
height: 14px;
background-color: $primary-light-7;
border-radius: 50%;
&.dot-top-left {
top: 140px;
left: 100px;
}
&.dot-top-right {
top: 140px;
right: 120px;
}
&.dot-center-right {
top: 46%;
right: 22%;
background-color: $primary-light-8;
}
}
// 叠加方块组
.squares-group {
position: absolute;
bottom: 18px;
left: 20px;
width: 140px;
height: 140px;
pointer-events: none;
.square {
position: absolute;
display: block;
border-radius: 8px;
box-shadow: 0 8px 24px rgb(64 87 167 / 12%);
&.square-blue {
top: 12px;
left: 30px;
z-index: 2;
width: 50px;
height: 50px;
background-color: rgb(from $primary-base r g b / 30%);
}
&.square-pink {
top: 30px;
left: 48px;
z-index: 1;
width: 70px;
height: 70px;
background-color: rgb(from $primary-base r g b / 15%);
}
&.square-purple {
top: 66px;
left: 86px;
z-index: 3;
width: 32px;
height: 32px;
background-color: rgb(from $primary-base r g b / 45%);
}
}
// 装饰线条
&::after {
position: absolute;
top: 86px;
left: 72px;
width: 80px;
height: 1px;
content: '';
background: linear-gradient(90deg, var(--el-color-primary-light-6), transparent);
opacity: 0;
transform: rotate(50deg);
animation: lineGrow 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
animation-delay: 1.2s;
}
}
}
@media only screen and (max-width: 1600px) {
width: 60vw;
.text-wrap {
bottom: 40px;
}
}
@media only screen and (max-width: 1280px) {
width: auto;
height: auto;
padding: 0;
// 隐藏背景和其他内容,只保留 logo
background: transparent;
.left-img,
.text-wrap,
.geometric-decorations {
display: none;
}
.logo {
position: fixed;
top: 15px;
left: 25px;
z-index: 1000;
}
}
}
// 暗色主题
.dark .wave-bg {
background-color: color-mix(in srgb, $primary-light-9 60%, #070707);
@media only screen and (max-width: 1280px) {
background: transparent;
}
.geometric-decorations {
// 月亮效果
.circle-top-right {
background-color: $bg-mix-light-8;
box-shadow: 0 0 25px #333 inset;
transition: all 0.3s ease-in-out 0.1s;
rotate: -48deg;
&::before {
position: absolute;
top: 0;
left: 15px;
width: 50px;
height: 50px;
content: '';
background-color: $bg-mix-light-9;
border-radius: 50%;
transition: all 0.3s ease-in-out;
}
&:hover {
background-color: transparent;
box-shadow: 0 40px 25px #ddd inset;
&::before {
left: 18px;
}
&::after {
opacity: 0;
}
}
}
.bg-bubble {
background-color: $bg-mix-light-9;
}
// 其他元素颜色调整
.square-rotated {
background-color: $bg-mix-light-9;
}
.circle-small,
.dot {
background-color: $primary-light-8;
}
.square-bottom-right {
background-color: $primary-light-9;
}
.dot.dot-top-right {
background-color: $primary-light-8;
}
}
// 方块组暗色调整
.squares-group {
.square {
box-shadow: none;
&.square-blue {
background-color: rgb(from $primary-base r g b / 18%);
}
&.square-pink {
background-color: rgb(from $primary-base r g b / 10%);
}
&.square-purple {
background-color: rgb(from $primary-base r g b / 20%);
}
}
&::after {
background: linear-gradient(90deg, $primary-light-8, transparent);
}
}
}
</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 class="h-full">
<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,62 @@
<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 expandedKeys = defineModel<CommonType.IdType[]>('expandedKeys', { 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;
// 设置默认展开的节点
if (data?.length && !expandedKeys.value.length) {
expandedKeys.value = [data[0].id];
}
endLoading();
}
getCategoryList();
</script>
<template>
<NTreeSelect
v-model:value="strValue"
v-model:expanded-keys="expandedKeys"
filterable
class="h-full"
:loading="loading"
key-field="id"
label-field="label"
:options="options as []"
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,222 @@
<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} (${taskInfo?.flowCode})`"
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" :loading="loading" type="primary" @click="openTransferModal">转办</NButton>
<NButton
v-if="isWaiting && isTicketOrSignInstance"
:loading="loading"
type="primary"
@click="openAddSignatureModal"
>
加签
</NButton>
<NButton
v-if="isWaiting && isTicketOrSignInstance"
:loading="loading"
type="primary"
@click="openReduceSignatureModal"
>
减签
</NButton>
<NButton v-if="isWaiting" :loading="loading" 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,95 @@
<script lang="ts" setup>
import { reactive, watch } from 'vue';
import { messageTypeOptions } from '@/constants/workflow';
import { fetchTaskUrge } from '@/service/api/workflow';
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
defineOptions({
name: 'FlowUrgeModal'
});
const { createRequiredRule } = useFormRules();
const { formRef, validate } = useNaiveForm();
interface Props {
taskIds: CommonType.IdType[];
}
const props = defineProps<Props>();
interface Emits {
(e: 'submit'): void;
}
const emit = defineEmits<Emits>();
const visible = defineModel<boolean>('visible', {
default: false
});
type Model = Api.Workflow.TaskUrgeOperateParams;
const model = reactive(createDefaultModel());
function createDefaultModel(): Model {
return {
taskIdList: props.taskIds,
messageType: ['1'],
message: ''
};
}
type RuleKey = Extract<keyof Model, 'taskIdList' | 'messageType' | 'message'>;
const rules: Record<RuleKey, App.Global.FormRule> = {
taskIdList: createRequiredRule('任务ID不能为空'),
messageType: createRequiredRule('消息提醒不能为空'),
message: createRequiredRule('消息内容不能为空')
};
function closeDrawer() {
visible.value = false;
}
watch(visible, () => {
if (visible.value) {
Object.assign(model, createDefaultModel());
}
});
async function handleSubmit() {
await validate();
const { error } = await fetchTaskUrge(model);
if (error) return;
window.$message?.success('催办成功');
closeDrawer();
emit('submit');
}
</script>
<template>
<NModal v-model:show="visible" preset="card" class="w-800px" title="催办" :native-scrollbar="false" closable>
<NForm ref="formRef" :model="model" :rules="rules">
<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="message">
<NInput v-model:value="model.message" type="textarea" />
</NFormItem>
</NForm>
<template #footer>
<NSpace justify="end" :size="16">
<NButton @click="closeDrawer">{{ $t('common.cancel') }}</NButton>
<NButton type="primary" @click="handleSubmit">{{ $t('common.confirm') }}</NButton>
</NSpace>
</template>
</NModal>
</template>

View File

@ -0,0 +1,317 @@
<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,
flowInstanceBizExtBo: 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;
startWorkflowModel.flowInstanceBizExtBo = {
businessCode: respLeave.value?.applyCode,
businessTitle: '请假申请'
};
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>

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

@ -0,0 +1,90 @@
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);
/** definition designer mode */
export const definitionDesignerModeRecord: Record<Api.Workflow.DefinitionDesignerMode, string> = {
CLASSICS: '经典模式',
MIMIC: '仿钉钉模式'
};
export const definitionDesignerModeOptions = transformRecordToOption(definitionDesignerModeRecord);
/** activity status */
export const workflowActivityStatusRecord: Record<Api.Workflow.WorkflowActivityStatus, string> = {
0: '挂起',
1: '激活'
};
export const workflowActivityStatusOptions = transformRecordToOption(workflowActivityStatusRecord);

View File

@ -113,6 +113,10 @@ export function useDownload() {
const response = await fetch(fullUrl, requestOptions);
if (response.status !== 200) {
throw new Error(errorCodeRecord.default);
}
await handleResponse(response);
const rawHeader = response.headers.get('Download-Filename');

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

@ -33,7 +33,7 @@ const toGitee = () => {
</NBadge>
</NButton>
</template>
消息
{{ $t('page.home.message') }}
</NTooltip>
</template>
<NCard

View File

@ -231,10 +231,23 @@ 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',
@ -328,6 +341,8 @@ const local: App.I18n.Schema = {
page: {
login: {
common: {
title: 'Modern enterprise-level multi-tenant management system',
subTitle: 'Provides developers with a complete enterprise management solution',
loginOrRegister: 'Login / Register',
register: 'Register',
userNamePlaceholder: 'Please enter user name',

View File

@ -231,10 +231,23 @@ 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: '租户管理',
@ -328,6 +341,8 @@ const local: App.I18n.Schema = {
page: {
login: {
common: {
title: '现代化的企业级多租户管理系统',
subTitle: '为开发者提供了完整的企业管理解决方案',
loginOrRegister: '登录 / 注册',
register: '注册',
userNamePlaceholder: '请输入用户名',

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

@ -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,78 @@
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
});
}
/** 更新流程变量信息 */
export function fetchUpdateInstanceVariable(data: Api.Workflow.InstanceVariableOperateParams) {
return request<boolean>({
url: '/workflow/instance/updateVariable',
method: 'put',
data
});
}
/** 获取流程变量信息 */
export function fetchGetInstanceVariable(instanceId: CommonType.IdType) {
return request<Api.Workflow.InstanceVariableInfo>({
url: `/workflow/instance/instanceVariable/${instanceId}`,
method: 'get'
});
}

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,142 @@
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
});
}
/** 修改办理人 */
export function fetchTaskAssignee(taskIds: CommonType.IdType[], userId: CommonType.IdType) {
return request<boolean>({
url: `/workflow/task/updateAssignee/${userId}`,
method: 'put',
data: taskIds
});
}
/** 任务催办 */
export function fetchTaskUrge(data: Api.Workflow.TaskUrgeOperateParams) {
return request<boolean>({
url: '/workflow/task/urgeTask',
method: 'post',
data
});
}

View File

@ -8,7 +8,7 @@ import { decrypt, encrypt } from '@/utils/jsencrypt';
import { getAuthorization, handleExpiredRequest, showErrorMsg } from './shared';
import type { RequestInstanceState } from './type';
const encryptHeader = 'encrypt-key';
const encryptHeader = import.meta.env.VITE_HEADER_FLAG || 'encrypt-key';
const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y';
const { baseURL } = getServiceBaseURL(import.meta.env, isHttpProxy);
@ -48,6 +48,14 @@ export const request = createFlatRequest<App.Service.Response, RequestInstanceSt
isBackendSuccess(response) {
// when the backend response code is "0000"(default), it means the request is success
// to change this logic by yourself, you can modify the `VITE_SERVICE_SUCCESS_CODE` in `.env` file
if (import.meta.env.VITE_APP_ENCRYPT === 'Y' && response.headers[encryptHeader]) {
const keyStr = response.headers[encryptHeader];
const data = String(response.data);
const base64Str = decrypt(keyStr);
const aesKey = decryptBase64(base64Str.toString());
const decryptData = decryptWithAes(data, aesKey);
response.data = JSON.parse(decryptData);
}
return String(response.data.code) === import.meta.env.VITE_SERVICE_SUCCESS_CODE;
},
async onBackendFail(response, instance) {
@ -125,23 +133,6 @@ export const request = createFlatRequest<App.Service.Response, RequestInstanceSt
return null;
},
transformBackendResponse(response) {
if (import.meta.env.VITE_APP_ENCRYPT === 'Y') {
// 加密后的 AES 秘钥
const keyStr = response.headers[encryptHeader];
// 加密
if (keyStr && keyStr !== '') {
const data = String(response.data);
// 请求体 AES 解密
const base64Str = decrypt(keyStr);
// base64 解码 得到请求头的 AES 秘钥
const aesKey = decryptBase64(base64Str.toString());
// aesKey 解码 data
const decryptData = decryptWithAes(data, aesKey);
// 将结果 (得到的是 JSON 字符串) 转为 JSON
response.data = JSON.parse(decryptData);
}
}
// 二进制数据则直接返回
if (response.request.responseType === 'blob' || response.request.responseType === 'arraybuffer') {
return response.data;

View File

@ -127,7 +127,7 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
}
// @ts-expect-error no hidden field
route.meta.hideInMenu = route.hidden;
if (route.meta.hideInMenu && parent) {
if (route.meta.hideInMenu && parent && !route.meta.activeMenu) {
// @ts-expect-error parent.name is activeMenu type
route.meta.activeMenu = parent.name;
}

View File

@ -6,6 +6,7 @@ html,
body,
#app {
height: 100%;
font-family: inherit;
}
html {

View File

@ -44,7 +44,7 @@ export const themeSettings: App.Theme.ThemeSetting = {
fixedHeaderAndTab: true,
sider: {
inverted: false,
width: 220,
width: 230,
collapsedWidth: 64,
mixWidth: 90,
mixCollapsedWidth: 64,

View File

@ -128,6 +128,7 @@ declare namespace Api {
type UserSearchParams = CommonType.RecordNullable<
Pick<User, 'deptId' | 'userName' | 'nickName' | 'phonenumber' | 'status'> & {
roleId: CommonType.IdType;
userIds: string;
} & Common.CommonSearchParams
>;

595
src/typings/api/workflow.api.d.ts vendored Normal file
View File

@ -0,0 +1,595 @@
/**
* Namespace Api
*
* All backend api type
*/
declare namespace Api {
/**
* namespace Workflow
*
* backend api module: "Workflow"
*/
namespace Workflow {
/** 业务流程状态 */
type BusinessStatus = 'cancel' | 'draft' | 'waiting' | 'finish' | 'invalid' | 'back' | 'termination';
/** 流程类型 */
type FlowCodeType = 'leave1' | 'leave2' | 'leave3' | 'leave4' | 'leave5' | 'leave6';
/** 请假状态 */
type LeaveType = '1' | '2' | '3' | '4';
/** leave */
type Leave = Common.CommonRecord<{
/** id */
id: CommonType.IdType;
/** 申请编码 */
applyCode: string;
/** 请假类型 */
leaveType: LeaveType;
/** 开始时间 */
startDate: string;
/** 结束时间 */
endDate: string;
/** 请假天数 */
leaveDays: number;
/** 请假原因 */
remark: string;
/** 状态 */
status: BusinessStatus;
}>;
/** leave search params */
type LeaveSearchParams = CommonType.RecordNullable<
{ startLeaveDays: number; endLeaveDays: number } & Api.Common.CommonSearchParams
>;
/** leave operate params */
type LeaveOperateParams = CommonType.RecordNullable<
Pick<Api.Workflow.Leave, 'id' | 'leaveType' | 'startDate' | 'endDate' | 'leaveDays' | 'remark'>
>;
/** leave detail */
type LeaveDetail = CommonType.RecordNullable<
Pick<Api.Workflow.Leave, 'id' | 'leaveType' | 'startDate' | 'endDate' | 'leaveDays' | 'remark' | 'status'>
>;
/** leave list */
type LeaveList = Api.Common.PaginatingQueryRecord<Leave>;
/** 工作流分类 */
type WorkflowCategory = Common.CommonRecord<{
/** 主键 */
categoryId: CommonType.IdType;
/** 租户编号 */
tenantId: CommonType.IdType;
/** 分类名称 */
categoryName: string;
/** 父级ID */
parentId: CommonType.IdType;
/** 祖级列表 */
ancestors: string;
/** 排序号 */
orderNum: number;
/** 删除标志 */
delFlag: number;
}>;
/** 工作流分类搜索参数 */
type WorkflowCategorySearchParams = CommonType.RecordNullable<
Pick<WorkflowCategory, 'categoryName'> & Api.Common.CommonSearchParams
>;
/** 工作流分类操作参数 */
type WorkflowCategoryOperateParams = CommonType.RecordNullable<
Pick<WorkflowCategory, 'categoryId' | 'categoryName' | 'parentId' | 'orderNum'>
>;
/** 工作流分类列表 */
type WorkflowCategoryList = WorkflowCategory[];
/** spel */
type Spel = Common.CommonRecord<{
/** 主键id */
id: CommonType.IdType;
/** 组件名称 */
componentName: string;
/** 方法名 */
methodName: string;
/** 参数 */
methodParams: string;
/** spel表达式 */
viewSpel: string;
/** 备注 */
remark: string;
/** 状态 */
status: string;
/** 删除标志 */
delFlag: string;
}>;
/** spel search params */
type SpelSearchParams = CommonType.RecordNullable<
Pick<Api.Workflow.Spel, 'componentName' | 'methodName' | 'status'> & Api.Common.CommonSearchParams
>;
/** spel operate params */
type SpelOperateParams = CommonType.RecordNullable<
Pick<Api.Workflow.Spel, 'id' | 'componentName' | 'methodName' | 'methodParams' | 'viewSpel' | 'remark' | 'status'>
>;
/** spel list */
type SpelList = Api.Common.PaginatingQueryRecord<Spel>;
/** 工作流发布状态 */
type WorkflowPublishStatus = 0 | 1 | 9;
/** 设计器模式 */
type DefinitionDesignerMode = 'CLASSICS' | 'MIMIC';
/** definition */
type Definition = Common.CommonTenantRecord<{
/** 主键id */
id: CommonType.IdType;
/** 流程编码 */
flowCode: string;
/** 流程名称 */
flowName: string;
/** 流程类别 */
category: string;
/** 流程分类名称 */
categoryName: string;
/** 流程版本 */
version: string;
/** 是否发布0未发布 1已发布 9失效 */
isPublish: WorkflowPublishStatus;
/** 审批表单是否自定义Y是 N否 */
formCustom: Api.Common.YesOrNoStatus;
/** 审批表单路径 */
formPath: string;
/** 流程激活状态0挂起 1激活 */
activityStatus: WorkflowActivityStatus;
/** 监听器类型 */
listenerType: string;
/** 监听器路径 */
listenerPath: string;
/** 业务详情 存业务表对象json字符串 */
ext: string;
/** 设计器模式 */
modelValue: DefinitionDesignerMode;
/** 删除标志 */
delFlag: string;
}>;
/** definition search params */
type DefinitionSearchParams = CommonType.RecordNullable<
Pick<Api.Workflow.Definition, 'flowCode' | 'flowName' | 'category'> & Api.Common.CommonSearchParams
>;
/** definition operate params */
type DefinitionOperateParams = CommonType.RecordNullable<
Pick<
Api.Workflow.Definition,
'id' | 'flowCode' | 'flowName' | 'category' | 'formPath' | 'formCustom' | 'modelValue' | 'ext'
>
>;
/** definition list */
type DefinitionList = Api.Common.PaginatingQueryRecord<Definition>;
type InstanceVariable = CommonType.RecordNullable<{
key: string;
value: string;
}>;
type InstanceVariableOperateParams = CommonType.RecordNullable<{
instanceId: CommonType.IdType;
}> &
InstanceVariable;
type InstanceVariableInfo = CommonType.RecordNullable<{
/** 键 */
variable: string;
/** 值 */
variableList: InstanceVariable[];
}>;
/** 节点类型 */
type WorkflowNodeType = 0 | 1 | 2 | 3 | 4;
/** 流程激活状态 */
type WorkflowActivityStatus = 0 | 1;
/** 流程实例 */
type Instance = Common.CommonRecord<{
/** 主键 */
id: CommonType.IdType;
/** 租户编号 */
tenantId: CommonType.IdType;
/** 分类ID */
category: CommonType.IdType;
/** 分类名称 */
categoryName: string;
/** 流程定义ID */
definitionId: CommonType.IdType;
/** 流程定义名称 */
flowName: string;
/** 流程定义编码 */
flowCode: string;
/** 业务ID */
businessId: CommonType.IdType;
/** 业务编码 */
businessCode: string;
/** 业务名称 */
businessTitle: string;
/** 节点类型 */
nodeType: WorkflowNodeType;
/** 节点编码 */
nodeCode: string;
/** 节点名称 */
nodeName: string;
/** 变量 */
variable: string;
/** 流程状态 */
flowStatus: string;
/** 流程状态名称 */
flowStatusName: string;
/** 流程激活状态 */
activityStatus: WorkflowActivityStatus;
/** 审批表单是否自定义 */
formCustom: Api.Common.YesOrNoStatus;
/** 审批表单路径 */
formPath: string;
/** 扩展字段 */
ext: string;
/** 流程定义版本 */
version: string;
/** 创建者名称 */
createByName: string;
/** 删除标志 */
delFlag: number;
}>;
/** 流程实例搜索参数 */
type InstanceSearchParams = CommonType.RecordNullable<
Pick<Instance, 'flowName' | 'flowCode' | 'businessId' | 'category' | 'nodeName'> &
Api.Common.CommonSearchParams & {
startUserId: CommonType.IdType;
createByIds: CommonType.IdType[];
}
>;
/** 流程实例列表 */
type InstanceList = Common.PaginatingQueryRecord<Instance>;
/** 流程作废操作参数 */
type FlowInvalidOperateParams = CommonType.RecordNullable<{
/** 主键 */
id: CommonType.IdType;
/** 作废原因 */
comment: string;
}>;
/** 流程撤销操作参数 */
type CancelProcessApplyParams = CommonType.RecordNullable<{
/** 主键 */
businessId: CommonType.IdType;
/** 撤销原因 */
message: string;
}>;
type BusinessInfo = CommonType.RecordNullable<{
/** 业务编码 */
businessCode: string;
/** 业务名称 */
businessTitle: string;
}>;
/** 启动流程操作参数 */
type StartWorkflowOperateParams = CommonType.RecordNullable<{
/** 流程定义ID */
flowCode: string;
/** 业务ID */
businessId: CommonType.IdType;
/** 业务信息 */
flowInstanceBizExtBo: BusinessInfo;
/** 变量 */
variables: { [key: string]: any };
}>;
/** 启动流程结果 */
type StartWorkflowResult = CommonType.RecordNullable<{
/** 流程实例ID */
instanceId: CommonType.IdType;
/** 任务ID */
taskId: CommonType.IdType;
}>;
/** 抄送人 */
type FlowCopy = CommonType.RecordNullable<{
/** 用户ID */
userId: CommonType.IdType;
/** 用户名称 */
userName: string;
}>;
/** 按钮权限 */
type ButtonPermission = CommonType.RecordNullable<{
/** 唯一编码 */
code: CommonType.IdType;
/** 选项值 */
value: string;
/** 是否显示 */
show: boolean;
}>;
type TaskOrHisTask = Task | HisTask;
/** 任务详情 */
type Task = Common.CommonTenantRecord<{
/** 任务ID */
id: CommonType.IdType;
/** 删除标志 */
delFlag: number;
/** 流程定义ID */
definitionId: CommonType.IdType;
/** 流程实例ID */
instanceId: CommonType.IdType;
/** 业务ID */
businessId: CommonType.IdType;
/** 节点编码 */
nodeCode: string;
/** 节点类型 */
nodeType: WorkflowNodeType;
/** 权限列表 */
permissionList: string[];
/** 用户列表 */
userList: any[];
/** 审批表单是否自定义 */
formCustom: Api.Common.YesOrNoStatus;
/** 审批表单路径 */
formPath: string;
/** 流程状态 */
flowStatus: string;
/** 流程状态名称 */
flowStatusName: string;
/** 分类ID */
category: CommonType.IdType;
/** 分类名称 */
categoryName: string;
/** 办理人类型 */
type: string;
/** 审批人 */
assigneeIds: string;
/** 审批人名称 */
assigneeNames: string;
/** 审批人 */
processedBy: string;
/** 审批人名称 */
processedByName: string;
/** 流程签署比例值 大于0为票签会签 */
nodeRatio: string;
/** 创建人名称 */
createByName: string;
/** 是否为申请人节点 */
applyNode: string;
/** 按钮列表 */
buttonList: ButtonPermission[];
/** 节点名称 */
nodeName: string;
/** 流程定义名称 */
flowName: string;
/** 流程定义编码 */
flowCode: string;
/** 流程版本号 */
version: string;
/** 业务编码 */
businessCode: string;
/** 业务名称 */
businessTitle: string;
}>;
/** 任务列表 */
type TaskList = Common.PaginatingQueryRecord<Task>;
/** 任务催办操作参数 */
type TaskUrgeOperateParams = CommonType.RecordNullable<{
taskIdList: CommonType.IdType[];
messageType: MessageType[];
message: string;
}>;
/** 任务操作类型 */
type TaskOperateType = 'delegateTask' | 'transferTask' | 'addSignature' | 'reductionSignature' | 'stopTask';
/** 任务操作参数 */
type TaskOperateParams = CommonType.RecordNullable<{
taskId: CommonType.IdType;
userId?: CommonType.IdType;
userIds?: CommonType.IdType[];
message?: string;
}>;
/** 终止任务 */
type TerminateTaskOperateParams = CommonType.RecordNullable<{
taskId: CommonType.IdType;
comment?: string;
}>;
/** 协作方式 */
type CooperateType = 1 | 2 | 3 | 4 | 5 | 6 | 7;
/** 历史任务 */
type HisTask = Common.CommonTenantRecord<{
/** 任务ID */
id: CommonType.IdType;
/** 删除标志 */
delFlag: number;
/** 流程定义ID */
definitionId: CommonType.IdType;
/** 流程定义名称 */
flowName: string;
/** 流程实例ID */
instanceId: CommonType.IdType;
/** 任务表ID */
taskId: CommonType.IdType;
/** 协作方式1审批 2转办 3委派 4会签 5票签 6加签 7减签 */
cooperateType: CooperateType;
/** 协作方式名称 */
cooperateTypeName: string;
/** 业务ID */
businessId: string;
/** 节点编码 */
nodeCode: string;
/** 节点名称 */
nodeName: string;
/** 节点类型0开始节点 1中间节点 2结束节点 3互斥网关 4并行网关 */
nodeType: WorkflowNodeType;
/** 目标节点编码 */
targetNodeCode: string;
/** 目标节点名称 */
targetNodeName: string;
/** 审批者 */
approver: string;
/** 审批者名称 */
approveName: string;
/** 协作人 */
collaborator: string;
/** 权限标识 */
permissionList: string[];
/** 跳转类型PASS通过 REJECT退回 NONE无动作 */
skipType: string;
/** 流程状态 */
flowStatus: string;
/** 任务状态 */
flowTaskStatus: string;
/** 流程状态名称 */
flowStatusName: string;
/** 审批意见 */
message: string;
/** 业务扩展信息JSON字符串 */
ext: string;
/** 创建者姓名(申请人名称) */
createByName: string;
/** 流程分类ID */
category: string;
/** 流程分类名称 */
categoryName: string;
/** 审批表单是否自定义Y是 N否 */
formCustom: Api.Common.YesOrNoStatus;
/** 表单路径 */
formPath: string;
/** 流程定义编码 */
flowCode: string;
/** 流程版本号 */
version: string;
/** 运行时长 */
runDuration: string;
/** 附件 */
attachmentList: Api.System.Oss[];
/** 业务编码 */
businessCode: string;
/** 业务名称 */
businessTitle: string;
}>;
/** 历史任务列表 */
type HisTaskList = Common.PaginatingQueryRecord<HisTask>;
/** 流程实例ID与历史任务 */
type InstanceIdWithHisTask = Common.CommonRecord<{
/** 流程实例ID */
instanceId: CommonType.IdType;
/** 历史任务 */
list: HisTask[];
}>;
/** 任务搜索参数 */
type TaskSearchParams = CommonType.RecordNullable<
Pick<Task, 'flowName' | 'flowCode' | 'businessId' | 'category' | 'nodeName' | 'instanceId' | 'permissionList'> &
Api.Common.CommonSearchParams & {
createByIds: CommonType.IdType[];
}
>;
type TaskNextNodeSearchParams = CommonType.RecordNullable<{
taskId: CommonType.IdType;
taskVariables: { [key: string]: any };
}>;
/** 消息类型 */
type MessageType = '1' | '2' | '3';
/** 完成任务操作参数 */
type CompleteTaskOperateParams = CommonType.RecordNullable<{
/** 任务ID */
taskId: CommonType.IdType;
/** 文件ID */
fileId: CommonType.IdType;
/** 抄送人 */
flowCopyList: FlowCopy[];
/** 消息类型 */
messageType: string[];
/** 消息 */
message: string;
/** 通知 */
notice: string;
/** 任务变量 */
taskVariables: { [key: string]: any };
/** 变量 */
variables: { [key: string]: any };
/** 审批人 */
assigneeMap: { [key: string]: string };
/** 扩展字段 */
ext: string;
}>;
/** 工作流节点 */
type FlowNode = Common.CommonTenantRecord<{
/** 节点ID */
id: CommonType.IdType;
/** 删除标志 */
delFlag: string;
/** 节点类型0开始节点 1中间节点 2结束节点 3互斥网关 4并行网关 */
nodeType: WorkflowNodeType;
/** 流程定义ID */
definitionId: CommonType.IdType;
/** 节点编码 */
nodeCode: string;
/** 节点名称 */
nodeName: string;
/** 权限标识 */
permissionFlag: string;
/** 流程签署比例值 */
nodeRatio: string;
/** 节点坐标 */
coordinate: string;
/** 流程版本号 */
version: string;
/** 是否允许任意节点跳转 */
anyNodeSkip: string;
/** 监听器类型 */
listenerType: string;
/** 监听器路径 */
listenerPath: string;
/** 处理器类型 */
handlerType: string;
/** 处理器路径 */
handlerPath: string;
/** 审批表单是否自定义Y是 N否 */
formCustom: Api.Common.YesOrNoStatus;
/** 审批表单路径 */
formPath: string;
/** 扩展字段 */
ext: string;
}>;
/** 工作流节点列表 */
type FlowNodeList = FlowNode[];
/** 驳回操作参数 */
type BackOperateParams = CommonType.RecordNullable<{
taskId: CommonType.IdType;
fileId: CommonType.IdType;
messageType: string[];
nodeCode: string;
message: string;
notice: string;
variables: { [key: string]: any };
}>;
}
}

View File

@ -487,6 +487,8 @@ declare namespace App {
};
login: {
common: {
title: string;
subTitle: string;
loginOrRegister: string;
register: string;
userNamePlaceholder: string;

View File

@ -43,4 +43,6 @@ declare namespace CommonType {
/** filter function */
filterFn?: (node: any) => boolean;
};
/** the type of workflow table operate */
type WorkflowTableOperateType = 'add' | 'edit' | 'detail' | 'approval';
}

View File

@ -9,6 +9,8 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
AppProvider: typeof import('./../components/common/app-provider.vue')['default']
ApprovalInfoPanel: typeof import('./../components/workflow/approval-info-panel.vue')['default']
BackTaskModal: typeof import('./../components/workflow/back-task-modal.vue')['default']
BetterScroll: typeof import('./../components/custom/better-scroll.vue')['default']
BooleanTag: typeof import('./../components/custom/boolean-tag.vue')['default']
ButtonIcon: typeof import('./../components/custom/button-icon.vue')['default']
@ -22,6 +24,12 @@ declare module 'vue' {
DictTag: typeof import('./../components/custom/dict-tag.vue')['default']
ExceptionBase: typeof import('./../components/common/exception-base.vue')['default']
FileUpload: typeof import('./../components/custom/file-upload.vue')['default']
FlowCategorySelect: typeof import('./../components/workflow/flow-category-select.vue')['default']
FlowDrawer: typeof import('./../components/workflow/flow-drawer.vue')['default']
FlowInterveneModal: typeof import('./../components/workflow/flow-intervene-modal.vue')['default']
FlowPreview: typeof import('./../components/workflow/flow-preview.vue')['default']
FlowTaskApprovalModal: typeof import('./../components/workflow/flow-task-approval-modal.vue')['default']
FlowUrgeModal: typeof import('./../components/workflow/flow-urge-modal.vue')['default']
FormTip: typeof import('./../components/custom/form-tip.vue')['default']
FullScreen: typeof import('./../components/common/full-screen.vue')['default']
IconAntDesignEnterOutlined: typeof import('~icons/ant-design/enter-outlined')['default']
@ -38,14 +46,11 @@ declare module 'vue' {
'IconMaterialSymbols:add': typeof import('~icons/material-symbols/add')['default']
'IconMaterialSymbols:deleteOutline': typeof import('~icons/material-symbols/delete-outline')['default']
'IconMaterialSymbols:downloadRounded': typeof import('~icons/material-symbols/download-rounded')['default']
'IconMaterialSymbols:driveFileRenameOutlineOutline': typeof import('~icons/material-symbols/drive-file-rename-outline-outline')['default']
'IconMaterialSymbols:imageOutline': typeof import('~icons/material-symbols/image-outline')['default']
'IconMaterialSymbols:refreshRounded': typeof import('~icons/material-symbols/refresh-rounded')['default']
'IconMaterialSymbols:syncOutline': typeof import('~icons/material-symbols/sync-outline')['default']
'IconMaterialSymbols:uploadRounded': typeof import('~icons/material-symbols/upload-rounded')['default']
'IconMaterialSymbols:warningOutlineRounded': typeof import('~icons/material-symbols/warning-outline-rounded')['default']
IconMaterialSymbolsAddRounded: typeof import('~icons/material-symbols/add-rounded')['default']
IconMaterialSymbolsDeleteOutline: typeof import('~icons/material-symbols/delete-outline')['default']
IconMaterialSymbolsDriveFileRenameOutlineOutline: typeof import('~icons/material-symbols/drive-file-rename-outline-outline')['default']
'IconMdi:github': typeof import('~icons/mdi/github')['default']
IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin')['default']
IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin')['default']
@ -58,6 +63,7 @@ declare module 'vue' {
IconUilSearch: typeof import('~icons/uil/search')['default']
JsonPreview: typeof import('./../components/custom/json-preview.vue')['default']
LangSwitch: typeof import('./../components/common/lang-switch.vue')['default']
LeaveEdit: typeof import('./../components/workflow/form/leave-edit/index.vue')['default']
LookForward: typeof import('./../components/custom/look-forward.vue')['default']
MenuToggler: typeof import('./../components/common/menu-toggler.vue')['default']
MenuTree: typeof import('./../components/custom/menu-tree.vue')['default']
@ -72,6 +78,7 @@ declare module 'vue' {
NButton: typeof import('naive-ui')['NButton']
NCard: typeof import('naive-ui')['NCard']
NCheckbox: typeof import('naive-ui')['NCheckbox']
NCheckboxGroup: typeof import('naive-ui')['NCheckboxGroup']
NCode: typeof import('naive-ui')['NCode']
NCollapse: typeof import('naive-ui')['NCollapse']
NCollapseItem: typeof import('naive-ui')['NCollapseItem']
@ -85,7 +92,6 @@ declare module 'vue' {
NDrawer: typeof import('naive-ui')['NDrawer']
NDrawerContent: typeof import('naive-ui')['NDrawerContent']
NDropdown: typeof import('naive-ui')['NDropdown']
NDynamicInput: typeof import('naive-ui')['NDynamicInput']
NEllipsis: typeof import('naive-ui')['NEllipsis']
NEmpty: typeof import('naive-ui')['NEmpty']
NForm: typeof import('naive-ui')['NForm']
@ -96,7 +102,6 @@ declare module 'vue' {
NGridItem: typeof import('naive-ui')['NGridItem']
NInput: typeof import('naive-ui')['NInput']
NInputGroup: typeof import('naive-ui')['NInputGroup']
NInputGroupLabel: typeof import('naive-ui')['NInputGroupLabel']
NInputNumber: typeof import('naive-ui')['NInputNumber']
NLayout: typeof import('naive-ui')['NLayout']
NLayoutContent: typeof import('naive-ui')['NLayoutContent']
@ -135,6 +140,7 @@ declare module 'vue' {
OssUpload: typeof import('./../components/custom/oss-upload.vue')['default']
PinToggler: typeof import('./../components/common/pin-toggler.vue')['default']
PostSelect: typeof import('./../components/custom/post-select.vue')['default']
ReduceSignatureModal: typeof import('./../components/workflow/reduce-signature-modal.vue')['default']
ReloadButton: typeof import('./../components/common/reload-button.vue')['default']
RoleSelect: typeof import('./../components/custom/role-select.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
@ -147,10 +153,12 @@ declare module 'vue' {
TableHeaderOperation: typeof import('./../components/advanced/table-header-operation.vue')['default']
TableRowCheckAlert: typeof import('./../components/advanced/table-row-check-alert.vue')['default']
TableSiderLayout: typeof import('./../components/advanced/table-sider-layout.vue')['default']
TagGroup: typeof import('./../components/custom/tag-group.vue')['default']
TenantSelect: typeof import('./../components/custom/tenant-select.vue')['default']
ThemeSchemaSwitch: typeof import('./../components/common/theme-schema-switch.vue')['default']
TinymceEditor: typeof import('./../components/custom/tinymce-editor.vue')['default']
UserSelect: typeof import('./../components/custom/user-select.vue')['default']
UserSelectModal: typeof import('./../components/custom/user-select-modal.vue')['default']
WaveBg: typeof import('./../components/custom/wave-bg.vue')['default']
}
}

View File

@ -53,6 +53,19 @@ declare module "@elegant-router/types" {
"tool": "/tool";
"tool_gen": "/tool/gen";
"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";
};
/**
@ -100,6 +113,7 @@ declare module "@elegant-router/types" {
| "system"
| "tool"
| "user-center"
| "workflow"
>;
/**
@ -145,6 +159,17 @@ declare module "@elegant-router/types" {
| "system_tenant"
| "system_user"
| "tool_gen"
| "workflow_category"
| "workflow_design"
| "workflow_leave"
| "workflow_process-definition"
| "workflow_process-instance"
| "workflow_spel"
| "workflow_task_all-task-waiting"
| "workflow_task_my-document"
| "workflow_task_task-copy"
| "workflow_task_task-finish"
| "workflow_task_task-waiting"
>;
/**

View File

@ -114,6 +114,7 @@ declare namespace Env {
readonly VITE_DEVTOOLS_LAUNCH_EDITOR?: import('vite-plugin-vue-devtools').VitePluginVueDevToolsOptions['launchEditor'];
readonly VITE_APP_CLIENT_ID?: string;
readonly VITE_APP_ENCRYPT?: CommonType.YesOrNo;
readonly VITE_HEADER_FLAG?: string;
readonly VITE_APP_RSA_PUBLIC_KEY?: string;
readonly VITE_APP_RSA_PRIVATE_KEY?: string;
readonly VITE_APP_WEBSOCKET: CommonType.YesOrNo;

View File

@ -1,5 +1,7 @@
import { defineAsyncComponent, markRaw } from 'vue';
import { AcceptType } from '@/enum/business';
import { $t } from '@/locales';
/**
* Transform record to option
*
@ -76,6 +78,30 @@ export function humpToLine(str: string, line: string = '-') {
return temp;
}
/** 动态加载组件 */
export async function loadDynamicComponent(
modules: Record<string, () => Promise<any>>,
formPath: string,
{ delay = 2000, timeout = 3000 } = {}
) {
const suffix = `${humpToLine(formPath)}.vue`;
const componentPath = suffix.replace('/workflow', '/workflow/form');
const matched = Object.entries(modules).find(([path]) => path.endsWith(componentPath));
if (!matched) {
window.$message?.error(`组件不存在: ${suffix}`);
throw new Error(`组件不存在: ${suffix}`);
}
return markRaw(
defineAsyncComponent({
loader: matched[1],
delay,
timeout
})
);
}
/** 判断是否为空 */
export function isNotNull(value: any) {
return value !== undefined && value !== null && value !== '';

71
src/utils/export.ts Normal file
View File

@ -0,0 +1,71 @@
import { utils, writeFile } from 'xlsx';
import { isNotNull } from '@/utils/common';
import { $t } from '@/locales';
export interface ExportExcelProps<T> {
columns: NaiveUI.TableColumn<NaiveUI.TableDataWithIndex<T>>[];
data: NaiveUI.TableDataWithIndex<T>[];
filename: string;
ignoreKeys?: (keyof NaiveUI.TableDataWithIndex<T> | NaiveUI.CustomColumnKey)[];
dicts?: Record<keyof NaiveUI.TableDataWithIndex<T>, string>;
}
export function exportExcel<T>({
columns,
data,
filename,
dicts,
ignoreKeys = ['index', 'operate']
}: ExportExcelProps<T>) {
const exportColumns = columns.filter(col => isTableColumnHasKey(col) && !ignoreKeys?.includes(col.key));
const excelList = data.map(item => exportColumns.map(col => getTableValue(col, item, dicts)));
const titleList = exportColumns.map(col => (isTableColumnHasTitle(col) && col.title) || null);
excelList.unshift(titleList);
const workBook = utils.book_new();
const workSheet = utils.aoa_to_sheet(excelList);
workSheet['!cols'] = exportColumns.map(item => ({
width: Math.round(Number(item.width) / 10 || 20)
}));
utils.book_append_sheet(workBook, workSheet, filename);
writeFile(workBook, `${filename}.xlsx`);
}
function getTableValue<T>(
col: NaiveUI.TableColumn<NaiveUI.TableDataWithIndex<T>>,
item: NaiveUI.TableDataWithIndex<T>,
dicts?: Record<keyof NaiveUI.TableDataWithIndex<T>, string>
) {
if (!isTableColumnHasKey(col)) {
return null;
}
const { key } = col;
if (key === 'operate') {
return null;
}
if (isNotNull(dicts?.[key]) && isNotNull(item[key])) {
return $t(item[key] as App.I18n.I18nKey);
}
return item[key];
}
function isTableColumnHasKey<T>(column: NaiveUI.TableColumn<T>): column is NaiveUI.TableColumnWithKey<T> {
return Boolean((column as NaiveUI.TableColumnWithKey<T>).key);
}
function isTableColumnHasTitle<T>(column: NaiveUI.TableColumn<T>): column is NaiveUI.TableColumnWithKey<T> & {
title: string;
} {
return Boolean((column as NaiveUI.TableColumnWithKey<T>).title);
}

View File

@ -39,20 +39,29 @@ const activeModule = computed(() => moduleMap[props.module || 'pwd-login']);
</script>
<template>
<div class="relative min-h-screen w-full flex flex-wrap">
<div class="hidden min-h-screen w-50% bg-primary-100 lg:block dark:bg-primary-800">
<div class="size-full flex-center">
<img class="w-60% sm:w-80%" :src="loginBackground" />
<!-- Copyright By https://github.com/Daymychen/art-design-pro/blob/main/src/components/core/views/login/LoginLeftView.vue -->
<div class="box-border size-full flex">
<div class="relative box-border hidden h-full w-65vw overflow-hidden bg-primary-50 xl:block dark:bg-primary-900">
<div class="relative z-100 flex items-center pl-30px pt-30px">
<SystemLogo class="text-32px text-primary" />
<h3 class="ml-10px text-20px font-400">{{ $t('system.title') }}</h3>
</div>
<div class="absolute inset-x-0 inset-b-10.5% inset-t-0 z-10 m-auto w-40%">
<img class="size-full" :src="loginBackground" />
</div>
<div class="w-full flex-col-center px-24px py-32px lg:w-50%">
<div class="mx-auto max-w-464px w-full">
<header class="flex-y-center justify-between">
<div class="flex-y-center gap-16px">
<SystemLogo class="text-30px text-primary sm:text-42px" />
<h3 class="text-24px text-primary font-500 sm:text-32px">{{ $t('system.title') }}</h3>
<div class="absolute bottom-80px w-full text-center">
<h1 class="text-24px font-400">{{ $t('page.login.common.title') }}</h1>
<p class="mt-8px text-14px color-gray-500">{{ $t('page.login.common.subTitle') }}</p>
</div>
<div class="flex-y-center">
<WaveBg />
</div>
<header class="relative h-full flex-1 xl:m-auto sm:!w-full">
<div class="relative z-100 block flex items-center pl-30px pt-30px xl:hidden">
<SystemLogo class="text-32px text-primary" />
<h3 class="ml-10px text-20px font-400">{{ $t('system.title') }}</h3>
</div>
<div class="position-fixed right-30px top-24px z-100 flex items-center justify-end">
<div class="ml-15px inline-block flex cursor-pointer select-none p-5px">
<ThemeSchemaSwitch
:theme-schema="themeStore.themeScheme"
:show-tooltip="false"
@ -68,16 +77,13 @@ const activeModule = computed(() => moduleMap[props.module || 'pwd-login']);
@change-lang="appStore.changeLocale"
/>
</div>
</header>
<main class="pt-24px">
<div>
</div>
<main class="absolute inset-0 m-auto h-630px max-w-450px w-full overflow-hidden rounded-5px bg-cover px-24px">
<Transition :name="themeStore.page.animateMode" mode="out-in" appear>
<component :is="activeModule.component" />
</Transition>
</div>
</main>
</div>
</div>
</header>
</div>
</template>

View File

@ -40,6 +40,10 @@ async function handleSubmit() {
</script>
<template>
<div class="mb-5px text-32px text-black font-600 sm:text-30px dark:text-white">
{{ $t('page.login.codeLogin.title') }}
</div>
<div class="pb-18px text-16px text-#858585">请输入您的手机号我们将发送验证码到您的手机</div>
<NForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false" @keyup.enter="handleSubmit">
<NFormItem path="phone">
<NInput v-model:value="model.phone" :placeholder="$t('page.login.common.phonePlaceholder')" />
@ -52,15 +56,32 @@ async function handleSubmit() {
</NButton>
</div>
</NFormItem>
<NSpace vertical :size="18" class="w-full">
<NButton type="primary" size="large" round block @click="handleSubmit">
{{ $t('common.confirm') }}
<NSpace vertical :size="20" class="w-full">
<NButton type="primary" size="large" block @click="handleSubmit">
{{ $t('page.login.codeLogin.title') }}
</NButton>
<NButton size="large" round block @click="toggleLoginModule('pwd-login')">
<NButton size="large" block @click="toggleLoginModule('pwd-login')">
{{ $t('page.login.common.back') }}
</NButton>
</NSpace>
</NForm>
</template>
<style scoped></style>
<style scoped>
:deep(.n-base-selection),
:deep(.n-input) {
--n-height: 42px !important;
--n-font-size: 16px !important;
--n-border-radius: 8px !important;
}
:deep(.n-base-selection-label) {
padding: 0 6px !important;
}
:deep(.n-button) {
--n-height: 42px !important;
--n-font-size: 18px !important;
--n-border-radius: 8px !important;
}
</style>

View File

@ -124,8 +124,8 @@ async function handleSocialLogin(type: Api.System.SocialSource) {
<template>
<div>
<div class="mb-12px text-24px text-black font-500 sm:text-30px dark:text-white">登录到您的账户</div>
<div class="pb-24px text-18px text-#858585">欢迎回来请输入您的账户信息</div>
<div class="mb-5px text-32px text-black font-600 dark:text-white">登录到您的账户</div>
<div class="pb-18px text-16px text-#858585">欢迎回来请输入您的账户信息</div>
<NForm
ref="formRef"
:model="model"
@ -156,16 +156,16 @@ async function handleSocialLogin(type: Api.System.SocialSource) {
<NFormItem v-if="captchaEnabled" path="code">
<div class="w-full flex-y-center gap-16px">
<NInput v-model:value="model.code" :placeholder="$t('page.login.common.codePlaceholder')" />
<NSpin :show="codeLoading" :size="28" class="h-52px">
<NButton :focusable="false" class="login-code h-52px w-136px" @click="handleFetchCaptchaCode">
<NSpin :show="codeLoading" :size="28" class="h-42px">
<NButton :focusable="false" class="login-code h-42px w-136px" @click="handleFetchCaptchaCode">
<img v-if="codeUrl" :src="codeUrl" />
<NEmpty v-else :show-icon="false" description="暂无验证码" />
</NButton>
</NSpin>
</div>
</NFormItem>
<NSpace vertical :size="16" class="mb-8px">
<div class="mx-6px mb-10px flex-y-center justify-between">
<NSpace vertical :size="12" class="mb-8px">
<div class="mx-6px mb-8px flex-y-center justify-between">
<NCheckbox v-model:checked="remberMe" size="large">{{ $t('page.login.pwdLogin.rememberMe') }}</NCheckbox>
<NA type="primary" class="text-18px" @click="toggleLoginModule('reset-pwd')">
{{ $t('page.login.pwdLogin.forgetPassword') }}
@ -199,7 +199,7 @@ async function handleSocialLogin(type: Api.System.SocialSource) {
</NButton>
</div>
<div class="mt-32px w-full text-center text-18px text-#858585">
<div class="mt-24px w-full text-center text-18px text-#858585">
您还没有账户
<NA type="primary" class="text-18px" @click="toggleLoginModule('register')">
{{ $t('page.login.common.register') }}
@ -216,13 +216,13 @@ async function handleSocialLogin(type: Api.System.SocialSource) {
}
img {
height: 52px;
height: 42px;
}
}
:deep(.n-base-selection),
:deep(.n-input) {
--n-height: 52px !important;
--n-height: 42px !important;
--n-font-size: 16px !important;
--n-border-radius: 8px !important;
}
@ -237,7 +237,7 @@ async function handleSocialLogin(type: Api.System.SocialSource) {
}
:deep(.n-button) {
--n-height: 52px !important;
--n-height: 42px !important;
--n-font-size: 18px !important;
--n-border-radius: 8px !important;
}

View File

@ -104,8 +104,8 @@ handleFetchCaptchaCode();
<template>
<div>
<div class="mb-12px text-24px text-black font-500 sm:text-30px dark:text-white">注册新账户</div>
<div class="pb-24px text-18px text-#858585">欢迎注册请输入您的账户信息</div>
<div class="mb-5px text-32px text-black font-600 sm:text-30px dark:text-white">注册新账户</div>
<div class="pb-18px text-16px text-#858585">欢迎注册请输入您的账户信息</div>
<NForm
ref="formRef"
:model="model"
@ -147,14 +147,14 @@ handleFetchCaptchaCode();
</NSpin>
</div>
</NFormItem>
<NSpace vertical :size="18" class="w-full pt-6px">
<NSpace vertical :size="18" class="w-full">
<NButton type="primary" size="large" block :loading="registerLoading" @click="handleSubmit">
{{ $t('page.login.common.register') }}
</NButton>
</NSpace>
</NForm>
<div class="mt-32px w-full text-center text-18px text-#858585">
<div class="mt-24px w-full text-center text-18px text-#858585">
您已有账户
<NA type="primary" class="text-18px" @click="toggleLoginModule('pwd-login')">
{{ $t('common.login') }}
@ -177,7 +177,7 @@ handleFetchCaptchaCode();
:deep(.n-base-selection),
:deep(.n-input) {
--n-height: 52px !important;
--n-height: 42px !important;
--n-font-size: 16px !important;
--n-border-radius: 8px !important;
}
@ -187,7 +187,7 @@ handleFetchCaptchaCode();
}
:deep(.n-button) {
--n-height: 52px !important;
--n-height: 42px !important;
--n-font-size: 18px !important;
--n-border-radius: 8px !important;
}

View File

@ -46,10 +46,10 @@ async function handleSubmit() {
<template>
<div>
<div class="mb-12px text-24px text-black font-500 sm:text-30px dark:text-white">
<div class="mb-5px text-32px text-black font-600 sm:text-30px dark:text-white">
{{ $t('page.login.resetPwd.title') }}
</div>
<div class="pb-24px text-18px text-#858585">请输入您的手机号我们将发送验证码到您的手机</div>
<div class="pb-18px text-16px text-#858585">请输入您的手机号我们将发送验证码到您的手机</div>
<NForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false" @keyup.enter="handleSubmit">
<NFormItem path="phone">
<NInput v-model:value="model.phone" :placeholder="$t('page.login.common.phonePlaceholder')" />
@ -73,7 +73,7 @@ async function handleSubmit() {
:placeholder="$t('page.login.common.confirmPasswordPlaceholder')"
/>
</NFormItem>
<NSpace vertical :size="18" class="w-full">
<NSpace vertical :size="20" class="w-full">
<NButton type="primary" size="large" block @click="handleSubmit">
{{ $t('page.login.resetPwd.title') }}
</NButton>
@ -88,7 +88,7 @@ async function handleSubmit() {
<style scoped>
:deep(.n-base-selection),
:deep(.n-input) {
--n-height: 52px !important;
--n-height: 42px !important;
--n-font-size: 16px !important;
--n-border-radius: 8px !important;
}
@ -98,7 +98,7 @@ async function handleSubmit() {
}
:deep(.n-button) {
--n-height: 52px !important;
--n-height: 42px !important;
--n-font-size: 18px !important;
--n-border-radius: 8px !important;
}

View File

@ -40,6 +40,7 @@ const deptData = ref<Api.System.Dept[]>([]);
const userOptions = ref<CommonType.Option<CommonType.IdType>[]>([]);
const placeholder = ref<string>($t('page.system.dept.placeholder.defaultLeaderPlaceHolder'));
const disabled = ref<boolean>(false);
const expandedKeys = ref<CommonType.IdType[]>([]);
const title = computed(() => {
const titles: Record<NaiveUI.TableOperateType, string> = {
@ -55,7 +56,7 @@ const model: Model = reactive(createDefaultModel());
function createDefaultModel(): Model {
return {
parentId: '',
parentId: props.rowData?.deptId || '',
deptName: '',
deptCategory: '',
orderNum: null,
@ -80,7 +81,6 @@ const rules: Record<RuleKey, App.Global.FormRule> = {
function handleUpdateModelWhenEdit() {
if (props.operateType === 'add') {
Object.assign(model, createDefaultModel());
model.parentId = props.rowData?.deptId || 0;
}
if (props.operateType === 'edit' && props.rowData) {
@ -144,6 +144,7 @@ async function getDeptData() {
if (data) {
deptData.value = handleTree(data, { idField: 'deptId' });
expandedKeys.value = [deptData.value[0].deptId];
}
endDeptLoading();
}
@ -186,15 +187,15 @@ watch(visible, () => {
<NDrawer v-model:show="visible" :title="title" display-directive="show" :width="800" class="max-w-90%">
<NDrawerContent :title="title" :native-scrollbar="false" closable>
<NForm ref="formRef" :model="model" :rules="rules">
<NFormItem v-if="model.parentId != 0" :label="$t('page.system.dept.parentId')" path="parentId">
<NFormItem v-if="model.parentId !== 0" :label="$t('page.system.dept.parentId')" path="parentId">
<NTreeSelect
v-model:value="model.parentId"
v-model:expanded-keys="expandedKeys"
:loading="deptLoading"
clearable
:options="deptData"
label-field="deptName"
key-field="deptId"
default-expand-all
:placeholder="$t('page.system.dept.form.parentId.required')"
/>
</NFormItem>

View File

@ -48,7 +48,6 @@ function closeDrawer() {
}
async function handleSubmit() {
console.log(data.value);
fileList.value.forEach(item => {
item.status = 'pending';
});

View File

@ -78,9 +78,9 @@ const rules: Record<RuleKey, App.Global.FormRule[]> = {
roleIds: [{ ...createRequiredRule('请选择角色'), type: 'array' }]
};
async function getUserInfo() {
async function getUserInfo(id: CommonType.IdType = '') {
startLoading();
const { error, data } = await fetchGetUserInfo(props.rowData?.userId);
const { error, data } = await fetchGetUserInfo(id);
if (!error) {
model.roleIds = data.roleIds;
model.postIds = data.postIds;
@ -94,6 +94,7 @@ async function getUserInfo() {
function handleUpdateModelWhenEdit() {
if (props.operateType === 'add') {
getUserInfo();
Object.assign(model, createDefaultModel());
model.deptId = props.deptId;
return;
@ -103,7 +104,7 @@ function handleUpdateModelWhenEdit() {
startDeptLoading();
Object.assign(model, props.rowData);
model.password = '';
getUserInfo();
getUserInfo(props.rowData.userId);
endDeptLoading();
}
}

View File

@ -356,7 +356,7 @@ const columns: NaiveUI.TableColumn<Api.Tool.GenTableColumn>[] = [
<NFormItemGi span="24 s:12" path="moduleName">
<template #label>
<div class="flex-center">
<FormTip content="可理解为子系统名,例如 system" />
<FormTip content="可理解为子系统名,例如 systemflow-instance。避免驼峰命名" />
<span class="pl-3px">生成模块名</span>
</div>
</template>

View File

@ -0,0 +1,210 @@
<script setup lang="tsx">
import { NDivider } from 'naive-ui';
import { jsonClone } from '@sa/utils';
import { type TableDataWithIndex } from '@sa/hooks';
import { fetchDeleteCategory, fetchGetCategoryList } from '@/service/api/workflow';
import { useAppStore } from '@/store/modules/app';
import { useAuth } from '@/hooks/business/auth';
import { useTreeTable, useTreeTableOperate } from '@/hooks/common/tree-table';
import { useDownload } from '@/hooks/business/download';
import { $t } from '@/locales';
import ButtonIcon from '@/components/custom/button-icon.vue';
import WorkflowCategoryOperateDrawer from './modules/category-operate-drawer.vue';
import WorkflowCategorySearch from './modules/category-search.vue';
defineOptions({
name: 'WorkflowCategoryList'
});
const appStore = useAppStore();
const { download } = useDownload();
const { hasAuth } = useAuth();
const {
columns,
columnChecks,
data,
getData,
loading,
searchParams,
resetSearchParams,
expandedRowKeys,
isCollapse,
expandAll,
collapseAll
} = useTreeTable({
apiFn: fetchGetCategoryList,
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
categoryName: null
},
idField: 'categoryId',
columns: () => [
{
key: 'categoryName',
title: '分类名称',
align: 'center',
minWidth: 120
},
{
key: 'orderNum',
title: '显示顺序',
align: 'center',
minWidth: 120
},
{
key: 'createTime',
title: '创建时间',
align: 'center',
minWidth: 120
},
{
key: 'operate',
title: $t('common.operate'),
align: 'center',
width: 130,
render: row => {
const addBtn = () => {
return (
<ButtonIcon
text
type="primary"
icon="material-symbols:add-2-rounded"
tooltipContent={$t('common.add')}
onClick={() => addInRow(row)}
/>
);
};
const editBtn = () => {
return (
<ButtonIcon
text
type="primary"
icon="material-symbols:drive-file-rename-outline-outline"
tooltipContent={$t('common.edit')}
onClick={() => edit(row)}
/>
);
};
const deleteBtn = () => {
return (
<ButtonIcon
text
type="error"
icon="material-symbols:delete-outline"
tooltipContent={$t('common.delete')}
popconfirmContent={$t('common.confirmDelete')}
onPositiveClick={() => handleDelete(row.categoryId!)}
/>
);
};
const buttons = [];
if (hasAuth('workflow:category:add')) buttons.push(addBtn());
if (hasAuth('workflow:category:edit')) buttons.push(editBtn());
if (hasAuth('workflow:category:remove')) buttons.push(deleteBtn());
return (
<div class="flex-center gap-4px">
{buttons.map((btn, index) => (
<>
{index !== 0 && <NDivider vertical />}
{btn}
</>
))}
</div>
);
}
}
]
});
const { drawerVisible, operateType, editingData, handleAdd, handleEdit, checkedRowKeys, onDeleted } =
useTreeTableOperate(data, getData);
async function handleDelete(id: CommonType.IdType) {
// request
const { error } = await fetchDeleteCategory(id);
if (error) return;
onDeleted();
}
async function edit(row: TableDataWithIndex<Api.Workflow.WorkflowCategory>) {
handleEdit(row);
}
async function addInRow(row: TableDataWithIndex<Api.Workflow.WorkflowCategory>) {
editingData.value = jsonClone(row);
handleAdd();
}
async function handleAddOperate() {
editingData.value = null;
handleAdd();
}
function handleExport() {
download('/workflow/category/export', searchParams, `流程分类_#[[${new Date().getTime()}]]#.xlsx`);
}
</script>
<template>
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
<WorkflowCategorySearch v-model:model="searchParams" @reset="resetSearchParams" @search="getData" />
<NCard title="流程分类列表" :bordered="false" size="small" class="card-wrapper sm:flex-1-hidden">
<template #header-extra>
<TableHeaderOperation
v-model:columns="columnChecks"
:loading="loading"
:show-add="hasAuth('workflow:category:add')"
:show-delete="false"
:show-export="false"
@add="handleAddOperate"
@export="handleExport"
@refresh="getData"
>
<template #prefix>
<NButton v-if="!isCollapse" :disabled="!data.length" size="small" @click="expandAll">
<template #icon>
<icon-quill:expand />
</template>
全部展开
</NButton>
<NButton v-if="isCollapse" :disabled="!data.length" size="small" @click="collapseAll">
<template #icon>
<icon-quill:collapse />
</template>
全部收起
</NButton>
</template>
</TableHeaderOperation>
</template>
<NDataTable
v-model:checked-row-keys="checkedRowKeys"
v-model:expanded-row-keys="expandedRowKeys"
:columns="columns"
:data="data"
size="small"
:indent="32"
:flex-height="!appStore.isMobile"
:scroll-x="962"
:loading="loading"
remote
:row-key="row => row.categoryId"
class="sm:h-full"
/>
<WorkflowCategoryOperateDrawer
v-model:visible="drawerVisible"
:operate-type="operateType"
:row-data="editingData"
@submitted="getData"
/>
</NCard>
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,128 @@
<script setup lang="ts">
import { computed, reactive, watch } from 'vue';
import { fetchCreateCategory, fetchUpdateCategory } from '@/service/api/workflow';
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import { $t } from '@/locales';
defineOptions({
name: 'WorkflowCategoryOperateDrawer'
});
interface Props {
/** the type of operation */
operateType: NaiveUI.TableOperateType;
/** the edit row data */
rowData?: Api.Workflow.WorkflowCategory | null;
}
const props = defineProps<Props>();
interface Emits {
(e: 'submitted'): void;
}
const emit = defineEmits<Emits>();
const visible = defineModel<boolean>('visible', {
default: false
});
const { formRef, validate, restoreValidation } = useNaiveForm();
const { createRequiredRule } = useFormRules();
const title = computed(() => {
const titles: Record<NaiveUI.TableOperateType, string> = {
add: '新增测试树',
edit: '编辑测试树'
};
return titles[props.operateType];
});
type Model = Api.Workflow.WorkflowCategoryOperateParams;
const model: Model = reactive(createDefaultModel());
function createDefaultModel(): Model {
return {
parentId: '',
categoryName: '',
orderNum: 0
};
}
type RuleKey = Extract<keyof Model, 'categoryId' | 'parentId' | 'categoryName'>;
const rules: Record<RuleKey, App.Global.FormRule> = {
categoryId: createRequiredRule('主键不能为空'),
parentId: createRequiredRule('上级分类不能为空'),
categoryName: createRequiredRule('分类名称不能为空')
};
function handleUpdateModelWhenEdit() {
if (props.operateType === 'add') {
Object.assign(model, createDefaultModel());
model.parentId = props.rowData?.categoryId || 0;
}
if (props.operateType === 'edit' && props.rowData) {
Object.assign(model, props.rowData);
}
}
function closeDrawer() {
visible.value = false;
}
async function handleSubmit() {
await validate();
// request
if (props.operateType === 'add') {
const { parentId, categoryName, orderNum } = model;
const { error } = await fetchCreateCategory({ parentId, categoryName, orderNum });
if (error) return;
}
if (props.operateType === 'edit') {
const { categoryId, parentId, categoryName, orderNum } = model;
const { error } = await fetchUpdateCategory({ categoryId, parentId, categoryName, orderNum });
if (error) return;
}
window.$message?.success($t('common.updateSuccess'));
closeDrawer();
emit('submitted');
}
watch(visible, () => {
if (visible.value) {
handleUpdateModelWhenEdit();
restoreValidation();
}
});
</script>
<template>
<NDrawer v-model:show="visible" :title="title" display-directive="show" :width="800" class="max-w-90%">
<NDrawerContent :title="title" :native-scrollbar="false" closable>
<NForm ref="formRef" :model="model" :rules="rules">
<NFormItem label="上级分类" path="parentId">
<FlowCategorySelect v-model:value="model.parentId" />
</NFormItem>
<NFormItem label="分类名称" path="categoryName">
<NInput v-model:value="model.categoryName" placeholder="请输入分类名称" />
</NFormItem>
<NFormItem label="排序" path="orderNum">
<NInputNumber v-model:value="model.orderNum" placeholder="请输入排序" />
</NFormItem>
</NForm>
<template #footer>
<NSpace :size="16">
<NButton @click="closeDrawer">{{ $t('common.cancel') }}</NButton>
<NButton type="primary" @click="handleSubmit">{{ $t('common.confirm') }}</NButton>
</NSpace>
</template>
</NDrawerContent>
</NDrawer>
</template>
<style scoped></style>

View File

@ -0,0 +1,63 @@
<script setup lang="ts">
import { useNaiveForm } from '@/hooks/common/form';
import { $t } from '@/locales';
defineOptions({
name: 'WorkflowCategorySearch'
});
interface Emits {
(e: 'reset'): void;
(e: 'search'): void;
}
const emit = defineEmits<Emits>();
const { formRef, validate, restoreValidation } = useNaiveForm();
const model = defineModel<Api.Workflow.WorkflowCategorySearchParams>('model', { required: true });
async function reset() {
await restoreValidation();
emit('reset');
}
async function search() {
await validate();
emit('search');
}
</script>
<template>
<NCard :bordered="false" size="small" class="card-wrapper">
<NCollapse>
<NCollapseItem :title="$t('common.search')" name="user-search">
<NForm ref="formRef" :model="model" label-placement="left" :label-width="80">
<NGrid responsive="screen" item-responsive>
<NFormItemGi span="24 s:12 m:12" label="分类名称" path="categoryName" class="pr-24px">
<NInput v-model:value="model.categoryName" placeholder="请输入分类名称" />
</NFormItemGi>
<NFormItemGi :show-feedback="false" span="24 s:12 m:12" class="pr-24px">
<NSpace class="w-full" justify="end">
<NButton @click="reset">
<template #icon>
<icon-ic-round-refresh class="text-icon" />
</template>
{{ $t('common.reset') }}
</NButton>
<NButton type="primary" ghost @click="search">
<template #icon>
<icon-ic-round-search class="text-icon" />
</template>
{{ $t('common.search') }}
</NButton>
</NSpace>
</NFormItemGi>
</NGrid>
</NForm>
</NCollapseItem>
</NCollapse>
</NCard>
</template>
<style scoped></style>

View File

@ -0,0 +1,48 @@
<script setup lang="ts">
import { useRoute } from 'vue-router';
import { useEventListener } from '@vueuse/core';
import { stringify } from 'qs';
import { getToken } from '@/store/modules/auth/shared';
import { getTabIdByRoute } from '@/store/modules/tab/shared';
import { useTabStore } from '@/store/modules/tab';
import { useRouterPush } from '@/hooks/common/router';
import { getServiceBaseURL } from '@/utils/service';
const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y';
const { baseURL } = getServiceBaseURL(import.meta.env, isHttpProxy);
const { removeTab } = useTabStore();
const { routerPushByKey } = useRouterPush();
const route = useRoute();
const disabled = route.query.disabled === 'true';
const urlParams = {
Authorization: `Bearer ${getToken()}`,
id: route.query.definitionId,
clientid: import.meta.env.VITE_APP_CLIENT_ID,
disabled
};
const iframeUrl = `${baseURL}/warm-flow-ui/index.html?${stringify(urlParams)}`;
function messageHandler(event: MessageEvent) {
switch (event.data.method) {
case 'close': {
const tabId = getTabIdByRoute(route);
removeTab(tabId);
routerPushByKey('workflow_process-definition');
break;
}
default: {
break;
}
}
}
// iframe监听组件内设计器保存事件
useEventListener('message', messageHandler);
</script>
<template>
<iframe :src="iframeUrl" class="size-full"></iframe>
</template>

View File

@ -0,0 +1,268 @@
<script setup lang="tsx">
import { ref } from 'vue';
import { NDivider, NTag } from 'naive-ui';
import { jsonClone } from '@sa/utils';
import { leaveTypeRecord } from '@/constants/workflow';
import { fetchBatchDeleteLeave, fetchGetLeaveList } from '@/service/api/workflow';
import { useAppStore } from '@/store/modules/app';
import { useAuth } from '@/hooks/business/auth';
import { useDownload } from '@/hooks/business/download';
import { useTable, useTableOperate } from '@/hooks/common/table';
import { useDict } from '@/hooks/business/dict';
import { $t } from '@/locales';
import DictTag from '@/components/custom/dict-tag.vue';
import ButtonIcon from '@/components/custom/button-icon.vue';
import LeaveEdit from '@/components/workflow/form/leave-edit/index.vue';
import LeaveSearch from './modules/leave-search.vue';
defineOptions({
name: 'LeaveList'
});
const appStore = useAppStore();
const { download } = useDownload();
const { hasAuth } = useAuth();
useDict('wf_business_status');
const workflowTableOperateType = ref<CommonType.WorkflowTableOperateType>('add');
const {
columns,
columnChecks,
data,
getData,
getDataByPage,
loading,
mobilePagination,
searchParams,
resetSearchParams
} = useTable({
apiFn: fetchGetLeaveList,
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
startLeaveDays: null,
endLeaveDays: null,
params: {}
},
columns: () => [
{
type: 'selection',
align: 'center',
width: 48
},
{
key: 'leaveType',
title: '请假类型',
align: 'center',
minWidth: 100,
render: row => {
return (
<NTag size="small" type="info">
{leaveTypeRecord[row.leaveType]}
</NTag>
);
}
},
{
key: 'startDate',
title: '开始时间',
align: 'center',
minWidth: 100
},
{
key: 'endDate',
title: '结束时间',
align: 'center',
minWidth: 100
},
{
key: 'leaveDays',
title: '请假天数',
align: 'center',
minWidth: 100,
render: row => {
return `${row.leaveDays}`;
}
},
{
key: 'remark',
title: '请假原因',
align: 'center',
minWidth: 100
},
{
key: 'status',
title: '状态',
align: 'center',
minWidth: 100,
render: row => {
return <DictTag size="small" value={row.status} dictCode="wf_business_status" />;
}
},
{
key: 'operate',
title: $t('common.operate'),
align: 'center',
width: 130,
render: row => {
const buttons = [];
const showEdit =
hasAuth('workflow:leave:edit') &&
(row.status === 'draft' || row.status === 'cancel' || row.status === 'back');
const showDelete =
hasAuth('workflow:leave:remove') &&
(row.status === 'draft' || row.status === 'cancel' || row.status === 'back');
const showCancel = row.status === 'waiting';
if (hasAuth('workflow:leave:query')) {
buttons.push(
<ButtonIcon
text
type="info"
icon="material-symbols:visibility-outline"
tooltipContent="查看"
onClick={() => view(row.id!)}
/>
);
}
if (showEdit) {
buttons.push(
<ButtonIcon
text
type="primary"
icon="material-symbols:drive-file-rename-outline-outline"
tooltipContent={$t('common.edit')}
onClick={() => edit(row.id!)}
/>
);
}
if (showCancel) {
buttons.push(
<ButtonIcon
text
type="warning"
icon="material-symbols:cancel-outline"
tooltipContent="撤销"
popconfirmContent="确定要撤销该申请吗?"
onPositiveClick={() => {}}
/>
);
}
if (showDelete) {
buttons.push(
<ButtonIcon
text
type="error"
icon="material-symbols:delete-outline"
tooltipContent={$t('common.delete')}
popconfirmContent={$t('common.confirmDelete')}
onPositiveClick={() => handleDelete(row.id!)}
/>
);
}
return (
<div class="flex-center gap-1px">
{buttons.map((btn, index) => (index > 0 ? [<NDivider vertical />, btn] : btn))}
</div>
);
}
}
]
});
const { drawerVisible, openDrawer, editingData, checkedRowKeys, onBatchDeleted, onDeleted } = useTableOperate(
data,
getData
);
async function handleBatchDelete() {
// request
const { error } = await fetchBatchDeleteLeave(checkedRowKeys.value);
if (error) return;
onBatchDeleted();
}
async function handleDelete(id: CommonType.IdType) {
// request
const { error } = await fetchBatchDeleteLeave([id]);
if (error) return;
onDeleted();
}
function handleAdd() {
workflowTableOperateType.value = 'add';
openDrawer();
}
function cloneAndOpenDrawer(id: CommonType.IdType) {
const findItem = data.value.find(item => item.id === id) || null;
editingData.value = jsonClone(findItem);
openDrawer();
}
function edit(id: CommonType.IdType) {
workflowTableOperateType.value = 'edit';
cloneAndOpenDrawer(id);
}
function view(id: CommonType.IdType) {
workflowTableOperateType.value = 'detail';
cloneAndOpenDrawer(id);
}
function handleExport() {
download('/workflow/leave/export', searchParams, `请假申请_${new Date().getTime()}.xlsx`);
}
</script>
<template>
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
<LeaveSearch v-model:model="searchParams" @reset="resetSearchParams" @search="getDataByPage" />
<NCard title="请假申请列表" :bordered="false" size="small" class="card-wrapper sm:flex-1-hidden">
<template #header-extra>
<TableHeaderOperation
v-model:columns="columnChecks"
:disabled-delete="checkedRowKeys.length === 0"
:loading="loading"
:show-add="hasAuth('workflow:leave:add')"
:show-delete="hasAuth('workflow:leave:remove')"
:show-export="hasAuth('workflow:leave:export')"
@add="handleAdd"
@delete="handleBatchDelete"
@export="handleExport"
@refresh="getData"
/>
</template>
<NDataTable
v-model:checked-row-keys="checkedRowKeys"
:columns="columns"
:data="data"
size="small"
:flex-height="!appStore.isMobile"
:scroll-x="778"
:loading="loading"
remote
:row-key="row => row.id"
:pagination="mobilePagination"
class="sm:h-full"
/>
<LeaveEdit
v-model:visible="drawerVisible"
:operate-type="workflowTableOperateType"
:row-data="editingData"
@submitted="getDataByPage"
/>
</NCard>
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,68 @@
<script setup lang="ts">
import { useNaiveForm } from '@/hooks/common/form';
import { $t } from '@/locales';
defineOptions({
name: 'LeaveSearch'
});
interface Emits {
(e: 'reset'): void;
(e: 'search'): void;
}
const emit = defineEmits<Emits>();
const { formRef, validate, restoreValidation } = useNaiveForm();
const model = defineModel<Api.Workflow.LeaveSearchParams>('model', { required: true });
async function reset() {
Object.assign(model.value.params!, {});
await restoreValidation();
emit('reset');
}
async function search() {
await validate();
emit('search');
}
</script>
<template>
<NCard :bordered="false" size="small" class="card-wrapper">
<NCollapse>
<NCollapseItem :title="$t('common.search')" name="user-search">
<NForm ref="formRef" :model="model" label-placement="left" :label-width="80">
<NGrid responsive="screen" item-responsive>
<NFormItemGi span="24 s:12 m:16" label="请假天数" path="startLeaveDays" class="pr-24px">
<NSpace align="center" class="w-full">
<NInputNumber v-model:value="model.startLeaveDays" placeholder="开始天数" />
<span class="mx-2"></span>
<NInputNumber v-model:value="model.endLeaveDays" placeholder="结束天数" />
</NSpace>
</NFormItemGi>
<NFormItemGi span="24 s:12 m:8" class="pr-24px">
<NSpace class="w-full" justify="end">
<NButton @click="reset">
<template #icon>
<icon-ic-round-refresh class="text-icon" />
</template>
{{ $t('common.reset') }}
</NButton>
<NButton type="primary" ghost @click="search">
<template #icon>
<icon-ic-round-search class="text-icon" />
</template>
{{ $t('common.search') }}
</NButton>
</NSpace>
</NFormItemGi>
</NGrid>
</NForm>
</NCollapseItem>
</NCollapse>
</NCard>
</template>
<style scoped></style>

View File

@ -0,0 +1,471 @@
<script setup lang="tsx">
import { computed, ref, watch } from 'vue';
import { NDivider, NSwitch, NTag } from 'naive-ui';
import { useBoolean, useLoading } from '@sa/hooks';
import { type TableDataWithIndex } from '@sa/hooks';
import { workflowPublishStatusRecord } from '@/constants/workflow';
import {
fetchActiveDefinition,
fetchBatchDeleteDefinition,
fetchCopyDefinition,
fetchGetCategoryTree,
fetchGetDefinitionList,
fetchGetUnPublishDefinitionList,
fetchPublishDefinition
} from '@/service/api/workflow';
import { useAppStore } from '@/store/modules/app';
import { useAuth } from '@/hooks/business/auth';
import { useDownload } from '@/hooks/business/download';
import { useTable, useTableOperate } from '@/hooks/common/table';
import { useRouterPush } from '@/hooks/common/router';
import { $t } from '@/locales';
import ButtonIcon from '@/components/custom/button-icon.vue';
import DefinitionOperateDrawer from './modules/definition-operate-drawer.vue';
import DefinitionSearch from './modules/definition-search.vue';
import DefinitionImportModal from './modules/definition-import-modal.vue';
defineOptions({
name: 'DefinitionList'
});
interface IsPublishOption {
label: string;
value: boolean;
}
const appStore = useAppStore();
const { download } = useDownload();
const { hasAuth } = useAuth();
const { routerPushByKey } = useRouterPush();
const { bool: importVisible, setTrue: showImportModal } = useBoolean();
const isPublish = ref<boolean>(true);
const isPublishOptions = ref<IsPublishOption[]>([
{
label: '已发布',
value: true
},
{
label: '未发布',
value: false
}
]);
const {
columns,
columnChecks,
data,
getData,
getDataByPage,
loading,
mobilePagination,
searchParams,
resetSearchParams,
updateApiFn
} = useTable({
apiFn: fetchGetDefinitionList,
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
flowCode: null,
flowName: null,
category: null,
params: {}
},
columns: () => [
{
type: 'selection',
align: 'center',
width: 48
},
{
key: 'flowName',
title: '流程定义名称',
align: 'center',
minWidth: 120
},
{
key: 'flowCode',
title: '流程定义编码',
align: 'center',
minWidth: 120
},
{
key: 'categoryName',
title: '流程分类',
align: 'center',
minWidth: 120
},
{
key: 'version',
title: '版本号',
align: 'center',
minWidth: 120,
render: row => <NTag type="info">v{row.version}.0</NTag>
},
{
key: 'activityStatus',
title: '激活状态',
align: 'center',
minWidth: 120,
render(row) {
const {
loading: activityLoading,
startLoading: startActivityLoading,
endLoading: endActivityLoading
} = useLoading();
/** 处理状态切换 */
async function handleStatusChange(value: boolean) {
window.$dialog?.warning({
title: '系统提示',
content: `确定要${value ? '激活' : '挂起'} ${row.flowCode} 吗?`,
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
startActivityLoading();
const { error } = await fetchActiveDefinition(row.id, value);
if (error) return;
if (!error) row.activityStatus = value ? 1 : 0;
window.$message?.success($t('page.system.user.statusChangeSuccess'));
getData();
endActivityLoading();
},
onNegativeClick: () => {}
});
}
return (
<NSwitch
v-model:value={row.activityStatus}
loading={activityLoading.value}
rubber-band={false}
checked-value={1}
unchecked-value={0}
on-update:value={handleStatusChange}
/>
);
}
},
{
key: 'isPublish',
title: '发布状态',
align: 'center',
minWidth: 120,
render: row => {
if (row.isPublish === null) {
return null;
}
const tagMap: Record<Api.Workflow.WorkflowPublishStatus, NaiveUI.ThemeColor> = {
0: 'warning',
1: 'success',
9: 'error'
};
return <NTag type={tagMap[row.isPublish]}>{workflowPublishStatusRecord[row.isPublish]}</NTag>;
}
},
{
key: 'operate',
title: $t('common.operate'),
align: 'center',
width: 150,
fixed: 'right',
render: row => {
const firstRowButtons = [];
firstRowButtons.push(
<ButtonIcon
text
type="primary"
icon="material-symbols:drive-file-rename-outline-outline"
tooltipContent={$t('common.edit')}
onClick={() => edit(row.id)}
/>
);
firstRowButtons.push(
<ButtonIcon
text
type="error"
icon="material-symbols:delete-outline"
tooltipContent={$t('common.delete')}
popconfirmContent={$t('common.confirmDelete')}
onPositiveClick={() => handleDelete(row.id)}
/>
);
firstRowButtons.push(
<ButtonIcon
text
type="primary"
icon="material-symbols:content-copy"
tooltipContent="复制流程"
popconfirmContent={`确定要复制 ${row.flowName} 吗?`}
onPositiveClick={() => handleCopy(row.id)}
/>
);
const firstRowWithDividers = firstRowButtons.map((btn, index) =>
index > 0 ? [<NDivider vertical />, btn] : btn
);
const secondRowButtons = [];
secondRowButtons.push(
<ButtonIcon
text
type="primary"
icon="material-symbols:file-export"
tooltipContent="导出流程"
onClick={() => handleExport(row)}
/>
);
secondRowButtons.push(
isPublish.value ? (
<ButtonIcon
text
type="primary"
icon="material-symbols:visibility-outline"
tooltipContent="查看流程"
onClick={() => handlePreview(row.id)}
/>
) : (
<ButtonIcon
text
type="primary"
icon="material-symbols:design-services"
tooltipContent="流程设计"
onClick={() => handleDesign(row.id)}
/>
)
);
if (!isPublish.value) {
secondRowButtons.push(
<ButtonIcon
text
type="primary"
icon="material-symbols:publish"
tooltipContent="发布流程"
popconfirmContent={`确定要发布 ${row.flowName} 吗?`}
onPositiveClick={() => handlePublish(row.id)}
/>
);
}
const secondRowWithDividers = secondRowButtons.map((btn, index) =>
index > 0 ? [<NDivider vertical />, btn] : btn
);
return (
<div class="flex-col">
<div class="h-[24px] flex-center gap-4px">{firstRowWithDividers}</div>
<div class="h-[24px] flex-center gap-4px">{secondRowWithDividers}</div>
</div>
);
}
}
]
});
// 监听运行状态变化
watch(isPublish, async () => {
const newApiFn = isPublish.value ? fetchGetDefinitionList : fetchGetUnPublishDefinitionList;
updateApiFn(newApiFn);
await getDataByPage();
});
const { drawerVisible, operateType, editingData, handleAdd, handleEdit, checkedRowKeys, onBatchDeleted, onDeleted } =
useTableOperate(data, getData);
async function handleBatchDelete() {
// request
const { error } = await fetchBatchDeleteDefinition(checkedRowKeys.value);
if (error) return;
onBatchDeleted();
}
async function handleDelete(id: CommonType.IdType) {
// request
const { error } = await fetchBatchDeleteDefinition([id]);
if (error) return;
onDeleted();
}
function edit(id: CommonType.IdType) {
handleEdit('id', id);
}
function handleDeploy() {
showImportModal();
}
async function handlePublish(id: CommonType.IdType) {
const { error } = await fetchPublishDefinition(id);
if (error) return;
window.$message?.success('发布成功');
getDataByPage();
}
async function handleCopy(id: CommonType.IdType) {
const { error } = await fetchCopyDefinition(id);
if (error) return;
window.$message?.success('复制成功');
// 如果当前是已发布状态,则切换到未发布状态
if (isPublish.value) {
isPublish.value = false;
} else {
getDataByPage();
}
}
function handleDesign(id: CommonType.IdType) {
routerPushByKey('workflow_design', {
query: {
definitionId: id.toString(),
disabled: 'false'
}
});
}
function handlePreview(id: CommonType.IdType) {
routerPushByKey('workflow_design', {
query: {
definitionId: id.toString(),
disabled: 'true'
}
});
}
function handleExport(row: TableDataWithIndex<Api.Workflow.Definition>) {
download(`/workflow/definition/exportDef/${row.id}`, {}, `${row.flowCode}.json`);
}
const { loading: categoryLoading, startLoading: startCategoryLoading, endLoading: endCategoryLoading } = useLoading();
const categoryPattern = ref<string>();
const categoryData = ref<Api.Common.CommonTreeRecord>([]);
const selectedKeys = ref<string[]>([]);
async function getTreeData() {
startCategoryLoading();
const { data: tree, error } = await fetchGetCategoryTree();
if (!error) {
categoryData.value = tree;
}
endCategoryLoading();
}
getTreeData();
function handleClickTree(keys: string[]) {
searchParams.category = keys.length ? keys[0] : null;
checkedRowKeys.value = [];
getDataByPage();
}
function handleResetTreeData() {
categoryPattern.value = undefined;
getTreeData();
}
const expandedKeys = ref<CommonType.IdType[]>(['100']);
const selectable = computed(() => {
return !loading.value;
});
</script>
<template>
<TableSiderLayout sider-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="categoryPattern" clearable :placeholder="$t('common.keywordSearch')" />
<NSpin class="dept-tree" :show="categoryLoading">
<NTree
v-model:expanded-keys="expandedKeys"
v-model:selected-keys="selectedKeys"
block-node
show-line
:data="categoryData as []"
:show-irrelevant-nodes="false"
:pattern="categoryPattern"
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="暂无流程分类" 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:overflow-auto">
<DefinitionSearch v-model:model="searchParams" @reset="resetSearchParams" @search="getDataByPage" />
<NCard :bordered="false" size="small" class="card-wrapper sm:flex-1-hidden">
<template #header>
<NSpace>
<NRadioGroup v-model:value="isPublish" on-up size="small">
<NRadioButton
v-for="(status, index) in isPublishOptions"
:key="index"
:value="status.value"
:label="status.label"
/>
</NRadioGroup>
</NSpace>
</template>
<template #header-extra>
<TableHeaderOperation
v-model:columns="columnChecks"
:disabled-delete="checkedRowKeys.length === 0"
:loading="loading"
:show-delete="hasAuth('workflow:definition:remove')"
@add="handleAdd"
@delete="handleBatchDelete"
@refresh="getData"
>
<template #prefix>
<NButton size="small" ghost @click="handleDeploy">
<template #icon>
<icon-material-symbols:upload-rounded class="text-icon" />
</template>
部署流程文件
</NButton>
</template>
</TableHeaderOperation>
</template>
<NDataTable
v-model:checked-row-keys="checkedRowKeys"
:columns="columns"
:data="data"
size="small"
:flex-height="!appStore.isMobile"
:scroll-x="962"
:loading="loading"
remote
:row-key="row => row.id"
:pagination="mobilePagination"
class="sm:h-full"
/>
<DefinitionOperateDrawer
v-model:visible="drawerVisible"
:operate-type="operateType"
:row-data="editingData"
@submitted="getDataByPage"
/>
<DefinitionImportModal v-model:visible="importVisible" @submitted="getDataByPage" />
</NCard>
</div>
</TableSiderLayout>
</template>
<style scoped></style>

View File

@ -0,0 +1,160 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import type { UploadFileInfo } from 'naive-ui';
import { getToken } from '@/store/modules/auth/shared';
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import { getServiceBaseURL } from '@/utils/service';
import type FileUpload from '@/components/custom/file-upload.vue';
import { $t } from '@/locales';
defineOptions({
name: 'DefinitionImportModal'
});
interface Emits {
(e: 'submitted'): void;
}
const { baseURL } = getServiceBaseURL(import.meta.env);
const headers: Record<string, string> = {
Authorization: `Bearer ${getToken()}`,
clientid: import.meta.env.VITE_APP_CLIENT_ID!
};
const emit = defineEmits<Emits>();
const uploadRef = ref<typeof FileUpload>();
const message = ref<string>('');
const success = ref<boolean>(false);
const visible = defineModel<boolean>('visible', {
default: false
});
const data = ref<Record<string, any>>({
category: undefined
});
const { formRef, validate, restoreValidation } = useNaiveForm();
const { createRequiredRule } = useFormRules();
const rules: Record<string, App.Global.FormRule> = {
category: createRequiredRule('流程分类不能为空')
};
const fileList = ref<UploadFileInfo[]>([]);
function closeDrawer() {
visible.value = false;
if (success.value) {
emit('submitted');
}
}
async function handleSubmit() {
await validate();
if (fileList.value.length === 0) {
window.$message?.error('请选择流程文件');
return;
}
fileList.value.forEach(item => {
item.status = 'pending';
});
await uploadRef.value?.submit();
visible.value = false;
emit('submitted');
}
function isErrorState(xhr: XMLHttpRequest) {
const responseText = xhr?.responseText;
const response = JSON.parse(responseText);
return response.code !== 200;
}
function handleFinish(options: { file: UploadFileInfo; event?: ProgressEvent }) {
const { file, event } = options;
// @ts-expect-error Ignore type errors
const responseText = event?.target?.responseText;
const response = JSON.parse(responseText);
message.value = response.msg;
window.$message?.success($t('common.importSuccess'));
success.value = true;
return file;
}
function handleError(options: { file: UploadFileInfo; event?: ProgressEvent }) {
const { event } = options;
// @ts-expect-error Ignore type errors
const responseText = event?.target?.responseText;
const msg = JSON.parse(responseText).msg;
message.value = msg;
window.$message?.error(msg || $t('common.importFail'));
success.value = false;
}
watch(visible, () => {
if (visible.value) {
fileList.value = [];
success.value = false;
message.value = '';
restoreValidation();
}
});
</script>
<template>
<NModal
v-model:show="visible"
title="部署流程文件"
preset="card"
:bordered="false"
display-directive="show"
class="max-w-90% w-600px"
@close="closeDrawer"
>
<NForm ref="formRef" label-placement="left" :model="data" :rules="rules">
<NFormItem label="流程分类" path="category">
<FlowCategorySelect v-model:value="data.category" />
</NFormItem>
</NForm>
<NUpload
ref="uploadRef"
v-model:file-list="fileList"
class="mt-12px"
:action="`${baseURL}/workflow/definition/importDef`"
:headers="headers"
:data="data"
:max="10"
:file-size="50"
accept=".json"
:multiple="true"
directory-dnd
:default-upload="false"
list-type="text"
:is-error-state="isErrorState"
@finish="handleFinish"
@error="handleError"
>
<NUploadDragger>
<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">请选择 JSON 流程文件上传</NText>
<NP depth="3" class="mt-8px text-center">
仅支持 JSON 格式文件
<br />
PS: 如若部署请部署从本项目模型管理导出的数据
</NP>
</NUploadDragger>
</NUpload>
<template #footer>
<NSpace justify="end" :size="16">
<NButton type="primary" @click="handleSubmit">{{ $t('common.import') }}</NButton>
</NSpace>
</template>
</NModal>
</template>
<style scoped></style>

View File

@ -0,0 +1,214 @@
<script setup lang="ts">
import { computed, reactive, ref, watch } from 'vue';
import type { SelectOption } from 'naive-ui';
import { definitionDesignerModeOptions } from '@/constants/workflow';
import { fetchCreateDefinition, fetchUpdateDefinition } from '@/service/api/workflow/definition';
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import { useDict } from '@/hooks/business/dict';
import { $t } from '@/locales';
defineOptions({
name: 'DefinitionOperateDrawer'
});
interface Props {
/** the type of operation */
operateType: NaiveUI.TableOperateType;
/** the edit row data */
rowData?: Api.Workflow.Definition | null;
}
const props = defineProps<Props>();
interface Emits {
(e: 'submitted'): void;
}
const emit = defineEmits<Emits>();
useDict('sys_yes_no');
const visible = defineModel<boolean>('visible', {
default: false
});
const { formRef, validate, restoreValidation } = useNaiveForm();
const { createRequiredRule } = useFormRules();
const title = computed(() => {
const titles: Record<NaiveUI.TableOperateType, string> = {
add: '新增流程定义',
edit: '编辑流程定义'
};
return titles[props.operateType];
});
const formPaths = ref<SelectOption[]>([]);
const modules = import.meta.glob('@/components/workflow/form/**/*.vue');
Object.keys(modules).forEach(key => {
const label = key.replace('/src/components/workflow/form/', '');
const value = key.replace('/src/components/workflow/form', '/workflow');
formPaths.value.push({ label, value });
});
type Model = Api.Workflow.DefinitionOperateParams;
const model: Model = reactive(createDefaultModel());
/** 是否自动通过 */
const autoPass = ref<boolean>(false);
function createDefaultModel(): Model {
return {
flowCode: '',
flowName: '',
category: '',
formPath: null,
formCustom: 'N',
modelValue: 'CLASSICS',
ext: ''
};
}
type RuleKey = Extract<keyof Model, 'flowCode' | 'flowName' | 'category' | 'modelValue' | 'formCustom'>;
const rules: Record<RuleKey, App.Global.FormRule> = {
flowCode: createRequiredRule('流程编码不能为空'),
flowName: createRequiredRule('流程名称不能为空'),
category: createRequiredRule('流程类别不能为空'),
modelValue: createRequiredRule('设计器模式不能为空'),
formCustom: createRequiredRule('审批表单是否自定义不能为空')
};
function handleUpdateModelWhenEdit() {
if (props.operateType === 'add') {
Object.assign(model, createDefaultModel());
model.formCustom = 'N';
autoPass.value = false;
// 设置默认的 ext JSON
model.ext = JSON.stringify({ autoPass: false });
return;
}
if (props.operateType === 'edit' && props.rowData) {
Object.assign(model, props.rowData);
// 从 ext 字段解析 JSON 并设置 autoPass 值
try {
if (props.rowData.ext) {
const extData = JSON.parse(props.rowData.ext);
autoPass.value = extData.autoPass || false;
} else {
autoPass.value = false;
}
} catch {
// 如果解析失败,设置默认值
autoPass.value = false;
}
}
}
function closeDrawer() {
visible.value = false;
}
async function handleSubmit() {
await validate();
// 将 autoPass 值序列化为 JSON 并存储到 ext 字段
model.ext = JSON.stringify({ autoPass: autoPass.value });
// request
if (props.operateType === 'add') {
const { flowCode, flowName, category, formPath, modelValue, formCustom, ext } = model;
const { error } = await fetchCreateDefinition({
flowCode,
flowName,
category,
formPath: formPath || '',
modelValue,
formCustom,
ext
});
if (error) return;
}
if (props.operateType === 'edit') {
const { id, flowCode, flowName, category, formPath, modelValue, formCustom, ext } = model;
const { error } = await fetchUpdateDefinition({
id,
flowCode,
flowName,
category,
formPath: formPath || '',
modelValue,
formCustom,
ext
});
if (error) return;
}
window.$message?.success($t('common.saveSuccess'));
closeDrawer();
emit('submitted');
}
watch(visible, () => {
if (visible.value) {
handleUpdateModelWhenEdit();
restoreValidation();
}
});
</script>
<template>
<NDrawer v-model:show="visible" :title="title" display-directive="show" :width="800" class="max-w-90%">
<NDrawerContent :title="title" :native-scrollbar="false" closable>
<NForm ref="formRef" :model="model" :rules="rules">
<NFormItem label="流程类别" path="category">
<FlowCategorySelect v-model:value="model.category" placeholder="请选择流程类别" />
</NFormItem>
<NFormItem label="流程编码" path="flowCode">
<NInput v-model:value="model.flowCode" placeholder="请输入流程编码" />
</NFormItem>
<NFormItem label="流程名称" path="flowName">
<NInput v-model:value="model.flowName" placeholder="请输入流程名称" />
</NFormItem>
<NFormItem label="设计器模式" path="modelValue">
<NRadioGroup v-model:value="model.modelValue" :disabled="operateType === 'edit'">
<NSpace>
<NRadioButton
v-for="option in definitionDesignerModeOptions"
:key="option.value"
:value="option.value"
:label="option.label"
/>
</NSpace>
</NRadioGroup>
</NFormItem>
<!-- 流程配置 -->
<NFormItem label="流程配置" path="ext">
<NCheckbox v-model:checked="autoPass" label="下一节点执行人是当前任务处理人自动审批" />
</NFormItem>
<NFormItem label="是否动态表单" path="formCustom">
<DictRadio v-model:value="model.formCustom" dict-code="sys_yes_no" />
</NFormItem>
<NFormItem label="审批表单路径" path="formPath">
<template #label>
<div class="flex-center">
<FormTip content="需要在 /src/components/workflow/form 路径下创建组件" />
<span class="pl-3px">审批表单路径</span>
</div>
</template>
<NSelect v-model:value="model.formPath" :options="formPaths" placeholder="请选择审批表单路径" />
</NFormItem>
</NForm>
<template #footer>
<NSpace :size="16">
<NButton @click="closeDrawer">{{ $t('common.cancel') }}</NButton>
<NButton type="primary" @click="handleSubmit">{{ $t('common.confirm') }}</NButton>
</NSpace>
</template>
</NDrawerContent>
</NDrawer>
</template>
<style scoped></style>

View File

@ -0,0 +1,67 @@
<script setup lang="ts">
import { useNaiveForm } from '@/hooks/common/form';
import { $t } from '@/locales';
defineOptions({
name: 'DefinitionSearch'
});
interface Emits {
(e: 'reset'): void;
(e: 'search'): void;
}
const emit = defineEmits<Emits>();
const { formRef, validate, restoreValidation } = useNaiveForm();
const model = defineModel<Api.Workflow.DefinitionSearchParams>('model', { required: true });
async function reset() {
Object.assign(model.value.params!, {});
await restoreValidation();
emit('reset');
}
async function search() {
await validate();
emit('search');
}
</script>
<template>
<NCard :bordered="false" size="small" class="card-wrapper">
<NCollapse>
<NCollapseItem :title="$t('common.search')" name="user-search">
<NForm ref="formRef" :model="model" label-placement="left" :label-width="80">
<NGrid responsive="screen" item-responsive>
<NFormItemGi span="24 s:12 m:8" label="流程编码" path="flowCode" class="pr-24px">
<NInput v-model:value="model.flowCode" placeholder="请输入流程编码" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:8" label="流程名称" path="flowName" class="pr-24px">
<NInput v-model:value="model.flowName" placeholder="请输入流程名称" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:8" class="pr-24px">
<NSpace class="w-full" justify="end">
<NButton @click="reset">
<template #icon>
<icon-ic-round-refresh class="text-icon" />
</template>
{{ $t('common.reset') }}
</NButton>
<NButton type="primary" ghost @click="search">
<template #icon>
<icon-ic-round-search class="text-icon" />
</template>
{{ $t('common.search') }}
</NButton>
</NSpace>
</NFormItemGi>
</NGrid>
</NForm>
</NCollapseItem>
</NCollapse>
</NCard>
</template>
<style scoped></style>

View File

@ -0,0 +1,428 @@
<script setup lang="tsx">
import { computed, reactive, ref, shallowRef, watch } from 'vue';
import { NButton, NDivider, NEmpty, NInput, NRadioButton, NRadioGroup, NSpin, NTag } from 'naive-ui';
import { useBoolean, useLoading } from '@sa/hooks';
import { workflowActivityStatusRecord } from '@/constants/workflow';
import { fetchGetCategoryTree } from '@/service/api/workflow/category';
import {
fetchBatchDeleteInstance,
fetchFlowInvalidOperate,
fetchGetFinishedInstanceList,
fetchGetRunningInstanceList
} from '@/service/api/workflow/instance';
import { useAppStore } from '@/store/modules/app';
import { useTable, useTableOperate } from '@/hooks/common/table';
import { useDict } from '@/hooks/business/dict';
import { loadDynamicComponent } from '@/utils/common';
import DictTag from '@/components/custom/dict-tag.vue';
import { $t } from '@/locales';
import ButtonIcon from '@/components/custom/button-icon.vue';
import InstanceSearch from './modules/process-instance-search.vue';
import InstanceVariableModal from './modules/process-instance-variable-modal.vue';
const dynamicComponent = shallowRef();
interface RunningStatusOption {
label: string;
value: boolean;
}
defineOptions({
name: 'InstanceList'
});
useDict('wf_business_status');
const appStore = useAppStore();
const { bool: variableVisible, setTrue: showVariableDrawer } = useBoolean(false);
const { bool: previewVisible, setTrue: showPreviewDrawer } = useBoolean(false);
const runningStatus = ref<boolean>(true);
const runningStatusOptions = ref<RunningStatusOption[]>([
{
label: '运行中',
value: true
},
{
label: '已完成',
value: false
}
]);
type CancelModel = Api.Workflow.FlowInvalidOperateParams;
const cancelModel: CancelModel = reactive(createDefaultModel());
function createDefaultModel(): CancelModel {
return {
id: null,
comment: ''
};
}
// 基础列
const baseColumns = ref<NaiveUI.TableColumn<Api.Workflow.Instance>[]>([
{
type: 'selection',
align: 'center',
width: 48
},
{
key: 'businessCode',
title: '业务编码',
align: 'center',
width: 120
},
{
key: 'businessTitle',
title: '业务名称',
align: 'center',
width: 120
},
{
key: 'flowName',
title: '流程名称',
align: 'center',
width: 120
},
{
key: 'nodeName',
title: '任务名称',
align: 'center',
minWidth: 120
},
{
key: 'flowCode',
title: '流程编码',
align: 'center',
minWidth: 120
},
{
key: 'categoryName',
title: '流程分类',
align: 'center',
minWidth: 120,
render: row => <NTag type="default">{row.categoryName}</NTag>
},
{
key: 'createByName',
title: '申请人',
align: 'center',
minWidth: 120
},
{
key: 'version',
title: '版本号',
align: 'center',
width: 80,
render: row => <NTag type="info">v{row.version}.0</NTag>
},
{
key: 'activityStatus',
title: '状态',
align: 'center',
minWidth: 80,
render(row) {
return (
<NTag type={row.activityStatus === 0 ? 'warning' : 'success'}>
{workflowActivityStatusRecord[row.activityStatus]}
</NTag>
);
}
},
{
key: 'flowStatus',
title: '流程状态',
align: 'center',
minWidth: 80,
render(row) {
return <DictTag value={row.flowStatus} dictCode="wf_business_status" />;
}
},
{
key: 'createTime',
title: '启动时间',
align: 'center',
minWidth: 150
}
]);
// 完成列
const finishColumns = ref<NaiveUI.TableColumn<Api.Workflow.Instance>[]>([
{
key: 'updateTime',
title: '结束时间',
align: 'center',
minWidth: 150
}
]);
// 操作列
const operateColumns = ref<NaiveUI.TableColumn<Api.Workflow.Instance>[]>([
{
key: 'operate',
title: $t('common.operate'),
align: 'center',
fixed: 'right',
width: 155,
render: row => {
const id = row.id;
const showAll = runningStatus.value;
const buttons = [];
buttons.push(
<ButtonIcon
text
type="info"
icon="material-symbols:visibility-outline"
tooltipContent="流程预览"
onClick={() => handlePreview(row)}
/>
);
buttons.push(
<ButtonIcon
text
type="info"
icon="material-symbols:variable-insert"
tooltipContent="流程变量"
onClick={() => handleShowVariable(id)}
/>
);
if (showAll) {
buttons.push(
<ButtonIcon
text
type="error"
showPopconfirmIcon={false}
icon="material-symbols:cancel-outline-rounded"
tooltipContent="作废流程"
popconfirmContent={
<NInput v-model:value={cancelModel.comment} size="large" type="textarea" placeholder="请输入作废原因" />
}
onPositiveClick={() => handleCancel(id)}
/>
);
}
if (showAll) {
buttons.push(
<ButtonIcon
text
type="error"
icon="material-symbols:delete-outline"
tooltipContent={$t('common.delete')}
popconfirmContent={$t('common.confirmDelete')}
onPositiveClick={() => handleDelete(id)}
/>
);
}
return (
<div class="flex-center gap-1px">
{buttons.map((btn, index) => (index > 0 ? [<NDivider vertical />, btn] : btn))}
</div>
);
}
}
]);
const {
columns,
columnChecks,
reloadColumns,
data,
getData,
getDataByPage,
loading,
mobilePagination,
searchParams,
resetSearchParams,
updateApiFn
} = useTable({
apiFn: fetchGetRunningInstanceList,
apiParams: {
pageNum: 1,
pageSize: 10,
category: null,
flowName: null,
flowCode: null,
nodeName: null,
createByIds: null
},
columns: () =>
runningStatus.value
? [...baseColumns.value, ...operateColumns.value]
: [...baseColumns.value, ...finishColumns.value, ...operateColumns.value]
});
const { checkedRowKeys, onBatchDeleted, onDeleted } = useTableOperate(data, getData);
// 监听运行状态变化
watch(runningStatus, async () => {
const newApiFn = runningStatus.value ? fetchGetRunningInstanceList : fetchGetFinishedInstanceList;
updateApiFn(newApiFn);
await getDataByPage();
reloadColumns();
});
const { loading: treeLoading, startLoading: startTreeLoading, endLoading: endTreeLoading } = useLoading();
const categoryPattern = ref<string>();
const categoryData = ref<Api.Common.CommonTreeRecord>([]);
const selectedKeys = ref<string[]>([]);
const expandedKeys = ref<CommonType.IdType[]>(['100']);
const instanceRowId = ref<CommonType.IdType>();
const selectable = computed(() => {
return !loading.value;
});
async function getTreeData() {
startTreeLoading();
const { data: tree, error } = await fetchGetCategoryTree();
if (!error) {
categoryData.value = tree;
}
endTreeLoading();
}
getTreeData();
function handleClickTree(keys: string[]) {
searchParams.category = keys.length ? keys[0] : null;
checkedRowKeys.value = [];
getDataByPage();
}
function handleResetTreeData() {
categoryPattern.value = undefined;
getTreeData();
}
function handleResetSearch() {
resetSearchParams();
selectedKeys.value = [];
}
async function handleBatchDelete() {
// request
const { error } = await fetchBatchDeleteInstance(checkedRowKeys.value);
if (error) return;
onBatchDeleted();
}
async function handleDelete(instanceId: CommonType.IdType) {
// request
const { error } = await fetchBatchDeleteInstance([instanceId]);
if (error) return;
onDeleted();
}
async function handleCancel(instanceId: CommonType.IdType) {
cancelModel.id = instanceId;
// request
const { error } = await fetchFlowInvalidOperate(cancelModel);
if (error) return;
window.$message?.success('作废成功');
getDataByPage();
}
async function handleShowVariable(id: CommonType.IdType) {
instanceRowId.value = id;
showVariableDrawer();
}
const modules = import.meta.glob('@/components/workflow/form/**/*.vue');
const businessId = ref<CommonType.IdType>();
/** 流程预览,动态加载组件 */
async function handlePreview(row: Api.Workflow.Instance) {
dynamicComponent.value = null;
previewVisible.value = false;
businessId.value = row.businessId;
const formPath = row.formPath;
if (!formPath) return;
dynamicComponent.value = await loadDynamicComponent(modules, formPath);
setTimeout(() => {
showPreviewDrawer();
}, 300);
}
</script>
<template>
<TableSiderLayout sider-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="categoryPattern" clearable :placeholder="$t('common.keywordSearch')" />
<NSpin class="category-tree" :show="treeLoading">
<NTree
v-model:selected-keys="selectedKeys"
v-model:expanded-keys="expandedKeys"
block-node
show-line
:data="categoryData as []"
:show-irrelevant-nodes="false"
:pattern="categoryPattern"
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="暂无分类信息" 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:overflow-auto">
<InstanceSearch v-model:model="searchParams" @reset="handleResetSearch" @search="getDataByPage" />
<NCard :bordered="false" size="small" class="card-wrapper sm:flex-1-hidden">
<template #header>
<NSpace>
<NRadioGroup v-model:value="runningStatus" on-up size="small">
<NRadioButton
v-for="(status, index) in runningStatusOptions"
:key="index"
:value="status.value"
:label="status.label"
/>
</NRadioGroup>
</NSpace>
</template>
<template #header-extra>
<TableHeaderOperation
v-model:columns="columnChecks"
:disabled-delete="checkedRowKeys.length === 0"
:loading="loading"
:show-add="false"
:show-delete="true"
:show-export="false"
@delete="handleBatchDelete"
@refresh="getData"
>
<template #prefix></template>
</TableHeaderOperation>
</template>
<NDataTable
v-model:checked-row-keys="checkedRowKeys"
:columns="columns"
:data="data"
size="small"
:flex-height="!appStore.isMobile"
:scroll-x="1405"
:loading="loading"
remote
:row-key="row => row.id"
:pagination="mobilePagination"
class="sm:h-full"
/>
<component :is="dynamicComponent" :visible="previewVisible" operate-type="detail" :business-id="businessId" />
<InstanceVariableModal v-model:visible="variableVisible" :instance-id="instanceRowId!" />
</NCard>
</div>
</TableSiderLayout>
</template>

View File

@ -0,0 +1,63 @@
<script setup lang="tsx">
import { useNaiveForm } from '@/hooks/common/form';
defineOptions({
name: 'WorkflowInstanceSearch'
});
interface Emits {
(e: 'reset'): void;
(e: 'search'): void;
}
const emit = defineEmits<Emits>();
const { formRef, validate, restoreValidation } = useNaiveForm();
const model = defineModel<Api.Workflow.InstanceSearchParams>('model', { required: true });
async function reset() {
await restoreValidation();
emit('reset');
}
async function search() {
await validate();
emit('search');
}
</script>
<template>
<NCard :bordered="false" size="small" class="card-wrapper">
<NCollapse>
<NCollapseItem :title="$t('common.search')">
<NForm ref="formRef" :model="model" label-placement="left" :label-width="100">
<NGrid responsive="screen" item-responsive>
<NFormItemGi span="24 s:12 m:6" label="任务名称" path="nodeName" class="pr-24px">
<NInput v-model:value="model.nodeName" placeholder="请输入任务名称" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="流程定义名称" path="bucketName" class="pr-24px">
<NInput v-model:value="model.flowName" placeholder="请输入流程定义名称" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="流程定义编码" path="flowCode" class="pr-24px">
<NInput v-model:value="model.flowCode" placeholder="请输入流程定义编码" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" class="pr-24px">
<NSpace class="w-full" justify="end">
<NButton @click="reset">
<template #icon>
<icon-ic-round-refresh class="text-icon" />
</template>
{{ $t('common.reset') }}
</NButton>
<NButton type="primary" ghost @click="search">
<template #icon>
<icon-ic-round-search class="text-icon" />
</template>
{{ $t('common.search') }}
</NButton>
</NSpace>
</NFormItemGi>
</NGrid>
</NForm>
</NCollapseItem>
</NCollapse>
</NCard>
</template>

View File

@ -0,0 +1,123 @@
<script setup lang="ts">
import { reactive, ref, watch } from 'vue';
import { useLoading } from '@sa/hooks';
import { fetchGetInstanceVariable, fetchUpdateInstanceVariable } from '@/service/api/workflow/instance';
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import JsonPreview from '@/components/custom/json-preview.vue';
import { $t } from '@/locales';
defineOptions({
name: 'InstanceVariableModal'
});
interface Props {
instanceId: CommonType.IdType;
}
const props = defineProps<Props>();
const { formRef, validate, restoreValidation } = useNaiveForm();
const { createRequiredRule } = useFormRules();
const { loading: updateLoading, startLoading: startUpdateLoading, endLoading: endUpdateLoading } = useLoading();
const visible = defineModel<boolean>('visible', {
default: false
});
const variableOptions = ref<CommonType.Option<string>[]>([]);
const instanceVariableInfo = ref<Api.Workflow.InstanceVariableInfo>();
type Model = Api.Workflow.InstanceVariableOperateParams;
const model: Model = reactive(createDefaultModel());
function createDefaultModel(): Model {
return {
instanceId: props.instanceId,
key: null,
value: ''
};
}
type RuleKey = Extract<keyof Model, 'key' | 'value'>;
const rules: Record<RuleKey, App.Global.FormRule> = {
key: createRequiredRule('变量KEY不能为空'),
value: createRequiredRule('变量值不能为空')
};
function closeDrawer() {
visible.value = false;
}
async function getInstanceVariable() {
startUpdateLoading();
const { error, data } = await fetchGetInstanceVariable(props.instanceId);
if (error) return;
instanceVariableInfo.value = data;
if (data.variableList) {
variableOptions.value = data.variableList.map(item => ({
label: item.key!,
value: item.key!
}));
}
endUpdateLoading();
}
function handleUpdateModelWhenEdit() {
Object.assign(model, createDefaultModel());
}
async function handleSubmit() {
await validate();
// request
const { error } = await fetchUpdateInstanceVariable(model);
if (error) return;
window.$message?.success($t('common.updateSuccess'));
await getInstanceVariable();
}
watch(visible, () => {
if (visible.value) {
getInstanceVariable();
handleUpdateModelWhenEdit();
restoreValidation();
}
});
</script>
<template>
<NModal
v-model:show="visible"
title="流程变量"
preset="card"
:bordered="false"
display-directive="show"
class="max-w-90% w-600px"
@close="closeDrawer"
>
<NSpin :show="updateLoading">
<NSpace vertical :size="16">
<div class="max-h-300px overflow-auto">
<JsonPreview :code="instanceVariableInfo?.variable as string" />
</div>
<NDivider>变量管理</NDivider>
<NForm ref="formRef" :model="model" :rules="rules">
<NGrid responsive="screen" item-responsive>
<NFormItemGi span="24 s:12 m:12" label="变量KEY" path="key" class="pr-24px">
<NSelect v-model:value="model.key" clearable :options="variableOptions" placeholder="请选择变量KEY" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:12" label="变量值" path="value" class="pr-24px">
<NInput v-model:value="model.value" placeholder="请输入变量值" />
</NFormItemGi>
</NGrid>
</NForm>
</NSpace>
</NSpin>
<template #footer>
<NSpace justify="end" :size="16">
<NButton type="primary" @click="handleSubmit">{{ $t('common.confirm') }}</NButton>
<NButton @click="closeDrawer">{{ $t('common.close') }}</NButton>
</NSpace>
</template>
</NModal>
</template>

View File

@ -0,0 +1,221 @@
<script setup lang="tsx">
import { NDivider } from 'naive-ui';
import { fetchBatchDeleteSpel, fetchGetSpelList } from '@/service/api/workflow/spel';
import { useAppStore } from '@/store/modules/app';
import { useAuth } from '@/hooks/business/auth';
import { useDownload } from '@/hooks/business/download';
import { useTable, useTableOperate } from '@/hooks/common/table';
import { $t } from '@/locales';
import ButtonIcon from '@/components/custom/button-icon.vue';
import DictTag from '@/components/custom/dict-tag.vue';
import TagGroup from '@/components/custom/tag-group.vue';
import SpelOperateDrawer from './modules/spel-operate-drawer.vue';
import SpelSearch from './modules/spel-search.vue';
defineOptions({
name: 'SpelList'
});
const appStore = useAppStore();
const { download } = useDownload();
const { hasAuth } = useAuth();
const {
columns,
columnChecks,
data,
getData,
getDataByPage,
loading,
mobilePagination,
searchParams,
resetSearchParams
} = useTable({
apiFn: fetchGetSpelList,
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
componentName: null,
methodName: null,
status: null,
params: {}
},
columns: () => [
{
type: 'selection',
align: 'center',
width: 48
},
{
key: 'index',
title: $t('common.index'),
align: 'center',
width: 64
},
{
key: 'componentName',
title: '组件名称',
align: 'center',
minWidth: 120
},
{
key: 'methodName',
title: '方法名',
align: 'center',
minWidth: 120
},
{
key: 'methodParams',
title: '参数',
align: 'center',
minWidth: 120,
render: row => {
return <TagGroup threadshold={4} value={row.methodParams} />;
}
},
{
key: 'viewSpel',
title: 'spel表达式',
align: 'center',
minWidth: 120
},
{
key: 'remark',
title: '备注',
align: 'center',
minWidth: 120
},
{
key: 'status',
title: '状态',
align: 'center',
minWidth: 120,
render: row => {
return <DictTag size="small" value={row.status} dict-code="sys_normal_disable" />;
}
},
{
key: 'operate',
title: $t('common.operate'),
align: 'center',
width: 130,
render: row => {
const divider = () => {
if (!hasAuth('workflow:spel:edit') || !hasAuth('workflow:spel:remove')) {
return null;
}
return <NDivider vertical />;
};
const editBtn = () => {
if (!hasAuth('workflow:spel:edit')) {
return null;
}
return (
<ButtonIcon
text
type="primary"
icon="material-symbols:drive-file-rename-outline-outline"
tooltipContent={$t('common.edit')}
onClick={() => edit(row.id!)}
/>
);
};
const deleteBtn = () => {
if (!hasAuth('workflow:spel:remove')) {
return null;
}
return (
<ButtonIcon
text
type="error"
icon="material-symbols:delete-outline"
tooltipContent={$t('common.delete')}
popconfirmContent={$t('common.confirmDelete')}
onPositiveClick={() => handleDelete(row.id!)}
/>
);
};
return (
<div class="flex-center gap-8px">
{editBtn()}
{divider()}
{deleteBtn()}
</div>
);
}
}
]
});
const { drawerVisible, operateType, editingData, handleAdd, handleEdit, checkedRowKeys, onBatchDeleted, onDeleted } =
useTableOperate(data, getData);
async function handleBatchDelete() {
// request
const { error } = await fetchBatchDeleteSpel(checkedRowKeys.value);
if (error) return;
onBatchDeleted();
}
async function handleDelete(id: CommonType.IdType) {
// request
const { error } = await fetchBatchDeleteSpel([id]);
if (error) return;
onDeleted();
}
function edit(id: CommonType.IdType) {
handleEdit('id', id);
}
function handleExport() {
download('/workflow/spel/export', searchParams, `流程spel达式定义_${new Date().getTime()}.xlsx`);
}
</script>
<template>
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
<SpelSearch v-model:model="searchParams" @reset="resetSearchParams" @search="getDataByPage" />
<NCard title="流程表达式列表" :bordered="false" size="small" class="card-wrapper sm:flex-1-hidden">
<template #header-extra>
<TableHeaderOperation
v-model:columns="columnChecks"
:disabled-delete="checkedRowKeys.length === 0"
:loading="loading"
:show-add="hasAuth('workflow:spel:add')"
:show-delete="hasAuth('workflow:spel:remove')"
:show-export="hasAuth('workflow:spel:export')"
@add="handleAdd"
@delete="handleBatchDelete"
@export="handleExport"
@refresh="getData"
/>
</template>
<NDataTable
v-model:checked-row-keys="checkedRowKeys"
:columns="columns"
:data="data"
size="small"
:flex-height="!appStore.isMobile"
:scroll-x="962"
:loading="loading"
remote
:row-key="row => row.id"
:pagination="mobilePagination"
class="sm:h-full"
/>
<SpelOperateDrawer
v-model:visible="drawerVisible"
:operate-type="operateType"
:row-data="editingData"
@submitted="getDataByPage"
/>
</NCard>
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,179 @@
<script setup lang="ts">
import { computed, reactive, ref, watch } from 'vue';
import { fetchCreateSpel, fetchUpdateSpel } from '@/service/api/workflow/spel';
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import { useDict } from '@/hooks/business/dict';
import { $t } from '@/locales';
defineOptions({
name: 'SpelOperateDrawer'
});
interface Props {
/** the type of operation */
operateType: NaiveUI.TableOperateType;
/** the edit row data */
rowData?: Api.Workflow.Spel | null;
}
const props = defineProps<Props>();
interface Emits {
(e: 'submitted'): void;
}
const emit = defineEmits<Emits>();
const visible = defineModel<boolean>('visible', {
default: false
});
const { options: sysNormalDisableOptions } = useDict('sys_normal_disable');
const { formRef, validate, restoreValidation } = useNaiveForm();
const { createRequiredRule } = useFormRules();
const title = computed(() => {
const titles: Record<NaiveUI.TableOperateType, string> = {
add: '新增流程表达式',
edit: '编辑流程表达式'
};
return titles[props.operateType];
});
// 参数标签
const methodParamTags = ref<string[]>([]);
type Model = Api.Workflow.SpelOperateParams;
const model: Model = reactive(createDefaultModel());
function createDefaultModel(): Model {
return {
componentName: '',
methodName: '',
methodParams: '',
viewSpel: '',
remark: '',
status: '0'
};
}
type RuleKey = Extract<keyof Model, 'id'>;
const rules: Record<RuleKey, App.Global.FormRule> = {
id: createRequiredRule('主键id不能为空')
};
function handleUpdateModelWhenEdit() {
if (props.operateType === 'add') {
Object.assign(model, createDefaultModel());
methodParamTags.value = [];
return;
}
if (props.operateType === 'edit' && props.rowData) {
Object.assign(model, props.rowData);
// 如果有参数,将逗号分隔的字符串转为数组
methodParamTags.value = model.methodParams ? model.methodParams.split(',') : [];
}
}
// 实时更新 SPEL 表达式
function updateSpelExpression() {
if (!model.componentName || !model.methodName) {
model.viewSpel = '';
return;
}
// 构建参数部分
const params = methodParamTags.value.map(param => `#${param}`).join(',');
// 生成 SPEL 表达式: #{@组件名.方法名(#参数1,#参数2,...)}
model.viewSpel = `#{@${model.componentName}.${model.methodName}(${params})}`;
}
// 监听组件名、方法名和参数变化
watch(() => model.componentName, updateSpelExpression);
watch(() => model.methodName, updateSpelExpression);
watch(methodParamTags, updateSpelExpression, { deep: true });
function closeDrawer() {
visible.value = false;
}
async function handleSubmit() {
await validate();
// 将参数标签数组转为逗号分隔的字符串
model.methodParams = methodParamTags.value.join(',');
const { id, componentName, methodName, methodParams, viewSpel, remark, status } = model;
// request
if (props.operateType === 'add') {
const { error } = await fetchCreateSpel({ componentName, methodName, methodParams, viewSpel, remark, status });
if (error) return;
}
if (props.operateType === 'edit') {
const { error } = await fetchUpdateSpel({ id, componentName, methodName, methodParams, viewSpel, remark, status });
if (error) return;
}
window.$message?.success($t('common.updateSuccess'));
closeDrawer();
emit('submitted');
}
watch(visible, () => {
if (visible.value) {
handleUpdateModelWhenEdit();
restoreValidation();
}
});
</script>
<template>
<NDrawer v-model:show="visible" :title="title" display-directive="show" :width="800" class="max-w-90%">
<NDrawerContent :title="title" :native-scrollbar="false" closable>
<NForm ref="formRef" :model="model" :rules="rules">
<NFormItem label="组件名称" path="componentName">
<NInput v-model:value="model.componentName" placeholder="请输入组件名称" />
</NFormItem>
<NFormItem label="方法名" path="methodName">
<NInput v-model:value="model.methodName" placeholder="请输入方法名" />
</NFormItem>
<NFormItem label="参数" path="methodParams">
<NDynamicTags v-model:value="methodParamTags" placeholder="请输入参数后回车" />
</NFormItem>
<NFormItem label="spel表达式" path="viewSpel">
<NInput v-model:value="model.viewSpel" placeholder="自动生成的spel表达式" disabled />
</NFormItem>
<NFormItem label="备注" path="remark">
<NInput v-model:value="model.remark" placeholder="请输入备注" />
</NFormItem>
<NFormItem label="状态" path="status">
<NRadioGroup v-model:value="model.status">
<NSpace>
<NRadio
v-for="option in sysNormalDisableOptions"
:key="option.value"
:value="option.value"
:label="option.label"
/>
</NSpace>
</NRadioGroup>
</NFormItem>
</NForm>
<template #footer>
<NSpace :size="16">
<NButton @click="closeDrawer">{{ $t('common.cancel') }}</NButton>
<NButton type="primary" @click="handleSubmit">{{ $t('common.confirm') }}</NButton>
</NSpace>
</template>
</NDrawerContent>
</NDrawer>
</template>
<style scoped></style>

View File

@ -0,0 +1,77 @@
<script setup lang="ts">
import { useNaiveForm } from '@/hooks/common/form';
import { useDict } from '@/hooks/business/dict';
import { $t } from '@/locales';
defineOptions({
name: 'SpelSearch'
});
interface Emits {
(e: 'reset'): void;
(e: 'search'): void;
}
const emit = defineEmits<Emits>();
const { formRef, validate, restoreValidation } = useNaiveForm();
const model = defineModel<Api.Workflow.SpelSearchParams>('model', { required: true });
const { options: sysNormalDisableOptions } = useDict('sys_normal_disable', false);
async function reset() {
Object.assign(model.value.params!, {});
await restoreValidation();
emit('reset');
}
async function search() {
await validate();
emit('search');
}
</script>
<template>
<NCard :bordered="false" size="small" class="card-wrapper">
<NCollapse>
<NCollapseItem :title="$t('common.search')" name="user-search">
<NForm ref="formRef" :model="model" label-placement="left" :label-width="80">
<NGrid responsive="screen" item-responsive>
<NFormItemGi span="24 s:12 m:6" label="组件名称" path="componentName" class="pr-24px">
<NInput v-model:value="model.componentName" placeholder="请输入组件名称" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="方法名" path="methodName" class="pr-24px">
<NInput v-model:value="model.methodName" placeholder="请输入方法名" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="状态" path="status" class="pr-24px">
<NSelect
v-model:value="model.status"
placeholder="请选择状态"
:options="sysNormalDisableOptions"
clearable
/>
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" class="pr-24px">
<NSpace class="w-full" justify="end">
<NButton @click="reset">
<template #icon>
<icon-ic-round-refresh class="text-icon" />
</template>
{{ $t('common.reset') }}
</NButton>
<NButton type="primary" ghost @click="search">
<template #icon>
<icon-ic-round-search class="text-icon" />
</template>
{{ $t('common.search') }}
</NButton>
</NSpace>
</NFormItemGi>
</NGrid>
</NForm>
</NCollapseItem>
</NCollapse>
</NCard>
</template>
<style scoped></style>

View File

@ -0,0 +1,365 @@
<script setup lang="tsx">
import { computed, ref, shallowRef, watch } from 'vue';
import { NButton, NDivider, NEmpty, NInput, NRadioButton, NRadioGroup, NTag } from 'naive-ui';
import { useBoolean, useLoading } from '@sa/hooks';
import { fetchGetAllFinishedTask, fetchGetAllWaitingTask, fetchTaskAssignee } from '@/service/api/workflow/task';
import { fetchGetCategoryTree } from '@/service/api/workflow/category';
import { useAppStore } from '@/store/modules/app';
import { useTable, useTableOperate } from '@/hooks/common/table';
import { useDict } from '@/hooks/business/dict';
import { loadDynamicComponent } from '@/utils/common';
import TagGroup from '@/components/custom/tag-group.vue';
import DictTag from '@/components/custom/dict-tag.vue';
import ButtonIcon from '@/components/custom/button-icon.vue';
import { $t } from '@/locales';
import AllTaskWaitingSearch from './modules/all-task-waiting-search.vue';
interface WaitingStatusOption {
label: string;
value: boolean;
}
defineOptions({
name: 'AllTaskWaitingList'
});
useDict('wf_business_status');
useDict('wf_task_status');
const appStore = useAppStore();
const { bool: viewVisible, setTrue: showViewDrawer } = useBoolean();
const { bool: interveneVisible, setTrue: showInterveneDrawer } = useBoolean();
const { bool: urgeVisible, setTrue: showUrgeModal } = useBoolean();
const { bool: assigneeVisible, setTrue: showAssigneeModal } = useBoolean();
const dynamicComponent = shallowRef();
const waitingStatus = ref<boolean>(true);
const waitingStatusOptions = ref<WaitingStatusOption[]>([
{ label: '待办任务', value: true },
{ label: '已办任务', value: false }
]);
const commonColumns: NaiveUI.TableColumn<Api.Workflow.TaskOrHisTask>[] = [
{ type: 'selection', align: 'center', width: 50 },
{ key: 'businessCode', title: '业务编码', align: 'center', width: 120 },
{ key: 'businessTitle', title: '业务名称', align: 'center', width: 120 },
{ key: 'flowName', title: '流程定义名称', align: 'center', width: 120 },
{ key: 'flowCode', title: '流程定义编码', align: 'center', width: 120 },
{
key: 'categoryName',
title: '流程分类',
align: 'center',
width: 120,
render: row => <NTag>{row.categoryName}</NTag>
},
{
key: 'version',
title: '版本号',
align: 'center',
width: 120,
render: row => <NTag type="info">v{row.version}.0</NTag>
},
{ key: 'nodeName', title: '任务名称', align: 'center', width: 120 },
{ key: 'createByName', title: '申请人', align: 'center', width: 120 },
{
key: 'flowStatus',
title: '流程状态',
align: 'center',
width: 120,
render: row => <DictTag size="small" value={row.flowStatus} dict-code="wf_business_status" />
}
];
const waitingColumns = ref<NaiveUI.TableColumn<Api.Workflow.Task>[]>([
...(commonColumns as NaiveUI.TableColumn<Api.Workflow.Task>[]),
{
key: 'assigneeNames',
title: '办理人',
align: 'center',
width: 120,
render: row => <TagGroup value={row.assigneeNames} />
},
{ key: 'createTime', title: '创建时间', align: 'center', width: 120 }
]);
const finishColumns = ref<NaiveUI.TableColumn<Api.Workflow.HisTask>[]>([
...(commonColumns as NaiveUI.TableColumn<Api.Workflow.HisTask>[]),
{
key: 'approveName',
title: '办理人',
align: 'center',
width: 120,
render: row => <NTag type="info">{row.approveName}</NTag>
},
{
key: 'flowTaskStatus',
title: '任务状态',
align: 'center',
width: 120,
render: row => <DictTag size="small" value={row.flowTaskStatus} dict-code="wf_task_status" />
},
{ key: 'createTime', title: '创建时间', align: 'center', width: 120 }
]);
const operateColumns = ref<NaiveUI.TableColumn<Api.Workflow.TaskOrHisTask>[]>([
{
key: 'operate',
title: $t('common.operate'),
align: 'center',
fixed: 'right',
width: 100,
render: row => {
const buttons = [
<ButtonIcon
text
type="info"
icon="material-symbols:visibility-outline"
tooltipContent="查看"
onClick={() => handleView(row)}
/>
];
if (waitingStatus.value && row.flowStatus !== 'draft') {
buttons.push(
<ButtonIcon
text
type="info"
icon="material-symbols:edit-document"
tooltipContent="流程干预"
onClick={() => handleIntervene(row as Api.Workflow.Task)}
/>
);
}
return (
<div class="flex-center gap-8px">
{buttons.map((btn, index) => (index > 0 ? [<NDivider vertical />, btn] : btn))}
</div>
);
}
}
]);
const {
columns,
reloadColumns,
columnChecks,
data,
getData,
getDataByPage,
loading,
mobilePagination,
searchParams,
resetSearchParams,
updateApiFn
} = useTable({
apiFn: fetchGetAllWaitingTask,
apiParams: {
pageNum: 1,
pageSize: 10,
category: null,
flowName: null,
flowCode: null,
nodeName: null,
createByIds: null
},
columns: () => {
const baseColumns = waitingStatus.value ? waitingColumns.value : finishColumns.value;
return [...baseColumns, ...operateColumns.value] as NaiveUI.TableColumn<Api.Workflow.TaskOrHisTask>[];
}
});
const { checkedRowKeys } = useTableOperate(data, getData);
watch(waitingStatus, async () => {
const newApiFn = waitingStatus.value ? fetchGetAllWaitingTask : fetchGetAllFinishedTask;
// @ts-expect-error - This is a workaround for the type issue
updateApiFn(newApiFn);
await getDataByPage();
reloadColumns();
});
const { loading: treeLoading, startLoading: startTreeLoading, endLoading: endTreeLoading } = useLoading();
const categoryPattern = ref<string>();
const categoryData = ref<Api.Common.CommonTreeRecord>([]);
const selectedKeys = ref<string[]>([]);
const expandedKeys = ref<CommonType.IdType[]>(['100']);
const selectable = computed(() => !loading.value);
async function getTreeData() {
startTreeLoading();
const { data: tree, error } = await fetchGetCategoryTree();
if (!error) {
categoryData.value = tree;
}
endTreeLoading();
}
getTreeData();
function handleClickTree(keys: string[]) {
searchParams.category = keys.length ? keys[0] : null;
checkedRowKeys.value = [];
getDataByPage();
}
function handleResetTreeData() {
categoryPattern.value = undefined;
getTreeData();
}
function handleResetSearch() {
resetSearchParams();
selectedKeys.value = [];
}
const modules = import.meta.glob('@/components/workflow/**/*.vue');
const businessId = ref<CommonType.IdType>();
async function handleView(row: Api.Workflow.TaskOrHisTask) {
dynamicComponent.value = null;
viewVisible.value = false;
businessId.value = row.businessId;
const formPath = row.formPath;
if (!formPath) return;
dynamicComponent.value = await loadDynamicComponent(modules, formPath);
setTimeout(() => {
showViewDrawer();
}, 300);
}
const taskId = ref<CommonType.IdType>('');
const assigneeIds = ref<CommonType.IdType[]>([]);
const assigneeNames = ref<string[]>([]);
function handleIntervene(row: Api.Workflow.Task) {
taskId.value = row.id;
assigneeIds.value = row.assigneeIds?.split(',') || [];
assigneeNames.value = row.assigneeNames?.split(',') || [];
showInterveneDrawer();
}
async function handleAssigneeConfirm(userIds: CommonType.IdType[]) {
const { error } = await fetchTaskAssignee(checkedRowKeys.value, userIds[0]);
if (error) return;
window.$message?.success('修改办理人成功');
assigneeVisible.value = false;
await getData();
}
</script>
<template>
<TableSiderLayout sider-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="categoryPattern" clearable :placeholder="$t('common.keywordSearch')" />
<NSpin class="category-tree" :show="treeLoading">
<NTree
v-model:selected-keys="selectedKeys"
v-model:expanded-keys="expandedKeys"
block-node
show-line
:data="categoryData as []"
:show-irrelevant-nodes="false"
:pattern="categoryPattern"
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="暂无分类信息" 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:overflow-auto">
<AllTaskWaitingSearch v-model:model="searchParams" @reset="handleResetSearch" @search="getDataByPage" />
<NCard :bordered="false" size="small" class="card-wrapper sm:flex-1-hidden">
<template #header>
<NSpace>
<NRadioGroup v-model:value="waitingStatus" on-up size="small">
<NRadioButton
v-for="(status, index) in waitingStatusOptions"
:key="index"
:value="status.value"
:label="status.label"
/>
</NRadioGroup>
</NSpace>
</template>
<template #header-extra>
<TableHeaderOperation
v-model:columns="columnChecks"
:disabled-delete="checkedRowKeys.length === 0"
:loading="loading"
:show-add="false"
:show-delete="false"
:show-export="false"
@refresh="getData"
>
<template #prefix>
<NButton
v-if="waitingStatus"
:disabled="checkedRowKeys.length === 0"
size="small"
type="warning"
ghost
@click="showAssigneeModal"
>
<template #icon>
<icon-material-symbols:drive-file-rename-outline-outline class="text-icon" />
</template>
修改办理人
</NButton>
<NButton
v-if="waitingStatus"
:disabled="checkedRowKeys.length === 0"
size="small"
type="success"
ghost
@click="showUrgeModal"
>
<template #icon>
<SvgIcon local-icon="bell" class="text-16px" />
</template>
催办
</NButton>
</template>
</TableHeaderOperation>
</template>
<NDataTable
v-model:checked-row-keys="checkedRowKeys"
:columns="columns"
:data="data"
size="small"
:flex-height="!appStore.isMobile"
:scroll-x="1405"
:loading="loading"
remote
:row-key="row => row.id"
:pagination="mobilePagination"
class="sm:h-full"
/>
<component :is="dynamicComponent" :visible="viewVisible" operate-type="detail" :business-id="businessId" />
<FlowInterveneModal
v-model:visible="interveneVisible"
:task-id="taskId"
:assignee-ids="assigneeIds"
:assignee-names="assigneeNames"
@refresh="getData"
/>
<!-- 催办 -->
<FlowUrgeModal v-model:visible="urgeVisible" :task-ids="checkedRowKeys" @submit="getData" />
<!-- 修改办理人 -->
<UserSelectModal v-model:visible="assigneeVisible" @confirm="handleAssigneeConfirm" />
</NCard>
</div>
</TableSiderLayout>
</template>

View File

@ -0,0 +1,63 @@
<script setup lang="tsx">
import { useNaiveForm } from '@/hooks/common/form';
defineOptions({
name: 'AllTaskWaitingSearch'
});
interface Emits {
(e: 'reset'): void;
(e: 'search'): void;
}
const emit = defineEmits<Emits>();
const { formRef, validate, restoreValidation } = useNaiveForm();
const model = defineModel<Api.Workflow.TaskSearchParams>('model', { required: true });
async function reset() {
await restoreValidation();
emit('reset');
}
async function search() {
await validate();
emit('search');
}
</script>
<template>
<NCard :bordered="false" size="small" class="card-wrapper">
<NCollapse>
<NCollapseItem :title="$t('common.search')">
<NForm ref="formRef" :model="model" label-placement="left" :label-width="100">
<NGrid responsive="screen" item-responsive>
<NFormItemGi span="24 s:12 m:6" label="任务名称" path="nodeName" class="pr-24px">
<NInput v-model:value="model.nodeName" placeholder="请输入任务名称" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="流程定义名称" path="bucketName" class="pr-24px">
<NInput v-model:value="model.flowName" placeholder="请输入流程定义名称" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="流程定义编码" path="flowCode" class="pr-24px">
<NInput v-model:value="model.flowCode" placeholder="流程定义编码" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" class="pr-24px">
<NSpace class="w-full" justify="end">
<NButton @click="reset">
<template #icon>
<icon-ic-round-refresh class="text-icon" />
</template>
{{ $t('common.reset') }}
</NButton>
<NButton type="primary" ghost @click="search">
<template #icon>
<icon-ic-round-search class="text-icon" />
</template>
{{ $t('common.search') }}
</NButton>
</NSpace>
</NFormItemGi>
</NGrid>
</NForm>
</NCollapseItem>
</NCollapse>
</NCard>
</template>

View File

@ -0,0 +1,319 @@
<script setup lang="tsx">
import { computed, ref, shallowRef } from 'vue';
import { NButton, NDivider, NEmpty, NInput, NTag } from 'naive-ui';
import { useBoolean, useLoading } from '@sa/hooks';
import { workflowActivityStatusRecord } from '@/constants/workflow';
import { fetchBatchDeleteInstance, fetchCancelProcessApply, fetchGetMyDocument } from '@/service/api/workflow/instance';
import { fetchGetCategoryTree } from '@/service/api/workflow/category';
import { useAppStore } from '@/store/modules/app';
import { useTable, useTableOperate } from '@/hooks/common/table';
import { useDict } from '@/hooks/business/dict';
import { loadDynamicComponent } from '@/utils/common';
import DictTag from '@/components/custom/dict-tag.vue';
import ButtonIcon from '@/components/custom/button-icon.vue';
import { $t } from '@/locales';
import MyDocumentSearch from './modules/my-document-search.vue';
defineOptions({
name: 'MyDocumentList'
});
useDict('wf_business_status');
const appStore = useAppStore();
const { bool: viewVisible, setTrue: showViewDrawer } = useBoolean();
const dynamicComponent = shallowRef();
const {
columns,
columnChecks,
data,
getData,
getDataByPage,
loading,
mobilePagination,
searchParams,
resetSearchParams
} = useTable({
apiFn: fetchGetMyDocument,
apiParams: {
pageNum: 1,
pageSize: 10,
category: null,
flowName: null,
flowCode: null,
nodeName: null,
createByIds: null
},
columns: () => [
{
key: 'index',
title: $t('common.index'),
align: 'center',
width: 64
},
{
title: '流程定义名称',
key: 'flowName',
align: 'center',
width: 120
},
{
title: '流程定义编码',
key: 'flowCode',
align: 'center',
width: 100
},
{
title: '流程分类',
key: 'categoryName',
align: 'center',
width: 80,
render: row => {
return <NTag>{row.categoryName}</NTag>;
}
},
{
title: '版本号',
key: 'version',
align: 'center',
width: 80,
render: row => {
return <NTag type="info">v{row.version}.0</NTag>;
}
},
{
title: '流程状态',
key: 'flowStatus',
align: 'center',
width: 80,
render(row) {
return <DictTag value={row.flowStatus} dict-code="wf_business_status" />;
}
},
{
title: '状态',
key: 'activityStatus',
align: 'center',
width: 80,
render(row) {
return (
<NTag type={row.activityStatus === 0 ? 'warning' : 'success'}>
{workflowActivityStatusRecord[row.activityStatus]}
</NTag>
);
}
},
{
title: '启动时间',
key: 'createTime',
align: 'center',
width: 100
},
{
title: '操作',
key: 'operate',
align: 'center',
fixed: 'right',
width: 100,
render(row) {
const buttons = [];
buttons.push(
<ButtonIcon
text
type="info"
icon="material-symbols:visibility-outline"
tooltipContent="查看"
onClick={() => handleOpen(row, 'detail')}
/>
);
const showEditAndDelete =
row.flowStatus === 'draft' || row.flowStatus === 'cancel' || row.flowStatus === 'back';
if (showEditAndDelete) {
buttons.push(
<ButtonIcon
text
type="info"
icon="material-symbols:drive-file-rename-outline-outline"
tooltipContent="编辑"
onClick={() => handleOpen(row, 'edit')}
/>
);
}
if (showEditAndDelete) {
buttons.push(
<ButtonIcon
text
type="error"
icon="material-symbols:delete-outline"
tooltipContent={$t('common.delete')}
popconfirmContent={$t('common.confirmDelete')}
onPositiveClick={() => handleDelete(row)}
/>
);
}
if (row.flowStatus === 'waiting') {
buttons.push(
<ButtonIcon
text
type="error"
showPopconfirmIcon={false}
icon="material-symbols:cancel-outline-rounded"
tooltipContent="撤销"
popconfirmContent="确认撤销此流程申请?"
onPositiveClick={() => handleCancelProcessApply(row.businessId)}
/>
);
}
return (
<div class="flex-center gap-1px">
{buttons.map((btn, index) => (index > 0 ? [<NDivider vertical />, btn] : btn))}
</div>
);
}
}
]
});
const { checkedRowKeys } = useTableOperate(data, getData);
const { loading: treeLoading, startLoading: startTreeLoading, endLoading: endTreeLoading } = useLoading();
const categoryPattern = ref<string>();
const categoryData = ref<Api.Common.CommonTreeRecord>([]);
const selectedKeys = ref<string[]>([]);
const expandedKeys = ref<CommonType.IdType[]>(['100']);
const selectable = computed(() => !loading.value);
async function getTreeData() {
startTreeLoading();
const { data: tree, error } = await fetchGetCategoryTree();
if (!error) {
categoryData.value = tree;
}
endTreeLoading();
}
getTreeData();
function handleClickTree(keys: string[]) {
searchParams.category = keys.length ? keys[0] : null;
checkedRowKeys.value = [];
getDataByPage();
}
function handleResetTreeData() {
categoryPattern.value = undefined;
getTreeData();
}
function handleResetSearch() {
resetSearchParams();
selectedKeys.value = [];
}
const modules = import.meta.glob('@/components/workflow/**/*.vue');
const businessId = ref<CommonType.IdType>();
const operateType = ref<CommonType.WorkflowTableOperateType>();
async function handleOpen(row: Api.Workflow.Instance, type: 'edit' | 'detail') {
dynamicComponent.value = null;
viewVisible.value = false;
operateType.value = type;
businessId.value = row.businessId;
const formPath = row.formPath;
if (!formPath) return;
dynamicComponent.value = await loadDynamicComponent(modules, formPath);
setTimeout(() => {
showViewDrawer();
}, 300);
}
async function handleDelete(row: Api.Workflow.Instance) {
const { error } = await fetchBatchDeleteInstance([row.id]);
if (error) return;
window.$message?.success('删除成功');
getData();
}
async function handleCancelProcessApply(id: CommonType.IdType) {
const { error } = await fetchCancelProcessApply({ businessId: id, message: '申请人撤销流程!' });
if (error) return;
window.$message?.success('撤销成功');
getData();
}
</script>
<template>
<TableSiderLayout sider-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="categoryPattern" clearable :placeholder="$t('common.keywordSearch')" />
<NSpin class="category-tree" :show="treeLoading">
<NTree
v-model:selected-keys="selectedKeys"
v-model:expanded-keys="expandedKeys"
block-node
show-line
:data="categoryData as []"
:show-irrelevant-nodes="false"
:pattern="categoryPattern"
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="暂无分类信息" 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:overflow-auto">
<MyDocumentSearch v-model:model="searchParams" @reset="handleResetSearch" @search="getDataByPage" />
<NCard title="我发起的" :bordered="false" size="small" class="card-wrapper sm:flex-1-hidden">
<template #header-extra>
<TableHeaderOperation
v-model:columns="columnChecks"
:disabled-delete="checkedRowKeys.length === 0"
: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="1405"
:loading="loading"
remote
:row-key="row => row.id"
:pagination="mobilePagination"
class="sm:h-full"
/>
<component
:is="dynamicComponent"
:visible="viewVisible"
:operate-type="operateType"
:business-id="businessId"
@submitted="getData"
/>
</NCard>
</div>
</TableSiderLayout>
</template>

View File

@ -0,0 +1,57 @@
<script setup lang="tsx">
import { useNaiveForm } from '@/hooks/common/form';
defineOptions({
name: 'MyDocumentSearch'
});
interface Emits {
(e: 'reset'): void;
(e: 'search'): void;
}
const emit = defineEmits<Emits>();
const { formRef, validate, restoreValidation } = useNaiveForm();
const model = defineModel<Api.Workflow.TaskSearchParams>('model', { required: true });
async function reset() {
await restoreValidation();
emit('reset');
}
async function search() {
await validate();
emit('search');
}
</script>
<template>
<NCard :bordered="false" size="small" class="card-wrapper">
<NCollapse>
<NCollapseItem :title="$t('common.search')">
<NForm ref="formRef" :model="model" label-placement="left" :label-width="100">
<NGrid responsive="screen" item-responsive>
<NFormItemGi span="24 s:12 m:12" label="流程定义编码" path="flowCode" class="pr-24px">
<NInput v-model:value="model.flowCode" placeholder="请输入流程定义编码" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:12" class="pr-24px">
<NSpace class="w-full" justify="end">
<NButton @click="reset">
<template #icon>
<icon-ic-round-refresh class="text-icon" />
</template>
{{ $t('common.reset') }}
</NButton>
<NButton type="primary" ghost @click="search">
<template #icon>
<icon-ic-round-search class="text-icon" />
</template>
{{ $t('common.search') }}
</NButton>
</NSpace>
</NFormItemGi>
</NGrid>
</NForm>
</NCollapseItem>
</NCollapse>
</NCard>
</template>

View File

@ -0,0 +1,268 @@
<script setup lang="tsx">
import { computed, ref, shallowRef } from 'vue';
import { NButton, NEmpty, NInput, NTag } from 'naive-ui';
import { useBoolean, useLoading } from '@sa/hooks';
import { fetchGetCategoryTree } from '@/service/api/workflow/category';
import { fetchGetCopyTask } from '@/service/api/workflow/task';
import { useAppStore } from '@/store/modules/app';
import { useTable, useTableOperate } from '@/hooks/common/table';
import { useDict } from '@/hooks/business/dict';
import { loadDynamicComponent } from '@/utils/common';
import DictTag from '@/components/custom/dict-tag.vue';
import ButtonIcon from '@/components/custom/button-icon.vue';
import { $t } from '@/locales';
import TaskCopySearch from './modules/task-copy-search.vue';
defineOptions({
name: 'TaskCopyList'
});
useDict('wf_business_status');
useDict('wf_task_status');
const appStore = useAppStore();
const { bool: viewVisible, setTrue: showViewDrawer } = useBoolean();
const dynamicComponent = shallowRef();
const {
columns,
columnChecks,
data,
getData,
getDataByPage,
loading,
mobilePagination,
searchParams,
resetSearchParams
} = useTable({
apiFn: fetchGetCopyTask,
apiParams: {
pageNum: 1,
pageSize: 10,
category: null,
flowName: null,
flowCode: null,
nodeName: null,
createByIds: null
},
columns: () => [
{
key: 'index',
title: $t('common.index'),
align: 'center',
width: 64
},
{
title: '业务编码',
key: 'businessCode',
align: 'center',
width: 120
},
{
title: '业务名称',
key: 'businessTitle',
align: 'center',
width: 120
},
{
title: '流程定义名称',
key: 'flowName',
align: 'center',
width: 120
},
{
title: '流程定义编码',
key: 'flowCode',
align: 'center',
width: 100
},
{
title: '流程分类',
key: 'categoryName',
align: 'center',
width: 80,
render: row => {
return <NTag>{row.categoryName}</NTag>;
}
},
{
title: '版本号',
key: 'version',
align: 'center',
width: 100,
render: row => <NTag type="info">v{row.version}.0</NTag>
},
{
title: '任务名称',
key: 'nodeName',
align: 'center',
width: 100
},
{
title: '流程状态',
key: 'flowStatus',
align: 'center',
width: 80,
render(row) {
return <DictTag value={row.flowStatus} dict-code="wf_business_status" />;
}
},
{
title: '申请人',
key: 'createByName',
align: 'center',
width: 100
},
{
key: 'createTime',
title: '创建时间',
align: 'center',
width: 120
},
{
title: '操作',
key: 'operate',
align: 'center',
fixed: 'right',
width: 50,
render(row) {
return (
<ButtonIcon
text
type="primary"
icon="material-symbols:visibility-outline"
tooltipContent="查看"
onClick={() => handleView(row)}
/>
);
}
}
]
});
const { checkedRowKeys } = useTableOperate(data, getData);
const { loading: treeLoading, startLoading: startTreeLoading, endLoading: endTreeLoading } = useLoading();
const categoryPattern = ref<string>();
const categoryData = ref<Api.Common.CommonTreeRecord>([]);
const selectedKeys = ref<string[]>([]);
const expandedKeys = ref<CommonType.IdType[]>(['100']);
const selectable = computed(() => !loading.value);
async function getTreeData() {
startTreeLoading();
const { data: tree, error } = await fetchGetCategoryTree();
if (!error) {
categoryData.value = tree;
}
endTreeLoading();
}
getTreeData();
function handleClickTree(keys: string[]) {
searchParams.category = keys.length ? keys[0] : null;
checkedRowKeys.value = [];
getDataByPage();
}
function handleResetTreeData() {
categoryPattern.value = undefined;
getTreeData();
}
function handleResetSearch() {
resetSearchParams();
selectedKeys.value = [];
}
const modules = import.meta.glob('@/components/workflow/**/*.vue');
const businessId = ref<CommonType.IdType>();
const taskId = ref<CommonType.IdType>();
async function handleView(row: Api.Workflow.Task) {
dynamicComponent.value = null;
viewVisible.value = false;
businessId.value = row.businessId;
taskId.value = row.id;
const formPath = row.formPath;
if (!formPath) return;
dynamicComponent.value = await loadDynamicComponent(modules, formPath);
setTimeout(() => {
showViewDrawer();
}, 300);
}
</script>
<template>
<TableSiderLayout sider-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="categoryPattern" clearable :placeholder="$t('common.keywordSearch')" />
<NSpin class="category-tree" :show="treeLoading">
<NTree
v-model:selected-keys="selectedKeys"
v-model:expanded-keys="expandedKeys"
block-node
show-line
:data="categoryData as []"
:show-irrelevant-nodes="false"
:pattern="categoryPattern"
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="暂无分类信息" 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:overflow-auto">
<TaskCopySearch v-model:model="searchParams" @reset="handleResetSearch" @search="getDataByPage" />
<NCard title="我的抄送" :bordered="false" size="small" class="card-wrapper sm:flex-1-hidden">
<template #header-extra>
<TableHeaderOperation
v-model:columns="columnChecks"
:disabled-delete="checkedRowKeys.length === 0"
: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="1405"
:loading="loading"
remote
:row-key="row => row.id"
:pagination="mobilePagination"
class="sm:h-full"
/>
<component
:is="dynamicComponent"
v-if="dynamicComponent"
:visible="viewVisible"
operate-type="detail"
:business-id="businessId"
:task-id="taskId"
@submitted="getData"
/>
</NCard>
</div>
</TableSiderLayout>
</template>

View File

@ -0,0 +1,63 @@
<script setup lang="tsx">
import { useNaiveForm } from '@/hooks/common/form';
defineOptions({
name: 'MyDocumentSearch'
});
interface Emits {
(e: 'reset'): void;
(e: 'search'): void;
}
const emit = defineEmits<Emits>();
const { formRef, validate, restoreValidation } = useNaiveForm();
const model = defineModel<Api.Workflow.TaskSearchParams>('model', { required: true });
async function reset() {
await restoreValidation();
emit('reset');
}
async function search() {
await validate();
emit('search');
}
</script>
<template>
<NCard :bordered="false" size="small" class="card-wrapper">
<NCollapse>
<NCollapseItem :title="$t('common.search')">
<NForm ref="formRef" :model="model" label-placement="left" :label-width="100">
<NGrid responsive="screen" item-responsive>
<NFormItemGi span="24 s:12 m:6" label="任务名称" path="nodeName" class="pr-24px">
<NInput v-model:value="model.nodeName" placeholder="请输入任务名称" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="流程定义名称" path="bucketName" class="pr-24px">
<NInput v-model:value="model.flowName" placeholder="请输入流程定义名称" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="流程定义编码" path="flowCode" class="pr-24px">
<NInput v-model:value="model.flowCode" placeholder="流程定义编码" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" class="pr-24px">
<NSpace class="w-full" justify="end">
<NButton @click="reset">
<template #icon>
<icon-ic-round-refresh class="text-icon" />
</template>
{{ $t('common.reset') }}
</NButton>
<NButton type="primary" ghost @click="search">
<template #icon>
<icon-ic-round-search class="text-icon" />
</template>
{{ $t('common.search') }}
</NButton>
</NSpace>
</NFormItemGi>
</NGrid>
</NForm>
</NCollapseItem>
</NCollapse>
</NCard>
</template>

View File

@ -0,0 +1,280 @@
<script setup lang="tsx">
import { computed, ref, shallowRef } from 'vue';
import { NButton, NEmpty, NInput, NTag } from 'naive-ui';
import { useBoolean, useLoading } from '@sa/hooks';
import { fetchGetCategoryTree } from '@/service/api/workflow/category';
import { fetchGetFinishedTask } from '@/service/api/workflow/task';
import { useAppStore } from '@/store/modules/app';
import { useTable, useTableOperate } from '@/hooks/common/table';
import { useDict } from '@/hooks/business/dict';
import { loadDynamicComponent } from '@/utils/common';
import DictTag from '@/components/custom/dict-tag.vue';
import ButtonIcon from '@/components/custom/button-icon.vue';
import { $t } from '@/locales';
import TaskFinishSearch from './modules/task-finish-search.vue';
defineOptions({
name: 'TaskFinishList'
});
useDict('wf_business_status');
useDict('wf_task_status');
const appStore = useAppStore();
const { bool: viewVisible, setTrue: showViewDrawer } = useBoolean();
const dynamicComponent = shallowRef();
const {
columns,
columnChecks,
data,
getData,
getDataByPage,
loading,
mobilePagination,
searchParams,
resetSearchParams
} = useTable({
apiFn: fetchGetFinishedTask,
apiParams: {
pageNum: 1,
pageSize: 10,
category: null,
flowName: null,
flowCode: null,
nodeName: null,
createByIds: null
},
columns: () => [
{
key: 'index',
title: $t('common.index'),
align: 'center',
width: 64
},
{
title: '业务编码',
key: 'businessCode',
align: 'center',
width: 120
},
{
title: '业务名称',
key: 'businessTitle',
align: 'center',
width: 120
},
{
title: '流程定义名称',
key: 'flowName',
align: 'center',
width: 120
},
{
title: '流程定义编码',
key: 'flowCode',
align: 'center',
width: 100
},
{
title: '流程分类',
key: 'categoryName',
align: 'center',
width: 80,
render: row => {
return <NTag>{row.categoryName}</NTag>;
}
},
{
title: '版本号',
key: 'version',
align: 'center',
width: 100,
render: row => <NTag type="info">v{row.version}.0</NTag>
},
{
title: '任务名称',
key: 'nodeName',
align: 'center',
width: 100
},
{
title: '流程状态',
key: 'flowStatus',
align: 'center',
width: 80,
render(row) {
return <DictTag value={row.flowStatus} dict-code="wf_business_status" />;
}
},
{
key: 'flowTaskStatus',
title: '任务状态',
align: 'center',
width: 80,
render: row => <DictTag size="small" value={row.flowTaskStatus} dict-code="wf_task_status" />
},
{
title: '申请人',
key: 'createByName',
align: 'center',
width: 100
},
{
title: '办理人',
key: 'approveName',
align: 'center',
width: 100
},
{
key: 'createTime',
title: '创建时间',
align: 'center',
width: 120
},
{
title: '操作',
key: 'operate',
align: 'center',
fixed: 'right',
width: 50,
render(row) {
return (
<ButtonIcon
text
type="primary"
icon="material-symbols:visibility-outline"
tooltipContent="查看"
onClick={() => handleView(row)}
/>
);
}
}
]
});
const { checkedRowKeys } = useTableOperate(data, getData);
const { loading: treeLoading, startLoading: startTreeLoading, endLoading: endTreeLoading } = useLoading();
const categoryPattern = ref<string>();
const categoryData = ref<Api.Common.CommonTreeRecord>([]);
const selectedKeys = ref<string[]>([]);
const expandedKeys = ref<CommonType.IdType[]>(['100']);
const selectable = computed(() => !loading.value);
async function getTreeData() {
startTreeLoading();
const { data: tree, error } = await fetchGetCategoryTree();
if (!error) {
categoryData.value = tree;
}
endTreeLoading();
}
getTreeData();
function handleClickTree(keys: string[]) {
searchParams.category = keys.length ? keys[0] : null;
checkedRowKeys.value = [];
getDataByPage();
}
function handleResetTreeData() {
categoryPattern.value = undefined;
getTreeData();
}
function handleResetSearch() {
resetSearchParams();
selectedKeys.value = [];
}
const modules = import.meta.glob('@/components/workflow/**/*.vue');
const businessId = ref<CommonType.IdType>();
const taskId = ref<CommonType.IdType>();
async function handleView(row: Api.Workflow.HisTask) {
dynamicComponent.value = null;
viewVisible.value = false;
businessId.value = row.businessId;
taskId.value = row.id;
const formPath = row.formPath;
if (!formPath) return;
dynamicComponent.value = await loadDynamicComponent(modules, formPath);
setTimeout(() => {
showViewDrawer();
}, 300);
}
</script>
<template>
<TableSiderLayout sider-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="categoryPattern" clearable :placeholder="$t('common.keywordSearch')" />
<NSpin class="category-tree" :show="treeLoading">
<NTree
v-model:selected-keys="selectedKeys"
v-model:expanded-keys="expandedKeys"
block-node
show-line
:data="categoryData as []"
:show-irrelevant-nodes="false"
:pattern="categoryPattern"
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="暂无分类信息" 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:overflow-auto">
<TaskFinishSearch v-model:model="searchParams" @reset="handleResetSearch" @search="getDataByPage" />
<NCard title="我的已办" :bordered="false" size="small" class="card-wrapper sm:flex-1-hidden">
<template #header-extra>
<TableHeaderOperation
v-model:columns="columnChecks"
:disabled-delete="checkedRowKeys.length === 0"
: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="1405"
:loading="loading"
remote
:row-key="row => row.id"
:pagination="mobilePagination"
class="sm:h-full"
/>
<component
:is="dynamicComponent"
:visible="viewVisible"
operate-type="detail"
:business-id="businessId"
:task-id="taskId"
@submitted="getData"
/>
</NCard>
</div>
</TableSiderLayout>
</template>

View File

@ -0,0 +1,63 @@
<script setup lang="tsx">
import { useNaiveForm } from '@/hooks/common/form';
defineOptions({
name: 'MyDocumentSearch'
});
interface Emits {
(e: 'reset'): void;
(e: 'search'): void;
}
const emit = defineEmits<Emits>();
const { formRef, validate, restoreValidation } = useNaiveForm();
const model = defineModel<Api.Workflow.TaskSearchParams>('model', { required: true });
async function reset() {
await restoreValidation();
emit('reset');
}
async function search() {
await validate();
emit('search');
}
</script>
<template>
<NCard :bordered="false" size="small" class="card-wrapper">
<NCollapse>
<NCollapseItem :title="$t('common.search')">
<NForm ref="formRef" :model="model" label-placement="left" :label-width="100">
<NGrid responsive="screen" item-responsive>
<NFormItemGi span="24 s:12 m:6" label="任务名称" path="nodeName" class="pr-24px">
<NInput v-model:value="model.nodeName" placeholder="请输入任务名称" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="流程定义名称" path="bucketName" class="pr-24px">
<NInput v-model:value="model.flowName" placeholder="请输入流程定义名称" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="流程定义编码" path="flowCode" class="pr-24px">
<NInput v-model:value="model.flowCode" placeholder="流程定义编码" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" class="pr-24px">
<NSpace class="w-full" justify="end">
<NButton @click="reset">
<template #icon>
<icon-ic-round-refresh class="text-icon" />
</template>
{{ $t('common.reset') }}
</NButton>
<NButton type="primary" ghost @click="search">
<template #icon>
<icon-ic-round-search class="text-icon" />
</template>
{{ $t('common.search') }}
</NButton>
</NSpace>
</NFormItemGi>
</NGrid>
</NForm>
</NCollapseItem>
</NCollapse>
</NCard>
</template>

View File

@ -0,0 +1,261 @@
<script setup lang="tsx">
import { computed, ref, shallowRef } from 'vue';
import { NButton, NEmpty, NInput, NTag } from 'naive-ui';
import { useBoolean, useLoading } from '@sa/hooks';
import { fetchGetCategoryTree } from '@/service/api/workflow/category';
import { fetchGetTaskWaitList } from '@/service/api/workflow/task';
import { useAppStore } from '@/store/modules/app';
import { useTable, useTableOperate } from '@/hooks/common/table';
import { useDict } from '@/hooks/business/dict';
import { loadDynamicComponent } from '@/utils/common';
import TagGroup from '@/components/custom/tag-group.vue';
import DictTag from '@/components/custom/dict-tag.vue';
import ButtonIcon from '@/components/custom/button-icon.vue';
import { $t } from '@/locales';
import TaskWaitingSearch from './modules/task-waiting-search.vue';
defineOptions({
name: 'TaskWaitingList'
});
useDict('wf_business_status');
const appStore = useAppStore();
const { bool: viewVisible, setTrue: showViewDrawer } = useBoolean();
const dynamicComponent = shallowRef();
const {
columns,
columnChecks,
data,
getData,
getDataByPage,
loading,
mobilePagination,
searchParams,
resetSearchParams
} = useTable({
apiFn: fetchGetTaskWaitList,
apiParams: {
pageNum: 1,
pageSize: 10,
category: null,
flowName: null,
flowCode: null,
nodeName: null,
createByIds: null
},
columns: () => [
{
key: 'index',
title: $t('common.index'),
align: 'center',
width: 64
},
{
title: '业务编码',
key: 'businessCode',
align: 'center',
width: 120
},
{
title: '业务名称',
key: 'businessTitle',
align: 'center',
width: 120
},
{
title: '流程定义名称',
key: 'flowName',
align: 'center',
width: 120
},
{
title: '流程定义编码',
key: 'flowCode',
align: 'center',
width: 100
},
{
title: '流程分类',
key: 'categoryName',
align: 'center',
width: 80,
render: row => {
return <NTag>{row.categoryName}</NTag>;
}
},
{
title: '任务名称',
key: 'nodeName',
align: 'center',
width: 100
},
{
title: '申请人',
key: 'createByName',
align: 'center',
width: 100
},
{
title: '办理人',
key: 'assigneeNames',
align: 'center',
width: 100,
render: row => <TagGroup value={row.assigneeNames} />
},
{
title: '流程状态',
key: 'flowStatus',
align: 'center',
width: 80,
render(row) {
return <DictTag value={row.flowStatus} dict-code="wf_business_status" />;
}
},
{
title: '操作',
key: 'operate',
align: 'center',
fixed: 'right',
width: 50,
render(row) {
return (
<ButtonIcon
text
type="primary"
icon="ph:check-circle-bold"
tooltipContent="办理"
onClick={() => handleApproval(row)}
/>
);
}
}
]
});
const { checkedRowKeys } = useTableOperate(data, getData);
const { loading: treeLoading, startLoading: startTreeLoading, endLoading: endTreeLoading } = useLoading();
const categoryPattern = ref<string>();
const categoryData = ref<Api.Common.CommonTreeRecord>([]);
const selectedKeys = ref<string[]>([]);
const expandedKeys = ref<CommonType.IdType[]>(['100']);
const selectable = computed(() => !loading.value);
async function getTreeData() {
startTreeLoading();
const { data: tree, error } = await fetchGetCategoryTree();
if (!error) {
categoryData.value = tree;
}
endTreeLoading();
}
getTreeData();
function handleClickTree(keys: string[]) {
searchParams.category = keys.length ? keys[0] : null;
checkedRowKeys.value = [];
getDataByPage();
}
function handleResetTreeData() {
categoryPattern.value = undefined;
getTreeData();
}
function handleResetSearch() {
resetSearchParams();
selectedKeys.value = [];
}
const modules = import.meta.glob('@/components/workflow/**/*.vue');
const businessId = ref<CommonType.IdType>();
const taskId = ref<CommonType.IdType>();
async function handleApproval(row: Api.Workflow.Task) {
dynamicComponent.value = null;
viewVisible.value = false;
businessId.value = row.businessId;
taskId.value = row.id;
const formPath = row.formPath;
if (!formPath) return;
dynamicComponent.value = await loadDynamicComponent(modules, formPath);
setTimeout(() => {
showViewDrawer();
}, 300);
}
</script>
<template>
<TableSiderLayout sider-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="categoryPattern" clearable :placeholder="$t('common.keywordSearch')" />
<NSpin class="category-tree" :show="treeLoading">
<NTree
v-model:selected-keys="selectedKeys"
v-model:expanded-keys="expandedKeys"
block-node
show-line
:data="categoryData as []"
:show-irrelevant-nodes="false"
:pattern="categoryPattern"
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="暂无分类信息" 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:overflow-auto">
<TaskWaitingSearch v-model:model="searchParams" @reset="handleResetSearch" @search="getDataByPage" />
<NCard title="我的待办" :bordered="false" size="small" class="card-wrapper sm:flex-1-hidden">
<template #header-extra>
<TableHeaderOperation
v-model:columns="columnChecks"
:disabled-delete="checkedRowKeys.length === 0"
: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="1405"
:loading="loading"
remote
:row-key="row => row.id"
:pagination="mobilePagination"
class="sm:h-full"
/>
<component
:is="dynamicComponent"
:visible="viewVisible"
operate-type="approval"
:business-id="businessId"
:task-id="taskId"
@submitted="getData"
/>
</NCard>
</div>
</TableSiderLayout>
</template>

View File

@ -0,0 +1,63 @@
<script setup lang="tsx">
import { useNaiveForm } from '@/hooks/common/form';
defineOptions({
name: 'TaskWaitingSearch'
});
interface Emits {
(e: 'reset'): void;
(e: 'search'): void;
}
const emit = defineEmits<Emits>();
const { formRef, validate, restoreValidation } = useNaiveForm();
const model = defineModel<Api.Workflow.TaskSearchParams>('model', { required: true });
async function reset() {
await restoreValidation();
emit('reset');
}
async function search() {
await validate();
emit('search');
}
</script>
<template>
<NCard :bordered="false" size="small" class="card-wrapper">
<NCollapse>
<NCollapseItem :title="$t('common.search')">
<NForm ref="formRef" :model="model" label-placement="left" :label-width="100">
<NGrid responsive="screen" item-responsive>
<NFormItemGi span="24 s:12 m:6" label="任务名称" path="nodeName" class="pr-24px">
<NInput v-model:value="model.nodeName" placeholder="请输入任务名称" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="流程定义名称" path="bucketName" class="pr-24px">
<NInput v-model:value="model.flowName" placeholder="请输入流程定义名称" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="流程定义编码" path="flowCode" class="pr-24px">
<NInput v-model:value="model.flowCode" placeholder="流程定义编码" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" class="pr-24px">
<NSpace class="w-full" justify="end">
<NButton @click="reset">
<template #icon>
<icon-ic-round-refresh class="text-icon" />
</template>
{{ $t('common.reset') }}
</NButton>
<NButton type="primary" ghost @click="search">
<template #icon>
<icon-ic-round-search class="text-icon" />
</template>
{{ $t('common.search') }}
</NButton>
</NSpace>
</NFormItemGi>
</NGrid>
</NForm>
</NCollapseItem>
</NCollapse>
</NCard>
</template>