163 Commits

Author SHA1 Message Date
AN
2c248d82f2 fix(packages): 修复tinymce层级问题 2025-09-19 17:48:41 +08:00
AN
28101cb2f1 feat(projects): 新增岗位部门树接口 2025-09-19 16:14:09 +08:00
4e27f3b5a5 fix(projects): 修复登录页面样式问题 2025-09-15 17:07:38 +08:00
e623b560e4 docs(other): 更新 cursor 规则 2025-09-15 11:40:20 +08:00
8aeb73627a fix(projects): 修复登录页面跳转问题 2025-09-13 13:59:51 +08:00
3f148a4e62 fix(projects): 修复消息通知字典值未处理问题 2025-09-12 10:04:36 +08:00
34ab7d5da2 fix(projects): 修复菜单默认图标问题 2025-09-11 11:45:52 +08:00
513dc31eaa feat(styles): 优化左侧树形结构样式 2025-09-11 10:28:43 +08:00
dc2fbbd556 fix(projects): 修复退出登录未清空消息列表问题 2025-09-11 10:21:11 +08:00
56fd5434ca optimize(projects): 字典状态使用枚举值 2025-09-11 10:16:20 +08:00
ad207255bb fix(projects): 修复菜单弹窗打开未清空默认值问题 2025-09-11 10:14:45 +08:00
3146c039f0 feat(projects): 用户列表新增头像展示 2025-09-11 10:09:35 +08:00
AN
d5bbc37dec fix(projects): 修复新增部门时不显示上级部门问题 2025-09-10 11:48:13 +08:00
AN
2f794c4b73 fix(projects): 修改代码生成功能模块名为驼峰时,路由错误问题 2025-09-10 10:40:23 +08:00
AN
378aa869bf style(components): 修改json预览组件样式问题 2025-09-08 13:51:28 +08:00
AN
4a4244b5c4 style(styles): 修复字体样式导致下划线不可见问题 2025-09-05 17:20:55 +08:00
AN
ecad1c3e78 optimize(components): 补充国际化 2025-09-05 14:31:23 +08:00
9ef0bd416e fix(utils): 修复请求工具响应解密问题 2025-09-04 14:57:12 +08:00
25ee32074a feat(projects): 路由兼容 activeMenu 选项 2025-09-02 15:10:59 +08:00
8412a8db16 feat(projects): 重构登录页面样式 2025-09-02 14:49:58 +08:00
e230b0da81 Merge remote-tracking branch 'Soybean/main' into dev
# Conflicts:
#	build/config/proxy.ts
#	package.json
#	pnpm-lock.yaml
2025-09-01 15:06:44 +08:00
12b25e0d58 fix(types): fix proxy types 2025-08-28 00:26:29 +08:00
e33f944a74 chore(deps): update deps 2025-08-28 00:26:12 +08:00
7f2f3bd088 feat(utils): 新增本地 Excel 导出工具类 2025-08-18 16:14:29 +08:00
5ef1c5de98 fix(hooks): 修复下载 hooks 错误未处理 2025-08-18 15:24:25 +08:00
90a14e338a fix(components): 修复字典标签会修改字典数据值问题 2025-08-18 14:10:12 +08:00
4e625111ce chore(projects): release v1.1.3 2025-08-16 11:14:41 +08:00
AN
8524ae7666 Merge branch 'dev' of https://gitee.com/xlsea/ruoyi-plus-soybean into dev 2025-08-15 17:49:07 +08:00
AN
d6ae85d218 fix(projects): 修复新增用户未查询角色列表问题 2025-08-15 17:45:11 +08:00
89719abe34 Merge remote-tracking branch 'Soybean/main' into dev
# Conflicts:
#	README.en_US.md
#	README.md
2025-08-14 18:10:28 +08:00
8d7f91dccf chore(other): update the ESLint validation configuration to support more file types. 2025-08-13 10:10:10 +08:00
33ade53904 chore(readme): remove DartNode sponsorship badge from README files 2025-08-11 10:46:15 +08:00
AN
ab9c84d831 Merge branch 'dev' of https://gitee.com/xlsea/ruoyi-plus-soybean into dev 2025-08-11 09:42:59 +08:00
AN
7651816495 typo(projects): 去除console打印 2025-08-11 09:22:05 +08:00
4539fe01fb fix(projects): Fix the icon size in the image preview toolbar 2025-08-08 17:17:23 +08:00
AN
4e9839bd48 fix(projects): 修复用户导入功能无法更新问题 2025-08-07 20:29:56 +08:00
AN
a15b683b1d fix(projects): 修复用户新增时角色下拉包含超级管理员问题 2025-08-06 09:12:24 +08:00
9df8d2f55f Merge remote-tracking branch 'origin/dev' into dev 2025-07-31 13:55:10 +08:00
710374398a fix(projects): 修复登录过期事件监听未被重置 2025-07-31 13:52:07 +08:00
AN
52318c106d fix(projects): 修复日期搜索条件清除问题 2025-07-31 13:30:51 +08:00
9027632bef !25 fix(project): 关闭多租户功能后仍然遍历租户列表导致控制台报错的问题
Merge pull request !25 from littleghost2016/dev
2025-07-30 12:43:44 +00:00
b96c46baa9 fix(project): 关闭多租户功能后仍然遍历租户列表导致控制台报错的问题 2025-07-30 18:02:24 +08:00
AN
2d31d7dc62 fix(hooks): 修复oss下载时未转码问题 2025-07-28 17:56:12 +08:00
e538355f2b refactor(projects): 菜单列表新增禁用菜单样式 2025-07-26 12:55:19 +08:00
f89835578c fix(hooks): 非安全环境下不使用流式下载 2025-07-26 12:43:10 +08:00
4eb77eac78 !24 refactor(menu): 菜单管理中隐藏的菜单显示灰色
Merge pull request !24 from NicholasLD/N/A
2025-07-25 07:27:35 +00:00
adca2e26be refactor(menu): 菜单管理中隐藏的菜单显示灰色
Signed-off-by: NicholasLD <nicholasld505@gmail.com>
2025-07-25 03:51:09 +00:00
AN
ff576f3f42 Merge branch 'dev' of https://gitee.com/xlsea/ruoyi-plus-soybean into dev 2025-07-25 10:31:57 +08:00
AN
8fcc70d73d fix(projects): 修复一级菜单隐藏失效问题 2025-07-25 10:25:50 +08:00
48f603ed32 chore(projects): release v1.1.2 2025-07-24 17:44:45 +08:00
f4038a2dc0 fix(projects): 修复菜单结构变动后路由无法进入问题 2025-07-24 17:36:52 +08:00
923eb98a5c fix(readme): update GitHub stars and forks links for gitee 2025-07-20 01:12:19 +08:00
AN
a1336d1536 optimize(projects): 优化搜索框FormItem 2025-07-19 12:21:53 +08:00
AN
cc29ea85c1 style(projects): 搜索FormItem占比调整 2025-07-17 21:44:00 +08:00
AN
2edd4bc9f1 Merge branch 'dev' of https://gitee.com/xlsea/ruoyi-plus-soybean into dev 2025-07-17 16:02:29 +08:00
AN
e485f680c7 fix(projects): 修复登录过期不弹窗问题 2025-07-17 16:00:50 +08:00
2f8797eb98 docs(projects): 修改更新日志 2025-07-17 11:06:52 +08:00
2a3f3a4812 Merge remote-tracking branch 'Soybean/main' into dev
# Conflicts:
#	package.json
#	pnpm-lock.yaml
2025-07-17 10:57:54 +08:00
AN
2587f8cbfa fix(projects): 修复刷新时跳转至登录页问题 2025-07-15 16:09:34 +08:00
2036391c41 !17 fix: 修复 api.d.ts.vm 代码生成模板bug
Merge pull request !17 from 月祈风华/dev
2025-07-14 04:12:42 +00:00
e89b86ce56 chore(deps): update deps 2025-07-14 00:44:44 +08:00
4e8c8715ae fix: 修复 api.d.ts.vm 代码生成模板bug 2025-07-13 01:42:21 +08:00
e5ec915fd9 chore(projects): release v1.1.1 2025-07-11 18:04:51 +08:00
89c716e12a chore(deps): update deps 2025-07-11 17:49:57 +08:00
2d02128164 Merge remote-tracking branch 'Soybean/main' into dev
# Conflicts:
#	.vscode/settings.json
#	README.en_US.md
#	README.md
#	package.json
#	pnpm-lock.yaml
2025-07-11 17:46:01 +08:00
9ca7ca8fda fix(packages): 修复 cleanup 会删除富文本编辑器资源问题 2025-07-11 17:40:29 +08:00
d85424ee83 feat(projects): 修改操作后列表查询方式 2025-07-11 17:16:03 +08:00
ff87415d7b fix(projects): 修复角色用户分配未调用接口问题 2025-07-10 13:55:07 +08:00
312709706b Merge remote-tracking branch 'origin/dev' into dev 2025-07-09 09:38:12 +08:00
3ae9922dc4 docs(other): 修改文档内容 2025-07-09 09:30:46 +08:00
247b98a542 Merge remote-tracking branch 'origin/dev' into dev 2025-07-08 23:06:21 +08:00
566b2c2db8 fix(hooks): 解决 streamsaver 访问不到 Github 资源问题 2025-07-08 23:05:56 +08:00
AN
90d32ee29a fix(utils): 修复isNull和IsNotNull判断方法潜在问题 2025-07-08 22:09:45 +08:00
AN
efc953c094 fix(projects): 修复用户导入结果信息未渲染标签问题 2025-07-08 17:39:44 +08:00
ad48d8e840 feat(projects): 角色分配用户新增部门与时间查询条件 2025-07-04 15:37:11 +08:00
62f2c6d571 fix(projects): 修复角色列表操作栏展示不全问题 2025-07-04 15:19:35 +08:00
03dd64c543 chore(packages): update Vite version to 7 in package.json and documentation.
(cherry picked from commit ef806edd9d0c48ad8669863516d52e2eb8870d6f)
2025-07-03 22:08:50 +08:00
aeb6369005 chore(deps): update deps 2025-07-03 22:06:30 +08:00
133196f337 chore(vscode): remove unused vue.server.hybridMode setting from .vscode/settings.json 2025-07-03 22:02:30 +08:00
41191d54fb fix(projects): Fix i18n-ally not working when setting moduleResolution to bundler. fixed #780 2025-07-03 21:59:18 +08:00
AN
229e00443f fix(projects): 修复未清空文件列表,上传回显问题 2025-07-02 10:26:42 +08:00
AN
85c8a9fffa Merge branch 'dev' of https://gitee.com/xlsea/ruoyi-plus-soybean into dev 2025-07-01 19:09:27 +08:00
AN
b99999355c refactor(projects): 调整租户套餐菜单接口 2025-07-01 19:06:02 +08:00
7ddf1cf5ae chore(projects): release v1.1.0 2025-07-01 10:46:39 +08:00
814b291c58 chore(projects): release v1.1.0 2025-07-01 10:42:40 +08:00
f7c7fc41da fix(other): 修复代码生成类型定义文件重复问题 2025-07-01 10:37:34 +08:00
53fa87dae2 Merge remote-tracking branch 'origin/dev' into dev 2025-07-01 10:09:53 +08:00
99675cbc0e docs(readme): 更新 README.md 文件 2025-07-01 10:08:09 +08:00
ad9386eb58 !11 fix(projects): 修复部门字典 sys_normal_disable 重复获取
Merge pull request !11 from 素还真/N/A
2025-07-01 02:05:34 +00:00
440fd836e2 update src/views/system/dept/modules/dept-search.vue.
部门字典 sys_normal_disable 重复获取

