Compare commits

..
110 Commits
Author SHA1 Message Date
insistence 3ecfa3c905 docs: 更新README文档 2026-08-01 20:07:13 +08:00
insistence 90311e2112 chore: 发布1.10.0版本 2026-08-01 20:01:41 +08:00
insistence 45b6b28b2d docs: 新增1.10.0版本更新日志 2026-08-01 19:39:00 +08:00
insistence ca96eafa13 chore: 升级依赖 (#119)
* chore: 升级依赖

* fix: 修复后端测试异常的问题
2026-08-01 18:39:08 +08:00
insistence 768bc15a8b perf: 优化jwt解析异常处理机制 (#118)
* perf: 优化jwt解析异常处理机制

* fix: 修复后端测试在windows下失败的问题

* chore: 新增后端测试ci

* chore: 更新后端测试ci

* chore: 更新后端测试ci
2026-07-31 21:56:40 +08:00
insistence 13143f41b2 feat: 同步ruoyi最新代码 (#117)
* perf: 优化代码

* perf: 优化字典类型属性提醒说明

* perf: 优化顶部导航更多菜单路由截取

* perf: 优化RightToolbar搜索栏切换动画

* perf: 优化操作日志详细页面

* feat: 新增锁定屏幕功能

* feat: 字典类型列表新增抽屉效果详细信息

* perf: 优化页签功能&支持全屏按钮操作

* perf: 优化tag全屏为页签内区域

* perf: 菜单搜索支持文本高亮&数量提示

* feat: 添加持久化标签页开关功能

* perf: 优化页签显示

* perf: 优化菜单主题风格

* perf: 黑暗模式下全屏不需要设置背景

* perf: 暗黑浅色主题优化

* perf: 优化快速点击页签刷新出现404问题

* feat: 支持表格列显隐状态记忆

* feat: 代码生成支持表单布局选项

* feat: 新增树分割组件TreePanel

* feat: 新增Excel导入组件ExcelImportDialog

* perf: 优化样式

* feat: 用户列表新增抽屉效果详细信息

* perf: 代码生成修改拖拽时显示手指样式

* perf: 优化登录页暗黑模式下样式

* feat: 新增标签页样式chrome风格

* perf: 优化白名单支持对通配符路径匹配

* feat: 新增菜单路由地址和名称的校验规则

* feat: 首页新增通知公告消息提醒

* perf&feat: 优化定时任务详情页展示&补充执行时间字段

* feat: 部门管理支持批量保存排序

* feat: 菜单管理支持批量保存排序

* perf: 优化点击任务名称查看详细

* feat: 菜单管理列表新增类型显示

* feat: 新增代码生成详情页功能

* perf: 优化样式

* feat: 通知公告新增详细显示

* feat: 通知公告新增阅读用户列表

* perf: 更换router4过期方法next

* perf: 优化暗黑模式下主题样式

* perf: 优化暗黑模式详情样式

* feat: 用户密码支持自定义配置规则

* fix: 修复E2E测试失败的问题
2026-07-31 19:22:06 +08:00
insistence 535fc29e9a feat: 新增自动生成jwt密钥 (#116) 2026-07-29 12:02:23 +08:00
insistence 59bfb6dbb7 perf: 补充演示模式限制路由 (#115) 2026-07-29 11:49:59 +08:00
insistence 4040223be3 fix: 修复tui卡顿的问题 (#114) 2026-07-29 10:45:46 +08:00
insistence c9dcdf32ac docs: 更新README文档 2026-07-28 21:54:26 +08:00
insistence b9b34b03c9 fix: 修复windows下应用启动日志异常的问题 (#113) 2026-07-28 21:42:59 +08:00
insistence 2a055ba648 feat: 新增插件系统 (#112)
* feat: 初始化插件系统

* refactor: 收口插件系统运行时重构

* perf: 优化插件系统类型提示

* fix&perf: 修复和优化插件系统

* fix: 修复gitignore规则误忽略插件文件的问题

* fix: 修复运行时插件根路径算错的问题

* fix: 加强插件发现和路由注册的防护措施

* revert: 回滚定时任务白名单

* fix: 移除未使用的应用路由注册探测

* revert: 恢复部分代码

* perf: 优化插件系统

* docs: 新增插件开发文档

* perf: 优化插件管理模块

* perf: 提升插件系统核心能力

* refactor: 重构生命周期 step runner

* fix: 修复lint错误

* test: 清理测试用例

* test: 调整测试目录名称

* fix: 修复前后端目录硬编码的问题

* fix: 修复插件系统安全性缺口

* refactor: 重新设计插件生命周期 Migration 事务与回滚

* perf: 优化插件系统边界问题

* refactor: 重构当前插件系统的依赖体系设计

* perf: 优化代码

* perf: 优化代码

* fix: 修复代码合并问题

* fix: 修复bug

* perf: 优化代码

* perf&fix: 优化代码和修复bug

* docs: 优化文档格式

* feat: 适配Vue2版本

* docs: 更新README文档

* fix: 修复ruff lint错误

* chore: 更新后端依赖文件
2026-07-28 20:35:18 +08:00
insistence 25acc9427c feat: 新增文件管理功能并增强文件服务安全性 (#111)
* feat: 新增文件管理功能,提升文件服务安全性

* docs: 新增文件管理使用文档

* docs: 更新README文档

* feat: 新增业务附件组件

* feat: 新增文件存储对账中心

* docs: 更新README文档

* feat: 新增到期文件处置功能

* perf: 完善上传人权限显式化

* feat: 新增Range 与断点下载

* docs: 更新README文档
2026-07-26 19:21:09 +08:00
insistence 7fd21e050e fix: 修复日志输出格式问题 (#104) 2026-05-19 09:26:47 +08:00
insistence 9f7a0daa3b feat: 新增cli系统 (#103)
* feat: 新增cli系统

* feat: ruoyi completion支持powershell

* perf: 优化tui显示

* fix: 修复前端构建异常
2026-05-13 11:19:31 +08:00
insistence 3c77f4f088 feat: 新增日志脱敏功能 (#101)
* feat: 新增日志脱敏功能

* perf: 优化日志入库格式
2026-04-22 18:16:08 +08:00
insistence 6687cff93d feat: 新增传输加密功能 (#100)
* feat: 新增传输加密功能

* docs: 更新README文档

* perf: 优化后端dev默认配置

* test: 优化测试稳定性

* test: 优化测试

* test: 优化测试

* revert: 回滚测试修改

* perf: 测试时关闭传输加密以提高稳定性
2026-04-16 18:35:16 +08:00
insistence 17e7f56fe5 feat: 新增接口限流装饰器 (#99)
* feat: 新增接口限流装饰器

* perf: 优化接口缓存装饰器和接口缓存失效装饰器

* chore: 更新github actions规则
2026-04-09 14:29:16 +08:00
insistence 11912cf993 feat: 新增ApiCache接口缓存装饰器和ApiCacheEvict接口缓存失效装饰器 (#97)
* feat: 新增ApiCache接口缓存装饰器和ApiCacheEvict接口缓存失效装饰器

* fix: 修复ruff lint异常
2026-04-07 17:40:56 +08:00
insistence 6dd08421b6 chore: 发布1.9.0版本 2026-02-13 16:50:52 +08:00
insistence a45594a4bc docs: 新增1.9.0版本更新日志 2026-02-13 16:50:23 +08:00
insistence d0127ca07c chore: 升级依赖 2026-02-13 16:47:36 +08:00
insistence b342b24aa5 perf: 优化ci测试稳定性 (#90) 2026-02-13 16:43:23 +08:00
董煜andsmock 76343f5838 perf: 优化验证码计算结果为非负数 (#88)
* 非负数结果的验证码更友好一些

* 增加手动导入路由、fix bug

* Revert "增加手动导入路由、fix bug"

This reverts commit 9d3eb66741.

---------

Co-authored-by: smock <smockg@gmail.com>
2026-02-13 11:32:38 +08:00
insistence 37aace8a87 perf: 优化防重提交间隔时间可自定义 (#87) 2026-02-13 10:22:49 +08:00
insistence be3744a0d1 perf: 优化热重载模式或单worker下scheduler的任务状态同步机制 (#85) 2026-02-12 16:36:06 +08:00
insistence babaf53237 perf: 优化代码 2026-02-12 10:46:37 +08:00
insistence 81019adff4 fix: 修复create_app重复执行的问题 (#84) 2026-02-12 09:58:05 +08:00
insistence 1108e1b321 perf: 优化暗黑模式切换效果 (#83)
* perf: 优化暗黑模式切换效果

* fix: 修复登录页和首页暗黑模式适配问题
2026-02-11 17:57:52 +08:00
小云andinsistence f41aa192fb perf: 优化项目启动速度 (#82)
* optimize_startup_speed

* rollback: 恢复导入策略

* perf: 优化路由自动注册目录扫描

* perf: 优化DNS探测

* refactor: 重构路由自动注册查找逻辑

* perf: 优化scheduler的executors为延迟创建

* perf: 优化AI SDK为懒加载

---------

Co-authored-by: insistence <3055204202@qq.com>
2026-02-11 17:57:10 +08:00
insistence 2c175b2324 perf: 优化代码 2026-02-06 17:34:59 +08:00
insistence b8be5859b1 perf: 优化部分页面字段tooltip说明 (#80)
* perf: 优化部分页面字段tooltip说明

* perf: 优化描述
2026-02-06 16:46:32 +08:00
insistence 58b2edc97b perf: 自动注册路由出错时抛出异常以便于调试 (#79) 2026-02-06 16:25:06 +08:00
insistence 200a6bf219 feat: 应用新增演示模式 (#78) 2026-02-06 15:49:47 +08:00
insistence f70644e497 chore: 升级项目依赖 (#77)
* chore: 升级app依赖

* docs: 修复app的README语法警告

* chore: 升级前端依赖

* chore: 升级后端依赖
2026-02-06 11:44:30 +08:00
insistence 5cd6d4788a feat: 新增多worker运行支持 (#76)
* feat: 定时任务新增redis分布式锁以在多worker下正常运行

* refactor: 重构日志系统以支持多worker

* perf: 增强scheduler在多worker下的调度机制

* perf: 增强锁丢失调度逻辑与异常容错

* perf: 优化trace中间件和日志打印显示

* chore: 更新.gitignore规则

* perf: 优化database的engine和session创建方法

* perf: 优化scheduler内部的sqlalchemy日志显示

* perf: 优化scheduler和应用生命周期

* fix: 修复重新获取锁时重复调用的问题

* perf: 优化函数无用参数

* perf: 优化env配置

* fix: 修复lint错误
2026-02-06 09:07:46 +08:00
insistence 4810ffee11 fix: 修复无法直接通过后端地址访问API文档的问题 (#74)
* fix: 修复无法直接通过后端地址访问API文档的问题

* feat: 新增server_util

* refactor: 重构自定义API文档路由逻辑
2026-01-31 19:38:54 +08:00
insistence 03fc133850 feat: 新增移动端模块 (#73) 2026-01-29 17:09:19 +08:00
insistence e17c4132bc perf: 优化代码生成后端模板 (#72) 2026-01-27 10:00:12 +08:00
insistence 5c51db1dfe fix: 修复登录接口response_model声明错误 (#71)
* fix: 修复登录接口response_model声明错误

* perf: 替换已弃用的regex参数
2026-01-27 09:18:08 +08:00
insistence 58b14c385c chore: 升级依赖 (#70)
* chore: 升级依赖

* chore: ci中新增python3.13测试
2026-01-26 17:56:10 +08:00
insistence 56036d6c00 perf: 优化AI管理模块 2026-01-26 16:22:19 +08:00
insistence daef6594b6 fix: 更新sql文件与do对应 2026-01-23 10:31:20 +08:00
insistence 76943141f7 feat: 新增AI管理模块 (#69)
* feat: 新增AI管理模块

* perf: 优化ai_chat_controller的order_num

* feat: AI模型管理新增数据权限

* feat: 新增加解密工具类

* feat: 为模型api_key新增加解密功能,提升安全性
2026-01-23 10:16:38 +08:00
insistence 7d54be028a perf: 优化alembic处理表模型逻辑,避免无关表影响 (#68)
* perf: 优化alembic处理表模型逻辑,避免无关表影响

* perf: 移除无用的ApschedulerJobs模型
2026-01-20 15:44:29 +08:00
insistence f5b2bf5adc chore: 移除对python3.9的支持 (#67)
* chore: 移除对python3.9的支持

* chore: ci移除python3.9测试
2026-01-20 15:15:33 +08:00
richardgong1987 f9994a5ce8 fix: 修复代码生成controller模板删除接口query_db参数异常的问题 (#63)
* Update controller.py.jinja2

修复:
https://github.com/insistence/RuoYi-Vue3-FastAPI/issues/62#issue-3774115997

* 修复:代码生成时,关于delete的接口错乱

修复https://github.com/insistence/RuoYi-Vue3-FastAPI/issues/62
2026-01-04 10:42:26 +08:00
insistence 8cdf58310e chore: 发布1.8.1版本 2025-12-26 15:59:50 +08:00
insistence 75c49e89c2 perf: 优化pg版本的docker compose配置文件 (#61)
* perf: 优化pg版本的docker compose配置文件

* docs: 新增更新日志
2025-12-26 15:18:37 +08:00
insistence 8405471afd fix: 修复新增和编辑部门无法使用已删除的部门名称问题 (#60) 2025-12-26 11:50:48 +08:00
insistence 1e0b38451c fix: 修复DictTag组件渲染异常的问题 (#59)
* fix: 修复DictTag组件渲染异常的问题

* docs: 新增更新日志

* docs: 修复更新日志书写错误
2025-12-26 11:37:29 +08:00
insistence b84823610c feat: 新增E2E测试 (#57)
* feat: 新增E2E测试

* fix: 修复docker compose命令书写错误

* fix: 运行测试前执行`playwright install`

* fix: 增强检查以尝试修复3.11连接拒绝的问题

* fix: 增加mysql服务检查

* fix: 修复python 3.9兼容性问题

* perf: 优化禁用验证码脚本

* feat: 新增pg测试

* fix: 修复pg测试检查服务名书写错误

* perf: 优化测试

* style: 格式化代码
2025-12-26 11:09:51 +08:00
insistence 332ddbe0e8 perf: 动态导入定时任务函数,移除eval (#56) 2025-12-22 16:33:25 +08:00
insistence 9cfbf48ce9 perf: 优化数据权限依赖 (#55) 2025-12-22 11:30:03 +08:00
insistence 8f791ff6e5 !40 RuoYi-Vue3-FastAPI v1.8.0
Merge pull request !40 from insistence/develop
2025-12-19 08:32:34 +00:00
insistence 6cb2afca39 feat: 新增github actions配置文件 2025-12-19 16:30:24 +08:00
insistence d47613970b docs: 新增更新日志文档 2025-12-19 16:13:27 +08:00
insistence 8833980008 docs: 更新README文档 2025-12-19 11:20:19 +08:00
insistence 0067e66fc8 chore: 升级版本至1.8.0 2025-12-19 11:20:09 +08:00
insistence 39c4590c3f chore: 更新后端依赖 2025-12-19 11:05:48 +08:00
insistence 3196962197 fix: 修复代码生成v3模板时间控件between选择后清空报错问题 2025-12-19 10:31:52 +08:00
insistence 1ffaaf5225 perf: 优化topbar顶部菜单样式 2025-12-19 09:35:44 +08:00
insistence 4ad671023d perf: 默认固定头部 2025-12-19 09:30:05 +08:00
insistence 45bd2ddb21 perf: 优化字典组件值宽松匹配 2025-12-19 09:28:13 +08:00
insistence 267f76d6a2 perf: 优化字典组件数字类型值处理逻辑 2025-12-19 09:26:48 +08:00
insistence 67c09f3113 perf: 优化index页面标题读取配置 2025-12-19 09:25:32 +08:00
insistence c8da8d5406 perf: 优化操作日志详细请求参数显示 2025-12-19 09:24:40 +08:00
insistence 6ad0a63c68 feat: 菜单导航设置支持纯顶部 2025-12-19 09:23:19 +08:00
insistence ab4eb00bc1 行内表单默认设置固定宽度 2025-12-19 09:05:14 +08:00
insistence 954085666a perf: 优化表单构建关闭页签销毁复制插件 2025-12-19 09:02:58 +08:00
insistence 35aa26f8a9 perf: 优化生成代码下载的zip文件名 2025-12-19 09:01:01 +08:00
insistence 1e281508b2 perf: 登录/注册页面底部版权信息修改为读取配置 2025-12-19 08:45:10 +08:00
insistence a4e4c20c5e fix: 修复表单构建移除所有控件后切换路由回来空白问题 2025-12-19 08:42:46 +08:00
insistence d4b85f38d8 fix: 修复固定头部时出现的导航栏偏移问题 2025-12-19 08:38:41 +08:00
insistence ed88512bf8 feat: 代码生成controller模版使用APIRouterPro 2025-12-19 08:23:00 +08:00
insistence b9b2f713b9 feat: 新增docker compose部署方式 2025-12-18 16:03:44 +08:00
insistence b1563b0d39 fix: 移除代码生成表业务表外键,修复无法删除的问题 2025-12-17 17:02:42 +08:00
insistence 1857663c9f feat: 新增自动注册路由功能 2025-12-17 15:52:22 +08:00
insistence a28922acf0 style: 格式化代码 2025-12-17 15:51:36 +08:00
insistence cdacb1643c feat: 新增配置文档静态资源方法 2025-12-16 16:15:52 +08:00
insistence ad0327bf51 refactor: 重构app与server设计 2025-12-16 14:38:31 +08:00
insistence 50af1bf573 fix: 修复在线用户列表与PageResponseModel不兼容的问题 2025-12-15 17:08:43 +08:00
insistence d364394575 fix: 修复磁盘存在异常时服务监控无法正常运行的问题 2025-12-15 16:49:30 +08:00
insistence 98651f4d8f perf: 接口API文档的接口描述信息 2025-12-15 16:18:20 +08:00
insistence 40ccb84922 perf: 优化所有接口的API文档响应模型显示 2025-12-15 15:02:49 +08:00
insistence d9a19ce3ee perf: 操作响应模型改为使用公共vo模块的CrudResponseModel 2025-12-13 11:28:43 +08:00
insistence e216b97163 perf: 代码生成模板使用公共vo模块 2025-12-13 11:20:33 +08:00
insistence 1f51b628af perf: 优化API文档的响应模型显示 2025-12-12 17:44:54 +08:00
insistence 1656670203 perf: 分页模型改为使用公共vo模块的PageModel 2025-12-12 17:44:27 +08:00
insistence b8dcbdc448 feat: 新增公共vo模块 2025-12-12 17:42:26 +08:00
insistence dc2e255cd2 fix: 确保ApschedulerJobs字段类型与apscheduler默认创建的表字段类型一致 #53 2025-12-12 09:32:18 +08:00
insistence d9245ed2c9 fix: 修复单账号登录模式下强退功能失效的问题 #52 2025-12-12 08:49:55 +08:00
insistence 8f4058977d chore: fastapi -> 0.124.2 2025-12-11 17:01:53 +08:00
insistence 044fa0f30b feat: 新增上下文清理中间件 2025-12-11 17:00:33 +08:00
insistence 6565c50e08 perf: 当前用户信息使用上下文变量 2025-12-11 17:00:12 +08:00
insistence 184982eb08 perf: controller层全部使用新依赖项 2025-12-11 16:59:18 +08:00
insistence 9a1c53d9f4 feat: 新增UserInterfaceAuthDependency和RoleInterfaceAuthDependency依赖 2025-12-11 16:57:58 +08:00
insistence 44678985e9 feat: 新增DBSessionDependency依赖 2025-12-11 16:57:35 +08:00
insistence 077c8870b4 feat: 新增DataScopeDependency依赖 2025-12-11 16:57:10 +08:00
insistence d1fcb6dc11 feat: 新增依赖项工具类 2025-12-11 16:56:48 +08:00
insistence 9114d65308 feat: 新增PreAuthDependency和CurrentUserDependency依赖 2025-12-11 16:55:46 +08:00
insistence f5ec612850 feat: 新增请求上下文管理类 2025-12-11 16:54:09 +08:00
insistence a3355e2545 refactor&feat: 优化项目结构,新增common模块,原annotation、aspect、constant、enums模块移动至common模块下 2025-12-05 15:02:04 +08:00
insistence 1516c1a941 refactor&perf: 增强ruff规则,完善类型提示 2025-12-05 10:00:25 +08:00
insistence b974e8bd3d !38 RuoYi-Vue3-FastAPI v1.7.1
Merge pull request !38 from insistence/develop
2025-10-14 03:09:29 +00:00
insistence a389ef66bc docs: 更新README文档 2025-10-14 11:05:47 +08:00
insistence dcdeaa0394 chore: 升级版本至1.7.1 2025-10-14 11:05:39 +08:00
insistence f4d7d7a416 chore&fix: 移除passlib直接使用bcrypt修复密码校验异常的问题 报错 #48 #49 2025-10-14 11:05:20 +08:00
insistence f5a91c0ece perf: 代码生成do模板补充表描述 2025-09-05 09:21:40 +08:00
insistence 5ba6587b6c fix: 修复代码生成controller模板编辑接口异常生成字段的问题 2025-09-05 09:21:29 +08:00
878 changed files with 165841 additions and 5736 deletions
+1
View File
@@ -0,0 +1 @@
github: [insistence]
+58
View File
@@ -0,0 +1,58 @@
name: Backend Tests
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
pytest:
runs-on: ubuntu-latest
env:
# Typer otherwise forces ANSI help output when GITHUB_ACTIONS is set.
_TYPER_FORCE_DISABLE_TERMINAL: "1"
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
defaults:
run:
working-directory: ruoyi-fastapi-backend
steps:
- uses: actions/checkout@v5
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip
cache-dependency-path: |
ruoyi-fastapi-backend/requirements.txt
ruoyi-fastapi-backend/plugins/ai/plugin.yaml
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt pytest pytest-asyncio aiosqlite
- name: Install AI plugin dependencies
run: |
pip install \
agno==2.8.6 \
anthropic==0.120.2 \
cerebras-cloud-sdk==1.91.0 \
cohere==7.0.8 \
google-genai==2.16.0 \
groq==1.6.0 \
litellm==1.94.0 \
llama-api-client==0.6.0 \
mistralai==2.8.0 \
ollama==0.6.2 \
openai==2.52.0 \
portkey-ai==2.3.4
- name: Run backend tests
run: pytest -q
+174
View File
@@ -0,0 +1,174 @@
name: Playwright Tests
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
mysql-test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v5
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Cache Docker layers
uses: actions/cache@v5
with:
path: /tmp/.docker_cache
key: ${{ runner.os }}-docker-${{ matrix.python-version }}-${{ hashFiles('**/Dockerfile*', '**/requirements.txt') }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Create temporary Dockerfile with Python ${{ matrix.python-version }}
run: |
# Save original Dockerfile.my
cp ruoyi-fastapi-backend/Dockerfile.my ruoyi-fastapi-backend/Dockerfile.my.bak
# Create temporary Dockerfile with target Python version and default PyPI index
sed -e "s/FROM python:3.10/FROM python:${{ matrix.python-version }}/g" \
-e 's| -i https://pypi.tuna.tsinghua.edu.cn/simple||g' \
ruoyi-fastapi-backend/Dockerfile.my.bak > ruoyi-fastapi-backend/Dockerfile.my
- name: Start services with docker-compose
run: |
cd ruoyi-fastapi-test
docker compose -f docker-compose.test.my.yml up -d --build
- name: Wait for services to be ready
run: |
cd ruoyi-fastapi-test
# Wait for backend to be running
timeout 180 bash -c 'until docker compose -f docker-compose.test.my.yml ps ruoyi-backend-my | grep -q "Up"; do sleep 5; done'
# Wait for frontend to be running
timeout 120 bash -c 'until docker compose -f docker-compose.test.my.yml ps ruoyi-frontend | grep -q "Up"; do sleep 5; done'
# Additional wait for services to be fully ready and listening on ports
sleep 30
# Check that backend is actually responding on the API endpoint
echo "Checking if backend service is ready..."
for i in {1..30}; do
if curl -f http://localhost:9099/captchaImage > /dev/null 2>&1; then
echo "Backend service is ready!"
break
fi
echo "Waiting for backend service to be ready... ($i/30)"
sleep 5
done
# Final check
if ! curl -f http://localhost:9099/captchaImage > /dev/null 2>&1; then
echo "Backend service failed to start properly. Checking logs..."
docker logs ruoyi-backend-my-test
exit 1
fi
- name: Run tests
run: |
cd ruoyi-fastapi-test
pip install -r requirements.txt
playwright install
pytest -v
- name: Stop services
if: always()
run: |
cd ruoyi-fastapi-test
docker compose -f docker-compose.test.my.yml down
- name: Restore original Dockerfile.my
if: always()
run: |
# Restore original Dockerfile.my after test
mv ruoyi-fastapi-backend/Dockerfile.my.bak ruoyi-fastapi-backend/Dockerfile.my
pg-test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v5
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Cache Docker layers
uses: actions/cache@v5
with:
path: /tmp/.docker_cache
key: ${{ runner.os }}-docker-${{ matrix.python-version }}-${{ hashFiles('**/Dockerfile*', '**/requirements.txt') }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Create temporary Dockerfile with Python ${{ matrix.python-version }}
run: |
# Save original Dockerfile.pg
cp ruoyi-fastapi-backend/Dockerfile.pg ruoyi-fastapi-backend/Dockerfile.pg.bak
# Create temporary Dockerfile with target Python version and default PyPI index
sed -e "s/FROM python:3.10/FROM python:${{ matrix.python-version }}/g" \
-e 's| -i https://pypi.tuna.tsinghua.edu.cn/simple||g' \
ruoyi-fastapi-backend/Dockerfile.pg.bak > ruoyi-fastapi-backend/Dockerfile.pg
- name: Start services with docker-compose
run: |
cd ruoyi-fastapi-test
docker compose -f docker-compose.test.pg.yml up -d --build
- name: Wait for services to be ready
run: |
cd ruoyi-fastapi-test
# Wait for backend to be running
timeout 180 bash -c 'until docker compose -f docker-compose.test.pg.yml ps ruoyi-backend-pg | grep -q "Up"; do sleep 5; done'
# Wait for frontend to be running
timeout 120 bash -c 'until docker compose -f docker-compose.test.pg.yml ps ruoyi-frontend | grep -q "Up"; do sleep 5; done'
# Additional wait for services to be fully ready and listening on ports
sleep 30
# Check that backend is actually responding on the API endpoint
echo "Checking if backend service is ready..."
for i in {1..30}; do
if curl -f http://localhost:9099/captchaImage > /dev/null 2>&1; then
echo "Backend service is ready!"
break
fi
echo "Waiting for backend service to be ready... ($i/30)"
sleep 5
done
# Final check
if ! curl -f http://localhost:9099/captchaImage > /dev/null 2>&1; then
echo "Backend service failed to start properly. Checking logs..."
docker logs ruoyi-backend-pg-test
exit 1
fi
- name: Run tests
run: |
cd ruoyi-fastapi-test
pip install -r requirements.txt
playwright install
pytest -v
- name: Stop services
if: always()
run: |
cd ruoyi-fastapi-test
docker compose -f docker-compose.test.pg.yml down
- name: Restore original Dockerfile.pg
if: always()
run: |
# Restore original Dockerfile.pg after test
mv ruoyi-fastapi-backend/Dockerfile.pg.bak ruoyi-fastapi-backend/Dockerfile.pg
+33
View File
@@ -0,0 +1,33 @@
name: Ruff Check
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
lint-format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install ruff
- name: Run linting
run: |
ruff check ruoyi-fastapi-backend
ruff check ruoyi-fastapi-test
- name: Run format check
run: |
ruff format ruoyi-fastapi-backend --check
ruff format ruoyi-fastapi-test --check
+10 -1
View File
@@ -24,7 +24,7 @@ share/python-wheels/
*.egg-info/ *.egg-info/
.installed.cfg .installed.cfg
*.egg *.egg
MANIFEST /MANIFEST
# PyInstaller # PyInstaller
# Usually these files are written by a python script from a template # Usually these files are written by a python script from a template
@@ -143,3 +143,12 @@ cython_debug/
# VSCode # VSCode
.vscode/ .vscode/
# AI Editor
.agent/
.claude/
.codebuddy/
.codex/
.cursor/
.opencode/
.qoder/
.trae/
+616
View File
@@ -0,0 +1,616 @@
# 更新日志
## RuoYi-Vue3-FastAPI v1.10.0
### 项目依赖
前后端依赖均有升级,请升级依赖或重新创建环境。
### 新增功能
1. 新增ApiCache接口缓存装饰器和ApiCacheEvict接口缓存失效装饰器 ([#97](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/97))。
2. 新增接口限流装饰器 ([#99](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/99))。
3. 新增传输加密功能 ([#100](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/100))。
4. 新增日志脱敏功能 ([#101](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/101))。
5. 新增cli系统 ([#103](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/103))。
6. 新增文件管理功能 ([#111](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/111))。
7. 新增插件系统 ([#112](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/112))。
8. 新增自动生成jwt密钥 ([#116](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/116))。
9. 新增锁定屏幕功能 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
10. 字典类型列表新增抽屉效果详细信息 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
11. 添加持久化标签页开关功能 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
12. 支持表格列显隐状态记忆 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
13. 代码生成支持表单布局选项 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
14. 新增树分割组件TreePanel ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
15. 新增Excel导入组件ExcelImportDialog ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
16. 用户列表新增抽屉效果详细信息 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
17. 新增标签页样式chrome风格 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
18. 新增菜单路由地址和名称的校验规则 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
19. 首页新增通知公告消息提醒 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
20. 定时任务详情补充执行时间字段 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
21. 部门管理支持批量保存排序 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
22. 菜单管理支持批量保存排序 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
23. 菜单管理列表新增类型显示 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
24. 新增代码生成详情页功能 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
25. 通知公告新增详细显示 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
26. 通知公告新增阅读用户列表 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
27. 用户密码支持自定义配置规则 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
### BUG修复
1. 修复日志输出格式问题 ([#104](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/104))。
2. 修复文件服务安全性问题 ([#111](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/111))。
3. 修复windows下应用启动日志异常的问题 ([#113](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/113))。
4. 修复tui卡顿的问题 ([#114](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/114))。
5. 修复E2E测试失败的问题 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
### 代码优化
1. 补充演示模式限制路由 ([#115](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/115))。
2. 优化字典类型属性提醒说明 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
3. 优化顶部导航更多菜单路由截取 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
4. 优化RightToolbar搜索栏切换动画 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
5. 优化操作日志详细页面 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
7. 优化页签功能&支持全屏按钮操作 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
8. 优化tag全屏为页签内区域 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
9. 菜单搜索支持文本高亮&数量提示 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
10. 优化页签显示 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
11. 优化菜单主题风格 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
12. 黑暗模式下全屏不需要设置背景 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
13. 暗黑浅色主题优化 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
14. 优化快速点击页签刷新出现404问题 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
15. 代码生成修改拖拽时显示手指样式 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
16. 优化登录页暗黑模式下样式 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
17. 优化白名单支持对通配符路径匹配 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
18. 优化定时任务详情页展示 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
19. 优化点击任务名称查看详细 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
20. 更换router4过期方法next ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
21. 优化暗黑模式下主题样式 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
22. 优化暗黑模式详情样式 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
23. 优化jwt解析异常处理机制 ([#118](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/118))。
## RuoYi-Vue3-FastAPI v1.9.0
### 项目依赖
前后端依赖均有升级,请升级依赖或重新创建环境。
### 新增功能
1. 新增AI管理模块 ([#69](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/69))。
2. 新增移动端模块 ([#73](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/73))。
3. 新增多worker运行支持 ([#76](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/76))。
4. 应用新增演示模式 ([#78](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/78))。
### BUG修复
1. 修复代码生成controller模板删除接口query_db参数异常的问题 ([#63](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/63))。
2. 修复登录接口response_model声明错误 ([#71](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/71))。
3. 修复无法直接通过后端地址访问API文档的问题 ([#74](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/74))。
4. 修复create_app重复执行的问题 ([#84](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/84))。
### 代码重构
1. 移除对python3.9的支持 ([#67](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/67))。
### 代码优化
1. 优化alembic处理表模型逻辑,避免无关表影响 ([#68](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/68))。
2. 优化代码生成后端模板 ([#72](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/72))。
3. 自动注册路由出错时抛出异常以便于调试 ([#79](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/79))。
4. 优化部分页面字段tooltip说明 (#80)。
5. 优化项目启动速度 ([#82](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/82))。
6. 优化暗黑模式切换效果 ([#83](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/83))。
7. 优化热重载模式或单worker下scheduler的任务状态同步机制 ([#85](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/85))。
8. 优化防重提交间隔时间可自定义 ([#87](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/87))。
9. 优化验证码计算结果为非负数 ([#88](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/88))。
10. 优化ci测试稳定性 ([#90](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/90))。
## RuoYi-Vue3-FastAPI v1.8.1
### 新增功能
1. 新增E2E测试 ([#57](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/57))。
### BUG修复
1. 修复DictTag组件渲染异常的问题 ([#59](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/59))。
### 代码优化
1. 优化数据权限依赖 ([#55](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/55))。
2. 动态导入定时任务函数,移除eval ([#56](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/56))。
3. 优化pg版本的docker compose配置文件 ([#61](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/61))。
## RuoYi-Vue3-FastAPI v1.8.0
### 项目依赖
#### 后端
1. 后端依赖升级到最新版本,请升级依赖或重新创建环境。
### 新增功能
1. 新增请求上下文管理类。
2. 新增`PreAuthDependency``CurrentUserDependency``DataScopeDependency``DBSessionDependency``UserInterfaceAuthDependency``RoleInterfaceAuthDependency`依赖函数。
3. 新增上下文清理中间件。
4. 新增公共vo模块。
5. 新增配置文档静态资源方法。
6. 新增自动注册路由功能。
7. 新增docker compose部署方式。
8. 菜单导航设置支持纯顶部。
### BUG修复
1. 修复单账号登录模式下强退功能失效的问题 [#52](https://github.com/insistence/RuoYi-Vue3-FastAPI/issues/52)。
2. 确保ApschedulerJobs字段类型与apscheduler默认创建的表字段类型一致 [#53](https://github.com/insistence/RuoYi-Vue3-FastAPI/issues/53)。
3. 修复磁盘存在异常时服务监控无法正常运行的问题。
4. 移除代码生成表业务表外键,修复无法删除的问题。
5. 修复固定头部时出现的导航栏偏移问题。
6. 修复表单构建移除所有控件后切换路由回来空白问题。
7. 修复代码生成v3模板时间控件between选择后清空报错问题。
### 代码重构
1. 增强ruff规则,完善类型提示。
2. 优化项目结构,新增common模块,原annotation、aspect、constant、enums模块移动至common模块下。
3. 重构app与server设计。
### 代码优化
1. controller层全部使用新依赖项。
2. 当前用户信息使用上下文变量。
3. 分页模型改为使用公共vo模块的PageModel。
4. 优化API文档的响应模型显示。
5. 操作响应模型改为使用公共vo模块的CrudResponseModel。
6. 优化API文档的接口描述信息。
7. 登录/注册页面底部版权信息修改为读取配置。
8. 优化生成代码下载的zip文件名。
9. 优化表单构建关闭页签销毁复制插件。
10. 行内表单默认设置固定宽度。
11. 优化操作日志详细请求参数显示。
12. 优化index页面标题读取配置。
13. 优化字典组件数字类型值处理逻辑。
14. 优化字典组件值宽松匹配。
15. 默认固定头部。
## RuoYi-Vue3-FastAPI v1.7.1
### 项目依赖
1. 后端依赖移除passlib,直接使用bcrypt。
### BUG修复
1. 修复代码生成controller模板编辑接口异常生成字段的问题。
2. 移除passlib直接使用bcrypt修复密码校验异常的问题 [#48](https://github.com/insistence/RuoYi-Vue3-FastAPI/issues/48) [#49](https://github.com/insistence/RuoYi-Vue3-FastAPI/issues/49)。
### 代码优化
1. 代码生成do模板补充表描述。
## RuoYi-Vue3-FastAPI v1.7.0
### 项目依赖
1. 前后端依赖升级,请升级依赖或重新创建环境。
### 新增功能
1. 新增alembic支持。
2. 文件&图片上传组件支持自定义地址&参数。
3. 新增默认打包配置项。
4. 显隐列组件支持全选/全不选。
5. 添加页签openPage支持传递参数。
6. 外链加载时遮罩信息提示。
7. 上传组件新增拖动排序属性。
8. 图片上传组件新增disabled属性。
9. 代码生成列支持拖动排序。
10. 新增用户默认初始化密码。
11. 新增页签图标显示开关功能。
12. 新增底部版权信息及开关。
13. 用户归属部门新增清除。
14. 用户导入新增验证提示。
15. 菜单搜索支持键盘选择&悬浮主题背景。
16. 新增apscheduler_jobs表对应sqlalchemy模型类。
17. 初始密码支持自定义修改策略。
18. 账号密码支持自定义更新周期。
19. 注册账号设置默认密码最后更新时间。
20. 显示列信息支持对象格式。
### BUG修复
1. 修复logout接口未按照app_same_time_login配置项动态判断的问题 [#IBZZ1S](https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/issues/IBZZ1S)。
2. 修复上传组件被多次引用拖动仅对第一个有效的问题。
### 代码优化
1. 优化接口耗时计算。
2. 优化启动信息显示。
3. 优化前端处理路由函数代码。
4. 登录页和注册页表头使用VITE_APP_BASE_API配置值。
5. 优化角色禁用不允许分配。
6. 优化富文本控制台警告异常。
7. 优化checkbox废弃API。
8. 优化导航栏显示昵称&设置。
### 代码重构
1. 重构IP归属区域查询为异步调用。
2. 调整do与sql使其相互适配以支持alembic。
3. 富文本复制粘贴图片上传至url。
## RuoYi-Vue3-FastAPI v1.6.2
### 新增功能
1. 文件上传组件新增disabled属性。
2. 文件上传组件新增类型。
### BUG修复
1. 修复日志管理时间查询报错 [#27](https://github.com/insistence/RuoYi-Vue3-FastAPI/issues/27)。
2. 修复定时任务状态暂停时执行单次任务会触发cron表达式的问题 [#31](https://github.com/insistence/RuoYi-Vue3-FastAPI/issues/31)。
3. 修复修改字典类型时获取dict_code异常的问题。
4. 修复修改字典类型时字典数据更新时间异常的问题。
5. 修复代码生成模板时间查询问题 [#28](https://github.com/insistence/RuoYi-Vue3-FastAPI/issues/28)。
6. 修复用户导出缺失部门名称的问题。
### 代码优化
1. 优化代码生成新增和编辑字段显示和渲染。
2. pagination更换成flex布局。
3. 优化代码生成vue模板 [#23](https://github.com/insistence/RuoYi-Vue3-FastAPI/issues/23)。
## RuoYi-Vue3-FastAPI v1.6.1
### 项目依赖
#### 后端
1. 新增sqlglot依赖
```bash
pip install sqlglot[rs]==26.6.0 -i https://pypi.tuna.tsinghua.edu.cn/simple
```
### BUG修复
1. 引入sqlglot修复sql语句解析异常的问题。
2. 修复代码生成字段唯一性校验dao层模板判断异常的问题。
3. 引入泛型修复as_query和as_form装饰模型文档丢失的问题。
4. 修复代码生成主子表vo模板可能缺失NotBlank的问题。
## RuoYi-Vue3-FastAPI v1.6.0
### 项目依赖
1. 后端依赖升级到最新版本,请升级依赖或重新创建环境。
### 新增功能
1. 新增代码生成功能,支持配置数据库表信息一键生成和下载前后端代码,需要重新执行sql文件,请先备份数据。
2. 新增表单构建功能。
3. 用户头像新增支持http(s)链接。
4. 新增trace中间件强化日志链路追踪和响应头 [@y1ren](https://gitee.com/y1ren)。
5. 用户管理支持分栏拖动。
6. 菜单面包屑导航支持多层级显示。
7. 白名单支持对通配符路径匹配。
8. 支持开启暗黑模式。
### BUG修复
1. 修复默认关闭Tags-Views时,内链页面打不开。
2. 修复删除当前登录用户拦截失效的问题。
3. 修复定时任务目标字符串规则校验不全的问题。
4. 修复执行单次任务时会覆盖已启用任务的问题 [#IBEKD2](https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/issues/IBEKD2)。
5. 修复个人中心特殊字符密码修改失败问题。
### 代码优化
1. 优化导出方法。
2. 参数键值更换为多行文本。
3. 优化日志中操作方法显示。
4. 优化日志装饰器获取核心参数的方式。
5. 用户管理过滤掉已禁用部门。
6. 优化TopNav内链菜单点击没有高亮。
7. ResponseUtil补充完整参数。
## RuoYi-Vue3-FastAPI v1.5.1
### 新增功能
1. 定时任务新增支持调用异步函数。
### 代码优化
1. 优化字典数组条件判断。
2. 校检文件名是否包含特殊字符。
3. 移除已弃用的log_decorator装饰器。
## RuoYi-Vue3-FastAPI v1.5.0
### 新增功能
1. 新增对PostgreSQL数据库的支持。
### BUG修复
1. 修复DictTag组件控制台抛异常的问题 [#IAYSVZ](https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/issues/IAYSVZ)。
2. 修复登录日志导出文件名称错误的问题。
### 代码回滚
1. 因fastapi查询参数模型底层存在bug,回滚查询参数模型声明方式为as_query。
### 代码优化
1. 优化CamelCaseUtil和SnakeCaseUtil以兼容更多转换场景。
2. 优化列表查询排序。
3. 优化参数设置页面。
4. 优化上传图片带域名时不增加前缀。
## RuoYi-Vue3-FastAPI v1.4.0
### 项目依赖
#### 后端
1. 更新fastapi版本为0.115.0
```bash
pip install fastapi[all]==0.115.0 -i https://pypi.tuna.tsinghua.edu.cn/simple
```
### 代码重构
1. 基于fastapi 0.115.0版本新特性,直接使用pydantic模型接收查询参数和表单数据,移除原有as_query和as_form使用方式。
### BUG修复
1. 修复角色管理service书写错误。
### 代码优化
1. 优化前端登录请求方法。
## RuoYi-Vue3-FastAPI v1.3.3
### 项目依赖
#### 后端
1. 更新pydantic-validation-decorator版本为0.1.4,修复了一些底层bug。
### BUG修复
1. 修复在线用户模块条件查询无效的问题。
### 代码优化
1. 优化在线用户模块前后端字段描述一致。
2. 日志装饰器异常处理增加logger打印日志。
## RuoYi-Vue3-FastAPI v1.3.2
### 新增功能
1. 新增gzip压缩中间件。
### BUG修复
1. 修复分页函数计算has_next错误的问题 [#10](https://github.com/insistence/RuoYi-Vue3-FastAPI/issues/10)。
2. 修复定时任务监听函数中事件没有job_id报错的问题。
### 代码优化
1. 优化添加中间件函数注释。
## RuoYi-Vue3-FastAPI v1.3.1
### BUG修复
1. 修复1.3.0版本采用新的异常处理机制后日志装饰器无法记录异常日志的问题。
### 代码优化
1. 补充定时任务违规字符串。
## RuoYi-Vue3-FastAPI v1.3.0
### 项目依赖
1. 前后端依赖均升级到最新版本,请升级依赖或重新创建环境。
2. 使用`PyJWT`替换`python-jose`以解决一些安全性问题。
### 新增功能
1. 新增字段校验装饰器,支持手动触发校验,已封装为`pydantic-validation-decorator`库。
2. 各模块`service`层新增字段唯一性校验。
3. 全局新增`ServiceException`自定义服务异常和`ServiceWarning`自定义服务警告,无需在接口中写大量的异常捕获。
4. 菜单管理新增路由名称,请执行以下sql为数据库新增字段:
```sql
ALTER TABLE sys_menu ADD COLUMN route_name varchar(50) DEFAULT '';
```
5. 新增`constant`常量配置及`enums`枚举类型配置。
6. 新增`StringUtil``CronUtil`工具类。
### BUG修复
1. 修复用户管理、角色管理、部门管理越权漏洞。
2. 修复各模块`dao``status``del_flag`类型与数据库不一致的问题。
3. 修复移动端左侧菜单无法显示的问题。
4. 修复其他已知BUG。
### 代码重构
1. 重构日志装饰器为`Log`,未来版本将删除`log_decorator`装饰器,请尽快迁移。
2. 重构`RedisInitKeyConfig`为枚举类型,现在可通过以下方式获取对应的`key``remark`
`RedisInitKeyConfig.ACCESS_TOKEN.key``RedisInitKeyConfig.ACCESS_TOKEN.remark`
3. 重构数据权限逻辑,底层进行优化,使用方法与之前相同。
### 代码优化
1. 引入`ruff`对后端代码进行格式化及检测修复,优化导入。
2. 各模块基于`ServiceException`自定义服务异常和`ServiceWarning`自定义服务警告优化了异常处理逻辑。
3. 各模块`vo`层使用`Field`声明字段。
4. 优化API文档字段描述显示。
## RuoYi-Vue3-FastAPI v1.2.2
### BUG修复
1. 修复删除定时任务时未移除调度中任务的问题。
2. 修复菜单生成路由时组件条件判断错误的问题。
## RuoYi-Vue3-FastAPI v1.2.1
### BUG修复
1. 修复各模块新增数据时创建时间记录异常的问题。
2. 修复菜单挂载到根目录时路由加载异常等一系列相关问题。
### 代码及性能优化
1. 修改代理localhost为127.0.0.1以适配部分设备解析localhost异常的问题。
## RuoYi-Vue3-FastAPI v1.2.0
### 重要说明
本次更新为 **_破坏性更新_** ,重构数据库orm为异步,代码改动很大,请谨慎升级。
1. 原有的Session类型声明统一变更为AsyncSession。
2. service层和dao层的函数修改为异步函数,请使用await调用。
3. orm查询不再支持query,请使用select、update、delete等语句,具体使用方法请参考[https://docs.sqlalchemy.org/en/20/orm/queryguide/index.html](https://docs.sqlalchemy.org/en/20/orm/queryguide/index.html)。
### 项目依赖
#### 后端
1. 增加asyncmy依赖用于支持orm异步操作mysql,请重新安装依赖
```bash
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple。
```
### 新增功能
1. 新增SnakeCaseUtil工具类,将原CamelCaseUtil工具类的camel_to_snake函数迁移至SnakeCaseUtil工具类。
### BUG修复
1. 修复用户管理模块重置用户密码时会异常重置用户岗位和角色的问题。
2. 修复清空定时任务日志异常的问题。
## RuoYi-Vue3-FastAPI v1.1.3
### 新增功能
1. 用户密码新增非法字符验证。
### BUG修复
1. 修复通知公告列表查询前后端字段不一致的问题。
2. 修复个人中心修改基本资料后端异常的问题。
## RuoYi-Vue3-FastAPI v1.1.2
### 新增功能
1. 配置文件新增数据库连接池相关配置。
### BUG修复
1. 修复个人中心修改密码后端异常的问题。
### 代码及性能优化
1. 使用@lru_cache缓存ip归属区域查询结果,避免重复调用ip归属区域查询接口以优化性能。
## RuoYi-Vue3-FastAPI v1.1.1
### BUG修复
1. 修复编辑定时任务时更新的信息未同步至scheduler的问题 [#I9EK56](https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/issues/I9EK56)。
2. 修复编辑角色数据权限时后端异常的问题 [#I9ENQN](https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/issues/I9ENQN)。
3. 修复菜单配置路由参数不生效的问题。
4. 修复获取路由信息时菜单排序不生效的问题。
5. 修复添加菜单时是否外链和是否缓存回显异常的问题。
## RuoYi-Vue3-FastAPI v1.1.0
### 新增功能
1. 后端配置文件新增sqlalchemy日志开关配置。
2. 后端配置文件新增IP归属区域查询开关配置。
3. 后端配置文件新增账号同时登录开关配置。
### BUG修复
1. 修复token本身过期时退出登录接口异常的问题 [#I9CBWT](https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/issues/I9CBWT)。
2. 修复系统版本号或浏览器版本号无法获取时登录异常的问题 [#I9CYNM](https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/issues/I9CYNM)。
## RuoYi-Vue3-FastAPI v1.0.3
### 新增功能
1. 账号密码登录新增IP黑名单校验。
### BUG修复
1. 修复外链菜单无法打开的问题 [#I95KBK](https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/issues/I95KBK)。
2. 修复添加和编辑菜单页面中是否缓存和是否外链字段回显异常的问题 [#I95KBK](https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/issues/I95KBK)。
## RuoYi-Vue3-FastAPI v1.0.2
### 新增功能
1. 用户接口权限校验增加列表接收参数,实现同一接口支持多个权限标识校验。
2. 新增按角色校验接口权限依赖
### BUG修复
1. 修复用户管理和部门管理模块数据权限异常的问题。
### 代码及性能优化
1. 调整参数设置、部门管理、字典管理、定时任务、日志管理、角色管理、菜单管理模块部分接口权限标识。
## RuoYi-Vue3-FastAPI v1.0.1
### 项目依赖
#### 后端
1. 更新fastapi版本为0.109.1,修复一些安全性问题,命令:
```bash
pip install fastapi[all]==0.109.1 -i https://mirrors.aliyun.com/pypi/simple/
```
### 新增功能
1. 日志管理模块新增字段排序查询。
## RuoYi-Vue3-FastAPI v1.0.0
RuoYi-Vue3-FastAPI第一个版本发布啦!
此版本功能如下:
1. 用户管理:用户是系统操作者,该功能主要完成系统用户配置。
2. 角色管理:角色菜单权限分配。
3. 菜单管理:配置系统菜单,操作权限,按钮权限标识等。
4. 部门管理:配置系统组织机构(公司、部门、小组)。
5. 岗位管理:配置系统用户所属担任职务。
6. 字典管理:对系统中经常使用的一些较为固定的数据进行维护。
7. 参数管理:对系统动态配置常用参数。
8. 通知公告:系统通知公告信息发布维护。
9. 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
10. 登录日志:系统登录日志记录查询包含登录异常。
11. 在线用户:当前系统中活跃用户状态监控。
12. 定时任务:在线(添加、修改、删除)任务调度包含执行结果日志。
13. 服务监控:监视当前系统CPU、内存、磁盘、堆栈等相关信息。
14. 缓存监控:对系统的缓存信息查询,命令统计等。
15. 系统接口:根据业务代码自动生成相关的api接口文档。
+211 -59
View File
@@ -1,16 +1,34 @@
<p align="center"> <h1 align="center">
<img alt="logo" src="https://oscimg.oschina.net/oscnet/up-d3d0a9303e11d522a06cd263f3079027715.png"> <img alt="logo" src="https://oscimg.oschina.net/oscnet/up-d3d0a9303e11d522a06cd263f3079027715.png">
</p> </h1>
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">RuoYi-Vue3-FastAPI v1.7.0</h1> <h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">RuoYi-Vue3-FastAPI</h1>
<h4 align="center">基于RuoYi-Vue3+FastAPI前后端分离的快速开发框架</h4> <h4 align="center">基于RuoYi-Vue3+FastAPI前后端分离的快速开发框架</h4>
<p align="center"> <p align="center">
<a href="https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/stargazers"><img src="https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/badge/star.svg?theme=dark"></a> <a href="https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/stargazers">
<a href="https://github.com/insistence/RuoYi-Vue3-FastAPI"><img src="https://img.shields.io/github/stars/insistence/RuoYi-Vue3-FastAPI?style=social"></a> <img alt="Gitee" src="https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/badge/star.svg?theme=dark">
<a href="https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI"><img src="https://img.shields.io/badge/RuoYiVue3FastAPI-v1.7.0-brightgreen.svg"></a> </a>
<a href="https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/blob/master/LICENSE"><img src="https://img.shields.io/github/license/mashape/apistatus.svg"></a> <a href="https://github.com/insistence/RuoYi-Vue3-FastAPI">
<img src="https://img.shields.io/badge/node-≥18-blue"> <img alt="Github" src="https://img.shields.io/github/stars/insistence/RuoYi-Vue3-FastAPI?style=social">
<img src="https://img.shields.io/badge/python-≥3.9-blue"> </a>
<img src="https://img.shields.io/badge/MySQL-≥5.7-blue"> <a href="https://github.com/insistence/RuoYi-Vue3-FastAPI/actions?query=branch%3Amaster+event%3Apush+workflow%3A%22%22Playwright+Tests%22%22">
<img alt="Playwright Tests" src="https://github.com/insistence/RuoYi-Vue3-FastAPI/workflows/Playwright Tests/badge.svg">
</a>
<a href="https://github.com/insistence/RuoYi-Vue3-FastAPI/actions?query=branch%3Amaster+event%3Apush+workflow%3A%22%22Ruff+Check%22%22">
<img alt="Ruff Check" src="https://github.com/insistence/RuoYi-Vue3-FastAPI/workflows/Ruff Check/badge.svg">
</a>
<a href="https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI">
<img alt="project version" src="https://img.shields.io/badge/version-1.10.0-brightgreen.svg">
</a>
<a href="https://github.com/astral-sh/ruff">
<img alt="Ruff" src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json">
</a>
<a href="https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/blob/master/LICENSE">
<img alt="LICENSE" src="https://img.shields.io/github/license/mashape/apistatus.svg">
</a>
<img alt="node version" src="https://img.shields.io/badge/node-≥18-blue">
<img alt="python version" src="https://img.shields.io/badge/python-≥3.10-blue">
<img alt="mysql version" src="https://img.shields.io/badge/MySQL-≥5.7-blue">
<img alt="redis version" src="https://img.shields.io/badge/redis-≥6.2-blue">
</p> </p>
## 平台简介 ## 平台简介
@@ -18,92 +36,174 @@
RuoYi-Vue3-FastAPI是一套全部开源的快速开发平台,毫无保留给个人及企业免费使用。 RuoYi-Vue3-FastAPI是一套全部开源的快速开发平台,毫无保留给个人及企业免费使用。
* 前端采用Vue3、Element Plus,基于<u>[RuoYi-Vue3](https://github.com/yangzongzhuan/RuoYi-Vue3)</u>前端项目修改。 * 前端采用Vue3、Element Plus,基于<u>[RuoYi-Vue3](https://github.com/yangzongzhuan/RuoYi-Vue3)</u>前端项目修改。
* 移动端采用uni-app、Vue3、Vite,内置tailwindcss,基于<u>[RuoYi-App](https://github.com/yangzongzhuan/RuoYi-App)</u>项目修改。
* 后端采用FastAPI、sqlalchemy、MySQLPostgreSQL)、Redis、OAuth2 & Jwt。 * 后端采用FastAPI、sqlalchemy、MySQLPostgreSQL)、Redis、OAuth2 & Jwt。
* 权限认证使用OAuth2 & Jwt,支持多终端认证系统。 * 权限认证使用OAuth2 & Jwt,支持多终端认证系统。
* 支持加载动态权限菜单,多方式轻松权限控制。 * 支持加载动态权限菜单,多方式轻松权限控制。
* Vue2版本: * Vue2版本:
- Gitte仓库地址:https://gitee.com/insistence2022/RuoYi-Vue-FastAPI * Gitte仓库地址:<https://gitee.com/insistence2022/RuoYi-Vue-FastAPI>
- GitHub仓库地址:https://github.com/insistence/RuoYi-Vue-FastAPI * GitHub仓库地址:<https://github.com/insistence/RuoYi-Vue-FastAPI>
* 纯Python版本: * 纯Python版本:
- Gitte仓库地址:https://gitee.com/insistence2022/dash-fastapi-admin * Gitte仓库地址:<https://gitee.com/insistence2022/dash-fastapi-admin>
- GitHub仓库地址:https://github.com/insistence/Dash-FastAPI-Admin * GitHub仓库地址:<https://github.com/insistence/Dash-FastAPI-Admin>
* 特别鸣谢:<u>[RuoYi-Vue3](https://github.com/yangzongzhuan/RuoYi-Vue3)</u> * 特别鸣谢:<u>[RuoYi-Vue3](https://github.com/yangzongzhuan/RuoYi-Vue3)</u>、<u>[RuoYi-App](https://github.com/yangzongzhuan/RuoYi-App)</u>
## 内置功能 ## 内置功能
1. 用户管理:用户是系统操作者,该功能主要完成系统用户配置。 1. 用户管理:用户是系统操作者,该功能主要完成系统用户配置。
2. 角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分。 2. 角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分。
3. 菜单管理:配置系统菜单,操作权限,按钮权限标识等。 3. 菜单管理:配置系统菜单,操作权限,按钮权限标识等。
4. 部门管理:配置系统组织机构(公司、部门、小组)。 4. 部门管理:配置系统组织机构(公司、部门、小组)。
5. 岗位管理:配置系统用户所属担任职务。 5. 岗位管理:配置系统用户所属担任职务。
6. 字典管理:对系统中经常使用的一些较为固定的数据进行维护。 6. 字典管理:对系统中经常使用的一些较为固定的数据进行维护。
7. 参数管理:对系统动态配置常用参数。 7. 参数管理:对系统动态配置常用参数。
8. 通知公告:系统通知公告信息发布维护。 8. 通知公告:系统通知公告信息发布维护。
9. 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。 9. 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
10. 登录日志:系统登录日志记录查询包含登录异常。 10. 登录日志:系统登录日志记录查询包含登录异常。
11. 在线用户:当前系统中活跃用户状态监控。 11. 在线用户:当前系统中活跃用户状态监控。
12. 定时任务:在线(添加、修改、删除)任务调度包含执行结果日志。 12. 定时任务:在线(添加、修改、删除)任务调度包含执行结果日志。
13. 服务监控:监视当前系统CPU、内存、磁盘、堆栈等相关信息。 13. 服务监控:监视当前系统CPU、内存、磁盘、堆栈等相关信息。
14. 缓存监控:对系统的缓存信息查询,命令统计等。 14. 缓存监控:对系统的缓存信息查询,命令统计等。
15. 在线构建器:拖动表单元素生成相应的HTML代码 15. 传输加密:支持前后端请求加密、响应解密、公钥轮换、运行策略下发与监控统计
16. 系统接口:根据业务代码自动生成相关的api接口文档 16. 在线构建器:拖动表单元素生成相应的HTML代码
17. 代码生成:配置数据库表信息一键生成前后端代码(python、sql、vue、js),支持下载 17. 系统接口:根据业务代码自动生成相关的api接口文档
18. 代码生成:配置数据库表信息一键生成前后端代码(python、sql、vue、js),支持下载。
19. AI管理:提供AI模型管理和AI对话功能。
20. 文件管理:统一管理公开文件和受保护附件,支持访问控制、业务引用保护、操作审计、回收站、保留策略及存储对账。
21. 插件系统:支持插件发现、安装、启停、升级、卸载与清理,提供依赖与配置管理、迁移与种子、定时任务、菜单权限、批量预演、健康诊断、操作审计及前后端插件脚手架。
## 演示图 ## 演示图
<table> <table>
<tr> <tr>
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/login.png"/></td> <td>
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/dashboard.png"/></td> <img alt="login" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/login.png">
</td>
<td>
<img alt="dashboard" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/dashboard.png">
</td>
</tr> </tr>
<tr> <tr>
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/user.png"/></td> <td>
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/role.png"/></td> <img alt="user" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/user.png">
</td>
<td>
<img alt="role" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/role.png">
</td>
</tr> </tr>
<tr> <tr>
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/menu.png"/></td> <td>
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/dept.png"/></td> <img alt="menu" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/menu.png">
</td>
<td>
<img alt="dept" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/dept.png">
</td>
</tr> </tr>
<tr> <tr>
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/post.png"/></td> <td>
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/dict.png"/></td> <img alt=""post src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/post.png">
</tr> </td>
<td>
<img alt="dict" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/dict.png">
</td>
</tr>
<tr> <tr>
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/config.png"/></td> <td>
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/notice.png"/></td> <img alt="config" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/config.png">
</td>
<td>
<img alt="notice" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/notice.png">
</td>
</tr> </tr>
<tr> <tr>
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/operLog.png"/></td> <td>
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/loginLog.png"/></td> <img alt="operLog" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/operLog.png">
</td>
<td>
<img alt="loginLog" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/loginLog.png">
</td>
</tr> </tr>
<tr> <tr>
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/online.png"/></td> <td>
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/job.png"/></td> <img alt="online" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/online.png">
</td>
<td>
<img alt="job" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/job.png">
</td>
</tr> </tr>
<tr> <tr>
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/server.png"/></td> <td>
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/cache.png"/></td> <img alt="server" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/server.png">
</td>
<td>
<img alt="cache" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/cache.png">
</td>
</tr> </tr>
<tr> <tr>
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/cacheList.png"></td> <td>
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/form.png"></td> <img alt="cacheList" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/cacheList.png">
</td>
<td>
<img alt="form" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/form.png">
</td>
</tr> </tr>
<tr> <tr>
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/api.png"></td> <td>
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/gen.png"/></td> <img alt="api" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/api.png">
</td>
<td>
<img alt="gen" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/gen.png">
</td>
</tr> </tr>
<tr> <tr>
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/profile.png"/></td> <td>
<img alt="aiModel" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/aiModel.png">
</td>
<td>
<img alt="aiChat" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/aiChat.png">
</td>
</tr>
<tr>
<td>
<img alt="file" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/file.png">
</td>
<td>
<img alt="plugin" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/plugin.png">
</td>
</tr>
<tr>
<td>
<img alt="profile" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/profile.png">
</td>
</tr>
</table>
<table>
<tr>
<td>
<img alt="applogin" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/app/login.png">
</td>
<td>
<img alt="appWorkbench" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/app/workbench.png">
</td>
<td>
<img alt="appProfile" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/app/profile.png">
</td>
</tr> </tr>
</table> </table>
## 在线体验 ## 在线体验
- *账号:admin*
- *密码admin123* * *账号admin*
- 演示地址:<a href="https://vfadmin.insistence.tech">vfadmin管理系统<a> * *密码:admin123*
* 演示地址:<a href="https://vfadmin.insistence.tech">vfadmin管理系统<a>
## 项目开发及发布相关 ## 项目开发及发布相关
### 传输层加解密配置说明
后端密钥配置与轮换说明:[ruoyi-fastapi-backend/docs/transport_crypto_config.md](./ruoyi-fastapi-backend/docs/transport_crypto_config.md)
### 开发 ### 开发
```bash ```bash
@@ -115,6 +215,7 @@ cd RuoYi-Vue3-FastAPI
``` ```
#### 前端 #### 前端
```bash ```bash
# 进入前端目录 # 进入前端目录
cd ruoyi-fastapi-frontend cd ruoyi-fastapi-frontend
@@ -129,7 +230,27 @@ npm install --registry=https://registry.npmmirror.com
npm run dev 或 yarn dev npm run dev 或 yarn dev
``` ```
#### 移动端
```bash
# 进入移动端目录
cd ruoyi-fastapi-app
# 安装依赖
npm install -g pnpm
pnpm install
# 启动 H5
pnpm dev:h5
# 启动微信小程序
pnpm dev:mp-weixin
```
移动端详细文档请参考:[ruoyi-fastapi-app/README.md](./ruoyi-fastapi-app/README.md)
#### 后端 #### 后端
```bash ```bash
# 进入后端目录 # 进入后端目录
cd ruoyi-fastapi-backend cd ruoyi-fastapi-backend
@@ -139,6 +260,9 @@ pip3 install -r requirements.txt
# 如果使用的是PostgreSQL数据库,请执行以下命令安装项目依赖环境 # 如果使用的是PostgreSQL数据库,请执行以下命令安装项目依赖环境
pip3 install -r requirements-pg.txt pip3 install -r requirements-pg.txt
# 安装AI插件依赖,如果不需要AI插件,可忽略此步骤
ruoyi plugin install-deps ai
# 配置环境 # 配置环境
在.env.dev文件中配置开发环境的数据库和redis 在.env.dev文件中配置开发环境的数据库和redis
@@ -147,10 +271,13 @@ pip3 install -r requirements-pg.txt
2.如果使用的是MySQL数据库,使用命令或数据库连接工具运行sql文件夹下的ruoyi-fastapi.sql;如果使用的是PostgreSQL数据库,使用命令或数据库连接工具运行sql文件夹下的ruoyi-fastapi-pg.sql 2.如果使用的是MySQL数据库,使用命令或数据库连接工具运行sql文件夹下的ruoyi-fastapi.sql;如果使用的是PostgreSQL数据库,使用命令或数据库连接工具运行sql文件夹下的ruoyi-fastapi-pg.sql
# 运行后端 # 运行后端
python3 app.py --env=dev ruoyi app run --env=dev
``` ```
后端 CLI 使用说明请参考:[ruoyi-fastapi-backend/docs/cli_usage.md](./ruoyi-fastapi-backend/docs/cli_usage.md)
#### 访问 #### 访问
```bash ```bash
# 默认账号密码 # 默认账号密码
账号:admin 账号:admin
@@ -163,6 +290,7 @@ python3 app.py --env=dev
### 发布 ### 发布
#### 前端 #### 前端
```bash ```bash
# 构建测试环境 # 构建测试环境
npm run build:stage 或 yarn build:stage npm run build:stage 或 yarn build:stage
@@ -172,22 +300,46 @@ npm run build:prod 或 yarn build:prod
``` ```
#### 后端 #### 后端
```bash ```bash
# 配置环境 # 配置环境
在.env.prod文件中配置生产环境的数据库和redis 在.env.prod文件中配置生产环境的数据库和redis
# 运行后端 # 运行后端
python3 app.py --env=prod ruoyi app run --env=prod
```
### Docker Compose部署方式
> ⚠️ **警告:** 默认未做数据持久化配置,请注意数据备份或自行配置持久化
#### MySQL版本
```bash
docker compose -f docker-compose.my.yml up -d --build
```
#### PostgreSQL版本
```bash
docker compose -f docker-compose.pg.yml up -d --build
``` ```
## 交流与赞助 ## 交流与赞助
如果有对本项目及FastAPI感兴趣的朋友,欢迎加入知识星球一起交流学习,让我们一起变得更强。如果你觉得这个项目帮助到了你,你可以请作者喝杯咖啡表示鼓励☕。扫描下面微信二维码添加微信备注VF-Admin即可进群。 如果有对本项目及FastAPI感兴趣的朋友,欢迎加入知识星球一起交流学习,让我们一起变得更强。如果你觉得这个项目帮助到了你,你可以请作者喝杯咖啡表示鼓励☕。扫描下面微信二维码添加微信备注VF-Admin即可进群。
<table> <table>
<tr> <tr>
<td><img alt="zsxq" src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/zsxq.jpg"></td> <td>
<td><img alt="zanzhu" src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/zanzhu.jpg"></td> <img alt="zsxq" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/common/zsxq.jpg">
</td>
<td>
<img alt="zanzhu" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/common/zanzhu.jpg">
</td>
</tr> </tr>
<tr> <tr>
<td><img alt="wxcode" src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/wxcode.jpg"></td> <td>
<img alt="wxcode" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/common/wxcode.jpg">
</td>
</tr> </tr>
</table> </table>
+73
View File
@@ -0,0 +1,73 @@
services:
# 前端服务
ruoyi-frontend:
build:
context: ./ruoyi-fastapi-frontend
dockerfile: Dockerfile
image: ruoyi-frontend:latest
container_name: ruoyi-frontend
ports:
- "12580:80"
volumes:
- ./ruoyi-fastapi-frontend/bin/nginx.dockermy.conf:/etc/nginx/conf.d/default.conf
depends_on:
- ruoyi-backend-my
networks:
- ruoyi-network
# 后端服务(MySQL版本)
ruoyi-backend-my:
build:
context: ./ruoyi-fastapi-backend
dockerfile: Dockerfile.my
image: ruoyi-backend-my:latest
container_name: ruoyi-backend-my
ports:
- "19099:9099"
depends_on:
ruoyi-mysql:
condition: service_healthy
ruoyi-redis:
condition: service_healthy
networks:
- ruoyi-network
# MySQL服务
ruoyi-mysql:
image: mysql:8.0
container_name: ruoyi-mysql
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: ruoyi-fastapi
ports:
- "13306:3306"
volumes:
- ./ruoyi-fastapi-backend/sql/ruoyi-fastapi.sql:/docker-entrypoint-initdb.d/ruoyi-fastapi.sql
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci --skip-character-set-client-handshake=1
networks:
- ruoyi-network
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-proot"]
interval: 5s
timeout: 10s
retries: 30
# Redis服务
ruoyi-redis:
image: redis:latest
container_name: ruoyi-redis
ports:
- "16379:6379"
networks:
- ruoyi-network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 10s
retries: 30
# 网络配置
networks:
ruoyi-network:
name: ruoyi-network
driver: bridge
+73
View File
@@ -0,0 +1,73 @@
services:
# 前端服务
ruoyi-frontend:
build:
context: ./ruoyi-fastapi-frontend
dockerfile: Dockerfile
image: ruoyi-frontend:latest
container_name: ruoyi-frontend
ports:
- "12580:80"
volumes:
- ./ruoyi-fastapi-frontend/bin/nginx.dockerpg.conf:/etc/nginx/conf.d/default.conf
depends_on:
- ruoyi-backend-pg
networks:
- ruoyi-network
# 后端服务(PostgreSQL版本)
ruoyi-backend-pg:
build:
context: ./ruoyi-fastapi-backend
dockerfile: Dockerfile.pg
image: ruoyi-backend-pg:latest
container_name: ruoyi-backend-pg
ports:
- "19099:9099"
depends_on:
ruoyi-pg:
condition: service_healthy
ruoyi-redis:
condition: service_healthy
networks:
- ruoyi-network
# PostgreSQL服务
ruoyi-pg:
image: postgres:14
container_name: ruoyi-pg
environment:
POSTGRES_PASSWORD: root
POSTGRES_DB: ruoyi-fastapi
POSTGRES_INITDB_ARGS: --encoding=UTF8 --lc-collate=C --lc-ctype=C
ports:
- "15432:5432"
volumes:
- ./ruoyi-fastapi-backend/sql/ruoyi-fastapi-pg.sql:/docker-entrypoint-initdb.d/ruoyi-fastapi-pg.sql
networks:
- ruoyi-network
healthcheck:
test: ["CMD", "pg_isready", "-U", "postgres"]
interval: 5s
timeout: 10s
retries: 30
# Redis服务
ruoyi-redis:
image: redis:latest
container_name: ruoyi-redis
ports:
- "16379:6379"
networks:
- ruoyi-network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 10s
retries: 30
# 网络配置
networks:
ruoyi-network:
name: ruoyi-network
driver: bridge
+12
View File
@@ -0,0 +1,12 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false
+27
View File
@@ -0,0 +1,27 @@
.DS_Store
node_modules/
unpackage/
dist/
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.project
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw*
# 要提交 .env 来确保 jit v2 的开发 watch mode
!.env
src/ignore
pnpm-lock.yaml
+7
View File
@@ -0,0 +1,7 @@
# 设置国内镜像地址
registry=http://registry.npmmirror.com/
# registry=http://registry.npmjs.com/
# 这个是给 pnpm 用的
shamefully-hoist=true
ignore-engines=true
+78
View File
@@ -0,0 +1,78 @@
# RuoYi-FastAPI-App
基于 `uni-app``vite` + `vue3` + `tailwindcss` 开发。
## 特性
- ⚡️ [Vue 3](https://github.com/vuejs/core), [Vite](https://github.com/vitejs/vite), [pnpm](https://pnpm.io/) - 快 & 稳定
- 🎨 [TailwindCSS](https://tailwindcss.com/) - 世界上最流行,生态最好的原子化CSS框架
- 😃 [集成 Iconify](https://github.com/egoist/tailwindcss-icons) - [icones.js.org](https://icones.js.org/) 中的所有图标都为你所用
- 📥 [API 自动加载](https://github.com/antfu/unplugin-auto-import) - 直接使用 Composition API 无需引入
- 🧬 [uni-app 条件编译样式](https://tw.icebreaker.top/docs/quick-start/uni-app-css-macro) - 帮助你在多端更灵活的使用 `TailwindCSS`
- 🦾 [TypeScript](https://www.typescriptlang.org/) & [ESLint](https://eslint.org/) & [Stylelint](https://stylelint.io/) - 样式,类型,统一的校验与格式化规则,保证你的代码风格和质量
## 快速开始
> [!IMPORTANT]
> 推荐使用 `"node": "^20.19.0 || >=22.12.0"` 的 Node.js 版本进行开发!
>
> 另外谨慎升级 `package.json` 中锁定的 `pinia`/`vue`/`@vue/*` 相关包的版本,新版本可能 `uni-app` 没有兼容,造成一些奇怪的 bug
### vscode
使用 `vscode` 的开发者,请先安装 [Tailwind CSS IntelliSense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) 智能提示与感应插件
其他 IDE 请参考: <https://tw.icebreaker.top/docs/quick-start/intelliSense>
### 更换 Appid
`src/manifest.json` 中的 `appid`, 更换为你自己的 `appid`, 比如 `uni-app` / `mp-weixin` 平台。
## 升级依赖
- `pnpm up:pkg` 升级除了 `uni-app` 相关的其他依赖
- `pnpm up:uniapp` 升级 `uni-app` 相关的依赖
推荐先使用 `pnpm up:pkg` 升级, 再使用 `pnpm up:uniapp` 进行升级,因为 `pnpm up:uniapp` 很有可能会进行版本的降级已达到和 `uni-app` 版本匹配的效果
## 切换镜像源
默认情况下,走的是淘宝镜像源 : `registry.npmmirror.com`
假如你需要修改镜像源,请修改目录下的 `.npmrc` 文件,然后重新进行 `pnpm i` 安装包即可
## 包管理器
本项目默认使用 `pnpm@10` 进行管理,当然你也可以切换到其他包管理器,比如 `yarn`, `npm`
你只需要把 `pnpm-lock.yaml` 删掉,然后把 `package.json` 中的 `packageManager` 字段去除或者换成你具体的包管理器版本,然后重新安装即可
### weapp-ide-cli
本项目已经集成 `weapp-ide-cli` 可以通过 `cli``ide` 进行额外操作
- `pnpm open:dev` 打开微信开发者工具,引入 `dist/dev/mp-weixin`
- `pnpm open:build` 打开微信开发者工具,引入 `dist/build/mp-weixin`
[详细信息](https://www.npmjs.com/package/weapp-ide-cli)
## tailwindcss 生态
详见:<https://github.com/aniftyco/awesome-tailwindcss>
你可以在这里找到许多现成的UI,组件模板。
## 单位转换
- `rem` -> `rpx` (默认开启, 见 `vite.config.ts``uvtw` 插件的 `rem2rpx` 选项)
- `px` -> `rpx` (默认不开启,可在 `postcss.config.ts` 中引入 `postcss-pxtransform` 开启配置)
## Tips
- 升级 `uni-app` 依赖的方式为 `npx @dcloudio/uvm` 后,选择对应的 `Package Manager` 即可。而升级其他包的方式,可以使用 `pnpm up -Li`,这个是 `pnpm` 自带的方式。
- 使用 `vscode` 记得安装官方插件 `stylelint`,`tailwindcss`, 已在 `.vscode/extensions.json` 中设置推荐
+29
View File
@@ -0,0 +1,29 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script>
var coverSupport =
'CSS' in window &&
typeof CSS.supports === 'function' &&
(CSS.supports('top: env(a)') || CSS.supports('top: constant(a)'))
document.write(
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
(coverSupport ? ', viewport-fit=cover' : '') +
'" />',
)
</script>
<style>
html {
font-size: 16px !important;
}
</style>
<title></title>
<!--preload-links-->
<!--app-context-->
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
+13
View File
@@ -0,0 +1,13 @@
# netlify 部署文件
# 用于部署此项目的 h5 产物,可删除
[build]
publish = "dist/build/h5"
command = "pnpm run build:h5"
[build.environment]
NODE_VERSION = "20"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
+98
View File
@@ -0,0 +1,98 @@
{
"name": "ruoyi-fastapi-app",
"version": "1.10.0",
"packageManager": "pnpm@10.28.1",
"scripts": {
"dev": "uni -p mp-weixin",
"build": "npm run build:mp-weixin",
"dev:app": "uni -p app",
"dev:custom": "uni -p",
"dev:h5": "uni --host",
"dev:h5:ssr": "uni --ssr",
"dev:mp-alipay": "uni -p mp-alipay",
"dev:mp-baidu": "uni -p mp-baidu",
"dev:mp-kuaishou": "uni -p mp-kuaishou",
"dev:mp-lark": "uni -p mp-lark",
"dev:mp-qq": "uni -p mp-qq",
"dev:mp-toutiao": "uni -p mp-toutiao",
"dev:mp-weixin": "uni -p mp-weixin",
"dev:quickapp-webview": "uni -p quickapp-webview",
"dev:quickapp-webview-huawei": "uni -p quickapp-webview-huawei",
"dev:quickapp-webview-union": "uni -p quickapp-webview-union",
"build:app": "uni build -p app",
"build:custom": "uni build -p",
"build:h5": "uni build",
"build:h5:ssr": "uni build --ssr",
"build:mp-alipay": "uni build -p mp-alipay",
"build:mp-baidu": "uni build -p mp-baidu",
"build:mp-kuaishou": "uni build -p mp-kuaishou",
"build:mp-lark": "uni build -p mp-lark",
"build:mp-qq": "uni build -p mp-qq",
"build:mp-toutiao": "uni build -p mp-toutiao",
"build:mp-weixin": "uni build -p mp-weixin",
"build:quickapp-webview": "uni build -p quickapp-webview",
"build:quickapp-webview-huawei": "uni build -p quickapp-webview-huawei",
"build:quickapp-webview-union": "uni build -p quickapp-webview-union",
"open:dev": "weapp open -p dist/dev/mp-weixin",
"open:build": "weapp open -p dist/build/mp-weixin",
"weapp:login": "weapp login",
"upload:dev": "weapp upload -p dist/dev/mp-weixin -v 1.0.0 -d \"dev version\"",
"upload:build": "weapp upload -p dist/build/mp-weixin -v 1.0.0 -d \"release version\"",
"postinstall": "weapp-tw patch",
"up:pkg": "pnpm up -rLi \"!@dcloudio/*\"",
"up:uniapp": "pnpx @dcloudio/uvm@latest"
},
"dependencies": {
"@dcloudio/uni-app": "3.0.0-4080720251210001",
"@dcloudio/uni-app-harmony": "3.0.0-4080720251210001",
"@dcloudio/uni-app-plus": "3.0.0-4080720251210001",
"@dcloudio/uni-components": "3.0.0-4080720251210001",
"@dcloudio/uni-h5": "3.0.0-4080720251210001",
"@dcloudio/uni-mp-alipay": "3.0.0-4080720251210001",
"@dcloudio/uni-mp-baidu": "3.0.0-4080720251210001",
"@dcloudio/uni-mp-harmony": "3.0.0-4080720251210001",
"@dcloudio/uni-mp-jd": "3.0.0-4080720251210001",
"@dcloudio/uni-mp-kuaishou": "3.0.0-4080720251210001",
"@dcloudio/uni-mp-lark": "3.0.0-4080720251210001",
"@dcloudio/uni-mp-qq": "3.0.0-4080720251210001",
"@dcloudio/uni-mp-toutiao": "3.0.0-4080720251210001",
"@dcloudio/uni-mp-weixin": "3.0.0-4080720251210001",
"@dcloudio/uni-mp-xhs": "3.0.0-4080720251210001",
"@dcloudio/uni-quickapp-webview": "3.0.0-4080720251210001",
"@vue/shared": "3.4.21",
"@weapp-tailwindcss/merge-v3": "^0.1.5",
"node-forge": "^1.4.0",
"pinia": "2.2.4",
"vue": "^3.4.21",
"vue-i18n": "^9.1.9",
"vuex": "^4.0.2"
},
"devDependencies": {
"@dcloudio/types": "^3.4.8",
"@dcloudio/uni-automator": "3.0.0-4080720251210001",
"@dcloudio/uni-cli-shared": "3.0.0-4080720251210001",
"@dcloudio/uni-stacktracey": "3.0.0-4080720251210001",
"@dcloudio/vite-plugin-uni": "3.0.0-4080720251210001",
"@egoist/tailwindcss-icons": "^1.9.2",
"@icebreakers/stylelint-config": "^1.2.5",
"@iconify-json/mdi": "^1.2.3",
"@iconify-json/svg-spinners": "^1.2.4",
"@types/node": "^24.10.1",
"@vue/runtime-core": "^3.5.27",
"autoprefixer": "^10.4.24",
"postcss": "^8.5.6",
"sass": "^1.97.3",
"tailwindcss": "^3.4.19",
"typescript": "^5.9.3",
"unplugin-auto-import": "^20.3.0",
"vite": "5.4.21",
"weapp-ide-cli": "^5.0.1",
"weapp-tailwindcss": "^4.9.8"
},
"pnpm": {
"onlyBuiltDependencies": [
"weapp-tailwindcss",
"@weapp-tailwindcss/merge-v3"
]
}
}
+8
View File
@@ -0,0 +1,8 @@
import process from "node:process";
const isH5 = process.env.UNI_PLATFORM === "h5";
const isApp = process.env.UNI_PLATFORM === "app";
const WeappTailwindcssDisabled = isH5 || isApp;
const isMp = !isH5 && !isApp;
export { isApp, isH5, isMp, WeappTailwindcssDisabled };
+13
View File
@@ -0,0 +1,13 @@
import type { AcceptedPlugin } from "postcss";
import autoprefixer from "autoprefixer";
import tailwindcss from "tailwindcss";
import cssMacro from "weapp-tailwindcss/css-macro/postcss";
const plugins: AcceptedPlugin[] = [tailwindcss(), autoprefixer()];
// 可以使用 postcss-pxtransform 来进行 px 转 rpx 的功能
// 详见: https://tw.icebreaker.top/docs/quick-start/css-unit-transform#px-%E8%BD%AC-rpx
plugins.push(cssMacro);
export default plugins;
+79
View File
@@ -0,0 +1,79 @@
<script setup>
import config from "./config";
import { getToken } from "@/utils/auth";
import { useConfigStore } from "@/store";
import { getCurrentInstance } from "vue";
import { onLaunch } from "@dcloudio/uni-app";
const { proxy } = getCurrentInstance();
onLaunch(() => {
initApp();
});
// 初始化应用
function initApp() {
// 初始化应用配置
initConfig();
// 检查用户登录状态
//#ifdef H5
checkLogin();
//#endif
}
function initConfig() {
useConfigStore().setConfig(config);
}
function checkLogin() {
if (!getToken()) {
proxy.$tab.reLaunch("/pages/login");
}
}
</script>
<style>
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Global Reset for UniApp/Mobile */
page,
body {
height: 100%;
min-height: 100%;
overflow-x: hidden;
/* Prevent default bounce effect on iOS if needed, or just handle overflow */
}
/* Global box-sizing for consistency with Tailwind preflight */
page,
view,
scroll-view,
image,
text,
button,
input,
textarea,
label,
navigator {
box-sizing: border-box;
}
/* 修复 H5 端 uni.showToast 图标在引入 Tailwind 后可能偏左的问题 */
/* #ifdef H5 */
uni-toast img,
uni-toast svg {
display: inline-block !important;
}
/* #endif */
/* Hide scrollbar for Chrome/Safari/Webkit */
::-webkit-scrollbar {
display: none;
width: 0 !important;
height: 0 !important;
-webkit-appearance: none;
background: transparent;
}
</style>
+62
View File
@@ -0,0 +1,62 @@
import request from "@/utils/request";
// 登录方法
export function login(username, password, code, uuid) {
const data = {
username,
password,
code,
uuid,
};
return request({
url: "/login",
header: {
"Content-Type": "application/x-www-form-urlencoded",
},
headers: {
isToken: false,
},
method: "post",
data: data,
});
}
// 注册方法
export function register(data) {
return request({
url: "/register",
headers: {
isToken: false,
},
method: "post",
data: data,
});
}
// 获取用户详细信息
export function getInfo() {
return request({
url: "/getInfo",
method: "get",
});
}
// 退出方法
export function logout() {
return request({
url: "/logout",
method: "post",
});
}
// 获取验证码
export function getCodeImg() {
return request({
url: "/captchaImage",
headers: {
isToken: false,
},
method: "get",
timeout: 20000,
});
}
@@ -0,0 +1,52 @@
import request from "@/utils/request";
// 查询字典数据列表
export function listData(query) {
return request({
url: "/system/dict/data/list",
method: "get",
params: query,
});
}
// 查询字典数据详细
export function getData(dictCode) {
return request({
url: "/system/dict/data/" + dictCode,
method: "get",
});
}
// 根据字典类型查询字典数据信息
export function getDicts(dictType) {
return request({
url: "/system/dict/data/type/" + dictType,
method: "get",
});
}
// 新增字典数据
export function addData(data) {
return request({
url: "/system/dict/data",
method: "post",
data: data,
});
}
// 修改字典数据
export function updateData(data) {
return request({
url: "/system/dict/data",
method: "put",
data: data,
});
}
// 删除字典数据
export function delData(dictCode) {
return request({
url: "/system/dict/data/" + dictCode,
method: "delete",
});
}
@@ -0,0 +1,60 @@
import request from "@/utils/request";
// 查询字典类型列表
export function listType(query) {
return request({
url: "/system/dict/type/list",
method: "get",
params: query,
});
}
// 查询字典类型详细
export function getType(dictId) {
return request({
url: "/system/dict/type/" + dictId,
method: "get",
});
}
// 新增字典类型
export function addType(data) {
return request({
url: "/system/dict/type",
method: "post",
data: data,
});
}
// 修改字典类型
export function updateType(data) {
return request({
url: "/system/dict/type",
method: "put",
data: data,
});
}
// 删除字典类型
export function delType(dictId) {
return request({
url: "/system/dict/type/" + dictId,
method: "delete",
});
}
// 刷新字典缓存
export function refreshCache() {
return request({
url: "/system/dict/type/refreshCache",
method: "delete",
});
}
// 获取字典选择框列表
export function optionselect() {
return request({
url: "/system/dict/type/optionselect",
method: "get",
});
}
+41
View File
@@ -0,0 +1,41 @@
import upload from "@/utils/upload";
import request from "@/utils/request";
// 用户密码重置
export function updateUserPwd(oldPassword, newPassword) {
const data = {
oldPassword,
newPassword,
};
return request({
url: "/system/user/profile/updatePwd",
method: "put",
data: data,
});
}
// 查询用户个人信息
export function getUserProfile() {
return request({
url: "/system/user/profile",
method: "get",
});
}
// 修改用户个人信息
export function updateUserProfile(data) {
return request({
url: "/system/user/profile",
method: "put",
data: data,
});
}
// 用户头像上传
export function uploadAvatar(data) {
return upload({
url: "/system/user/profile/avatar",
name: data.name,
filePath: data.filePath,
});
}
+109
View File
@@ -0,0 +1,109 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
const EffectScope: typeof import('vue').EffectScope
const acceptHMRUpdate: typeof import('pinia').acceptHMRUpdate
const computed: typeof import('vue').computed
const createApp: typeof import('vue').createApp
const createPinia: typeof import('pinia').createPinia
const customRef: typeof import('vue').customRef
const defineAsyncComponent: typeof import('vue').defineAsyncComponent
const defineComponent: typeof import('vue').defineComponent
const defineStore: typeof import('pinia').defineStore
const effectScope: typeof import('vue').effectScope
const getActivePinia: typeof import('pinia').getActivePinia
const getCurrentInstance: typeof import('vue').getCurrentInstance
const getCurrentScope: typeof import('vue').getCurrentScope
const getCurrentWatcher: typeof import('vue').getCurrentWatcher
const h: typeof import('vue').h
const inject: typeof import('vue').inject
const isProxy: typeof import('vue').isProxy
const isReactive: typeof import('vue').isReactive
const isReadonly: typeof import('vue').isReadonly
const isRef: typeof import('vue').isRef
const isShallow: typeof import('vue').isShallow
const mapActions: typeof import('pinia').mapActions
const mapGetters: typeof import('pinia').mapGetters
const mapState: typeof import('pinia').mapState
const mapStores: typeof import('pinia').mapStores
const mapWritableState: typeof import('pinia').mapWritableState
const markRaw: typeof import('vue').markRaw
const nextTick: typeof import('vue').nextTick
const onActivated: typeof import('vue').onActivated
const onAddToFavorites: typeof import('@dcloudio/uni-app').onAddToFavorites
const onBackPress: typeof import('@dcloudio/uni-app').onBackPress
const onBeforeMount: typeof import('vue').onBeforeMount
const onBeforeUnmount: typeof import('vue').onBeforeUnmount
const onBeforeUpdate: typeof import('vue').onBeforeUpdate
const onDeactivated: typeof import('vue').onDeactivated
const onError: typeof import('@dcloudio/uni-app').onError
const onErrorCaptured: typeof import('vue').onErrorCaptured
const onHide: typeof import('@dcloudio/uni-app').onHide
const onLaunch: typeof import('@dcloudio/uni-app').onLaunch
const onLoad: typeof import('@dcloudio/uni-app').onLoad
const onMounted: typeof import('vue').onMounted
const onNavigationBarButtonTap: typeof import('@dcloudio/uni-app').onNavigationBarButtonTap
const onNavigationBarSearchInputChanged: typeof import('@dcloudio/uni-app').onNavigationBarSearchInputChanged
const onNavigationBarSearchInputClicked: typeof import('@dcloudio/uni-app').onNavigationBarSearchInputClicked
const onNavigationBarSearchInputConfirmed: typeof import('@dcloudio/uni-app').onNavigationBarSearchInputConfirmed
const onNavigationBarSearchInputFocusChanged: typeof import('@dcloudio/uni-app').onNavigationBarSearchInputFocusChanged
const onPageNotFound: typeof import('@dcloudio/uni-app').onPageNotFound
const onPageScroll: typeof import('@dcloudio/uni-app').onPageScroll
const onPullDownRefresh: typeof import('@dcloudio/uni-app').onPullDownRefresh
const onReachBottom: typeof import('@dcloudio/uni-app').onReachBottom
const onReady: typeof import('@dcloudio/uni-app').onReady
const onRenderTracked: typeof import('vue').onRenderTracked
const onRenderTriggered: typeof import('vue').onRenderTriggered
const onResize: typeof import('@dcloudio/uni-app').onResize
const onScopeDispose: typeof import('vue').onScopeDispose
const onServerPrefetch: typeof import('vue').onServerPrefetch
const onShareAppMessage: typeof import('@dcloudio/uni-app').onShareAppMessage
const onShareTimeline: typeof import('@dcloudio/uni-app').onShareTimeline
const onShow: typeof import('@dcloudio/uni-app').onShow
const onTabItemTap: typeof import('@dcloudio/uni-app').onTabItemTap
const onThemeChange: typeof import('@dcloudio/uni-app').onThemeChange
const onUnhandledRejection: typeof import('@dcloudio/uni-app').onUnhandledRejection
const onUnload: typeof import('@dcloudio/uni-app').onUnload
const onUnmounted: typeof import('vue').onUnmounted
const onUpdated: typeof import('vue').onUpdated
const onWatcherCleanup: typeof import('vue').onWatcherCleanup
const provide: typeof import('vue').provide
const reactive: typeof import('vue').reactive
const readonly: typeof import('vue').readonly
const ref: typeof import('vue').ref
const resolveComponent: typeof import('vue').resolveComponent
const setActivePinia: typeof import('pinia').setActivePinia
const setMapStoreSuffix: typeof import('pinia').setMapStoreSuffix
const shallowReactive: typeof import('vue').shallowReactive
const shallowReadonly: typeof import('vue').shallowReadonly
const shallowRef: typeof import('vue').shallowRef
const storeToRefs: typeof import('pinia').storeToRefs
const toRaw: typeof import('vue').toRaw
const toRef: typeof import('vue').toRef
const toRefs: typeof import('vue').toRefs
const toValue: typeof import('vue').toValue
const triggerRef: typeof import('vue').triggerRef
const unref: typeof import('vue').unref
const useAttrs: typeof import('vue').useAttrs
const useCssModule: typeof import('vue').useCssModule
const useCssVars: typeof import('vue').useCssVars
const useId: typeof import('vue').useId
const useModel: typeof import('vue').useModel
const useSlots: typeof import('vue').useSlots
const useTemplateRef: typeof import('vue').useTemplateRef
const watch: typeof import('vue').watch
const watchEffect: typeof import('vue').watchEffect
const watchPostEffect: typeof import('vue').watchPostEffect
const watchSyncEffect: typeof import('vue').watchSyncEffect
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
}
+26
View File
@@ -0,0 +1,26 @@
// 应用全局配置
export default {
baseUrl: "http://localhost:9099",
// 应用信息
appInfo: {
// 应用名称
name: "RuoYi-FastAPI-APP",
// 应用版本
version: "1.10.0",
// 应用logo
logo: "/static/logo.png",
// 官方网站
site_url: "https://vfadmin.insistence.tech",
// 政策协议
agreements: [
{
title: "隐私政策",
url: "https://ruoyi.vip/protocol.html",
},
{
title: "用户服务协议",
url: "https://ruoyi.vip/protocol.html",
},
],
},
};
+8
View File
@@ -0,0 +1,8 @@
/// <reference types="vite/client" />
declare module "*.vue" {
import type { DefineComponent } from "vue";
const component: DefineComponent<object, object, any>;
export default component;
}
+17
View File
@@ -0,0 +1,17 @@
// @ts-nocheck
import { createSSRApp } from "vue";
import App from "./App.vue";
import store from "./store"; // store
import { install } from "./plugins"; // plugins
import "./permission"; // permission
import { useDict } from "@/utils/dict";
export function createApp() {
const app = createSSRApp(App);
app.use(store);
app.config.globalProperties.useDict = useDict;
install(app);
return {
app,
};
}
+70
View File
@@ -0,0 +1,70 @@
{
"name": "RuoYi-FastAPI移动端",
"appid": "__UNI__25A9D80",
"description": "",
"versionName": "1.10.0",
"versionCode": "100",
"transformPx": false,
"app-plus": {
"usingComponents": true,
"nvueCompiler": "uni-app",
"splashscreen": {
"alwaysShowBeforeRender": true,
"waiting": true,
"autoclose": true,
"delay": 0
},
"modules": {},
"distribute": {
"android": {
"permissions": [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
},
"ios": {
"dSYMs": false
},
"sdkConfigs": {}
}
},
"quickapp": {},
"mp-weixin": {
"appid": "wxccd7e2a0911b3397",
"setting": {
"urlCheck": false,
"es6": false,
"minified": true,
"postcss": true
},
"optimization": {
"subPackages": true
},
"usingComponents": true
},
"vueVersion": "3",
"h5": {
"devServer": {
"port": 9090,
"https": false
},
"title": "RuoYi-FastAPI-APP",
"router": {
"mode": "hash",
"base": "./"
}
}
}
+132
View File
@@ -0,0 +1,132 @@
{
"pages": [
{
"path": "pages/login",
"style": {
"navigationBarTitleText": "登录"
}
},
{
"path": "pages/register",
"style": {
"navigationBarTitleText": "注册"
}
},
{
"path": "pages/index",
"style": {
"navigationBarTitleText": "RuoYi-FastAPI移动端",
"navigationStyle": "custom"
}
},
{
"path": "pages/work/index",
"style": {
"navigationBarTitleText": "工作台"
}
},
{
"path": "pages/mine/index",
"style": {
"navigationBarTitleText": "我的"
}
},
{
"path": "pages/mine/avatar/index",
"style": {
"navigationBarTitleText": "修改头像"
}
},
{
"path": "pages/mine/info/index",
"style": {
"navigationBarTitleText": "个人信息"
}
},
{
"path": "pages/mine/info/edit",
"style": {
"navigationBarTitleText": "编辑资料"
}
},
{
"path": "pages/mine/pwd/index",
"style": {
"navigationBarTitleText": "修改密码"
}
},
{
"path": "pages/mine/setting/index",
"style": {
"navigationBarTitleText": "应用设置"
}
},
{
"path": "pages/mine/help/index",
"style": {
"navigationBarTitleText": "常见问题"
}
},
{
"path": "pages/mine/about/index",
"style": {
"navigationBarTitleText": "关于我们"
}
},
{
"path": "pages/common/agreement/index",
"style": {
"navigationBarTitleText": "用户协议"
}
},
{
"path": "pages/common/privacy/index",
"style": {
"navigationBarTitleText": "隐私协议"
}
},
{
"path": "pages/common/webview/index",
"style": {
"navigationBarTitleText": "浏览网页"
}
},
{
"path": "pages/common/textview/index",
"style": {
"navigationBarTitleText": "浏览文本"
}
}
],
"tabBar": {
"color": "#000000",
"selectedColor": "#000000",
"borderStyle": "white",
"backgroundColor": "#ffffff",
"list": [
{
"pagePath": "pages/index",
"iconPath": "static/images/tabbar/home.png",
"selectedIconPath": "static/images/tabbar/home_.png",
"text": "首页"
},
{
"pagePath": "pages/work/index",
"iconPath": "static/images/tabbar/work.png",
"selectedIconPath": "static/images/tabbar/work_.png",
"text": "工作台"
},
{
"pagePath": "pages/mine/index",
"iconPath": "static/images/tabbar/mine.png",
"selectedIconPath": "static/images/tabbar/mine_.png",
"text": "我的"
}
]
},
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "RuoYi",
"navigationBarBackgroundColor": "#FFFFFF"
}
}
@@ -0,0 +1,63 @@
<template>
<view class="min-h-full bg-white px-5 py-8">
<view class="mb-6 text-center">
<text class="text-xl font-bold text-gray-900">用户协议</text>
</view>
<view class="space-y-6 text-justify text-sm leading-relaxed text-gray-600">
<view>
<text class="mb-2 block font-bold text-gray-800"
>1. 服务条款的确认和接纳</text
>
<text
>欢迎使用RuoYi-FastAPI移动端框架以下简称本产品本产品的服务所有权和运作权归RuoYi-FastAPI所有用户在使用本产品前请仔细阅读本协议当您点击同意或实际使用本产品服务时即视为您已阅读并完全同意本协议的所有条款</text
>
</view>
<view>
<text class="mb-2 block font-bold text-gray-800"
>2. 用户账号与安全</text
>
<text
>您在注册账号时应提供真实准确最新和完整的个人资料您有责任维护账号和密码的安全并对以您账号进行的所有活动承担法律责任如发现账号被非法使用请立即通知我们</text
>
</view>
<view>
<text class="mb-2 block font-bold text-gray-800">3. 用户行为规范</text>
<text
>用户在使用本产品过程中必须遵守国家法律法规不得利用本产品从事违法违规活动包括但不限于发布虚假信息侵犯他人知识产权传播淫秽色情信息进行网络攻击等</text
>
</view>
<view>
<text class="mb-2 block font-bold text-gray-800"
>4. 服务内容的变更中断或终止</text
>
<text
>鉴于网络服务的特殊性我们有权随时变更中断或终止部分或全部网络服务对于因服务器维护网络故障等原因导致的服务中断我们将尽量提前公告但不对因此产生的损失承担责任</text
>
</view>
<view>
<text class="mb-2 block font-bold text-gray-800">5. 知识产权声明</text>
<text
>本产品包含的所有内容包括但不限于文字图片音频视频软件代码等的知识产权归RuoYi-FastAPI或相关权利人所有未经授权任何人不得擅自使用复制修改或传播</text
>
</view>
<view>
<text class="mb-2 block font-bold text-gray-800">6. 免责声明</text>
<text
>本产品按现状提供不包含任何明示或暗示的保证我们不对因不可抗力黑客攻击系统不稳定等原因导致的服务中断或数据丢失承担责任</text
>
</view>
</view>
<view class="mt-10 text-center text-xs text-gray-400">
<text>更新日期2024年01月01日</text>
</view>
</view>
</template>
<script setup></script>
@@ -0,0 +1,87 @@
<template>
<view class="min-h-full bg-white px-5 py-8">
<view class="mb-6 text-center">
<text class="text-xl font-bold text-gray-900">隐私协议</text>
</view>
<view class="space-y-6 text-justify text-sm leading-relaxed text-gray-600">
<view>
<text
>RuoYi-FastAPI非常重视您的隐私保护本隐私政策旨在向您说明我们如何收集使用存储和保护您的个人信息请您在使用本产品前仔细阅读本政策</text
>
</view>
<view>
<text class="mb-2 block font-bold text-gray-800"
>1. 我们如何收集和使用您的个人信息</text
>
<text>为了向您提供更好的服务我们可能会收集您的以下信息</text>
<text class="mt-1 block pl-2"
>
账号信息当您注册或登录时我们需要收集您的用户名密码手机号码等信息</text
>
<text class="mt-1 block pl-2"
>
设备信息为了保障账户安全和优化服务我们会收集您的设备型号操作系统版本唯一设备标识符等信息</text
>
<text class="mt-1 block pl-2"
>
日志信息当您使用本产品时我们会自动收集您的操作日志IP地址访问时间等信息</text
>
</view>
<view>
<text class="mb-2 block font-bold text-gray-800"
>2. 我们如何共享转让公开披露您的个人信息</text
>
<text
>我们不会向任何第三方共享转让或公开披露您的个人信息但以下情况除外</text
>
<text class="mt-1 block pl-2"> 获得您的明确授权或同意</text>
<text class="mt-1 block pl-2"> 根据法律法规或司法机关的要求</text>
<text class="mt-1 block pl-2"
> 为了维护国家安全公共安全或重大公共利益</text
>
</view>
<view>
<text class="mb-2 block font-bold text-gray-800"
>3. 我们如何保护您的个人信息</text
>
<text
>我们采取了多种安全措施来保护您的个人信息包括数据加密访问控制安全审计等我们会尽力防止您的信息被未经授权的访问篡改或泄露</text
>
</view>
<view>
<text class="mb-2 block font-bold text-gray-800">4. 您的权利</text>
<text
>您有权访问更正或删除您的个人信息您也可以随时注销账号如您需要行使这些权利请通过联系我们章节中的方式与我们联系</text
>
</view>
<view>
<text class="mb-2 block font-bold text-gray-800"
>5. 本政策如何更新</text
>
<text
>随着法律法规的变化或业务的发展我们可能会不时更新本隐私政策更新后的政策将在本产品中发布并在生效前通过适当方式通知您</text
>
</view>
<view>
<text class="mb-2 block font-bold text-gray-800">6. 联系我们</text>
<text
>如果您对本隐私政策有任何疑问或建议请通过以下方式联系我们</text
>
<text class="mt-1 block">邮箱xxx@xxx.com</text>
</view>
</view>
<view class="mt-10 text-center text-xs text-gray-400">
<text>更新日期2024年01月01日</text>
</view>
</view>
</template>
<script setup></script>
@@ -0,0 +1,37 @@
<template>
<view class="h-full overflow-y-auto p-4">
<view class="bg-white rounded-xl shadow-sm p-4">
<view
class="text-lg font-bold text-gray-900 mb-3 border-b border-gray-100 pb-2"
>
{{ title }}
</view>
<text class="block text-sm text-gray-600 leading-relaxed">
{{ content }}
</text>
</view>
</view>
</template>
<script setup>
import { ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
const title = ref("");
const content = ref("");
onLoad((options) => {
title.value = options.title;
content.value = options.content;
uni.setNavigationBarTitle({
title: options.title,
});
});
</script>
<style>
page {
height: 100%;
background-color: #f9fafb;
}
</style>
@@ -0,0 +1,33 @@
<template>
<view v-if="params.url">
<web-view :webview-styles="webviewStyles" :src="`${params.url}`"></web-view>
</view>
</template>
<script setup>
import { ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
const params = ref({});
const webviewStyles = ref({
progress: {
color: "#FF3333",
},
});
const props = defineProps({
src: {
type: String,
default: null,
},
});
onLoad((event) => {
params.value = event;
if (event.title) {
uni.setNavigationBarTitle({
title: event.title,
});
}
});
</script>
+208
View File
@@ -0,0 +1,208 @@
<template>
<view class="flex h-full flex-col bg-gray-50">
<!-- Header Area -->
<view class="bg-white pb-6 rounded-b-3xl shadow-sm z-50">
<!-- Title -->
<view class="pt-12 pb-4 px-4 flex justify-center items-center">
<text class="text-lg font-bold text-gray-800">RuoYi-FastAPI移动端</text>
</view>
<!-- Search & Notification -->
<view class="px-4 pb-2">
<view class="flex items-center space-x-3">
<!-- Search Input -->
<view class="flex-1 relative">
<view
class="absolute left-3 top-0 bottom-0 flex items-center text-gray-400"
>
<view class="i-mdi-magnify text-lg"></view>
</view>
<input
type="text"
placeholder="搜索..."
placeholder-class="text-gray-400"
class="w-full h-10 rounded-full bg-gray-100 pl-10 pr-4 text-sm text-gray-800"
/>
</view>
<!-- Notification -->
<view
class="relative flex h-10 w-10 items-center justify-center rounded-full bg-gray-50 text-gray-600 active:scale-95 transition-transform border border-gray-100"
>
<view class="i-mdi-bell-outline text-xl"></view>
<view
class="absolute top-2.5 right-2.5 h-2 w-2 rounded-full bg-red-500 border border-white"
></view>
</view>
</view>
</view>
</view>
<scroll-view
scroll-y
class="flex-1 overflow-hidden"
:show-scrollbar="false"
>
<view class="p-4 space-y-5 pb-24">
<!-- Hero Card -->
<view
class="relative overflow-hidden rounded-3xl bg-gradient-to-r from-indigo-500 to-purple-600 p-6 text-white shadow-xl shadow-indigo-200"
>
<view
class="absolute -right-10 -top-10 h-32 w-32 rounded-full bg-white/20 blur-2xl"
></view>
<view
class="absolute -bottom-10 -left-10 h-32 w-32 rounded-full bg-white/20 blur-2xl"
></view>
<view class="relative z-10">
<view class="mb-4 flex items-center justify-between">
<view
class="rounded-lg bg-white/20 px-3 py-1 text-xs font-medium backdrop-blur-sm"
>系统运行正常</view
>
<view class="text-xs opacity-80">刚刚更新</view>
</view>
<view class="flex items-end justify-between">
<view>
<view class="text-3xl font-bold">85%</view>
<view class="text-sm opacity-90">工作效率</view>
</view>
<view
class="h-10 w-24 overflow-hidden rounded-lg bg-white/10 p-1"
>
<view class="h-full w-full flex items-end justify-between px-1">
<view class="w-1 bg-white/50 h-2 rounded-full"></view>
<view class="w-1 bg-white/70 h-4 rounded-full"></view>
<view class="w-1 bg-white/40 h-3 rounded-full"></view>
<view class="w-1 bg-white/90 h-6 rounded-full"></view>
<view class="w-1 bg-white/60 h-4 rounded-full"></view>
</view>
</view>
</view>
</view>
</view>
<!-- Stats Grid -->
<view class="grid grid-cols-2 gap-4">
<view
class="rounded-2xl bg-white p-4 shadow-sm border border-gray-100 flex items-center justify-between"
>
<view>
<view class="text-xs text-gray-500 mb-1">新通知</view>
<view class="text-2xl font-bold text-gray-800">12</view>
</view>
<view
class="flex h-12 w-12 items-center justify-center rounded-full bg-blue-50 text-blue-500"
>
<view class="i-mdi-bell text-xl"></view>
</view>
</view>
<view
class="rounded-2xl bg-white p-4 shadow-sm border border-gray-100 flex items-center justify-between"
>
<view>
<view class="text-xs text-gray-500 mb-1">待办任务</view>
<view class="text-2xl font-bold text-gray-800">5</view>
</view>
<view
class="flex h-12 w-12 items-center justify-center rounded-full bg-purple-50 text-purple-500"
>
<view class="i-mdi-calendar text-xl"></view>
</view>
</view>
</view>
<!-- Quick Functions -->
<view>
<view class="mb-4 flex items-center justify-between">
<view class="text-base font-bold text-gray-800">快捷入口</view>
</view>
<view
class="grid grid-cols-4 gap-2 rounded-2xl bg-white p-4 shadow-sm"
>
<view class="flex flex-col items-center space-y-2">
<view
class="flex h-12 w-12 items-center justify-center rounded-2xl bg-orange-50 text-orange-500"
>
<view class="i-mdi-file-document-edit text-xl"></view>
</view>
<view class="text-xs font-medium text-gray-600">申请</view>
</view>
<view class="flex flex-col items-center space-y-2">
<view
class="flex h-12 w-12 items-center justify-center rounded-2xl bg-green-50 text-green-500"
>
<view class="i-mdi-newspaper text-xl"></view>
</view>
<view class="text-xs font-medium text-gray-600">新闻</view>
</view>
<view class="flex flex-col items-center space-y-2">
<view
class="flex h-12 w-12 items-center justify-center rounded-2xl bg-indigo-50 text-indigo-500"
>
<view class="i-mdi-chart-bar text-xl"></view>
</view>
<view class="text-xs font-medium text-gray-600">统计</view>
</view>
<view class="flex flex-col items-center space-y-2">
<view
class="flex h-12 w-12 items-center justify-center rounded-2xl bg-gray-50 text-gray-400"
>
<view class="i-mdi-calendar text-xl"></view>
</view>
<view class="text-xs font-medium text-gray-600">计划</view>
</view>
</view>
</view>
<!-- Recent Apps -->
<view>
<view class="mb-4 flex items-center justify-between">
<view class="text-base font-bold text-gray-800">最近更新</view>
<view class="text-xs text-blue-500">查看全部</view>
</view>
<view class="space-y-3">
<view
class="flex items-center rounded-2xl bg-white p-4 shadow-sm border border-gray-50"
>
<view
class="flex h-12 w-12 items-center justify-center rounded-xl bg-blue-50 text-blue-500"
>
<view class="i-mdi-clipboard-text-outline text-xl"></view>
</view>
<view class="ml-4 flex-1">
<view class="font-bold text-gray-800">请假申请</view>
<view class="text-xs text-gray-500">等待经理审批</view>
</view>
<view
class="rounded-full bg-yellow-50 px-3 py-1 text-xs font-medium text-yellow-600"
>待处理</view
>
</view>
<view
class="flex items-center rounded-2xl bg-white p-4 shadow-sm border border-gray-50"
>
<view
class="flex h-12 w-12 items-center justify-center rounded-xl bg-green-50 text-green-500"
>
<view class="i-mdi-newspaper text-xl"></view>
</view>
<view class="ml-4 flex-1">
<view class="font-bold text-gray-800">周报</view>
<view class="text-xs text-gray-500">提交成功</view>
</view>
<view
class="rounded-full bg-green-50 px-3 py-1 text-xs font-medium text-green-600"
>已完成</view
>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
const globalConfig = getApp().globalData.config;
</script>
+224
View File
@@ -0,0 +1,224 @@
<template>
<view
class="flex h-full flex-col items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100 px-6 pb-20 overflow-hidden"
>
<!-- Logo Section -->
<view class="mb-6 flex flex-col items-center">
<view
class="mb-4 flex size-16 items-center justify-center rounded-2xl bg-white shadow-lg"
>
<image
class="size-10"
:src="globalConfig.appInfo.logo"
mode="widthFix"
/>
</view>
<text class="text-xl font-bold tracking-wide text-gray-800"
>RuoYi-FastAPI移动端登录</text
>
</view>
<!-- Form Section -->
<view class="w-full rounded-3xl bg-white/80 p-6 shadow-xl backdrop-blur-md">
<!-- Username -->
<view class="group relative mb-5">
<view
class="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 transition-colors group-focus-within:text-blue-500"
>
<view class="i-mdi-account text-xl"></view>
</view>
<input
v-model="loginForm.username"
class="h-12 w-full rounded-xl bg-gray-50 pl-12 pr-4 text-sm text-gray-700 outline-none transition-all focus:bg-white focus:ring-2 focus:ring-blue-400"
type="text"
placeholder="请输入账号"
maxlength="30"
/>
</view>
<!-- Password -->
<view class="group relative mb-5">
<view
class="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 transition-colors group-focus-within:text-blue-500"
>
<view class="i-mdi-lock text-xl"></view>
</view>
<input
v-model="loginForm.password"
type="password"
class="h-12 w-full rounded-xl bg-gray-50 pl-12 pr-4 text-sm text-gray-700 outline-none transition-all focus:bg-white focus:ring-2 focus:ring-blue-400"
placeholder="请输入密码"
maxlength="20"
/>
</view>
<!-- Captcha -->
<view
class="mb-8 flex items-center justify-between"
v-if="captchaEnabled"
>
<view class="group relative mr-3 flex-1">
<view
class="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 transition-colors group-focus-within:text-blue-500"
>
<view class="i-mdi-security text-xl"></view>
</view>
<input
v-model="loginForm.code"
type="number"
class="h-12 w-full rounded-xl bg-gray-50 pl-12 pr-4 text-sm text-gray-700 outline-none transition-all focus:bg-white focus:ring-2 focus:ring-blue-400"
placeholder="验证码"
maxlength="4"
/>
</view>
<view
class="h-12 w-28 overflow-hidden rounded-xl bg-gray-100 shadow-sm transition-opacity active:opacity-80"
@click="getCode"
>
<image :src="codeUrl" class="size-full object-cover"></image>
</view>
</view>
<!-- Login Button -->
<button
@click="handleLogin"
class="flex h-12 w-full items-center justify-center rounded-xl bg-gradient-to-r from-blue-500 to-indigo-600 text-base font-semibold text-white shadow-lg shadow-blue-500/30 transition-transform active:scale-95"
>
</button>
<!-- Footer Links -->
<view class="mt-6 flex flex-col items-center space-y-3">
<view class="flex items-center text-sm text-gray-500" v-if="register">
<text>没有账号</text>
<text
@click="handleUserRegister"
class="ml-1 font-medium text-blue-600 active:opacity-70"
>立即注册</text
>
</view>
<view
class="flex flex-wrap items-center justify-center text-xs text-gray-400"
>
<text>登录即代表同意</text>
<text
@click="handleUserAgrement"
class="mx-1 text-blue-500 active:opacity-70"
>用户协议</text
>
<text></text>
<text
@click="handlePrivacy"
class="mx-1 text-blue-500 active:opacity-70"
>隐私协议</text
>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, getCurrentInstance } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import { getToken } from "@/utils/auth";
import { getCodeImg } from "@/api/login";
import { useConfigStore, useUserStore } from "@/store";
const { proxy } = getCurrentInstance();
const globalConfig = useConfigStore().config;
const codeUrl = ref("");
// 验证码开关
const captchaEnabled = ref(true);
// 用户注册开关
const register = ref(false);
const loginForm = ref({
username: "admin",
password: "admin123",
code: "",
uuid: "",
});
// 用户注册
function handleUserRegister() {
proxy.$tab.redirectTo(`/pages/register`);
}
// 隐私协议
function handlePrivacy() {
proxy.$tab.navigateTo(`/pages/common/privacy/index`);
}
// 用户协议
function handleUserAgrement() {
proxy.$tab.navigateTo(`/pages/common/agreement/index`);
}
// 获取图形验证码
function getCode() {
getCodeImg().then((res) => {
captchaEnabled.value =
res.captchaEnabled === undefined ? true : res.captchaEnabled;
if (captchaEnabled.value) {
codeUrl.value = "data:image/gif;base64," + res.img;
loginForm.value.uuid = res.uuid;
}
});
}
// 登录方法
async function handleLogin() {
if (loginForm.value.username === "") {
proxy.$modal.msgError("请输入账号");
} else if (loginForm.value.password === "") {
proxy.$modal.msgError("请输入密码");
} else if (loginForm.value.code === "" && captchaEnabled.value) {
proxy.$modal.msgError("请输入验证码");
} else {
proxy.$modal.loading("登录中,请耐心等待...");
pwdLogin();
}
}
// 密码登录
async function pwdLogin() {
useUserStore()
.login(loginForm.value)
.then(() => {
proxy.$modal.closeLoading();
loginSuccess();
})
.catch(() => {
if (captchaEnabled.value) {
getCode();
}
});
}
// 登录成功后,处理函数
function loginSuccess(result) {
// 设置用户信息
useUserStore()
.getInfo()
.then((res) => {
proxy.$tab.reLaunch("/pages/index");
});
}
onLoad(() => {
//#ifdef H5
if (getToken()) {
proxy.$tab.reLaunch("/pages/index");
}
//#endif
});
getCode();
</script>
<style lang="scss" scoped>
page {
background-color: #ffffff;
}
</style>
@@ -0,0 +1,68 @@
<template>
<view class="flex h-full flex-col bg-gray-50 pb-10">
<!-- Header -->
<view class="flex flex-col items-center py-10">
<image
class="size-20 rounded-xl shadow-md"
src="/static/logo200.png"
mode="widthFix"
/>
<text class="mt-4 text-xl font-bold text-gray-800"
>RuoYi-FastAPI移动端</text
>
</view>
<!-- Info List -->
<view class="bg-white">
<!-- Version -->
<view
class="flex items-center justify-between border-b border-gray-100 px-5 py-4"
>
<text class="text-base text-gray-800">版本信息</text>
<text class="text-sm text-gray-500">v{{ version }}</text>
</view>
<!-- Email -->
<view
class="flex items-center justify-between border-b border-gray-100 px-5 py-4"
>
<text class="text-base text-gray-800">官方邮箱</text>
<text class="text-sm text-gray-500">xxx@xxx.com</text>
</view>
<!-- Hotline -->
<view
class="flex items-center justify-between border-b border-gray-100 px-5 py-4"
>
<text class="text-base text-gray-800">服务热线</text>
<text class="text-sm text-gray-500">400-999-9999</text>
</view>
<!-- Website -->
<view class="flex items-center justify-between px-5 py-4">
<text class="text-base text-gray-800">公司网站</text>
<text class="text-sm text-blue-500 underline active:opacity-70">{{
url
}}</text>
</view>
</view>
<!-- Copyright -->
<view class="mt-10 text-center text-xs text-gray-400">
Copyright &copy; 2026 insistence.tech All Rights Reserved.
</view>
</view>
</template>
<script setup>
import { useConfigStore } from "@/store";
const url = useConfigStore().config.appInfo.site_url;
const version = useConfigStore().config.appInfo.version;
</script>
<style>
page {
background-color: #f9fafb;
}
</style>
@@ -0,0 +1,678 @@
<template>
<view class="h-full bg-black">
<view class="relative h-full w-full">
<view class="min-h-[750rpx] w-full">
<view
v-if="isShowImg"
class="relative box-border overflow-hidden"
:style="
'width:' +
cropperInitW +
'px;height:' +
cropperInitH +
'px;background:#000'
"
>
<view
class="relative"
:style="
'width:' +
cropperW +
'px;height:' +
cropperH +
'px;left:' +
cropperL +
'px;top:' +
cropperT +
'px'
"
>
<image
:src="imageSrc"
:style="
'width:' +
cropperW +
'px;height:' +
cropperH +
'px;display:block;margin:0 auto;'
"
></image>
<view
class="absolute z-10 bg-white/30"
@touchstart.stop="contentStartMove"
@touchmove.stop="contentMoveing"
@touchend.stop="contentTouchEnd"
:style="
'left:' +
cutL +
'px;top:' +
cutT +
'px;right:' +
cutR +
'px;bottom:' +
cutB +
'px'
"
>
<view
class="relative block h-full w-full overflow-visible outline outline-1 outline-[rgba(102,153,255,0.75)]"
>
<view class="uni-cropper-dashed-h"></view>
<view class="uni-cropper-dashed-v"></view>
<view
class="uni-cropper-line-t"
data-drag="top"
@touchstart.stop="dragStart"
@touchmove.stop="dragMove"
></view>
<view
class="uni-cropper-line-r"
data-drag="right"
@touchstart.stop="dragStart"
@touchmove.stop="dragMove"
></view>
<view
class="uni-cropper-line-b"
data-drag="bottom"
@touchstart.stop="dragStart"
@touchmove.stop="dragMove"
></view>
<view
class="uni-cropper-line-l"
data-drag="left"
@touchstart.stop="dragStart"
@touchmove.stop="dragMove"
></view>
<view
class="uni-cropper-point point-t"
data-drag="top"
@touchstart.stop="dragStart"
@touchmove.stop="dragMove"
></view>
<view
class="uni-cropper-point point-tr"
data-drag="topTight"
></view>
<view
class="uni-cropper-point point-r"
data-drag="right"
@touchstart.stop="dragStart"
@touchmove.stop="dragMove"
></view>
<view
class="uni-cropper-point point-rb"
data-drag="rightBottom"
@touchstart.stop="dragStart"
@touchmove.stop="dragMove"
></view>
<view
class="uni-cropper-point point-b"
data-drag="bottom"
@touchstart.stop="dragStart"
@touchmove.stop="dragMove"
></view>
<view
class="uni-cropper-point point-bl"
data-drag="bottomLeft"
></view>
<view
class="uni-cropper-point point-l"
data-drag="left"
@touchstart.stop="dragStart"
@touchmove.stop="dragMove"
></view>
<view
class="uni-cropper-point point-lt"
data-drag="leftTop"
></view>
</view>
</view>
</view>
</view>
</view>
<view class="fixed bottom-8 left-0 w-full px-10">
<button
class="mt-4 w-full rounded-full bg-gradient-to-r from-blue-500 to-blue-600 text-white shadow-lg transition-all active:scale-95"
@click="getImage"
>
选择头像
</button>
<button
class="mt-4 w-full rounded-full bg-gradient-to-r from-red-500 to-red-600 text-white shadow-lg transition-all active:scale-95"
@click="getImageInfo"
>
提交
</button>
</view>
<canvas
canvas-id="myCanvas"
:style="
'position:absolute;border: 1px solid red; width:' +
imageW +
'px;height:' +
imageH +
'px;top:-9999px;left:-9999px;'
"
></canvas>
</view>
</view>
</template>
<script>
import config from "@/config";
import { useUserStore } from "@/store";
import { uploadAvatar } from "@/api/system/user";
const baseUrl = config.baseUrl;
let sysInfo = uni.getSystemInfoSync();
let SCREEN_WIDTH = sysInfo.screenWidth;
let PAGE_X, // 手按下的x位置
PAGE_Y, // 手按下y的位置
PR = sysInfo.pixelRatio, // dpi
T_PAGE_X, // 手移动的时候x的位置
T_PAGE_Y, // 手移动的时候Y的位置
CUT_L, // 初始化拖拽元素的left值
CUT_T, // 初始化拖拽元素的top值
CUT_R, // 初始化拖拽元素的
CUT_B, // 初始化拖拽元素的
CUT_W, // 初始化拖拽元素的宽度
CUT_H, // 初始化拖拽元素的高度
IMG_RATIO, // 图片比例
IMG_REAL_W, // 图片实际的宽度
IMG_REAL_H, // 图片实际的高度
DRAFG_MOVE_RATIO = 1, //移动时候的比例,
INIT_DRAG_POSITION = 100, // 初始化屏幕宽度和裁剪区域的宽度之差,用于设置初始化裁剪的宽度
DRAW_IMAGE_W = sysInfo.screenWidth; // 设置生成的图片宽度
export default {
/**
* 页面的初始数据
*/
data() {
return {
imageSrc: useUserStore().avatar,
isShowImg: false,
// 初始化的宽高
cropperInitW: SCREEN_WIDTH,
cropperInitH: SCREEN_WIDTH,
// 动态的宽高
cropperW: SCREEN_WIDTH,
cropperH: SCREEN_WIDTH,
// 动态的left top值
cropperL: 0,
cropperT: 0,
transL: 0,
transT: 0,
// 图片缩放值
scaleP: 0,
imageW: 0,
imageH: 0,
// 裁剪框 宽高
cutL: 0,
cutT: 0,
cutB: SCREEN_WIDTH,
cutR: "100%",
qualityWidth: DRAW_IMAGE_W,
innerAspectRadio: DRAFG_MOVE_RATIO,
};
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady: function () {
this.loadImage();
},
methods: {
setData: function (obj) {
let that = this;
Object.keys(obj).forEach(function (key) {
that.$set(that.$data, key, obj[key]);
});
},
getImage: function () {
var _this = this;
uni.chooseImage({
success: function (res) {
_this.setData({
imageSrc: res.tempFilePaths[0],
});
_this.loadImage();
},
});
},
loadImage: function () {
var _this = this;
uni.getImageInfo({
src: _this.imageSrc,
success: function success(res) {
IMG_RATIO = 1 / 1;
if (IMG_RATIO >= 1) {
IMG_REAL_W = SCREEN_WIDTH;
IMG_REAL_H = SCREEN_WIDTH / IMG_RATIO;
} else {
IMG_REAL_W = SCREEN_WIDTH * IMG_RATIO;
IMG_REAL_H = SCREEN_WIDTH;
}
let minRange = IMG_REAL_W > IMG_REAL_H ? IMG_REAL_W : IMG_REAL_H;
INIT_DRAG_POSITION =
minRange > INIT_DRAG_POSITION ? INIT_DRAG_POSITION : minRange;
// 根据图片的宽高显示不同的效果 保证图片可以正常显示
if (IMG_RATIO >= 1) {
let cutT = Math.ceil(
(SCREEN_WIDTH / IMG_RATIO -
(SCREEN_WIDTH / IMG_RATIO - INIT_DRAG_POSITION)) /
2,
);
let cutB = cutT;
let cutL = Math.ceil(
(SCREEN_WIDTH - SCREEN_WIDTH + INIT_DRAG_POSITION) / 2,
);
let cutR = cutL;
_this.setData({
cropperW: SCREEN_WIDTH,
cropperH: SCREEN_WIDTH / IMG_RATIO,
// 初始化left right
cropperL: Math.ceil((SCREEN_WIDTH - SCREEN_WIDTH) / 2),
cropperT: Math.ceil(
(SCREEN_WIDTH - SCREEN_WIDTH / IMG_RATIO) / 2,
),
cutL: cutL,
cutT: cutT,
cutR: cutR,
cutB: cutB,
// 图片缩放值
imageW: IMG_REAL_W,
imageH: IMG_REAL_H,
scaleP: IMG_REAL_W / SCREEN_WIDTH,
qualityWidth: DRAW_IMAGE_W,
innerAspectRadio: IMG_RATIO,
});
} else {
let cutL = Math.ceil(
(SCREEN_WIDTH * IMG_RATIO - SCREEN_WIDTH * IMG_RATIO) / 2,
);
let cutR = cutL;
let cutT = Math.ceil((SCREEN_WIDTH - INIT_DRAG_POSITION) / 2);
let cutB = cutT;
_this.setData({
cropperW: SCREEN_WIDTH * IMG_RATIO,
cropperH: SCREEN_WIDTH,
// 初始化left right
cropperL: Math.ceil(
(SCREEN_WIDTH - SCREEN_WIDTH * IMG_RATIO) / 2,
),
cropperT: Math.ceil((SCREEN_WIDTH - SCREEN_WIDTH) / 2),
cutL: cutL,
cutT: cutT,
cutR: cutR,
cutB: cutB,
// 图片缩放值
imageW: IMG_REAL_W,
imageH: IMG_REAL_H,
scaleP: IMG_REAL_W / SCREEN_WIDTH,
qualityWidth: DRAW_IMAGE_W,
innerAspectRadio: IMG_RATIO,
});
}
_this.setData({
isShowImg: true,
});
uni.hideLoading();
},
});
},
// 拖动时候触发的touchStart事件
contentStartMove(e) {
PAGE_X = e.touches[0].pageX;
PAGE_Y = e.touches[0].pageY;
},
// 拖动时候触发的touchMove事件
contentMoveing(e) {
var _this = this;
var dragLengthX = (PAGE_X - e.touches[0].pageX) * DRAFG_MOVE_RATIO;
var dragLengthY = (PAGE_Y - e.touches[0].pageY) * DRAFG_MOVE_RATIO;
// 左移
if (dragLengthX > 0) {
if (this.cutL - dragLengthX < 0) dragLengthX = this.cutL;
} else {
if (this.cutR + dragLengthX < 0) dragLengthX = -this.cutR;
}
if (dragLengthY > 0) {
if (this.cutT - dragLengthY < 0) dragLengthY = this.cutT;
} else {
if (this.cutB + dragLengthY < 0) dragLengthY = -this.cutB;
}
this.setData({
cutL: this.cutL - dragLengthX,
cutT: this.cutT - dragLengthY,
cutR: this.cutR + dragLengthX,
cutB: this.cutB + dragLengthY,
});
PAGE_X = e.touches[0].pageX;
PAGE_Y = e.touches[0].pageY;
},
contentTouchEnd() {},
// 获取图片
getImageInfo() {
var _this = this;
uni.showLoading({
title: "图片生成中...",
});
// 将图片写入画布
const ctx = uni.createCanvasContext("myCanvas");
ctx.drawImage(_this.imageSrc, 0, 0, IMG_REAL_W, IMG_REAL_H);
ctx.draw(true, () => {
// 获取画布要裁剪的位置和宽度 均为百分比 * 画布中图片的宽度 保证了在微信小程序中裁剪的图片模糊 位置不对的问题 canvasT = (_this.cutT / _this.cropperH) * (_this.imageH / pixelRatio)
var canvasW =
((_this.cropperW - _this.cutL - _this.cutR) / _this.cropperW) *
IMG_REAL_W;
var canvasH =
((_this.cropperH - _this.cutT - _this.cutB) / _this.cropperH) *
IMG_REAL_H;
var canvasL = (_this.cutL / _this.cropperW) * IMG_REAL_W;
var canvasT = (_this.cutT / _this.cropperH) * IMG_REAL_H;
uni.canvasToTempFilePath({
x: canvasL,
y: canvasT,
width: canvasW,
height: canvasH,
destWidth: canvasW,
destHeight: canvasH,
quality: 0.5,
canvasId: "myCanvas",
success: function (res) {
uni.hideLoading();
let data = { name: "avatarfile", filePath: res.tempFilePath };
uploadAvatar(data).then((response) => {
useUserStore().SET_AVATAR(baseUrl + response.imgUrl);
uni.showToast({ title: "修改成功", icon: "success" });
uni.navigateBack();
});
},
});
});
},
// 设置大小的时候触发的touchStart事件
dragStart(e) {
T_PAGE_X = e.touches[0].pageX;
T_PAGE_Y = e.touches[0].pageY;
CUT_L = this.cutL;
CUT_R = this.cutR;
CUT_B = this.cutB;
CUT_T = this.cutT;
},
// 设置大小的时候触发的touchMove事件
dragMove(e) {
var _this = this;
var dragType = e.target.dataset.drag;
switch (dragType) {
case "right":
var dragLength = (T_PAGE_X - e.touches[0].pageX) * DRAFG_MOVE_RATIO;
if (CUT_R + dragLength < 0) dragLength = -CUT_R;
this.setData({
cutR: CUT_R + dragLength,
});
break;
case "left":
var dragLength = (T_PAGE_X - e.touches[0].pageX) * DRAFG_MOVE_RATIO;
if (CUT_L - dragLength < 0) dragLength = CUT_L;
if (CUT_L - dragLength > this.cropperW - this.cutR)
dragLength = CUT_L - (this.cropperW - this.cutR);
this.setData({
cutL: CUT_L - dragLength,
});
break;
case "top":
var dragLength = (T_PAGE_Y - e.touches[0].pageY) * DRAFG_MOVE_RATIO;
if (CUT_T - dragLength < 0) dragLength = CUT_T;
if (CUT_T - dragLength > this.cropperH - this.cutB)
dragLength = CUT_T - (this.cropperH - this.cutB);
this.setData({
cutT: CUT_T - dragLength,
});
break;
case "bottom":
var dragLength = (T_PAGE_Y - e.touches[0].pageY) * DRAFG_MOVE_RATIO;
if (CUT_B + dragLength < 0) dragLength = -CUT_B;
this.setData({
cutB: CUT_B + dragLength,
});
break;
case "rightBottom":
var dragLengthX = (T_PAGE_X - e.touches[0].pageX) * DRAFG_MOVE_RATIO;
var dragLengthY = (T_PAGE_Y - e.touches[0].pageY) * DRAFG_MOVE_RATIO;
if (CUT_B + dragLengthY < 0) dragLengthY = -CUT_B;
if (CUT_R + dragLengthX < 0) dragLengthX = -CUT_R;
let cutB = CUT_B + dragLengthY;
let cutR = CUT_R + dragLengthX;
this.setData({
cutB: cutB,
cutR: cutR,
});
break;
default:
break;
}
},
},
};
</script>
<style scoped>
/* 横向虚线 */
.uni-cropper-dashed-h {
position: absolute;
top: 33.33333333%;
left: 0;
width: 100%;
height: 33.33333333%;
border-top: 1rpx dashed rgba(255, 255, 255, 0.5);
border-bottom: 1rpx dashed rgba(255, 255, 255, 0.5);
}
/* 纵向虚线 */
.uni-cropper-dashed-v {
position: absolute;
left: 33.33333333%;
top: 0;
width: 33.33333333%;
height: 100%;
border-left: 1rpx dashed rgba(255, 255, 255, 0.5);
border-right: 1rpx dashed rgba(255, 255, 255, 0.5);
}
/* 四个方向的线 为了之后的拖动事件*/
.uni-cropper-line-t {
position: absolute;
display: block;
width: 100%;
background-color: #69f;
top: 0;
left: 0;
height: 1rpx;
opacity: 0.1;
cursor: n-resize;
}
.uni-cropper-line-t::before {
content: "";
position: absolute;
top: 50%;
right: 0rpx;
width: 100%;
-webkit-transform: translate3d(0, -50%, 0);
transform: translate3d(0, -50%, 0);
bottom: 0;
height: 41rpx;
background: transparent;
z-index: 11;
}
.uni-cropper-line-r {
position: absolute;
display: block;
background-color: #69f;
top: 0;
right: 0rpx;
width: 1rpx;
opacity: 0.1;
height: 100%;
cursor: e-resize;
}
.uni-cropper-line-r::before {
content: "";
position: absolute;
top: 0;
left: 50%;
width: 41rpx;
-webkit-transform: translate3d(-50%, 0, 0);
transform: translate3d(-50%, 0, 0);
bottom: 0;
height: 100%;
background: transparent;
z-index: 11;
}
.uni-cropper-line-b {
position: absolute;
display: block;
width: 100%;
background-color: #69f;
bottom: 0;
left: 0;
height: 1rpx;
opacity: 0.1;
cursor: s-resize;
}
.uni-cropper-line-b::before {
content: "";
position: absolute;
top: 50%;
right: 0rpx;
width: 100%;
-webkit-transform: translate3d(0, -50%, 0);
transform: translate3d(0, -50%, 0);
bottom: 0;
height: 41rpx;
background: transparent;
z-index: 11;
}
.uni-cropper-line-l {
position: absolute;
display: block;
background-color: #69f;
top: 0;
left: 0;
width: 1rpx;
opacity: 0.1;
height: 100%;
cursor: w-resize;
}
.uni-cropper-line-l::before {
content: "";
position: absolute;
top: 0;
left: 50%;
width: 41rpx;
-webkit-transform: translate3d(-50%, 0, 0);
transform: translate3d(-50%, 0, 0);
bottom: 0;
height: 100%;
background: transparent;
z-index: 11;
}
.uni-cropper-point {
width: 5rpx;
height: 5rpx;
background-color: #69f;
opacity: 0.75;
position: absolute;
z-index: 3;
}
.point-t {
top: -3rpx;
left: 50%;
margin-left: -3rpx;
cursor: n-resize;
}
.point-tr {
top: -3rpx;
left: 100%;
margin-left: -3rpx;
cursor: n-resize;
}
.point-r {
top: 50%;
left: 100%;
margin-left: -3rpx;
margin-top: -3rpx;
cursor: n-resize;
}
.point-rb {
left: 100%;
top: 100%;
-webkit-transform: translate3d(-50%, -50%, 0);
transform: translate3d(-50%, -50%, 0);
cursor: n-resize;
width: 36rpx;
height: 36rpx;
background-color: #69f;
position: absolute;
z-index: 1112;
opacity: 1;
}
.point-b {
left: 50%;
top: 100%;
margin-left: -3rpx;
margin-top: -3rpx;
cursor: n-resize;
}
.point-bl {
left: 0%;
top: 100%;
margin-left: -3rpx;
margin-top: -3rpx;
cursor: n-resize;
}
.point-l {
left: 0%;
top: 50%;
margin-left: -3rpx;
margin-top: -3rpx;
cursor: n-resize;
}
.point-lt {
left: 0%;
top: 0%;
margin-left: -3rpx;
margin-top: -3rpx;
cursor: n-resize;
}
</style>
@@ -0,0 +1,91 @@
<template>
<view class="flex h-full flex-col overflow-y-auto bg-gray-50 p-4 pb-24">
<view v-for="(item, findex) in list" :key="findex" class="mb-6">
<view
class="mb-3 ml-2 flex items-center text-base font-bold text-gray-800"
>
<view :class="['mr-2 text-lg text-blue-500', item.icon]"></view
>{{ item.title }}
</view>
<view
class="overflow-hidden rounded-xl bg-white shadow-sm ring-1 ring-gray-100"
>
<view
v-for="(child, zindex) in item.childList"
:key="zindex"
class="relative"
hover-class="bg-gray-50"
@click="handleText(child)"
>
<view class="p-4 text-sm text-gray-600">{{ child.title }}</view>
<view
class="h-px w-full bg-gray-100"
v-if="zindex !== item.childList.length - 1"
></view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, getCurrentInstance } from "vue";
const { proxy } = getCurrentInstance();
const list = ref([
{
icon: "i-mdi-github",
title: "RuoYi-FastAPI问题",
childList: [
{
title: "RuoYi-FastAPI开源吗?",
content: "开源",
},
{
title: "RuoYi-FastAPI可以商用吗?",
content: "可以",
},
{
title: "RuoYi-FastAPI官网地址多少?",
content: "https://vfadmin.insistence.tech",
},
{
title: "RuoYi-FastAPI文档地址多少?",
content: "https://vfadmin.insistence.tech",
},
],
},
{
icon: "i-mdi-help-circle-outline",
title: "其他问题",
childList: [
{
title: "如何退出登录?",
content: "请点击[我的] - [应用设置] - [退出登录]即可退出登录",
},
{
title: "如何修改用户头像?",
content: "请点击[我的] - [选择头像] - [点击提交]即可更换用户头像",
},
{
title: "如何修改登录密码?",
content: "请点击[我的] - [应用设置] - [修改密码]即可修改登录密码",
},
],
},
]);
function handleText(item) {
proxy.$tab.navigateTo(
`/pages/common/textview/index?title=${item.title}&content=${item.content}`,
);
}
</script>
<style>
page {
height: 100%;
background-color: #f9fafb;
}
</style>
+209
View File
@@ -0,0 +1,209 @@
<template>
<view class="flex h-full flex-col bg-gray-50 overflow-hidden">
<!-- Header Section -->
<view
class="relative overflow-hidden bg-gradient-to-r from-blue-500 to-indigo-600 pb-20 pt-16 text-white shadow-lg"
>
<view
class="absolute -right-10 -top-10 size-64 rounded-full bg-white/10 blur-3xl"
></view>
<view
class="absolute -bottom-10 -left-10 size-40 rounded-full bg-white/10 blur-2xl"
></view>
<view class="relative z-10 flex items-center justify-between px-6">
<view class="flex items-center space-x-4">
<!-- Avatar -->
<view
class="relative overflow-hidden rounded-full border-4 border-white/30 bg-white/20 shadow-xl transition-transform active:scale-95"
>
<image
v-if="avatar"
@click="handleToAvatar"
:src="avatar"
class="size-20 object-cover"
/>
<view
v-else
class="flex size-20 items-center justify-center bg-white text-gray-400"
>
<view class="i-mdi-account text-5xl"></view>
</view>
</view>
<!-- User Info -->
<view class="flex flex-col">
<template v-if="name">
<view
class="text-xl font-bold tracking-wide"
@click="handleToInfo"
>
{{ name }}
</view>
<view
class="mt-1 flex items-center text-sm text-blue-100"
@click="handleToInfo"
>
<text>查看个人信息</text>
<view class="i-mdi-chevron-right ml-1 text-xs"></view>
</view>
</template>
<view v-else class="text-xl font-bold" @click="handleToLogin">
点击登录
</view>
</view>
</view>
</view>
</view>
<!-- Content Section -->
<view class="relative z-20 -mt-12 flex-1 px-4 overflow-y-auto">
<!-- Quick Actions -->
<view
class="mb-4 flex items-center justify-between rounded-2xl bg-white p-4 shadow-lg shadow-gray-200/50"
>
<view
class="flex flex-1 flex-col items-center justify-center space-y-2 active:opacity-70"
@click="handleJiaoLiuQun"
>
<view
class="flex size-12 items-center justify-center rounded-full bg-pink-50 text-pink-500"
>
<view class="i-mdi-account-group text-2xl"></view>
</view>
<text class="text-xs font-medium text-gray-600">交流群</text>
</view>
<view
class="flex flex-1 flex-col items-center justify-center space-y-2 active:opacity-70"
@click="handleBuilding"
>
<view
class="flex size-12 items-center justify-center rounded-full bg-blue-50 text-blue-500"
>
<view class="i-mdi-face-agent text-2xl"></view>
</view>
<text class="text-xs font-medium text-gray-600">在线客服</text>
</view>
<view
class="flex flex-1 flex-col items-center justify-center space-y-2 active:opacity-70"
@click="handleBuilding"
>
<view
class="flex size-12 items-center justify-center rounded-full bg-purple-50 text-purple-500"
>
<view class="i-mdi-forum text-2xl"></view>
</view>
<text class="text-xs font-medium text-gray-600">反馈社区</text>
</view>
<view
class="flex flex-1 flex-col items-center justify-center space-y-2 active:opacity-70"
@click="handleBuilding"
>
<view
class="flex size-12 items-center justify-center rounded-full bg-green-50 text-green-500"
>
<view class="i-mdi-thumb-up text-2xl"></view>
</view>
<text class="text-xs font-medium text-gray-600">点赞我们</text>
</view>
</view>
<!-- Menu List -->
<view
class="overflow-hidden rounded-2xl bg-white shadow-lg shadow-gray-200/50"
>
<view
class="group flex items-center justify-between border-b border-gray-100 p-4 transition-colors active:bg-gray-50"
@click="handleToEditInfo"
>
<view class="flex items-center space-x-3">
<view class="i-mdi-account-edit text-xl text-blue-500"></view>
<text class="text-base text-gray-700">编辑资料</text>
</view>
<view class="i-mdi-chevron-right text-gray-400"></view>
</view>
<view
class="group flex items-center justify-between border-b border-gray-100 p-4 transition-colors active:bg-gray-50"
@click="handleHelp"
>
<view class="flex items-center space-x-3">
<view class="i-mdi-help-circle text-xl text-orange-500"></view>
<text class="text-base text-gray-700">常见问题</text>
</view>
<view class="i-mdi-chevron-right text-gray-400"></view>
</view>
<view
class="group flex items-center justify-between border-b border-gray-100 p-4 transition-colors active:bg-gray-50"
@click="handleAbout"
>
<view class="flex items-center space-x-3">
<view class="i-mdi-heart-outline text-xl text-red-500"></view>
<text class="text-base text-gray-700">关于我们</text>
</view>
<view class="i-mdi-chevron-right text-gray-400"></view>
</view>
<view
class="group flex items-center justify-between p-4 transition-colors active:bg-gray-50"
@click="handleToSetting"
>
<view class="flex items-center space-x-3">
<view class="i-mdi-cog-outline text-xl text-gray-500"></view>
<text class="text-base text-gray-700">应用设置</text>
</view>
<view class="i-mdi-chevron-right text-gray-400"></view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { useUserStore } from "@/store";
import { computed, getCurrentInstance } from "vue";
const { proxy } = getCurrentInstance();
const userStore = useUserStore();
const name = computed(() => userStore.name);
const avatar = computed(() => userStore.avatar);
const windowHeight = computed(() => uni.getSystemInfoSync().windowHeight - 50);
function handleToInfo() {
proxy.$tab.navigateTo("/pages/mine/info/index");
}
function handleToEditInfo() {
proxy.$tab.navigateTo("/pages/mine/info/edit");
}
function handleToSetting() {
proxy.$tab.navigateTo("/pages/mine/setting/index");
}
function handleToLogin() {
proxy.$tab.reLaunch("/pages/login");
}
function handleToAvatar() {
proxy.$tab.navigateTo("/pages/mine/avatar/index");
}
function handleHelp() {
proxy.$tab.navigateTo("/pages/mine/help/index");
}
function handleAbout() {
proxy.$tab.navigateTo("/pages/mine/about/index");
}
function handleJiaoLiuQun() {
proxy.$modal.showToast("模块建设中~");
}
function handleBuilding() {
proxy.$modal.showToast("模块建设中~");
}
</script>
@@ -0,0 +1,145 @@
<template>
<view class="flex h-full flex-col overflow-y-auto bg-white p-4">
<view class="space-y-5">
<!-- Nickname -->
<view class="group relative">
<text class="mb-2 block text-sm font-medium text-gray-700"
>用户昵称</text
>
<input
v-model="user.nickName"
class="h-11 w-full rounded-xl bg-gray-50 px-4 text-sm text-gray-800 outline-none ring-1 ring-gray-200 transition-all focus:bg-white focus:ring-2 focus:ring-blue-500"
type="text"
placeholder="请输入昵称"
/>
</view>
<!-- Phone -->
<view class="group relative">
<text class="mb-2 block text-sm font-medium text-gray-700"
>手机号码</text
>
<input
v-model="user.phonenumber"
class="h-11 w-full rounded-xl bg-gray-50 px-4 text-sm text-gray-800 outline-none ring-1 ring-gray-200 transition-all focus:bg-white focus:ring-2 focus:ring-blue-500"
type="number"
placeholder="请输入手机号码"
maxlength="11"
/>
</view>
<!-- Email -->
<view class="group relative">
<text class="mb-2 block text-sm font-medium text-gray-700">邮箱</text>
<input
v-model="user.email"
class="h-11 w-full rounded-xl bg-gray-50 px-4 text-sm text-gray-800 outline-none ring-1 ring-gray-200 transition-all focus:bg-white focus:ring-2 focus:ring-blue-500"
type="text"
placeholder="请输入邮箱"
/>
</view>
<!-- Sex -->
<view class="group relative">
<text class="mb-2 block text-sm font-medium text-gray-700">性别</text>
<view class="flex space-x-4">
<view
v-for="item in sexs"
:key="item.value"
@click="user.sex = item.value"
class="flex flex-1 items-center justify-center rounded-xl border py-2.5 transition-all active:scale-95"
:class="
user.sex === item.value
? 'bg-blue-50 border-blue-500 text-blue-600'
: 'bg-gray-50 border-transparent text-gray-500'
"
>
<text class="text-sm font-medium">{{ item.text }}</text>
</view>
</view>
</view>
<!-- Submit Button -->
<view class="pt-6">
<button
@click="submit"
class="flex h-12 w-full items-center justify-center rounded-xl bg-blue-500 text-base font-semibold text-white shadow-lg shadow-blue-500/30 transition-all active:scale-95 active:bg-blue-600"
>
</button>
</view>
</view>
</view>
</template>
<script setup>
import { getUserProfile, updateUserProfile } from "@/api/system/user";
import { ref, getCurrentInstance } from "vue";
import { onLoad } from "@dcloudio/uni-app";
const { proxy } = getCurrentInstance();
const user = ref({
nickName: "",
phonenumber: "",
email: "",
sex: "",
});
const sexs = [
{
text: "男",
value: "0",
},
{
text: "女",
value: "1",
},
];
function getUser() {
getUserProfile().then((response) => {
const { nickName, phonenumber, email, sex } = response.data;
user.value = { nickName, phonenumber, email, sex };
});
}
function submit() {
if (!user.value.nickName) {
proxy.$modal.msgError("用户昵称不能为空");
return;
}
if (!user.value.phonenumber) {
proxy.$modal.msgError("手机号码不能为空");
return;
}
if (!/^1[3|4|5|6|7|8|9][0-9]\d{8}$/.test(user.value.phonenumber)) {
proxy.$modal.msgError("请输入正确的手机号码");
return;
}
if (!user.value.email) {
proxy.$modal.msgError("邮箱地址不能为空");
return;
}
if (!/^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/.test(user.value.email)) {
proxy.$modal.msgError("请输入正确的邮箱地址");
return;
}
updateUserProfile(user.value).then((response) => {
proxy.$modal.msgSuccess("修改成功");
setTimeout(() => {
proxy.$tab.navigateBack();
}, 1500);
});
}
onLoad(() => {
getUser();
});
</script>
<style>
page {
height: 100%;
background-color: #ffffff;
}
</style>
@@ -0,0 +1,121 @@
<template>
<view class="flex h-full flex-col overflow-y-auto bg-gray-50 pt-4 pb-10">
<view class="overflow-hidden bg-white shadow-sm">
<!-- Nickname -->
<view
class="flex items-center justify-between border-b border-gray-100 px-5 py-4 active:bg-gray-50"
>
<view class="flex items-center">
<view
class="flex size-9 items-center justify-center rounded-full bg-blue-50 text-blue-500"
>
<view class="i-mdi-account text-xl"></view>
</view>
<text class="ml-3 text-base font-medium text-gray-800">昵称</text>
</view>
<text class="text-sm text-gray-500">{{ user.nickName }}</text>
</view>
<!-- Phone -->
<view
class="flex items-center justify-between border-b border-gray-100 px-5 py-4 active:bg-gray-50"
>
<view class="flex items-center">
<view
class="flex size-9 items-center justify-center rounded-full bg-green-50 text-green-500"
>
<view class="i-mdi-phone text-xl"></view>
</view>
<text class="ml-3 text-base font-medium text-gray-800">手机号码</text>
</view>
<text class="text-sm text-gray-500">{{ user.phonenumber }}</text>
</view>
<!-- Email -->
<view
class="flex items-center justify-between border-b border-gray-100 px-5 py-4 active:bg-gray-50"
>
<view class="flex items-center">
<view
class="flex size-9 items-center justify-center rounded-full bg-indigo-50 text-indigo-500"
>
<view class="i-mdi-email text-xl"></view>
</view>
<text class="ml-3 text-base font-medium text-gray-800">邮箱</text>
</view>
<text class="text-sm text-gray-500">{{ user.email }}</text>
</view>
<!-- Post -->
<view
class="flex items-center justify-between border-b border-gray-100 px-5 py-4 active:bg-gray-50"
>
<view class="flex items-center">
<view
class="flex size-9 items-center justify-center rounded-full bg-orange-50 text-orange-500"
>
<view class="i-mdi-shield-account text-xl"></view>
</view>
<text class="ml-3 text-base font-medium text-gray-800">岗位</text>
</view>
<text class="text-sm text-gray-500">{{ postGroup }}</text>
</view>
<!-- Role -->
<view
class="flex items-center justify-between border-b border-gray-100 px-5 py-4 active:bg-gray-50"
>
<view class="flex items-center">
<view
class="flex size-9 items-center justify-center rounded-full bg-purple-50 text-purple-500"
>
<view class="i-mdi-badge-account text-xl"></view>
</view>
<text class="ml-3 text-base font-medium text-gray-800">角色</text>
</view>
<text class="text-sm text-gray-500">{{ roleGroup }}</text>
</view>
<!-- Create Time -->
<view
class="flex items-center justify-between px-5 py-4 active:bg-gray-50"
>
<view class="flex items-center">
<view
class="flex size-9 items-center justify-center rounded-full bg-pink-50 text-pink-500"
>
<view class="i-mdi-calendar text-xl"></view>
</view>
<text class="ml-3 text-base font-medium text-gray-800">创建日期</text>
</view>
<text class="text-sm text-gray-500">{{ user.createTime }}</text>
</view>
</view>
</view>
</template>
<script setup>
import { getUserProfile } from "@/api/system/user";
import { ref } from "vue";
const user = ref({});
const roleGroup = ref("");
const postGroup = ref("");
function getUser() {
getUserProfile().then((response) => {
user.value = response.data;
roleGroup.value = response.roleGroup;
postGroup.value = response.postGroup;
});
}
getUser();
</script>
<style>
page {
height: 100%;
background-color: #f9fafb;
}
</style>
@@ -0,0 +1,90 @@
<template>
<view class="min-h-screen bg-white p-4">
<view class="space-y-5">
<!-- Old Password -->
<view class="group relative">
<text class="mb-2 block text-sm font-medium text-gray-700">旧密码</text>
<input
v-model="user.oldPassword"
class="h-11 w-full rounded-xl bg-gray-50 px-4 text-sm text-gray-800 outline-none ring-1 ring-gray-200 transition-all focus:bg-white focus:ring-2 focus:ring-blue-500"
type="password"
placeholder="请输入旧密码"
/>
</view>
<!-- New Password -->
<view class="group relative">
<text class="mb-2 block text-sm font-medium text-gray-700">新密码</text>
<input
v-model="user.newPassword"
class="h-11 w-full rounded-xl bg-gray-50 px-4 text-sm text-gray-800 outline-none ring-1 ring-gray-200 transition-all focus:bg-white focus:ring-2 focus:ring-blue-500"
type="password"
placeholder="请输入新密码"
/>
</view>
<!-- Confirm Password -->
<view class="group relative">
<text class="mb-2 block text-sm font-medium text-gray-700"
>确认密码</text
>
<input
v-model="user.confirmPassword"
class="h-11 w-full rounded-xl bg-gray-50 px-4 text-sm text-gray-800 outline-none ring-1 ring-gray-200 transition-all focus:bg-white focus:ring-2 focus:ring-blue-500"
type="password"
placeholder="请确认新密码"
/>
</view>
<!-- Submit Button -->
<view class="pt-6">
<button
@click="submit"
class="flex h-12 w-full items-center justify-center rounded-xl bg-blue-500 text-base font-semibold text-white shadow-lg shadow-blue-500/30 transition-all active:scale-95 active:bg-blue-600"
>
</button>
</view>
</view>
</view>
</template>
<script setup>
import { updateUserPwd } from "@/api/system/user";
import { ref, reactive, getCurrentInstance } from "vue";
const { proxy } = getCurrentInstance();
const user = reactive({
oldPassword: "",
newPassword: "",
confirmPassword: "",
});
function submit() {
if (!user.oldPassword) {
proxy.$modal.msgError("旧密码不能为空");
return;
}
if (!user.newPassword) {
proxy.$modal.msgError("新密码不能为空");
return;
}
if (user.newPassword.length < 6 || user.newPassword.length > 20) {
proxy.$modal.msgError("长度在 6 到 20 个字符");
return;
}
if (!user.confirmPassword) {
proxy.$modal.msgError("确认密码不能为空");
return;
}
if (user.newPassword !== user.confirmPassword) {
proxy.$modal.msgError("两次输入的密码不一致");
return;
}
updateUserPwd(user.oldPassword, user.newPassword).then((response) => {
proxy.$modal.msgSuccess("修改成功");
setTimeout(() => {
proxy.$tab.navigateBack();
}, 1500);
});
}
</script>
@@ -0,0 +1,87 @@
<template>
<view class="flex h-full flex-col overflow-y-auto bg-gray-50 pt-4 pb-10">
<view class="bg-white">
<!-- Change Password -->
<view
class="flex items-center justify-between border-b border-gray-100 px-5 py-4 active:bg-gray-50"
@click="handleToPwd"
>
<view class="flex items-center">
<view class="i-mdi-lock text-xl text-gray-600 mr-3"></view>
<text class="text-base text-gray-800">修改密码</text>
</view>
<view class="i-mdi-chevron-right text-base text-gray-400"></view>
</view>
<!-- Check Update -->
<view
class="flex items-center justify-between border-b border-gray-100 px-5 py-4 active:bg-gray-50"
@click="handleToUpgrade"
>
<view class="flex items-center">
<view class="i-mdi-refresh text-xl text-gray-600 mr-3"></view>
<text class="text-base text-gray-800">检查更新</text>
</view>
<view class="i-mdi-chevron-right text-base text-gray-400"></view>
</view>
<!-- Clean Cache -->
<view
class="flex items-center justify-between px-5 py-4 active:bg-gray-50"
@click="handleCleanTmp"
>
<view class="flex items-center">
<view class="i-mdi-delete text-xl text-gray-600 mr-3"></view>
<text class="text-base text-gray-800">清理缓存</text>
</view>
<view class="i-mdi-chevron-right text-base text-gray-400"></view>
</view>
</view>
<!-- Logout -->
<view class="mt-8 px-4">
<view
class="flex h-12 w-full items-center justify-center rounded-xl bg-red-50 text-base font-semibold text-red-600 transition-colors active:bg-red-100"
@click="handleLogout"
>退出登录</view
>
</view>
</view>
</template>
<script setup>
import { useUserStore } from "@/store";
import { getCurrentInstance } from "vue";
const { proxy } = getCurrentInstance();
function handleToPwd() {
proxy.$tab.navigateTo("/pages/mine/pwd/index");
}
function handleToUpgrade() {
proxy.$modal.showToast("模块建设中~");
}
function handleCleanTmp() {
proxy.$modal.showToast("模块建设中~");
}
function handleLogout() {
proxy.$modal.confirm("确定注销并退出系统吗?").then(() => {
useUserStore()
.logOut()
.then(() => {})
.finally(() => {
proxy.$tab.reLaunch("/pages/index");
});
});
}
</script>
<style>
page {
height: 100%;
background-color: #f9fafb;
}
</style>
+206
View File
@@ -0,0 +1,206 @@
<template>
<view
class="flex h-full flex-col items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100 px-6 pb-20 overflow-hidden"
>
<!-- Logo Section -->
<view class="mb-6 flex flex-col items-center">
<view
class="mb-4 flex size-16 items-center justify-center rounded-2xl bg-white shadow-lg"
>
<image
class="size-10"
:src="globalConfig.appInfo.logo"
mode="widthFix"
/>
</view>
<text class="text-xl font-bold tracking-wide text-gray-800"
>RuoYi-FastAPI移动端注册</text
>
</view>
<!-- Form Section -->
<view class="w-full rounded-3xl bg-white/80 p-6 shadow-xl backdrop-blur-md">
<!-- Username -->
<view class="group relative mb-5">
<view
class="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 transition-colors group-focus-within:text-blue-500"
>
<view class="i-mdi-account text-xl"></view>
</view>
<input
v-model="registerForm.username"
class="h-12 w-full rounded-xl bg-gray-50 pl-12 pr-4 text-sm text-gray-700 outline-none transition-all focus:bg-white focus:ring-2 focus:ring-blue-400"
type="text"
placeholder="请输入账号"
maxlength="30"
/>
</view>
<!-- Password -->
<view class="group relative mb-5">
<view
class="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 transition-colors group-focus-within:text-blue-500"
>
<view class="i-mdi-lock text-xl"></view>
</view>
<input
v-model="registerForm.password"
type="password"
class="h-12 w-full rounded-xl bg-gray-50 pl-12 pr-4 text-sm text-gray-700 outline-none transition-all focus:bg-white focus:ring-2 focus:ring-blue-400"
placeholder="请输入密码"
maxlength="20"
/>
</view>
<!-- Confirm Password -->
<view class="group relative mb-5">
<view
class="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 transition-colors group-focus-within:text-blue-500"
>
<view class="i-mdi-lock text-xl"></view>
</view>
<input
v-model="registerForm.confirmPassword"
type="password"
class="h-12 w-full rounded-xl bg-gray-50 pl-12 pr-4 text-sm text-gray-700 outline-none transition-all focus:bg-white focus:ring-2 focus:ring-blue-400"
placeholder="请输入重复密码"
maxlength="20"
/>
</view>
<!-- Captcha -->
<view
class="mb-8 flex items-center justify-between"
v-if="captchaEnabled"
>
<view class="group relative mr-3 flex-1">
<view
class="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 transition-colors group-focus-within:text-blue-500"
>
<view class="i-mdi-security text-xl"></view>
</view>
<input
v-model="registerForm.code"
type="number"
class="h-12 w-full rounded-xl bg-gray-50 pl-12 pr-4 text-sm text-gray-700 outline-none transition-all focus:bg-white focus:ring-2 focus:ring-blue-400"
placeholder="验证码"
maxlength="4"
/>
</view>
<view
class="h-12 w-28 overflow-hidden rounded-xl bg-gray-100 shadow-sm transition-opacity active:opacity-80"
@click="getCode"
>
<image :src="codeUrl" class="size-full object-cover"></image>
</view>
</view>
<!-- Register Button -->
<button
@click="handleRegister"
class="flex h-12 w-full items-center justify-center rounded-xl bg-gradient-to-r from-blue-500 to-indigo-600 text-base font-semibold text-white shadow-lg shadow-blue-500/30 transition-transform active:scale-95"
>
</button>
<!-- Footer Links -->
<view class="mt-6 flex flex-col items-center space-y-3">
<view class="flex items-center text-sm text-gray-500">
<text
@click="handleUserLogin"
class="ml-1 font-medium text-blue-600 active:opacity-70"
>使用已有账号登录</text
>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { getCodeImg, register } from "@/api/login";
import { ref, getCurrentInstance } from "vue";
import { useConfigStore } from "@/store";
const { proxy } = getCurrentInstance();
const globalConfig = useConfigStore().config;
const codeUrl = ref("");
// 验证码开关
const captchaEnabled = ref(true);
const registerForm = ref({
username: "",
password: "",
confirmPassword: "",
code: "",
uuid: "",
});
// 用户登录
function handleUserLogin() {
proxy.$tab.navigateTo(`/pages/login`);
}
// 获取图形验证码
function getCode() {
getCodeImg().then((res) => {
captchaEnabled.value =
res.captchaEnabled === undefined ? true : res.captchaEnabled;
if (captchaEnabled.value) {
codeUrl.value = "data:image/gif;base64," + res.img;
registerForm.value.uuid = res.uuid;
}
});
}
// 注册方法
async function handleRegister() {
if (registerForm.value.username === "") {
proxy.$modal.msgError("请输入您的账号");
} else if (registerForm.value.password === "") {
proxy.$modal.msgError("请输入您的密码");
} else if (registerForm.value.confirmPassword === "") {
proxy.$modal.msgError("请再次输入您的密码");
} else if (
registerForm.value.password !== registerForm.value.confirmPassword
) {
proxy.$modal.msgError("两次输入的密码不一致");
} else if (registerForm.value.code === "" && captchaEnabled.value) {
proxy.$modal.msgError("请输入验证码");
} else {
proxy.$modal.loading("注册中,请耐心等待...");
userRegister();
}
}
// 用户注册
async function userRegister() {
register(registerForm.value)
.then((res) => {
proxy.$modal.closeLoading();
uni.showModal({
title: "系统提示",
content:
"恭喜你,您的账号 " + registerForm.value.username + " 注册成功!",
success: function (res) {
if (res.confirm) {
uni.redirectTo({ url: `/pages/login` });
}
},
});
})
.catch(() => {
if (captchaEnabled.value) {
getCode();
}
});
}
getCode();
</script>
<style lang="scss" scoped>
page {
background-color: #ffffff;
height: 100%;
}
</style>
+235
View File
@@ -0,0 +1,235 @@
<template>
<view class="flex h-full flex-col bg-gray-50 overflow-hidden">
<scroll-view scroll-y class="flex-1" :show-scrollbar="false">
<view class="p-4 pb-24 space-y-6">
<!-- Banner -->
<view
class="relative h-40 w-full overflow-hidden rounded-2xl shadow-lg shadow-indigo-100"
>
<swiper
class="h-40 w-full"
:current="swiperDotIndex"
@change="changeSwiper"
autoplay
interval="3000"
circular
>
<swiper-item v-for="(item, index) in data" :key="index">
<view class="h-full w-full" @click="clickBannerItem(item)">
<image
:src="item.image"
mode="scaleToFill"
class="block h-full w-full"
/>
</view>
</swiper-item>
</swiper>
<!-- Custom Dots -->
<view
class="absolute bottom-3 right-0 left-0 flex justify-center space-x-2 pointer-events-none"
>
<view
v-for="(item, index) in data"
:key="index"
class="h-1.5 rounded-full transition-all duration-300"
:class="current === index ? 'bg-white w-4' : 'bg-white/50 w-1.5'"
>
</view>
</view>
</view>
<!-- AI Tools Section -->
<view>
<view class="mb-4 flex items-center space-x-2">
<view class="h-4 w-1 rounded-full bg-indigo-500"></view>
<text class="text-base font-bold text-gray-800">AI 生产力</text>
</view>
<view class="grid grid-cols-2 gap-4">
<view
class="group relative overflow-hidden rounded-2xl bg-white p-5 shadow-sm transition-all active:scale-95"
@click="handleToAiChat"
>
<view
class="absolute -right-4 -top-4 size-20 rounded-full bg-indigo-50 opacity-50"
></view>
<view class="relative z-10 flex flex-col">
<view
class="mb-3 flex size-10 items-center justify-center rounded-xl bg-indigo-500 text-white shadow-md shadow-indigo-200"
>
<view class="i-mdi-chat text-xl"></view>
</view>
<text class="font-bold text-gray-800">智能对话</text>
<text class="mt-1 text-xs text-gray-500">智能助手</text>
</view>
</view>
<view
class="group relative overflow-hidden rounded-2xl bg-white p-5 shadow-sm transition-all active:scale-95"
@click="handleBuilding"
>
<view
class="absolute -right-4 -top-4 size-20 rounded-full bg-purple-50 opacity-50"
></view>
<view class="relative z-10 flex flex-col">
<view
class="mb-3 flex size-10 items-center justify-center rounded-xl bg-purple-500 text-white shadow-md shadow-purple-200"
>
<view class="i-mdi-image-multiple text-xl"></view>
</view>
<text class="font-bold text-gray-800">图像生成</text>
<text class="mt-1 text-xs text-gray-500">创意工坊</text>
</view>
</view>
</view>
</view>
<!-- System Management Section -->
<view>
<view class="mb-4 flex items-center space-x-2">
<view class="h-4 w-1 rounded-full bg-blue-500"></view>
<text class="text-base font-bold text-gray-800">系统管理</text>
</view>
<view class="rounded-2xl bg-white p-5 shadow-sm">
<view class="flex flex-wrap justify-between gap-y-4">
<view
class="w-[22%] flex flex-col items-center space-y-1.5 active:opacity-60"
@click="handleToUserList"
>
<view
class="flex size-10 items-center justify-center rounded-xl bg-blue-50 text-blue-500 transition-colors group-active:bg-blue-100"
>
<view class="i-mdi-account text-xl"></view>
</view>
<text class="text-[11px] font-medium text-gray-600"
>用户管理</text
>
</view>
<view
class="w-[22%] flex flex-col items-center space-y-1.5 active:opacity-60"
@click="handleBuilding"
>
<view
class="flex size-10 items-center justify-center rounded-xl bg-orange-50 text-orange-500 transition-colors group-active:bg-orange-100"
>
<view class="i-mdi-badge-account text-xl"></view>
</view>
<text class="text-[11px] font-medium text-gray-600"
>角色管理</text
>
</view>
<view
class="w-[22%] flex flex-col items-center space-y-1.5 active:opacity-60"
@click="handleBuilding"
>
<view
class="flex size-10 items-center justify-center rounded-xl bg-green-50 text-green-500 transition-colors group-active:bg-green-100"
>
<view class="i-mdi-office-building text-xl"></view>
</view>
<text class="text-[11px] font-medium text-gray-600"
>部门管理</text
>
</view>
<view
class="w-[22%] flex flex-col items-center space-y-1.5 active:opacity-60"
@click="handleBuilding"
>
<view
class="flex size-10 items-center justify-center rounded-xl bg-red-50 text-red-500 transition-colors group-active:bg-red-100"
>
<view class="i-mdi-cog text-xl"></view>
</view>
<text class="text-[11px] font-medium text-gray-600"
>配置管理</text
>
</view>
<view
class="w-[22%] flex flex-col items-center space-y-1.5 active:opacity-60"
@click="handleBuilding"
>
<view
class="flex size-10 items-center justify-center rounded-xl bg-cyan-50 text-cyan-500 transition-colors group-active:bg-cyan-100"
>
<view class="i-mdi-book-open-page-variant text-xl"></view>
</view>
<text class="text-[11px] font-medium text-gray-600"
>字典管理</text
>
</view>
<view
class="w-[22%] flex flex-col items-center space-y-1.5 active:opacity-60"
@click="handleBuilding"
>
<view
class="flex size-10 items-center justify-center rounded-xl bg-yellow-50 text-yellow-500 transition-colors group-active:bg-yellow-100"
>
<view class="i-mdi-bullhorn text-xl"></view>
</view>
<text class="text-[11px] font-medium text-gray-600"
>通知公告</text
>
</view>
<view
class="w-[22%] flex flex-col items-center space-y-1.5 active:opacity-60"
@click="handleBuilding"
>
<view
class="flex size-10 items-center justify-center rounded-xl bg-pink-50 text-pink-500 transition-colors group-active:bg-pink-100"
>
<view class="i-mdi-file-document-outline text-xl"></view>
</view>
<text class="text-[11px] font-medium text-gray-600"
>日志管理</text
>
</view>
<view
class="w-[22%] flex flex-col items-center space-y-1.5 active:opacity-60"
@click="handleBuilding"
>
<view
class="flex size-10 items-center justify-center rounded-xl bg-gray-50 text-gray-500 transition-colors group-active:bg-gray-100"
>
<view class="i-mdi-dots-horizontal text-xl"></view>
</view>
<text class="text-[11px] font-medium text-gray-600">更多</text>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { ref, getCurrentInstance } from "vue";
const { proxy } = getCurrentInstance();
const current = ref(0);
const swiperDotIndex = ref(0);
const data = ref([
{ image: "/static/images/banner/banner01.jpg" },
{ image: "/static/images/banner/banner02.jpg" },
{ image: "/static/images/banner/banner03.jpg" },
]);
function clickBannerItem(item) {
console.info(item);
}
function changeSwiper(e) {
current.value = e.detail.current;
}
function handleToAiChat() {
handleBuilding();
}
function handleToUserList() {
handleBuilding();
}
function handleBuilding() {
proxy.$modal.msg("模块建设中~");
}
</script>
+43
View File
@@ -0,0 +1,43 @@
import { getToken } from "@/utils/auth";
// 登录页面
const loginPage = "/pages/login";
// 页面白名单
const whiteList = [
"/pages/login",
"/pages/register",
"/pages/common/webview/index",
"/pages/common/agreement/index",
"/pages/common/privacy/index",
];
// 检查地址白名单
function checkWhite(url) {
const path = url.split("?")[0];
return whiteList.indexOf(path) !== -1;
}
// 页面跳转验证拦截器
let list = ["navigateTo", "redirectTo", "reLaunch", "switchTab"];
list.forEach((item) => {
uni.addInterceptor(item, {
invoke(to) {
if (getToken()) {
if (to.url === loginPage) {
uni.reLaunch({ url: "/" });
}
return true;
} else {
if (checkWhite(to.url)) {
return true;
}
uni.reLaunch({ url: loginPage });
return false;
}
},
fail(err) {
console.log(err);
},
});
});
+60
View File
@@ -0,0 +1,60 @@
import { useUserStore } from "@/store";
function authPermission(permission) {
const all_permission = "*:*:*";
const permissions = useUserStore().permissions;
if (permission && permission.length > 0) {
return permissions.some((v) => {
return all_permission === v || v === permission;
});
} else {
return false;
}
}
function authRole(role) {
const super_admin = "admin";
const roles = useUserStore().roles;
if (role && role.length > 0) {
return roles.some((v) => {
return super_admin === v || v === role;
});
} else {
return false;
}
}
export default {
// 验证用户是否具备某权限
hasPermi(permission) {
return authPermission(permission);
},
// 验证用户是否含有指定权限,只需包含其中一个
hasPermiOr(permissions) {
return permissions.some((item) => {
return authPermission(item);
});
},
// 验证用户是否含有指定权限,必须全部拥有
hasPermiAnd(permissions) {
return permissions.every((item) => {
return authPermission(item);
});
},
// 验证用户是否具备某角色
hasRole(role) {
return authRole(role);
},
// 验证用户是否含有指定角色,只需包含其中一个
hasRoleOr(roles) {
return roles.some((item) => {
return authRole(item);
});
},
// 验证用户是否含有指定角色,必须全部拥有
hasRoleAnd(roles) {
return roles.every((item) => {
return authRole(item);
});
},
};
+12
View File
@@ -0,0 +1,12 @@
import tab from "./tab";
import auth from "./auth";
import modal from "./modal";
export function install(app) {
// 页签操作
app.config.globalProperties.$tab = tab;
// 认证对象
app.config.globalProperties.$auth = auth;
// 模态框对象
app.config.globalProperties.$modal = modal;
}
+78
View File
@@ -0,0 +1,78 @@
export default {
// 消息提示
msg(content) {
uni.showToast({
title: content,
icon: "none",
});
},
// 错误消息
msgError(content) {
uni.showToast({
title: content,
icon: "error",
});
},
// 成功消息
msgSuccess(content) {
uni.showToast({
title: content,
icon: "success",
});
},
// 隐藏消息
hideMsg(content) {
uni.hideToast();
},
// 弹出提示
alert(content, title) {
uni.showModal({
title: title || "系统提示",
content: content,
showCancel: false,
});
},
// 确认窗体
confirm(content, title) {
return new Promise((resolve, reject) => {
uni.showModal({
title: title || "系统提示",
content: content,
cancelText: "取消",
confirmText: "确定",
success: function (res) {
if (res.confirm) {
resolve(res.confirm);
}
},
});
});
},
// 提示信息
showToast(option) {
if (typeof option === "object") {
uni.showToast(option);
} else {
uni.showToast({
title: option,
icon: "none",
duration: 2500,
});
}
},
// 打开遮罩层
loading(content) {
uni.showLoading({
title: content,
icon: "none",
});
},
// 关闭遮罩层
closeLoading() {
try {
uni.hideLoading();
} catch (e) {
console.log(e);
}
},
};
+30
View File
@@ -0,0 +1,30 @@
export default {
// 关闭所有页面,打开到应用内的某个页面
reLaunch(url) {
return uni.reLaunch({
url: url,
});
},
// 跳转到tabBar页面,并关闭其他所有非tabBar页面
switchTab(url) {
return uni.switchTab({
url: url,
});
},
// 关闭当前页面,跳转到应用内的某个页面
redirectTo(url) {
return uni.redirectTo({
url: url,
});
},
// 保留当前页面,跳转到应用内的某个页面
navigateTo(url) {
return uni.navigateTo({
url: url,
});
},
// 关闭当前页面,返回上一页面或多级页面
navigateBack() {
return uni.navigateBack();
},
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

+9
View File
@@ -0,0 +1,9 @@
import { createPinia } from "pinia";
import { useUserStore } from "./modules/user";
import { useConfigStore } from "./modules/config";
const pinia = createPinia();
export default pinia;
export { useUserStore, useConfigStore };
@@ -0,0 +1,13 @@
import { defineStore } from "pinia";
import { ref } from "vue";
export const useConfigStore = defineStore("config", () => {
const config = ref();
const setConfig = (val) => {
config.value = val;
};
return {
config,
setConfig,
};
});
@@ -0,0 +1,56 @@
import { defineStore } from "pinia";
const useDictStore = defineStore("dict", {
state: () => ({
dict: new Array(),
}),
actions: {
// 获取字典
getDict(_key) {
if (_key == null && _key == "") {
return null;
}
try {
for (let i = 0; i < this.dict.length; i++) {
if (this.dict[i].key == _key) {
return this.dict[i].value;
}
}
} catch (e) {
return null;
}
},
// 设置字典
setDict(_key, value) {
if (_key !== null && _key !== "") {
this.dict.push({
key: _key,
value: value,
});
}
},
// 删除字典
removeDict(_key) {
var bln = false;
try {
for (let i = 0; i < this.dict.length; i++) {
if (this.dict[i].key == _key) {
this.dict.splice(i, 1);
return true;
}
}
} catch (e) {
bln = false;
}
return bln;
},
// 清空字典
cleanDict() {
this.dict = new Array();
},
// 初始字典
initDict() {},
},
});
export default useDictStore;
+125
View File
@@ -0,0 +1,125 @@
import { defineStore } from "pinia";
import { ref } from "vue";
import config from "@/config";
import storage from "@/utils/storage";
import constant from "@/utils/constant";
import { isHttp, isEmpty } from "@/utils/validate";
import { getInfo, login, logout } from "@/api/login";
import { getToken, removeToken, setToken } from "@/utils/auth";
import defAva from "@/static/images/profile.jpg";
const baseUrl = config.baseUrl;
export const useUserStore = defineStore("user", () => {
const token = ref(getToken());
const id = ref(storage.get(constant.id));
const name = ref(storage.get(constant.name));
const avatar = ref(storage.get(constant.avatar));
const roles = ref(storage.get(constant.roles));
const permissions = ref(storage.get(constant.permissions));
const SET_TOKEN = (val) => {
token.value = val;
};
const SET_ID = (val) => {
id.value = val;
storage.set(constant.id, val);
};
const SET_NAME = (val) => {
name.value = val;
storage.set(constant.name, val);
};
const SET_AVATAR = (val) => {
avatar.value = val;
storage.set(constant.avatar, val);
};
const SET_ROLES = (val) => {
roles.value = val;
storage.set(constant.roles, val);
};
const SET_PERMISSIONS = (val) => {
permissions.value = val;
storage.set(constant.permissions, val);
};
// 登录
const loginAction = (userInfo) => {
const username = userInfo.username.trim();
const password = userInfo.password;
const code = userInfo.code;
const uuid = userInfo.uuid;
return new Promise((resolve, reject) => {
login(username, password, code, uuid)
.then((res) => {
setToken(res.token);
SET_TOKEN(res.token);
resolve();
})
.catch((error) => {
reject(error);
});
});
};
// 获取用户信息
const getInfoAction = () => {
return new Promise((resolve, reject) => {
getInfo()
.then((res) => {
const user = res.user;
let avatar = user.avatar || "";
if (!isHttp(avatar)) {
avatar = isEmpty(avatar) ? defAva : baseUrl + avatar;
}
const userid =
isEmpty(user) || isEmpty(user.userId) ? "" : user.userId;
const username =
isEmpty(user) || isEmpty(user.userName) ? "" : user.userName;
if (res.roles && res.roles.length > 0) {
SET_ROLES(res.roles);
SET_PERMISSIONS(res.permissions);
} else {
SET_ROLES(["ROLE_DEFAULT"]);
}
SET_ID(userid);
SET_NAME(username);
SET_AVATAR(avatar);
resolve(res);
})
.catch((error) => {
reject(error);
});
});
};
// 退出系统
const logOutAction = () => {
return new Promise((resolve, reject) => {
logout(token.value)
.then(() => {
SET_TOKEN("");
SET_ROLES([]);
SET_PERMISSIONS([]);
removeToken();
storage.clean();
resolve();
})
.catch((error) => {
reject(error);
});
});
};
return {
token,
id,
name,
avatar,
roles,
permissions,
SET_AVATAR,
login: loginAction,
getInfo: getInfoAction,
logOut: logOutAction,
};
});
+10
View File
@@ -0,0 +1,10 @@
{
"light": {
"navBgColor": "#f6f6f6",
"navTxtStyle": "black"
},
"dark": {
"navBgColor": "#191919",
"navTxtStyle": "white"
}
}
View File
+13
View File
@@ -0,0 +1,13 @@
const TokenKey = "App-Token";
export function getToken() {
return uni.getStorageSync(TokenKey);
}
export function setToken(token) {
return uni.setStorageSync(TokenKey, token);
}
export function removeToken() {
return uni.removeStorageSync(TokenKey);
}
+58
View File
@@ -0,0 +1,58 @@
/**
* 显示消息提示框
* @param content 提示的标题
*/
export function toast(content) {
uni.showToast({
icon: "none",
title: content,
});
}
/**
* 显示模态弹窗
* @param content 提示的标题
*/
export function showConfirm(content) {
return new Promise((resolve, reject) => {
uni.showModal({
title: "提示",
content: content,
cancelText: "取消",
confirmText: "确定",
success: function (res) {
resolve(res);
},
});
});
}
/**
* 参数处理
* @param params 参数
*/
export function tansParams(params) {
let result = "";
for (const propName of Object.keys(params)) {
const value = params[propName];
var part = encodeURIComponent(propName) + "=";
if (value !== null && value !== "" && typeof value !== "undefined") {
if (typeof value === "object") {
for (const key of Object.keys(value)) {
if (
value[key] !== null &&
value[key] !== "" &&
typeof value[key] !== "undefined"
) {
let params = propName + "[" + key + "]";
var subPart = encodeURIComponent(params) + "=";
result += subPart + encodeURIComponent(value[key]) + "&";
}
}
} else {
result += part + encodeURIComponent(value) + "&";
}
}
}
return result;
}
+9
View File
@@ -0,0 +1,9 @@
const constant = {
avatar: "user_avatar",
id: "user_id",
name: "user_name",
roles: "user_roles",
permissions: "user_permissions",
};
export default constant;
+30
View File
@@ -0,0 +1,30 @@
import useDictStore from "@/store/modules/dict";
import { getDicts } from "@/api/system/dict/data";
import { ref, toRefs } from "vue";
/**
* 获取字典数据
*/
export function useDict(...args) {
const res = ref({});
return (() => {
args.forEach((dictType, index) => {
res.value[dictType] = [];
const dicts = useDictStore().getDict(dictType);
if (dicts) {
res.value[dictType] = dicts;
} else {
getDicts(dictType).then((resp) => {
res.value[dictType] = resp.data.map((p) => ({
label: p.dictLabel,
value: p.dictValue,
elTagType: p.listClass,
elTagClass: p.cssClass,
}));
useDictStore().setDict(dictType, res.value[dictType]);
});
}
});
return toRefs(res.value);
})();
}
+6
View File
@@ -0,0 +1,6 @@
export default {
401: "认证失败,无法访问系统资源",
403: "当前操作没有权限",
404: "访问资源不存在",
default: "系统未知错误,请反馈给管理员",
};
+55
View File
@@ -0,0 +1,55 @@
import store from "@/store";
/**
* 字符权限校验
* @param {Array} value 校验值
* @returns {Boolean}
*/
export function checkPermi(value) {
if (value && value instanceof Array && value.length > 0) {
const permissions = store.getters && store.getters.permissions;
const permissionDatas = value;
const all_permission = "*:*:*";
const hasPermission = permissions.some((permission) => {
return (
all_permission === permission || permissionDatas.includes(permission)
);
});
if (!hasPermission) {
return false;
}
return true;
} else {
console.error(
`need roles! Like checkPermi="['system:user:add','system:user:edit']"`,
);
return false;
}
}
/**
* 角色权限校验
* @param {Array} value 校验值
* @returns {Boolean}
*/
export function checkRole(value) {
if (value && value instanceof Array && value.length > 0) {
const roles = store.getters && store.getters.roles;
const permissionRoles = value;
const super_admin = "admin";
const hasRole = roles.some((role) => {
return super_admin === role || permissionRoles.includes(role);
});
if (!hasRole) {
return false;
}
return true;
} else {
console.error(`need roles! Like checkRole="['admin','editor']"`);
return false;
}
}
+131
View File
@@ -0,0 +1,131 @@
import config from "@/config";
import { getToken } from "@/utils/auth";
import errorCode from "@/utils/errorCode";
import { useUserStore } from "@/store/modules/user";
import { toast, showConfirm, tansParams } from "@/utils/common";
import {
decryptTransportErrorResponse,
decryptTransportResponse,
encryptTransportRequest,
invalidateTransportKeyMeta,
resetTransportRequestConfig,
shouldRetryTransportWithFreshKey,
} from "@/utils/transportCrypto";
let timeout = 10000;
const baseUrl = config.baseUrl;
const request = async (config) => {
// 是否需要设置 token
const isToken = (config.headers || {}).isToken === false;
config.header = config.header || {};
config.headers = config.headers || {};
if (getToken() && !isToken) {
config.header["Authorization"] = "Bearer " + getToken();
}
try {
config = await encryptTransportRequest(config);
// get请求映射params参数
if (config.params) {
let url = config.url + "?" + tansParams(config.params);
url = url.slice(0, -1);
config.url = url;
}
return await new Promise((resolve, reject) => {
uni.request({
method: config.method || "get",
timeout: config.timeout || timeout,
url: config.baseUrl || baseUrl + config.url,
data: config.data,
header: config.header,
dataType: "json",
success: async (response) => {
try {
const res = await decryptTransportResponse(response, config);
if (shouldRetryTransportWithFreshKey(res) && !config.__transportRetried) {
invalidateTransportKeyMeta();
config.__transportRetried = true;
config.headers.repeatSubmit = false;
resetTransportRequestConfig(config);
resolve(await request(config));
return;
}
const code = res.data.code || 200;
const msg = errorCode[code] || res.data.msg || errorCode["default"];
if (code === 401) {
showConfirm(
"登录状态已过期,您可以继续留在该页面,或者重新登录?",
).then((res) => {
if (res.confirm) {
useUserStore()
.logOut()
.then(() => {
uni.reLaunch({ url: "/pages/login" });
});
}
});
const error = new Error("无效的会话,或者会话已过期,请重新登录。");
error.response = res;
reject(error);
} else if (code === 500) {
const error = new Error(msg);
error.response = res;
reject(error);
} else if (code !== 200) {
const error = new Error(msg);
error.response = res;
reject(error);
} else {
resolve(res.data);
}
} catch (error) {
reject(error);
}
},
fail: reject,
});
});
} catch (error) {
error = await decryptTransportErrorResponse(error, config);
if (shouldRetryTransportWithFreshKey(error) && !config.__transportRetried) {
invalidateTransportKeyMeta();
config.__transportRetried = true;
config.headers.repeatSubmit = false;
resetTransportRequestConfig(config);
return request(config);
}
const response = error.response;
const responseStatus = response?.status ?? response?.statusCode;
const responseCode = response?.data?.code;
const responseMsg = response?.data?.msg;
if (responseMsg) {
uni.showToast({
title: responseMsg,
icon: "none",
duration: responseStatus === 429 || responseCode === 429 ? 5000 : 3000,
});
throw error;
}
let { message } = error;
if (message === "Network Error") {
message = "后端接口连接异常";
} else if (message && message.includes("timeout")) {
message = "系统接口请求超时";
} else if (message && message.includes("Request failed with status code")) {
message = "系统接口" + message.substr(message.length - 3) + "异常";
}
if (message) {
toast(message);
}
throw error;
}
};
export default request;
+38
View File
@@ -0,0 +1,38 @@
import constant from "./constant";
// 存储变量名
let storageKey = "storage_data";
// 存储节点变量名
let storageNodeKeys = [
constant.avatar,
constant.id,
constant.name,
constant.roles,
constant.permissions,
];
const storage = {
set: function (key, value) {
if (storageNodeKeys.indexOf(key) != -1) {
let tmp = uni.getStorageSync(storageKey);
tmp = tmp ? tmp : {};
tmp[key] = value;
uni.setStorageSync(storageKey, tmp);
}
},
get: function (key) {
let storageData = uni.getStorageSync(storageKey) || {};
return storageData[key] || "";
},
remove: function (key) {
let storageData = uni.getStorageSync(storageKey) || {};
delete storageData[key];
uni.setStorageSync(storageKey, storageData);
},
clean: function () {
uni.removeStorageSync(storageKey);
},
};
export default storage;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,438 @@
import config from "@/config";
const TRANSPORT_BASE_URL = config.baseUrl;
const EXCLUDED_URL_PATTERNS = [
"/transport/crypto/frontend-config",
"/transport/crypto/public-key",
"/common/download",
"/common/download/resource",
"/common/files/",
"/system/file/download/",
];
const TRANSPORT_FRONTEND_CONFIG_CACHE_KEY = "transportCryptoFrontendConfig";
const TRANSPORT_FRONTEND_CONFIG_URL = "/transport/crypto/frontend-config";
const TRANSPORT_FRONTEND_CONFIG_FALLBACK_TTL_SECONDS = 60;
const DEFAULT_TRANSPORT_ENVELOPE_VERSION = "1";
const DEFAULT_REQUEST_ENVELOPE_ALGORITHM = "RSA_OAEP_AES_256_GCM";
const DEFAULT_RESPONSE_ENVELOPE_ALGORITHM = "AES_256_GCM";
const DEFAULT_TRANSPORT_MAX_GET_URL_LENGTH = 4096;
let cachedTransportPolicy = null;
let inflightTransportPolicyPromise = null;
/**
* 获取当前 Unix 秒级时间戳。
*
* @returns {number} 当前时间戳
*/
function getNowTimestamp() {
return Math.floor(Date.now() / 1000);
}
/**
* 判断请求地址是否命中固定排除名单。
*
* @param {string} url 请求地址
* @returns {boolean} 是否命中排除规则
*/
function matchExcludedUrl(url = "") {
return EXCLUDED_URL_PATTERNS.some((pattern) => url.includes(pattern));
}
/**
* 判断请求路径是否命中路径前缀列表。
*
* @param {string} path 待匹配路径
* @param {string[]} pathPatterns 路径前缀集合
* @returns {boolean} 是否匹配成功
*/
function matchPathPrefix(path = "", pathPatterns = []) {
return pathPatterns.some(
(pattern) => path === pattern || path.startsWith(`${pattern}/`),
);
}
/**
* 从绝对地址中提取 pathname,或直接返回相对地址的路径部分。
*
* @param {string} url 请求地址
* @returns {string} 标准化后的路径
*/
function parseAbsoluteUrlPath(url = "") {
const normalizedUrl = String(url || "");
if (!normalizedUrl) {
return "/";
}
if (
!normalizedUrl.startsWith("http://") &&
!normalizedUrl.startsWith("https://")
) {
return normalizedUrl.split("?")[0] || "/";
}
const pathMatch = normalizedUrl.match(/^https?:\/\/[^/]+(\/[^?#]*)?/i);
return pathMatch?.[1] || "/";
}
/**
* 解析基础 API 地址对应的路径前缀。
*
* @param {string} baseUrl 基础 API 地址
* @returns {string} 基础路径前缀
*/
function getBaseApiPath(baseUrl = TRANSPORT_BASE_URL) {
if (!baseUrl) {
return "";
}
const baseApiPath = parseAbsoluteUrlPath(baseUrl);
return baseApiPath === "/" ? "" : baseApiPath;
}
/**
* 计算后端用于 AAD 与策略匹配的标准请求路径。
*
* @param {string} url 请求地址
* @param {string} baseUrl 基础 API 地址
* @returns {string} 标准化请求路径
*/
function getRequestPath(url = "", baseUrl = TRANSPORT_BASE_URL) {
const baseApiPath = getBaseApiPath(baseUrl);
const pathname = parseAbsoluteUrlPath(url);
if (baseApiPath && pathname.startsWith(baseApiPath)) {
const normalizedPath = pathname.slice(baseApiPath.length);
return normalizedPath || "/";
}
return pathname || "/";
}
/**
* 标准化后端下发的路径列表配置。
*
* @param {Array} paths 原始路径集合
* @returns {string[]} 标准化后的路径数组
*/
function normalizePaths(paths) {
if (!Array.isArray(paths)) {
return [];
}
return paths.map((path) => String(path || "").trim()).filter(Boolean);
}
/**
* 从请求头对象中读取指定字段。
*
* @param {Object} headers 请求头对象
* @param {string} name 请求头名称
* @returns {*} 请求头值
*/
function getHeaderValue(headers, name) {
if (!headers) {
return undefined;
}
return headers[name] ?? headers[name.toLowerCase()] ?? headers[name.toUpperCase()];
}
/**
* 将后端配置响应转换为前端统一使用的策略对象。
*
* @param {Object} payload 后端返回的配置数据
* @returns {Object} 标准化后的传输加密策略
*/
function normalizeTransportPolicy(payload) {
return {
transportCryptoEnabled: Boolean(payload?.transportCryptoEnabled),
transportCryptoMode: String(payload?.transportCryptoMode || "off"),
transportCryptoActive: Boolean(payload?.transportCryptoActive),
envelopeVersion: String(
payload?.envelopeVersion || DEFAULT_TRANSPORT_ENVELOPE_VERSION,
),
publicKeyUrl: String(payload?.publicKeyUrl || "/transport/crypto/public-key"),
requestEnvelopeAlgorithm: String(
payload?.requestEnvelopeAlgorithm || DEFAULT_REQUEST_ENVELOPE_ALGORITHM,
),
responseEnvelopeAlgorithm: String(
payload?.responseEnvelopeAlgorithm || DEFAULT_RESPONSE_ENVELOPE_ALGORITHM,
),
enabledPaths: normalizePaths(payload?.enabledPaths),
requiredPaths: normalizePaths(payload?.requiredPaths),
excludePaths: normalizePaths(payload?.excludePaths),
maxEncryptedGetUrlLength: Number(
payload?.maxEncryptedGetUrlLength || DEFAULT_TRANSPORT_MAX_GET_URL_LENGTH,
),
configExpireAt: Number(payload?.configExpireAt || 0),
retryAt: Number(payload?.retryAt || payload?.configExpireAt || 0),
};
}
/**
* 构建不可用场景下的本地兜底策略。
*
* @returns {Object} 明文回退策略
*/
function buildFallbackTransportPolicy() {
const nowTimestamp = getNowTimestamp();
return {
transportCryptoEnabled: false,
transportCryptoMode: "off",
transportCryptoActive: false,
envelopeVersion: DEFAULT_TRANSPORT_ENVELOPE_VERSION,
publicKeyUrl: "/transport/crypto/public-key",
requestEnvelopeAlgorithm: DEFAULT_REQUEST_ENVELOPE_ALGORITHM,
responseEnvelopeAlgorithm: DEFAULT_RESPONSE_ENVELOPE_ALGORITHM,
enabledPaths: [],
requiredPaths: [],
excludePaths: [...EXCLUDED_URL_PATTERNS],
maxEncryptedGetUrlLength: DEFAULT_TRANSPORT_MAX_GET_URL_LENGTH,
configExpireAt:
nowTimestamp + TRANSPORT_FRONTEND_CONFIG_FALLBACK_TTL_SECONDS,
retryAt: nowTimestamp + TRANSPORT_FRONTEND_CONFIG_FALLBACK_TTL_SECONDS,
};
}
/**
* 基于旧策略生成短期可重试的缓存策略。
*
* @param {Object} policy 旧的策略对象
* @returns {Object} 可重试策略
*/
function buildRetryableTransportPolicy(policy) {
const normalizedPolicy = normalizeTransportPolicy(policy);
return {
...normalizedPolicy,
retryAt: getNowTimestamp() + TRANSPORT_FRONTEND_CONFIG_FALLBACK_TTL_SECONDS,
};
}
/**
* 判断当前策略是否仍在可用期内。
*
* @param {Object} policy 待校验策略
* @returns {boolean} 是否可继续使用
*/
function isUsableTransportPolicy(policy) {
if (!policy || !policy.publicKeyUrl || !policy.retryAt) {
return false;
}
return policy.retryAt > getNowTimestamp();
}
/**
* 从本地缓存加载最近一次持久化的策略。
*
* @returns {Object|null} 缓存策略
*/
function loadPersistedTransportPolicy() {
const persistedTransportPolicy = uni.getStorageSync(
TRANSPORT_FRONTEND_CONFIG_CACHE_KEY,
);
if (!persistedTransportPolicy) {
return null;
}
return normalizeTransportPolicy(persistedTransportPolicy);
}
/**
* 请求后端传输加密前端配置。
* 这里直接使用原始 uni.request,避免策略初始化阶段反向依赖统一 request
* 包装器,导致“是否加密尚未判定时又要先走加密请求”的循环依赖。
*
* @returns {Promise<Object>} Uni 请求响应结果
*/
function requestFrontendConfig() {
return new Promise((resolve, reject) => {
uni.request({
url: `${TRANSPORT_BASE_URL}${TRANSPORT_FRONTEND_CONFIG_URL}`,
method: "GET",
timeout: 10000,
success: resolve,
fail: reject,
});
});
}
/**
* 获取请求加密使用的标准路径。
*
* @param {string} url 请求地址
* @param {string} baseUrl 基础 API 地址
* @returns {string} 标准请求路径
*/
export function getTransportRequestPath(
url = "",
baseUrl = TRANSPORT_BASE_URL,
) {
return getRequestPath(url, baseUrl);
}
/**
* 获取当前生效的传输加密策略。
*
* @returns {Object} 当前策略对象
*/
export function getTransportCryptoPolicy() {
return cachedTransportPolicy || buildFallbackTransportPolicy();
}
/**
* 清空当前策略缓存与持久化数据。
*
* @returns {void}
*/
export function invalidateTransportCryptoPolicy() {
cachedTransportPolicy = null;
inflightTransportPolicyPromise = null;
uni.removeStorageSync(TRANSPORT_FRONTEND_CONFIG_CACHE_KEY);
}
/**
* 确保本地已加载一份可用的传输加密策略。
*
* @param {boolean} forceRefresh 是否强制从后端刷新
* @returns {Promise<Object>} 当前可用策略
*/
export async function ensureTransportCryptoPolicyLoaded(forceRefresh = false) {
if (!forceRefresh && !cachedTransportPolicy) {
const persistedTransportPolicy = loadPersistedTransportPolicy();
if (isUsableTransportPolicy(persistedTransportPolicy)) {
cachedTransportPolicy = persistedTransportPolicy;
}
}
if (!forceRefresh && isUsableTransportPolicy(cachedTransportPolicy)) {
return cachedTransportPolicy;
}
if (inflightTransportPolicyPromise) {
return inflightTransportPolicyPromise;
}
inflightTransportPolicyPromise = requestFrontendConfig()
.then((response) => {
const payload = normalizeTransportPolicy(response?.data?.data || {});
cachedTransportPolicy = payload;
uni.setStorageSync(TRANSPORT_FRONTEND_CONFIG_CACHE_KEY, payload);
inflightTransportPolicyPromise = null;
return cachedTransportPolicy;
})
.catch((error) => {
const staleTransportPolicy =
cachedTransportPolicy || loadPersistedTransportPolicy();
inflightTransportPolicyPromise = null;
cachedTransportPolicy = staleTransportPolicy
? buildRetryableTransportPolicy(staleTransportPolicy)
: buildFallbackTransportPolicy();
uni.setStorageSync(
TRANSPORT_FRONTEND_CONFIG_CACHE_KEY,
cachedTransportPolicy,
);
if (staleTransportPolicy) {
console.warn(
"加载传输加密前端配置失败,当前继续沿用最近一次后端策略",
error,
);
} else {
console.warn("加载传输加密前端配置失败,当前回退为明文请求策略", error);
}
return cachedTransportPolicy;
});
return inflightTransportPolicyPromise;
}
/**
* 判断当前请求是否需要执行请求体加密。
*
* @param {Object} requestConfig 请求配置
* @param {Object} transportPolicy 传输加密策略
* @returns {boolean} 是否启用请求加密
*/
export function shouldEncryptRequest(
requestConfig,
transportPolicy = getTransportCryptoPolicy(),
) {
if (!transportPolicy.transportCryptoActive) {
return false;
}
const requestPath = getRequestPath(
requestConfig.url,
requestConfig.baseUrl || TRANSPORT_BASE_URL,
);
if (matchPathPrefix(requestPath, transportPolicy.excludePaths || [])) {
return false;
}
if (
(transportPolicy.enabledPaths || []).length &&
!matchPathPrefix(requestPath, transportPolicy.enabledPaths || [])
) {
return false;
}
if ((requestConfig.headers || {}).encrypt === false) {
return false;
}
if (matchExcludedUrl(requestConfig.url)) {
return false;
}
const contentType =
getHeaderValue(requestConfig.header, "Content-Type") ||
getHeaderValue(requestConfig.headers, "Content-Type") ||
"";
if (String(contentType).includes("multipart/form-data")) {
return false;
}
return true;
}
/**
* 判断当前响应是否需要执行自动解密。
*
* @param {Object} requestConfig 请求配置
* @param {Object} transportPolicy 传输加密策略
* @returns {boolean} 是否启用响应解密
*/
export function shouldEncryptResponse(
requestConfig,
transportPolicy = getTransportCryptoPolicy(),
) {
const requestPath = getRequestPath(
requestConfig.url,
requestConfig.baseUrl || TRANSPORT_BASE_URL,
);
if (matchPathPrefix(requestPath, transportPolicy.excludePaths || [])) {
return false;
}
if (
(transportPolicy.enabledPaths || []).length &&
!matchPathPrefix(requestPath, transportPolicy.enabledPaths || [])
) {
return false;
}
if ((requestConfig.headers || {}).encryptResponse === false) {
return false;
}
if (matchExcludedUrl(requestConfig.url)) {
return false;
}
if (requestConfig.__transportCryptoEnabledForRequest === true) {
return true;
}
if (requestConfig.__transportCryptoEnabledForRequest === false) {
return false;
}
return transportPolicy.transportCryptoActive;
}
/**
* 判断查询参数是否需要封装为加密信封。
*
* @param {Object} requestConfig 请求配置
* @param {Object} transportPolicy 传输加密策略
* @returns {boolean} 是否启用查询参数加密
*/
export function shouldEncryptQuery(
requestConfig,
transportPolicy = getTransportCryptoPolicy(),
) {
if ((requestConfig.headers || {}).encryptQuery === false) {
return false;
}
return shouldEncryptRequest(requestConfig, transportPolicy);
}
@@ -0,0 +1,214 @@
import forge from "node-forge/lib/forge";
import "node-forge/lib/util";
import "node-forge/lib/asn1";
import "node-forge/lib/oids";
import "node-forge/lib/cipher";
import "node-forge/lib/cipherModes";
import "node-forge/lib/aes";
import "node-forge/lib/jsbn";
import "node-forge/lib/pkcs1";
import "node-forge/lib/prime";
import "node-forge/lib/random";
import "node-forge/lib/md";
import "node-forge/lib/sha256";
import "node-forge/lib/mgf1";
import "node-forge/lib/pem";
import "node-forge/lib/pki";
let randomBytePool = "";
const FORGE_RANDOM_REFILL_BYTES = 4096;
/**
* 将 Uint8Array 转为 Forge 兼容的二进制字符串。
*
* @param {Uint8Array} uint8Array 字节数组
* @returns {string} 二进制字符串
*/
function uint8ArrayToBytes(uint8Array) {
return Array.from(uint8Array, (item) => String.fromCharCode(item)).join("");
}
/**
* 获取当前运行时的全局对象引用。
*
* @returns {Object|undefined} 全局对象
*/
function getRuntimeGlobal() {
if (typeof globalThis !== "undefined") {
return globalThis;
}
if (typeof self !== "undefined") {
return self;
}
if (typeof window !== "undefined") {
return window;
}
if (typeof global !== "undefined") {
return global;
}
return undefined;
}
/**
* 通过 Web Crypto 同步获取随机字节。
*
* @param {number} length 需要的字节长度
* @returns {string|null} 随机字节串,当前运行环境不支持时返回 null
*/
function getWebCryptoRandomBytes(length) {
const runtimeCrypto = getRuntimeGlobal()?.crypto;
if (!runtimeCrypto?.getRandomValues) {
return null;
}
const bytes = new Uint8Array(length);
runtimeCrypto.getRandomValues(bytes);
return uint8ArrayToBytes(bytes);
}
/**
* 在 app-plus iOS 端通过原生 NSUUID 获取随机字节兜底。
*
* @param {number} length 需要的字节长度
* @returns {string|null} 随机字节串,当前运行环境不支持时返回 null
*/
function getAppPlusIosRandomBytes(length) {
// #ifdef APP-PLUS
if (typeof plus === "undefined") {
return null;
}
const osName = String(plus.os?.name || "").toLowerCase();
if (osName !== "ios" || typeof plus.ios?.importClass !== "function") {
return null;
}
try {
const uuidClass = plus.ios.importClass("NSUUID");
let randomBytes = "";
while (randomBytes.length < length) {
const uuidObject = plus.ios.invoke(uuidClass, "UUID");
const uuidText = String(plus.ios.invoke(uuidObject, "UUIDString") || "")
.replace(/-/g, "")
.toLowerCase();
plus.ios.deleteObject(uuidObject);
if (!uuidText) {
return null;
}
for (
let index = 0;
index < uuidText.length && randomBytes.length < length;
index += 2
) {
const byteValue = parseInt(uuidText.slice(index, index + 2), 16);
if (isNaN(byteValue)) {
return null;
}
randomBytes += String.fromCharCode(byteValue);
}
}
return randomBytes;
} catch (error) {
return null;
}
// #endif
return null;
}
/**
* 在 app-plus Android 端通过原生 SecureRandom 获取随机字节兜底。
*
* @param {number} length 需要的字节长度
* @returns {string|null} 随机字节串,当前运行环境不支持时返回 null
*/
function getAppPlusAndroidRandomBytes(length) {
// #ifdef APP-PLUS
if (typeof plus === "undefined") {
return null;
}
const osName = String(plus.os?.name || "").toLowerCase();
if (osName !== "android" || typeof plus.android?.importClass !== "function") {
return null;
}
try {
plus.android.importClass("java.security.SecureRandom");
const secureRandom =
typeof plus.android.newObject === "function"
? plus.android.newObject("java.security.SecureRandom")
: null;
if (!secureRandom) {
return null;
}
plus.android.importClass(secureRandom);
let randomBytes = "";
for (let index = 0; index < length; index += 1) {
randomBytes += String.fromCharCode(secureRandom.nextInt(256));
}
if (typeof plus.android.autoCollection === "function") {
plus.android.autoCollection(secureRandom);
}
return randomBytes;
} catch (error) {
return null;
}
// #endif
return null;
}
/**
* 在随机池不足时尝试同步补充一段新的随机字节。
*
* @param {number} length 当前至少需要的字节长度
* @returns {void}
*/
function refillRandomBytePool(length) {
const refillLength = Math.max(length, FORGE_RANDOM_REFILL_BYTES);
const randomBytes =
getWebCryptoRandomBytes(refillLength) ||
getAppPlusAndroidRandomBytes(refillLength) ||
getAppPlusIosRandomBytes(refillLength);
if (randomBytes) {
randomBytePool += randomBytes;
}
}
/**
* 从随机数池中按需提取字节串。
*
* @param {number} length 需要提取的字节长度
* @returns {string} Forge 使用的二进制字符串
*/
function consumeRandomBytes(length) {
if (randomBytePool.length < length) {
refillRandomBytePool(length);
}
if (randomBytePool.length < length) {
throw new Error("传输加密随机数池不足,且当前运行环境无法同步补充安全随机数");
}
const bytes = randomBytePool.slice(0, length);
randomBytePool = randomBytePool.slice(length);
return bytes;
}
forge.random.getBytesSync = function getBytesSync(length) {
return consumeRandomBytes(length);
};
forge.random.getBytes = function getBytes(length, callback) {
const bytes = consumeRandomBytes(length);
if (typeof callback === "function") {
callback(null, bytes);
}
return bytes;
};
/**
* 向 Forge 随机数池预填充平台侧生成的随机字节。
*
* @param {string} bytes 待注入的二进制字符串
* @returns {void}
*/
export function primeForgeRandomBytes(bytes) {
randomBytePool += String(bytes || "");
}
export default forge;
+72
View File
@@ -0,0 +1,72 @@
import { useUserStore } from "@/store";
import config from "@/config";
import { getToken } from "@/utils/auth";
import errorCode from "@/utils/errorCode";
import { toast, showConfirm, tansParams } from "@/utils/common";
let timeout = 10000;
const baseUrl = config.baseUrl;
export default function upload(config) {
// 是否需要设置 token
const isToken = (config.headers || {}).isToken === false;
config.header = config.header || {};
if (getToken() && !isToken) {
config.header["Authorization"] = "Bearer " + getToken();
}
// get请求映射params参数
if (config.params) {
let url = config.url + "?" + tansParams(config.params);
url = url.slice(0, -1);
config.url = url;
}
return new Promise((resolve, reject) => {
uni.uploadFile({
timeout: config.timeout || timeout,
url: baseUrl + config.url,
filePath: config.filePath,
name: config.name || "file",
header: config.header,
formData: config.formData,
success: (res) => {
let result = JSON.parse(res.data);
const code = result.code || 200;
const msg = errorCode[code] || result.msg || errorCode["default"];
if (code === 200) {
resolve(result);
} else if (code == 401) {
showConfirm(
"登录状态已过期,您可以继续留在该页面,或者重新登录?",
).then((res) => {
if (res.confirm) {
useUserStore()
.logOut()
.then((res) => {
uni.reLaunch({ url: "/pages/login/login" });
});
}
});
reject("无效的会话,或者会话已过期,请重新登录。");
} else if (code === 500) {
toast(msg);
reject("500");
} else if (code !== 200) {
toast(msg);
reject(code);
}
},
fail: (error) => {
let { message } = error;
if (message == "Network Error") {
message = "后端接口连接异常";
} else if (message.includes("timeout")) {
message = "系统接口请求超时";
} else if (message.includes("Request failed with status code")) {
message = "系统接口" + message.substr(message.length - 3) + "异常";
}
toast(message);
reject(error);
},
});
});
}
+124
View File
@@ -0,0 +1,124 @@
/**
* 路径匹配器
* @param {string} pattern
* @param {string} path
* @returns {Boolean}
*/
export function isPathMatch(pattern, path) {
const regexPattern = pattern
.replace(/\//g, "\\/")
.replace(/\*\*/g, ".*")
.replace(/\*/g, "[^\\/]*");
const regex = new RegExp(`^${regexPattern}$`);
return regex.test(path);
}
/**
* 判断value字符串是否为空
* @param {string} value
* @returns {Boolean}
*/
export function isEmpty(value) {
if (
value == null ||
value == "" ||
value == undefined ||
value == "undefined"
) {
return true;
}
return false;
}
/**
* 判断url是否是http或https
* @param {string} url
* @returns {Boolean}
*/
export function isHttp(url) {
return url.indexOf("http://") !== -1 || url.indexOf("https://") !== -1;
}
/**
* 判断path是否为外链
* @param {string} path
* @returns {Boolean}
*/
export function isExternal(path) {
return /^(https?:|mailto:|tel:)/.test(path);
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function validUsername(str) {
const valid_map = ["admin", "editor"];
return valid_map.indexOf(str.trim()) >= 0;
}
/**
* @param {string} url
* @returns {Boolean}
*/
export function validURL(url) {
const reg =
/^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/;
return reg.test(url);
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function validLowerCase(str) {
const reg = /^[a-z]+$/;
return reg.test(str);
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function validUpperCase(str) {
const reg = /^[A-Z]+$/;
return reg.test(str);
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function validAlphabets(str) {
const reg = /^[A-Za-z]+$/;
return reg.test(str);
}
/**
* @param {string} email
* @returns {Boolean}
*/
export function validEmail(email) {
const reg =
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return reg.test(email);
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function isString(str) {
return typeof str === "string" || str instanceof String;
}
/**
* @param {Array} arg
* @returns {Boolean}
*/
export function isArray(arg) {
if (typeof Array.isArray === "undefined") {
return Object.prototype.toString.call(arg) === "[object Array]";
}
return Array.isArray(arg);
}
+3
View File
@@ -0,0 +1,3 @@
import { icebreaker } from "@icebreakers/stylelint-config";
export default icebreaker();
+53
View File
@@ -0,0 +1,53 @@
import type { Config } from "tailwindcss";
import { getIconCollections, iconsPlugin } from "@egoist/tailwindcss-icons";
import cssMacro from "weapp-tailwindcss/css-macro";
import { isMp } from "./platform";
export default <Config>{
content: ["./index.html", "./src/**/*.{html,js,ts,jsx,tsx,vue}"],
theme: {
extend: {
// colors: {
// // 你可以在这里进行颜色的扩展
// primary: {
// 'DEFAULT': 'var(--color-primary, #0089FF)',
// 'light-3': 'var(--color-primary-light-3, rgb(85, 199, 255))',
// 'light-5': 'var(--color-primary-light-5, rgb(130, 217, 255))',
// 'light-7': 'var(--color-primary-light-7, rgb(175, 235, 255))',
// 'light-9': 'var(--color-primary-light-9, rgb(219, 252, 255))',
// 'dark-2': 'var(--color-primary-dark-2, rgb(0, 135, 204))',
// },
// },
},
},
// https://tw.icebreaker.top/docs/quick-start/uni-app-css-macro
plugins: [
cssMacro({
variantsMap: {
wx: "MP-WEIXIN",
"-wx": {
value: "MP-WEIXIN",
negative: true,
},
// 定义多个条件判断
// mv: {
// value: 'H5 || MP-WEIXIN'
// },
// '-mv': {
// value: 'H5 || MP-WEIXIN',
// negative: true
// }
},
}),
iconsPlugin({
// 在这里可以选择你要使用的 icon, 更多详见:
// https://icon-sets.iconify.design/
collections: getIconCollections(["svg-spinners", "mdi"]),
}),
],
corePlugins: {
// 小程序去使用 h5 的 preflight 和响应式 container 没有意义
preflight: !isMp,
container: !isMp,
},
};
+22
View File
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "esnext",
"outDir": "true",
"jsx": "preserve",
"lib": ["esnext", "dom"],
"useDefineForClassFields": true,
"baseUrl": ".",
"module": "esnext",
"moduleResolution": "bundler",
"paths": {
"@/*": ["src/*"]
},
"resolveJsonModule": true,
"types": ["@dcloudio/types"],
"allowJs": true,
"strict": true,
"sourceMap": true,
"esModuleInterop": true
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}
+70
View File
@@ -0,0 +1,70 @@
import uni from "@dcloudio/vite-plugin-uni";
import { defineConfig } from "vite";
import { UnifiedViteWeappTailwindcssPlugin } from "weapp-tailwindcss/vite";
import { WeappTailwindcssDisabled } from "./platform";
import postcssPlugins from "./postcss.config";
function patchNodeForgeMiniProgramRuntime() {
return {
name: "patch-node-forge-mini-program-runtime",
enforce: "pre",
transform(code: string, id: string) {
if (id.includes("node-forge/lib/util.js")) {
return {
code: code.replace(
/return typeof self === 'undefined' \? window : self;/,
"return typeof globalThis !== 'undefined' ? globalThis : (typeof self === 'undefined' ? (typeof window === 'undefined' ? {} : window) : self);",
),
map: null,
};
}
if (id.includes("node-forge/lib/random.js")) {
return {
code: code.replace(
/var _crypto = globalScope\.crypto \|\| globalScope\.msCrypto;/,
"var _crypto = (globalScope && (globalScope.crypto || globalScope.msCrypto)) || null;",
),
map: null,
};
}
return null;
},
};
}
// https://vitejs.dev/config/
export default defineConfig(async () => {
// 新版本的 unplugin-auto-import 改成了只有 esm 格式的产物,而 uni-app 目前必须 cjs 格式
// 所以需要改成动态 import 的写法来进行引入
// 详见 https://github.com/sonofmagic/uni-app-vite-vue3-tailwind-vscode-template/issues/29
const { default: AutoImport } = await import("unplugin-auto-import/vite");
return {
// uvtw 一定要放在 uni 后面
plugins: [
uni(),
patchNodeForgeMiniProgramRuntime(),
UnifiedViteWeappTailwindcssPlugin({
rem2rpx: true,
disabled: WeappTailwindcssDisabled,
}),
AutoImport({
imports: ["vue", "uni-app", "pinia"],
dts: "./src/auto-imports.d.ts",
}),
],
// 内联 postcss 注册 tailwindcss
css: {
postcss: {
plugins: postcssPlugins,
},
// https://vitejs.dev/config/shared-options.html#css-preprocessoroptions
preprocessorOptions: {
scss: {
silenceDeprecations: ["legacy-js-api"],
},
},
},
};
});
+138 -3
View File
@@ -10,16 +10,32 @@ APP_HOST = '0.0.0.0'
# 应用端口 # 应用端口
APP_PORT = 9099 APP_PORT = 9099
# 应用版本 # 应用版本
APP_VERSION= '1.7.0' APP_VERSION= '1.10.0'
# 发布代际标识(生产滚动发布建议注入镜像 digest 或 commit SHA;留空时使用源码指纹)
APP_RELEASE_ID = ''
# 应用是否开启热重载 # 应用是否开启热重载
APP_RELOAD = true APP_RELOAD = true
# 应用工作进程数
APP_WORKERS = 1
# 应用是否开启IP归属区域查询 # 应用是否开启IP归属区域查询
APP_IP_LOCATION_QUERY = true APP_IP_LOCATION_QUERY = true
# 应用是否允许账号同时登录 # 应用是否允许账号同时登录
APP_SAME_TIME_LOGIN = true APP_SAME_TIME_LOGIN = true
# 应用是否为演示模式
APP_DEMO_MODE = false
# 应用是否禁用Swagger文档
APP_DISABLE_SWAGGER = false
# 应用是否禁用ReDoc文档
APP_DISABLE_REDOC = false
# 可信代理IP列表,多个值使用逗号分隔
APP_TRUSTED_PROXY_IPS = '127.0.0.1,::1'
# 可信代理跳数,单层Nginx代理通常为1
APP_TRUSTED_PROXY_HOPS = 1
# 首次启动默认安装并启用的内置插件,多个值使用逗号分隔,留空表示不自动启用
APP_DEFAULT_ENABLED_PLUGINS = 'ai'
# -------- Jwt配置 -------- # -------- Jwt配置 --------
# Jwt秘钥 # Jwt秘钥,留空时自动生成,开发环境下不要留空;也可使用 openssl rand -hex 32 手动生成
JWT_SECRET_KEY = 'b01c66dc2c58dc6a0aabfe2144256be36226de378bf87f72c0c795dda67f4d55' JWT_SECRET_KEY = 'b01c66dc2c58dc6a0aabfe2144256be36226de378bf87f72c0c795dda67f4d55'
# Jwt算法 # Jwt算法
JWT_ALGORITHM = 'HS256' JWT_ALGORITHM = 'HS256'
@@ -63,4 +79,123 @@ REDIS_USERNAME = ''
# Redis密码 # Redis密码
REDIS_PASSWORD = '' REDIS_PASSWORD = ''
# Redis数据库 # Redis数据库
REDIS_DATABASE = 2 REDIS_DATABASE = 2
# -------- 日志配置 --------
# 是否启用日志脱敏
LOG_MASK_ENABLED = false
# 日志脱敏占位符
LOG_MASK_PLACEHOLDER = '******'
# 以下三项为互补关系(非互斥):
# 1) LOG_MASK_FIELDS:命中后执行全量脱敏
# 2) LOG_PARTIAL_MASK_FIELDS:命中后执行部分脱敏
# 3) LOG_CONFIG_SECRET_PATTERNS:按 configKey 关键词决定是否脱敏 configValue
# 全量脱敏字段,多个值使用逗号分隔
LOG_MASK_FIELDS = 'password,old_password,new_password,confirm_password,api_key,token,access_token,refresh_token,authorization,client_secret,secret,secret_key,private_key,private_key_pem,credential,credentials,sms_code,captcha_code,system_prompt'
# 部分脱敏字段,多个值使用逗号分隔
# 当前默认不对IP脱敏;如后期需要开启,可直接追加 ip,ipaddr,oper_ip,login_ip 并重启服务
LOG_PARTIAL_MASK_FIELDS = 'phonenumber,phone,mobile,email'
# 按配置键名识别敏感配置的关键词,多个值使用逗号分隔
LOG_CONFIG_SECRET_PATTERNS = 'password,token,secret,key,private,credential,access,jwt,captcha,sms'
# Redis Stream Key
LOG_STREAM_KEY = 'log:stream'
# Redis Stream 消费组名称
LOG_STREAM_GROUP = 'log_aggregator'
# Redis Stream 消费者名称前缀
LOG_STREAM_CONSUMER_PREFIX = 'worker'
# 每次读取的最大消息数量
LOG_STREAM_BATCH_SIZE = 100
# 阻塞读取等待时间(毫秒)
LOG_STREAM_BLOCK_MS = 2000
# Stream 最大长度(近似裁剪)
LOG_STREAM_MAXLEN = 100000
# Pending 回收最小空闲时间(毫秒)
LOG_STREAM_CLAIM_IDLE_MS = 60000
# Pending 回收检查间隔(毫秒)
LOG_STREAM_CLAIM_INTERVAL_MS = 5000
# 每次回收的最大消息数量
LOG_STREAM_CLAIM_BATCH_SIZE = 100
# 去重 Key 过期时间(秒)
LOG_STREAM_DEDUP_TTL = 3600
# 去重 Key 前缀
LOG_STREAM_DEDUP_PREFIX = 'log:dedup'
# stdout 输出是否为 JSON
LOGURU_JSON = false
# Loguru 最低输出级别
LOGURU_LEVEL = 'INFO'
# 是否输出到 stdout
LOGURU_STDOUT = true
# 是否启用文件日志
LOG_FILE_ENABLED = true
# 文件日志根目录
LOG_FILE_BASE_DIR = 'logs'
# 文件滚动策略
LOGURU_ROTATION = '50MB'
# 文件保留策略
LOGURU_RETENTION = '30 days'
# 文件压缩格式
LOGURU_COMPRESSION = 'zip'
# 实例标识(用于区分实例)
LOG_INSTANCE_ID = 'dev'
# 服务名称(用于统一标识服务)
LOG_SERVICE_NAME = 'ruoyi-fastapi-backend'
# Worker 标识(auto 自动生成)
LOG_WORKER_ID = 'auto'
# -------- 传输层加解密配置 --------
# 是否启用传输层加解密
TRANSPORT_CRYPTO_ENABLED = false
# 传输层加解密模式:off=关闭,optional=明文/密文兼容,required=命中接口强制加密
TRANSPORT_CRYPTO_MODE = 'off'
# 传输层加解密算法标识
TRANSPORT_CRYPTO_ALGORITHM = 'RSA_OAEP_AES_256_GCM'
# 当前启用的密钥版本标识
TRANSPORT_CRYPTO_KID = 'default'
# 传输层RSA位数,需与下方密钥对匹配
TRANSPORT_CRYPTO_RSA_KEY_SIZE = 4096
# 传输层公钥,默认提供一套可用示例值;团队使用时可按需替换
TRANSPORT_CRYPTO_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAwA2ooWpsxLzIVMJp7Wcv\nvR0Bu8paFn8NVPpzz+wGpUlwP5DGK6pBGItsglNMZx56MSYMp3zVyqB95XUVZ6Ub\nQVyHJ6BXXIMs/BpzcHCbyXR/wWG+pKxQ8UEwaHVhK8X21xW1R0kzzwhgMw51unEs\nA81kskVlDdGeNyaOJg5QuofEErCfR1y0e+iqd1PBpIwdEY5L+BzvbVtyhsPz6dBu\n9YbEbKs7JvNle5vjc72ebbMIeGejHFQRYxihuamPCAEylj1qqpHk8U+r+3icxQsj\n8F/TySLuOy37iVWfD+5ikLyou4ZDI3hOYnIYHl194ZM5xVyOGBD/xdqZadXTLqL6\nSrldwZfZDBl3EGgRby8yJqO6SqGgyvyGWXZAaHoPMmF+quu/nUooqnp0fpl+LCqX\nhK67kbYaA9xJeHRaP04cg16imB7sBIXBqzkyZtkhC2BtlL0h5X7dRlAddrQ23z6d\nWdJA3qe3iBSSM8pmedv+Jgfau/PFam2051HqFxvJEh/jnc6rq1aIjL/d4Kk5imIa\nQ6acv+v5N6QVFptlcx6I7j8yZJ7WUHZlB1IxVqfPb69+985eknZyLul60gyu0kPG\nYUecypUu2wiNDBAErZlUxEujMWgJDFYCSonHxtwr88gDInjP0lvwd/OgqjDQ7hSo\nRYxPhTbwo68RJDpImBjn+YsCAwEAAQ==\n-----END PUBLIC KEY-----'
# 传输层私钥,必须与上方公钥成对使用
TRANSPORT_CRYPTO_PRIVATE_KEY = '-----BEGIN RSA PRIVATE KEY-----\n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\n-----END RSA PRIVATE KEY-----'
# 历史密钥对配置,JSON数组格式
TRANSPORT_CRYPTO_LEGACY_KEY_PAIRS = '[]'
# 公钥缓存有效期(秒)
TRANSPORT_CRYPTO_PUBLIC_KEY_TTL_SECONDS = 3600
# 前端传输加密策略缓存有效期(秒)
TRANSPORT_CRYPTO_FRONTEND_CONFIG_TTL_SECONDS = 300
# GET/DELETE 请求参数加密后的最大 URL 长度
TRANSPORT_CRYPTO_MAX_GET_URL_LENGTH = 4096
# 请求时间窗允许偏差(秒)
TRANSPORT_CRYPTO_CLOCK_SKEW_SECONDS = 120
# 防重放随机数有效期(秒)
TRANSPORT_CRYPTO_REPLAY_TTL_SECONDS = 300
# 启用传输层加密的路径列表,多个值使用逗号分隔,留空表示默认全部启用
TRANSPORT_CRYPTO_ENABLED_PATHS = ''
# 强制要求传输层加密的路径列表,多个值使用逗号分隔
TRANSPORT_CRYPTO_REQUIRED_PATHS = ''
# 排除传输层加密的路径列表,多个值使用逗号分隔
TRANSPORT_CRYPTO_EXCLUDE_PATHS = '/openapi.json,/docs,/docs/oauth2-redirect,/redoc,/transport/crypto/frontend-config,/transport/crypto/public-key,/common/download,/common/download/resource,/common/files,/system/file/download'
# -------- 插件依赖安装策略配置 --------
# 策略模式:可填单一模式,或按环境映射。可选 disabled、plan_only、explicit、locked、offline
PLUGIN_DEPENDENCY_POLICY_MODE = 'dev=explicit,test=plan_only,stage=locked,prod=plan_only'
# 生产环境是否允许真实安装插件声明的 Python/npm 依赖
PLUGIN_DEPENDENCY_ALLOW_PROD_INSTALL = false
# 执行真实安装时是否要求显式确认
PLUGIN_DEPENDENCY_REQUIRE_YES = true
# 是否强制依赖命中允许列表;开发环境默认关闭
PLUGIN_DEPENDENCY_REQUIRE_ALLOWLIST = false
# 是否强制提供锁文件;开发环境默认关闭
PLUGIN_DEPENDENCY_REQUIRE_LOCKFILE = false
# 默认锁文件路径,CLI --lockfile 会覆盖该值
PLUGIN_DEPENDENCY_LOCKFILE = ''
# 默认允许列表路径
PLUGIN_DEPENDENCY_ALLOWLIST = ''
# 离线制品根目录
PLUGIN_DEPENDENCY_OFFLINE_DIR = ''
# 受控 PyPI 镜像地址,留空表示不改写 pip 安装参数
PLUGIN_DEPENDENCY_PIP_INDEX_URL = ''
# 受控 npm registry 地址,留空表示不改写 npm 安装参数
PLUGIN_DEPENDENCY_NPM_REGISTRY = ''
# 单条依赖安装命令超时时间(秒)
PLUGIN_DEPENDENCY_INSTALL_TIMEOUT = 600
+201
View File
@@ -0,0 +1,201 @@
# -------- 应用配置 --------
# 应用运行环境
APP_ENV = 'prod'
# 应用名称
APP_NAME = 'RuoYi-FastAPI'
# 应用代理路径
APP_ROOT_PATH = '/docker-api'
# 应用主机
APP_HOST = '0.0.0.0'
# 应用端口
APP_PORT = 9099
# 应用版本
APP_VERSION= '1.10.0'
# 发布代际标识(生产滚动发布建议注入镜像 digest 或 commit SHA;留空时使用源码指纹)
APP_RELEASE_ID = ''
# 应用是否开启热重载
APP_RELOAD = false
# 应用工作进程数
APP_WORKERS = 1
# 应用是否开启IP归属区域查询
APP_IP_LOCATION_QUERY = true
# 应用是否允许账号同时登录
APP_SAME_TIME_LOGIN = true
# 应用是否为演示模式
APP_DEMO_MODE = false
# 应用是否禁用Swagger文档
APP_DISABLE_SWAGGER = true
# 应用是否禁用ReDoc文档
APP_DISABLE_REDOC = true
# 可信代理IP列表,多个值使用逗号分隔
APP_TRUSTED_PROXY_IPS = '127.0.0.1,::1'
# 可信代理跳数,单层Nginx代理通常为1
APP_TRUSTED_PROXY_HOPS = 1
# 首次启动默认安装并启用的内置插件,多个值使用逗号分隔,留空表示不自动启用
APP_DEFAULT_ENABLED_PLUGINS = 'ai'
# -------- Jwt配置 --------
# Jwt秘钥,留空时自动生成;也可使用 openssl rand -hex 32 手动生成
JWT_SECRET_KEY = ''
# Jwt算法
JWT_ALGORITHM = 'HS256'
# 令牌过期时间
JWT_EXPIRE_MINUTES = 1440
# redis中令牌过期时间
JWT_REDIS_EXPIRE_MINUTES = 30
# -------- 数据库配置 --------
# 数据库类型,可选的有'mysql'、'postgresql',默认为'mysql'
DB_TYPE = 'mysql'
# 数据库主机
DB_HOST = 'ruoyi-mysql'
# 数据库端口
DB_PORT = 3306
# 数据库用户名
DB_USERNAME = 'root'
# 数据库密码
DB_PASSWORD = 'root'
# 数据库名称
DB_DATABASE = 'ruoyi-fastapi'
# 是否开启sqlalchemy日志
DB_ECHO = true
# 允许溢出连接池大小的最大连接数
DB_MAX_OVERFLOW = 10
# 连接池大小,0表示连接数无限制
DB_POOL_SIZE = 50
# 连接回收时间(单位:秒)
DB_POOL_RECYCLE = 3600
# 连接池中没有线程可用时,最多等待的时间(单位:秒)
DB_POOL_TIMEOUT = 30
# -------- Redis配置 --------
# Redis主机
REDIS_HOST = 'ruoyi-redis'
# Redis端口
REDIS_PORT = 6379
# Redis用户名
REDIS_USERNAME = ''
# Redis密码
REDIS_PASSWORD = ''
# Redis数据库
REDIS_DATABASE = 2
# -------- 日志配置 --------
# 是否启用日志脱敏
LOG_MASK_ENABLED = true
# 日志脱敏占位符
LOG_MASK_PLACEHOLDER = '******'
# 以下三项为互补关系(非互斥):
# 1) LOG_MASK_FIELDS:命中后执行全量脱敏
# 2) LOG_PARTIAL_MASK_FIELDS:命中后执行部分脱敏
# 3) LOG_CONFIG_SECRET_PATTERNS:按 configKey 关键词决定是否脱敏 configValue
# 全量脱敏字段,多个值使用逗号分隔
LOG_MASK_FIELDS = 'password,old_password,new_password,confirm_password,api_key,token,access_token,refresh_token,authorization,client_secret,secret,secret_key,private_key,private_key_pem,credential,credentials,sms_code,captcha_code,system_prompt'
# 部分脱敏字段,多个值使用逗号分隔
# 当前默认不对IP脱敏;如后期需要开启,可直接追加 ip,ipaddr,oper_ip,login_ip 并重启服务
LOG_PARTIAL_MASK_FIELDS = 'phonenumber,phone,mobile,email'
# 按配置键名识别敏感配置的关键词,多个值使用逗号分隔
LOG_CONFIG_SECRET_PATTERNS = 'password,token,secret,key,private,credential,access,jwt,captcha,sms'
# Redis Stream Key
LOG_STREAM_KEY = 'log:stream'
# Redis Stream 消费组名称
LOG_STREAM_GROUP = 'log_aggregator'
# Redis Stream 消费者名称前缀
LOG_STREAM_CONSUMER_PREFIX = 'worker'
# 每次读取的最大消息数量
LOG_STREAM_BATCH_SIZE = 100
# 阻塞读取等待时间(毫秒)
LOG_STREAM_BLOCK_MS = 2000
# Stream 最大长度(近似裁剪)
LOG_STREAM_MAXLEN = 100000
# Pending 回收最小空闲时间(毫秒)
LOG_STREAM_CLAIM_IDLE_MS = 60000
# Pending 回收检查间隔(毫秒)
LOG_STREAM_CLAIM_INTERVAL_MS = 5000
# 每次回收的最大消息数量
LOG_STREAM_CLAIM_BATCH_SIZE = 100
# 去重 Key 过期时间(秒)
LOG_STREAM_DEDUP_TTL = 3600
# 去重 Key 前缀
LOG_STREAM_DEDUP_PREFIX = 'log:dedup'
# stdout 输出是否为 JSON
LOGURU_JSON = false
# Loguru 最低输出级别
LOGURU_LEVEL = 'INFO'
# 是否输出到 stdout
LOGURU_STDOUT = true
# 是否启用文件日志
LOG_FILE_ENABLED = true
# 文件日志根目录
LOG_FILE_BASE_DIR = 'logs'
# 文件滚动策略
LOGURU_ROTATION = '50MB'
# 文件保留策略
LOGURU_RETENTION = '30 days'
# 文件压缩格式
LOGURU_COMPRESSION = 'zip'
# 实例标识(用于区分实例)
LOG_INSTANCE_ID = 'dockermy'
# 服务名称(用于统一标识服务)
LOG_SERVICE_NAME = 'ruoyi-fastapi-backend'
# Worker 标识(auto 自动生成)
LOG_WORKER_ID = 'auto'
# -------- 传输层加解密配置 --------
# 是否启用传输层加解密
TRANSPORT_CRYPTO_ENABLED = true
# 传输层加解密模式:off=关闭,optional=明文/密文兼容,required=命中接口强制加密
TRANSPORT_CRYPTO_MODE = 'optional'
# 传输层加解密算法标识
TRANSPORT_CRYPTO_ALGORITHM = 'RSA_OAEP_AES_256_GCM'
# 当前启用的密钥版本标识
TRANSPORT_CRYPTO_KID = 'default'
# 传输层RSA位数,需与下方密钥对匹配
TRANSPORT_CRYPTO_RSA_KEY_SIZE = 4096
# 传输层公钥,默认提供一套可用示例值;Docker 部署前请替换为正式密钥
TRANSPORT_CRYPTO_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAwA2ooWpsxLzIVMJp7Wcv\nvR0Bu8paFn8NVPpzz+wGpUlwP5DGK6pBGItsglNMZx56MSYMp3zVyqB95XUVZ6Ub\nQVyHJ6BXXIMs/BpzcHCbyXR/wWG+pKxQ8UEwaHVhK8X21xW1R0kzzwhgMw51unEs\nA81kskVlDdGeNyaOJg5QuofEErCfR1y0e+iqd1PBpIwdEY5L+BzvbVtyhsPz6dBu\n9YbEbKs7JvNle5vjc72ebbMIeGejHFQRYxihuamPCAEylj1qqpHk8U+r+3icxQsj\n8F/TySLuOy37iVWfD+5ikLyou4ZDI3hOYnIYHl194ZM5xVyOGBD/xdqZadXTLqL6\nSrldwZfZDBl3EGgRby8yJqO6SqGgyvyGWXZAaHoPMmF+quu/nUooqnp0fpl+LCqX\nhK67kbYaA9xJeHRaP04cg16imB7sBIXBqzkyZtkhC2BtlL0h5X7dRlAddrQ23z6d\nWdJA3qe3iBSSM8pmedv+Jgfau/PFam2051HqFxvJEh/jnc6rq1aIjL/d4Kk5imIa\nQ6acv+v5N6QVFptlcx6I7j8yZJ7WUHZlB1IxVqfPb69+985eknZyLul60gyu0kPG\nYUecypUu2wiNDBAErZlUxEujMWgJDFYCSonHxtwr88gDInjP0lvwd/OgqjDQ7hSo\nRYxPhTbwo68RJDpImBjn+YsCAwEAAQ==\n-----END PUBLIC KEY-----'
# 传输层私钥,必须与上方公钥成对使用;Docker 部署前请替换为正式密钥
TRANSPORT_CRYPTO_PRIVATE_KEY = '-----BEGIN RSA PRIVATE KEY-----\n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\n-----END RSA PRIVATE KEY-----'
# 历史密钥对配置,JSON数组格式,密钥轮换说明见 docs/transport_crypto_deployment.md
TRANSPORT_CRYPTO_LEGACY_KEY_PAIRS = '[]'
# 公钥缓存有效期(秒)
TRANSPORT_CRYPTO_PUBLIC_KEY_TTL_SECONDS = 3600
# 前端传输加密策略缓存有效期(秒)
TRANSPORT_CRYPTO_FRONTEND_CONFIG_TTL_SECONDS = 300
# GET/DELETE 请求参数加密后的最大 URL 长度
TRANSPORT_CRYPTO_MAX_GET_URL_LENGTH = 4096
# 请求时间窗允许偏差(秒)
TRANSPORT_CRYPTO_CLOCK_SKEW_SECONDS = 120
# 防重放随机数有效期(秒)
TRANSPORT_CRYPTO_REPLAY_TTL_SECONDS = 300
# 启用传输层加密的路径列表,多个值使用逗号分隔,留空表示默认全部启用
TRANSPORT_CRYPTO_ENABLED_PATHS = ''
# 强制要求传输层加密的路径列表,多个值使用逗号分隔
TRANSPORT_CRYPTO_REQUIRED_PATHS = ''
# 排除传输层加密的路径列表,多个值使用逗号分隔
TRANSPORT_CRYPTO_EXCLUDE_PATHS = '/openapi.json,/docs,/docs/oauth2-redirect,/redoc,/transport/crypto/frontend-config,/transport/crypto/public-key,/common/download,/common/download/resource,/common/files,/system/file/download'
# -------- 插件依赖安装策略配置 --------
# 策略模式:可填单一模式,或按环境映射。可选 disabled、plan_only、explicit、locked、offline
PLUGIN_DEPENDENCY_POLICY_MODE = 'dev=explicit,test=plan_only,stage=locked,prod=plan_only'
# 生产环境是否允许真实安装插件声明的 Python/npm 依赖
PLUGIN_DEPENDENCY_ALLOW_PROD_INSTALL = false
# 执行真实安装时是否要求显式确认
PLUGIN_DEPENDENCY_REQUIRE_YES = true
# Docker 生产环境默认要求依赖命中允许列表
PLUGIN_DEPENDENCY_REQUIRE_ALLOWLIST = true
# Docker 生产环境默认要求锁文件
PLUGIN_DEPENDENCY_REQUIRE_LOCKFILE = true
# 默认锁文件路径,CLI --lockfile 会覆盖该值
PLUGIN_DEPENDENCY_LOCKFILE = ''
# 默认允许列表路径
PLUGIN_DEPENDENCY_ALLOWLIST = ''
# 离线制品根目录
PLUGIN_DEPENDENCY_OFFLINE_DIR = ''
# 受控 PyPI 镜像地址,留空表示不改写 pip 安装参数
PLUGIN_DEPENDENCY_PIP_INDEX_URL = ''
# 受控 npm registry 地址,留空表示不改写 npm 安装参数
PLUGIN_DEPENDENCY_NPM_REGISTRY = ''
# 单条依赖安装命令超时时间(秒)
PLUGIN_DEPENDENCY_INSTALL_TIMEOUT = 600
+201
View File
@@ -0,0 +1,201 @@
# -------- 应用配置 --------
# 应用运行环境
APP_ENV = 'prod'
# 应用名称
APP_NAME = 'RuoYi-FastAPI'
# 应用代理路径
APP_ROOT_PATH = '/docker-api'
# 应用主机
APP_HOST = '0.0.0.0'
# 应用端口
APP_PORT = 9099
# 应用版本
APP_VERSION= '1.10.0'
# 发布代际标识(生产滚动发布建议注入镜像 digest 或 commit SHA;留空时使用源码指纹)
APP_RELEASE_ID = ''
# 应用是否开启热重载
APP_RELOAD = false
# 应用工作进程数
APP_WORKERS = 1
# 应用是否开启IP归属区域查询
APP_IP_LOCATION_QUERY = true
# 应用是否为演示模式
APP_DEMO_MODE = false
# 应用是否允许账号同时登录
APP_SAME_TIME_LOGIN = true
# 应用是否禁用Swagger文档
APP_DISABLE_SWAGGER = true
# 应用是否禁用ReDoc文档
APP_DISABLE_REDOC = true
# 可信代理IP列表,多个值使用逗号分隔
APP_TRUSTED_PROXY_IPS = '127.0.0.1,::1'
# 可信代理跳数,单层Nginx代理通常为1
APP_TRUSTED_PROXY_HOPS = 1
# 首次启动默认安装并启用的内置插件,多个值使用逗号分隔,留空表示不自动启用
APP_DEFAULT_ENABLED_PLUGINS = 'ai'
# -------- Jwt配置 --------
# Jwt秘钥,留空时自动生成;也可使用 openssl rand -hex 32 手动生成
JWT_SECRET_KEY = ''
# Jwt算法
JWT_ALGORITHM = 'HS256'
# 令牌过期时间
JWT_EXPIRE_MINUTES = 1440
# redis中令牌过期时间
JWT_REDIS_EXPIRE_MINUTES = 30
# -------- 数据库配置 --------
# 数据库类型,可选的有'mysql'、'postgresql',默认为'mysql'
DB_TYPE = 'postgresql'
# 数据库主机
DB_HOST = 'ruoyi-pg'
# 数据库端口
DB_PORT = 5432
# 数据库用户名
DB_USERNAME = 'postgres'
# 数据库密码
DB_PASSWORD = 'root'
# 数据库名称
DB_DATABASE = 'ruoyi-fastapi'
# 是否开启sqlalchemy日志
DB_ECHO = true
# 允许溢出连接池大小的最大连接数
DB_MAX_OVERFLOW = 10
# 连接池大小,0表示连接数无限制
DB_POOL_SIZE = 50
# 连接回收时间(单位:秒)
DB_POOL_RECYCLE = 3600
# 连接池中没有线程可用时,最多等待的时间(单位:秒)
DB_POOL_TIMEOUT = 30
# -------- Redis配置 --------
# Redis主机
REDIS_HOST = 'ruoyi-redis'
# Redis端口
REDIS_PORT = 6379
# Redis用户名
REDIS_USERNAME = ''
# Redis密码
REDIS_PASSWORD = ''
# Redis数据库
REDIS_DATABASE = 2
# -------- 日志配置 --------
# 是否启用日志脱敏
LOG_MASK_ENABLED = true
# 日志脱敏占位符
LOG_MASK_PLACEHOLDER = '******'
# 以下三项为互补关系(非互斥):
# 1) LOG_MASK_FIELDS:命中后执行全量脱敏
# 2) LOG_PARTIAL_MASK_FIELDS:命中后执行部分脱敏
# 3) LOG_CONFIG_SECRET_PATTERNS:按 configKey 关键词决定是否脱敏 configValue
# 全量脱敏字段,多个值使用逗号分隔
LOG_MASK_FIELDS = 'password,old_password,new_password,confirm_password,api_key,token,access_token,refresh_token,authorization,client_secret,secret,secret_key,private_key,private_key_pem,credential,credentials,sms_code,captcha_code,system_prompt'
# 部分脱敏字段,多个值使用逗号分隔
# 当前默认不对IP脱敏;如后期需要开启,可直接追加 ip,ipaddr,oper_ip,login_ip 并重启服务
LOG_PARTIAL_MASK_FIELDS = 'phonenumber,phone,mobile,email'
# 按配置键名识别敏感配置的关键词,多个值使用逗号分隔
LOG_CONFIG_SECRET_PATTERNS = 'password,token,secret,key,private,credential,access,jwt,captcha,sms'
# Redis Stream Key
LOG_STREAM_KEY = 'log:stream'
# Redis Stream 消费组名称
LOG_STREAM_GROUP = 'log_aggregator'
# Redis Stream 消费者名称前缀
LOG_STREAM_CONSUMER_PREFIX = 'worker'
# 每次读取的最大消息数量
LOG_STREAM_BATCH_SIZE = 100
# 阻塞读取等待时间(毫秒)
LOG_STREAM_BLOCK_MS = 2000
# Stream 最大长度(近似裁剪)
LOG_STREAM_MAXLEN = 100000
# Pending 回收最小空闲时间(毫秒)
LOG_STREAM_CLAIM_IDLE_MS = 60000
# Pending 回收检查间隔(毫秒)
LOG_STREAM_CLAIM_INTERVAL_MS = 5000
# 每次回收的最大消息数量
LOG_STREAM_CLAIM_BATCH_SIZE = 100
# 去重 Key 过期时间(秒)
LOG_STREAM_DEDUP_TTL = 3600
# 去重 Key 前缀
LOG_STREAM_DEDUP_PREFIX = 'log:dedup'
# stdout 输出是否为 JSON
LOGURU_JSON = false
# Loguru 最低输出级别
LOGURU_LEVEL = 'INFO'
# 是否输出到 stdout
LOGURU_STDOUT = true
# 是否启用文件日志
LOG_FILE_ENABLED = true
# 文件日志根目录
LOG_FILE_BASE_DIR = 'logs'
# 文件滚动策略
LOGURU_ROTATION = '50MB'
# 文件保留策略
LOGURU_RETENTION = '30 days'
# 文件压缩格式
LOGURU_COMPRESSION = 'zip'
# 实例标识(用于区分实例)
LOG_INSTANCE_ID = 'dockerpg'
# 服务名称(用于统一标识服务)
LOG_SERVICE_NAME = 'ruoyi-fastapi-backend'
# Worker 标识(auto 自动生成)
LOG_WORKER_ID = 'auto'
# -------- 传输层加解密配置 --------
# 是否启用传输层加解密
TRANSPORT_CRYPTO_ENABLED = true
# 传输层加解密模式:off=关闭,optional=明文/密文兼容,required=命中接口强制加密
TRANSPORT_CRYPTO_MODE = 'optional'
# 传输层加解密算法标识
TRANSPORT_CRYPTO_ALGORITHM = 'RSA_OAEP_AES_256_GCM'
# 当前启用的密钥版本标识
TRANSPORT_CRYPTO_KID = 'default'
# 传输层RSA位数,需与下方密钥对匹配
TRANSPORT_CRYPTO_RSA_KEY_SIZE = 4096
# 传输层公钥,默认提供一套可用示例值;Docker 部署前请替换为正式密钥
TRANSPORT_CRYPTO_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAwA2ooWpsxLzIVMJp7Wcv\nvR0Bu8paFn8NVPpzz+wGpUlwP5DGK6pBGItsglNMZx56MSYMp3zVyqB95XUVZ6Ub\nQVyHJ6BXXIMs/BpzcHCbyXR/wWG+pKxQ8UEwaHVhK8X21xW1R0kzzwhgMw51unEs\nA81kskVlDdGeNyaOJg5QuofEErCfR1y0e+iqd1PBpIwdEY5L+BzvbVtyhsPz6dBu\n9YbEbKs7JvNle5vjc72ebbMIeGejHFQRYxihuamPCAEylj1qqpHk8U+r+3icxQsj\n8F/TySLuOy37iVWfD+5ikLyou4ZDI3hOYnIYHl194ZM5xVyOGBD/xdqZadXTLqL6\nSrldwZfZDBl3EGgRby8yJqO6SqGgyvyGWXZAaHoPMmF+quu/nUooqnp0fpl+LCqX\nhK67kbYaA9xJeHRaP04cg16imB7sBIXBqzkyZtkhC2BtlL0h5X7dRlAddrQ23z6d\nWdJA3qe3iBSSM8pmedv+Jgfau/PFam2051HqFxvJEh/jnc6rq1aIjL/d4Kk5imIa\nQ6acv+v5N6QVFptlcx6I7j8yZJ7WUHZlB1IxVqfPb69+985eknZyLul60gyu0kPG\nYUecypUu2wiNDBAErZlUxEujMWgJDFYCSonHxtwr88gDInjP0lvwd/OgqjDQ7hSo\nRYxPhTbwo68RJDpImBjn+YsCAwEAAQ==\n-----END PUBLIC KEY-----'
# 传输层私钥,必须与上方公钥成对使用;Docker 部署前请替换为正式密钥
TRANSPORT_CRYPTO_PRIVATE_KEY = '-----BEGIN RSA PRIVATE KEY-----\n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\n-----END RSA PRIVATE KEY-----'
# 历史密钥对配置,JSON数组格式,密钥轮换说明见 docs/transport_crypto_deployment.md
TRANSPORT_CRYPTO_LEGACY_KEY_PAIRS = '[]'
# 公钥缓存有效期(秒)
TRANSPORT_CRYPTO_PUBLIC_KEY_TTL_SECONDS = 3600
# 前端传输加密策略缓存有效期(秒)
TRANSPORT_CRYPTO_FRONTEND_CONFIG_TTL_SECONDS = 300
# GET/DELETE 请求参数加密后的最大 URL 长度
TRANSPORT_CRYPTO_MAX_GET_URL_LENGTH = 4096
# 请求时间窗允许偏差(秒)
TRANSPORT_CRYPTO_CLOCK_SKEW_SECONDS = 120
# 防重放随机数有效期(秒)
TRANSPORT_CRYPTO_REPLAY_TTL_SECONDS = 300
# 启用传输层加密的路径列表,多个值使用逗号分隔,留空表示默认全部启用
TRANSPORT_CRYPTO_ENABLED_PATHS = ''
# 强制要求传输层加密的路径列表,多个值使用逗号分隔
TRANSPORT_CRYPTO_REQUIRED_PATHS = ''
# 排除传输层加密的路径列表,多个值使用逗号分隔
TRANSPORT_CRYPTO_EXCLUDE_PATHS = '/openapi.json,/docs,/docs/oauth2-redirect,/redoc,/transport/crypto/frontend-config,/transport/crypto/public-key,/common/download,/common/download/resource,/common/files,/system/file/download'
# -------- 插件依赖安装策略配置 --------
# 策略模式:可填单一模式,或按环境映射。可选 disabled、plan_only、explicit、locked、offline
PLUGIN_DEPENDENCY_POLICY_MODE = 'dev=explicit,test=plan_only,stage=locked,prod=plan_only'
# 生产环境是否允许真实安装插件声明的 Python/npm 依赖
PLUGIN_DEPENDENCY_ALLOW_PROD_INSTALL = false
# 执行真实安装时是否要求显式确认
PLUGIN_DEPENDENCY_REQUIRE_YES = true
# Docker 生产环境默认要求依赖命中允许列表
PLUGIN_DEPENDENCY_REQUIRE_ALLOWLIST = true
# Docker 生产环境默认要求锁文件
PLUGIN_DEPENDENCY_REQUIRE_LOCKFILE = true
# 默认锁文件路径,CLI --lockfile 会覆盖该值
PLUGIN_DEPENDENCY_LOCKFILE = ''
# 默认允许列表路径
PLUGIN_DEPENDENCY_ALLOWLIST = ''
# 离线制品根目录
PLUGIN_DEPENDENCY_OFFLINE_DIR = ''
# 受控 PyPI 镜像地址,留空表示不改写 pip 安装参数
PLUGIN_DEPENDENCY_PIP_INDEX_URL = ''
# 受控 npm registry 地址,留空表示不改写 npm 安装参数
PLUGIN_DEPENDENCY_NPM_REGISTRY = ''
# 单条依赖安装命令超时时间(秒)
PLUGIN_DEPENDENCY_INSTALL_TIMEOUT = 600
+139 -4
View File
@@ -10,17 +10,33 @@ APP_HOST = '0.0.0.0'
# 应用端口 # 应用端口
APP_PORT = 9099 APP_PORT = 9099
# 应用版本 # 应用版本
APP_VERSION= '1.7.0' APP_VERSION= '1.10.0'
# 发布代际标识(生产滚动发布建议注入镜像 digest 或 commit SHA;留空时使用源码指纹)
APP_RELEASE_ID = ''
# 应用是否开启热重载 # 应用是否开启热重载
APP_RELOAD = false APP_RELOAD = false
# 应用工作进程数
APP_WORKERS = 1
# 应用是否开启IP归属区域查询 # 应用是否开启IP归属区域查询
APP_IP_LOCATION_QUERY = true APP_IP_LOCATION_QUERY = true
# 应用是否允许账号同时登录 # 应用是否允许账号同时登录
APP_SAME_TIME_LOGIN = true APP_SAME_TIME_LOGIN = true
# 应用是否为演示模式
APP_DEMO_MODE = false
# 应用是否禁用Swagger文档
APP_DISABLE_SWAGGER = true
# 应用是否禁用ReDoc文档
APP_DISABLE_REDOC = true
# 可信代理IP列表,多个值使用逗号分隔
APP_TRUSTED_PROXY_IPS = '127.0.0.1,::1'
# 可信代理跳数,单层Nginx代理通常为1
APP_TRUSTED_PROXY_HOPS = 1
# 首次启动默认安装并启用的内置插件,多个值使用逗号分隔,留空表示不自动启用
APP_DEFAULT_ENABLED_PLUGINS = 'ai'
# -------- Jwt配置 -------- # -------- Jwt配置 --------
# Jwt秘钥 # Jwt秘钥,留空时自动生成;也可使用 openssl rand -hex 32 手动生成
JWT_SECRET_KEY = 'b01c66dc2c58dc6a0aabfe2144256be36226de378bf87f72c0c795dda67f4d55' JWT_SECRET_KEY = ''
# Jwt算法 # Jwt算法
JWT_ALGORITHM = 'HS256' JWT_ALGORITHM = 'HS256'
# 令牌过期时间 # 令牌过期时间
@@ -63,4 +79,123 @@ REDIS_USERNAME = ''
# Redis密码 # Redis密码
REDIS_PASSWORD = '' REDIS_PASSWORD = ''
# Redis数据库 # Redis数据库
REDIS_DATABASE = 2 REDIS_DATABASE = 2
# -------- 日志配置 --------
# 是否启用日志脱敏
LOG_MASK_ENABLED = true
# 日志脱敏占位符
LOG_MASK_PLACEHOLDER = '******'
# 以下三项为互补关系(非互斥):
# 1) LOG_MASK_FIELDS:命中后执行全量脱敏
# 2) LOG_PARTIAL_MASK_FIELDS:命中后执行部分脱敏
# 3) LOG_CONFIG_SECRET_PATTERNS:按 configKey 关键词决定是否脱敏 configValue
# 全量脱敏字段,多个值使用逗号分隔
LOG_MASK_FIELDS = 'password,old_password,new_password,confirm_password,api_key,token,access_token,refresh_token,authorization,client_secret,secret,secret_key,private_key,private_key_pem,credential,credentials,sms_code,captcha_code,system_prompt'
# 部分脱敏字段,多个值使用逗号分隔
# 当前默认不对IP脱敏;如后期需要开启,可直接追加 ip,ipaddr,oper_ip,login_ip 并重启服务
LOG_PARTIAL_MASK_FIELDS = 'phonenumber,phone,mobile,email'
# 按配置键名识别敏感配置的关键词,多个值使用逗号分隔
LOG_CONFIG_SECRET_PATTERNS = 'password,token,secret,key,private,credential,access,jwt,captcha,sms'
# Redis Stream Key
LOG_STREAM_KEY = 'log:stream'
# Redis Stream 消费组名称
LOG_STREAM_GROUP = 'log_aggregator'
# Redis Stream 消费者名称前缀
LOG_STREAM_CONSUMER_PREFIX = 'worker'
# 每次读取的最大消息数量
LOG_STREAM_BATCH_SIZE = 100
# 阻塞读取等待时间(毫秒)
LOG_STREAM_BLOCK_MS = 2000
# Stream 最大长度(近似裁剪)
LOG_STREAM_MAXLEN = 100000
# Pending 回收最小空闲时间(毫秒)
LOG_STREAM_CLAIM_IDLE_MS = 60000
# Pending 回收检查间隔(毫秒)
LOG_STREAM_CLAIM_INTERVAL_MS = 5000
# 每次回收的最大消息数量
LOG_STREAM_CLAIM_BATCH_SIZE = 100
# 去重 Key 过期时间(秒)
LOG_STREAM_DEDUP_TTL = 3600
# 去重 Key 前缀
LOG_STREAM_DEDUP_PREFIX = 'log:dedup'
# stdout 输出是否为 JSON
LOGURU_JSON = false
# Loguru 最低输出级别
LOGURU_LEVEL = 'INFO'
# 是否输出到 stdout
LOGURU_STDOUT = true
# 是否启用文件日志
LOG_FILE_ENABLED = true
# 文件日志根目录
LOG_FILE_BASE_DIR = 'logs'
# 文件滚动策略
LOGURU_ROTATION = '50MB'
# 文件保留策略
LOGURU_RETENTION = '30 days'
# 文件压缩格式
LOGURU_COMPRESSION = 'zip'
# 实例标识(用于区分实例)
LOG_INSTANCE_ID = 'prod'
# 服务名称(用于统一标识服务)
LOG_SERVICE_NAME = 'ruoyi-fastapi-backend'
# Worker 标识(auto 自动生成)
LOG_WORKER_ID = 'auto'
# -------- 传输层加解密配置 --------
# 是否启用传输层加解密
TRANSPORT_CRYPTO_ENABLED = true
# 传输层加解密模式:off=关闭,optional=明文/密文兼容,required=命中接口强制加密
TRANSPORT_CRYPTO_MODE = 'optional'
# 传输层加解密算法标识
TRANSPORT_CRYPTO_ALGORITHM = 'RSA_OAEP_AES_256_GCM'
# 当前启用的密钥版本标识
TRANSPORT_CRYPTO_KID = 'default'
# 传输层RSA位数,需与下方密钥对匹配
TRANSPORT_CRYPTO_RSA_KEY_SIZE = 4096
# 传输层公钥,默认提供一套可用示例值;生产部署前请替换为正式密钥
TRANSPORT_CRYPTO_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAwA2ooWpsxLzIVMJp7Wcv\nvR0Bu8paFn8NVPpzz+wGpUlwP5DGK6pBGItsglNMZx56MSYMp3zVyqB95XUVZ6Ub\nQVyHJ6BXXIMs/BpzcHCbyXR/wWG+pKxQ8UEwaHVhK8X21xW1R0kzzwhgMw51unEs\nA81kskVlDdGeNyaOJg5QuofEErCfR1y0e+iqd1PBpIwdEY5L+BzvbVtyhsPz6dBu\n9YbEbKs7JvNle5vjc72ebbMIeGejHFQRYxihuamPCAEylj1qqpHk8U+r+3icxQsj\n8F/TySLuOy37iVWfD+5ikLyou4ZDI3hOYnIYHl194ZM5xVyOGBD/xdqZadXTLqL6\nSrldwZfZDBl3EGgRby8yJqO6SqGgyvyGWXZAaHoPMmF+quu/nUooqnp0fpl+LCqX\nhK67kbYaA9xJeHRaP04cg16imB7sBIXBqzkyZtkhC2BtlL0h5X7dRlAddrQ23z6d\nWdJA3qe3iBSSM8pmedv+Jgfau/PFam2051HqFxvJEh/jnc6rq1aIjL/d4Kk5imIa\nQ6acv+v5N6QVFptlcx6I7j8yZJ7WUHZlB1IxVqfPb69+985eknZyLul60gyu0kPG\nYUecypUu2wiNDBAErZlUxEujMWgJDFYCSonHxtwr88gDInjP0lvwd/OgqjDQ7hSo\nRYxPhTbwo68RJDpImBjn+YsCAwEAAQ==\n-----END PUBLIC KEY-----'
# 传输层私钥,必须与上方公钥成对使用;生产部署前请替换为正式密钥
TRANSPORT_CRYPTO_PRIVATE_KEY = '-----BEGIN RSA PRIVATE KEY-----\n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\n-----END RSA PRIVATE KEY-----'
# 历史密钥对配置,JSON数组格式,密钥轮换说明见 docs/transport_crypto_deployment.md
TRANSPORT_CRYPTO_LEGACY_KEY_PAIRS = '[]'
# 公钥缓存有效期(秒)
TRANSPORT_CRYPTO_PUBLIC_KEY_TTL_SECONDS = 3600
# 前端传输加密策略缓存有效期(秒)
TRANSPORT_CRYPTO_FRONTEND_CONFIG_TTL_SECONDS = 300
# GET/DELETE 请求参数加密后的最大 URL 长度
TRANSPORT_CRYPTO_MAX_GET_URL_LENGTH = 4096
# 请求时间窗允许偏差(秒)
TRANSPORT_CRYPTO_CLOCK_SKEW_SECONDS = 120
# 防重放随机数有效期(秒)
TRANSPORT_CRYPTO_REPLAY_TTL_SECONDS = 300
# 启用传输层加密的路径列表,多个值使用逗号分隔,留空表示默认全部启用
TRANSPORT_CRYPTO_ENABLED_PATHS = ''
# 强制要求传输层加密的路径列表,多个值使用逗号分隔
TRANSPORT_CRYPTO_REQUIRED_PATHS = ''
# 排除传输层加密的路径列表,多个值使用逗号分隔
TRANSPORT_CRYPTO_EXCLUDE_PATHS = '/openapi.json,/docs,/docs/oauth2-redirect,/redoc,/transport/crypto/frontend-config,/transport/crypto/public-key,/common/download,/common/download/resource,/common/files,/system/file/download'
# -------- 插件依赖安装策略配置 --------
# 策略模式:可填单一模式,或按环境映射。可选 disabled、plan_only、explicit、locked、offline
PLUGIN_DEPENDENCY_POLICY_MODE = 'dev=explicit,test=plan_only,stage=locked,prod=plan_only'
# 生产环境是否允许真实安装插件声明的 Python/npm 依赖
PLUGIN_DEPENDENCY_ALLOW_PROD_INSTALL = false
# 执行真实安装时是否要求显式确认
PLUGIN_DEPENDENCY_REQUIRE_YES = true
# 生产环境默认要求依赖命中允许列表
PLUGIN_DEPENDENCY_REQUIRE_ALLOWLIST = true
# 生产环境默认要求锁文件
PLUGIN_DEPENDENCY_REQUIRE_LOCKFILE = true
# 默认锁文件路径,CLI --lockfile 会覆盖该值
PLUGIN_DEPENDENCY_LOCKFILE = ''
# 默认允许列表路径
PLUGIN_DEPENDENCY_ALLOWLIST = ''
# 离线制品根目录
PLUGIN_DEPENDENCY_OFFLINE_DIR = ''
# 受控 PyPI 镜像地址,留空表示不改写 pip 安装参数
PLUGIN_DEPENDENCY_PIP_INDEX_URL = ''
# 受控 npm registry 地址,留空表示不改写 npm 安装参数
PLUGIN_DEPENDENCY_NPM_REGISTRY = ''
# 单条依赖安装命令超时时间(秒)
PLUGIN_DEPENDENCY_INSTALL_TIMEOUT = 600
+14
View File
@@ -0,0 +1,14 @@
FROM python:3.10
WORKDIR /app
# 复制源代码
COPY . .
# 安装依赖
RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
# 暴露端口
EXPOSE 9099
# 启动命令
CMD ["ruoyi", "app", "run", "--env=dockermy"]
+14
View File
@@ -0,0 +1,14 @@
FROM python:3.10
WORKDIR /app
# 复制源代码
COPY . .
# 安装依赖
RUN pip install --no-cache-dir -r requirements-pg.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
# 暴露端口
EXPOSE 9099
# 启动命令
CMD ["ruoyi", "app", "run", "--env=dockerpg"]
+23 -4
View File
@@ -1,13 +1,18 @@
import asyncio import asyncio
import os import os
from alembic import context from collections.abc import Iterable, MutableMapping
from logging.config import fileConfig from logging.config import fileConfig
from typing import Literal
from alembic import context
from alembic.migration import MigrationContext
from alembic.operations.ops import MigrationScript
from sqlalchemy import pool from sqlalchemy import pool
from sqlalchemy.engine import Connection from sqlalchemy.engine import Connection
from sqlalchemy.ext.asyncio import async_engine_from_config from sqlalchemy.ext.asyncio import async_engine_from_config
from config.database import Base, ASYNC_SQLALCHEMY_DATABASE_URL
from utils.import_util import ImportUtil
from config.database import ASYNC_SQLALCHEMY_DATABASE_URL, Base
from utils.import_util import ImportUtil
# 判断vesrions目录是否存在,如果不存在则创建 # 判断vesrions目录是否存在,如果不存在则创建
alembic_veresions_path = 'alembic/versions' alembic_veresions_path = 'alembic/versions'
@@ -60,7 +65,20 @@ def run_migrations_offline() -> None:
def do_run_migrations(connection: Connection) -> None: def do_run_migrations(connection: Connection) -> None:
def process_revision_directives(context, revision, directives): def include_name(
name: str | None,
type_: Literal['schema', 'table', 'column', 'index', 'unique_constraint', 'foreign_key_constraint'],
parent_names: MutableMapping[Literal['schema_name', 'table_name', 'schema_qualified_table_name'], str | None],
) -> bool:
if type_ == 'table':
return name in target_metadata.tables
return True
def process_revision_directives(
context: MigrationContext,
revision: str | Iterable[str | None] | Iterable[str],
directives: list[MigrationScript],
) -> None:
script = directives[0] script = directives[0]
# 检查所有操作集是否为空 # 检查所有操作集是否为空
@@ -79,6 +97,7 @@ def do_run_migrations(connection: Connection) -> None:
compare_type=True, compare_type=True,
compare_server_default=True, compare_server_default=True,
transaction_per_migration=True, transaction_per_migration=True,
include_name=include_name,
process_revision_directives=process_revision_directives, process_revision_directives=process_revision_directives,
) )
+8 -2
View File
@@ -1,12 +1,18 @@
import uvicorn import uvicorn
from server import app, AppConfig # noqa: F401
from config.env import AppConfig
from server import create_app
if __name__ != '__main__':
app = create_app()
if __name__ == '__main__': if __name__ == '__main__':
uvicorn.run( uvicorn.run(
app='app:app', app='server:create_app',
host=AppConfig.app_host, host=AppConfig.app_host,
port=AppConfig.app_port, port=AppConfig.app_port,
root_path=AppConfig.app_root_path, root_path=AppConfig.app_root_path,
reload=AppConfig.app_reload, reload=AppConfig.app_reload,
workers=AppConfig.app_workers,
factory=True,
) )
+69
View File
@@ -0,0 +1,69 @@
import ntpath
import os
import subprocess
from cli.runtime import RUNTIME_ENVIRONMENT, RuntimeEnvironmentService
_IS_WINDOWS = os.name == 'nt'
class AppBootstrapService:
"""
应用引导服务。
该服务负责解析应用原始入口路径、构建启动命令,并使用与当前平台兼容的方式
将 CLI 进程切换为或连接到 FastAPI 应用入口进程。
"""
def __init__(self, *, runtime_environment: RuntimeEnvironmentService | None = None) -> None:
"""
初始化应用引导服务。
:param runtime_environment: 运行时环境服务
:return: None
"""
self.runtime_environment = runtime_environment or RUNTIME_ENVIRONMENT
def get_app_entry_path(self) -> str:
"""
获取应用启动入口路径。
:return: 应用启动入口绝对路径
"""
backend_dir = self.runtime_environment.get_backend_dir()
if ntpath.splitdrive(backend_dir)[0] or backend_dir.startswith('\\\\'):
return ntpath.join(backend_dir, 'app.py')
return os.path.join(backend_dir, 'app.py')
def build_app_run_command(self, env: str) -> list[str]:
"""
构建应用启动命令。
:param env: 运行环境
:return: 应用启动命令参数列表
"""
return [
self.runtime_environment.get_python_executable(),
self.get_app_entry_path(),
'--env',
env,
]
def exec_app_run_command(self, env: str) -> None:
"""
以应用原始入口进程启动当前 FastAPI 应用。
:param env: 运行环境
:return: None
"""
command = self.build_app_run_command(env)
if _IS_WINDOWS:
try:
completed = subprocess.run(command, check=False)
except KeyboardInterrupt:
raise SystemExit(130) from None
raise SystemExit(completed.returncode)
os.execvp(command[0], command)
APP_BOOTSTRAP = AppBootstrapService()
@@ -0,0 +1,155 @@
from dataclasses import dataclass, field
from pathlib import Path
import typer
from cli.completion.controller import CompletionCommandController
from cli.completion.providers import COMPLETION_PROVIDER_GATEWAY, CompletionProviderGateway
from cli.context import OutputOption
@dataclass(frozen=True)
class CompletionCommandRegistration:
"""
completion 子命令注册描述。
:param name: 子命令名称
:param help_text: 子命令帮助文本
"""
name: str
help_text: str
@dataclass
class CompletionSubcommandRegistrar:
"""
completion 子命令注册器。
该对象负责将 `show/install/doctor` 子命令挂载到 completion 应用,
将 Typer 命令声明细节从构建器主体中拆出。
:param completion_provider_gateway: completion 提供器对外网关
"""
completion_provider_gateway: CompletionProviderGateway = field(default_factory=lambda: COMPLETION_PROVIDER_GATEWAY)
@staticmethod
def build_registrations() -> tuple[CompletionCommandRegistration, ...]:
"""
返回 completion 子命令注册描述列表。
:return: 子命令注册描述元组
"""
return (
CompletionCommandRegistration(name='show', help_text='输出指定 shell 的 completion 脚本'),
CompletionCommandRegistration(name='install', help_text='安装指定 shell 的 completion 脚本'),
CompletionCommandRegistration(name='doctor', help_text='检查当前 completion 配置状态'),
)
def register(self, app: typer.Typer, *, controller: CompletionCommandController) -> None:
"""
向 completion 应用注册全部子命令。
:param app: completion 子应用
:param controller: completion 命令控制器
:return: None
"""
registrations = {registration.name: registration for registration in self.build_registrations()}
@app.command(registrations['show'].name, help=registrations['show'].help_text)
def show(
shell: str = typer.Argument(
...,
help='shell 名称,如 bash、zsh、fish',
autocompletion=self.completion_provider_gateway.complete_shell_names,
),
) -> None:
"""
输出指定 shell 的 completion 脚本。
:param shell: shell 名称
:return: None
"""
controller.show(shell)
@app.command(registrations['install'].name, help=registrations['install'].help_text)
def install(
shell: str | None = typer.Option(
None,
'--shell',
help='shell 名称,默认自动识别当前 shell',
autocompletion=self.completion_provider_gateway.complete_shell_names,
),
output: OutputOption = 'text',
target_file: Path | None = typer.Option(None, '--target-file', help='completion 脚本目标文件路径'),
activate: bool = typer.Option(False, '--activate', help='将激活命令写入 shell rc 文件'),
rc_file: Path | None = typer.Option(None, '--rc-file', help='自定义 shell rc 文件路径'),
force: bool = typer.Option(False, '--force', help='覆盖已存在且内容不同的目标文件'),
) -> None:
"""
安装指定 shell 的 completion 脚本。
:param shell: shell 名称,默认自动识别当前 shell
:param output: 输出格式
:param target_file: completion 脚本目标文件路径
:param activate: 是否写入 shell rc 文件激活命令
:param rc_file: 自定义 shell rc 文件路径
:param force: 是否强制覆盖已存在文件
:return: None
"""
controller.install(
output,
shell=shell,
target_file=target_file,
activate=activate,
rc_file=rc_file,
force=force,
)
@app.command(registrations['doctor'].name, help=registrations['doctor'].help_text)
def doctor(
output: OutputOption = 'text',
) -> None:
"""
检查当前 completion 配置状态。
:param output: 输出格式
:return: None
"""
controller.doctor(output)
@dataclass
class CompletionCommandBuilder:
"""
completion 子应用构建器。
该构建器负责装配 `completion` 子应用,并将控制器实例和子命令注册
细节收口到类式对象协作中。
:param completion_subcommand_registrar: completion 子命令注册器
"""
completion_subcommand_registrar: CompletionSubcommandRegistrar = field(
default_factory=CompletionSubcommandRegistrar
)
def build(self, root_cli: typer.Typer) -> typer.Typer:
"""
构建 completion 命令组。
:param root_cli: 根 Typer 应用
:return: completion 子应用
"""
app = typer.Typer(
help='shell completion 相关命令',
no_args_is_help=True,
context_settings={'help_option_names': ['-h', '--help']},
)
completion_command_controller = CompletionCommandController(root_cli)
self.completion_subcommand_registrar.register(app, controller=completion_command_controller)
return app
COMPLETION_COMMAND_BUILDER = CompletionCommandBuilder()
@@ -0,0 +1,116 @@
from pathlib import Path
import typer
from cli.completion.doctor import COMPLETION_DOCTOR, CompletionDoctorService
from cli.completion.installers import COMPLETION_INSTALLER, CompletionInstallerService
from cli.completion.presenter import CompletionCommandPresenter
from cli.core import (
DEFAULT_CORE_SERVICES,
CliContextFactory,
CliExecutionService,
)
class CompletionCommandController:
"""
completion 命令控制器。
该控制器负责组织 `completion` 命令组的上下文准备、payload 生成、
文本渲染与脚本输出收口。
:param root_cli: 根 Typer 应用
:param context_factory: CLI 上下文工厂
:param execution_service: CLI 执行服务
:param presenter: completion 命令文本渲染器
:param installer_service: completion 安装与脚本生成服务
:param doctor_service: completion 诊断服务
"""
def __init__(
self,
root_cli: typer.Typer,
*,
context_factory: CliContextFactory | None = None,
execution_service: CliExecutionService | None = None,
presenter: CompletionCommandPresenter | None = None,
installer_service: CompletionInstallerService | None = None,
doctor_service: CompletionDoctorService | None = None,
) -> None:
"""
初始化 completion 命令控制器。
:param root_cli: 根 Typer 应用
:param context_factory: CLI 上下文工厂
:param execution_service: CLI 执行服务
:param presenter: completion 命令文本渲染器
:param installer_service: completion 安装与脚本生成服务
:param doctor_service: completion 诊断服务
:return: None
"""
self.root_cli = root_cli
self.context_factory = context_factory or DEFAULT_CORE_SERVICES.context_factory
self.execution_service = execution_service or DEFAULT_CORE_SERVICES.execution_service
self.presenter = presenter or CompletionCommandPresenter()
self.installer_service = installer_service or COMPLETION_INSTALLER
self.doctor_service = doctor_service or COMPLETION_DOCTOR
def show(self, shell: str) -> None:
"""
输出指定 shell 的 completion 脚本。
:param shell: shell 名称
:return: None
"""
typer.echo(self.installer_service.render_completion_script(self.root_cli, shell), nl=False)
def install(
self,
output: str,
*,
shell: str | None,
target_file: Path | None,
activate: bool,
rc_file: Path | None,
force: bool,
) -> None:
"""
安装指定 shell 的 completion 脚本。
:param output: 输出格式
:param shell: shell 名称
:param target_file: completion 脚本目标文件路径
:param activate: 是否写入 shell rc 文件激活命令
:param rc_file: 自定义 shell rc 文件路径
:param force: 是否强制覆盖已存在文件
:return: None
"""
ctx = self.context_factory.build_readonly('dev', output)
payload = self.installer_service.install_completion_script(
self.root_cli,
shell,
target_file=target_file,
activate=activate,
rc_file=rc_file,
force=force,
)
self.execution_service.complete_payload_result(
ctx,
payload,
text_builder=self.presenter.build_completion_install_text,
)
def doctor(self, output: str) -> None:
"""
检查当前 completion 配置状态。
:param output: 输出格式
:return: None
"""
ctx = self.context_factory.build_readonly('dev', output)
payload = self.doctor_service.build_completion_doctor_payload()
self.execution_service.complete_payload_result(
ctx,
payload,
text_builder=self.presenter.build_completion_doctor_text,
)
@@ -0,0 +1,95 @@
from pathlib import Path
from typing import Any
from cli.completion.installers import (
CLICK_COMPLETE_ENV_VAR,
COMPLETION_INSTALLER,
CompletionInstallerService,
)
from cli.metadata import (
COMPLETION_SHELL_SPEC_REGISTRY,
ENVIRONMENT_OPTION_SERVICE,
CompletionShellSpecRegistry,
EnvironmentOptionService,
)
class CompletionDoctorService:
"""
completion 诊断服务。
该服务负责汇总活跃 shell、目标脚本路径、rc 文件状态、推荐安装命令
与环境候选列表,生成 `completion doctor` 所需的结构化结果。
:param installer_service: completion 安装服务
:param shell_spec_registry: shell 元数据注册表
:param environment_option_service: 环境选项服务
"""
def __init__(
self,
*,
installer_service: CompletionInstallerService | None = None,
shell_spec_registry: CompletionShellSpecRegistry | None = None,
environment_option_service: EnvironmentOptionService | None = None,
) -> None:
"""
初始化 completion 诊断服务。
:param installer_service: completion 安装服务
:param shell_spec_registry: shell 元数据注册表
:param environment_option_service: 环境选项服务
:return: None
"""
self.installer_service = installer_service or COMPLETION_INSTALLER
self.shell_spec_registry = shell_spec_registry or COMPLETION_SHELL_SPEC_REGISTRY
self.environment_option_service = environment_option_service or ENVIRONMENT_OPTION_SERVICE
def build_completion_doctor_payload(self) -> dict[str, Any]:
"""
构建 completion 诊断结果。
:return: 诊断结果字典
"""
active_shell = self.installer_service.detect_active_shell()
shells: dict[str, dict[str, Any]] = {}
for shell_name, shell_spec in self.shell_spec_registry.specs.items():
target_file = self.installer_service.resolve_completion_target(shell_name)
rc_file = self.installer_service.resolve_completion_rc_file(shell_name)
source_command = None
if shell_spec.supported and shell_spec.generator in {'click', 'custom'}:
source_command = self.installer_service.build_source_command(target_file, shell_name)
recommended_install_command = f'ruoyi completion install --shell={shell_name}'
if shell_spec.supported and not shell_spec.auto_discovery:
recommended_install_command = f'{recommended_install_command} --activate'
shells[shell_name] = {
'supported': shell_spec.supported,
'detected': shell_name == active_shell,
'description': shell_spec.description,
'targetFile': str(target_file),
'targetFileExists': target_file.exists(),
'rcFile': str(rc_file) if rc_file is not None else None,
'rcFileExists': rc_file.exists() if rc_file is not None else None,
'autoDiscovery': shell_spec.auto_discovery,
'sourceCommand': source_command,
'recommendedInstallCommand': recommended_install_command,
}
recommended_shell = active_shell if self.shell_spec_registry.get_spec(active_shell) is not None else None
recommended_install_command = None
if recommended_shell is not None:
recommended_install_command = shells[recommended_shell]['recommendedInstallCommand']
return {
'ok': True,
'message': 'completion 诊断信息已生成',
'activeShell': active_shell or None,
'projectDir': str(Path.cwd().resolve()),
'envChoices': self.environment_option_service.discover_env_names(),
'completeEnvVar': CLICK_COMPLETE_ENV_VAR,
'recommendedInstallCommand': recommended_install_command,
'shells': shells,
}
COMPLETION_DOCTOR = CompletionDoctorService()

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