mirror of
https://github.com/m-xlsea/ruoyi-plus-soybean.git
synced 2025-09-24 07:49:47 +08:00
Compare commits
174 Commits
v1.1.0
...
34ad79e815
Author | SHA1 | Date | |
---|---|---|---|
34ad79e815 | |||
7de471aa68 | |||
378aa869bf | |||
4a4244b5c4 | |||
ecad1c3e78 | |||
1eb0a3dac2 | |||
bc44d2cddc | |||
b07729edd1 | |||
9ef0bd416e | |||
9b60512165 | |||
f739e1e031 | |||
b70095e5ff | |||
25ee32074a | |||
8412a8db16 | |||
e230b0da81 | |||
12b25e0d58 | |||
e33f944a74 | |||
7f2f3bd088 | |||
5ef1c5de98 | |||
90a14e338a | |||
8a453f5852 | |||
4d120ffe63 | |||
4e625111ce | |||
8524ae7666 | |||
d6ae85d218 | |||
89719abe34 | |||
8d7f91dccf | |||
33ade53904 | |||
ab9c84d831 | |||
7651816495 | |||
4539fe01fb | |||
71297cf2e4 | |||
30316d7e4a | |||
aaad817131 | |||
4e9839bd48 | |||
a15b683b1d | |||
9df8d2f55f | |||
710374398a | |||
52318c106d | |||
41c74db1cd | |||
9027632bef | |||
b96c46baa9 | |||
befc61a4ff | |||
d1f0ce28cb | |||
2d31d7dc62 | |||
e538355f2b | |||
f89835578c | |||
4eb77eac78 | |||
adca2e26be | |||
ff576f3f42 | |||
8fcc70d73d | |||
c87aa6652c | |||
48f603ed32 | |||
f4038a2dc0 | |||
7484f79b8f | |||
a9c58b2590 | |||
d562f8c155 | |||
2e02992906 | |||
f138e34ef9 | |||
923eb98a5c | |||
94fafe3938 | |||
c7c4e23be5 | |||
e64d9b38c9 | |||
a1336d1536 | |||
b6c7b1b383 | |||
b46f172637 | |||
7ffaac5893 | |||
cc29ea85c1 | |||
2edd4bc9f1 | |||
e485f680c7 | |||
0dcfc893dd | |||
2f8797eb98 | |||
2a3f3a4812 | |||
22710ecbd5 | |||
6c6086f89d | |||
59a69dd9f0 | |||
01d42722f5 | |||
bd6b575af6 | |||
6dc7b2a234 | |||
2587f8cbfa | |||
30878fb4d3 | |||
5e49622ea8 | |||
130ee1dcec | |||
2036391c41 | |||
e89b86ce56 | |||
523aca6b75 | |||
4e8c8715ae | |||
e5ec915fd9 | |||
89c716e12a | |||
2d02128164 | |||
9ca7ca8fda | |||
d85424ee83 | |||
3a506df9b9 | |||
39b0c636f2 | |||
ff87415d7b | |||
400eaf8990 | |||
312709706b | |||
3ae9922dc4 | |||
247b98a542 | |||
566b2c2db8 | |||
2dbf8b3dfa | |||
90d32ee29a | |||
efc953c094 | |||
ad48d8e840 | |||
62f2c6d571 | |||
03dd64c543 | |||
aeb6369005 | |||
133196f337 | |||
41191d54fb | |||
229e00443f | |||
fbec787a99 | |||
4e1f539576 | |||
85c8a9fffa | |||
b99999355c | |||
baa0584bdc | |||
a77edc2e36 | |||
c3ea81dc0d | |||
f1d7b9733f | |||
55dceca28b | |||
188533adc9 | |||
80faf4b47c | |||
89e7edb380 | |||
b8c771cd1d | |||
81449ea77a | |||
1c322e28ef | |||
54fa7caf03 | |||
b3dccb542e | |||
ae5c7e8372 | |||
2f8a6b4b84 | |||
e86a6d1b7a | |||
a3dcee4a11 | |||
50e7b5158d | |||
2b5735ab34 | |||
56bfd64fbe | |||
47d5c1c71b | |||
49224afe2d | |||
8ce02aa15a | |||
496ed978ca | |||
fb652ce7ff | |||
90ebf83501 | |||
b3f81ba3f0 | |||
20b96ac54b | |||
85115ce327 | |||
1cbeb59c0b | |||
9aa6597d5e | |||
d1377baaed | |||
b6f4fb5aec | |||
1af4e96382 | |||
997f4a2d61 | |||
f52fa40326 | |||
7b2c857f6d | |||
4f6c14f358 | |||
ffd6211e4d | |||
56d6d77da5 | |||
394db6fec2 | |||
4614b97796 | |||
49521b667d | |||
ab1d3a237e | |||
3629c7a9dd | |||
89a2a6cbf4 | |||
d7e0516cfb | |||
b265f590e4 | |||
210c00c686 | |||
f004e75cc1 | |||
14a29070c9 | |||
92b9c213d5 | |||
ed1180696f | |||
1c64693774 | |||
350de08f8f | |||
f9d57f1b71 | |||
11aba9e2c8 | |||
32c241564d | |||
b1cb10581e | |||
25790d4b0f |
@ -16,7 +16,7 @@
|
||||
},
|
||||
"playwright": {
|
||||
"command": "npx",
|
||||
"args": ["@playwright/mcp@latest"]
|
||||
"args": ["@playwright/mcp@0.0.29"]
|
||||
},
|
||||
"mcp-server-time": {
|
||||
"command": "uvx",
|
||||
@ -26,7 +26,7 @@
|
||||
"command": "npx",
|
||||
"args": ["-y", "mcp-shrimp-task-manager"],
|
||||
"env": {
|
||||
"DATA_DIR": "D:/workspace/Aother/mcp-shrimp-task-manager/data",
|
||||
"DATA_DIR": "D:/workspace/mcp-shrimp-task-manager/data",
|
||||
"TEMPLATES_USE": "en",
|
||||
"ENABLE_GUI": "false"
|
||||
}
|
||||
|
2
.env.dev
2
.env.dev
@ -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 私钥与后端加密公钥对应 如更换需前后端一同更换
|
||||
|
@ -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 私钥与后端加密公钥对应 如更换需前后端一同更换
|
||||
|
@ -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 私钥与后端加密公钥对应 如更换需前后端一同更换
|
||||
|
16
.vscode/settings.json
vendored
16
.vscode/settings.json
vendored
@ -4,13 +4,27 @@
|
||||
"source.organizeImports": "never"
|
||||
},
|
||||
"editor.formatOnSave": false,
|
||||
"eslint.validate": ["html", "css", "scss", "json", "jsonc"],
|
||||
"eslint.validate": [
|
||||
"html",
|
||||
"css",
|
||||
"scss",
|
||||
"json",
|
||||
"jsonc",
|
||||
"javascript",
|
||||
"javascriptreact",
|
||||
"typescript",
|
||||
"typescriptreact",
|
||||
"vue"
|
||||
],
|
||||
"i18n-ally.displayLanguage": "zh-cn",
|
||||
"i18n-ally.enabledParsers": ["ts"],
|
||||
"i18n-ally.enabledFrameworks": ["vue"],
|
||||
"i18n-ally.editor.preferEditor": true,
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"i18n-ally.localesPaths": ["src/locales/langs"],
|
||||
"i18n-ally.parsers.typescript.compilerOptions": {
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"prettier.enable": false,
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"unocss.root": ["./"],
|
||||
|
199
CHANGELOG.md
199
CHANGELOG.md
@ -1,5 +1,202 @@
|
||||
# 更新日志
|
||||
|
||||
## [v1.1.3](https://gitee.com/xlsea/ruoyi-plus-soybean/compare/v1.1.2...v1.1.3) (2025-08-16)
|
||||
|
||||
### 🚀 新功能
|
||||
|
||||
- 对接工作流分类模块 - by **AN** [<samp>(25790)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/25790d4b)
|
||||
- 新增流程定义页面 - by @m-xlsea [<samp>(11aba)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/11aba9e2)
|
||||
- 添加流程实例功能 - by **AN** [<samp>(f9d57)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/f9d57f1b)
|
||||
- 增加流程作废功能并优化按钮组件 - by **AN** [<samp>(350de)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/350de08f)
|
||||
- 流程实例,查看变量功能 - by **AN** [<samp>(92b9c)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/92b9c213)
|
||||
- 更新工作流分类选择组件,修复值回显问题并优化添加数据操作 - by **AN** [<samp>(14a29)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/14a29070)
|
||||
- 添加请假申请功能 - by **AN** [<samp>(d7e05)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/d7e0516c)
|
||||
- 更新请假申请功能,添加日期范围选择和请假天数自动计算 - by **AN** [<samp>(89a2a)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/89a2a6cb)
|
||||
- 添加请假申请详情接口,优化请假操作表单样式 - by **AN** [<samp>(ab1d3)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/ab1d3a23)
|
||||
- 更新请假申请表单,添加流程类型选择和流程启动功能 - by **AN** [<samp>(49521)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/49521b66)
|
||||
- 更新工作流任务申请模态框样式,添加消息类型选择禁用功能 - by **AN** [<samp>(4614b)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/4614b977)
|
||||
- **components**:
|
||||
- 增强审批信息面板,优化附件处理 - by **AN** [<samp>(49224)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/49224afe)
|
||||
- 面板添加流程图 - by **AN** [<samp>(ae5c7)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/ae5c7e83)
|
||||
- **projects**:
|
||||
- 优化组件,完成流程实例-流程预览 - by **AN** [<samp>(50e7b)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/50e7b515)
|
||||
- 新增group-tag组件,待办任务查看功能 - by **AN** [<samp>(1c322)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/1c322e28)
|
||||
- 新增用户选择器组件,添加流程干预按钮 - by **AN** [<samp>(b8c77)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/b8c771cd)
|
||||
- 新增转办和终止功能 - by **AN** [<samp>(80faf)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/80faf4b4)
|
||||
- 新增加签功能 - by **AN** [<samp>(55dce)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/55dceca2)
|
||||
- 新增减签功能 - by **AN** [<samp>(f1d7b)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/f1d7b973)
|
||||
- 新增 '我发起的' 功能 - by **AN** [<samp>(a77ed)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/a77edc2e)
|
||||
- 新增抄送、下一审批人提交功能,优化组件通用性 - by **AN** [<samp>(523ac)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/523aca6b)
|
||||
- 新增我的待办功能,新增审批,驳回组件 - by **AN** [<samp>(130ee)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/130ee1dc)
|
||||
- 新增我的已办,我的抄送功能 - by **AN** [<samp>(01d42)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/01d42722)
|
||||
- 补充搜索条件 - by **AN** [<samp>(b6c7b)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/b6c7b1b3)
|
||||
- 新增流程表达式功能 - by **AN** [<samp>(d562f)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/d562f8c1)
|
||||
- **types**:
|
||||
- 补充类型定义 - by **AN** [<samp>(bd6b5)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/bd6b575a)
|
||||
|
||||
### 🐞 Bug 修复
|
||||
|
||||
- 修复控制台报错:Message compilation error,i18n的@为特殊符号 - by **AN** [<samp>(1c646)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/1c646937)
|
||||
- 修复运行状态变化时数据加载顺序问题 - by **AN** [<samp>(ed118)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/ed118069)
|
||||
- 修复添加行时的操作顺序问题 - by **AN** [<samp>(f004e)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/f004e75c)
|
||||
- **hooks**:
|
||||
- 非安全环境下不使用流式下载 - by @m-xlsea [<samp>(f8983)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/f8983557)
|
||||
- 修复oss下载时未转码问题 - by **AN** [<samp>(2d31d)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/2d31d7dc)
|
||||
- **other**:
|
||||
- 修复代码生成字典相关问题 - by @m-xlsea [<samp>(9aa65)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/9aa6597d)
|
||||
- **project**:
|
||||
- 关闭多租户功能后仍然遍历租户列表导致控制台报错的问题 - by **wang_rui** [<samp>(b96c4)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/b96c46ba)
|
||||
- 关闭多租户功能后仍然遍历租户列表导致控制台报错的问题 Merge pull request !25 from littleghost2016/dev - by **不寻俗** [<samp>(90276)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/9027632b)
|
||||
- **projects**:
|
||||
- 修复更新后产生问题 - by **AN** [<samp>(400ea)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/400eaf89)
|
||||
- 修复动态组件弹窗动画问题 - by @m-xlsea [<samp>(2e029)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/2e029929)
|
||||
- 修复抽屉问题 - by **AN** [<samp>(a9c58)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/a9c58b25)
|
||||
- 修复一级菜单隐藏失效问题 - by **AN** [<samp>(8fcc7)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/8fcc70d7)
|
||||
- 修复日期搜索条件清除问题 - by **AN** [<samp>(52318)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/52318c10)
|
||||
- 修复登录过期事件监听未被重置 - by @m-xlsea [<samp>(71037)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/71037439)
|
||||
- 修复用户新增时角色下拉包含超级管理员问题 - by **AN** [<samp>(a15b6)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/a15b683b)
|
||||
- 修复用户导入功能无法更新问题 - by **AN** [<samp>(4e983)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/4e9839bd)
|
||||
- Fix the icon size in the image preview toolbar - by @m-xlsea [<samp>(4539f)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/4539fe01)
|
||||
- 修复新增用户未查询角色列表问题 - by **AN** [<samp>(d6ae8)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/d6ae85d2)
|
||||
- **readme**:
|
||||
- update GitHub stars and forks links for gitee - by @soybeanjs [<samp>(923eb)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/923eb98a)
|
||||
|
||||
### 🛠 优化
|
||||
|
||||
- **projects**:
|
||||
- 统一button的添加方式 - by **AN** [<samp>(b3f81)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/b3f81ba3)
|
||||
- 动态加载组件方法抽取为公共函数 - by **AN** [<samp>(2f8a6)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/2f8a6b4b)
|
||||
- 优化代码 - by **AN** [<samp>(59a69)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/59a69dd9)
|
||||
- 优化代码 - by **AN** [<samp>(6c608)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/6c6086f8)
|
||||
- 优化搜索FormItem展示 - by **AN** [<samp>(94faf)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/94fafe39)
|
||||
- **types**:
|
||||
- 补充标签类型 - by **AN** [<samp>(30316)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/30316d7e)
|
||||
- 补充标签类型 - by **AN** [<samp>(71297)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/71297cf2)
|
||||
|
||||
### 💅 重构
|
||||
|
||||
- remove WorkflowLeaveForm type from Vue typings - by **AN** [<samp>(394db)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/394db6fe)
|
||||
- 优化代码 - by **AN** [<samp>(56d6d)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/56d6d77d)
|
||||
- **menu**:
|
||||
- 菜单管理中隐藏的菜单显示灰色 - by **NicholasLD** [<samp>(adca2)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/adca2e26)
|
||||
- 菜单管理中隐藏的菜单显示灰色 Merge pull request !24 from NicholasLD/N/A - by **不寻俗** [<samp>(4eb77)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/4eb77eac)
|
||||
- **projects**:
|
||||
- 调整为批量上传文件 - by **AN** [<samp>(ffd62)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/ffd6211e)
|
||||
- 重构流程设计菜单层级 - by **AN** [<samp>(b6f4f)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/b6f4fb5a)
|
||||
- 修改流程实例动态引入组件 - by **AN** [<samp>(a3dce)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/a3dcee4a)
|
||||
- 文件命名修正 - by **AN** [<samp>(5e496)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/5e49622e)
|
||||
- 移动工作流组件位置 - by @m-xlsea [<samp>(7ffaa)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/7ffaac58)
|
||||
- 菜单列表新增禁用菜单样式 - by @m-xlsea [<samp>(e5383)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/e538355f)
|
||||
- **types**:
|
||||
- 移除无用type - by **AN** [<samp>(e86a6)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/e86a6d1b)
|
||||
- **utils**:
|
||||
- 简化加载组件方法 - by **AN** [<samp>(22710)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/22710ecb)
|
||||
|
||||
### 📖 文档
|
||||
|
||||
- **projects**:
|
||||
- 流程定义菜单更新 - by **AN** [<samp>(1af4e)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/1af4e963)
|
||||
- 新增工作流更新sql - by **AN** [<samp>(89e7e)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/89e7edb3)
|
||||
- 流程表达式菜单sql更新 - by **AN** [<samp>(7484f)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/7484f79b)
|
||||
|
||||
### 🏡 杂项
|
||||
|
||||
- **other**: update the ESLint validation configuration to support more file types. - by **Azir-11** [<samp>(8d7f9)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/8d7f91dc)
|
||||
- **readme**: remove DartNode sponsorship badge from README files - by @soybeanjs [<samp>(33ade)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/33ade539)
|
||||
|
||||
### ❤️ 贡献者
|
||||
|
||||
[](https://github.com/soybeanjs) [](https://github.com/m-xlsea) [](https://gitee.com/elio-an) [](https://github.com/Azir-11) [](https://github.com/NicholasLD)
|
||||
[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)
|
||||
|
||||
### 🐞 Bug 修复
|
||||
|
||||
- 修复 api.d.ts.vm 代码生成模板bug - by **zygalaxy** [<samp>(4e8c8)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/4e8c8715)
|
||||
- **projects**:
|
||||
- 修复刷新时跳转至登录页问题 - by **AN** [<samp>(2587f)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/2587f8cb)
|
||||
- 修复登录过期不弹窗问题 - by **AN** [<samp>(e485f)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/e485f680)
|
||||
- 修复菜单结构变动后路由无法进入问题 - by @m-xlsea [<samp>(f4038)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/f4038a2d)
|
||||
|
||||
### 🛠 优化
|
||||
|
||||
- **projects**: 优化搜索框FormItem - by **AN** [<samp>(a1336)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/a1336d15)
|
||||
|
||||
### 🏡 杂项
|
||||
|
||||
- **deps**: update deps - by @soybeanjs [<samp>(e89b8)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/e89b86ce)
|
||||
|
||||
### 🎨 样式
|
||||
|
||||
- **projects**: 搜索FormItem占比调整 - by **AN** [<samp>(cc29e)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/cc29ea85)
|
||||
|
||||
### ❤️ 贡献者
|
||||
|
||||
[](https://github.com/m-xlsea) [](https://gitee.com/elio-an) [](https://github.com/soybeanjs)
|
||||
[zygalaxy](mailto:zygalaxy@qq.com)
|
||||
|
||||
## [v1.1.1](https://gitee.com/xlsea/ruoyi-plus-soybean/compare/v1.1.0...v1.1.1) (2025-07-11)
|
||||
|
||||
### 🚀 新功能
|
||||
|
||||
- **hooks**:
|
||||
- 重构下载方法,支持流式下载 - by @m-xlsea [<samp>(65067)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/650673e2)
|
||||
- **projects**:
|
||||
- 角色分配用户新增部门与时间查询条件 - by @m-xlsea [<samp>(ad48d)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/ad48d8e8)
|
||||
- 修改操作后列表查询方式 - by @m-xlsea [<samp>(d8542)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/d85424ee)
|
||||
|
||||
### 🐞 Bug 修复
|
||||
|
||||
- **hooks**:
|
||||
- 解决 streamsaver 访问不到 Github 资源问题 - by @m-xlsea [<samp>(566b2)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/566b2c2d)
|
||||
- **other**:
|
||||
- 修复代码生成类型定义文件重复问题 - by @m-xlsea [<samp>(f7c7f)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/f7c7fc41)
|
||||
- **packages**:
|
||||
- 修复 cleanup 会删除富文本编辑器资源问题 - by @m-xlsea [<samp>(9ca7c)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/9ca7ca8f)
|
||||
- **projects**:
|
||||
- 修复字典数据重复获取问题 - by @m-xlsea [<samp>(3628c)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/3628c249)
|
||||
- 修改强退在线设备接口 - by **AN** [<samp>(dbcf8)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/dbcf8d42)
|
||||
- 修复代码生成逻辑判断问题 - by **AN** [<samp>(6fc7b)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/6fc7b11b)
|
||||
- 修复部门字典 sys_normal_disable 重复获取 Merge pull request !11 from 素还真/N/A - by @m-xlsea [<samp>(ad938)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/ad9386eb)
|
||||
- 修复未清空文件列表,上传回显问题 - by **AN** [<samp>(229e0)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/229e0044)
|
||||
- Fix i18n-ally not working when setting moduleResolution to bundler. fixed #780 - by @xiaobao0505 in https://gitee.com/xlsea/ruoyi-plus-soybean/issues/780 [<samp>(41191)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/41191d54)
|
||||
- 修复角色列表操作栏展示不全问题 - by @m-xlsea [<samp>(62f2c)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/62f2c6d5)
|
||||
- 修复用户导入结果信息未渲染标签问题 - by **AN** [<samp>(efc95)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/efc953c0)
|
||||
- 修复角色用户分配未调用接口问题 - by @m-xlsea [<samp>(ff874)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/ff87415d)
|
||||
- **styles**:
|
||||
- 修复登录页平板界面滚动问题 - by @m-xlsea [<samp>(90145)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/90145fa5)
|
||||
- **utils**:
|
||||
- 修复isNull和IsNotNull判断方法潜在问题 - by **AN** [<samp>(90d32)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/90d32ee2)
|
||||
|
||||
### 💅 重构
|
||||
|
||||
- **projects**: 调整租户套餐菜单接口 - by **AN** [<samp>(b9999)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/b9999935)
|
||||
|
||||
### 📖 文档
|
||||
|
||||
- **other**: 修改文档内容 - by @m-xlsea [<samp>(3ae99)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/3ae9922d)
|
||||
- **projects**: 优化 cursor 规则及 mcp - by @m-xlsea [<samp>(a3199)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/a31994dc)
|
||||
- **readme**: 更新 README.md 文件 - by @m-xlsea [<samp>(99675)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/99675cbc)
|
||||
|
||||
### 🏡 杂项
|
||||
|
||||
- **deps**:
|
||||
- update NodeJS and pnpm version requirements in package.json and documentation - by **Junior25306** [<samp>(a5c4b)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/a5c4b4e3)
|
||||
- update deps - by @soybeanjs [<samp>(5cb1c)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/5cb1cebd)
|
||||
- update deps - by @soybeanjs [<samp>(aeb63)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/aeb63690)
|
||||
- update deps - by @m-xlsea [<samp>(89c71)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/89c716e1)
|
||||
- **packages**:
|
||||
- update Vite version to 7 in package.json and documentation. - by **Azir** [<samp>(03dd6)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/03dd64c5)
|
||||
- **projects**:
|
||||
- update pnpm-lock.yaml - by @m-xlsea [<samp>(7c6ca)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/7c6ca91e)
|
||||
- **vscode**:
|
||||
- remove unused vue.server.hybridMode setting from .vscode/settings.json - by @soybeanjs [<samp>(13319)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/133196f3)
|
||||
|
||||
### ❤️ 贡献值
|
||||
|
||||
[](https://github.com/m-xlsea) [](https://github.com/soybeanjs) [](https://github.com/xiaobao0505) [](https://gitee.com/elio-an) [](https://github.com/Azir-11) [Junior25306](mailto:dayu429@qq.com)
|
||||
|
||||
## [v1.1.0](https://gitee.com/xlsea/ruoyi-plus-soybean/compare/v1.0.0...v1.1.0) (2025-07-01)
|
||||
|
||||
### 🚀 新功能
|
||||
@ -90,7 +287,7 @@
|
||||
|
||||
### ❤️ 贡献者
|
||||
|
||||
[](https://gitee.com/xlsea) [](https://github.com/soybeanjs) [](https://github.com/wenyuanw) [](https://gitee.com/elio-an) [](https://github.com/chen-ziwen)
|
||||
[](https://gitee.com/xlsea) [](https://github.com/soybeanjs) [](https://github.com/wenyuanw) [](https://gitee.com/elio-an) [](https://github.com/chen-ziwen)
|
||||
[](https://gitee.com/wangzhongqi0917) [](https://gitee.com/qq1822213252) [](https://gitee.com/tangzc), [metabytes](https://gitee.com/metabytes)
|
||||
|
||||
|
||||
|
12
README.md
12
README.md
@ -22,13 +22,12 @@
|
||||
|
||||
# 📢 重要通知
|
||||
|
||||
1.1.0 版本已发布,但仍然建议:
|
||||
1.1.3 版本已经正式发布(工作流版本请切换 [flow](https://gitee.com/xlsea/ruoyi-plus-soybean/tree/flow/) 分支查看),但仍然建议:
|
||||
- 在生产环境使用前进行充分测试
|
||||
- 关注项目更新,及时获取最新版本
|
||||
- 积极反馈问题,帮助我们快速迭代
|
||||
|
||||
**后续规划**
|
||||
- 工作流引擎集成
|
||||
- 多语言国际化完善
|
||||
- 性能优化和稳定性提升
|
||||
|
||||
@ -41,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 的现代化前端特性,为开发者提供了完整的企业管理解决方案。
|
||||
@ -119,8 +123,8 @@ root
|
||||
## 🚀 环境要求与安装
|
||||
|
||||
### 环境要求
|
||||
- Node.js >= 18.20.0
|
||||
- pnpm >= 8.7.0
|
||||
- Node.js >= 20.19.0
|
||||
- pnpm >= 10.5.0
|
||||
- Git
|
||||
|
||||
### 安装步骤及说明
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
77
docs/template/typings/api.d.ts.vm
vendored
77
docs/template/typings/api.d.ts.vm
vendored
@ -1,44 +1,51 @@
|
||||
#set($BaseEntity = ['createDept', 'createBy', 'createTime', 'updateBy', 'updateTime'])
|
||||
#set($ModuleName = $moduleName.substring(0, 1).toUpperCase() + $moduleName.substring(1))
|
||||
/**
|
||||
* namespace ${ModuleName}
|
||||
* Namespace Api
|
||||
*
|
||||
* backend api module: "${ModuleName}"
|
||||
* All backend api type
|
||||
*/
|
||||
namespace ${ModuleName} {
|
||||
/** ${businessname} */
|
||||
type ${BusinessName} = Common.CommonRecord<{
|
||||
#foreach($column in $columns)#if(!$BaseEntity.contains($column.javaField))
|
||||
/** $column.columnComment */
|
||||
$column.javaField:#if($column.javaField.indexOf("id") != -1 || $column.javaField.indexOf("Id") != -1) CommonType.IdType; #elseif($column.javaType == 'Long' || $column.javaType == 'Integer' || $column.javaType == 'Double' || $column.javaType == 'Float' || $column.javaType == 'BigDecimal') number; #elseif($column.javaType == 'Boolean') boolean; #else string; #end
|
||||
#end#end
|
||||
}>;
|
||||
declare namespace Api {
|
||||
/**
|
||||
* namespace ${ModuleName}
|
||||
*
|
||||
* backend api module: "${ModuleName}"
|
||||
*/
|
||||
namespace ${ModuleName} {
|
||||
/** ${businessname} */
|
||||
type ${BusinessName} = Common.CommonRecord<{
|
||||
#foreach($column in $columns)#if(!$BaseEntity.contains($column.javaField))
|
||||
/** $column.columnComment */
|
||||
$column.javaField:#if($column.javaField.indexOf("id") != -1 || $column.javaField.indexOf("Id") != -1) CommonType.IdType; #elseif($column.javaType == 'Long' || $column.javaType == 'Integer' || $column.javaType == 'Double' || $column.javaType == 'Float' || $column.javaType == 'BigDecimal') number; #elseif($column.javaType == 'Boolean') boolean; #else string; #end
|
||||
#end#end
|
||||
}>;
|
||||
|
||||
/** ${businessname} search params */
|
||||
type ${BusinessName}SearchParams = CommonType.RecordNullable<
|
||||
Pick<
|
||||
Api.${ModuleName}.${BusinessName},
|
||||
#foreach($column in $columns)
|
||||
#if($column.query && $column.queryType != 'BETWEEN')
|
||||
| '${column.javaField}'
|
||||
#end
|
||||
#end
|
||||
> &
|
||||
Api.Common.CommonSearchParams
|
||||
>;
|
||||
/** ${businessname} search params */
|
||||
type ${BusinessName}SearchParams = CommonType.RecordNullable<
|
||||
Pick<
|
||||
Api.${ModuleName}.${BusinessName},
|
||||
#foreach($column in $columns)
|
||||
#if($column.query && $column.queryType != 'BETWEEN')
|
||||
| '${column.javaField}'
|
||||
#end
|
||||
#end
|
||||
> &
|
||||
Api.Common.CommonSearchParams
|
||||
>;
|
||||
|
||||
/** ${businessname} operate params */
|
||||
type ${BusinessName}OperateParams = CommonType.RecordNullable<
|
||||
Pick<
|
||||
Api.${ModuleName}.${BusinessName},
|
||||
#foreach($column in $columns)
|
||||
#if($column.insert || $column.edit)
|
||||
| '${column.javaField}'
|
||||
#end
|
||||
#end
|
||||
>
|
||||
>;
|
||||
/** ${businessname} operate params */
|
||||
type ${BusinessName}OperateParams = CommonType.RecordNullable<
|
||||
Pick<
|
||||
Api.${ModuleName}.${BusinessName},
|
||||
#foreach($column in $columns)
|
||||
#if($column.insert || $column.edit)
|
||||
| '${column.javaField}'
|
||||
#end
|
||||
#end
|
||||
>
|
||||
>;
|
||||
|
||||
/** ${businessname} list */
|
||||
type ${BusinessName}List = Api.Common.PaginatingQueryRecord<${BusinessName}>;
|
||||
/** ${businessname} list */
|
||||
type ${BusinessName}List = Api.Common.PaginatingQueryRecord<${BusinessName}>;
|
||||
}
|
||||
}
|
||||
|
65
package.json
65
package.json
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "ruoyi-vue-plus",
|
||||
"type": "module",
|
||||
"version": "1.1.0",
|
||||
"version": "1.1.3",
|
||||
"description": "结合了 RuoYi-Vue-Plus 的强大后端功能和 Soybean Admin 的现代化前端特性,为开发者提供了完整的企业管理解决方案。",
|
||||
"author": {
|
||||
"name": "xlsea",
|
||||
@ -21,7 +21,7 @@
|
||||
"Soybean Admin",
|
||||
"Vue3 admin ",
|
||||
"vue-admin-template",
|
||||
"Vite6",
|
||||
"Vite7",
|
||||
"TypeScript",
|
||||
"naive-ui",
|
||||
"naive-ui-admin",
|
||||
@ -66,11 +66,11 @@
|
||||
"@sa/tinymce": "workspace:*",
|
||||
"@sa/utils": "workspace:*",
|
||||
"@types/streamsaver": "^2.0.5",
|
||||
"@vueuse/core": "13.4.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.7",
|
||||
"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.353",
|
||||
"@iconify/json": "2.2.378",
|
||||
"@sa/scripts": "workspace:*",
|
||||
"@sa/uno-preset": "workspace:*",
|
||||
"@soybeanjs/eslint-config": "1.7.0",
|
||||
"@types/node": "24.0.4",
|
||||
"@soybeanjs/eslint-config": "1.7.1",
|
||||
"@types/node": "24.3.0",
|
||||
"@types/nprogress": "0.2.3",
|
||||
"@unocss/eslint-config": "66.3.2",
|
||||
"@unocss/preset-icons": "66.3.2",
|
||||
"@unocss/preset-uno": "66.3.2",
|
||||
"@unocss/transformer-directives": "66.3.2",
|
||||
"@unocss/transformer-variant-group": "66.3.2",
|
||||
"@unocss/vite": "66.3.2",
|
||||
"@vitejs/plugin-vue": "6.0.0",
|
||||
"@vitejs/plugin-vue-jsx": "5.0.0",
|
||||
"@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.29.0",
|
||||
"eslint-plugin-vue": "10.2.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.7.0",
|
||||
"vite": "7.0.0",
|
||||
"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.0.2",
|
||||
"vite-plugin-static-copy": "^3.1.0",
|
||||
"vite-plugin-svg-icons": "2.0.1",
|
||||
"vite-plugin-vue-devtools": "7.7.7",
|
||||
"vue-eslint-parser": "10.1.4",
|
||||
"vue-tsc": "2.2.10"
|
||||
"vite-plugin-vue-devtools": "8.0.1",
|
||||
"vue-eslint-parser": "10.2.0",
|
||||
"vue-tsc": "3.0.6"
|
||||
},
|
||||
"simple-git-hooks": {
|
||||
"commit-msg": "pnpm sa git-commit-verify",
|
||||
|
@ -15,6 +15,6 @@
|
||||
"dependencies": {
|
||||
"@alova/mock": "2.0.17",
|
||||
"@sa/utils": "workspace:*",
|
||||
"alova": "3.3.3"
|
||||
"alova": "3.3.4"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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
|
||||
};
|
||||
}
|
||||
|
@ -11,7 +11,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@sa/utils": "workspace:*",
|
||||
"simplebar-vue": "2.4.1"
|
||||
"simplebar-vue": "2.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typed-css-modules": "0.9.1"
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import type { CliOption } from '../types';
|
||||
const defaultOptions: CliOption = {
|
||||
cwd: process.cwd(),
|
||||
cleanupDirs: [
|
||||
'**/dist',
|
||||
'dist',
|
||||
'**/package-lock.json',
|
||||
'**/yarn.lock',
|
||||
'**/pnpm-lock.yaml',
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@sa/tinymce",
|
||||
"version": "1.3.13",
|
||||
"version": "1.3.15",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
|
2897
pnpm-lock.yaml
generated
2897
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
179
public/streamsaver/mitm.html
Normal file
179
public/streamsaver/mitm.html
Normal file
@ -0,0 +1,179 @@
|
||||
<!--
|
||||
mitm.html is the lite "man in the middle"
|
||||
|
||||
This is only meant to signal the opener's messageChannel to
|
||||
the service worker - when that is done this mitm can be closed
|
||||
but it's better to keep it alive since this also stops the sw
|
||||
from restarting
|
||||
|
||||
The service worker is capable of intercepting all request and fork their
|
||||
own "fake" response - wish we are going to craft
|
||||
when the worker then receives a stream then the worker will tell the opener
|
||||
to open up a link that will start the download
|
||||
-->
|
||||
<script>
|
||||
// This will prevent the sw from restarting
|
||||
let keepAlive = () => {
|
||||
keepAlive = () => {};
|
||||
var ping = location.href.substr(0, location.href.lastIndexOf('/')) + '/ping';
|
||||
var interval = setInterval(() => {
|
||||
if (sw) {
|
||||
sw.postMessage('ping');
|
||||
} else {
|
||||
fetch(ping).then(res => res.text(!res.ok && clearInterval(interval)));
|
||||
}
|
||||
}, 10000);
|
||||
};
|
||||
|
||||
// message event is the first thing we need to setup a listner for
|
||||
// don't want the opener to do a random timeout - instead they can listen for
|
||||
// the ready event
|
||||
// but since we need to wait for the Service Worker registration, we store the
|
||||
// message for later
|
||||
let messages = [];
|
||||
window.onmessage = evt => messages.push(evt);
|
||||
|
||||
let sw = null;
|
||||
let scope = '';
|
||||
|
||||
function registerWorker() {
|
||||
return navigator.serviceWorker
|
||||
.getRegistration('./')
|
||||
.then(swReg => {
|
||||
return swReg || navigator.serviceWorker.register('sw.js', { scope: './' });
|
||||
})
|
||||
.then(swReg => {
|
||||
const swRegTmp = swReg.installing || swReg.waiting;
|
||||
|
||||
scope = swReg.scope;
|
||||
|
||||
return (
|
||||
(sw = swReg.active) ||
|
||||
new Promise(resolve => {
|
||||
swRegTmp.addEventListener(
|
||||
'statechange',
|
||||
(fn = () => {
|
||||
if (swRegTmp.state === 'activated') {
|
||||
swRegTmp.removeEventListener('statechange', fn);
|
||||
sw = swReg.active;
|
||||
resolve();
|
||||
}
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Now that we have the Service Worker registered we can process messages
|
||||
function onMessage(event) {
|
||||
let { data, ports, origin } = event;
|
||||
|
||||
// It's important to have a messageChannel, don't want to interfere
|
||||
// with other simultaneous downloads
|
||||
if (!ports || !ports.length) {
|
||||
throw new TypeError("[StreamSaver] You didn't send a messageChannel");
|
||||
}
|
||||
|
||||
if (typeof data !== 'object') {
|
||||
throw new TypeError("[StreamSaver] You didn't send a object");
|
||||
}
|
||||
|
||||
// the default public service worker for StreamSaver is shared among others.
|
||||
// so all download links needs to be prefixed to avoid any other conflict
|
||||
data.origin = origin;
|
||||
|
||||
// if we ever (in some feature versoin of streamsaver) would like to
|
||||
// redirect back to the page of who initiated a http request
|
||||
data.referrer = data.referrer || document.referrer || origin;
|
||||
|
||||
// pass along version for possible backwards compatibility in sw.js
|
||||
data.streamSaverVersion = new URLSearchParams(location.search).get('version');
|
||||
|
||||
if (data.streamSaverVersion === '1.2.0') {
|
||||
console.warn('[StreamSaver] please update streamsaver');
|
||||
}
|
||||
|
||||
/** @since v2.0.0 */
|
||||
if (!data.headers) {
|
||||
console.warn(
|
||||
"[StreamSaver] pass `data.headers` that you would like to pass along to the service worker\nit should be a 2D array or a key/val object that fetch's Headers api accepts"
|
||||
);
|
||||
} else {
|
||||
// test if it's correct
|
||||
// should thorw a typeError if not
|
||||
new Headers(data.headers);
|
||||
}
|
||||
|
||||
/** @since v2.0.0 */
|
||||
if (typeof data.filename === 'string') {
|
||||
console.warn(
|
||||
"[StreamSaver] You shouldn't send `data.filename` anymore. It should be included in the Content-Disposition header option"
|
||||
);
|
||||
// Do what File constructor do with fileNames
|
||||
data.filename = data.filename.replace(/\//g, ':');
|
||||
}
|
||||
|
||||
/** @since v2.0.0 */
|
||||
if (data.size) {
|
||||
console.warn(
|
||||
"[StreamSaver] You shouldn't send `data.size` anymore. It should be included in the content-length header option"
|
||||
);
|
||||
}
|
||||
|
||||
/** @since v2.0.0 */
|
||||
if (data.readableStream) {
|
||||
console.warn('[StreamSaver] You should send the readableStream in the messageChannel, not throught mitm');
|
||||
}
|
||||
|
||||
/** @since v2.0.0 */
|
||||
if (!data.pathname) {
|
||||
console.warn('[StreamSaver] Please send `data.pathname` (eg: /pictures/summer.jpg)');
|
||||
data.pathname = Math.random().toString().slice(-6) + '/' + data.filename;
|
||||
}
|
||||
|
||||
// remove all leading slashes
|
||||
data.pathname = data.pathname.replace(/^\/+/g, '');
|
||||
|
||||
// remove protocol
|
||||
let org = origin.replace(/(^\w+:|^)\/\//, '');
|
||||
|
||||
// set the absolute pathname to the download url.
|
||||
data.url = new URL(`${scope + org}/${data.pathname}`).toString();
|
||||
|
||||
if (!data.url.startsWith(`${scope + org}/`)) {
|
||||
throw new TypeError('[StreamSaver] bad `data.pathname`');
|
||||
}
|
||||
|
||||
// This sends the message data as well as transferring
|
||||
// messageChannel.port2 to the service worker. The service worker can
|
||||
// then use the transferred port to reply via postMessage(), which
|
||||
// will in turn trigger the onmessage handler on messageChannel.port1.
|
||||
|
||||
const transferable = data.readableStream ? [ports[0], data.readableStream] : [ports[0]];
|
||||
|
||||
if (!(data.readableStream || data.transferringReadable)) {
|
||||
keepAlive();
|
||||
}
|
||||
|
||||
return sw.postMessage(data, transferable);
|
||||
}
|
||||
|
||||
if (window.opener) {
|
||||
// The opener can't listen to onload event, so we need to help em out!
|
||||
// (telling them that we are ready to accept postMessage's)
|
||||
window.opener.postMessage('StreamSaver::loadedPopup', '*');
|
||||
}
|
||||
|
||||
if (navigator.serviceWorker) {
|
||||
registerWorker().then(() => {
|
||||
window.onmessage = onMessage;
|
||||
messages.forEach(window.onmessage);
|
||||
});
|
||||
}
|
||||
|
||||
// FF v102 just started to supports transferable streams, but still needs to ping sw.js
|
||||
// even tough the service worker dose not have to do any kind of work and listen to any
|
||||
// messages... #305
|
||||
keepAlive();
|
||||
</script>
|
132
public/streamsaver/sw.js
Normal file
132
public/streamsaver/sw.js
Normal file
@ -0,0 +1,132 @@
|
||||
/* eslint-disable */
|
||||
/* global self ReadableStream Response */
|
||||
|
||||
self.addEventListener('install', () => {
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener('activate', event => {
|
||||
event.waitUntil(self.clients.claim());
|
||||
});
|
||||
|
||||
const map = new Map();
|
||||
|
||||
// This should be called once per download
|
||||
// Each event has a dataChannel that the data will be piped through
|
||||
self.onmessage = event => {
|
||||
// We send a heartbeat every x second to keep the
|
||||
// service worker alive if a transferable stream is not sent
|
||||
if (event.data === 'ping') {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = event.data;
|
||||
const downloadUrl =
|
||||
data.url || `${self.registration.scope + Math.random()}/${typeof data === 'string' ? data : data.filename}`;
|
||||
const port = event.ports[0];
|
||||
const metadata = Array.from({ length: 3 }); // [stream, data, port]
|
||||
|
||||
metadata[1] = data;
|
||||
metadata[2] = port;
|
||||
|
||||
// Note to self:
|
||||
// old streamsaver v1.2.0 might still use `readableStream`...
|
||||
// but v2.0.0 will always transfer the stream through MessageChannel #94
|
||||
if (event.data.readableStream) {
|
||||
metadata[0] = event.data.readableStream;
|
||||
} else if (event.data.transferringReadable) {
|
||||
port.onmessage = evt => {
|
||||
port.onmessage = null;
|
||||
metadata[0] = evt.data.readableStream;
|
||||
};
|
||||
} else {
|
||||
metadata[0] = createStream(port);
|
||||
}
|
||||
|
||||
map.set(downloadUrl, metadata);
|
||||
port.postMessage({ download: downloadUrl });
|
||||
};
|
||||
|
||||
function createStream(port) {
|
||||
// ReadableStream is only supported by chrome 52
|
||||
return new ReadableStream({
|
||||
start(controller) {
|
||||
// When we receive data on the messageChannel, we write
|
||||
port.onmessage = ({ data }) => {
|
||||
if (data === 'end') {
|
||||
return controller.close();
|
||||
}
|
||||
|
||||
if (data === 'abort') {
|
||||
controller.error('Aborted the download');
|
||||
return;
|
||||
}
|
||||
|
||||
controller.enqueue(data);
|
||||
};
|
||||
},
|
||||
cancel(reason) {
|
||||
console.log('user aborted', reason);
|
||||
port.postMessage({ abort: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
self.onfetch = event => {
|
||||
const url = event.request.url;
|
||||
|
||||
// this only works for Firefox
|
||||
if (url.endsWith('/ping')) {
|
||||
return event.respondWith(new Response('pong'));
|
||||
}
|
||||
|
||||
const hijacke = map.get(url);
|
||||
|
||||
if (!hijacke) return null;
|
||||
|
||||
const [stream, data, port] = hijacke;
|
||||
|
||||
map.delete(url);
|
||||
|
||||
// Not comfortable letting any user control all headers
|
||||
// so we only copy over the length & disposition
|
||||
const responseHeaders = new Headers({
|
||||
'Content-Type': 'application/octet-stream; charset=utf-8',
|
||||
|
||||
// To be on the safe side, The link can be opened in a iframe.
|
||||
// but octet-stream should stop it.
|
||||
'Content-Security-Policy': "default-src 'none'",
|
||||
'X-Content-Security-Policy': "default-src 'none'",
|
||||
'X-WebKit-CSP': "default-src 'none'",
|
||||
'X-XSS-Protection': '1; mode=block',
|
||||
'Cross-Origin-Embedder-Policy': 'require-corp'
|
||||
});
|
||||
|
||||
const headers = new Headers(data.headers || {});
|
||||
|
||||
if (headers.has('Content-Length')) {
|
||||
responseHeaders.set('Content-Length', headers.get('Content-Length'));
|
||||
}
|
||||
|
||||
if (headers.has('Content-Disposition')) {
|
||||
responseHeaders.set('Content-Disposition', headers.get('Content-Disposition'));
|
||||
}
|
||||
|
||||
// data, data.filename and size should not be used anymore
|
||||
if (data.size) {
|
||||
console.warn('Depricated');
|
||||
responseHeaders.set('Content-Length', data.size);
|
||||
}
|
||||
|
||||
let fileName = typeof data === 'string' ? data : data.filename;
|
||||
if (fileName) {
|
||||
console.warn('Depricated');
|
||||
// Make filename RFC5987 compatible
|
||||
fileName = encodeURIComponent(fileName).replace(/['()]/g, escape).replace(/\*/g, '%2A');
|
||||
responseHeaders.set('Content-Disposition', `attachment; filename*=UTF-8''${fileName}`);
|
||||
}
|
||||
|
||||
event.respondWith(new Response(stream, { headers: responseHeaders }));
|
||||
|
||||
port.postMessage({ debug: 'Download started' });
|
||||
};
|
@ -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);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -20,44 +20,45 @@ const fileList = ref<UploadFileInfo[]>([]);
|
||||
|
||||
async function handleFetchOssList(ossIds: string[]) {
|
||||
startLoading();
|
||||
const { error, data } = await fetchGetOssListByIds(ossIds);
|
||||
if (error) return;
|
||||
fileList.value = data.map(item => ({
|
||||
id: String(item.ossId),
|
||||
url: item.url,
|
||||
name: item.originalName,
|
||||
status: 'finished'
|
||||
}));
|
||||
endLoading();
|
||||
try {
|
||||
const { error, data } = await fetchGetOssListByIds(ossIds);
|
||||
if (error) return;
|
||||
fileList.value = data.map(item => ({
|
||||
id: String(item.ossId),
|
||||
url: item.url,
|
||||
name: item.originalName,
|
||||
status: 'finished'
|
||||
}));
|
||||
} catch (error) {
|
||||
window.$message?.error(`获取文件列表失败: ${error}`);
|
||||
} finally {
|
||||
endLoading();
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
value,
|
||||
async val => {
|
||||
const ossIds = val?.split(',')?.filter(item => isNotNull(item)) || [];
|
||||
const fileIds = new Set(fileList.value.filter(item => item.status === 'finished').map(item => item.id));
|
||||
if (ossIds.every(item => fileIds.has(item))) {
|
||||
return;
|
||||
}
|
||||
if (ossIds.length === 0) {
|
||||
fileList.value = [];
|
||||
return;
|
||||
}
|
||||
const fileIds = new Set(fileList.value.filter(item => item.status === 'finished').map(item => item.id));
|
||||
if (ossIds.every(item => fileIds.has(item))) {
|
||||
return;
|
||||
}
|
||||
await handleFetchOssList(ossIds);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
fileList,
|
||||
val => {
|
||||
value.value = val
|
||||
.filter(item => item.status === 'finished')
|
||||
.map(item => item.id)
|
||||
.join(',');
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
watch(fileList, val => {
|
||||
value.value = val
|
||||
.filter(item => item.status === 'finished')
|
||||
.map(item => item.id)
|
||||
.join(',');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -21,27 +21,27 @@ const attrs: SelectProps = useAttrs();
|
||||
|
||||
const { loading: postLoading, startLoading: startPostLoading, endLoading: endPostLoading } = useLoading();
|
||||
|
||||
/** the enabled role options */
|
||||
const roleOptions = ref<CommonType.Option<CommonType.IdType>[]>([]);
|
||||
/** the enabled post options */
|
||||
const postOptions = ref<CommonType.Option<CommonType.IdType>[]>([]);
|
||||
|
||||
watch(
|
||||
() => props.deptId,
|
||||
() => {
|
||||
if (!props.deptId) {
|
||||
roleOptions.value = [];
|
||||
postOptions.value = [];
|
||||
return;
|
||||
}
|
||||
getRoleOptions();
|
||||
getPostOptions();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
async function getRoleOptions() {
|
||||
async function getPostOptions() {
|
||||
startPostLoading();
|
||||
const { error, data } = await fetchGetPostSelect(props.deptId!);
|
||||
|
||||
if (!error) {
|
||||
roleOptions.value = data.map(item => ({
|
||||
postOptions.value = data.map(item => ({
|
||||
label: item.postName,
|
||||
value: item.postId
|
||||
}));
|
||||
@ -54,7 +54,7 @@ async function getRoleOptions() {
|
||||
<NSelect
|
||||
v-model:value="value"
|
||||
:loading="postLoading"
|
||||
:options="roleOptions"
|
||||
:options="postOptions"
|
||||
v-bind="attrs"
|
||||
placeholder="请选择岗位"
|
||||
/>
|
||||
|
79
src/components/custom/tag-group.vue
Normal file
79
src/components/custom/tag-group.vue
Normal 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>
|
370
src/components/custom/user-select-modal.vue
Normal file
370
src/components/custom/user-select-modal.vue
Normal 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>
|
@ -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>
|
||||
<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 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>
|
||||
</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>
|
||||
|
178
src/components/workflow/approval-info-panel.vue
Normal file
178
src/components/workflow/approval-info-panel.vue
Normal 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>
|
142
src/components/workflow/back-task-modal.vue
Normal file
142
src/components/workflow/back-task-modal.vue
Normal 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>
|
62
src/components/workflow/flow-category-select.vue
Normal file
62
src/components/workflow/flow-category-select.vue
Normal 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();
|
||||
|
||||
/** 转换为str,id可能是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>
|
96
src/components/workflow/flow-drawer.vue
Normal file
96
src/components/workflow/flow-drawer.vue
Normal 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>
|
222
src/components/workflow/flow-intervene-modal.vue
Normal file
222
src/components/workflow/flow-intervene-modal.vue
Normal 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>
|
30
src/components/workflow/flow-preview.vue
Normal file
30
src/components/workflow/flow-preview.vue
Normal 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>
|
496
src/components/workflow/flow-task-approval-modal.vue
Normal file
496
src/components/workflow/flow-task-approval-modal.vue
Normal 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>
|
95
src/components/workflow/flow-urge-modal.vue
Normal file
95
src/components/workflow/flow-urge-modal.vue
Normal 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>
|
317
src/components/workflow/form/leave-edit/index.vue
Normal file
317
src/components/workflow/form/leave-edit/index.vue
Normal 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>
|
154
src/components/workflow/reduce-signature-modal.vue
Normal file
154
src/components/workflow/reduce-signature-modal.vue
Normal 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
90
src/constants/workflow.ts
Normal 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);
|
@ -16,6 +16,12 @@ export function useDownload() {
|
||||
const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y';
|
||||
const { baseURL } = getServiceBaseURL(import.meta.env, isHttpProxy);
|
||||
|
||||
const isHttps = () => {
|
||||
const protocol = document.location.protocol;
|
||||
const hostname = document.location.hostname;
|
||||
return protocol === 'https' || hostname === 'localhost' || hostname === '127.0.0.1';
|
||||
};
|
||||
|
||||
/** 获取通用请求头 */
|
||||
const getCommonHeaders = (contentType = 'application/octet-stream') => ({
|
||||
Authorization: `Bearer ${localStg.get('token')}`,
|
||||
@ -51,6 +57,7 @@ export function useDownload() {
|
||||
contentLength?: number
|
||||
): Promise<void> {
|
||||
window.$loading?.endLoading();
|
||||
StreamSaver.mitm = '/streamsaver/mitm.html?version=2.0.0';
|
||||
const fileStream = StreamSaver.createWriteStream(filename, { size: contentLength });
|
||||
|
||||
if (window.WritableStream && readableStream?.pipeTo) {
|
||||
@ -106,11 +113,16 @@ export function useDownload() {
|
||||
|
||||
const response = await fetch(fullUrl, requestOptions);
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new Error(errorCodeRecord.default);
|
||||
}
|
||||
|
||||
await handleResponse(response);
|
||||
|
||||
const finalFilename = filename || response.headers.get('Download-Filename') || `download-${timestamp}`;
|
||||
const rawHeader = response.headers.get('Download-Filename');
|
||||
const finalFilename = filename || (rawHeader ? decodeURIComponent(rawHeader) : null) || `download-${timestamp}`;
|
||||
|
||||
if (response.body) {
|
||||
if (response.body && isHttps()) {
|
||||
const contentLength = Number(response.headers.get('Content-Length'));
|
||||
await downloadByStream(response.body, finalFilename, contentLength);
|
||||
return;
|
||||
|
@ -2,6 +2,7 @@ import { ref, toValue } from 'vue';
|
||||
import type { ComputedRef, Ref } from 'vue';
|
||||
import type { FormInst } from 'naive-ui';
|
||||
import { REG_CODE_SIX, REG_EMAIL, REG_PHONE, REG_PWD, REG_USER_NAME } from '@/constants/reg';
|
||||
import { isNull } from '@/utils/common';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
export function useFormRules() {
|
||||
@ -52,7 +53,7 @@ export function useFormRules() {
|
||||
required: true,
|
||||
trigger: ['input', 'blur'],
|
||||
validator: (_rule: any, value: any) => {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
if (isNull(value) || (Array.isArray(value) && value.length === 0)) {
|
||||
return new Error(message);
|
||||
}
|
||||
return true;
|
||||
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -33,7 +33,7 @@ const toGitee = () => {
|
||||
</NBadge>
|
||||
</NButton>
|
||||
</template>
|
||||
消息
|
||||
{{ $t('page.home.message') }}
|
||||
</NTooltip>
|
||||
</template>
|
||||
<NCard
|
||||
|
@ -61,6 +61,7 @@ const local: App.I18n.Schema = {
|
||||
update: 'Update',
|
||||
saveSuccess: 'Save Success',
|
||||
updateSuccess: 'Update Success',
|
||||
noChange: 'No actions were taken',
|
||||
userCenter: 'User Center',
|
||||
yesOrNo: {
|
||||
yes: 'Yes',
|
||||
@ -230,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',
|
||||
@ -327,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',
|
||||
|
@ -61,6 +61,7 @@ const local: App.I18n.Schema = {
|
||||
update: '更新',
|
||||
saveSuccess: '保存成功',
|
||||
updateSuccess: '更新成功',
|
||||
noChange: '没有进行任何操作',
|
||||
userCenter: '个人中心',
|
||||
yesOrNo: {
|
||||
yes: '是',
|
||||
@ -230,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: '租户管理',
|
||||
@ -327,6 +341,8 @@ const local: App.I18n.Schema = {
|
||||
page: {
|
||||
login: {
|
||||
common: {
|
||||
title: '现代化的企业级多租户管理系统',
|
||||
subTitle: '为开发者提供了完整的企业管理解决方案',
|
||||
loginOrRegister: '登录 / 注册',
|
||||
register: '注册',
|
||||
userNamePlaceholder: '请输入用户名',
|
||||
|
@ -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"),
|
||||
};
|
||||
|
@ -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'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
@ -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"
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -78,3 +78,21 @@ export function fetchGetRoleUserList(params: Api.System.UserSearchParams) {
|
||||
params
|
||||
});
|
||||
}
|
||||
|
||||
/** 批量选择用户授权 */
|
||||
export function fetchUpdateRoleAuthUser(roleId: CommonType.IdType, userIds: CommonType.IdType[]) {
|
||||
return request<boolean>({
|
||||
url: '/system/role/authUser/selectAll',
|
||||
method: 'put',
|
||||
params: { roleId, userIds: userIds.join(',') }
|
||||
});
|
||||
}
|
||||
|
||||
/** 批量取消用户授权 */
|
||||
export function fetchUpdateRoleAuthUserCancel(roleId: CommonType.IdType, userIds: CommonType.IdType[]) {
|
||||
return request<boolean>({
|
||||
url: '/system/role/authUser/cancelAll',
|
||||
method: 'put',
|
||||
params: { roleId, userIds: userIds.join(',') }
|
||||
});
|
||||
}
|
||||
|
53
src/service/api/workflow/category.ts
Normal file
53
src/service/api/workflow/category.ts
Normal 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'
|
||||
});
|
||||
}
|
72
src/service/api/workflow/definition.ts
Normal file
72
src/service/api/workflow/definition.ts
Normal 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'
|
||||
});
|
||||
}
|
5
src/service/api/workflow/index.ts
Normal file
5
src/service/api/workflow/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export * from './category';
|
||||
export * from './leave';
|
||||
export * from './instance';
|
||||
export * from './definition';
|
||||
export * from './task';
|
78
src/service/api/workflow/instance.ts
Normal file
78
src/service/api/workflow/instance.ts
Normal 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'
|
||||
});
|
||||
}
|
51
src/service/api/workflow/leave.ts
Normal file
51
src/service/api/workflow/leave.ts
Normal 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'
|
||||
});
|
||||
}
|
36
src/service/api/workflow/spel.ts
Normal file
36
src/service/api/workflow/spel.ts
Normal 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'
|
||||
});
|
||||
}
|
142
src/service/api/workflow/task.ts
Normal file
142
src/service/api/workflow/task.ts
Normal 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
|
||||
});
|
||||
}
|
@ -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) {
|
||||
@ -65,37 +73,47 @@ export const request = createFlatRequest<App.Service.Response, RequestInstanceSt
|
||||
request.state.errMsgStack = request.state.errMsgStack.filter(msg => msg !== response.data.msg);
|
||||
}
|
||||
|
||||
const isLogin = Boolean(localStg.get('token'));
|
||||
|
||||
// when the backend response code is in `logoutCodes`, it means the user will be logged out and redirected to login page
|
||||
// const logoutCodes = import.meta.env.VITE_SERVICE_LOGOUT_CODES?.split(',') || [];
|
||||
// if (logoutCodes.includes(responseCode)) {
|
||||
// handleLogout();
|
||||
// return null;
|
||||
// }
|
||||
const logoutCodes = import.meta.env.VITE_SERVICE_LOGOUT_CODES?.split(',') || [];
|
||||
if (logoutCodes.includes(responseCode) && !isLogin) {
|
||||
logoutAndCleanup();
|
||||
return null;
|
||||
}
|
||||
|
||||
// when the backend response code is in `modalLogoutCodes`, it means the user will be logged out by displaying a modal
|
||||
const modalLogoutCodes = import.meta.env.VITE_SERVICE_MODAL_LOGOUT_CODES?.split(',') || [];
|
||||
if (modalLogoutCodes.includes(responseCode)) {
|
||||
request.state.errMsgStack = [...(request.state.errMsgStack || []), response.data.msg];
|
||||
|
||||
// prevent the user from refreshing the page
|
||||
window.addEventListener('beforeunload', handleLogout);
|
||||
|
||||
if (!window.location.pathname?.startsWith('/login')) {
|
||||
window.$dialog?.warning({
|
||||
title: '系统提示',
|
||||
content: '登录状态已过期,您可以继续留在该页面,或者重新登录',
|
||||
positiveText: '重新登录',
|
||||
negativeText: '取消',
|
||||
maskClosable: false,
|
||||
closeOnEsc: false,
|
||||
onPositiveClick() {
|
||||
logoutAndCleanup();
|
||||
}
|
||||
});
|
||||
|
||||
request.cancelAllRequest();
|
||||
if (modalLogoutCodes.includes(responseCode) && isLogin) {
|
||||
const isExist = request.state.errMsgStack?.includes(response.data.msg);
|
||||
if (isExist) {
|
||||
return null;
|
||||
}
|
||||
if (window.location.pathname?.startsWith('/login')) {
|
||||
logoutAndCleanup();
|
||||
return null;
|
||||
}
|
||||
|
||||
request.state.errMsgStack = [...(request.state.errMsgStack || []), response.data.msg];
|
||||
|
||||
window.$dialog?.warning({
|
||||
title: '系统提示',
|
||||
content: '登录状态已过期,请重新登录',
|
||||
positiveText: '重新登录',
|
||||
maskClosable: false,
|
||||
closeOnEsc: false,
|
||||
onAfterEnter() {
|
||||
// prevent the user from refreshing the page
|
||||
window.addEventListener('beforeunload', handleLogout);
|
||||
},
|
||||
onPositiveClick() {
|
||||
logoutAndCleanup();
|
||||
},
|
||||
onClose() {
|
||||
logoutAndCleanup();
|
||||
}
|
||||
});
|
||||
request.cancelAllRequest();
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -115,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;
|
||||
|
@ -65,11 +65,15 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
||||
|
||||
routes.forEach(route => {
|
||||
if (authRouteMode.value === 'dynamic') {
|
||||
if (route.path === '/') {
|
||||
route.children?.forEach(child => {
|
||||
parseRouter(child);
|
||||
authRoutesMap.set(child.name, child);
|
||||
});
|
||||
if (route.path === '/' && route.children?.length) {
|
||||
const child = route.children[0];
|
||||
// @ts-expect-error no hidden field
|
||||
child.hidden = route.hidden;
|
||||
parseRouter(child);
|
||||
child.name = Math.random().toString(36).slice(2, 12);
|
||||
Object.assign(route, child);
|
||||
delete route.children;
|
||||
authRoutesMap.set(route.name, route);
|
||||
return;
|
||||
}
|
||||
parseRouter(route);
|
||||
@ -121,10 +125,9 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
||||
} else if (!isNotNull(route.meta.icon)) {
|
||||
route.meta.icon = defaultIcon;
|
||||
}
|
||||
|
||||
// @ts-expect-error no hidden field
|
||||
route.meta.hideInMenu = route.hidden;
|
||||
if (route.meta.hideInMenu && parent) {
|
||||
if (route.meta.hideInMenu && parent && !route.meta.activeMenu) {
|
||||
// @ts-expect-error parent.name is activeMenu type
|
||||
route.meta.activeMenu = parent.name;
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ html,
|
||||
body,
|
||||
#app {
|
||||
height: 100%;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
html {
|
||||
|
@ -13,6 +13,13 @@
|
||||
border-color: var(--un-default-border-color, #e5e7eb); /* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
* [Naive UI] Fix the icon size in the image preview toolbar
|
||||
*/
|
||||
.n-image-preview-toolbar .n-base-icon {
|
||||
box-sizing: unset !important;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Use a consistent sensible line-height in all browsers.
|
||||
2. Prevent adjustments of font size after orientation changes in iOS.
|
||||
|
@ -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,
|
||||
|
3
src/typings/api/system.api.d.ts
vendored
3
src/typings/api/system.api.d.ts
vendored
@ -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
|
||||
>;
|
||||
|
||||
@ -163,6 +164,8 @@ declare namespace Api {
|
||||
postIds: string[];
|
||||
/** user role ids */
|
||||
roleIds: string[];
|
||||
/** roles */
|
||||
roles: Role[];
|
||||
};
|
||||
|
||||
/** user list */
|
||||
|
595
src/typings/api/workflow.api.d.ts
vendored
Normal file
595
src/typings/api/workflow.api.d.ts
vendored
Normal 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 };
|
||||
}>;
|
||||
}
|
||||
}
|
3
src/typings/app.d.ts
vendored
3
src/typings/app.d.ts
vendored
@ -376,6 +376,7 @@ declare namespace App {
|
||||
update: string;
|
||||
updateSuccess: string;
|
||||
saveSuccess: string;
|
||||
noChange: string;
|
||||
userCenter: string;
|
||||
yesOrNo: {
|
||||
yes: string;
|
||||
@ -486,6 +487,8 @@ declare namespace App {
|
||||
};
|
||||
login: {
|
||||
common: {
|
||||
title: string;
|
||||
subTitle: string;
|
||||
loginOrRegister: string;
|
||||
register: string;
|
||||
userNamePlaceholder: string;
|
||||
|
2
src/typings/common.d.ts
vendored
2
src/typings/common.d.ts
vendored
@ -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';
|
||||
}
|
||||
|
20
src/typings/components.d.ts
vendored
20
src/typings/components.d.ts
vendored
@ -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']
|
||||
}
|
||||
}
|
||||
|
25
src/typings/elegant-router.d.ts
vendored
25
src/typings/elegant-router.d.ts
vendored
@ -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"
|
||||
>;
|
||||
|
||||
/**
|
||||
|
1
src/typings/vite-env.d.ts
vendored
1
src/typings/vite-env.d.ts
vendored
@ -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;
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { defineAsyncComponent, markRaw } from 'vue';
|
||||
import { AcceptType } from '@/enum/business';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
/**
|
||||
* Transform record to option
|
||||
*
|
||||
@ -76,14 +78,38 @@ 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 !== '' && value !== 'undefined' && value !== 'null';
|
||||
return value !== undefined && value !== null && value !== '';
|
||||
}
|
||||
|
||||
/** 判断是否为空 */
|
||||
export function isNull(value: any) {
|
||||
return value === undefined || value === null || value === '' || value === 'undefined' || value === 'null';
|
||||
return value === undefined || value === null || value === '';
|
||||
}
|
||||
|
||||
/** 判断是否为图片类型 */
|
||||
@ -191,3 +217,13 @@ export function transformToURLSearchParams(obj: Record<string, any>, excludeKeys
|
||||
});
|
||||
return searchParams;
|
||||
}
|
||||
|
||||
/** 判断两个数组是否相等 */
|
||||
export function arraysEqualSet(arr1: Array<any>, arr2: Array<any>) {
|
||||
return (
|
||||
arr1.length === arr2.length &&
|
||||
new Set(arr1).size === arr1.length &&
|
||||
new Set(arr2).size === arr2.length &&
|
||||
[...arr1].sort().join() === [...arr2].sort().join()
|
||||
);
|
||||
}
|
||||
|
71
src/utils/export.ts
Normal file
71
src/utils/export.ts
Normal 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);
|
||||
}
|
@ -39,45 +39,51 @@ 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>
|
||||
<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>
|
||||
<div class="flex-y-center">
|
||||
<ThemeSchemaSwitch
|
||||
:theme-schema="themeStore.themeScheme"
|
||||
:show-tooltip="false"
|
||||
class="text-20px lt-sm:text-18px"
|
||||
@switch="themeStore.toggleThemeScheme"
|
||||
/>
|
||||
<LangSwitch
|
||||
v-if="themeStore.header.multilingual.visible"
|
||||
:lang="appStore.locale"
|
||||
:lang-options="appStore.localeOptions"
|
||||
:show-tooltip="false"
|
||||
class="text-20px lt-sm:text-18px"
|
||||
@change-lang="appStore.changeLocale"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
<main class="pt-24px">
|
||||
<div>
|
||||
<Transition :name="themeStore.page.animateMode" mode="out-in" appear>
|
||||
<component :is="activeModule.component" />
|
||||
</Transition>
|
||||
</div>
|
||||
</main>
|
||||
<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="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>
|
||||
<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"
|
||||
class="text-20px lt-sm:text-18px"
|
||||
@switch="themeStore.toggleThemeScheme"
|
||||
/>
|
||||
<LangSwitch
|
||||
v-if="themeStore.header.multilingual.visible"
|
||||
:lang="appStore.locale"
|
||||
:lang-options="appStore.localeOptions"
|
||||
:show-tooltip="false"
|
||||
class="text-20px lt-sm:text-18px"
|
||||
@change-lang="appStore.changeLocale"
|
||||
/>
|
||||
</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>
|
||||
</main>
|
||||
</header>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -53,12 +53,14 @@ async function handleFetchTenantList() {
|
||||
const { data, error } = await fetchTenantList();
|
||||
if (error) return;
|
||||
tenantEnabled.value = data.tenantEnabled;
|
||||
tenantOption.value = data.voList.map(tenant => {
|
||||
return {
|
||||
label: tenant.companyName,
|
||||
value: tenant.tenantId
|
||||
};
|
||||
});
|
||||
if (data.tenantEnabled) {
|
||||
tenantOption.value = data.voList.map(tenant => {
|
||||
return {
|
||||
label: tenant.companyName,
|
||||
value: tenant.tenantId
|
||||
};
|
||||
});
|
||||
}
|
||||
endTenantLoading();
|
||||
}
|
||||
|
||||
@ -122,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"
|
||||
@ -154,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') }}
|
||||
@ -197,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') }}
|
||||
@ -214,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;
|
||||
}
|
||||
@ -235,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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -200,7 +200,7 @@ async function handleExport() {
|
||||
v-model:visible="drawerVisible"
|
||||
:operate-type="operateType"
|
||||
:row-data="editingData"
|
||||
@submitted="getDataByPage"
|
||||
@submitted="getData"
|
||||
/>
|
||||
</NCard>
|
||||
</div>
|
||||
|
@ -34,13 +34,13 @@ async function search() {
|
||||
<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="IP地址" path="ipaddr" class="pr-24px">
|
||||
<NFormItemGi span="24 s:12 m:8" label="IP地址" path="ipaddr" class="pr-24px">
|
||||
<NInput v-model:value="model.ipaddr" placeholder="请输入IP地址" />
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="24 s:12 m:6" label="用户账号" path="userName" class="pr-24px">
|
||||
<NFormItemGi span="24 s:12 m:8" label="用户账号" path="userName" class="pr-24px">
|
||||
<NInput v-model:value="model.userName" placeholder="请输入用户账号" />
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="24" class="pr-24px">
|
||||
<NFormItemGi span="24 s:24 m:8" class="pr-24px">
|
||||
<NSpace class="w-full" justify="end">
|
||||
<NButton @click="reset">
|
||||
<template #icon>
|
||||
|
@ -239,7 +239,7 @@ async function handleExport() {
|
||||
v-model:visible="drawerVisible"
|
||||
:operate-type="operateType"
|
||||
:row-data="editingData"
|
||||
@submitted="getDataByPage"
|
||||
@submitted="getData"
|
||||
/>
|
||||
</NCard>
|
||||
</div>
|
||||
|
@ -61,7 +61,7 @@ async function search() {
|
||||
clearable
|
||||
/>
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="24" class="pr-24px">
|
||||
<NFormItemGi span="24 s:12 m:6" class="pr-24px">
|
||||
<NSpace class="w-full" justify="end">
|
||||
<NButton @click="reset">
|
||||
<template #icon>
|
||||
|
@ -234,7 +234,7 @@ async function handleRefreshCache() {
|
||||
v-model:visible="drawerVisible"
|
||||
:operate-type="operateType"
|
||||
:row-data="editingData"
|
||||
@submitted="getDataByPage"
|
||||
@submitted="getData"
|
||||
/>
|
||||
</NCard>
|
||||
</div>
|
||||
|
@ -21,9 +21,12 @@ const dateRangeCreateTime = ref<[string, string] | null>(null);
|
||||
const model = defineModel<Api.System.ConfigSearchParams>('model', { required: true });
|
||||
|
||||
function onDateRangeCreateTimeUpdate(value: [string, string] | null) {
|
||||
if (value?.length) {
|
||||
model.value.params!.beginTime = value[0];
|
||||
model.value.params!.endTime = value[1];
|
||||
const params = model.value.params!;
|
||||
if (value && value.length === 2) {
|
||||
[params.beginTime, params.endTime] = value;
|
||||
} else {
|
||||
params.beginTime = undefined;
|
||||
params.endTime = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@ -77,7 +80,7 @@ async function search() {
|
||||
clearable
|
||||
/>
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="24 s:12 m:12" label="创建时间" path="createTime" class="pr-24px">
|
||||
<NFormItemGi span="24 s:12 m:6" label="创建时间" path="createTime" class="pr-24px">
|
||||
<NDatePicker
|
||||
v-model:formatted-value="dateRangeCreateTime"
|
||||
type="datetimerange"
|
||||
@ -86,7 +89,7 @@ async function search() {
|
||||
@update:formatted-value="onDateRangeCreateTimeUpdate"
|
||||
/>
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="24 s:12 m:12" class="pr-24px">
|
||||
<NFormItemGi span="24" class="pr-24px">
|
||||
<NSpace class="w-full" justify="end">
|
||||
<NButton @click="reset">
|
||||
<template #icon>
|
||||
|
@ -36,10 +36,10 @@ async function search() {
|
||||
<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="8" :label="$t('page.system.dept.deptName')" path="deptName" class="pr-24px">
|
||||
<NFormItemGi span="24 s:12 m:8" :label="$t('page.system.dept.deptName')" path="deptName" class="pr-24px">
|
||||
<NInput v-model:value="model.deptName" :placeholder="$t('page.system.dept.form.deptName.required')" />
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="8 " :label="$t('page.system.dept.status')" path="status" class="pr-24px">
|
||||
<NFormItemGi span="24 s:12 m:8 " :label="$t('page.system.dept.status')" path="status" class="pr-24px">
|
||||
<NSelect
|
||||
v-model:value="model.status"
|
||||
:placeholder="$t('page.system.dept.form.status.required')"
|
||||
@ -47,7 +47,7 @@ async function search() {
|
||||
clearable
|
||||
/>
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="8" class="pr-24px">
|
||||
<NFormItemGi span="24 s:12 m:8" class="pr-24px">
|
||||
<NSpace class="w-full" justify="end">
|
||||
<NButton @click="reset">
|
||||
<template #icon>
|
||||
|
@ -405,7 +405,7 @@ const selectable = computed(() => {
|
||||
:operate-type="operateType"
|
||||
:row-data="editingData"
|
||||
:dict-type="searchParams.dictType || ''"
|
||||
@submitted="getDataByPage"
|
||||
@submitted="getData"
|
||||
/>
|
||||
<DictTypeOperateDrawer
|
||||
v-model:visible="dictTypeDrawerVisible"
|
||||
|
@ -33,17 +33,16 @@ async function search() {
|
||||
<NCollapse>
|
||||
<NCollapseItem :title="$t('common.search')" name="user-search">
|
||||
<NForm ref="formRef" :model="model" label-placement="left" :label-width="80">
|
||||
<NGrid responsive="self" item-responsive>
|
||||
<NGrid responsive="screen" item-responsive>
|
||||
<NFormItemGi
|
||||
:show-feedback="false"
|
||||
span="12"
|
||||
span="24 s:12 m:12"
|
||||
:label="$t('page.system.dict.data.label')"
|
||||
path="dictLabel"
|
||||
class="pr-24px"
|
||||
>
|
||||
<NInput v-model:value="model.dictLabel" :placeholder="$t('page.system.dict.form.dictLabel.required')" />
|
||||
</NFormItemGi>
|
||||
<NFormItemGi :show-feedback="false" span="12" class="pr-24px">
|
||||
<NFormItemGi span="24 s:12 m:12" class="pr-24px">
|
||||
<NSpace class="w-full" justify="end">
|
||||
<NButton @click="reset">
|
||||
<template #icon>
|
||||
|
@ -113,6 +113,24 @@ function renderLabel({ option }: { option: TreeOption }) {
|
||||
if (label?.startsWith('route.') || label?.startsWith('menu.')) {
|
||||
label = $t(label as App.I18n.I18nKey);
|
||||
}
|
||||
// 禁用的菜单显示红色
|
||||
if (option.status === '1') {
|
||||
return (
|
||||
<div class="flex items-center gap-4px text-error-200">
|
||||
{label}
|
||||
<SvgIcon icon="ri:prohibited-line" class="text-16px" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// 隐藏的菜单显示灰色
|
||||
if (option.visible === '1') {
|
||||
return (
|
||||
<div class="flex items-center gap-4px text-gray-400">
|
||||
{label}
|
||||
<SvgIcon icon="codex:hidden" class="text-21px" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <div>{label}</div>;
|
||||
}
|
||||
|
||||
|
@ -194,7 +194,7 @@ async function edit(noticeId: CommonType.IdType) {
|
||||
v-model:visible="drawerVisible"
|
||||
:operate-type="operateType"
|
||||
:row-data="editingData"
|
||||
@submitted="getDataByPage"
|
||||
@submitted="getData"
|
||||
/>
|
||||
</NCard>
|
||||
</div>
|
||||
|
@ -33,13 +33,13 @@ async function search() {
|
||||
<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="8" label="公告标题" path="noticeTitle" class="pr-24px">
|
||||
<NFormItemGi span="24 s:12 m:8" label="公告标题" path="noticeTitle" class="pr-24px">
|
||||
<NInput v-model:value="model.noticeTitle" placeholder="请输入公告标题" />
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="8" label="公告类型" path="noticeType" class="pr-24px">
|
||||
<NFormItemGi span="24 s:12 m:8" label="公告类型" path="noticeType" class="pr-24px">
|
||||
<DictSelect v-model:value="model.noticeType" dict-code="sys_notice_type" placeholder="请选择公告类型" />
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="8" class="pr-24px">
|
||||
<NFormItemGi span="24 s:12 m:8" class="pr-24px">
|
||||
<NSpace class="w-full" justify="end">
|
||||
<NButton @click="reset">
|
||||
<template #icon>
|
||||
|
@ -252,7 +252,7 @@ async function handleStatusChange(
|
||||
v-model:visible="drawerVisible"
|
||||
:operate-type="operateType"
|
||||
:row-data="editingData"
|
||||
@submitted="getDataByPage"
|
||||
@submitted="getData"
|
||||
/>
|
||||
</NCard>
|
||||
</div>
|
||||
|
@ -49,6 +49,8 @@ const {
|
||||
originalName: null,
|
||||
fileSuffix: null,
|
||||
service: null,
|
||||
isAsc: 'desc',
|
||||
orderByColumn: 'createTime',
|
||||
params: {}
|
||||
},
|
||||
columns: () => [
|
||||
@ -333,7 +335,7 @@ function handleToOssConfig() {
|
||||
:pagination="mobilePagination"
|
||||
class="sm:h-full"
|
||||
/>
|
||||
<OssUploadModal v-model:visible="uploadVisible" :upload-type="fileUploadType" @close="getData" />
|
||||
<OssUploadModal v-model:visible="uploadVisible" :upload-type="fileUploadType" @close="getDataByPage" />
|
||||
</NCard>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -21,9 +21,12 @@ const dateRangeCreateTime = ref<[string, string] | null>(null);
|
||||
const model = defineModel<Api.System.OssSearchParams>('model', { required: true });
|
||||
|
||||
function onDateRangeCreateTimeUpdate(value: [string, string] | null) {
|
||||
if (value?.length) {
|
||||
model.value.params!.beginCreateTime = value[0];
|
||||
model.value.params!.endCreateTime = value[1];
|
||||
const params = model.value.params!;
|
||||
if (value && value.length === 2) {
|
||||
[params.beginTime, params.endTime] = value;
|
||||
} else {
|
||||
params.beginTime = undefined;
|
||||
params.endTime = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -28,7 +28,9 @@ const accept = computed(() => (props.uploadType === 'file' ? AcceptType.File : A
|
||||
|
||||
const fileList = ref<UploadFileInfo[]>([]);
|
||||
|
||||
function handleUpdateModelWhenUpload() {}
|
||||
function handleUpdateModelWhenUpload() {
|
||||
fileList.value = [];
|
||||
}
|
||||
|
||||
function closeDrawer() {
|
||||
visible.value = false;
|
||||
|
@ -288,7 +288,7 @@ function handleResetSearch() {
|
||||
:operate-type="operateType"
|
||||
:row-data="editingData"
|
||||
:dept-data="deptData"
|
||||
@submitted="getDataByPage"
|
||||
@submitted="getData"
|
||||
/>
|
||||
</NCard>
|
||||
</div>
|
||||
|
@ -36,13 +36,13 @@ async function search() {
|
||||
<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="6 s:12 m:6" label="岗位编码" path="postCode" class="pr-24px">
|
||||
<NFormItemGi span="24 s:12 m:6" label="岗位编码" path="postCode" class="pr-24px">
|
||||
<NInput v-model:value="model.postCode" placeholder="请输入岗位编码" />
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="6 s:12 m:6" label="岗位名称" path="postName" class="pr-24px">
|
||||
<NFormItemGi span="24 s:12 m:6" label="岗位名称" path="postName" class="pr-24px">
|
||||
<NInput v-model:value="model.postName" placeholder="请输入岗位名称" />
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="6 s:12 m:6" label="状态" path="status" class="pr-24px">
|
||||
<NFormItemGi span="24 s:12 m:6" label="状态" path="status" class="pr-24px">
|
||||
<NSelect
|
||||
v-model:value="model.status"
|
||||
placeholder="请选择状态"
|
||||
@ -50,7 +50,7 @@ async function search() {
|
||||
clearable
|
||||
/>
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="6" class="pr-24px">
|
||||
<NFormItemGi span="24 s:12 m:6" class="pr-24px">
|
||||
<NSpace class="w-full" justify="end">
|
||||
<NButton @click="reset">
|
||||
<template #icon>
|
||||
|
@ -273,7 +273,7 @@ function handleAuthUser(row: Api.System.Role) {
|
||||
:data="data"
|
||||
size="small"
|
||||
:flex-height="!appStore.isMobile"
|
||||
:scroll-x="962"
|
||||
:scroll-x="1200"
|
||||
:loading="loading"
|
||||
remote
|
||||
:row-key="row => row.roleId"
|
||||
@ -284,14 +284,10 @@ function handleAuthUser(row: Api.System.Role) {
|
||||
v-model:visible="drawerVisible"
|
||||
:operate-type="operateType"
|
||||
:row-data="editingData"
|
||||
@submitted="getDataByPage"
|
||||
@submitted="getData"
|
||||
/>
|
||||
<RoleDataScopeDrawer
|
||||
v-model:visible="dataScopeDrawerVisible"
|
||||
:row-data="editingData"
|
||||
@submitted="getDataByPage"
|
||||
/>
|
||||
<RoleAuthUserDrawer v-model:visible="authUserDrawerVisible" :row-data="editingData" @submitted="getDataByPage" />
|
||||
<RoleDataScopeDrawer v-model:visible="dataScopeDrawerVisible" :row-data="editingData" @submitted="getData" />
|
||||
<RoleAuthUserDrawer v-model:visible="authUserDrawerVisible" :row-data="editingData" @submitted="getData" />
|
||||
</NCard>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -1,9 +1,16 @@
|
||||
<script setup lang="tsx">
|
||||
import { computed, watch } from 'vue';
|
||||
import { fetchGetRoleUserList, fetchGetUserList } from '@/service/api/system';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { NDatePicker } from 'naive-ui';
|
||||
import {
|
||||
fetchGetRoleUserList,
|
||||
fetchGetUserList,
|
||||
fetchUpdateRoleAuthUser,
|
||||
fetchUpdateRoleAuthUserCancel
|
||||
} from '@/service/api/system';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { useDict } from '@/hooks/business/dict';
|
||||
import { useTable, useTableOperate } from '@/hooks/common/table';
|
||||
import { arraysEqualSet } from '@/utils/common';
|
||||
import { $t } from '@/locales';
|
||||
import DictTag from '@/components/custom/dict-tag.vue';
|
||||
|
||||
@ -109,13 +116,16 @@ const { columns, data, getData, getDataByPage, loading, mobilePagination, search
|
||||
|
||||
const { checkedRowKeys } = useTableOperate(data, getData);
|
||||
|
||||
const checkedUserIds = ref<CommonType.IdType[]>([]);
|
||||
|
||||
async function handleUpdateModelWhenEdit() {
|
||||
checkedRowKeys.value = [];
|
||||
getDataByPage();
|
||||
const { data: roleUserList } = await fetchGetRoleUserList({
|
||||
roleId: props.rowData?.roleId
|
||||
});
|
||||
checkedRowKeys.value = roleUserList?.rows.map(item => item.userId) || [];
|
||||
checkedUserIds.value = roleUserList?.rows.map(item => item.userId) || [];
|
||||
checkedRowKeys.value = checkedUserIds.value;
|
||||
}
|
||||
|
||||
function closeDrawer() {
|
||||
@ -123,6 +133,25 @@ function closeDrawer() {
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (arraysEqualSet(checkedUserIds.value, checkedRowKeys.value)) {
|
||||
window.$message?.warning($t('common.noChange'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 批量取消用户授权
|
||||
const cancelUserIds = checkedUserIds.value.filter(item => !checkedRowKeys.value.includes(item));
|
||||
if (cancelUserIds.length > 0) {
|
||||
const { error: cancelError } = await fetchUpdateRoleAuthUserCancel(props.rowData!.roleId, cancelUserIds);
|
||||
if (cancelError) return;
|
||||
}
|
||||
|
||||
// 批量选择用户授权
|
||||
const addUserIds = checkedRowKeys.value.filter(item => !checkedUserIds.value.includes(item));
|
||||
if (addUserIds.length > 0) {
|
||||
const { error: addError } = await fetchUpdateRoleAuthUser(props.rowData!.roleId, addUserIds);
|
||||
if (addError) return;
|
||||
}
|
||||
|
||||
window.$message?.success($t('common.updateSuccess'));
|
||||
closeDrawer();
|
||||
emit('submitted');
|
||||
@ -133,6 +162,25 @@ watch(visible, () => {
|
||||
handleUpdateModelWhenEdit();
|
||||
}
|
||||
});
|
||||
|
||||
const dateRangeCreateTime = ref<[string, string] | null>(null);
|
||||
|
||||
const datePickerRef = ref<InstanceType<typeof NDatePicker>>();
|
||||
|
||||
function onDateRangeCreateTimeUpdate(value: [string, string] | null) {
|
||||
const params = searchParams.params!;
|
||||
if (value && value.length === 2) {
|
||||
[params.beginTime, params.endTime] = value;
|
||||
} else {
|
||||
params.beginTime = undefined;
|
||||
params.endTime = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
dateRangeCreateTime.value = null;
|
||||
resetSearchParams();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -158,9 +206,22 @@ watch(visible, () => {
|
||||
<NFormItemGi span="24 s:12 m:8" label="手机号码" path="phonenumber" class="pr-24px">
|
||||
<NInput v-model:value="searchParams.phonenumber" placeholder="请输入手机号码" />
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="24 s:12 m:24" class="pr-24px" :show-feedback="false">
|
||||
<NFormItemGi span="24 s:12 m:8" label="所属部门" path="deptId" class="pr-24px">
|
||||
<DeptTreeSelect v-model:value="searchParams.deptId" placeholder="请选择部门" />
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="24 s:12 m:10" label="创建时间" path="createTime" class="pr-24px">
|
||||
<NDatePicker
|
||||
ref="datePickerRef"
|
||||
v-model:formatted-value="dateRangeCreateTime"
|
||||
type="datetimerange"
|
||||
value-format="yyyy-MM-dd HH:mm:ss"
|
||||
clearable
|
||||
@update:formatted-value="onDateRangeCreateTimeUpdate"
|
||||
/>
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="24 s:12 m:6" class="pr-24px" :show-feedback="false">
|
||||
<NSpace class="w-full" justify="end">
|
||||
<NButton @click="resetSearchParams">
|
||||
<NButton @click="reset">
|
||||
<template #icon>
|
||||
<icon-ic-round-refresh class="text-icon" />
|
||||
</template>
|
||||
|
@ -24,9 +24,12 @@ const model = defineModel<Api.System.RoleSearchParams>('model', { required: true
|
||||
const { options: sysNormalDisableOptions } = useDict('sys_normal_disable', false);
|
||||
|
||||
function onDateRangeCreateTimeUpdate(value: [string, string] | null) {
|
||||
if (value?.length) {
|
||||
model.value.params!.beginTime = `${value[0]} 00:00:00`;
|
||||
model.value.params!.endTime = `${value[1]} 23:59:59`;
|
||||
const params = model.value.params!;
|
||||
if (value && value.length === 2) {
|
||||
[params.beginTime, params.endTime] = value;
|
||||
} else {
|
||||
params.beginTime = undefined;
|
||||
params.endTime = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -233,7 +233,7 @@ async function handleStatusChange(
|
||||
v-model:visible="drawerVisible"
|
||||
:operate-type="operateType"
|
||||
:row-data="editingData"
|
||||
@submitted="getDataByPage"
|
||||
@submitted="getData"
|
||||
/>
|
||||
</NCard>
|
||||
</div>
|
||||
|
@ -72,8 +72,11 @@ async function handleUpdateModelWhenEdit() {
|
||||
model.menuIds = [];
|
||||
|
||||
if (props.operateType === 'add') {
|
||||
menuTreeRef.value?.refresh();
|
||||
Object.assign(model, createDefaultModel());
|
||||
const { data, error } = await fetchGetTenantPackageMenuTreeSelect(0);
|
||||
if (error) return;
|
||||
model.menuIds = data.checkedKeys;
|
||||
menuOptions.value = data.menus;
|
||||
return;
|
||||
}
|
||||
|
||||
@ -145,7 +148,7 @@ watch(visible, () => {
|
||||
v-model:options="menuOptions"
|
||||
v-model:cascade="model.menuCheckStrictly"
|
||||
v-model:loading="menuLoading"
|
||||
:immediate="operateType === 'add'"
|
||||
:immediate="false"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem :label="$t('page.system.tenantPackage.remark')" path="remark">
|
||||
|
@ -38,7 +38,7 @@ async function search() {
|
||||
<NForm ref="formRef" :model="model" label-placement="left" :label-width="80">
|
||||
<NGrid responsive="screen" item-responsive>
|
||||
<NFormItemGi
|
||||
span="8"
|
||||
span="24 s:12 m:8"
|
||||
:label="$t('page.system.tenantPackage.packageName')"
|
||||
path="packageName"
|
||||
class="pr-24px"
|
||||
@ -48,7 +48,12 @@ async function search() {
|
||||
:placeholder="$t('page.system.tenantPackage.form.packageName.required')"
|
||||
/>
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="8" :label="$t('page.system.tenantPackage.status')" path="status" class="pr-24px">
|
||||
<NFormItemGi
|
||||
span="24 s:12 m:8"
|
||||
:label="$t('page.system.tenantPackage.status')"
|
||||
path="status"
|
||||
class="pr-24px"
|
||||
>
|
||||
<NSelect
|
||||
v-model:value="model.status"
|
||||
:placeholder="$t('page.system.tenantPackage.form.status.required')"
|
||||
@ -56,7 +61,7 @@ async function search() {
|
||||
clearable
|
||||
/>
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="8" class="pr-24px">
|
||||
<NFormItemGi span="24 s:24 m:8" class="pr-24px">
|
||||
<NSpace class="w-full" justify="end">
|
||||
<NButton @click="reset">
|
||||
<template #icon>
|
||||
|
@ -261,7 +261,7 @@ async function handleExport() {
|
||||
v-model:visible="drawerVisible"
|
||||
:operate-type="operateType"
|
||||
:row-data="editingData"
|
||||
@submitted="getDataByPage"
|
||||
@submitted="getData"
|
||||
/>
|
||||
</NCard>
|
||||
</div>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user