Signed-off-by: 素还真 <11555891+metabytes@user.noreply.gitee.com>
2025-07-01 01:59:02 +00:00
AN
6fc7b11b18 fix(projects): 修复代码生成逻辑判断问题 2025-06-30 20:44:20 +08:00
7c6ca91ef2 chore(projects): update pnpm-lock.yaml 2025-06-30 17:21:05 +08:00
c789867de3 Merge remote-tracking branch 'soybean/main' into dev
# Conflicts:
#	README.en_US.md
#	README.md
#	package.json
#	pnpm-lock.yaml
2025-06-30 17:17:43 +08:00
90145fa53b fix(styles): 修复登录页平板界面滚动问题 2025-06-30 11:55:32 +08:00
a31994dc98 docs(projects): 优化 cursor 规则及 mcp 2025-06-30 11:44:32 +08:00
9a480d2245 !8 优化模板:index[-tree].vue页面引用API接口路径遇到驼峰报错问题
Merge pull request !8 from 唐振超/dev
2025-06-30 01:15:55 +00:00
7b18c0c210 !7 fix 修复api层级目录不正确
Merge pull request !7 from 这夏天依然平凡/dev
2025-06-29 12:32:03 +00:00
d13beac046 update docs/template/index-tree.vue.vm.
Signed-off-by: 唐振超 <imtzc@qq.com>
2025-06-29 01:07:22 +00:00
3122cf4df5 update docs/template/index.vue.vm.
Signed-off-by: 唐振超 <imtzc@qq.com>
2025-06-29 01:07:04 +00:00
2f1733bae1 fix 修复api层级目录不正确
Signed-off-by: 这夏天依然平凡 <1822213252@qq.com>
2025-06-28 10:01:20 +00:00
AN
dbcf8d422a fix(projects): 修改强退在线设备接口 2025-06-27 21:26:47 +08:00
5cb1cebd88 chore(deps): update deps 2025-06-27 18:17:05 +08:00
3628c2496a fix(projects): 修复字典数据重复获取问题 2025-06-26 17:12:51 +08:00
650673e2db feat(hooks): 重构下载方法,支持流式下载 2025-06-26 14:14:02 +08:00
a5c4b4e3b7 chore(deps): update NodeJS and pnpm version requirements in package.json and documentation 2025-06-25 18:12:25 +08:00
fec0563ef7 chore(deps): update deps 2025-06-25 11:20:18 +08:00
1680ce4e26 Merge remote-tracking branch 'soybean/main' into dev
# Conflicts:
#	CHANGELOG.md
#	README.en_US.md
#	README.md
#	package.json
#	pnpm-lock.yaml
#	src/App.vue
#	src/theme/settings.ts
#	src/views/_builtin/iframe-page/[url].vue
2025-06-25 11:17:46 +08:00
87a675bf62 chore(projects): release v1.3.15 2025-06-24 21:40:19 +08:00
4d42dcbea8 docs(readme): add warning about upcoming V2 version and link to plan list 2025-06-24 21:39:21 +08:00
276d836c87 refactor(iframe-page): remove unused lifecycle hooks and clean up script setup 2025-06-24 21:31:57 +08:00
7d84062e2c fix(app): replace console.error with window.console.error for consistency 2025-06-24 21:31:17 +08:00
afd604212b fix(projects): ensure proper text color when themes are inverted 2025-06-24 21:20:24 +08:00
fcb89883fa optimize(components): optimize spacing for lang-switch dropdown options 2025-06-24 21:20:20 +08:00
dc674ce870 chore(deps): update deps 2025-06-24 21:11:08 +08:00
dbd995c12c chore(projects): update deps & fix moduleResolution 2025-06-24 19:29:12 +08:00
7b2e510a2f docs(other): update docs with video tutorial link. 2025-06-24 10:17:50 +08:00
AN
62e6c7763c fix(projects): 调整属性名 2025-06-22 14:41:05 +08:00
AN
8b3151b8ce fix(projects): 修复个人信息-修改密码未加密且参数错误问题 2025-06-22 12:49:51 +08:00
AN
f36ac9abc6 refactor(projects): 补充formTip信息 2025-06-20 21:47:43 +08:00
8b4e41ce1b style(projects): 优化移动端字体大小 2025-06-20 00:02:43 +08:00
742e3858ab feat(styles): 修复登录页移动端显示问题 2025-06-19 23:52:39 +08:00
907f043969 style(projects): 修改按钮文本颜色 2025-06-19 23:30:54 +08:00
adf3d87e5c revert(projects): 单点登录回调页面回滚 2025-06-19 23:20:06 +08:00
7846b2cb1f typo(projects): 修改标题内容 2025-06-19 23:06:41 +08:00
406800de59 style(projects): 重构登录页样式 2025-06-19 23:04:09 +08:00
7e4ecae6cb style(projects): 更换 logo 与加载样式 2025-06-19 21:28:19 +08:00
471912e17f optimize(projects): 优化接口请求异常拦截代码 2025-06-19 09:33:46 +08:00
031d071af1 fix(projects): 修复接口请求异常拦截问题 2025-06-19 09:26:46 +08:00
6e6cc4d91f fix(components): 修复菜单选择标签渲染问题 2025-06-18 23:48:49 +08:00
8c84063ad1 feat(projects): 新增字典多语言适配 2025-06-18 23:20:36 +08:00
0f33f4a301 feat(other): 新增菜单字典多语言适配 SQL 2025-06-18 22:58:48 +08:00
27f061957e fix(projects): 修复切换用户或登录过期部分问题 2025-06-18 21:07:59 +08:00
AN
72b8f56e32 fix(projects): 目录类型禁用iframe选项 2025-06-18 17:06:13 +08:00
AN
0ac0a093a4 fix(projects): 修复权限字符显示逻辑错误问题 2025-06-18 14:02:50 +08:00
94d1863ef3 fix(other): 修复代码生成字典相关问题 2025-06-17 14:03:47 +08:00
ffa47c37fa fix(projects): 修复导出查询参数问题 2025-06-17 14:01:41 +08:00
031b7f698a fix(projects): 修复首页未从环境变量获取问题 2025-06-16 17:56:25 +08:00
da149e5bbd fix(types): The environment variable VITE_ICON_LOCAL_PREFIX has the wrong type. 2025-06-14 22:01:01 +08:00
f0810bce4c fix(other): 代码生成模板 dateRangeTime 错误 2025-06-14 13:42:47 +08:00
1ec1099179 fix(other): 修复代码生成问题 2025-06-14 13:40:15 +08:00
AN
cafee1dbd9 fix(projects): 修复登录过期后,重复弹窗问题 2025-06-12 09:45:50 +08:00
39dd9acca9 feat(projects): 菜单字典适配 i18n 2025-06-11 20:36:53 +08:00
AN
d141ed5bef chore(projects): 移除未使用代码 2025-06-11 11:23:51 +08:00
AN
e16a0fa6ed fix(components): 修复上传组件回显问题,修改accept参数逻辑 2025-06-11 11:09:34 +08:00
03c8a7f5b7 feat(components): 新增表单上传组件 2025-06-10 22:00:28 +08:00
AN
da1c16e023 fix(components): 修复部门选择组件非树结构,默认展开失败问题 2025-06-10 20:33:11 +08:00
7c3dac4212 feat(projects): add configurable user name watermark option 2025-06-10 19:05:08 +08:00
aeb736ebf1 fix(components): 修复树选择组件再次勾选父子联动导致全选问题 2025-06-10 11:19:04 +08:00
bbda803e90 fix(components): 修复菜单树选择组件 2025-06-10 11:05:50 +08:00
94f183e7b5 merge(project): merge soybean 1.3.14 2025-06-10 09:46:43 +08:00
39b89a1234 chore(projects): release v1.3.14 2025-06-09 22:40:03 +08:00
c57f88aad2 fix(auth): remove redundant authStore declaration in resetStore function 2025-06-09 22:34:57 +08:00
3e4e17abd8 chore(deps): update deps 2025-06-09 22:10:30 +08:00
AN
858c318002 optimize(projects): optimize tab deletion logic 2025-06-09 21:28:03 +08:00
e6044d0fc7 optimize(projects): optimize tab deletion logic. closed #755 2025-06-09 14:19:44 +08:00
AN
a0f33664ec fix(projects): 修复自定义数据权限没有保存角色部门bug 2025-06-09 13:20:28 +08:00
AN
8bb31b1c36 Merge branch 'dev' of https://gitee.com/xlsea/ruoyi-plus-soybean into dev 2025-06-08 22:51:46 +08:00
AN
64bd119c29 fix(utils): 修复 删除当前tab为最后一个时,tab切换错误bug. 2025-06-08 22:49:41 +08:00
2ed0b6484c feat(docs): add DartNode sponsorship badge to README files 2025-06-06 22:50:17 +08:00
e75c552551 docs(projects): 新增赞助列表 2025-06-06 14:34:14 +08:00
AN
d37adc362d fix(styles): 添加滚动条,去除页码 2025-06-06 13:07:21 +08:00
9ff15feee4 docs(projects): 更换微信群聊二维码 2025-06-06 09:49:25 +08:00
7bec9d1476 docs(projects): 修改部署工作流与项目说明文件 2025-06-06 09:36:29 +08:00
153 changed files with 5673 additions and 4812 deletions

View File

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

View File

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

File diff suppressed because it is too large Load Diff

46
.cursor/mcp.json Normal file
View File

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

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

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

4
.env
View File

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

View File

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

View File

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

View File

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

View File

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

19
.vscode/settings.json vendored
View File

@ -4,20 +4,35 @@
"source.organizeImports": "never"
},
"editor.formatOnSave": false,
"eslint.validate": ["html", "css", "scss", "json", "jsonc"],
"eslint.validate": [
"html",
"css",
"scss",
"json",
"jsonc",
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue"
],
"i18n-ally.displayLanguage": "zh-cn",
"i18n-ally.enabledParsers": ["ts"],
"i18n-ally.enabledFrameworks": ["vue"],
"i18n-ally.editor.preferEditor": true,
"i18n-ally.keystyle": "nested",
"i18n-ally.localesPaths": ["src/locales/langs"],
"i18n-ally.parsers.typescript.compilerOptions": {
"moduleResolution": "node"
},
"prettier.enable": false,
"typescript.tsdk": "node_modules/typescript/lib",
"unocss.root": ["./"],
"vue.server.hybridMode": true,
"files.exclude": { "/docs": true },
"search.exclude": {
"/docs": true
"/docs": true,
"**/dist/**": true
},
"cSpell.words": ["Axios", "tinymce"]
}

View File

