mirror of
https://github.com/m-xlsea/ruoyi-plus-soybean.git
synced 2025-09-24 07:49:47 +08:00
Compare commits
68 Commits
v1.1.0
...
4a4244b5c4
Author | SHA1 | Date | |
---|---|---|---|
4a4244b5c4 | |||
ecad1c3e78 | |||
9ef0bd416e | |||
25ee32074a | |||
8412a8db16 | |||
e230b0da81 | |||
12b25e0d58 | |||
e33f944a74 | |||
7f2f3bd088 | |||
5ef1c5de98 | |||
90a14e338a | |||
4e625111ce | |||
8524ae7666 | |||
d6ae85d218 | |||
89719abe34 | |||
8d7f91dccf | |||
33ade53904 | |||
ab9c84d831 | |||
7651816495 | |||
4539fe01fb | |||
4e9839bd48 | |||
a15b683b1d | |||
9df8d2f55f | |||
710374398a | |||
52318c106d | |||
9027632bef | |||
b96c46baa9 | |||
2d31d7dc62 | |||
e538355f2b | |||
f89835578c | |||
4eb77eac78 | |||
adca2e26be | |||
ff576f3f42 | |||
8fcc70d73d | |||
48f603ed32 | |||
f4038a2dc0 | |||
923eb98a5c | |||
a1336d1536 | |||
cc29ea85c1 | |||
2edd4bc9f1 | |||
e485f680c7 | |||
2f8797eb98 | |||
2a3f3a4812 | |||
2587f8cbfa | |||
2036391c41 | |||
e89b86ce56 | |||
4e8c8715ae | |||
e5ec915fd9 | |||
89c716e12a | |||
2d02128164 | |||
9ca7ca8fda | |||
d85424ee83 | |||
ff87415d7b | |||
312709706b | |||
3ae9922dc4 | |||
247b98a542 | |||
566b2c2db8 | |||
90d32ee29a | |||
efc953c094 | |||
ad48d8e840 | |||
62f2c6d571 | |||
03dd64c543 | |||
aeb6369005 | |||
133196f337 | |||
41191d54fb | |||
229e00443f | |||
85c8a9fffa | |||
b99999355c |
@ -16,7 +16,7 @@
|
|||||||
},
|
},
|
||||||
"playwright": {
|
"playwright": {
|
||||||
"command": "npx",
|
"command": "npx",
|
||||||
"args": ["@playwright/mcp@latest"]
|
"args": ["@playwright/mcp@0.0.29"]
|
||||||
},
|
},
|
||||||
"mcp-server-time": {
|
"mcp-server-time": {
|
||||||
"command": "uvx",
|
"command": "uvx",
|
||||||
@ -26,7 +26,7 @@
|
|||||||
"command": "npx",
|
"command": "npx",
|
||||||
"args": ["-y", "mcp-shrimp-task-manager"],
|
"args": ["-y", "mcp-shrimp-task-manager"],
|
||||||
"env": {
|
"env": {
|
||||||
"DATA_DIR": "D:/workspace/Aother/mcp-shrimp-task-manager/data",
|
"DATA_DIR": "D:/workspace/mcp-shrimp-task-manager/data",
|
||||||
"TEMPLATES_USE": "en",
|
"TEMPLATES_USE": "en",
|
||||||
"ENABLE_GUI": "false"
|
"ENABLE_GUI": "false"
|
||||||
}
|
}
|
||||||
|
2
.env.dev
2
.env.dev
@ -15,6 +15,8 @@ VITE_APP_CLIENT_ID=e5cd7e4891bf95d1d19206ce24a7b32e
|
|||||||
|
|
||||||
# 接口加密功能开关(如需关闭 后端也必须对应关闭)
|
# 接口加密功能开关(如需关闭 后端也必须对应关闭)
|
||||||
VITE_APP_ENCRYPT=Y
|
VITE_APP_ENCRYPT=Y
|
||||||
|
# AES 加密头标识
|
||||||
|
VITE_HEADER_FLAG=encrypt-key
|
||||||
# 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
|
# 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
|
||||||
VITE_APP_RSA_PUBLIC_KEY='MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
|
VITE_APP_RSA_PUBLIC_KEY='MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
|
||||||
# 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换
|
# 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换
|
||||||
|
@ -12,6 +12,8 @@ VITE_APP_CLIENT_ID=e5cd7e4891bf95d1d19206ce24a7b32e
|
|||||||
|
|
||||||
# 接口加密功能开关(如需关闭 后端也必须对应关闭)
|
# 接口加密功能开关(如需关闭 后端也必须对应关闭)
|
||||||
VITE_APP_ENCRYPT=Y
|
VITE_APP_ENCRYPT=Y
|
||||||
|
# AES 加密头标识
|
||||||
|
VITE_HEADER_FLAG=encrypt-key
|
||||||
# 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
|
# 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
|
||||||
VITE_APP_RSA_PUBLIC_KEY='MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
|
VITE_APP_RSA_PUBLIC_KEY='MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
|
||||||
# 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换
|
# 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换
|
||||||
|
@ -12,6 +12,8 @@ VITE_APP_CLIENT_ID=e5cd7e4891bf95d1d19206ce24a7b32e
|
|||||||
|
|
||||||
# 接口加密功能开关(如需关闭 后端也必须对应关闭)
|
# 接口加密功能开关(如需关闭 后端也必须对应关闭)
|
||||||
VITE_APP_ENCRYPT=Y
|
VITE_APP_ENCRYPT=Y
|
||||||
|
# AES 加密头标识
|
||||||
|
VITE_HEADER_FLAG=encrypt-key
|
||||||
# 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
|
# 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
|
||||||
VITE_APP_RSA_PUBLIC_KEY='MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
|
VITE_APP_RSA_PUBLIC_KEY='MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
|
||||||
# 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换
|
# 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换
|
||||||
|
16
.vscode/settings.json
vendored
16
.vscode/settings.json
vendored
@ -4,13 +4,27 @@
|
|||||||
"source.organizeImports": "never"
|
"source.organizeImports": "never"
|
||||||
},
|
},
|
||||||
"editor.formatOnSave": false,
|
"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.displayLanguage": "zh-cn",
|
||||||
"i18n-ally.enabledParsers": ["ts"],
|
"i18n-ally.enabledParsers": ["ts"],
|
||||||
"i18n-ally.enabledFrameworks": ["vue"],
|
"i18n-ally.enabledFrameworks": ["vue"],
|
||||||
"i18n-ally.editor.preferEditor": true,
|
"i18n-ally.editor.preferEditor": true,
|
||||||
"i18n-ally.keystyle": "nested",
|
"i18n-ally.keystyle": "nested",
|
||||||
"i18n-ally.localesPaths": ["src/locales/langs"],
|
"i18n-ally.localesPaths": ["src/locales/langs"],
|
||||||
|
"i18n-ally.parsers.typescript.compilerOptions": {
|
||||||
|
"moduleResolution": "node"
|
||||||
|
},
|
||||||
"prettier.enable": false,
|
"prettier.enable": false,
|
||||||
"typescript.tsdk": "node_modules/typescript/lib",
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
"unocss.root": ["./"],
|
"unocss.root": ["./"],
|
||||||
|
129
CHANGELOG.md
129
CHANGELOG.md
@ -1,5 +1,132 @@
|
|||||||
# 更新日志
|
# 更新日志
|
||||||
|
|
||||||
|
## [v1.1.3](https://gitee.com/xlsea/ruoyi-plus-soybean/compare/v1.1.2...v1.1.3) (2025-08-16)
|
||||||
|
|
||||||
|
### 🐞 Bug 修复
|
||||||
|
|
||||||
|
- **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)
|
||||||
|
- **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>(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)
|
||||||
|
|
||||||
|
### 💅 重构
|
||||||
|
|
||||||
|
- **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 @m-xlsea [<samp>(e5383)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/e538355f)
|
||||||
|
|
||||||
|
### 🏡 杂项
|
||||||
|
|
||||||
|
- **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)
|
## [v1.1.0](https://gitee.com/xlsea/ruoyi-plus-soybean/compare/v1.0.0...v1.1.0) (2025-07-01)
|
||||||
|
|
||||||
### 🚀 新功能
|
### 🚀 新功能
|
||||||
@ -90,7 +217,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)
|
[](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>
|
<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 的现代化前端特性,为开发者提供了完整的企业管理解决方案。
|
RuoYi-Plus-Soybean 是一个现代化的企业级多租户管理系统,它结合了 RuoYi-Vue-Plus 的强大后端功能和 Soybean Admin 的现代化前端特性,为开发者提供了完整的企业管理解决方案。
|
||||||
@ -119,8 +123,8 @@ root
|
|||||||
## 🚀 环境要求与安装
|
## 🚀 环境要求与安装
|
||||||
|
|
||||||
### 环境要求
|
### 环境要求
|
||||||
- Node.js >= 18.20.0
|
- Node.js >= 20.19.0
|
||||||
- pnpm >= 8.7.0
|
- pnpm >= 10.5.0
|
||||||
- Git
|
- Git
|
||||||
|
|
||||||
### 安装步骤及说明
|
### 安装步骤及说明
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { HttpProxy, ProxyOptions } from 'vite';
|
import type { ProxyOptions } from 'vite';
|
||||||
import { bgRed, bgYellow, green, lightBlue } from 'kolorist';
|
import { bgRed, bgYellow, green, lightBlue } from 'kolorist';
|
||||||
import { consola } from 'consola';
|
import { consola } from 'consola';
|
||||||
import { createServiceConfig } from '../../src/utils/service';
|
import { createServiceConfig } from '../../src/utils/service';
|
||||||
@ -34,7 +34,7 @@ function createProxyItem(item: App.Service.ServiceConfigItem, enableLog: boolean
|
|||||||
target: item.baseURL,
|
target: item.baseURL,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
ws: item.ws,
|
ws: item.ws,
|
||||||
configure: (_proxy: HttpProxy.Server, options: ProxyOptions) => {
|
configure: (_proxy, options) => {
|
||||||
_proxy.on('proxyReq', (_proxyReq, req, _res) => {
|
_proxy.on('proxyReq', (_proxyReq, req, _res) => {
|
||||||
if (!enableLog) return;
|
if (!enableLog) return;
|
||||||
|
|
||||||
|
7
docs/template/typings/api.d.ts.vm
vendored
7
docs/template/typings/api.d.ts.vm
vendored
@ -1,5 +1,11 @@
|
|||||||
#set($BaseEntity = ['createDept', 'createBy', 'createTime', 'updateBy', 'updateTime'])
|
#set($BaseEntity = ['createDept', 'createBy', 'createTime', 'updateBy', 'updateTime'])
|
||||||
#set($ModuleName = $moduleName.substring(0, 1).toUpperCase() + $moduleName.substring(1))
|
#set($ModuleName = $moduleName.substring(0, 1).toUpperCase() + $moduleName.substring(1))
|
||||||
|
/**
|
||||||
|
* Namespace Api
|
||||||
|
*
|
||||||
|
* All backend api type
|
||||||
|
*/
|
||||||
|
declare namespace Api {
|
||||||
/**
|
/**
|
||||||
* namespace ${ModuleName}
|
* namespace ${ModuleName}
|
||||||
*
|
*
|
||||||
@ -42,3 +48,4 @@ namespace ${ModuleName} {
|
|||||||
/** ${businessname} list */
|
/** ${businessname} list */
|
||||||
type ${BusinessName}List = Api.Common.PaginatingQueryRecord<${BusinessName}>;
|
type ${BusinessName}List = Api.Common.PaginatingQueryRecord<${BusinessName}>;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
65
package.json
65
package.json
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "ruoyi-vue-plus",
|
"name": "ruoyi-vue-plus",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "1.1.0",
|
"version": "1.1.3",
|
||||||
"description": "结合了 RuoYi-Vue-Plus 的强大后端功能和 Soybean Admin 的现代化前端特性,为开发者提供了完整的企业管理解决方案。",
|
"description": "结合了 RuoYi-Vue-Plus 的强大后端功能和 Soybean Admin 的现代化前端特性,为开发者提供了完整的企业管理解决方案。",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "xlsea",
|
"name": "xlsea",
|
||||||
@ -21,7 +21,7 @@
|
|||||||
"Soybean Admin",
|
"Soybean Admin",
|
||||||
"Vue3 admin ",
|
"Vue3 admin ",
|
||||||
"vue-admin-template",
|
"vue-admin-template",
|
||||||
"Vite6",
|
"Vite7",
|
||||||
"TypeScript",
|
"TypeScript",
|
||||||
"naive-ui",
|
"naive-ui",
|
||||||
"naive-ui-admin",
|
"naive-ui-admin",
|
||||||
@ -66,11 +66,11 @@
|
|||||||
"@sa/tinymce": "workspace:*",
|
"@sa/tinymce": "workspace:*",
|
||||||
"@sa/utils": "workspace:*",
|
"@sa/utils": "workspace:*",
|
||||||
"@types/streamsaver": "^2.0.5",
|
"@types/streamsaver": "^2.0.5",
|
||||||
"@vueuse/core": "13.4.0",
|
"@vueuse/core": "13.8.0",
|
||||||
"clipboard": "2.0.11",
|
"clipboard": "2.0.11",
|
||||||
"dayjs": "1.11.13",
|
"dayjs": "1.11.14",
|
||||||
"defu": "6.1.4",
|
"defu": "6.1.4",
|
||||||
"echarts": "5.6.0",
|
"echarts": "6.0.0",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
"jsencrypt": "^3.3.2",
|
"jsencrypt": "^3.3.2",
|
||||||
"json5": "2.2.3",
|
"json5": "2.2.3",
|
||||||
@ -80,46 +80,47 @@
|
|||||||
"pinia": "3.0.3",
|
"pinia": "3.0.3",
|
||||||
"streamsaver": "^2.0.6",
|
"streamsaver": "^2.0.6",
|
||||||
"tailwind-merge": "3.3.1",
|
"tailwind-merge": "3.3.1",
|
||||||
"vue": "3.5.17",
|
"vue": "3.5.20",
|
||||||
"vue-advanced-cropper": "^2.8.9",
|
"vue-advanced-cropper": "^2.8.9",
|
||||||
"vue-draggable-plus": "0.6.0",
|
"vue-draggable-plus": "0.6.0",
|
||||||
"vue-i18n": "11.1.7",
|
"vue-i18n": "11.1.11",
|
||||||
"vue-router": "4.5.1"
|
"vue-router": "4.5.1",
|
||||||
|
"xlsx": "0.18.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@elegant-router/vue": "0.3.8",
|
"@elegant-router/vue": "0.3.8",
|
||||||
"@iconify/json": "2.2.353",
|
"@iconify/json": "2.2.378",
|
||||||
"@sa/scripts": "workspace:*",
|
"@sa/scripts": "workspace:*",
|
||||||
"@sa/uno-preset": "workspace:*",
|
"@sa/uno-preset": "workspace:*",
|
||||||
"@soybeanjs/eslint-config": "1.7.0",
|
"@soybeanjs/eslint-config": "1.7.1",
|
||||||
"@types/node": "24.0.4",
|
"@types/node": "24.3.0",
|
||||||
"@types/nprogress": "0.2.3",
|
"@types/nprogress": "0.2.3",
|
||||||
"@unocss/eslint-config": "66.3.2",
|
"@unocss/eslint-config": "66.4.2",
|
||||||
"@unocss/preset-icons": "66.3.2",
|
"@unocss/preset-icons": "66.4.2",
|
||||||
"@unocss/preset-uno": "66.3.2",
|
"@unocss/preset-uno": "66.4.2",
|
||||||
"@unocss/transformer-directives": "66.3.2",
|
"@unocss/transformer-directives": "66.4.2",
|
||||||
"@unocss/transformer-variant-group": "66.3.2",
|
"@unocss/transformer-variant-group": "66.4.2",
|
||||||
"@unocss/vite": "66.3.2",
|
"@unocss/vite": "66.4.2",
|
||||||
"@vitejs/plugin-vue": "6.0.0",
|
"@vitejs/plugin-vue": "6.0.1",
|
||||||
"@vitejs/plugin-vue-jsx": "5.0.0",
|
"@vitejs/plugin-vue-jsx": "5.1.0",
|
||||||
"consola": "3.4.2",
|
"consola": "3.4.2",
|
||||||
"eslint": "9.29.0",
|
"eslint": "9.34.0",
|
||||||
"eslint-plugin-vue": "10.2.0",
|
"eslint-plugin-vue": "10.4.0",
|
||||||
"kolorist": "1.8.0",
|
"kolorist": "1.8.0",
|
||||||
"sass": "1.89.2",
|
"sass": "1.91.0",
|
||||||
"simple-git-hooks": "2.13.0",
|
"simple-git-hooks": "2.13.1",
|
||||||
"tsx": "4.20.3",
|
"tsx": "4.20.5",
|
||||||
"typescript": "5.8.3",
|
"typescript": "5.9.2",
|
||||||
"unplugin-icons": "22.1.0",
|
"unplugin-icons": "22.2.0",
|
||||||
"unplugin-vue-components": "28.7.0",
|
"unplugin-vue-components": "29.0.0",
|
||||||
"vite": "7.0.0",
|
"vite": "7.1.3",
|
||||||
"vite-plugin-monaco-editor": "^1.1.0",
|
"vite-plugin-monaco-editor": "^1.1.0",
|
||||||
"vite-plugin-progress": "0.0.7",
|
"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-svg-icons": "2.0.1",
|
||||||
"vite-plugin-vue-devtools": "7.7.7",
|
"vite-plugin-vue-devtools": "8.0.1",
|
||||||
"vue-eslint-parser": "10.1.4",
|
"vue-eslint-parser": "10.2.0",
|
||||||
"vue-tsc": "2.2.10"
|
"vue-tsc": "3.0.6"
|
||||||
},
|
},
|
||||||
"simple-git-hooks": {
|
"simple-git-hooks": {
|
||||||
"commit-msg": "pnpm sa git-commit-verify",
|
"commit-msg": "pnpm sa git-commit-verify",
|
||||||
|
@ -15,6 +15,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alova/mock": "2.0.17",
|
"@alova/mock": "2.0.17",
|
||||||
"@sa/utils": "workspace:*",
|
"@sa/utils": "workspace:*",
|
||||||
"alova": "3.3.3"
|
"alova": "3.3.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sa/utils": "workspace:*",
|
"@sa/utils": "workspace:*",
|
||||||
"axios": "1.10.0",
|
"axios": "1.11.0",
|
||||||
"axios-retry": "4.5.0",
|
"axios-retry": "4.5.0",
|
||||||
"qs": "6.14.0"
|
"qs": "6.14.0"
|
||||||
},
|
},
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sa/utils": "workspace:*",
|
"@sa/utils": "workspace:*",
|
||||||
"simplebar-vue": "2.4.1"
|
"simplebar-vue": "2.4.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typed-css-modules": "0.9.1"
|
"typed-css-modules": "0.9.1"
|
||||||
|
@ -14,14 +14,14 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@soybeanjs/changelog": "0.3.24",
|
"@soybeanjs/changelog": "0.3.24",
|
||||||
"bumpp": "10.2.0",
|
"bumpp": "10.2.3",
|
||||||
"c12": "3.0.4",
|
"c12": "3.2.0",
|
||||||
"cac": "6.7.14",
|
"cac": "6.7.14",
|
||||||
"consola": "3.4.2",
|
"consola": "3.4.2",
|
||||||
"enquirer": "2.4.1",
|
"enquirer": "2.4.1",
|
||||||
"execa": "9.6.0",
|
"execa": "9.6.0",
|
||||||
"kolorist": "1.8.0",
|
"kolorist": "1.8.0",
|
||||||
"npm-check-updates": "18.0.1",
|
"npm-check-updates": "18.0.3",
|
||||||
"rimraf": "6.0.1"
|
"rimraf": "6.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import type { CliOption } from '../types';
|
|||||||
const defaultOptions: CliOption = {
|
const defaultOptions: CliOption = {
|
||||||
cwd: process.cwd(),
|
cwd: process.cwd(),
|
||||||
cleanupDirs: [
|
cleanupDirs: [
|
||||||
'**/dist',
|
'dist',
|
||||||
'**/package-lock.json',
|
'**/package-lock.json',
|
||||||
'**/yarn.lock',
|
'**/yarn.lock',
|
||||||
'**/pnpm-lock.yaml',
|
'**/pnpm-lock.yaml',
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@sa/tinymce",
|
"name": "@sa/tinymce",
|
||||||
"version": "1.3.13",
|
"version": "1.3.15",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts"
|
".": "./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">
|
<script setup lang="ts">
|
||||||
import { computed, useAttrs } from 'vue';
|
import { computed, useAttrs } from 'vue';
|
||||||
import type { TagProps } from 'naive-ui';
|
import type { TagProps } from 'naive-ui';
|
||||||
|
import { jsonClone } from '@sa/utils';
|
||||||
import { useDict } from '@/hooks/business/dict';
|
import { useDict } from '@/hooks/business/dict';
|
||||||
import { isNotNull } from '@/utils/common';
|
import { isNotNull } from '@/utils/common';
|
||||||
import { $t } from '@/locales';
|
import { $t } from '@/locales';
|
||||||
@ -28,7 +29,7 @@ const { transformDictData } = useDict(props.dictCode, props.immediate);
|
|||||||
|
|
||||||
const dictTagData = computed<Api.System.DictData[]>(() => {
|
const dictTagData = computed<Api.System.DictData[]>(() => {
|
||||||
if (props.dictData) {
|
if (props.dictData) {
|
||||||
const dictData = props.dictData;
|
const dictData = jsonClone(props.dictData);
|
||||||
if (dictData.dictLabel?.startsWith(`dict.${dictData.dictType}.`)) {
|
if (dictData.dictLabel?.startsWith(`dict.${dictData.dictType}.`)) {
|
||||||
dictData.dictLabel = $t(dictData.dictLabel as App.I18n.I18nKey);
|
dictData.dictLabel = $t(dictData.dictLabel as App.I18n.I18nKey);
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ const fileList = ref<UploadFileInfo[]>([]);
|
|||||||
|
|
||||||
async function handleFetchOssList(ossIds: string[]) {
|
async function handleFetchOssList(ossIds: string[]) {
|
||||||
startLoading();
|
startLoading();
|
||||||
|
try {
|
||||||
const { error, data } = await fetchGetOssListByIds(ossIds);
|
const { error, data } = await fetchGetOssListByIds(ossIds);
|
||||||
if (error) return;
|
if (error) return;
|
||||||
fileList.value = data.map(item => ({
|
fileList.value = data.map(item => ({
|
||||||
@ -28,36 +29,36 @@ async function handleFetchOssList(ossIds: string[]) {
|
|||||||
name: item.originalName,
|
name: item.originalName,
|
||||||
status: 'finished'
|
status: 'finished'
|
||||||
}));
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
window.$message?.error(`获取文件列表失败: ${error}`);
|
||||||
|
} finally {
|
||||||
endLoading();
|
endLoading();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
value,
|
value,
|
||||||
async val => {
|
async val => {
|
||||||
const ossIds = val?.split(',')?.filter(item => isNotNull(item)) || [];
|
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) {
|
if (ossIds.length === 0) {
|
||||||
fileList.value = [];
|
fileList.value = [];
|
||||||
return;
|
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);
|
await handleFetchOssList(ossIds);
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
watch(
|
watch(fileList, val => {
|
||||||
fileList,
|
|
||||||
val => {
|
|
||||||
value.value = val
|
value.value = val
|
||||||
.filter(item => item.status === 'finished')
|
.filter(item => item.status === 'finished')
|
||||||
.map(item => item.id)
|
.map(item => item.id)
|
||||||
.join(',');
|
.join(',');
|
||||||
},
|
});
|
||||||
{ deep: true }
|
|
||||||
);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -21,27 +21,27 @@ const attrs: SelectProps = useAttrs();
|
|||||||
|
|
||||||
const { loading: postLoading, startLoading: startPostLoading, endLoading: endPostLoading } = useLoading();
|
const { loading: postLoading, startLoading: startPostLoading, endLoading: endPostLoading } = useLoading();
|
||||||
|
|
||||||
/** the enabled role options */
|
/** the enabled post options */
|
||||||
const roleOptions = ref<CommonType.Option<CommonType.IdType>[]>([]);
|
const postOptions = ref<CommonType.Option<CommonType.IdType>[]>([]);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.deptId,
|
() => props.deptId,
|
||||||
() => {
|
() => {
|
||||||
if (!props.deptId) {
|
if (!props.deptId) {
|
||||||
roleOptions.value = [];
|
postOptions.value = [];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
getRoleOptions();
|
getPostOptions();
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
async function getRoleOptions() {
|
async function getPostOptions() {
|
||||||
startPostLoading();
|
startPostLoading();
|
||||||
const { error, data } = await fetchGetPostSelect(props.deptId!);
|
const { error, data } = await fetchGetPostSelect(props.deptId!);
|
||||||
|
|
||||||
if (!error) {
|
if (!error) {
|
||||||
roleOptions.value = data.map(item => ({
|
postOptions.value = data.map(item => ({
|
||||||
label: item.postName,
|
label: item.postName,
|
||||||
value: item.postId
|
value: item.postId
|
||||||
}));
|
}));
|
||||||
@ -54,7 +54,7 @@ async function getRoleOptions() {
|
|||||||
<NSelect
|
<NSelect
|
||||||
v-model:value="value"
|
v-model:value="value"
|
||||||
:loading="postLoading"
|
:loading="postLoading"
|
||||||
:options="roleOptions"
|
:options="postOptions"
|
||||||
v-bind="attrs"
|
v-bind="attrs"
|
||||||
placeholder="请选择岗位"
|
placeholder="请选择岗位"
|
||||||
/>
|
/>
|
||||||
|
@ -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>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue';
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
import { getPaletteColorByNumber } from '@sa/color';
|
|
||||||
|
|
||||||
defineOptions({ name: 'WaveBg' });
|
defineOptions({ name: 'WaveBg' });
|
||||||
|
|
||||||
interface Props {
|
const themeStore = useThemeStore();
|
||||||
/** Theme color */
|
|
||||||
themeColor: string;
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="absolute-lt z-1 size-full overflow-hidden">
|
<div class="wave-bg">
|
||||||
<div class="absolute -right-300px -top-900px lt-sm:(-right-100px -top-1170px)">
|
<!-- 几何装饰元素 -->
|
||||||
<svg height="1337" width="1337">
|
<div class="geometric-decorations">
|
||||||
<defs>
|
<!-- 基础几何形状 -->
|
||||||
<path
|
<div class="geo-element circle-outline animate-fade-in-up animate-delay-0s"></div>
|
||||||
id="path-1"
|
<div class="geo-element square-rotated animate-fade-in-left animate-delay-0s"></div>
|
||||||
opacity="1"
|
<div class="geo-element circle-small animate-fade-in-up animate-delay-0.3s"></div>
|
||||||
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"
|
<div class="geo-element square-bottom-right animate-fade-in-right animate-delay-0s"></div>
|
||||||
/>
|
|
||||||
<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" />
|
<div class="geo-element bg-bubble animate-scale-in animate-delay-0.5s"></div>
|
||||||
<stop offset="1" :stop-color="darkColor" stop-opacity="1" />
|
|
||||||
</linearGradient>
|
<!-- 太阳/月亮 -->
|
||||||
</defs>
|
<div
|
||||||
<g opacity="1">
|
class="geo-element circle-top-right animate-fade-in-down animate-delay-0.5s"
|
||||||
<use xlink:href="#path-1" fill="url(#linearGradient-2)" fill-opacity="1" />
|
@click="toggleThemeScheme"
|
||||||
</g>
|
></div>
|
||||||
</svg>
|
|
||||||
|
<!-- 装饰点 -->
|
||||||
|
<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 class="absolute -bottom-400px -left-200px lt-sm:(-bottom-760px -left-100px)">
|
|
||||||
<svg height="896" width="967.8852157128662">
|
|
||||||
<defs>
|
|
||||||
<path
|
|
||||||
id="path-2"
|
|
||||||
opacity="1"
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M896,448 C1142.6325445712241,465.5747656464056 695.2579309733121,896 448,896 C200.74206902668806,896 5.684341886080802e-14,695.2579309733121 0,448.0000000000001 C0,200.74206902668806 200.74206902668791,5.684341886080802e-14 447.99999999999994,0 C695.2579309733121,0 475,418 896,448Z"
|
|
||||||
/>
|
|
||||||
<linearGradient id="linearGradient-3" x1="0.5" y1="0" x2="0.5" y2="1">
|
|
||||||
<stop offset="0" :stop-color="darkColor" stop-opacity="1" />
|
|
||||||
<stop offset="1" :stop-color="lightColor" stop-opacity="1" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<g opacity="1">
|
|
||||||
<use xlink:href="#path-2" fill="url(#linearGradient-3)" fill-opacity="1" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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>
|
||||||
|
@ -16,6 +16,12 @@ export function useDownload() {
|
|||||||
const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y';
|
const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y';
|
||||||
const { baseURL } = getServiceBaseURL(import.meta.env, isHttpProxy);
|
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') => ({
|
const getCommonHeaders = (contentType = 'application/octet-stream') => ({
|
||||||
Authorization: `Bearer ${localStg.get('token')}`,
|
Authorization: `Bearer ${localStg.get('token')}`,
|
||||||
@ -51,6 +57,7 @@ export function useDownload() {
|
|||||||
contentLength?: number
|
contentLength?: number
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
window.$loading?.endLoading();
|
window.$loading?.endLoading();
|
||||||
|
StreamSaver.mitm = '/streamsaver/mitm.html?version=2.0.0';
|
||||||
const fileStream = StreamSaver.createWriteStream(filename, { size: contentLength });
|
const fileStream = StreamSaver.createWriteStream(filename, { size: contentLength });
|
||||||
|
|
||||||
if (window.WritableStream && readableStream?.pipeTo) {
|
if (window.WritableStream && readableStream?.pipeTo) {
|
||||||
@ -106,11 +113,16 @@ export function useDownload() {
|
|||||||
|
|
||||||
const response = await fetch(fullUrl, requestOptions);
|
const response = await fetch(fullUrl, requestOptions);
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw new Error(errorCodeRecord.default);
|
||||||
|
}
|
||||||
|
|
||||||
await handleResponse(response);
|
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'));
|
const contentLength = Number(response.headers.get('Content-Length'));
|
||||||
await downloadByStream(response.body, finalFilename, contentLength);
|
await downloadByStream(response.body, finalFilename, contentLength);
|
||||||
return;
|
return;
|
||||||
|
@ -2,6 +2,7 @@ import { ref, toValue } from 'vue';
|
|||||||
import type { ComputedRef, Ref } from 'vue';
|
import type { ComputedRef, Ref } from 'vue';
|
||||||
import type { FormInst } from 'naive-ui';
|
import type { FormInst } from 'naive-ui';
|
||||||
import { REG_CODE_SIX, REG_EMAIL, REG_PHONE, REG_PWD, REG_USER_NAME } from '@/constants/reg';
|
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';
|
import { $t } from '@/locales';
|
||||||
|
|
||||||
export function useFormRules() {
|
export function useFormRules() {
|
||||||
@ -52,7 +53,7 @@ export function useFormRules() {
|
|||||||
required: true,
|
required: true,
|
||||||
trigger: ['input', 'blur'],
|
trigger: ['input', 'blur'],
|
||||||
validator: (_rule: any, value: any) => {
|
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 new Error(message);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
@ -33,7 +33,7 @@ const toGitee = () => {
|
|||||||
</NBadge>
|
</NBadge>
|
||||||
</NButton>
|
</NButton>
|
||||||
</template>
|
</template>
|
||||||
消息
|
{{ $t('page.home.message') }}
|
||||||
</NTooltip>
|
</NTooltip>
|
||||||
</template>
|
</template>
|
||||||
<NCard
|
<NCard
|
||||||
|
@ -61,6 +61,7 @@ const local: App.I18n.Schema = {
|
|||||||
update: 'Update',
|
update: 'Update',
|
||||||
saveSuccess: 'Save Success',
|
saveSuccess: 'Save Success',
|
||||||
updateSuccess: 'Update Success',
|
updateSuccess: 'Update Success',
|
||||||
|
noChange: 'No actions were taken',
|
||||||
userCenter: 'User Center',
|
userCenter: 'User Center',
|
||||||
yesOrNo: {
|
yesOrNo: {
|
||||||
yes: 'Yes',
|
yes: 'Yes',
|
||||||
@ -327,6 +328,8 @@ const local: App.I18n.Schema = {
|
|||||||
page: {
|
page: {
|
||||||
login: {
|
login: {
|
||||||
common: {
|
common: {
|
||||||
|
title: 'Modern enterprise-level multi-tenant management system',
|
||||||
|
subTitle: 'Provides developers with a complete enterprise management solution',
|
||||||
loginOrRegister: 'Login / Register',
|
loginOrRegister: 'Login / Register',
|
||||||
register: 'Register',
|
register: 'Register',
|
||||||
userNamePlaceholder: 'Please enter user name',
|
userNamePlaceholder: 'Please enter user name',
|
||||||
|
@ -61,6 +61,7 @@ const local: App.I18n.Schema = {
|
|||||||
update: '更新',
|
update: '更新',
|
||||||
saveSuccess: '保存成功',
|
saveSuccess: '保存成功',
|
||||||
updateSuccess: '更新成功',
|
updateSuccess: '更新成功',
|
||||||
|
noChange: '没有进行任何操作',
|
||||||
userCenter: '个人中心',
|
userCenter: '个人中心',
|
||||||
yesOrNo: {
|
yesOrNo: {
|
||||||
yes: '是',
|
yes: '是',
|
||||||
@ -327,6 +328,8 @@ const local: App.I18n.Schema = {
|
|||||||
page: {
|
page: {
|
||||||
login: {
|
login: {
|
||||||
common: {
|
common: {
|
||||||
|
title: '现代化的企业级多租户管理系统',
|
||||||
|
subTitle: '为开发者提供了完整的企业管理解决方案',
|
||||||
loginOrRegister: '登录 / 注册',
|
loginOrRegister: '登录 / 注册',
|
||||||
register: '注册',
|
register: '注册',
|
||||||
userNamePlaceholder: '请输入用户名',
|
userNamePlaceholder: '请输入用户名',
|
||||||
|
@ -78,3 +78,21 @@ export function fetchGetRoleUserList(params: Api.System.UserSearchParams) {
|
|||||||
params
|
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(',') }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@ -8,7 +8,7 @@ import { decrypt, encrypt } from '@/utils/jsencrypt';
|
|||||||
import { getAuthorization, handleExpiredRequest, showErrorMsg } from './shared';
|
import { getAuthorization, handleExpiredRequest, showErrorMsg } from './shared';
|
||||||
import type { RequestInstanceState } from './type';
|
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 isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y';
|
||||||
const { baseURL } = getServiceBaseURL(import.meta.env, isHttpProxy);
|
const { baseURL } = getServiceBaseURL(import.meta.env, isHttpProxy);
|
||||||
|
|
||||||
@ -48,6 +48,14 @@ export const request = createFlatRequest<App.Service.Response, RequestInstanceSt
|
|||||||
isBackendSuccess(response) {
|
isBackendSuccess(response) {
|
||||||
// when the backend response code is "0000"(default), it means the request is success
|
// 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
|
// 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;
|
return String(response.data.code) === import.meta.env.VITE_SERVICE_SUCCESS_CODE;
|
||||||
},
|
},
|
||||||
async onBackendFail(response, instance) {
|
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);
|
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
|
// 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(',') || [];
|
const logoutCodes = import.meta.env.VITE_SERVICE_LOGOUT_CODES?.split(',') || [];
|
||||||
// if (logoutCodes.includes(responseCode)) {
|
if (logoutCodes.includes(responseCode) && !isLogin) {
|
||||||
// handleLogout();
|
logoutAndCleanup();
|
||||||
// return null;
|
return null;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// when the backend response code is in `modalLogoutCodes`, it means the user will be logged out by displaying a modal
|
// 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(',') || [];
|
const modalLogoutCodes = import.meta.env.VITE_SERVICE_MODAL_LOGOUT_CODES?.split(',') || [];
|
||||||
if (modalLogoutCodes.includes(responseCode)) {
|
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];
|
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({
|
window.$dialog?.warning({
|
||||||
title: '系统提示',
|
title: '系统提示',
|
||||||
content: '登录状态已过期,您可以继续留在该页面,或者重新登录',
|
content: '登录状态已过期,请重新登录',
|
||||||
positiveText: '重新登录',
|
positiveText: '重新登录',
|
||||||
negativeText: '取消',
|
|
||||||
maskClosable: false,
|
maskClosable: false,
|
||||||
closeOnEsc: false,
|
closeOnEsc: false,
|
||||||
|
onAfterEnter() {
|
||||||
|
// prevent the user from refreshing the page
|
||||||
|
window.addEventListener('beforeunload', handleLogout);
|
||||||
|
},
|
||||||
onPositiveClick() {
|
onPositiveClick() {
|
||||||
logoutAndCleanup();
|
logoutAndCleanup();
|
||||||
|
},
|
||||||
|
onClose() {
|
||||||
|
logoutAndCleanup();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
request.cancelAllRequest();
|
request.cancelAllRequest();
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,23 +133,6 @@ export const request = createFlatRequest<App.Service.Response, RequestInstanceSt
|
|||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
transformBackendResponse(response) {
|
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') {
|
if (response.request.responseType === 'blob' || response.request.responseType === 'arraybuffer') {
|
||||||
return response.data;
|
return response.data;
|
||||||
|
@ -65,11 +65,15 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
|||||||
|
|
||||||
routes.forEach(route => {
|
routes.forEach(route => {
|
||||||
if (authRouteMode.value === 'dynamic') {
|
if (authRouteMode.value === 'dynamic') {
|
||||||
if (route.path === '/') {
|
if (route.path === '/' && route.children?.length) {
|
||||||
route.children?.forEach(child => {
|
const child = route.children[0];
|
||||||
|
// @ts-expect-error no hidden field
|
||||||
|
child.hidden = route.hidden;
|
||||||
parseRouter(child);
|
parseRouter(child);
|
||||||
authRoutesMap.set(child.name, child);
|
child.name = Math.random().toString(36).slice(2, 12);
|
||||||
});
|
Object.assign(route, child);
|
||||||
|
delete route.children;
|
||||||
|
authRoutesMap.set(route.name, route);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
parseRouter(route);
|
parseRouter(route);
|
||||||
@ -121,10 +125,9 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
|
|||||||
} else if (!isNotNull(route.meta.icon)) {
|
} else if (!isNotNull(route.meta.icon)) {
|
||||||
route.meta.icon = defaultIcon;
|
route.meta.icon = defaultIcon;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-expect-error no hidden field
|
// @ts-expect-error no hidden field
|
||||||
route.meta.hideInMenu = route.hidden;
|
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
|
// @ts-expect-error parent.name is activeMenu type
|
||||||
route.meta.activeMenu = parent.name;
|
route.meta.activeMenu = parent.name;
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ html,
|
|||||||
body,
|
body,
|
||||||
#app {
|
#app {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
|
@ -13,6 +13,13 @@
|
|||||||
border-color: var(--un-default-border-color, #e5e7eb); /* 2 */
|
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.
|
1. Use a consistent sensible line-height in all browsers.
|
||||||
2. Prevent adjustments of font size after orientation changes in iOS.
|
2. Prevent adjustments of font size after orientation changes in iOS.
|
||||||
|
@ -44,7 +44,7 @@ export const themeSettings: App.Theme.ThemeSetting = {
|
|||||||
fixedHeaderAndTab: true,
|
fixedHeaderAndTab: true,
|
||||||
sider: {
|
sider: {
|
||||||
inverted: false,
|
inverted: false,
|
||||||
width: 220,
|
width: 230,
|
||||||
collapsedWidth: 64,
|
collapsedWidth: 64,
|
||||||
mixWidth: 90,
|
mixWidth: 90,
|
||||||
mixCollapsedWidth: 64,
|
mixCollapsedWidth: 64,
|
||||||
|
2
src/typings/api/system.api.d.ts
vendored
2
src/typings/api/system.api.d.ts
vendored
@ -163,6 +163,8 @@ declare namespace Api {
|
|||||||
postIds: string[];
|
postIds: string[];
|
||||||
/** user role ids */
|
/** user role ids */
|
||||||
roleIds: string[];
|
roleIds: string[];
|
||||||
|
/** roles */
|
||||||
|
roles: Role[];
|
||||||
};
|
};
|
||||||
|
|
||||||
/** user list */
|
/** user list */
|
||||||
|
3
src/typings/app.d.ts
vendored
3
src/typings/app.d.ts
vendored
@ -376,6 +376,7 @@ declare namespace App {
|
|||||||
update: string;
|
update: string;
|
||||||
updateSuccess: string;
|
updateSuccess: string;
|
||||||
saveSuccess: string;
|
saveSuccess: string;
|
||||||
|
noChange: string;
|
||||||
userCenter: string;
|
userCenter: string;
|
||||||
yesOrNo: {
|
yesOrNo: {
|
||||||
yes: string;
|
yes: string;
|
||||||
@ -486,6 +487,8 @@ declare namespace App {
|
|||||||
};
|
};
|
||||||
login: {
|
login: {
|
||||||
common: {
|
common: {
|
||||||
|
title: string;
|
||||||
|
subTitle: string;
|
||||||
loginOrRegister: string;
|
loginOrRegister: string;
|
||||||
register: string;
|
register: string;
|
||||||
userNamePlaceholder: string;
|
userNamePlaceholder: string;
|
||||||
|
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_DEVTOOLS_LAUNCH_EDITOR?: import('vite-plugin-vue-devtools').VitePluginVueDevToolsOptions['launchEditor'];
|
||||||
readonly VITE_APP_CLIENT_ID?: string;
|
readonly VITE_APP_CLIENT_ID?: string;
|
||||||
readonly VITE_APP_ENCRYPT?: CommonType.YesOrNo;
|
readonly VITE_APP_ENCRYPT?: CommonType.YesOrNo;
|
||||||
|
readonly VITE_HEADER_FLAG?: string;
|
||||||
readonly VITE_APP_RSA_PUBLIC_KEY?: string;
|
readonly VITE_APP_RSA_PUBLIC_KEY?: string;
|
||||||
readonly VITE_APP_RSA_PRIVATE_KEY?: string;
|
readonly VITE_APP_RSA_PRIVATE_KEY?: string;
|
||||||
readonly VITE_APP_WEBSOCKET: CommonType.YesOrNo;
|
readonly VITE_APP_WEBSOCKET: CommonType.YesOrNo;
|
||||||
|
@ -78,12 +78,12 @@ export function humpToLine(str: string, line: string = '-') {
|
|||||||
|
|
||||||
/** 判断是否为空 */
|
/** 判断是否为空 */
|
||||||
export function isNotNull(value: any) {
|
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) {
|
export function isNull(value: any) {
|
||||||
return value === undefined || value === null || value === '' || value === 'undefined' || value === 'null';
|
return value === undefined || value === null || value === '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 判断是否为图片类型 */
|
/** 判断是否为图片类型 */
|
||||||
@ -191,3 +191,13 @@ export function transformToURLSearchParams(obj: Record<string, any>, excludeKeys
|
|||||||
});
|
});
|
||||||
return searchParams;
|
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,20 +39,29 @@ const activeModule = computed(() => moduleMap[props.module || 'pwd-login']);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="relative min-h-screen w-full flex flex-wrap">
|
<!-- Copyright By https://github.com/Daymychen/art-design-pro/blob/main/src/components/core/views/login/LoginLeftView.vue -->
|
||||||
<div class="hidden min-h-screen w-50% bg-primary-100 lg:block dark:bg-primary-800">
|
<div class="box-border size-full flex">
|
||||||
<div class="size-full flex-center">
|
<div class="relative box-border hidden h-full w-65vw overflow-hidden bg-primary-50 xl:block dark:bg-primary-900">
|
||||||
<img class="w-60% sm:w-80%" :src="loginBackground" />
|
<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="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>
|
||||||
<div class="w-full flex-col-center px-24px py-32px lg:w-50%">
|
<div class="absolute bottom-80px w-full text-center">
|
||||||
<div class="mx-auto max-w-464px w-full">
|
<h1 class="text-24px font-400">{{ $t('page.login.common.title') }}</h1>
|
||||||
<header class="flex-y-center justify-between">
|
<p class="mt-8px text-14px color-gray-500">{{ $t('page.login.common.subTitle') }}</p>
|
||||||
<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>
|
||||||
<div class="flex-y-center">
|
<WaveBg />
|
||||||
|
</div>
|
||||||
|
<header class="relative h-full flex-1 xl:m-auto sm:!w-full">
|
||||||
|
<div class="relative z-100 block flex items-center pl-30px pt-30px xl:hidden">
|
||||||
|
<SystemLogo class="text-32px text-primary" />
|
||||||
|
<h3 class="ml-10px text-20px font-400">{{ $t('system.title') }}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="position-fixed right-30px top-24px z-100 flex items-center justify-end">
|
||||||
|
<div class="ml-15px inline-block flex cursor-pointer select-none p-5px">
|
||||||
<ThemeSchemaSwitch
|
<ThemeSchemaSwitch
|
||||||
:theme-schema="themeStore.themeScheme"
|
:theme-schema="themeStore.themeScheme"
|
||||||
:show-tooltip="false"
|
:show-tooltip="false"
|
||||||
@ -68,16 +77,13 @@ const activeModule = computed(() => moduleMap[props.module || 'pwd-login']);
|
|||||||
@change-lang="appStore.changeLocale"
|
@change-lang="appStore.changeLocale"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</div>
|
||||||
<main class="pt-24px">
|
<main class="absolute inset-0 m-auto h-630px max-w-450px w-full overflow-hidden rounded-5px bg-cover px-24px">
|
||||||
<div>
|
|
||||||
<Transition :name="themeStore.page.animateMode" mode="out-in" appear>
|
<Transition :name="themeStore.page.animateMode" mode="out-in" appear>
|
||||||
<component :is="activeModule.component" />
|
<component :is="activeModule.component" />
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</header>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -40,6 +40,10 @@ async function handleSubmit() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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">
|
<NForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false" @keyup.enter="handleSubmit">
|
||||||
<NFormItem path="phone">
|
<NFormItem path="phone">
|
||||||
<NInput v-model:value="model.phone" :placeholder="$t('page.login.common.phonePlaceholder')" />
|
<NInput v-model:value="model.phone" :placeholder="$t('page.login.common.phonePlaceholder')" />
|
||||||
@ -52,15 +56,32 @@ async function handleSubmit() {
|
|||||||
</NButton>
|
</NButton>
|
||||||
</div>
|
</div>
|
||||||
</NFormItem>
|
</NFormItem>
|
||||||
<NSpace vertical :size="18" class="w-full">
|
<NSpace vertical :size="20" class="w-full">
|
||||||
<NButton type="primary" size="large" round block @click="handleSubmit">
|
<NButton type="primary" size="large" block @click="handleSubmit">
|
||||||
{{ $t('common.confirm') }}
|
{{ $t('page.login.codeLogin.title') }}
|
||||||
</NButton>
|
</NButton>
|
||||||
<NButton size="large" round block @click="toggleLoginModule('pwd-login')">
|
<NButton size="large" block @click="toggleLoginModule('pwd-login')">
|
||||||
{{ $t('page.login.common.back') }}
|
{{ $t('page.login.common.back') }}
|
||||||
</NButton>
|
</NButton>
|
||||||
</NSpace>
|
</NSpace>
|
||||||
</NForm>
|
</NForm>
|
||||||
</template>
|
</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();
|
const { data, error } = await fetchTenantList();
|
||||||
if (error) return;
|
if (error) return;
|
||||||
tenantEnabled.value = data.tenantEnabled;
|
tenantEnabled.value = data.tenantEnabled;
|
||||||
|
if (data.tenantEnabled) {
|
||||||
tenantOption.value = data.voList.map(tenant => {
|
tenantOption.value = data.voList.map(tenant => {
|
||||||
return {
|
return {
|
||||||
label: tenant.companyName,
|
label: tenant.companyName,
|
||||||
value: tenant.tenantId
|
value: tenant.tenantId
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
}
|
||||||
endTenantLoading();
|
endTenantLoading();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,8 +124,8 @@ async function handleSocialLogin(type: Api.System.SocialSource) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-12px text-24px text-black font-500 sm:text-30px dark:text-white">登录到您的账户</div>
|
<div class="mb-5px text-32px text-black font-600 dark:text-white">登录到您的账户</div>
|
||||||
<div class="pb-24px text-18px text-#858585">欢迎回来!请输入您的账户信息</div>
|
<div class="pb-18px text-16px text-#858585">欢迎回来!请输入您的账户信息</div>
|
||||||
<NForm
|
<NForm
|
||||||
ref="formRef"
|
ref="formRef"
|
||||||
:model="model"
|
:model="model"
|
||||||
@ -154,16 +156,16 @@ async function handleSocialLogin(type: Api.System.SocialSource) {
|
|||||||
<NFormItem v-if="captchaEnabled" path="code">
|
<NFormItem v-if="captchaEnabled" path="code">
|
||||||
<div class="w-full flex-y-center gap-16px">
|
<div class="w-full flex-y-center gap-16px">
|
||||||
<NInput v-model:value="model.code" :placeholder="$t('page.login.common.codePlaceholder')" />
|
<NInput v-model:value="model.code" :placeholder="$t('page.login.common.codePlaceholder')" />
|
||||||
<NSpin :show="codeLoading" :size="28" class="h-52px">
|
<NSpin :show="codeLoading" :size="28" class="h-42px">
|
||||||
<NButton :focusable="false" class="login-code h-52px w-136px" @click="handleFetchCaptchaCode">
|
<NButton :focusable="false" class="login-code h-42px w-136px" @click="handleFetchCaptchaCode">
|
||||||
<img v-if="codeUrl" :src="codeUrl" />
|
<img v-if="codeUrl" :src="codeUrl" />
|
||||||
<NEmpty v-else :show-icon="false" description="暂无验证码" />
|
<NEmpty v-else :show-icon="false" description="暂无验证码" />
|
||||||
</NButton>
|
</NButton>
|
||||||
</NSpin>
|
</NSpin>
|
||||||
</div>
|
</div>
|
||||||
</NFormItem>
|
</NFormItem>
|
||||||
<NSpace vertical :size="16" class="mb-8px">
|
<NSpace vertical :size="12" class="mb-8px">
|
||||||
<div class="mx-6px mb-10px flex-y-center justify-between">
|
<div class="mx-6px mb-8px flex-y-center justify-between">
|
||||||
<NCheckbox v-model:checked="remberMe" size="large">{{ $t('page.login.pwdLogin.rememberMe') }}</NCheckbox>
|
<NCheckbox v-model:checked="remberMe" size="large">{{ $t('page.login.pwdLogin.rememberMe') }}</NCheckbox>
|
||||||
<NA type="primary" class="text-18px" @click="toggleLoginModule('reset-pwd')">
|
<NA type="primary" class="text-18px" @click="toggleLoginModule('reset-pwd')">
|
||||||
{{ $t('page.login.pwdLogin.forgetPassword') }}
|
{{ $t('page.login.pwdLogin.forgetPassword') }}
|
||||||
@ -197,7 +199,7 @@ async function handleSocialLogin(type: Api.System.SocialSource) {
|
|||||||
</NButton>
|
</NButton>
|
||||||
</div>
|
</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')">
|
<NA type="primary" class="text-18px" @click="toggleLoginModule('register')">
|
||||||
{{ $t('page.login.common.register') }}
|
{{ $t('page.login.common.register') }}
|
||||||
@ -214,13 +216,13 @@ async function handleSocialLogin(type: Api.System.SocialSource) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
height: 52px;
|
height: 42px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.n-base-selection),
|
:deep(.n-base-selection),
|
||||||
:deep(.n-input) {
|
:deep(.n-input) {
|
||||||
--n-height: 52px !important;
|
--n-height: 42px !important;
|
||||||
--n-font-size: 16px !important;
|
--n-font-size: 16px !important;
|
||||||
--n-border-radius: 8px !important;
|
--n-border-radius: 8px !important;
|
||||||
}
|
}
|
||||||
@ -235,7 +237,7 @@ async function handleSocialLogin(type: Api.System.SocialSource) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
:deep(.n-button) {
|
:deep(.n-button) {
|
||||||
--n-height: 52px !important;
|
--n-height: 42px !important;
|
||||||
--n-font-size: 18px !important;
|
--n-font-size: 18px !important;
|
||||||
--n-border-radius: 8px !important;
|
--n-border-radius: 8px !important;
|
||||||
}
|
}
|
||||||
|
@ -104,8 +104,8 @@ handleFetchCaptchaCode();
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-12px text-24px text-black font-500 sm:text-30px dark:text-white">注册新账户</div>
|
<div class="mb-5px text-32px text-black font-600 sm:text-30px dark:text-white">注册新账户</div>
|
||||||
<div class="pb-24px text-18px text-#858585">欢迎注册!请输入您的账户信息</div>
|
<div class="pb-18px text-16px text-#858585">欢迎注册!请输入您的账户信息</div>
|
||||||
<NForm
|
<NForm
|
||||||
ref="formRef"
|
ref="formRef"
|
||||||
:model="model"
|
:model="model"
|
||||||
@ -147,14 +147,14 @@ handleFetchCaptchaCode();
|
|||||||
</NSpin>
|
</NSpin>
|
||||||
</div>
|
</div>
|
||||||
</NFormItem>
|
</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">
|
<NButton type="primary" size="large" block :loading="registerLoading" @click="handleSubmit">
|
||||||
{{ $t('page.login.common.register') }}
|
{{ $t('page.login.common.register') }}
|
||||||
</NButton>
|
</NButton>
|
||||||
</NSpace>
|
</NSpace>
|
||||||
</NForm>
|
</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')">
|
<NA type="primary" class="text-18px" @click="toggleLoginModule('pwd-login')">
|
||||||
{{ $t('common.login') }}
|
{{ $t('common.login') }}
|
||||||
@ -177,7 +177,7 @@ handleFetchCaptchaCode();
|
|||||||
|
|
||||||
:deep(.n-base-selection),
|
:deep(.n-base-selection),
|
||||||
:deep(.n-input) {
|
:deep(.n-input) {
|
||||||
--n-height: 52px !important;
|
--n-height: 42px !important;
|
||||||
--n-font-size: 16px !important;
|
--n-font-size: 16px !important;
|
||||||
--n-border-radius: 8px !important;
|
--n-border-radius: 8px !important;
|
||||||
}
|
}
|
||||||
@ -187,7 +187,7 @@ handleFetchCaptchaCode();
|
|||||||
}
|
}
|
||||||
|
|
||||||
:deep(.n-button) {
|
:deep(.n-button) {
|
||||||
--n-height: 52px !important;
|
--n-height: 42px !important;
|
||||||
--n-font-size: 18px !important;
|
--n-font-size: 18px !important;
|
||||||
--n-border-radius: 8px !important;
|
--n-border-radius: 8px !important;
|
||||||
}
|
}
|
||||||
|
@ -46,10 +46,10 @@ async function handleSubmit() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<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') }}
|
{{ $t('page.login.resetPwd.title') }}
|
||||||
</div>
|
</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">
|
<NForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false" @keyup.enter="handleSubmit">
|
||||||
<NFormItem path="phone">
|
<NFormItem path="phone">
|
||||||
<NInput v-model:value="model.phone" :placeholder="$t('page.login.common.phonePlaceholder')" />
|
<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')"
|
:placeholder="$t('page.login.common.confirmPasswordPlaceholder')"
|
||||||
/>
|
/>
|
||||||
</NFormItem>
|
</NFormItem>
|
||||||
<NSpace vertical :size="18" class="w-full">
|
<NSpace vertical :size="20" class="w-full">
|
||||||
<NButton type="primary" size="large" block @click="handleSubmit">
|
<NButton type="primary" size="large" block @click="handleSubmit">
|
||||||
{{ $t('page.login.resetPwd.title') }}
|
{{ $t('page.login.resetPwd.title') }}
|
||||||
</NButton>
|
</NButton>
|
||||||
@ -88,7 +88,7 @@ async function handleSubmit() {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
:deep(.n-base-selection),
|
:deep(.n-base-selection),
|
||||||
:deep(.n-input) {
|
:deep(.n-input) {
|
||||||
--n-height: 52px !important;
|
--n-height: 42px !important;
|
||||||
--n-font-size: 16px !important;
|
--n-font-size: 16px !important;
|
||||||
--n-border-radius: 8px !important;
|
--n-border-radius: 8px !important;
|
||||||
}
|
}
|
||||||
@ -98,7 +98,7 @@ async function handleSubmit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
:deep(.n-button) {
|
:deep(.n-button) {
|
||||||
--n-height: 52px !important;
|
--n-height: 42px !important;
|
||||||
--n-font-size: 18px !important;
|
--n-font-size: 18px !important;
|
||||||
--n-border-radius: 8px !important;
|
--n-border-radius: 8px !important;
|
||||||
}
|
}
|
||||||
|
@ -200,7 +200,7 @@ async function handleExport() {
|
|||||||
v-model:visible="drawerVisible"
|
v-model:visible="drawerVisible"
|
||||||
:operate-type="operateType"
|
:operate-type="operateType"
|
||||||
:row-data="editingData"
|
:row-data="editingData"
|
||||||
@submitted="getDataByPage"
|
@submitted="getData"
|
||||||
/>
|
/>
|
||||||
</NCard>
|
</NCard>
|
||||||
</div>
|
</div>
|
||||||
|
@ -34,13 +34,13 @@ async function search() {
|
|||||||
<NCollapseItem :title="$t('common.search')" name="user-search">
|
<NCollapseItem :title="$t('common.search')" name="user-search">
|
||||||
<NForm ref="formRef" :model="model" label-placement="left" :label-width="80">
|
<NForm ref="formRef" :model="model" label-placement="left" :label-width="80">
|
||||||
<NGrid responsive="screen" item-responsive>
|
<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地址" />
|
<NInput v-model:value="model.ipaddr" placeholder="请输入IP地址" />
|
||||||
</NFormItemGi>
|
</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="请输入用户账号" />
|
<NInput v-model:value="model.userName" placeholder="请输入用户账号" />
|
||||||
</NFormItemGi>
|
</NFormItemGi>
|
||||||
<NFormItemGi span="24" class="pr-24px">
|
<NFormItemGi span="24 s:24 m:8" class="pr-24px">
|
||||||
<NSpace class="w-full" justify="end">
|
<NSpace class="w-full" justify="end">
|
||||||
<NButton @click="reset">
|
<NButton @click="reset">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
|
@ -239,7 +239,7 @@ async function handleExport() {
|
|||||||
v-model:visible="drawerVisible"
|
v-model:visible="drawerVisible"
|
||||||
:operate-type="operateType"
|
:operate-type="operateType"
|
||||||
:row-data="editingData"
|
:row-data="editingData"
|
||||||
@submitted="getDataByPage"
|
@submitted="getData"
|
||||||
/>
|
/>
|
||||||
</NCard>
|
</NCard>
|
||||||
</div>
|
</div>
|
||||||
|
@ -61,7 +61,7 @@ async function search() {
|
|||||||
clearable
|
clearable
|
||||||
/>
|
/>
|
||||||
</NFormItemGi>
|
</NFormItemGi>
|
||||||
<NFormItemGi span="24" class="pr-24px">
|
<NFormItemGi span="24 s:12 m:6" class="pr-24px">
|
||||||
<NSpace class="w-full" justify="end">
|
<NSpace class="w-full" justify="end">
|
||||||
<NButton @click="reset">
|
<NButton @click="reset">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
|
@ -234,7 +234,7 @@ async function handleRefreshCache() {
|
|||||||
v-model:visible="drawerVisible"
|
v-model:visible="drawerVisible"
|
||||||
:operate-type="operateType"
|
:operate-type="operateType"
|
||||||
:row-data="editingData"
|
:row-data="editingData"
|
||||||
@submitted="getDataByPage"
|
@submitted="getData"
|
||||||
/>
|
/>
|
||||||
</NCard>
|
</NCard>
|
||||||
</div>
|
</div>
|
||||||
|
@ -21,9 +21,12 @@ const dateRangeCreateTime = ref<[string, string] | null>(null);
|
|||||||
const model = defineModel<Api.System.ConfigSearchParams>('model', { required: true });
|
const model = defineModel<Api.System.ConfigSearchParams>('model', { required: true });
|
||||||
|
|
||||||
function onDateRangeCreateTimeUpdate(value: [string, string] | null) {
|
function onDateRangeCreateTimeUpdate(value: [string, string] | null) {
|
||||||
if (value?.length) {
|
const params = model.value.params!;
|
||||||
model.value.params!.beginTime = value[0];
|
if (value && value.length === 2) {
|
||||||
model.value.params!.endTime = value[1];
|
[params.beginTime, params.endTime] = value;
|
||||||
|
} else {
|
||||||
|
params.beginTime = undefined;
|
||||||
|
params.endTime = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,7 +80,7 @@ async function search() {
|
|||||||
clearable
|
clearable
|
||||||
/>
|
/>
|
||||||
</NFormItemGi>
|
</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
|
<NDatePicker
|
||||||
v-model:formatted-value="dateRangeCreateTime"
|
v-model:formatted-value="dateRangeCreateTime"
|
||||||
type="datetimerange"
|
type="datetimerange"
|
||||||
@ -86,7 +89,7 @@ async function search() {
|
|||||||
@update:formatted-value="onDateRangeCreateTimeUpdate"
|
@update:formatted-value="onDateRangeCreateTimeUpdate"
|
||||||
/>
|
/>
|
||||||
</NFormItemGi>
|
</NFormItemGi>
|
||||||
<NFormItemGi span="24 s:12 m:12" class="pr-24px">
|
<NFormItemGi span="24" class="pr-24px">
|
||||||
<NSpace class="w-full" justify="end">
|
<NSpace class="w-full" justify="end">
|
||||||
<NButton @click="reset">
|
<NButton @click="reset">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
|
@ -36,10 +36,10 @@ async function search() {
|
|||||||
<NCollapseItem :title="$t('common.search')" name="user-search">
|
<NCollapseItem :title="$t('common.search')" name="user-search">
|
||||||
<NForm ref="formRef" :model="model" label-placement="left" :label-width="80">
|
<NForm ref="formRef" :model="model" label-placement="left" :label-width="80">
|
||||||
<NGrid responsive="screen" item-responsive>
|
<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')" />
|
<NInput v-model:value="model.deptName" :placeholder="$t('page.system.dept.form.deptName.required')" />
|
||||||
</NFormItemGi>
|
</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
|
<NSelect
|
||||||
v-model:value="model.status"
|
v-model:value="model.status"
|
||||||
:placeholder="$t('page.system.dept.form.status.required')"
|
:placeholder="$t('page.system.dept.form.status.required')"
|
||||||
@ -47,7 +47,7 @@ async function search() {
|
|||||||
clearable
|
clearable
|
||||||
/>
|
/>
|
||||||
</NFormItemGi>
|
</NFormItemGi>
|
||||||
<NFormItemGi span="8" class="pr-24px">
|
<NFormItemGi span="24 s:12 m:8" class="pr-24px">
|
||||||
<NSpace class="w-full" justify="end">
|
<NSpace class="w-full" justify="end">
|
||||||
<NButton @click="reset">
|
<NButton @click="reset">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
|
@ -405,7 +405,7 @@ const selectable = computed(() => {
|
|||||||
:operate-type="operateType"
|
:operate-type="operateType"
|
||||||
:row-data="editingData"
|
:row-data="editingData"
|
||||||
:dict-type="searchParams.dictType || ''"
|
:dict-type="searchParams.dictType || ''"
|
||||||
@submitted="getDataByPage"
|
@submitted="getData"
|
||||||
/>
|
/>
|
||||||
<DictTypeOperateDrawer
|
<DictTypeOperateDrawer
|
||||||
v-model:visible="dictTypeDrawerVisible"
|
v-model:visible="dictTypeDrawerVisible"
|
||||||
|
@ -33,17 +33,16 @@ async function search() {
|
|||||||
<NCollapse>
|
<NCollapse>
|
||||||
<NCollapseItem :title="$t('common.search')" name="user-search">
|
<NCollapseItem :title="$t('common.search')" name="user-search">
|
||||||
<NForm ref="formRef" :model="model" label-placement="left" :label-width="80">
|
<NForm ref="formRef" :model="model" label-placement="left" :label-width="80">
|
||||||
<NGrid responsive="self" item-responsive>
|
<NGrid responsive="screen" item-responsive>
|
||||||
<NFormItemGi
|
<NFormItemGi
|
||||||
:show-feedback="false"
|
span="24 s:12 m:12"
|
||||||
span="12"
|
|
||||||
:label="$t('page.system.dict.data.label')"
|
:label="$t('page.system.dict.data.label')"
|
||||||
path="dictLabel"
|
path="dictLabel"
|
||||||
class="pr-24px"
|
class="pr-24px"
|
||||||
>
|
>
|
||||||
<NInput v-model:value="model.dictLabel" :placeholder="$t('page.system.dict.form.dictLabel.required')" />
|
<NInput v-model:value="model.dictLabel" :placeholder="$t('page.system.dict.form.dictLabel.required')" />
|
||||||
</NFormItemGi>
|
</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">
|
<NSpace class="w-full" justify="end">
|
||||||
<NButton @click="reset">
|
<NButton @click="reset">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
|
@ -113,6 +113,24 @@ function renderLabel({ option }: { option: TreeOption }) {
|
|||||||
if (label?.startsWith('route.') || label?.startsWith('menu.')) {
|
if (label?.startsWith('route.') || label?.startsWith('menu.')) {
|
||||||
label = $t(label as App.I18n.I18nKey);
|
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>;
|
return <div>{label}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -194,7 +194,7 @@ async function edit(noticeId: CommonType.IdType) {
|
|||||||
v-model:visible="drawerVisible"
|
v-model:visible="drawerVisible"
|
||||||
:operate-type="operateType"
|
:operate-type="operateType"
|
||||||
:row-data="editingData"
|
:row-data="editingData"
|
||||||
@submitted="getDataByPage"
|
@submitted="getData"
|
||||||
/>
|
/>
|
||||||
</NCard>
|
</NCard>
|
||||||
</div>
|
</div>
|
||||||
|
@ -33,13 +33,13 @@ async function search() {
|
|||||||
<NCollapseItem :title="$t('common.search')" name="user-search">
|
<NCollapseItem :title="$t('common.search')" name="user-search">
|
||||||
<NForm ref="formRef" :model="model" label-placement="left" :label-width="80">
|
<NForm ref="formRef" :model="model" label-placement="left" :label-width="80">
|
||||||
<NGrid responsive="screen" item-responsive>
|
<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="请输入公告标题" />
|
<NInput v-model:value="model.noticeTitle" placeholder="请输入公告标题" />
|
||||||
</NFormItemGi>
|
</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="请选择公告类型" />
|
<DictSelect v-model:value="model.noticeType" dict-code="sys_notice_type" placeholder="请选择公告类型" />
|
||||||
</NFormItemGi>
|
</NFormItemGi>
|
||||||
<NFormItemGi span="8" class="pr-24px">
|
<NFormItemGi span="24 s:12 m:8" class="pr-24px">
|
||||||
<NSpace class="w-full" justify="end">
|
<NSpace class="w-full" justify="end">
|
||||||
<NButton @click="reset">
|
<NButton @click="reset">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
|
@ -252,7 +252,7 @@ async function handleStatusChange(
|
|||||||
v-model:visible="drawerVisible"
|
v-model:visible="drawerVisible"
|
||||||
:operate-type="operateType"
|
:operate-type="operateType"
|
||||||
:row-data="editingData"
|
:row-data="editingData"
|
||||||
@submitted="getDataByPage"
|
@submitted="getData"
|
||||||
/>
|
/>
|
||||||
</NCard>
|
</NCard>
|
||||||
</div>
|
</div>
|
||||||
|
@ -49,6 +49,8 @@ const {
|
|||||||
originalName: null,
|
originalName: null,
|
||||||
fileSuffix: null,
|
fileSuffix: null,
|
||||||
service: null,
|
service: null,
|
||||||
|
isAsc: 'desc',
|
||||||
|
orderByColumn: 'createTime',
|
||||||
params: {}
|
params: {}
|
||||||
},
|
},
|
||||||
columns: () => [
|
columns: () => [
|
||||||
@ -333,7 +335,7 @@ function handleToOssConfig() {
|
|||||||
:pagination="mobilePagination"
|
:pagination="mobilePagination"
|
||||||
class="sm:h-full"
|
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>
|
</NCard>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -21,9 +21,12 @@ const dateRangeCreateTime = ref<[string, string] | null>(null);
|
|||||||
const model = defineModel<Api.System.OssSearchParams>('model', { required: true });
|
const model = defineModel<Api.System.OssSearchParams>('model', { required: true });
|
||||||
|
|
||||||
function onDateRangeCreateTimeUpdate(value: [string, string] | null) {
|
function onDateRangeCreateTimeUpdate(value: [string, string] | null) {
|
||||||
if (value?.length) {
|
const params = model.value.params!;
|
||||||
model.value.params!.beginCreateTime = value[0];
|
if (value && value.length === 2) {
|
||||||
model.value.params!.endCreateTime = value[1];
|
[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[]>([]);
|
const fileList = ref<UploadFileInfo[]>([]);
|
||||||
|
|
||||||
function handleUpdateModelWhenUpload() {}
|
function handleUpdateModelWhenUpload() {
|
||||||
|
fileList.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
function closeDrawer() {
|
function closeDrawer() {
|
||||||
visible.value = false;
|
visible.value = false;
|
||||||
|
@ -288,7 +288,7 @@ function handleResetSearch() {
|
|||||||
:operate-type="operateType"
|
:operate-type="operateType"
|
||||||
:row-data="editingData"
|
:row-data="editingData"
|
||||||
:dept-data="deptData"
|
:dept-data="deptData"
|
||||||
@submitted="getDataByPage"
|
@submitted="getData"
|
||||||
/>
|
/>
|
||||||
</NCard>
|
</NCard>
|
||||||
</div>
|
</div>
|
||||||
|
@ -36,13 +36,13 @@ async function search() {
|
|||||||
<NCollapseItem :title="$t('common.search')" name="user-search">
|
<NCollapseItem :title="$t('common.search')" name="user-search">
|
||||||
<NForm ref="formRef" :model="model" label-placement="left" :label-width="80">
|
<NForm ref="formRef" :model="model" label-placement="left" :label-width="80">
|
||||||
<NGrid responsive="screen" item-responsive>
|
<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="请输入岗位编码" />
|
<NInput v-model:value="model.postCode" placeholder="请输入岗位编码" />
|
||||||
</NFormItemGi>
|
</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="请输入岗位名称" />
|
<NInput v-model:value="model.postName" placeholder="请输入岗位名称" />
|
||||||
</NFormItemGi>
|
</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
|
<NSelect
|
||||||
v-model:value="model.status"
|
v-model:value="model.status"
|
||||||
placeholder="请选择状态"
|
placeholder="请选择状态"
|
||||||
@ -50,7 +50,7 @@ async function search() {
|
|||||||
clearable
|
clearable
|
||||||
/>
|
/>
|
||||||
</NFormItemGi>
|
</NFormItemGi>
|
||||||
<NFormItemGi span="6" class="pr-24px">
|
<NFormItemGi span="24 s:12 m:6" class="pr-24px">
|
||||||
<NSpace class="w-full" justify="end">
|
<NSpace class="w-full" justify="end">
|
||||||
<NButton @click="reset">
|
<NButton @click="reset">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
|
@ -273,7 +273,7 @@ function handleAuthUser(row: Api.System.Role) {
|
|||||||
:data="data"
|
:data="data"
|
||||||
size="small"
|
size="small"
|
||||||
:flex-height="!appStore.isMobile"
|
:flex-height="!appStore.isMobile"
|
||||||
:scroll-x="962"
|
:scroll-x="1200"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
remote
|
remote
|
||||||
:row-key="row => row.roleId"
|
:row-key="row => row.roleId"
|
||||||
@ -284,14 +284,10 @@ function handleAuthUser(row: Api.System.Role) {
|
|||||||
v-model:visible="drawerVisible"
|
v-model:visible="drawerVisible"
|
||||||
:operate-type="operateType"
|
:operate-type="operateType"
|
||||||
:row-data="editingData"
|
:row-data="editingData"
|
||||||
@submitted="getDataByPage"
|
@submitted="getData"
|
||||||
/>
|
/>
|
||||||
<RoleDataScopeDrawer
|
<RoleDataScopeDrawer v-model:visible="dataScopeDrawerVisible" :row-data="editingData" @submitted="getData" />
|
||||||
v-model:visible="dataScopeDrawerVisible"
|
<RoleAuthUserDrawer v-model:visible="authUserDrawerVisible" :row-data="editingData" @submitted="getData" />
|
||||||
:row-data="editingData"
|
|
||||||
@submitted="getDataByPage"
|
|
||||||
/>
|
|
||||||
<RoleAuthUserDrawer v-model:visible="authUserDrawerVisible" :row-data="editingData" @submitted="getDataByPage" />
|
|
||||||
</NCard>
|
</NCard>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,9 +1,16 @@
|
|||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { computed, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import { fetchGetRoleUserList, fetchGetUserList } from '@/service/api/system';
|
import { NDatePicker } from 'naive-ui';
|
||||||
|
import {
|
||||||
|
fetchGetRoleUserList,
|
||||||
|
fetchGetUserList,
|
||||||
|
fetchUpdateRoleAuthUser,
|
||||||
|
fetchUpdateRoleAuthUserCancel
|
||||||
|
} from '@/service/api/system';
|
||||||
import { useAppStore } from '@/store/modules/app';
|
import { useAppStore } from '@/store/modules/app';
|
||||||
import { useDict } from '@/hooks/business/dict';
|
import { useDict } from '@/hooks/business/dict';
|
||||||
import { useTable, useTableOperate } from '@/hooks/common/table';
|
import { useTable, useTableOperate } from '@/hooks/common/table';
|
||||||
|
import { arraysEqualSet } from '@/utils/common';
|
||||||
import { $t } from '@/locales';
|
import { $t } from '@/locales';
|
||||||
import DictTag from '@/components/custom/dict-tag.vue';
|
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 { checkedRowKeys } = useTableOperate(data, getData);
|
||||||
|
|
||||||
|
const checkedUserIds = ref<CommonType.IdType[]>([]);
|
||||||
|
|
||||||
async function handleUpdateModelWhenEdit() {
|
async function handleUpdateModelWhenEdit() {
|
||||||
checkedRowKeys.value = [];
|
checkedRowKeys.value = [];
|
||||||
getDataByPage();
|
getDataByPage();
|
||||||
const { data: roleUserList } = await fetchGetRoleUserList({
|
const { data: roleUserList } = await fetchGetRoleUserList({
|
||||||
roleId: props.rowData?.roleId
|
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() {
|
function closeDrawer() {
|
||||||
@ -123,6 +133,25 @@ function closeDrawer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleSubmit() {
|
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'));
|
window.$message?.success($t('common.updateSuccess'));
|
||||||
closeDrawer();
|
closeDrawer();
|
||||||
emit('submitted');
|
emit('submitted');
|
||||||
@ -133,6 +162,25 @@ watch(visible, () => {
|
|||||||
handleUpdateModelWhenEdit();
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -158,9 +206,22 @@ watch(visible, () => {
|
|||||||
<NFormItemGi span="24 s:12 m:8" label="手机号码" path="phonenumber" class="pr-24px">
|
<NFormItemGi span="24 s:12 m:8" label="手机号码" path="phonenumber" class="pr-24px">
|
||||||
<NInput v-model:value="searchParams.phonenumber" placeholder="请输入手机号码" />
|
<NInput v-model:value="searchParams.phonenumber" placeholder="请输入手机号码" />
|
||||||
</NFormItemGi>
|
</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">
|
<NSpace class="w-full" justify="end">
|
||||||
<NButton @click="resetSearchParams">
|
<NButton @click="reset">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<icon-ic-round-refresh class="text-icon" />
|
<icon-ic-round-refresh class="text-icon" />
|
||||||
</template>
|
</template>
|
||||||
|
@ -24,9 +24,12 @@ const model = defineModel<Api.System.RoleSearchParams>('model', { required: true
|
|||||||
const { options: sysNormalDisableOptions } = useDict('sys_normal_disable', false);
|
const { options: sysNormalDisableOptions } = useDict('sys_normal_disable', false);
|
||||||
|
|
||||||
function onDateRangeCreateTimeUpdate(value: [string, string] | null) {
|
function onDateRangeCreateTimeUpdate(value: [string, string] | null) {
|
||||||
if (value?.length) {
|
const params = model.value.params!;
|
||||||
model.value.params!.beginTime = `${value[0]} 00:00:00`;
|
if (value && value.length === 2) {
|
||||||
model.value.params!.endTime = `${value[1]} 23:59:59`;
|
[params.beginTime, params.endTime] = value;
|
||||||
|
} else {
|
||||||
|
params.beginTime = undefined;
|
||||||
|
params.endTime = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -233,7 +233,7 @@ async function handleStatusChange(
|
|||||||
v-model:visible="drawerVisible"
|
v-model:visible="drawerVisible"
|
||||||
:operate-type="operateType"
|
:operate-type="operateType"
|
||||||
:row-data="editingData"
|
:row-data="editingData"
|
||||||
@submitted="getDataByPage"
|
@submitted="getData"
|
||||||
/>
|
/>
|
||||||
</NCard>
|
</NCard>
|
||||||
</div>
|
</div>
|
||||||
|
@ -72,8 +72,11 @@ async function handleUpdateModelWhenEdit() {
|
|||||||
model.menuIds = [];
|
model.menuIds = [];
|
||||||
|
|
||||||
if (props.operateType === 'add') {
|
if (props.operateType === 'add') {
|
||||||
menuTreeRef.value?.refresh();
|
|
||||||
Object.assign(model, createDefaultModel());
|
Object.assign(model, createDefaultModel());
|
||||||
|
const { data, error } = await fetchGetTenantPackageMenuTreeSelect(0);
|
||||||
|
if (error) return;
|
||||||
|
model.menuIds = data.checkedKeys;
|
||||||
|
menuOptions.value = data.menus;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,7 +148,7 @@ watch(visible, () => {
|
|||||||
v-model:options="menuOptions"
|
v-model:options="menuOptions"
|
||||||
v-model:cascade="model.menuCheckStrictly"
|
v-model:cascade="model.menuCheckStrictly"
|
||||||
v-model:loading="menuLoading"
|
v-model:loading="menuLoading"
|
||||||
:immediate="operateType === 'add'"
|
:immediate="false"
|
||||||
/>
|
/>
|
||||||
</NFormItem>
|
</NFormItem>
|
||||||
<NFormItem :label="$t('page.system.tenantPackage.remark')" path="remark">
|
<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">
|
<NForm ref="formRef" :model="model" label-placement="left" :label-width="80">
|
||||||
<NGrid responsive="screen" item-responsive>
|
<NGrid responsive="screen" item-responsive>
|
||||||
<NFormItemGi
|
<NFormItemGi
|
||||||
span="8"
|
span="24 s:12 m:8"
|
||||||
:label="$t('page.system.tenantPackage.packageName')"
|
:label="$t('page.system.tenantPackage.packageName')"
|
||||||
path="packageName"
|
path="packageName"
|
||||||
class="pr-24px"
|
class="pr-24px"
|
||||||
@ -48,7 +48,12 @@ async function search() {
|
|||||||
:placeholder="$t('page.system.tenantPackage.form.packageName.required')"
|
:placeholder="$t('page.system.tenantPackage.form.packageName.required')"
|
||||||
/>
|
/>
|
||||||
</NFormItemGi>
|
</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
|
<NSelect
|
||||||
v-model:value="model.status"
|
v-model:value="model.status"
|
||||||
:placeholder="$t('page.system.tenantPackage.form.status.required')"
|
:placeholder="$t('page.system.tenantPackage.form.status.required')"
|
||||||
@ -56,7 +61,7 @@ async function search() {
|
|||||||
clearable
|
clearable
|
||||||
/>
|
/>
|
||||||
</NFormItemGi>
|
</NFormItemGi>
|
||||||
<NFormItemGi span="8" class="pr-24px">
|
<NFormItemGi span="24 s:24 m:8" class="pr-24px">
|
||||||
<NSpace class="w-full" justify="end">
|
<NSpace class="w-full" justify="end">
|
||||||
<NButton @click="reset">
|
<NButton @click="reset">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
|
@ -261,7 +261,7 @@ async function handleExport() {
|
|||||||
v-model:visible="drawerVisible"
|
v-model:visible="drawerVisible"
|
||||||
:operate-type="operateType"
|
:operate-type="operateType"
|
||||||
:row-data="editingData"
|
:row-data="editingData"
|
||||||
@submitted="getDataByPage"
|
@submitted="getData"
|
||||||
/>
|
/>
|
||||||
</NCard>
|
</NCard>
|
||||||
</div>
|
</div>
|
||||||
|
@ -348,7 +348,7 @@ function handleResetSearch() {
|
|||||||
:pagination="mobilePagination"
|
:pagination="mobilePagination"
|
||||||
class="h-full"
|
class="h-full"
|
||||||
/>
|
/>
|
||||||
<UserImportModal v-model:visible="importVisible" @submitted="getDataByPage" />
|
<UserImportModal v-model:visible="importVisible" @submitted="getData" />
|
||||||
<UserOperateDrawer
|
<UserOperateDrawer
|
||||||
v-model:visible="drawerVisible"
|
v-model:visible="drawerVisible"
|
||||||
:operate-type="operateType"
|
:operate-type="operateType"
|
||||||
|
@ -91,6 +91,7 @@ function handleDownloadTemplate() {
|
|||||||
|
|
||||||
watch(visible, () => {
|
watch(visible, () => {
|
||||||
if (visible.value) {
|
if (visible.value) {
|
||||||
|
data.value.updateSupport = false;
|
||||||
fileList.value = [];
|
fileList.value = [];
|
||||||
success.value = false;
|
success.value = false;
|
||||||
message.value = '';
|
message.value = '';
|
||||||
@ -140,10 +141,14 @@ watch(visible, () => {
|
|||||||
</NUploadDragger>
|
</NUploadDragger>
|
||||||
</NUpload>
|
</NUpload>
|
||||||
<div class="flex-center">
|
<div class="flex-center">
|
||||||
<NCheckbox v-model="data.updateSupport">{{ $t('common.updateExisting') }}</NCheckbox>
|
<NCheckbox v-model:checked="data.updateSupport">{{ $t('common.updateExisting') }}</NCheckbox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<NAlert v-if="message" :title="$t('common.importResult')" :type="success ? 'success' : 'error'" :bordered="false">
|
<NAlert v-if="message" :title="$t('common.importResult')" :type="success ? 'success' : 'error'" :bordered="false">
|
||||||
{{ message }}
|
<NScrollbar class="max-h-200px">
|
||||||
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
|
<span v-html="message" />
|
||||||
|
</NScrollbar>
|
||||||
</NAlert>
|
</NAlert>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<NSpace justify="end" :size="16">
|
<NSpace justify="end" :size="16">
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, reactive, watch } from 'vue';
|
import { computed, reactive, ref, watch } from 'vue';
|
||||||
import { useLoading } from '@sa/hooks';
|
import { useLoading } from '@sa/hooks';
|
||||||
import { fetchCreateUser, fetchGetUserInfo, fetchUpdateUser } from '@/service/api/system';
|
import { fetchCreateUser, fetchGetUserInfo, fetchUpdateUser } from '@/service/api/system';
|
||||||
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
|
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
|
||||||
@ -49,6 +49,8 @@ type Model = Api.System.UserOperateParams;
|
|||||||
|
|
||||||
const model: Model = reactive(createDefaultModel());
|
const model: Model = reactive(createDefaultModel());
|
||||||
|
|
||||||
|
const roleOptions = ref<CommonType.Option<CommonType.IdType>[]>([]);
|
||||||
|
|
||||||
function createDefaultModel(): Model {
|
function createDefaultModel(): Model {
|
||||||
return {
|
return {
|
||||||
deptId: null,
|
deptId: null,
|
||||||
@ -65,28 +67,34 @@ function createDefaultModel(): Model {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
type RuleKey = Extract<keyof Model, 'userName' | 'nickName' | 'password' | 'status' | 'phonenumber'>;
|
type RuleKey = Extract<keyof Model, 'userName' | 'nickName' | 'password' | 'status' | 'phonenumber' | 'roleIds'>;
|
||||||
|
|
||||||
const rules: Record<RuleKey, App.Global.FormRule[]> = {
|
const rules: Record<RuleKey, App.Global.FormRule[]> = {
|
||||||
userName: [createRequiredRule($t('page.system.user.form.userName.required'))],
|
userName: [createRequiredRule($t('page.system.user.form.userName.required'))],
|
||||||
nickName: [createRequiredRule($t('page.system.user.form.nickName.required'))],
|
nickName: [createRequiredRule($t('page.system.user.form.nickName.required'))],
|
||||||
password: [{ ...patternRules.pwd, required: props.operateType === 'add' }],
|
password: [{ ...patternRules.pwd, required: props.operateType === 'add' }],
|
||||||
phonenumber: [patternRules.phone],
|
phonenumber: [patternRules.phone],
|
||||||
status: [createRequiredRule($t('page.system.user.form.status.required'))]
|
status: [createRequiredRule($t('page.system.user.form.status.required'))],
|
||||||
|
roleIds: [{ ...createRequiredRule('请选择角色'), type: 'array' }]
|
||||||
};
|
};
|
||||||
|
|
||||||
async function getUserInfo() {
|
async function getUserInfo(id: CommonType.IdType = '') {
|
||||||
startLoading();
|
startLoading();
|
||||||
const { error, data } = await fetchGetUserInfo(props.rowData?.userId);
|
const { error, data } = await fetchGetUserInfo(id);
|
||||||
if (!error) {
|
if (!error) {
|
||||||
model.roleIds = data.roleIds;
|
model.roleIds = data.roleIds;
|
||||||
model.postIds = data.postIds;
|
model.postIds = data.postIds;
|
||||||
|
roleOptions.value = data.roles.map(role => ({
|
||||||
|
label: role.roleName,
|
||||||
|
value: role.roleId
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
endLoading();
|
endLoading();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleUpdateModelWhenEdit() {
|
function handleUpdateModelWhenEdit() {
|
||||||
if (props.operateType === 'add') {
|
if (props.operateType === 'add') {
|
||||||
|
getUserInfo();
|
||||||
Object.assign(model, createDefaultModel());
|
Object.assign(model, createDefaultModel());
|
||||||
model.deptId = props.deptId;
|
model.deptId = props.deptId;
|
||||||
return;
|
return;
|
||||||
@ -96,7 +104,7 @@ function handleUpdateModelWhenEdit() {
|
|||||||
startDeptLoading();
|
startDeptLoading();
|
||||||
Object.assign(model, props.rowData);
|
Object.assign(model, props.rowData);
|
||||||
model.password = '';
|
model.password = '';
|
||||||
getUserInfo();
|
getUserInfo(props.rowData.userId);
|
||||||
endDeptLoading();
|
endDeptLoading();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -208,7 +216,14 @@ watch(visible, () => {
|
|||||||
<PostSelect v-model:value="model.postIds" :dept-id="model.deptId" multiple clearable />
|
<PostSelect v-model:value="model.postIds" :dept-id="model.deptId" multiple clearable />
|
||||||
</NFormItem>
|
</NFormItem>
|
||||||
<NFormItem :label="$t('page.system.user.roleIds')" path="roleIds">
|
<NFormItem :label="$t('page.system.user.roleIds')" path="roleIds">
|
||||||
<RoleSelect v-model:value="model.roleIds" multiple clearable />
|
<NSelect
|
||||||
|
v-model:value="model.roleIds"
|
||||||
|
:loading="loading"
|
||||||
|
:options="roleOptions"
|
||||||
|
multiple
|
||||||
|
clearable
|
||||||
|
placeholder="请选择角色"
|
||||||
|
/>
|
||||||
</NFormItem>
|
</NFormItem>
|
||||||
<NFormItem :label="$t('page.system.user.status')" path="status">
|
<NFormItem :label="$t('page.system.user.status')" path="status">
|
||||||
<DictRadio v-model:value="model.status" dict-code="sys_normal_disable" />
|
<DictRadio v-model:value="model.status" dict-code="sys_normal_disable" />
|
||||||
|
@ -24,9 +24,12 @@ const datePickerRef = ref<InstanceType<typeof NDatePicker>>();
|
|||||||
const model = defineModel<Api.System.UserSearchParams>('model', { required: true });
|
const model = defineModel<Api.System.UserSearchParams>('model', { required: true });
|
||||||
|
|
||||||
function onDateRangeCreateTimeUpdate(value: [string, string] | null) {
|
function onDateRangeCreateTimeUpdate(value: [string, string] | null) {
|
||||||
if (value?.length) {
|
const params = model.value.params!;
|
||||||
model.value.params!.beginTime = value[0];
|
if (value && value.length === 2) {
|
||||||
model.value.params!.endTime = value[1];
|
[params.beginTime, params.endTime] = value;
|
||||||
|
} else {
|
||||||
|
params.beginTime = undefined;
|
||||||
|
params.endTime = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -313,8 +313,8 @@ getDataNames();
|
|||||||
:pagination="mobilePagination"
|
:pagination="mobilePagination"
|
||||||
class="sm:h-full"
|
class="sm:h-full"
|
||||||
/>
|
/>
|
||||||
<GenTableImportDrawer v-model:visible="importVisible" :options="dataNameOptions" @submitted="getDataByPage" />
|
<GenTableImportDrawer v-model:visible="importVisible" :options="dataNameOptions" @submitted="getData" />
|
||||||
<GenTableOperateDrawer v-model:visible="drawerVisible" :row-data="editingData" @submitted="getDataByPage" />
|
<GenTableOperateDrawer v-model:visible="drawerVisible" :row-data="editingData" @submitted="getData" />
|
||||||
<GenTablePreviewDrawer
|
<GenTablePreviewDrawer
|
||||||
v-model:visible="previewVisible"
|
v-model:visible="previewVisible"
|
||||||
:row-data="editingData"
|
:row-data="editingData"
|
||||||
|
Reference in New Issue
Block a user