@ -1,5 +1,226 @@
# 更新日志
## [v1.1.3](https://gitee.com/xlsea/ruoyi-plus-soybean/compare/v1.1.2...v1.1.3) (2025-08-16)
### &nbsp;&nbsp;&nbsp;🐞 Bug 修复
- **hooks**:
- 非安全环境下不使用流式下载 &nbsp;-&nbsp; by @m-xlsea [<samp>(f8983)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/f8983557)
- 修复oss下载时未转码问题 &nbsp;-&nbsp; by **AN** [<samp>(2d31d)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/2d31d7dc)
- **project**:
- 关闭多租户功能后仍然遍历租户列表导致控制台报错的问题 &nbsp;-&nbsp; by **wang_rui** [<samp>(b96c4)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/b96c46ba)
- 关闭多租户功能后仍然遍历租户列表导致控制台报错的问题 Merge pull request !25 from littleghost2016/dev &nbsp;-&nbsp; by **不寻俗** [<samp>(90276)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/9027632b)
- **projects**:
- 修复一级菜单隐藏失效问题 &nbsp;-&nbsp; by **AN** [<samp>(8fcc7)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/8fcc70d7)
- 修复日期搜索条件清除问题 &nbsp;-&nbsp; by **AN** [<samp>(52318)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/52318c10)
- 修复登录过期事件监听未被重置 &nbsp;-&nbsp; by @m-xlsea [<samp>(71037)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/71037439)
- 修复用户新增时角色下拉包含超级管理员问题 &nbsp;-&nbsp; by **AN** [<samp>(a15b6)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/a15b683b)
- 修复用户导入功能无法更新问题 &nbsp;-&nbsp; by **AN** [<samp>(4e983)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/4e9839bd)
- Fix the icon size in the image preview toolbar &nbsp;-&nbsp; by @m-xlsea [<samp>(4539f)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/4539fe01)
- 修复新增用户未查询角色列表问题 &nbsp;-&nbsp; by **AN** [<samp>(d6ae8)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/d6ae85d2)
- **readme**:
- update GitHub stars and forks links for gitee &nbsp;-&nbsp; by @soybeanjs [<samp>(923eb)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/923eb98a)
### &nbsp;&nbsp;&nbsp;💅 重构
- **menu**:
- 菜单管理中隐藏的菜单显示灰色 &nbsp;-&nbsp; by **NicholasLD** [<samp>(adca2)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/adca2e26)
- 菜单管理中隐藏的菜单显示灰色 Merge pull request !24 from NicholasLD/N/A &nbsp;-&nbsp; by **不寻俗** [<samp>(4eb77)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/4eb77eac)
- **projects**:
- 菜单列表新增禁用菜单样式 &nbsp;-&nbsp; by @m-xlsea [<samp>(e5383)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/e538355f)
### &nbsp;&nbsp;&nbsp;🏡 杂项
- **other**: update the ESLint validation configuration to support more file types. &nbsp;-&nbsp; by **Azir-11** [<samp>(8d7f9)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/8d7f91dc)
- **readme**: remove DartNode sponsorship badge from README files &nbsp;-&nbsp; by @soybeanjs [<samp>(33ade)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/33ade539)
### &nbsp;&nbsp;&nbsp;❤️ 贡献者
[![soybeanjs](https://github.com/soybeanjs.png?size=48)](https://github.com/soybeanjs)&nbsp;&nbsp;[![m-xlsea](https://github.com/m-xlsea.png?size=48)](https://github.com/m-xlsea)&nbsp;&nbsp;[![Elio-An](https://github.com/Elio-An.png?size=48)](https://gitee.com/elio-an)&nbsp;&nbsp;[![Azir-11](https://github.com/Azir-11.png?size=48)](https://github.com/Azir-11)&nbsp;&nbsp;[![Azir-11](https://github.com/NicholasLD.png?size=48)](https://github.com/NicholasLD)&nbsp;&nbsp;
[wang_rui](mailto:wrr1996@163.com)
## [v1.1.2](https://gitee.com/xlsea/ruoyi-plus-soybean/compare/v1.1.1...v1.1.2) (2025-07-24)
### &nbsp;&nbsp;&nbsp;🐞 Bug 修复
- 修复 api.d.ts.vm 代码生成模板bug &nbsp;-&nbsp; by **zygalaxy** [<samp>(4e8c8)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/4e8c8715)
- **projects**:
- 修复刷新时跳转至登录页问题 &nbsp;-&nbsp; by **AN** [<samp>(2587f)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/2587f8cb)
- 修复登录过期不弹窗问题 &nbsp;-&nbsp; by **AN** [<samp>(e485f)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/e485f680)
- 修复菜单结构变动后路由无法进入问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(f4038)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/f4038a2d)
### &nbsp;&nbsp;&nbsp;🛠 优化
- **projects**: 优化搜索框FormItem &nbsp;-&nbsp; by **AN** [<samp>(a1336)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/a1336d15)
### &nbsp;&nbsp;&nbsp;🏡 杂项
- **deps**: update deps &nbsp;-&nbsp; by @soybeanjs [<samp>(e89b8)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/e89b86ce)
### &nbsp;&nbsp;&nbsp;🎨 样式
- **projects**: 搜索FormItem占比调整 &nbsp;-&nbsp; by **AN** [<samp>(cc29e)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/cc29ea85)
### &nbsp;&nbsp;&nbsp;❤️ 贡献者
[![m-xlsea](https://github.com/m-xlsea.png?size=48)](https://github.com/m-xlsea)&nbsp;&nbsp;[![Elio-An](https://github.com/Elio-An.png?size=48)](https://gitee.com/elio-an)&nbsp;&nbsp;[![soybeanjs](https://github.com/soybeanjs.png?size=48)](https://github.com/soybeanjs)&nbsp;&nbsp;
[zygalaxy](mailto:zygalaxy@qq.com)
## [v1.1.1](https://gitee.com/xlsea/ruoyi-plus-soybean/compare/v1.1.0...v1.1.1) (2025-07-11)
### &nbsp;&nbsp;&nbsp;🚀 新功能
- **hooks**:
- 重构下载方法,支持流式下载 &nbsp;-&nbsp; by @m-xlsea [<samp>(65067)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/650673e2)
- **projects**:
- 角色分配用户新增部门与时间查询条件 &nbsp;-&nbsp; by @m-xlsea [<samp>(ad48d)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/ad48d8e8)
- 修改操作后列表查询方式 &nbsp;-&nbsp; by @m-xlsea [<samp>(d8542)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/d85424ee)
### &nbsp;&nbsp;&nbsp;🐞 Bug 修复
- **hooks**:
- 解决 streamsaver 访问不到 Github 资源问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(566b2)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/566b2c2d)
- **other**:
- 修复代码生成类型定义文件重复问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(f7c7f)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/f7c7fc41)
- **packages**:
- 修复 cleanup 会删除富文本编辑器资源问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(9ca7c)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/9ca7ca8f)
- **projects**:
- 修复字典数据重复获取问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(3628c)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/3628c249)
- 修改强退在线设备接口 &nbsp;-&nbsp; by **AN** [<samp>(dbcf8)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/dbcf8d42)
- 修复代码生成逻辑判断问题 &nbsp;-&nbsp; by **AN** [<samp>(6fc7b)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/6fc7b11b)
- 修复部门字典 sys_normal_disable 重复获取 Merge pull request !11 from 素还真/N/A &nbsp;-&nbsp; by @m-xlsea [<samp>(ad938)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/ad9386eb)
- 修复未清空文件列表,上传回显问题 &nbsp;-&nbsp; by **AN** [<samp>(229e0)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/229e0044)
- Fix i18n-ally not working when setting moduleResolution to bundler. fixed #780 &nbsp;-&nbsp; by @xiaobao0505 in https://gitee.com/xlsea/ruoyi-plus-soybean/issues/780 [<samp>(41191)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/41191d54)
- 修复角色列表操作栏展示不全问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(62f2c)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/62f2c6d5)
- 修复用户导入结果信息未渲染标签问题 &nbsp;-&nbsp; by **AN** [<samp>(efc95)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/efc953c0)
- 修复角色用户分配未调用接口问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(ff874)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/ff87415d)
- **styles**:
- 修复登录页平板界面滚动问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(90145)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/90145fa5)
- **utils**:
- 修复isNull和IsNotNull判断方法潜在问题 &nbsp;-&nbsp; by **AN** [<samp>(90d32)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/90d32ee2)
### &nbsp;&nbsp;&nbsp;💅 重构
- **projects**: 调整租户套餐菜单接口 &nbsp;-&nbsp; by **AN** [<samp>(b9999)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/b9999935)
### &nbsp;&nbsp;&nbsp;📖 文档
- **other**: 修改文档内容 &nbsp;-&nbsp; by @m-xlsea [<samp>(3ae99)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/3ae9922d)
- **projects**: 优化 cursor 规则及 mcp &nbsp;-&nbsp; by @m-xlsea [<samp>(a3199)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/a31994dc)
- **readme**: 更新 README.md 文件 &nbsp;-&nbsp; by @m-xlsea [<samp>(99675)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/99675cbc)
### &nbsp;&nbsp;&nbsp;🏡 杂项
- **deps**:
- update NodeJS and pnpm version requirements in package.json and documentation &nbsp;-&nbsp; by **Junior25306** [<samp>(a5c4b)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/a5c4b4e3)
- update deps &nbsp;-&nbsp; by @soybeanjs [<samp>(5cb1c)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/5cb1cebd)
- update deps &nbsp;-&nbsp; by @soybeanjs [<samp>(aeb63)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/aeb63690)
- update deps &nbsp;-&nbsp; by @m-xlsea [<samp>(89c71)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/89c716e1)
- **packages**:
- update Vite version to 7 in package.json and documentation. &nbsp;-&nbsp; by **Azir** [<samp>(03dd6)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/03dd64c5)
- **projects**:
- update pnpm-lock.yaml &nbsp;-&nbsp; by @m-xlsea [<samp>(7c6ca)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/7c6ca91e)
- **vscode**:
- remove unused vue.server.hybridMode setting from .vscode/settings.json &nbsp;-&nbsp; by @soybeanjs [<samp>(13319)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/133196f3)
### &nbsp;&nbsp;&nbsp;❤️ 贡献值
[![m-xlsea](https://github.com/m-xlsea.png?size=48)](https://github.com/m-xlsea)&nbsp;&nbsp;[![soybeanjs](https://github.com/soybeanjs.png?size=48)](https://github.com/soybeanjs)&nbsp;&nbsp;[![xiaobao0505](https://github.com/xiaobao0505.png?size=48)](https://github.com/xiaobao0505)&nbsp;&nbsp;[![Elio-An](https://github.com/Elio-An.png?size=48)](https://gitee.com/elio-an)&nbsp;&nbsp;[![Azir-11](https://github.com/Azir-11.png?size=48)](https://github.com/Azir-11)&nbsp;&nbsp;[Junior25306](mailto:dayu429@qq.com)
## [v1.1.0](https://gitee.com/xlsea/ruoyi-plus-soybean/compare/v1.0.0...v1.1.0) (2025-07-01)
### &nbsp;&nbsp;&nbsp;🚀 新功能
- **components**:
- 新增表单上传组件 &nbsp;-&nbsp; by @m-xlsea [<samp>(03c8a)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/03c8a7f5)
- **other**:
- 新增菜单字典多语言适配 SQL &nbsp;-&nbsp; by @m-xlsea [<samp>(0f33f)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/0f33f4a3)
- **projects**:
- add configurable user name watermark option &nbsp;-&nbsp; by @wenyuanw [<samp>(7c3da)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/7c3dac42)
- 菜单字典适配 i18n &nbsp;-&nbsp; by @m-xlsea [<samp>(39dd9)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/39dd9acc)
- 新增字典多语言适配 &nbsp;-&nbsp; by @m-xlsea [<samp>(8c840)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/8c84063a)
- **styles**:
- 修复登录页移动端显示问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(742e3)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/742e3858)
### &nbsp;&nbsp;&nbsp;🐞 Bug 修复
- **app**:
- replace console.error with window.console.error for consistency &nbsp;-&nbsp; by @soybeanjs [<samp>(7d840)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/7d84062e)
- **auth**:
- remove redundant authStore declaration in resetStore function &nbsp;-&nbsp; by @soybeanjs [<samp>(c57f8)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/c57f88aa)
- **components**:
- 修复菜单树选择组件 &nbsp;-&nbsp; by @m-xlsea [<samp>(bbda8)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/bbda803e)
- 修复树选择组件再次勾选父子联动导致全选问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(aeb73)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/aeb736eb)
- 修复部门选择组件非树结构,默认展开失败问题 &nbsp;-&nbsp; by **AN** [<samp>(da1c1)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/da1c16e0)
- 修复上传组件回显问题修改accept参数逻辑 &nbsp;-&nbsp; by **AN** [<samp>(e16a0)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/e16a0fa6)
- 修复菜单选择标签渲染问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(6e6cc)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/6e6cc4d9)
- **other**:
- 修复代码生成问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(1ec10)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/1ec10991)
- 代码生成模板 dateRangeTime 错误 &nbsp;-&nbsp; by @m-xlsea [<samp>(f0810)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/f0810bce)
- 修复代码生成字典相关问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(94d18)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/94d1863e)
- 修复代码生成类型定义文件重复问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(f7c7fc41)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/f7c7fc41)
- **projects**:
- 修复自定义数据权限没有保存角色部门bug &nbsp;-&nbsp; by **AN** [<samp>(a0f33)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/a0f33664)
- 修复登录过期后,重复弹窗问题 &nbsp;-&nbsp; by **AN** [<samp>(cafee)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/cafee1db)
- 修复首页未从环境变量获取问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(031b7)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/031b7f69)
- 修复导出查询参数问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(ffa47)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/ffa47c37)
- 修复权限字符显示逻辑错误问题 &nbsp;-&nbsp; by **AN** [<samp>(0ac0a)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/0ac0a093)
- 目录类型禁用iframe选项 &nbsp;-&nbsp; by **AN** [<samp>(72b8f)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/72b8f56e)
- 修复切换用户或登录过期部分问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(27f06)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/27f06195)
- 修复接口请求异常拦截问题 &nbsp;-&nbsp; by @m-xlsea [<samp>(031d0)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/031d071a)
- 修复个人信息-修改密码未加密且参数错误问题 &nbsp;-&nbsp; by **AN** [<samp>(8b315)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/8b3151b8)
- 调整属性名 &nbsp;-&nbsp; by **AN** [<samp>(62e6c)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/62e6c776)
- ensure proper text color when themes are inverted &nbsp;-&nbsp; by @wenyuanw [<samp>(afd60)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/afd60421)
- **styles**:
- 添加滚动条,去除页码 &nbsp;-&nbsp; by **AN** [<samp>(d37ad)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/d37adc36)
- **types**:
- The environment variable VITE_ICON_LOCAL_PREFIX has the wrong type. &nbsp;-&nbsp; by **chenziwen** [<samp>(da149)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/da149e5b)
- **utils**:
- 修复 删除当前tab为最后一个时tab切换错误bug. &nbsp;-&nbsp; by **AN** [<samp>(64bd1)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/64bd119c)
### &nbsp;&nbsp;&nbsp;🛠 优化
- **components**:
- optimize spacing for lang-switch dropdown options &nbsp;-&nbsp; by @wenyuanw [<samp>(fcb89)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/fcb89883)
- **projects**:
- optimize tab deletion logic. closed #755 &nbsp;-&nbsp; by @wenyuanw in https://gitee.com/xlsea/ruoyi-plus-soybean/issues/755 [<samp>(e6044)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/e6044d0f)
- optimize tab deletion logic &nbsp;-&nbsp; by **AN** [<samp>(858c3)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/858c3180)
- 优化接口请求异常拦截代码 &nbsp;-&nbsp; by @m-xlsea [<samp>(47191)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/471912e1)
### &nbsp;&nbsp;&nbsp;💅 重构
- **iframe-page**: remove unused lifecycle hooks and clean up script setup &nbsp;-&nbsp; by @soybeanjs [<samp>(276d8)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/276d836c)
- **projects**: 补充formTip信息 &nbsp;-&nbsp; by **AN** [<samp>(f36ac)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/f36ac9ab)
### &nbsp;&nbsp;&nbsp;📖 文档
- **readme**:
- 更新 README.md 文件 &nbsp;-&nbsp; by @m-xlsea [<samp>(99675cb)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/99675cb)
### &nbsp;&nbsp;&nbsp;🏡 杂项
- **deps**:
- update deps &nbsp;-&nbsp; by @soybeanjs [<samp>(3e4e1)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/3e4e17ab)
- update deps &nbsp;-&nbsp; by @soybeanjs [<samp>(dc674)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/dc674ce8)
- update deps &nbsp;-&nbsp; by @m-xlsea [<samp>(fec05)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/fec0563e)
- **projects**:
- 移除未使用代码 &nbsp;-&nbsp; by **AN** [<samp>(d141e)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/d141ed5b)
- update deps & fix `moduleResolution` &nbsp;-&nbsp; by @soybeanjs [<samp>(dbd99)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/dbd995c1)
### &nbsp;&nbsp;&nbsp;🎨 样式
- **projects**:
- 更换 logo 与加载样式 &nbsp;-&nbsp; by @m-xlsea [<samp>(7e4ec)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/7e4ecae6)
- 重构登录页样式 &nbsp;-&nbsp; by @m-xlsea [<samp>(40680)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/406800de)
- 修改按钮文本颜色 &nbsp;-&nbsp; by @m-xlsea [<samp>(907f0)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/907f0439)
- 优化移动端字体大小 &nbsp;-&nbsp; by @m-xlsea [<samp>(8b4e4)</samp>](https://gitee.com/xlsea/ruoyi-plus-soybean/commit/8b4e41ce)
### &nbsp;&nbsp;&nbsp;❤️ 贡献者
[![xlsea](https://github.com/m-xlsea.png?size=48)](https://gitee.com/xlsea)&nbsp;&nbsp;[![soybeanjs](https://github.com/soybeanjs.png?size=48)](https://github.com/soybeanjs)&nbsp;&nbsp;[![wenyuanw](https://github.com/wenyuanw.png?size=48)](https://github.com/wenyuanw)&nbsp;&nbsp;[![Elio-An](https://github.com/Elio-An.png?size=48)](https://gitee.com/elio-an)&nbsp;&nbsp;[![chen-ziwen](https://github.com/chen-ziwen.png?size=48)](https://github.com/chen-ziwen)&nbsp;&nbsp;
[![wangzhongqi0917](https://gitee.com/wangzhongqi0917.png?width=48)](https://gitee.com/wangzhongqi0917)&nbsp;&nbsp;[![qq1822213252](https://gitee.com/qq1822213252.png?width=48)](https://gitee.com/qq1822213252)&nbsp;&nbsp;[![tangzc](https://gitee.com/tangzc.png?width=48)](https://gitee.com/tangzc),&nbsp;[metabytes](https://gitee.com/metabytes)
## [v1.0.0](https://gitee.com/xlsea/ruoyi-plus-soybean/releases/tag/v1.0.0) (2025-06-05)
### &nbsp;&nbsp;&nbsp;🚀 新功能

107
README.md
View File

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

View File

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

View File

@ -10,6 +10,7 @@ import org.apache.velocity.VelocityContext;
import org.dromara.common.core.utils.DateUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.mybatis.enums.DataBaseType;
import org.dromara.common.mybatis.helper.DataBaseHelper;
import org.dromara.generator.constant.GenConstants;
import org.dromara.generator.domain.GenTable;
@ -58,7 +59,7 @@ public class VelocityUtils {
velocityContext.put("functionName", StringUtils.isNotEmpty(functionName) ? functionName : "【请填写功能名称】");
velocityContext.put("ClassName", genTable.getClassName());
velocityContext.put("className", StringUtils.uncapitalize(genTable.getClassName()));
velocityContext.put("moduleName", genTable.getModuleName());
velocityContext.put("moduleName", StrUtil.toSymbolCase(genTable.getModuleName(), '-'));
velocityContext.put("BusinessName", StringUtils.capitalize(genTable.getBusinessName()));
velocityContext.put("businessName", genTable.getBusinessName());
velocityContext.put("business_name", StrUtil.toUnderlineCase(genTable.getBusinessName()));
@ -73,8 +74,9 @@ public class VelocityUtils {
velocityContext.put("permissionPrefix", getPermissionPrefix(moduleName, businessName));
velocityContext.put("dicts", getDicts(genTable));
velocityContext.put("dictList", getDictList(genTable));
velocityContext.put("columns", getColumns(genTable));
velocityContext.put("columns", genTable.getColumns());
velocityContext.put("table", genTable);
velocityContext.put("StrUtil", new StrUtil());
setMenuVelocityContext(velocityContext, genTable);
if (GenConstants.TPL_TREE.equals(tplCategory)) {
setTreeVelocityContext(velocityContext, genTable);
@ -123,11 +125,12 @@ public class VelocityUtils {
templates.add("vm/java/serviceImpl.java.vm");
templates.add("vm/java/controller.java.vm");
templates.add("vm/xml/mapper.xml.vm");
if (DataBaseHelper.isOracle()) {
DataBaseType dataBaseType = DataBaseHelper.getDataBaseType();
if (dataBaseType.isOracle()) {
templates.add("vm/sql/oracle/sql.vm");
} else if (DataBaseHelper.isPostgerSql()) {
} else if (dataBaseType.isPostgreSql()) {
templates.add("vm/sql/postgres/sql.vm");
} else if (DataBaseHelper.isSqlServer()) {
} else if (dataBaseType.isSqlServer()) {
templates.add("vm/sql/sqlserver/sql.vm");
} else {
templates.add("vm/sql/sql.vm");
@ -162,7 +165,7 @@ public class VelocityUtils {
String javaPath = PROJECT_PATH + "/" + StringUtils.replace(packageName, ".", "/");
String mybatisPath = MYBATIS_PATH + "/" + moduleName;
String soybeanPath = "soy";
String soybeanModuleName = StrUtil.toSymbolCase(moduleName, '-');
if (template.contains("domain.java.vm")) {
fileName = StringUtils.format("{}/domain/{}.java", javaPath, className);
}
@ -185,17 +188,17 @@ public class VelocityUtils {
} else if (template.contains("sql.vm")) {
fileName = businessName + "Menu.sql";
} else if (template.contains("index.vue.vm")) {
fileName = StringUtils.format("{}/views/{}/{}/index.vue", soybeanPath, moduleName, StrUtil.toSymbolCase(businessName, '-'));
fileName = StringUtils.format("{}/views/{}/{}/index.vue", soybeanPath, soybeanModuleName, StrUtil.toSymbolCase(businessName, '-'));
} else if (template.contains("index-tree.vue.vm")) {
fileName = StringUtils.format("{}/views/{}/{}/index.vue", soybeanPath, moduleName, StrUtil.toSymbolCase(businessName, '-'));
fileName = StringUtils.format("{}/views/{}/{}/index.vue", soybeanPath, soybeanModuleName, StrUtil.toSymbolCase(businessName, '-'));
} else if (template.contains("api.d.ts.vm")) {
fileName = StringUtils.format("{}/typings/api/{}.api.d.ts", soybeanPath, moduleName);
fileName = StringUtils.format("{}/typings/api/{}.{}.api.d.ts", soybeanPath, soybeanModuleName, StrUtil.toSymbolCase(businessName, '-'));
} else if (template.contains("api.ts.vm")) {
fileName = StringUtils.format("{}/api/{}/{}.ts", soybeanPath, moduleName, StrUtil.toSymbolCase(businessName, '-'));
fileName = StringUtils.format("{}/service/api/{}/{}.ts", soybeanPath, soybeanModuleName, StrUtil.toSymbolCase(businessName, '-'));
} else if (template.contains("search.vue.vm")) {
fileName = StringUtils.format("{}/views/{}/{}/modules/{}-search.vue", soybeanPath, moduleName, StrUtil.toSymbolCase(businessName, '-'), StrUtil.toSymbolCase(businessName, '-'));
fileName = StringUtils.format("{}/views/{}/{}/modules/{}-search.vue", soybeanPath, soybeanModuleName, StrUtil.toSymbolCase(businessName, '-'), StrUtil.toSymbolCase(businessName, '-'));
} else if (template.contains("operate-drawer.vue.vm")) {
fileName = StringUtils.format("{}/views/{}/{}/modules/{}-operate-drawer.vue", soybeanPath, moduleName, StrUtil.toSymbolCase(businessName, '-'), StrUtil.toSymbolCase(businessName, '-'));
fileName = StringUtils.format("{}/views/{}/{}/modules/{}-operate-drawer.vue", soybeanPath, soybeanModuleName, StrUtil.toSymbolCase(businessName, '-'), StrUtil.toSymbolCase(businessName, '-'));
}
return fileName;
}
@ -296,23 +299,6 @@ public class VelocityUtils {
}
}
/**
* 根据列类型获取字典组
*
* @param genTable 业务表对象
* @return 返回字典组
*/
public static List<GenTableColumn> getColumns(GenTable genTable) {
List<GenTableColumn> columns = genTable.getColumns();
for (GenTableColumn column : columns) {
if (StringUtils.isNotBlank(column.getDictType())) {
column.setDictType(StringUtils.toCamelCase(column.getDictType()));
}
}
return columns;
}
/**
* 获取权限前缀
*

View File

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

View File

@ -1,38 +1,38 @@
-- 目录类型菜单
UPDATE `sys_menu` SET `component` = 'Layout', `icon` = 'carbon:cloud-service-management' WHERE `menu_id` = 1;
UPDATE `sys_menu` SET `component` = 'Layout', `icon` = 'stash:dashboard' WHERE `menu_id` = 2;
UPDATE `sys_menu` SET `component` = 'Layout', `icon` = 'tabler:tools' WHERE `menu_id` = 3;
UPDATE `sys_menu` SET `component` = 'Layout', `icon` = 'material-symbols:kid-star-outline' WHERE `menu_id` = 5;
UPDATE `sys_menu` SET `component` = 'Layout', `icon` = 'tabler:building-cog' WHERE `menu_id` = 6;
UPDATE `sys_menu` SET `component` = 'Layout', `icon` = 'tabler:logs' WHERE `menu_id` = 108;
UPDATE `sys_menu` SET `component` = 'Layout', `icon` = 'carbon:cloud-service-management', `menu_name` = 'route.system' WHERE `menu_id` = 1;
UPDATE `sys_menu` SET `component` = 'Layout', `icon` = 'stash:dashboard', `menu_name` = 'route.monitor' WHERE `menu_id` = 2;
UPDATE `sys_menu` SET `component` = 'Layout', `icon` = 'tabler:tools', `menu_name` = 'route.tool' WHERE `menu_id` = 3;
UPDATE `sys_menu` SET `component` = 'Layout', `icon` = 'material-symbols:kid-star-outline', `menu_name` = 'route.demo' WHERE `menu_id` = 5;
UPDATE `sys_menu` SET `component` = 'Layout', `icon` = 'tabler:building-cog', `menu_name` = 'menu.system_tenant' WHERE `menu_id` = 6;
UPDATE `sys_menu` SET `component` = 'Layout', `icon` = 'tabler:logs', `menu_name` = 'menu.system_log' WHERE `menu_id` = 108;
-- 页面类型
UPDATE `sys_menu` SET `icon` = 'ic:round-manage-accounts' WHERE `menu_id` = 100;
UPDATE `sys_menu` SET `icon` = 'carbon:user-role' WHERE `menu_id` = 101;
UPDATE `sys_menu` SET `icon` = 'material-symbols:route' WHERE `menu_id` = 102;
UPDATE `sys_menu` SET `icon` = 'mingcute:department-line' WHERE `menu_id` = 103;
UPDATE `sys_menu` SET `icon` = 'hugeicons:permanent-job' WHERE `menu_id` = 104;
UPDATE `sys_menu` SET `icon` = 'qlementine-icons:dictionary-16' WHERE `menu_id` = 105;
UPDATE `sys_menu` SET `icon` = 'carbon:parameter' WHERE `menu_id` = 106;
UPDATE `sys_menu` SET `icon` = 'solar:chat-line-outline' WHERE `menu_id` = 107;
UPDATE `sys_menu` SET `icon` = 'majesticons:status-online-line' WHERE `menu_id` = 109;
UPDATE `sys_menu` SET `icon` = 'simple-icons:redis' WHERE `menu_id` = 113;
UPDATE `sys_menu` SET `icon` = 'material-symbols:code-blocks-outline' WHERE `menu_id` = 115;
UPDATE `sys_menu` SET `icon` = 'material-symbols:attach-file' WHERE `menu_id` = 118;
UPDATE `sys_menu` SET `icon` = 'tabler:building-skyscraper' WHERE `menu_id` = 121;
UPDATE `sys_menu` SET `icon` = 'lets-icons:package-box-alt' WHERE `menu_id` = 122;
UPDATE `sys_menu` SET `icon` = 'tabler:device-imac-cog' WHERE `menu_id` = 123;
UPDATE `sys_menu` SET `icon` = 'carbon:operations-record' WHERE `menu_id` = 500;
UPDATE `sys_menu` SET `icon` = 'tabler:login-2' WHERE `menu_id` = 501;
UPDATE `sys_menu` SET `icon` = 'gg:debug' WHERE `menu_id` = 1500;
UPDATE `sys_menu` SET `icon` = 'gg:debug' WHERE `menu_id` = 1506;
UPDATE `sys_menu` SET `path` = 'oss/config', `component` = 'system/oss-config/index', `icon` = 'hugeicons:configuration-01' WHERE `menu_id` = 133;
UPDATE `sys_menu` SET `icon` = 'ic:round-manage-accounts', `menu_name` = 'route.system_user' WHERE `menu_id` = 100;
UPDATE `sys_menu` SET `icon` = 'carbon:user-role', `menu_name` = 'route.system_role' WHERE `menu_id` = 101;
UPDATE `sys_menu` SET `icon` = 'material-symbols:route', `menu_name` = 'route.system_menu' WHERE `menu_id` = 102;
UPDATE `sys_menu` SET `icon` = 'mingcute:department-line', `menu_name` = 'route.system_dept' WHERE `menu_id` = 103;
UPDATE `sys_menu` SET `icon` = 'hugeicons:permanent-job', `menu_name` = 'route.system_post' WHERE `menu_id` = 104;
UPDATE `sys_menu` SET `icon` = 'qlementine-icons:dictionary-16', `menu_name` = 'route.system_dict' WHERE `menu_id` = 105;
UPDATE `sys_menu` SET `icon` = 'carbon:parameter', `menu_name` = 'route.system_config' WHERE `menu_id` = 106;
UPDATE `sys_menu` SET `icon` = 'solar:chat-line-outline', `menu_name` = 'route.system_notice' WHERE `menu_id` = 107;
UPDATE `sys_menu` SET `icon` = 'majesticons:status-online-line', `menu_name` = 'route.monitor_online' WHERE `menu_id` = 109;
UPDATE `sys_menu` SET `icon` = 'simple-icons:redis', `menu_name` = 'route.monitor_cache' WHERE `menu_id` = 113;
UPDATE `sys_menu` SET `icon` = 'material-symbols:code-blocks-outline', `menu_name` = 'route.tool_gen' WHERE `menu_id` = 115;
UPDATE `sys_menu` SET `icon` = 'material-symbols:attach-file', `menu_name` = 'route.system_oss' WHERE `menu_id` = 118;
UPDATE `sys_menu` SET `icon` = 'tabler:building-skyscraper', `menu_name` = 'route.system_tenant' WHERE `menu_id` = 121;
UPDATE `sys_menu` SET `icon` = 'lets-icons:package-box-alt', `menu_name` = 'route.system_tenant-package' WHERE `menu_id` = 122;
UPDATE `sys_menu` SET `icon` = 'tabler:device-imac-cog', `menu_name` = 'route.system_client' WHERE `menu_id` = 123;
UPDATE `sys_menu` SET `icon` = 'carbon:operations-record', `menu_name` = 'route.monitor_operlog' WHERE `menu_id` = 500;
UPDATE `sys_menu` SET `icon` = 'tabler:login-2', `menu_name` = 'route.monitor_logininfor' WHERE `menu_id` = 501;
UPDATE `sys_menu` SET `icon` = 'gg:debug', `menu_name` = 'route.demo_demo' WHERE `menu_id` = 1500;
UPDATE `sys_menu` SET `icon` = 'gg:debug', `menu_name` = 'route.demo_tree' WHERE `menu_id` = 1506;
UPDATE `sys_menu` SET `path` = 'oss/config', `component` = 'system/oss-config/index', `icon` = 'hugeicons:configuration-01', `menu_name` = 'route.system_oss-config' WHERE `menu_id` = 133;
-- IFrame 类型
UPDATE `sys_menu` SET `component` = 'FrameView', `query_param` = 'https://ruoyi.xlsea.cn/admin/', `is_frame` = 2, `icon` = 'bx:bxl-spring-boot' WHERE `menu_id` = 117;
UPDATE `sys_menu` SET `component` = 'FrameView', `query_param` = 'https://preview.snailjob.opensnail.com/', `is_frame` = 2, `icon` = 'gridicons:scheduled' WHERE `menu_id` = 120;
UPDATE `sys_menu` SET `component` = 'FrameView', `query_param` = 'https://ruoyi.xlsea.cn/admin/', `is_frame` = 2, `icon` = 'bx:bxl-spring-boot', `menu_name` = 'menu.monitor_admin' WHERE `menu_id` = 117;
UPDATE `sys_menu` SET `component` = 'FrameView', `query_param` = 'https://preview.snailjob.opensnail.com/', `is_frame` = 2, `icon` = 'gridicons:scheduled', `menu_name` = 'menu.monitor_snail-job' WHERE `menu_id` = 120;
-- 外链类型
UPDATE `sys_menu` SET `path` = 'https://gitee.com/xlsea/ruoyi-plus-soybean', `component` = 'FrameView', `icon` = 'local-icon-gitee' WHERE `menu_id` = 4;
UPDATE `sys_menu` SET `path` = 'https://gitee.com/xlsea/ruoyi-plus-soybean', `component` = 'FrameView', `icon` = 'local-icon-gitee', `menu_name` = 'RuoYi-Plus-Soybean' WHERE `menu_id` = 4;
-- plus-ui 需要禁用的页面
UPDATE `sys_menu` SET `status` = '1' WHERE `menu_id` IN ( '116', '130', '131', '132', '11700', '11701' );

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -220,7 +220,7 @@ const events = computed(() => {
<style lang="scss">
.tox.tox-silver-sink.tox-tinymce-aux {
/** 该样式默认为1300的zIndex */
z-index: 2025;
z-index: 2025 !important;
}
.app-tinymce {

View File

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

View File

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

4751
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

4
public/favicon.svg Normal file
View File

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

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

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

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

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 192 KiB

View File

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

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,61 +1,524 @@
<!-- Copyright By https://github.com/Daymychen/art-design-pro/blob/main/src/components/core/views/login/LoginLeftView.vue -->
<script lang="ts" setup>
import { computed } from 'vue';
import { getPaletteColorByNumber } from '@sa/color';
import { useThemeStore } from '@/store/modules/theme';
defineOptions({ name: 'WaveBg' });
interface Props {
/** Theme color */
themeColor: string;
const themeStore = useThemeStore();
function toggleThemeScheme() {
if (themeStore.darkMode) {
themeStore.setThemeScheme('light');
return;
}
themeStore.setThemeScheme('dark');
}
const props = defineProps<Props>();
const lightColor = computed(() => getPaletteColorByNumber(props.themeColor, 200));
const darkColor = computed(() => getPaletteColorByNumber(props.themeColor, 500));
</script>
<template>
<div class="absolute-lt z-1 size-full overflow-hidden">
<div class="absolute -right-300px -top-900px lt-sm:(-right-100px -top-1170px)">
<svg height="1337" width="1337">
<defs>
<path
id="path-1"
opacity="1"
fill-rule="evenodd"
d="M1337,668.5 C1337,1037.455193874239 1037.455193874239,1337 668.5,1337 C523.6725684305388,1337 337,1236 370.50000000000006,1094 C434.03835568300906,824.6732385973953 6.906089672974592e-14,892.6277623047779 0,668.5000000000001 C0,299.5448061257611 299.5448061257609,1.1368683772161603e-13 668.4999999999999,0 C1037.455193874239,0 1337,299.544806125761 1337,668.5Z"
/>
<linearGradient id="linearGradient-2" x1="0.79" y1="0.62" x2="0.21" y2="0.86">
<stop offset="0" :stop-color="lightColor" stop-opacity="1" />
<stop offset="1" :stop-color="darkColor" stop-opacity="1" />
</linearGradient>
</defs>
<g opacity="1">
<use xlink:href="#path-1" fill="url(#linearGradient-2)" fill-opacity="1" />
</g>
</svg>
</div>
<div class="absolute -bottom-400px -left-200px lt-sm:(-bottom-760px -left-100px)">
<svg height="896" width="967.8852157128662">
<defs>
<path
id="path-2"
opacity="1"
fill-rule="evenodd"
d="M896,448 C1142.6325445712241,465.5747656464056 695.2579309733121,896 448,896 C200.74206902668806,896 5.684341886080802e-14,695.2579309733121 0,448.0000000000001 C0,200.74206902668806 200.74206902668791,5.684341886080802e-14 447.99999999999994,0 C695.2579309733121,0 475,418 896,448Z"
/>
<linearGradient id="linearGradient-3" x1="0.5" y1="0" x2="0.5" y2="1">
<stop offset="0" :stop-color="darkColor" stop-opacity="1" />
<stop offset="1" :stop-color="lightColor" stop-opacity="1" />
</linearGradient>
</defs>
<g opacity="1">
<use xlink:href="#path-2" fill="url(#linearGradient-3)" fill-opacity="1" />
</g>
</svg>
<div class="wave-bg">
<!-- 几何装饰元素 -->
<div class="geometric-decorations">
<!-- 基础几何形状 -->
<div class="geo-element circle-outline animate-fade-in-up animate-delay-0s"></div>
<div class="geo-element square-rotated animate-fade-in-left animate-delay-0s"></div>
<div class="geo-element circle-small animate-fade-in-up animate-delay-0.3s"></div>
<div class="geo-element square-bottom-right animate-fade-in-right animate-delay-0s"></div>
<!-- 背景泡泡 -->
<div class="geo-element bg-bubble animate-scale-in animate-delay-0.5s"></div>
<!-- 太阳/月亮 -->
<div
class="geo-element circle-top-right animate-fade-in-down animate-delay-0.5s"
@click="toggleThemeScheme"
></div>
<!-- 装饰点 -->
<div class="geo-element dot dot-top-left animate-bounce-in animate-delay-0s"></div>
<div class="geo-element dot dot-top-right animate-bounce-in animate-delay-0s"></div>
<div class="geo-element dot dot-center-right animate-bounce-in animate-delay-0s"></div>
<!-- 叠加方块组 -->
<div class="squares-group">
<i class="geo-element square square-blue animate-fade-in-left-rotated-blue animate-delay-0.2s"></i>
<i class="geo-element square square-pink animate-fade-in-left-rotated-pink animate-delay-0.4s"></i>
<i class="geo-element square square-purple animate-fade-in-left-no-rotation animate-delay-0.6s"></i>
</div>
</div>
</div>
</template>
<style scoped></style>
<style lang="scss" scoped>
// 颜色变量定义
$primary-light-7: rgb(var(--primary-50-color));
$primary-light-8: rgb(var(--primary-100-color));
$primary-light-9: rgb(var(--primary-200-color));
$primary-base: rgb(var(--primary-color));
$main-bg: rgb(var(--primary-50-color));
// 混合颜色函数
$bg-mix-light-9: color-mix(in srgb, $primary-light-9 100%, $main-bg);
$bg-mix-light-8: color-mix(in srgb, $primary-light-8 80%, $main-bg);
$bg-mix-light-7: color-mix(in srgb, $primary-light-7 80%, $main-bg);
.wave-bg {
.geometric-decorations {
.geo-element {
position: absolute;
opacity: 0;
animation-fill-mode: forwards;
animation-duration: 0.8s;
animation-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
// 动画 mixin
@mixin fadeAnimation($direction: '', $rotation: 0deg) {
from {
opacity: 0;
@if $direction == 'up' {
transform: translateY(30px) rotate($rotation);
} @else if $direction == 'down' {
transform: translateY(-30px) rotate($rotation);
} @else if $direction == 'left' {
transform: translateX(-30px) rotate($rotation);
} @else if $direction == 'right' {
transform: translateX(30px) rotate($rotation);
}
}
to {
opacity: 1;
@if $direction == 'up' or $direction == 'down' {
transform: translateY(0) rotate($rotation);
} @else {
transform: translateX(0) rotate($rotation);
}
}
}
// 动画定义
@keyframes fadeInUp {
@include fadeAnimation('up');
}
@keyframes fadeInDown {
@include fadeAnimation('down');
}
@keyframes fadeInLeft {
@include fadeAnimation('left');
}
@keyframes fadeInLeftRotated {
@include fadeAnimation('left', -25deg);
}
@keyframes fadeInRight {
@include fadeAnimation('right');
}
@keyframes fadeInRightRotated {
@include fadeAnimation('right', 45deg);
}
@keyframes fadeInLeftRotatedBlue {
@include fadeAnimation('left', -10deg);
}
@keyframes fadeInLeftRotatedPink {
@include fadeAnimation('left', 10deg);
}
@keyframes fadeInLeftNoRotation {
@include fadeAnimation('left');
}
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.8);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes bounceIn {
0% {
opacity: 0;
transform: scale(0.3);
}
50% {
opacity: 1;
transform: scale(1.05);
}
70% {
transform: scale(0.9);
}
100% {
opacity: 1;
transform: scale(1);
}
}
@keyframes lineGrow {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideInLeft {
from {
opacity: 0;
transform: translateX(-30px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
// 动画类
.animate-fade-in-up {
animation-name: fadeInUp;
}
.animate-fade-in-down {
animation-name: fadeInDown;
}
.animate-fade-in-left {
animation-name: fadeInLeft;
}
.animate-fade-in-right {
animation-name: fadeInRight;
}
.animate-scale-in {
animation-name: scaleIn;
animation-duration: 1.2s;
}
.animate-bounce-in {
animation-name: bounceIn;
animation-duration: 0.6s;
}
.animate-fade-in-left-rotated-blue {
animation-name: fadeInLeftRotatedBlue;
}
.animate-fade-in-left-rotated-pink {
animation-name: fadeInLeftRotatedPink;
}
.animate-fade-in-left-no-rotation {
animation-name: fadeInLeftNoRotation;
}
// 基础几何形状
.circle-outline {
top: 10%;
left: 25%;
width: 42px;
height: 42px;
border: 2px solid $primary-light-8;
border-radius: 50%;
}
.square-rotated {
top: 50%;
left: 16%;
width: 60px;
height: 60px;
background-color: $bg-mix-light-8;
&.animate-fade-in-left {
animation-name: fadeInLeftRotated;
}
}
.circle-small {
bottom: 26%;
left: 30%;
width: 18px;
height: 18px;
background-color: $primary-light-8;
border-radius: 50%;
}
// 太阳/月亮效果
.circle-top-right {
top: 3%;
right: 3%;
z-index: 100;
width: 50px;
height: 50px;
cursor: pointer;
background: $bg-mix-light-7;
border-radius: 50%;
transition: all 0.3s;
&::after {
position: absolute;
top: 50%;
left: 50%;
width: 100%;
height: 100%;
content: '';
background: linear-gradient(to right, #fcbb04, #fffc00);
border-radius: 50%;
opacity: 0;
transition: all 0.5s;
transform: translate(-50%, -50%);
}
&:hover {
box-shadow: 0 0 36px #fffc00;
&::after {
opacity: 1;
}
}
}
.square-bottom-right {
right: 10%;
bottom: 10%;
width: 50px;
height: 50px;
background-color: $primary-light-8;
&.animate-fade-in-right {
animation-name: fadeInRightRotated;
}
}
// 背景泡泡
.bg-bubble {
top: -120px;
right: -120px;
width: 360px;
height: 360px;
background-color: $bg-mix-light-8;
border-radius: 50%;
}
// 装饰点
.dot {
width: 14px;
height: 14px;
background-color: $primary-light-7;
border-radius: 50%;
&.dot-top-left {
top: 140px;
left: 100px;
}
&.dot-top-right {
top: 140px;
right: 120px;
}
&.dot-center-right {
top: 46%;
right: 22%;
background-color: $primary-light-8;
}
}
// 叠加方块组
.squares-group {
position: absolute;
bottom: 18px;
left: 20px;
width: 140px;
height: 140px;
pointer-events: none;
.square {
position: absolute;
display: block;
border-radius: 8px;
box-shadow: 0 8px 24px rgb(64 87 167 / 12%);
&.square-blue {
top: 12px;
left: 30px;
z-index: 2;
width: 50px;
height: 50px;
background-color: rgb(from $primary-base r g b / 30%);
}
&.square-pink {
top: 30px;
left: 48px;
z-index: 1;
width: 70px;
height: 70px;
background-color: rgb(from $primary-base r g b / 15%);
}
&.square-purple {
top: 66px;
left: 86px;
z-index: 3;
width: 32px;
height: 32px;
background-color: rgb(from $primary-base r g b / 45%);
}
}
// 装饰线条
&::after {
position: absolute;
top: 86px;
left: 72px;
width: 80px;
height: 1px;
content: '';
background: linear-gradient(90deg, var(--el-color-primary-light-6), transparent);
opacity: 0;
transform: rotate(50deg);
animation: lineGrow 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
animation-delay: 1.2s;
}
}
}
@media only screen and (max-width: 1600px) {
width: 60vw;
.text-wrap {
bottom: 40px;
}
}
@media only screen and (max-width: 1280px) {
width: auto;
height: auto;
padding: 0;
// 隐藏背景和其他内容,只保留 logo
background: transparent;
.left-img,
.text-wrap,
.geometric-decorations {
display: none;
}
.logo {
position: fixed;
top: 15px;
left: 25px;
z-index: 1000;
}
}
}
// 暗色主题
.dark .wave-bg {
background-color: color-mix(in srgb, $primary-light-9 60%, #070707);
@media only screen and (max-width: 1280px) {
background: transparent;
}
.geometric-decorations {
// 月亮效果
.circle-top-right {
background-color: $bg-mix-light-8;
box-shadow: 0 0 25px #333 inset;
transition: all 0.3s ease-in-out 0.1s;
rotate: -48deg;
&::before {
position: absolute;
top: 0;
left: 15px;
width: 50px;
height: 50px;
content: '';
background-color: $bg-mix-light-9;
border-radius: 50%;
transition: all 0.3s ease-in-out;
}
&:hover {
background-color: transparent;
box-shadow: 0 40px 25px #ddd inset;
&::before {
left: 18px;
}
&::after {
opacity: 0;
}
}
}
.bg-bubble {
background-color: $bg-mix-light-9;
}
// 其他元素颜色调整
.square-rotated {
background-color: $bg-mix-light-9;
}
.circle-small,
.dot {
background-color: $primary-light-8;
}
.square-bottom-right {
background-color: $primary-light-9;
}
.dot.dot-top-right {
background-color: $primary-light-8;
}
}
// 方块组暗色调整
.squares-group {
.square {
box-shadow: none;
&.square-blue {
background-color: rgb(from $primary-base r g b / 18%);
}
&.square-pink {
background-color: rgb(from $primary-base r g b / 10%);
}
&.square-purple {
background-color: rgb(from $primary-base r g b / 20%);
}
}
&::after {
background: linear-gradient(90deg, $primary-light-8, transparent);
}
}
}
</style>

View File

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

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

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

View File

@ -4,5 +4,6 @@ export enum SetupStoreId {
Auth = 'auth-store',
Route = 'route-store',
Tab = 'tab-store',
Notice = 'notice-store'
Notice = 'notice-store',
Dict = 'dict-store'
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
const local: App.I18n.Schema = {
system: {
title: 'RuoYi Vue Plus',
title: 'RuoYi Plus Soybean',
updateTitle: 'System Version Update Notification',
updateContent: 'A new version of the system has been detected. Do you want to refresh the page immediately?',
updateConfirm: 'Refresh immediately',
@ -61,6 +61,7 @@ const local: App.I18n.Schema = {
update: 'Update',
saveSuccess: 'Save Success',
updateSuccess: 'Update Success',
noChange: 'No actions were taken',
userCenter: 'User Center',
yesOrNo: {
yes: 'Yes',
@ -166,7 +167,8 @@ const local: App.I18n.Schema = {
},
watermark: {
visible: 'Watermark Full Screen Visible',
text: 'Watermark Text'
text: 'Watermark Text',
enableUserName: 'Enable User Name Watermark'
},
tablePropsTitle: 'Table Props',
table: {
@ -234,9 +236,100 @@ const local: App.I18n.Schema = {
exception_404: '404',
exception_500: '500'
},
menu: {
system_tenant: 'Tenant Management',
system_log: 'Log Management',
'monitor_snail-job': 'Job Management',
monitor_admin: 'Admin Monitor'
},
dict: {
sys_user_sex: {
male: 'Male',
female: 'Female',
unknown: 'Unknown'
},
sys_show_hide: {
show: 'Show',
hide: 'Hide'
},
sys_normal_disable: {
normal: 'Normal',
disable: 'Disable'
},
sys_yes_no: {
yes: 'Yes',
no: 'No'
},
sys_notice_type: {
notice: 'Notice',
announcement: 'Announcement'
},
sys_notice_status: {
normal: 'Normal',
close: 'Close'
},
sys_oper_type: {
insert: 'Insert',
update: 'Update',
delete: 'Delete',
grant: 'Grant',
export: 'Export',
import: 'Import',
force: 'Force',
gencode: 'Generate Code',
clean: 'Clean Data',
other: 'Other'
},
sys_common_status: {
success: 'Success',
fail: 'Fail'
},
sys_grant_type: {
password: 'Password Auth',
sms: 'SMS Auth',
email: 'Email Auth',
miniapp: 'Mini App Auth',
social: 'Social Auth'
},
sys_device_type: {
pc: 'PC',
android: 'Android',
ios: 'iOS',
miniapp: 'Mini App'
},
wf_business_status: {
revoked: 'Revoked',
draft: 'Draft',
pending: 'Pending',
completed: 'Completed',
cancelled: 'Cancelled',
returned: 'Returned',
terminated: 'Terminated'
},
wf_form_type: {
custom_form: 'Custom Form',
dynamic_form: 'Dynamic Form'
},
wf_task_status: {
revoke: 'Revoke',
pass: 'Pass',
pending_review: 'Pending Review',
cancel: 'Cancel',
return: 'Return',
terminate: 'Terminate',
transfer: 'Transfer',
delegate: 'Delegate',
copy: 'Copy',
add_sign: 'Add Sign',
minus_sign: 'Minus Sign',
timeout: 'Timeout'
}
},
page: {
login: {
common: {
title: 'Modern enterprise-level multi-tenant management system',
subTitle: 'Provides developers with a complete enterprise management solution',
loginOrRegister: 'Login / Register',
register: 'Register',
userNamePlaceholder: 'Please enter user name',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -43,3 +43,10 @@ export function fetchGetPostSelect(deptId?: CommonType.IdType, postIds?: CommonT
params: { postIds, deptId }
});
}
/** 获取部门选择框列表 */
export function fetchGetPostDeptSelect() {
return request<Api.Common.CommonTreeRecord>({
url: '/system/post/deptTree',
method: 'get'
});
}

View File

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

View File

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

View File

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

View File

@ -8,7 +8,7 @@ import { decrypt, encrypt } from '@/utils/jsencrypt';
import { getAuthorization, handleExpiredRequest, showErrorMsg } from './shared';
import type { RequestInstanceState } from './type';
const encryptHeader = 'encrypt-key';
const encryptHeader = import.meta.env.VITE_HEADER_FLAG || 'encrypt-key';
const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y';
const { baseURL } = getServiceBaseURL(import.meta.env, isHttpProxy);
@ -48,6 +48,14 @@ export const request = createFlatRequest<App.Service.Response, RequestInstanceSt
isBackendSuccess(response) {
// when the backend response code is "0000"(default), it means the request is success
// to change this logic by yourself, you can modify the `VITE_SERVICE_SUCCESS_CODE` in `.env` file
if (import.meta.env.VITE_APP_ENCRYPT === 'Y' && response.headers[encryptHeader]) {
const keyStr = response.headers[encryptHeader];
const data = String(response.data);
const base64Str = decrypt(keyStr);
const aesKey = decryptBase64(base64Str.toString());
const decryptData = decryptWithAes(data, aesKey);
response.data = JSON.parse(decryptData);
}
return String(response.data.code) === import.meta.env.VITE_SERVICE_SUCCESS_CODE;
},
async onBackendFail(response, instance) {
@ -65,33 +73,47 @@ export const request = createFlatRequest<App.Service.Response, RequestInstanceSt
request.state.errMsgStack = request.state.errMsgStack.filter(msg => msg !== response.data.msg);
}
const isLogin = Boolean(localStg.get('token'));
// when the backend response code is in `logoutCodes`, it means the user will be logged out and redirected to login page
// const logoutCodes = import.meta.env.VITE_SERVICE_LOGOUT_CODES?.split(',') || [];
// if (logoutCodes.includes(responseCode)) {
// handleLogout();
// return null;
// }
const logoutCodes = import.meta.env.VITE_SERVICE_LOGOUT_CODES?.split(',') || [];
if (logoutCodes.includes(responseCode) && !isLogin) {
logoutAndCleanup();
return null;
}
// when the backend response code is in `modalLogoutCodes`, it means the user will be logged out by displaying a modal
const modalLogoutCodes = import.meta.env.VITE_SERVICE_MODAL_LOGOUT_CODES?.split(',') || [];
if (modalLogoutCodes.includes(responseCode)) {
request.state.errMsgStack = [...(request.state.errMsgStack || []), response.data.msg];
if (modalLogoutCodes.includes(responseCode) && isLogin) {
const isExist = request.state.errMsgStack?.includes(response.data.msg);
if (isExist) {
return null;
}
if (window.location.pathname?.startsWith('/login')) {
logoutAndCleanup();
return null;
}
// prevent the user from refreshing the page
window.addEventListener('beforeunload', handleLogout);
request.state.errMsgStack = [...(request.state.errMsgStack || []), response.data.msg];
window.$dialog?.warning({
title: '系统提示',
content: '登录状态已过期,您可以继续留在该页面,或者重新登录',
content: '登录状态已过期,重新登录',
positiveText: '重新登录',
negativeText: '取消',
maskClosable: false,
closeOnEsc: false,
onAfterEnter() {
// prevent the user from refreshing the page
window.addEventListener('beforeunload', handleLogout);
},
onPositiveClick() {
logoutAndCleanup();
},
onClose() {
logoutAndCleanup();
}
});
request.cancelAllRequest();
return null;
}
@ -111,23 +133,6 @@ export const request = createFlatRequest<App.Service.Response, RequestInstanceSt
return null;
},
transformBackendResponse(response) {
if (import.meta.env.VITE_APP_ENCRYPT === 'Y') {
// 加密后的 AES 秘钥
const keyStr = response.headers[encryptHeader];
// 加密
if (keyStr && keyStr !== '') {
const data = String(response.data);
// 请求体 AES 解密
const base64Str = decrypt(keyStr);
// base64 解码 得到请求头的 AES 秘钥
const aesKey = decryptBase64(base64Str.toString());
// aesKey 解码 data
const decryptData = decryptWithAes(data, aesKey);
// 将结果 (得到的是 JSON 字符串) 转为 JSON
response.data = JSON.parse(decryptData);
}
}
// 二进制数据则直接返回
if (response.request.responseType === 'blob' || response.request.responseType === 'arraybuffer') {
return response.data;

View File

@ -8,12 +8,15 @@ import { localStg } from '@/utils/storage';
import { SetupStoreId } from '@/enum';
import { useRouteStore } from '../route';
import { useTabStore } from '../tab';
import useNoticeStore from '../notice';
import { clearAuthStorage, getToken } from './shared';
export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
const route = useRoute();
const authStore = useAuthStore();
const routeStore = useRouteStore();
const tabStore = useTabStore();
const noticeStore = useNoticeStore();
const { toLogin, redirectFromLogin } = useRouterPush(false);
const { loading: loginLoading, startLoading, endLoading } = useLoading();
@ -37,8 +40,6 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
/** Reset auth store */
async function resetStore() {
const authStore = useAuthStore();
recordUserId();
clearAuthStorage();
@ -49,6 +50,7 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
await toLogin();
}
noticeStore.clearNotice();
tabStore.cacheTabs();
routeStore.resetStore();
}

View File

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

View File

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

View File

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

View File

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

View File

@ -6,8 +6,10 @@ html,
body,
#app {
height: 100%;
font-family: inherit;
}
html {
overflow-x: hidden;
color: rgb(var(--base-text-color));
}

View File

@ -13,6 +13,13 @@
border-color: var(--un-default-border-color, #e5e7eb); /* 2 */
}
/*
* [Naive UI] Fix the icon size in the image preview toolbar
*/
.n-image-preview-toolbar .n-base-icon {
box-sizing: unset !important;
}
/*
1. Use a consistent sensible line-height in all browsers.
2. Prevent adjustments of font size after orientation changes in iOS.

View File

@ -0,0 +1,105 @@
@use 'sass:math';
$base-size: 100px;
$em-to-px: math.div($base-size, 2.5);
$loader-color-1: rgb(var(--error-color) / 75%);
$loader-color-2: rgb(var(--primary-color) / 75%);
$loader-color-3: rgb(var(--success-color) / 75%);
$loader-color-4: rgb(var(--warning-color) / 75%);
@function loader-size($em-value) {
@return $em-to-px * $em-value;
}
/* From Uiverse.io by SchawnnahJ */
.loader {
position: relative;
width: loader-size(2.5);
height: loader-size(2.5);
transform: rotate(165deg);
}
.loader:before,
.loader:after {
content: '';
position: absolute;
top: 50%;
left: 50%;
display: block;
width: loader-size(0.5);
height: loader-size(0.5);
border-radius: loader-size(0.25);
transform: translate(-50%, -50%);
}
.loader:before {
animation: before8 2s infinite;
}
.loader:after {
animation: after6 2s infinite;
}
@keyframes before8 {
0% {
width: loader-size(0.5);
box-shadow:
loader-size(1) loader-size(-0.5) $loader-color-1,
loader-size(-1) loader-size(0.5) $loader-color-2;
}
35% {
width: loader-size(2.5);
box-shadow:
0 loader-size(-0.5) $loader-color-1,
0 loader-size(0.5) $loader-color-2;
}
70% {
width: loader-size(0.5);
box-shadow:
loader-size(-1) loader-size(-0.5) $loader-color-1,
loader-size(1) loader-size(0.5) $loader-color-2;
}
100% {
box-shadow:
loader-size(1) loader-size(-0.5) $loader-color-1,
loader-size(-1) loader-size(0.5) $loader-color-2;
}
}
@keyframes after6 {
0% {
height: loader-size(0.5);
box-shadow:
loader-size(0.5) loader-size(1) $loader-color-3,
loader-size(-0.5) loader-size(-1) $loader-color-4;
}
35% {
height: loader-size(2.5);
box-shadow:
loader-size(0.5) 0 $loader-color-3,
loader-size(-0.5) 0 $loader-color-4;
}
70% {
height: loader-size(0.5);
box-shadow:
loader-size(0.5) loader-size(-1) $loader-color-3,
loader-size(-0.5) loader-size(1) $loader-color-4;
}
100% {
box-shadow:
loader-size(0.5) loader-size(1) $loader-color-3,
loader-size(-0.5) loader-size(-1) $loader-color-4;
}
}
.loader {
position: absolute;
top: calc(50% - #{math.div(loader-size(2.5), 2)});
left: calc(50% - #{math.div(loader-size(2.5), 2)});
}

View File

@ -44,7 +44,7 @@ export const themeSettings: App.Theme.ThemeSetting = {
fixedHeaderAndTab: true,
sider: {
inverted: false,
width: 220,
width: 230,
collapsedWidth: 64,
mixWidth: 90,
mixCollapsedWidth: 64,
@ -58,7 +58,8 @@ export const themeSettings: App.Theme.ThemeSetting = {
},
watermark: {
visible: import.meta.env.VITE_WATERMARK === 'Y',
text: 'RuoYi-Vue-Plus'
text: 'RuoYi-Vue-Plus',
enableUserName: false
},
table: {
bordered: true,

View File

@ -152,9 +152,10 @@ declare namespace Api {
type UserProfileOperateParams = CommonType.RecordNullable<Pick<User, 'nickName' | 'email' | 'phonenumber' | 'sex'>>;
/** user password operate params */
type UserPasswordOperateParams = CommonType.RecordNullable<
Pick<User, 'userId' | 'password'> & { newPassword: string }
>;
type UserPasswordOperateParams = CommonType.RecordNullable<{
oldPassword: string;
newPassword: string;
}>;
/** user info */
type UserInfo = {
@ -162,6 +163,8 @@ declare namespace Api {
postIds: string[];
/** user role ids */
roleIds: string[];
/** roles */
roles: Role[];
};
/** user list */
@ -372,7 +375,15 @@ declare namespace Api {
type DictDataOperateParams = CommonType.RecordNullable<
Pick<
Api.System.DictData,
'dictCode' | 'dictSort' | 'dictLabel' | 'dictValue' | 'dictType' | 'cssClass' | 'listClass' | 'remark'
| 'dictCode'
| 'dictSort'
| 'dictLabel'
| 'dictValue'
| 'dictType'
| 'cssClass'
| 'listClass'
| 'isDefault'
| 'remark'
>
>;

View File

@ -112,6 +112,8 @@ declare namespace App {
visible: boolean;
/** Watermark text */
text: string;
/** Whether to use user name as watermark text */
enableUserName: boolean;
};
table: {
/** Whether to show the table border */
@ -374,6 +376,7 @@ declare namespace App {
update: string;
updateSuccess: string;
saveSuccess: string;
noChange: string;
userCenter: string;
yesOrNo: {
yes: string;
@ -446,6 +449,7 @@ declare namespace App {
watermark: {
visible: string;
text: string;
enableUserName: string;
};
tablePropsTitle: string;
table: {
@ -467,6 +471,8 @@ declare namespace App {
};
};
route: Record<I18nRouteKey, string>;
menu: Record<string, string>;
dict: Record<string, Record<string, string>>;
page: {
common: {
id: string;
@ -481,6 +487,8 @@ declare namespace App {
};
login: {
common: {
title: string;
subTitle: string;
loginOrRegister: string;
register: string;
userNamePlaceholder: string;

View File

@ -34,6 +34,7 @@ declare module 'vue' {
IconIcRoundRefresh: typeof import('~icons/ic/round-refresh')['default']
IconIcRoundSearch: typeof import('~icons/ic/round-search')['default']
IconLocalBanner: typeof import('~icons/local/banner')['default']
IconLocalLogo: typeof import('~icons/local/logo')['default']
'IconMaterialSymbols:add': typeof import('~icons/material-symbols/add')['default']
'IconMaterialSymbols:deleteOutline': typeof import('~icons/material-symbols/delete-outline')['default']
'IconMaterialSymbols:downloadRounded': typeof import('~icons/material-symbols/download-rounded')['default']
@ -45,6 +46,7 @@ declare module 'vue' {
IconMaterialSymbolsAddRounded: typeof import('~icons/material-symbols/add-rounded')['default']
IconMaterialSymbolsDeleteOutline: typeof import('~icons/material-symbols/delete-outline')['default']
IconMaterialSymbolsDriveFileRenameOutlineOutline: typeof import('~icons/material-symbols/drive-file-rename-outline-outline')['default']
'IconMdi:github': typeof import('~icons/mdi/github')['default']
IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin')['default']
IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin')['default']
IconMdiDrag: typeof import('~icons/mdi/drag')['default']
@ -52,6 +54,7 @@ declare module 'vue' {
IconMdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default']
'IconQuill:collapse': typeof import('~icons/quill/collapse')['default']
'IconQuill:expand': typeof import('~icons/quill/expand')['default']
'IconSimpleIcons:gitee': typeof import('~icons/simple-icons/gitee')['default']
IconUilSearch: typeof import('~icons/uil/search')['default']
JsonPreview: typeof import('./../components/custom/json-preview.vue')['default']
LangSwitch: typeof import('./../components/common/lang-switch.vue')['default']
@ -129,6 +132,7 @@ declare module 'vue' {
NUpload: typeof import('naive-ui')['NUpload']
NUploadDragger: typeof import('naive-ui')['NUploadDragger']
NWatermark: typeof import('naive-ui')['NWatermark']
OssUpload: typeof import('./../components/custom/oss-upload.vue')['default']
PinToggler: typeof import('./../components/common/pin-toggler.vue')['default']
PostSelect: typeof import('./../components/custom/post-select.vue')['default']
ReloadButton: typeof import('./../components/common/reload-button.vue')['default']
@ -145,6 +149,7 @@ declare module 'vue' {
TableSiderLayout: typeof import('./../components/advanced/table-sider-layout.vue')['default']
TenantSelect: typeof import('./../components/custom/tenant-select.vue')['default']
ThemeSchemaSwitch: typeof import('./../components/common/theme-schema-switch.vue')['default']
TinymceEditor: typeof import('./../components/custom/tinymce-editor.vue')['default']
UserSelect: typeof import('./../components/custom/user-select.vue')['default']
WaveBg: typeof import('./../components/custom/wave-bg.vue')['default']
}

View File

@ -25,7 +25,7 @@ declare namespace Env {
*
* This prefix is start with the icon prefix
*/
readonly VITE_ICON_LOCAL_PREFIX: 'local-icon';
readonly VITE_ICON_LOCAL_PREFIX: 'icon-local';
/** backend service base url */
readonly VITE_SERVICE_BASE_URL: string;
/**
@ -114,6 +114,7 @@ declare namespace Env {
readonly VITE_DEVTOOLS_LAUNCH_EDITOR?: import('vite-plugin-vue-devtools').VitePluginVueDevToolsOptions['launchEditor'];
readonly VITE_APP_CLIENT_ID?: string;
readonly VITE_APP_ENCRYPT?: CommonType.YesOrNo;
readonly VITE_HEADER_FLAG?: string;
readonly VITE_APP_RSA_PUBLIC_KEY?: string;
readonly VITE_APP_RSA_PRIVATE_KEY?: string;
readonly VITE_APP_WEBSOCKET: CommonType.YesOrNo;

View File

@ -1,3 +1,4 @@
import { AcceptType } from '@/enum/business';
import { $t } from '@/locales';
/**
* Transform record to option
@ -77,18 +78,17 @@ export function humpToLine(str: string, line: string = '-') {
/** 判断是否为空 */
export function isNotNull(value: any) {
return value !== undefined && value !== null && value !== '' && value !== 'undefined' && value !== 'null';
return value !== undefined && value !== null && value !== '';
}
/** 判断是否为空 */
export function isNull(value: any) {
return value === undefined || value === null || value === '' || value === 'undefined' || value === 'null';
return value === undefined || value === null || value === '';
}
/** 判断是否为图片类型 */
export function isImage(suffix: string) {
const imgSuffixList = ['.jpg', '.jpeg', '.png', '.gif', '.webp'];
return imgSuffixList.includes(suffix.toLowerCase());
return AcceptType.Image.split(',').includes(suffix.toLowerCase());
}
/**
@ -163,3 +163,41 @@ export const handleTree = <T extends Record<string, any>>(data: T[], config: Com
return tree;
};
/**
* 将对象转换为 URLSearchParams
*
* @param obj
*/
export function transformToURLSearchParams(obj: Record<string, any>, excludeKeys: string[] = []) {
const searchParams = new URLSearchParams();
if (!isNotNull(obj)) {
return searchParams;
}
Object.entries(obj).forEach(([key, value]) => {
if (excludeKeys.includes(key)) {
return;
}
if (typeof value === 'object') {
transformToURLSearchParams(value).forEach((v, k) => {
searchParams.append(`${key}[${k}]`, v);
});
return;
}
if (!isNotNull(value)) {
return;
}
searchParams.append(key, value);
});
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
View File

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

View File

@ -1,6 +1,7 @@
import { watch } from 'vue';
import { useEventSource } from '@vueuse/core';
import useNoticeStore from '@/store/modules/notice';
import { $t } from '@/locales';
import { localStg } from './storage';
// 初始化
@ -34,9 +35,14 @@ export const initSSE = (url: any) => {
read: false,
time: new Date().toLocaleString()
});
let content = data.value;
const noticeType = content.match(/\[dict\.(.*?)\]/)?.[1];
if (noticeType) {
content = content.replace(`dict.${noticeType}`, $t(`dict.${noticeType}` as App.I18n.I18nKey));
}
window.$notification?.create({
title: '消息',
content: data.value,
content,
type: 'success',
duration: 3000
});

View File

@ -1,21 +1,9 @@
<script setup lang="ts">
import { onActivated, onMounted } from 'vue';
interface Props {
url: string;
}
defineProps<Props>();
onMounted(() => {
// eslint-disable-next-line no-console
console.log('mounted');
});
onActivated(() => {
// eslint-disable-next-line no-console
console.log('activated');
});
</script>
<template>

View File

@ -1,10 +1,10 @@
<script setup lang="ts">
import { computed } from 'vue';
import type { Component } from 'vue';
import { getPaletteColorByNumber, mixColor } from '@sa/color';
import { loginModuleRecord } from '@/constants/app';
import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme';
import loginBackground from '@/assets/svg-icon/login-background.svg';
import { $t } from '@/locales';
import PwdLogin from './modules/pwd-login.vue';
import CodeLogin from './modules/code-login.vue';
@ -36,55 +36,72 @@ const moduleMap: Record<UnionKey.LoginModule, LoginModule> = {
};
const activeModule = computed(() => moduleMap[props.module || 'pwd-login']);
const bgThemeColor = computed(() =>
themeStore.darkMode ? getPaletteColorByNumber(themeStore.themeColor, 600) : themeStore.themeColor
);
const bgColor = computed(() => {
const COLOR_WHITE = '#ffffff';
const ratio = themeStore.darkMode ? 0.5 : 0.2;
return mixColor(COLOR_WHITE, themeStore.themeColor, ratio);
});
</script>
<template>
<div class="relative size-full flex-center overflow-hidden" :style="{ backgroundColor: bgColor }">
<WaveBg :theme-color="bgThemeColor" />
<NCard :bordered="false" class="relative z-4 w-auto rd-12px">
<div class="w-400px lt-sm:w-300px">
<header class="flex-y-center justify-between">
<SystemLogo class="w-64px text-primary lt-sm:text-48px" />
<h3 class="text-28px text-primary font-500 lt-sm:text-22px">{{ $t('system.title') }}</h3>
<div class="i-flex-col">
<ThemeSchemaSwitch
:theme-schema="themeStore.themeScheme"
:show-tooltip="false"
class="text-20px lt-sm:text-18px"
@switch="themeStore.toggleThemeScheme"
/>
<LangSwitch
v-if="themeStore.header.multilingual.visible"
:lang="appStore.locale"
:lang-options="appStore.localeOptions"
:show-tooltip="false"
@change-lang="appStore.changeLocale"
/>
</div>
</header>
<main class="pt-24px">
<h3 class="text-18px text-primary font-medium">{{ $t(activeModule.label) }}</h3>
<div class="pt-24px">
<Transition :name="themeStore.page.animateMode" mode="out-in" appear>
<component :is="activeModule.component" />
</Transition>
</div>
</main>
<div class="scroll box-border size-full flex">
<div class="relative box-border hidden h-full w-65vw overflow-hidden bg-primary-50 xl:block dark:bg-primary-900">
<div class="relative z-100 flex items-center pl-30px pt-30px">
<SystemLogo class="text-32px text-primary" />
<h3 class="ml-10px text-20px font-400">{{ $t('system.title') }}</h3>
</div>
</NCard>
<div class="absolute inset-x-0 inset-b-10.5% inset-t-0 z-10 m-auto w-40%">
<img class="size-full" :src="loginBackground" />
</div>
<div class="absolute bottom-80px w-full text-center">
<h1 class="text-24px font-400">{{ $t('page.login.common.title') }}</h1>
<p class="mt-8px text-14px color-gray-500">{{ $t('page.login.common.subTitle') }}</p>
</div>
<WaveBg />
</div>
<div class="relative h-full flex-1 xl:m-auto sm:!w-full">
<header class="flex-y-center justify-between px-30px pt-30px xl:justify-end">
<div class="relative z-100 block flex items-center xl:hidden">
<SystemLogo class="text-32px text-primary" />
<h3 class="ml-10px text-20px font-400">{{ $t('system.title') }}</h3>
</div>
<div class="flex items-center justify-end">
<ThemeSchemaSwitch
:theme-schema="themeStore.themeScheme"
:show-tooltip="false"
class="text-20px lt-sm:text-18px"
@switch="themeStore.toggleThemeScheme"
/>
<LangSwitch
v-if="themeStore.header.multilingual.visible"
:lang="appStore.locale"
:lang-options="appStore.localeOptions"
:show-tooltip="false"
class="text-20px lt-sm:text-18px"
@change-lang="appStore.changeLocale"
/>
</div>
</header>
<main
class="m-auto mt-10% h-630px max-w-450px w-full rounded-5px bg-cover px-24px xl:absolute xl:inset-0 lg:mt-15% xl:mt-auto"
>
<Transition :name="themeStore.page.animateMode" mode="out-in" appear>
<component :is="activeModule.component" />
</Transition>
</main>
</div>
</div>
</template>
<style scoped></style>
<style scoped>
.scroll {
overflow: auto;
}
.scroll::-webkit-scrollbar {
display: none;
}
.scroll {
-ms-overflow-style: none;
}
.scroll {
scrollbar-width: none;
}
</style>

View File

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

View File

@ -53,12 +53,14 @@ async function handleFetchTenantList() {
const { data, error } = await fetchTenantList();
if (error) return;
tenantEnabled.value = data.tenantEnabled;
tenantOption.value = data.voList.map(tenant => {
return {
label: tenant.companyName,
value: tenant.tenantId
};
});
if (data.tenantEnabled) {
tenantOption.value = data.voList.map(tenant => {
return {
label: tenant.companyName,
value: tenant.tenantId
};
});
}
endTenantLoading();
}
@ -121,63 +123,89 @@ async function handleSocialLogin(type: Api.System.SocialSource) {
</script>
<template>
<NForm
ref="formRef"
:model="model"
:rules="rules"
size="large"
:show-label="false"
@keyup.enter="() => !authStore.loginLoading && handleSubmit()"
>
<NFormItem v-if="tenantEnabled" path="tenantId">
<NSelect
v-model:value="model.tenantId"
placeholder="请选择租户"
:options="tenantOption"
:loading="tenantLoading"
/>
</NFormItem>
<NFormItem path="username">
<NInput v-model:value="model.username" :placeholder="$t('page.login.common.userNamePlaceholder')" />
</NFormItem>
<NFormItem path="password">
<NInput
v-model:value="model.password"
type="password"
show-password-on="click"
:placeholder="$t('page.login.common.passwordPlaceholder')"
/>
</NFormItem>
<NFormItem v-if="captchaEnabled" path="code">
<div class="w-full flex-y-center gap-16px">
<NInput v-model:value="model.code" :placeholder="$t('page.login.common.codePlaceholder')" />
<NSpin :show="codeLoading" :size="28" class="h-42px">
<NButton :focusable="false" class="login-code h-42px w-116px" @click="handleFetchCaptchaCode">
<img v-if="codeUrl" :src="codeUrl" />
<NEmpty v-else :show-icon="false" description="暂无验证码" />
</NButton>
</NSpin>
</div>
</NFormItem>
<NSpace vertical :size="16" class="mb-8px">
<div class="mx-6px flex-y-center justify-between">
<NCheckbox v-model:checked="remberMe">{{ $t('page.login.pwdLogin.rememberMe') }}</NCheckbox>
<NSpace :size="1">
<ButtonIcon class="color-#44b549" icon="ic:outline-wechat" @click="handleSocialLogin('wechat_open')" />
<ButtonIcon local-icon="topiam" @click="handleSocialLogin('topiam')" />
<ButtonIcon local-icon="maxkey" @click="handleSocialLogin('maxkey')" />
<ButtonIcon class="color-#c71d23" icon="simple-icons:gitee" @click="handleSocialLogin('gitee')" />
<ButtonIcon class="color-#010409" icon="mdi:github" @click="handleSocialLogin('github')" />
</NSpace>
</div>
<NButton type="primary" size="large" block :loading="authStore.loginLoading" @click="handleSubmit">
{{ $t('common.login') }}
<div>
<div class="mb-5px text-32px text-black font-600 dark:text-white">登录到您的账户</div>
<div class="pb-18px text-16px text-#858585">欢迎回来请输入您的账户信息</div>
<NForm
ref="formRef"
:model="model"
:rules="rules"
size="large"
:show-label="false"
@keyup.enter="() => !authStore.loginLoading && handleSubmit()"
>
<NFormItem v-if="tenantEnabled" path="tenantId">
<NSelect
v-model:value="model.tenantId"
placeholder="请选择租户"
:options="tenantOption"
:loading="tenantLoading"
/>
</NFormItem>
<NFormItem path="username">
<NInput v-model:value="model.username" :placeholder="$t('page.login.common.userNamePlaceholder')" />
</NFormItem>
<NFormItem path="password">
<NInput
v-model:value="model.password"
type="password"
show-password-on="click"
:placeholder="$t('page.login.common.passwordPlaceholder')"
/>
</NFormItem>
<NFormItem v-if="captchaEnabled" path="code">
<div class="w-full flex-y-center gap-16px">
<NInput v-model:value="model.code" :placeholder="$t('page.login.common.codePlaceholder')" />
<NSpin :show="codeLoading" :size="28" class="h-42px">
<NButton :focusable="false" class="login-code h-42px w-136px" @click="handleFetchCaptchaCode">
<img v-if="codeUrl" :src="codeUrl" />
<NEmpty v-else :show-icon="false" description="暂无验证码" />
</NButton>
</NSpin>
</div>
</NFormItem>
<NSpace vertical :size="12" class="mb-8px">
<div class="mx-6px mb-8px flex-y-center justify-between">
<NCheckbox v-model:checked="remberMe" size="large">{{ $t('page.login.pwdLogin.rememberMe') }}</NCheckbox>
<NA type="primary" class="text-18px" @click="toggleLoginModule('reset-pwd')">
{{ $t('page.login.pwdLogin.forgetPassword') }}
</NA>
</div>
<NButton type="primary" size="large" block :loading="authStore.loginLoading" @click="handleSubmit">
{{ $t('common.login') }}
</NButton>
<NButton v-if="registerEnabled" size="large" block @click="toggleLoginModule('register')">
{{ $t('page.login.common.register') }}
</NButton>
</NSpace>
</NForm>
<NDivider>
<div class="color-#858585">{{ $t('page.login.pwdLogin.otherAccountLogin') }}</div>
</NDivider>
<div class="w-full flex-y-center gap-16px">
<NButton class="flex-1" @click="handleSocialLogin('gitee')">
<template #icon>
<icon-simple-icons:gitee class="color-#c71d23" />
</template>
<span class="ml-6px">Gitee</span>
</NButton>
<NButton v-if="registerEnabled" size="large" block @click="toggleLoginModule('register')">
<NButton class="flex-1" @click="handleSocialLogin('github')">
<template #icon>
<icon-mdi:github class="color-#010409" />
</template>
<span class="ml-6px">GitHub</span>
</NButton>
</div>
<div class="mt-24px w-full text-center text-18px text-#858585">
您还没有账户
<NA type="primary" class="text-18px" @click="toggleLoginModule('register')">
{{ $t('page.login.common.register') }}
</NButton>
</NSpace>
</NForm>
</NA>
</div>
</div>
</template>
<style scoped>
@ -188,7 +216,34 @@ async function handleSocialLogin(type: Api.System.SocialSource) {
}
img {
height: 40px;
height: 42px;
}
}
: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-checkbox) {
--n-size: 18px !important;
--n-font-size: 16px !important;
}
:deep(.n-button) {
--n-height: 42px !important;
--n-font-size: 18px !important;
--n-border-radius: 8px !important;
}
:deep(.n-divider) {
--n-font-size: 16px !important;
--n-font-weight: 400 !important;
}
</style>

View File

@ -103,56 +103,64 @@ handleFetchCaptchaCode();
</script>
<template>
<NForm
ref="formRef"
:model="model"
:rules="rules"
size="large"
:show-label="false"
@keyup.enter="() => !registerLoading && handleSubmit()"
>
<NFormItem v-if="tenantEnabled" path="tenantId">
<NSelect v-model:value="model.tenantId" :options="tenantOption" :enabled="tenantEnabled" />
</NFormItem>
<NFormItem path="username">
<NInput v-model:value="model.username" :placeholder="$t('page.login.common.userNamePlaceholder')" />
</NFormItem>
<NFormItem path="password">
<NInput
v-model:value="model.password"
type="password"
show-password-on="click"
:placeholder="$t('page.login.common.passwordPlaceholder')"
/>
</NFormItem>
<NFormItem path="confirmPassword">
<NInput
v-model:value="model.confirmPassword"
type="password"
show-password-on="click"
:placeholder="$t('page.login.common.confirmPasswordPlaceholder')"
/>
</NFormItem>
<NFormItem v-if="captchaEnabled" path="code">
<div class="w-full flex-y-center gap-16px">
<NInput v-model:value="model.code" :placeholder="$t('page.login.common.codePlaceholder')" />
<NSpin :show="codeLoading" :size="28" class="h-42px">
<NButton :focusable="false" class="login-code h-42px w-116px" @click="handleFetchCaptchaCode">
<img v-if="codeUrl" :src="codeUrl" />
<NEmpty v-else :show-icon="false" description="暂无验证码" />
</NButton>
</NSpin>
</div>
</NFormItem>
<NSpace vertical :size="18" class="w-full">
<NButton type="primary" size="large" block :loading="registerLoading" @click="handleSubmit">
{{ $t('page.login.common.register') }}
</NButton>
<NButton size="large" block @click="toggleLoginModule('pwd-login')">
{{ $t('page.login.common.back') }}
</NButton>
</NSpace>
</NForm>
<div>
<div class="mb-5px text-32px text-black font-600 sm:text-30px dark:text-white">注册新账户</div>
<div class="pb-18px text-16px text-#858585">欢迎注册请输入您的账户信息</div>
<NForm
ref="formRef"
:model="model"
:rules="rules"
size="large"
:show-label="false"
@keyup.enter="() => !registerLoading && handleSubmit()"
>
<NFormItem v-if="tenantEnabled" path="tenantId">
<NSelect v-model:value="model.tenantId" :options="tenantOption" :enabled="tenantEnabled" />
</NFormItem>
<NFormItem path="username">
<NInput v-model:value="model.username" :placeholder="$t('page.login.common.userNamePlaceholder')" />
</NFormItem>
<NFormItem path="password">
<NInput
v-model:value="model.password"
type="password"
show-password-on="click"
:placeholder="$t('page.login.common.passwordPlaceholder')"
/>
</NFormItem>
<NFormItem path="confirmPassword">
<NInput
v-model:value="model.confirmPassword"
type="password"
show-password-on="click"
:placeholder="$t('page.login.common.confirmPasswordPlaceholder')"
/>
</NFormItem>
<NFormItem v-if="captchaEnabled" path="code">
<div class="w-full flex-y-center gap-16px">
<NInput v-model:value="model.code" :placeholder="$t('page.login.common.codePlaceholder')" />
<NSpin :show="codeLoading" :size="28" class="h-52px">
<NButton :focusable="false" class="login-code h-52px w-136px" @click="handleFetchCaptchaCode">
<img v-if="codeUrl" :src="codeUrl" />
<NEmpty v-else :show-icon="false" description="暂无验证码" />
</NButton>
</NSpin>
</div>
</NFormItem>
<NSpace vertical :size="18" class="w-full">
<NButton type="primary" size="large" block :loading="registerLoading" @click="handleSubmit">
{{ $t('page.login.common.register') }}
</NButton>
</NSpace>
</NForm>
<div class="mt-24px w-full text-center text-18px text-#858585">
您已有账户
<NA type="primary" class="text-18px" @click="toggleLoginModule('pwd-login')">
{{ $t('common.login') }}
</NA>
</div>
</div>
</template>
<style scoped lang="scss">
@ -166,4 +174,21 @@ handleFetchCaptchaCode();
height: 40px;
}
}
: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>

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