Compare commits

...
403 Commits
Author SHA1 Message Date
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
insistence f2de18aa43 !36 docs: 更新README文档
Merge pull request !36 from insistence/develop
2025-09-04 02:10:06 +00:00
insistence 24cc7e360a docs: 更新README文档 2025-09-04 09:45:19 +08:00
insistence 3c5f6bf873 chore: 升级版本至1.7.0 2025-09-04 09:45:07 +08:00
insistence 81c9b82abd perf: 优化样式 2025-09-03 17:20:25 +08:00
insistence ac3056b9a7 feat: 显示列信息支持对象格式 2025-09-03 17:18:37 +08:00
insistence 10719a4067 feat: 注册账号设置默认密码最后更新时间 2025-09-03 17:01:25 +08:00
insistence 5597eb651f feat: 账号密码支持自定义更新周期 2025-09-03 16:58:30 +08:00
insistence 385013db9d feat: 初始密码支持自定义修改策略 2025-09-03 16:44:31 +08:00
insistence 2699039c0b perf: 优化布局显示 2025-09-03 16:44:01 +08:00
insistence 831768dafd feat: 新增apscheduler_jobs表对应sqlalchemy模型类 2025-09-03 16:37:41 +08:00
insistence 6ea37823c3 feat&refactor: 菜单搜索支持键盘选择&悬浮主题背景 2025-08-29 16:13:31 +08:00
insistence 67e9f6c98b perf: 优化导航栏显示昵称&设置 2025-08-29 15:52:06 +08:00
insistence f19f192170 feat: 用户导入新增验证提示 2025-08-29 15:51:07 +08:00
insistence 7eb4b75ab1 feat: 用户归属部门新增清除 2025-08-29 15:46:50 +08:00
insistence d0138516e7 perf: 优化checkbox废弃API 2025-08-29 15:44:33 +08:00
insistence ae4f37f1c5 perf: 消除控制台警告信息 2025-08-29 15:42:13 +08:00
insistence 41a9d1edf6 chore: 升级组件依赖 2025-08-29 15:39:07 +08:00
insistence 61fba98d89 feat: 新增底部版权信息及开关 2025-08-29 14:47:44 +08:00
insistence cc6a35d7c7 feat: 新增页签图标显示开关功能 2025-08-29 11:33:05 +08:00
insistence ea0303835f feat: 新增用户默认初始化密码 2025-08-29 08:29:05 +08:00
insistence 7b993655ba perf: 优化代码 2025-08-29 08:21:15 +08:00
insistence d9d7c94513 feat: 代码生成列支持拖动排序 2025-08-27 09:26:01 +08:00
insistence cc6b38acdd feat: 图片上传组件新增disabled属性 2025-08-27 09:25:38 +08:00
insistence 27d847fd25 fix: 修复上传组件被多次引用拖动仅对第一个有效的问题 2025-08-27 08:54:10 +08:00
insistence 9e76400c8e feat: 上传组件新增拖动排序属性 2025-08-27 08:50:07 +08:00
insistence c365459d88 feat: 外链加载时遮罩信息提示 2025-08-27 08:43:17 +08:00
insistence 079b2eefb6 feat: 添加页签openPage支持传递参数 2025-08-27 08:38:43 +08:00
insistence c6d4c6c1a9 refactor: 富文本复制粘贴图片上传至url 2025-08-27 08:33:38 +08:00
insistence 9b6355c68d perf: 优化富文本控制台警告异常 2025-08-27 08:28:55 +08:00
insistence ccacee5f95 perf: 优化代码 2025-08-27 08:25:35 +08:00
insistence 397a51bd96 feat: 显隐列组件支持全选/全不选 2025-08-26 15:07:51 +08:00
insistence f144945215 perf: 优化代码 2025-08-26 11:16:17 +08:00
insistence a1c78172ca feat: 新增默认打包配置项 2025-08-26 11:08:34 +08:00
insistence f03ab1a845 feat: 支持文件&图片组件自定义地址&参数 2025-08-26 11:04:37 +08:00
insistence a64eca3aeb perf: 优化角色禁用不允许分配 2025-08-26 10:51:27 +08:00
insistence bc547f90f9 perf: 登录页和注册页表头使用VITE_APP_BASE_API配置值 2025-08-26 10:42:31 +08:00
insistence 7a66854a22 perf: 优化顶部菜单搜索栏为多层级显示 2025-08-26 10:28:18 +08:00
insistence 795a9204ae perf: 优化前端处理路由函数代码 2025-08-26 10:23:49 +08:00
insistence 11debaf213 refactor: 调整do与sql使其相互适配 2025-08-22 16:17:46 +08:00
insistence 857b7dba80 chore: 更新依赖文件 2025-08-22 16:17:33 +08:00
insistence 5d6bd822d3 perf: 优化启动信息显示 2025-08-21 17:20:55 +08:00
insistence 0b7df2cfc1 feat: 集成alembic以支持数据迁移 2025-08-21 17:20:13 +08:00
insistence 20bc7ebaaa chore: 更新依赖文件 2025-08-04 10:56:14 +08:00
insistence c1a8060139 refactor: 重构IP归属区域查询为异步调用 2025-08-04 10:55:43 +08:00
insistence 5558dd181b perf: 优化接口耗时计算 2025-07-28 15:07:31 +08:00
insistence 0fd4fb283a fix: 修复logout接口未按照app_same_time_login配置项动态判断的问题 #IBZZ1S 2025-07-28 14:55:21 +08:00
insistence 00a7af2d5b !30 RuoYi-Vue3-FastAPI 1.6.2
Merge pull request !30 from insistence/develop
2025-03-19 07:59:40 +00:00
insistence e34a493b3e docs: 更新README文档 2025-03-19 15:55:10 +08:00
insistence a9df9ea56a chore: 升级版本至1.6.2 2025-03-19 15:54:12 +08:00
insistence d89c0df425 perf: 优化代码生成vue模板 #23 2025-03-19 15:46:28 +08:00
insistence d0730e7993 fix: 修复用户导出缺失部门名称的问题 2025-03-18 16:55:16 +08:00
insistence 5fe376f978 perf: pagination更换成flex布局 2025-03-18 08:28:46 +08:00
insistence dd5b1c55c9 feat: 文件上传组件新增类型 2025-03-18 08:12:13 +08:00
insistence 7645c6d9fd feat: 文件上传组件新增disabled属性 2025-03-18 08:11:18 +08:00
insistence 61073970fa fix: 修复代码生成模板时间查询问题 #28 2025-03-17 16:32:46 +08:00
insistence 9ae2ac02eb perf: 优化代码生成新增和编辑字段显示和渲染 2025-03-17 09:51:05 +08:00
insistence 6622c329fc fix: 修复修改字典类型时字典数据更新时间异常的问题 2025-03-17 08:27:39 +08:00
insistence b66d545985 fix: 修复修改字典类型时获取dict_code异常的问题 2025-03-17 08:25:39 +08:00
insistence 1789cb5a9b fix: 修复定时任务状态暂停时执行单次任务会触发cron表达式的问题 #31 2025-03-15 21:59:28 +08:00
insistence ee376c477d fix: 修复日志管理时间查询报错 #27 2025-03-09 18:43:57 +08:00
jiangbaihe 2b6f0905a9 chore: 调整.gitignore策略 2025-03-04 14:58:04 +08:00
insistence c953754bed chore: pg依赖文件新增sqlglot依赖 2025-03-03 17:13:37 +08:00
insistence cea4a10baa !29 RuoYi-Vue3-FastAPI v1.6.1
Merge pull request !29 from insistence/develop
2025-03-03 08:50:47 +00:00
insistence b708d86eff docs: 更新README文档 2025-03-03 16:44:31 +08:00
insistence 28aab8c7d4 chore: 升级版本至1.6.1 2025-03-03 16:43:47 +08:00
insistence 9412e3f344 fix: 修复代码生成主子表vo模板可能缺失NotBlank的问题 2025-02-28 10:46:45 +08:00
insistence b4d5619b1b fix: 引入泛型修复as_query和as_form装饰模型文档丢失的问题 2025-02-28 10:36:29 +08:00
insistence 8ce598ad54 fix: 修复代码生成字段唯一性校验dao层模板判断异常的问题 2025-02-26 09:00:35 +08:00
insistence ca641055e0 fix: 引入sqlglot修复sql语句解析异常的问题 2025-02-21 15:40:47 +08:00
insistence a5a4099374 !28 RuoYi-Vue3-FastAPI v1.6.0
Merge pull request !28 from insistence/develop
2025-02-19 04:29:42 +00:00
insistence 729dc23a16 docs: 更新README文档 2025-02-19 12:25:48 +08:00
insistence 3e898083ac chore: 升级版本至1.6.0 2025-02-19 12:22:40 +08:00
insistence 492dd7b052 perf: 优化TopNav内链菜单点击没有高亮 2025-02-19 12:20:54 +08:00
insistence 3e5890b8c2 perf: 用户管理过滤掉已禁用部门 2025-02-19 12:18:35 +08:00
insistence e21bde54be chore: 更新后端依赖至最新版本 2025-02-19 11:51:04 +08:00
insistence fdf8e600a0 perf: 代码生成默认不允许生成文件覆盖到本地 2025-02-19 10:59:44 +08:00
insistence 1b5b89e282 perf: ResponseUtil补充完整参数 2025-02-19 10:54:56 +08:00
insistence 4cc9037f25 fix: 修复个人中心特殊字符密码修改失败问题 2025-02-19 10:32:02 +08:00
insistence e8b3a6c49b fix: 修复vue2模板主子表异常的问题 2025-02-19 08:43:47 +08:00
insistence 6bae2d2d0c fix: 修复vue2模版异常问题 2025-02-19 00:16:23 +08:00
insistence f42a0b530b fix: 修复代码生成树表功能异常的问题 2025-02-18 17:59:57 +08:00
insistence 0fdf45c73f fix: 修复代码生成主子表功能异常的问题 2025-02-18 17:46:37 +08:00
insistence fd07ad088c feat: 代码生成新增字段唯一性配置 2025-02-18 16:10:57 +08:00
insistence 2a486dc01e feat: 代码生成功能适配pg数据库 2025-02-18 11:36:33 +08:00
insistence 15686b44ad feat: 代码生成初步适配pg数据库 2025-02-17 23:06:22 +08:00
insistence 2b474ddb35 perf: 优化代码生成功能 2025-02-17 17:43:29 +08:00
insistence 25e2a1e931 feat: postgresql数据库sql语句新增视图 2025-02-16 23:30:28 +08:00
insistence 59f6e0091f feat: 新增代码生成vue2前端模板 2025-02-16 23:29:44 +08:00
insistence fe41a207fa perf: 优化代码生成功能 2025-02-16 23:29:15 +08:00
insistence 1d36c0c56e feat: 新增代码生成功能 2025-02-14 17:42:36 +08:00
insistence 7cca5c88f3 perf: 优化日志装饰器获取核心参数的方式 2025-01-26 16:34:27 +08:00
py1ren 00011f8419 !27 feat: 新增trace中间件强化日志链路追踪和响应头
* refactor: trace_log重命名为trace_middleware
* refactor: 日志处理器重构为类式写法
* perf: 移除无用文件
* perf: 优化trace中间件部分写法
* style: 格式化代码
* Merge branch 'master' into develop
* feature: 1.日志添加traceId链路追踪 2.response-header默认添加request-id与traceId对应
2025-01-24 01:34:33 +00:00
insistence 1cfd85f9de fix: 修复执行单次任务时会覆盖已启用任务的问题 #IBEKD2 2025-01-08 15:32:56 +08:00
insistence 3e05f1b8b3 fix: 修复定时任务目标字符串规则校验不全的问题 2024-12-27 15:02:53 +08:00
insistence a67c629fa6 perf: 优化日志中操作方法显示 2024-12-26 08:48:47 +08:00
insistence 691076bb14 fix: 修复删除当前登录用户拦截失效的问题 2024-12-26 08:48:22 +08:00
insistence 1d408e6a09 perf: 优化导出方法 2024-12-20 10:26:00 +08:00
insistence 3a3bbcc32a feat: 新增表单构建功能 2024-12-11 10:08:07 +08:00
insistence a2a0e4ff1c perf: 优化代码 2024-12-11 10:01:45 +08:00
insistence 1453402ac3 feat: 支持开启暗黑模式 2024-12-11 09:59:50 +08:00
insistence 7ce127a9e6 feat: 白名单支持对通配符路径匹配 2024-12-11 09:22:48 +08:00
insistence c9ecfd213c perf: 优化代码 2024-12-11 09:16:41 +08:00
insistence 1d08ba9905 feat: 菜单面包屑导航支持多层级显示 2024-12-11 09:04:55 +08:00
insistence 6ffcf4dca7 feat: 用户管理支持分栏拖动 2024-12-11 08:47:14 +08:00
insistence 8f851f5e51 fix: 修复默认关闭Tags-Views时,内链页面打不开 2024-12-10 15:53:06 +08:00
insistence 29b672cdab perf: 参数键值更换为多行文本 2024-12-10 15:16:40 +08:00
insistence 27fe0e04f6 feat: 用户头像新增支持http(s)链接 2024-12-10 15:02:16 +08:00
insistence 591dbe06a2 !22 RuoYi-Vue3-FastAPI v1.5.1
Merge pull request !22 from insistence/develop
2024-11-12 07:35:06 +00:00
insistence 43b7931591 docs: 更新README文档 2024-11-12 15:31:01 +08:00
insistence 51257df07c chore: 升级版本至1.5.1 2024-11-12 15:30:54 +08:00
insistence c282f362f4 perf: 移除已弃用的log_decorator装饰器 2024-11-12 15:10:17 +08:00
insistence 425d6e00eb perf: 校检文件名是否包含特殊字符 2024-11-08 17:10:54 +08:00
insistence 564d470240 perf: 优化字典数组条件判断 2024-11-08 17:06:21 +08:00
insistence a1937448d6 refactor: 定时任务改用AsyncIOScheduler和AsyncIOExecutor 2024-11-07 10:01:49 +08:00
insistence 1ddb1bd5b5 feat: 定时任务新增支持调用异步函数 2024-11-06 22:28:09 +08:00
insistence 4f4ee34e8d style: 修复书写错误 2024-10-31 15:38:58 +08:00
insistence 8202e8c9db !21 RuoYi-Vue3-FastAPI v1.5.0
Merge pull request !21 from insistence/develop
2024-10-22 12:05:05 +00:00
insistence b797ffb5af docs: 更新README文档 2024-10-22 20:02:09 +08:00
insistence c2795c49e4 chore: 升级版本至1.5.0 2024-10-22 20:01:59 +08:00
insistence 545cb6606c revert: 因fastapi查询参数模型底层存在bug,回滚查询参数模型声明方式为as_query 2024-10-22 19:41:28 +08:00
insistence d0211cfd11 perf: 优化上传图片带域名时不增加前缀 2024-10-22 19:36:27 +08:00
insistence a3c9ffdad5 fix: 修复登录日志导出文件名称错误的问题 2024-10-22 19:34:13 +08:00
insistence 5ce1ffd557 perf: 优化参数设置页面 2024-10-22 17:04:34 +08:00
insistence 850d8d7890 fix: 修复DictTag组件控制台抛异常的问题 #IAYSVZ 2024-10-22 17:04:08 +08:00
insistence 53cc6eb8ce perf: 优化列表查询排序 2024-10-22 16:28:08 +08:00
insistence c7af974d3f feat: 新增对PostgreSQL数据库的支持 2024-10-22 16:27:47 +08:00
insistence bb143ac15a perf: 优化CamelCaseUtil和SnakeCaseUtil以兼容更多转换场景 2024-10-21 11:11:05 +08:00
insistence c6c9020d89 !18 RuoYi-Vue3-FastAPI v1.4.0
Merge pull request !18 from insistence/develop
2024-09-29 02:24:08 +00:00
insistence df1929ab24 docs: 更新README文档 2024-09-29 09:50:32 +08:00
insistence 3d15c37182 chore: 升级版本至1.4.0 2024-09-29 09:50:23 +08:00
insistence d9810bfb67 perf: 优化前端登录请求方法 2024-09-29 09:47:25 +08:00
insistence 5a04889f84 refactor: 基于fastapi0.115.0版本新特性,直接使用pydantic模型接收查询参数和表单数据,移除as_query和as_form使用方式 2024-09-29 09:43:38 +08:00
insistence 8a964bf3f5 chore: 更新fastapi版本至0.115.0 2024-09-29 09:39:56 +08:00
insistence f573e3e93b style: 修复书写错误 2024-09-24 20:20:04 +08:00
insistence 84a02a42aa !17 RuoYi-Vue3-FastAPI v1.3.3
Merge pull request !17 from insistence/develop
2024-09-04 01:16:02 +00:00
insistence c750908c04 docs: 更新README文档 2024-09-04 09:07:42 +08:00
insistence af89388294 chore: 升级版本至1.3.3 2024-09-04 09:07:34 +08:00
insistence 09380dc94f chore: 更新pydantic-validation-decorator版本为0.1.4,修复了一些底层bug 2024-09-04 09:02:13 +08:00
insistence 82c84b2afe perf: 日志装饰器异常处理增加logger打印日志 2024-09-04 09:01:41 +08:00
insistence f404aba689 fix: 修复在线用户模块条件查询无效的问题 2024-08-17 16:52:46 +08:00
insistence 9013b1917e perf: 优化在线用户模块前后端字段描述一致 2024-08-17 16:51:38 +08:00
insistence c415dfa8e1 !16 RuoYi-Vue3-FastAPI v1.3.2
Merge pull request !16 from insistence/develop
2024-08-08 01:38:22 +00:00
insistence 5765e967ae docs: 更新README文档 2024-08-08 09:28:33 +08:00
insistence 1ba4d959ce chore: 升级版本至1.3.2 2024-08-08 09:27:51 +08:00
insistence df8ab6bc55 chore: 调整.gitignore策略 2024-08-08 09:24:09 +08:00
insistence ca6668331f style: 优化添加中间件函数注释 2024-08-08 09:20:57 +08:00
insistence d49d05b776 feat: 新增gzip压缩中间件 2024-08-08 09:18:07 +08:00
insistence d8e3f7dca1 fix: 修复定时任务监听函数中事件没有job_id报错的问题 2024-08-07 14:41:57 +08:00
insistence 5ee1a64587 fix: 修复分页函数计算has_next错误的问题 #10 2024-08-02 10:27:06 +08:00
insistence 9a31c21943 !15 RuoYi-Vue3-FastAPI v1.3.1
Merge pull request !15 from insistence/develop
2024-07-31 03:24:06 +00:00
insistence e52f0f42cb docs: 更新README文档 2024-07-31 11:14:38 +08:00
insistence 1f51525dfa chore: 升级版本至1.3.1 2024-07-31 11:13:57 +08:00
insistence a1c77829cd perf: 补充定时任务违规字符串 2024-07-31 10:59:01 +08:00
insistence 07771c180d fix: 修复1.3.0版本采用新的异常处理机制后日志装饰器无法记录异常日志的问题 2024-07-27 16:31:01 +08:00
insistence d55ddbebaf !14 RuoYi-Vue3-FastAPI v1.3.0
Merge pull request !14 from insistence/develop
2024-07-16 10:11:31 +00:00
insistence bea5d9428e docs: 更新README文档 2024-07-16 15:24:51 +08:00
insistence e275cbdaee chore: 升级版本至1.3.0 2024-07-16 15:23:25 +08:00
insistence 5742582073 chore: 调整vue版本以适应部分模块 2024-07-16 15:21:52 +08:00
insistence eab51c0355 perf: 优化API文档字段描述显示 2024-07-16 15:19:36 +08:00
insistence 6734df119a perf: 优化用户管理模块部分接口逻辑 2024-07-16 15:15:43 +08:00
insistence 42269f78c3 chore: 使用PyJWT替换python-jose以解决一些安全性问题 2024-07-16 09:40:56 +08:00
insistence 93d7ecb1ee chore: 更新依赖至最新版本 2024-07-16 09:39:24 +08:00
insistence 70e1b7a078 refactor: 重构RedisInitKeyConfig为枚举类型 2024-07-16 08:52:49 +08:00
insistence e359471361 perf: 对数据权限sql去重 2024-07-16 08:13:11 +08:00
insistence 7df6616e54 refactor: 重构数据权限逻辑 2024-07-15 21:31:01 +08:00
insistence 2263897111 perf: 优化部分ids字符串分割写法 2024-07-15 21:24:17 +08:00
insistence d1b6db4769 feat: 菜单管理新增路由名称 2024-07-15 15:46:20 +08:00
insistence a001ff8099 fix: 修复重复的getList 2024-07-15 14:59:33 +08:00
insistence ee625ea3ed style: 优化代码 2024-07-15 14:57:58 +08:00
insistence 3cf31828e0 perf: cron生成的表达式hour优化 2024-07-15 14:37:40 +08:00
insistence bcf7467308 perf: 优化部分ids进行字符串分割后的校验 2024-07-15 14:21:05 +08:00
insistence 997a3068ac fix: 修复升级element-plus后el-radio使用label过期的问题 2024-07-15 11:38:29 +08:00
insistence 753403b0f8 fix: 修复check_role_data_scope_services参数类型不匹配的问题 2024-07-15 11:37:28 +08:00
insistence 1d4fb2cdb2 fix: 修复validate_second_or_minute参数丢失的问题 2024-07-15 11:36:34 +08:00
insistence 82ce4f00f5 refactor: 重构日志装饰器为Log 2024-07-15 10:39:52 +08:00
insistence 068503d275 feat: 新增CronUtil工具类 2024-07-15 08:49:15 +08:00
insistence c39375b776 perf: 优化定时任务模块service层及异常处理 2024-07-14 22:37:35 +08:00
insistence 4669d6b29b chore: 调整ServiceException的日志级别 2024-07-13 18:16:55 +08:00
insistence e3b422cc6e style: 优化部分文件中的函数及类注释 2024-07-13 18:15:35 +08:00
insistence c23a3ccfaf style: 使用ruff格式化app.py,优化导入 2024-07-12 11:36:06 +08:00
insistence 5d8b177a12 style: 使用ruff格式化server.py,优化导入 2024-07-12 11:35:58 +08:00
insistence 5dd6513706 style: 使用ruff格式化aspect模块,优化导入 2024-07-12 11:31:10 +08:00
insistence bdb1a67d44 style: 使用ruff格式化annotation模块,优化导入 2024-07-12 11:29:15 +08:00
insistence 7af22e97ad style: 使用ruff格式化sub_applications模块,优化导入 2024-07-12 11:23:46 +08:00
insistence fea346413c style: 使用ruff格式化module_task模块,优化导入 2024-07-12 11:23:01 +08:00
insistence 44af38612c style: 使用ruff格式化middlewares模块,优化导入 2024-07-12 11:22:23 +08:00
insistence 13cfc60782 style: 使用ruff格式化exceptions模块,优化导入 2024-07-12 11:21:37 +08:00
insistence 9b40c05e64 style: 使用ruff格式化config模块,优化导入 2024-07-12 11:19:07 +08:00
insistence ed9ad54b46 style: 使用ruff格式化utils模块,优化导入 2024-07-12 11:13:01 +08:00
insistence 380693c66d style: 使用ruff格式化验证码模块,优化导入 2024-07-12 11:06:57 +08:00
insistence 91e31cfbf7 style: 使用ruff格式化服务监控模块,优化导入 2024-07-12 11:04:40 +08:00
insistence 371fed69df style: 使用ruff格式化缓存监控及列表模块,优化导入 2024-07-12 10:59:41 +08:00
insistence f41ee26067 style: 使用ruff格式化通用模块,优化导入 2024-07-12 10:56:26 +08:00
insistence 749fe85cad style: 使用ruff格式化参数配置模块,优化导入 2024-07-12 10:51:34 +08:00
insistence eb29d616d1 style: 使用ruff格式化部门管理模块,优化导入 2024-07-12 10:45:10 +08:00
insistence eac920fcae style: 使用ruff格式化字典管理模块,优化导入 2024-07-12 10:38:36 +08:00
insistence 659163d662 style: 使用ruff格式化定时任务模块,优化导入 2024-07-12 10:28:28 +08:00
insistence ccb9e2a314 style: 使用ruff格式化日志管理模块,优化导入 2024-07-12 10:13:34 +08:00
insistence 5891ec1930 style: 使用ruff格式化菜单管理模块,优化导入 2024-07-12 09:59:12 +08:00
insistence 2c86992cac style: 使用ruff格式化通知公告管理模块,优化导入 2024-07-12 09:50:10 +08:00
insistence 6d0d772749 style: 使用ruff格式化在线用户模块,优化导入 2024-07-12 09:43:36 +08:00
insistence ca86c5ffb7 style: 使用ruff格式化岗位管理模块,优化导入 2024-07-12 09:38:56 +08:00
insistence e80a44c43a style: 使用ruff格式化用户管理模块do层,优化导入 2024-07-12 09:36:08 +08:00
insistence 4a7c9367d4 style: 使用ruff格式化角色管理模块,优化导入 2024-07-12 09:35:55 +08:00
insistence bd242c95f7 style: 使用ruff格式化登录模块,优化导入 2024-07-12 09:35:26 +08:00
insistence 40a8c73461 style: 使用ruff格式化用户管理模块,优化导入 2024-07-11 22:24:58 +08:00
insistence 5ae234714f feat: 新增ruff配置文件 2024-07-11 22:24:17 +08:00
insistence 449b7581d8 perf: 优化用户管理模块部分接口数据权限校验 2024-07-11 21:38:44 +08:00
insistence d0eafb1e09 feat: 部门管理和菜单管理service层引入ServiceWarning 2024-07-11 17:17:56 +08:00
insistence 47d5697653 feat: 新增全局处理自定义服务警告 2024-07-11 16:47:23 +08:00
insistence 0dfc201990 perf: 优化角色管理模块数据权限 2024-07-11 16:36:28 +08:00
insistence cd260fbcda perf: 优化部门管理模块service层及异常处理 2024-07-11 16:36:04 +08:00
insistence f87ae9a25b perf: 优化数据权限设置 2024-07-10 21:30:11 +08:00
insistence 0ee473fd07 perf: 优化部分接口dao层查询 2024-07-10 21:29:50 +08:00
insistence 36f45b2c26 fix: 修复升级axios到最新版本后表单数据需手动指定Content-Type的问题 2024-07-10 21:24:00 +08:00
insistence 51bed591d4 perf: 优化缓存监控及缓存列表模块service层及异常处理 2024-07-10 21:21:22 +08:00
insistence f255595b30 perf: 优化通用模块service层及异常处理 2024-07-10 21:14:41 +08:00
insistence bbd4a4cd95 perf: 优化日志管理模块service层及异常处理 2024-07-10 21:08:55 +08:00
insistence 95b0cdebfb perf: 优化验证码模块异常处理 2024-07-10 21:02:40 +08:00
insistence 0737adb389 perf: 优化登录模块service层及异常处理 2024-07-10 21:00:43 +08:00
insistence 9878482654 feat: 新增全局处理自定义登录异常 2024-07-10 21:00:25 +08:00
insistence 4d31fe7a92 perf: 优化在线用户模块service层及异常处理 2024-07-10 20:50:39 +08:00
insistence 55babcee4c perf: 优化服务监控模块异常处理 2024-07-10 20:47:51 +08:00
insistence 1bd4bba2f5 perf: 优化角色管理模块service层及异常处理 2024-07-10 17:11:03 +08:00
insistence 013ea25731 perf: 优化菜单管理模块service层及异常处理 2024-07-10 11:08:07 +08:00
insistence 4a17ffbebe perf: 优化字典管理模块service层及异常处理 2024-07-10 10:33:44 +08:00
insistence e3efca9648 perf: 优化岗位管理模块service层及异常处理 2024-07-09 21:17:50 +08:00
insistence da54a7e5f5 perf: 优化通知公告管理模块service层及异常处理 2024-07-09 21:03:05 +08:00
insistence 1eff840afa perf: 优化用户管理模块service层及异常处理 2024-07-09 17:07:33 +08:00
insistence 2b19022e29 perf: 优化参数管理模块service层及异常处理 2024-07-09 16:25:27 +08:00
insistence 2fef16e58b feat: 新增全局处理自定义服务异常及其他异常 2024-07-09 16:25:13 +08:00
insistence 09f9dd2784 perf: 参数管理模块service层优化 2024-07-08 16:47:17 +08:00
insistence e9c4f295ce perf: 编辑用户时接口请求体中删除password 2024-07-07 13:07:48 +08:00
insistence 3684d5a703 fix: 修复编辑字典时修改字典类型异常的问题 2024-07-07 13:06:16 +08:00
insistence 82c61753e3 style: 删除无用的自定义异常 2024-07-07 09:46:29 +08:00
insistence 19f34d6a81 chore: 将validate_annotation封装为pydantic-validation-decorator库 2024-07-07 09:09:19 +08:00
insistence d500e22b2f feat: 用户管理模块新增和编辑接口新增字段校验 2024-07-05 12:52:41 +08:00
insistence 568ed2b9d7 feat: 角色管理模块新增和编辑接口新增字段校验 2024-07-05 12:52:26 +08:00
insistence bfac3ff26e feat: 通知公告管理模块新增和编辑接口新增字段校验 2024-07-05 12:52:18 +08:00
insistence ddfe7c8851 feat: 菜单管理模块新增和编辑接口新增字段校验 2024-07-05 12:52:09 +08:00
insistence d12c1c31f3 feat: 定时任务管理模块新增和编辑接口新增字段校验 2024-07-05 12:52:00 +08:00
insistence d5cd7d1ae4 feat: 字典管理模块新增和编辑接口新增字段校验 2024-07-05 12:51:50 +08:00
insistence 841fbd1ae1 feat: 部门管理模块新增和编辑接口新增字段校验 2024-07-05 12:51:41 +08:00
insistence 1f0f35b402 feat: 参数管理模块新增和编辑接口新增字段校验 2024-07-05 12:51:34 +08:00
insistence ffc932951b perf: 优化validate_annotation中各装饰器逻辑 2024-07-05 12:50:36 +08:00
insistence 9f09d37936 perf: 日志管理模块vo层优化 2024-07-05 11:43:52 +08:00
insistence f8c246796d perf: 服务监控模块vo层优化 2024-07-05 11:19:31 +08:00
insistence 68906b7e81 perf: 在线用户监控模块vo层优化 2024-07-05 11:07:55 +08:00
insistence fffff50a00 perf: 登录模块vo层优化 2024-07-05 11:07:33 +08:00
insistence b7260a40ad perf: 通用模块vo层优化 2024-07-05 11:07:22 +08:00
insistence a12713f89a perf: 缓存监控模块vo层优化 2024-07-05 11:07:06 +08:00
insistence 0fa264a7bc feat&perf: 用户管理模块vo层优化,新增字段校验 2024-07-05 09:56:49 +08:00
insistence 5dcef7b408 feat&perf: 角色管理模块vo层优化,新增字段校验 2024-07-05 09:31:04 +08:00
insistence b6f1827fe0 feat&perf: 通知公告管理模块vo层优化,新增字段校验 2024-07-05 09:13:54 +08:00
insistence 57dd4178f4 feat&perf: 菜单管理模块vo层优化,新增字段校验 2024-07-05 09:07:17 +08:00
insistence cd131ba7a2 feat&perf: 定时任务管理模块vo层优化,新增字段校验 2024-07-05 08:50:42 +08:00
insistence fad9f484e8 feat&perf: 字典管理模块vo层优化,新增字段校验 2024-07-05 08:31:38 +08:00
insistence 45063936eb perf: 优化参数管理vo层config_type的类型提示 2024-07-05 08:31:19 +08:00
insistence d1cc297169 refactor: check_annotation.py重命名为validate_annotation.py 2024-07-05 08:11:52 +08:00
insistence cebbe90eac feat&perf: 部门管理模块vo层优化,新增字段校验 2024-07-04 17:30:14 +08:00
insistence 0ca5aab05a feat: 新增@NetWork字段校验装饰器 2024-07-04 17:29:54 +08:00
insistence a66061aa89 chore: 更新fastapi版本至0.111.0 2024-07-04 17:29:44 +08:00
insistence d98d5e8c41 feat&perf: 参数管理模块vo层优化,新增字段校验 2024-07-04 14:32:46 +08:00
insistence 212f981b6a feat: 新增@Pattern字段校验装饰器 2024-07-04 11:52:05 +08:00
insistence 2b3849a573 feat: 新增@Xss字段校验装饰器 2024-07-04 10:57:37 +08:00
insistence b5d0b5a170 feat: 新增字符串工具类 2024-07-04 10:56:44 +08:00
insistence 66595d10a3 feat: 岗位管理模块新增和编辑接口新增字段校验 2024-07-04 10:56:23 +08:00
insistence 2a20042680 feat: 新增@ValidateFields字段校验装饰器 2024-07-04 10:56:05 +08:00
insistence 549948ba37 feat: 新增FieldValidatorException全局异常处理 2024-07-04 09:54:49 +08:00
insistence 2f3e5171b0 feat&perf: 岗位管理模块vo层优化,新增字段校验 2024-07-03 21:09:43 +08:00
insistence cfa68999a9 feat: 新增@NotBlank和@Size装饰器用于字段校验 2024-07-03 21:08:00 +08:00
insistence 6109c45dbe feat: 新增常量配置 2024-07-03 16:41:20 +08:00
insistence e8a4922541 style: 删除多余print 2024-07-03 09:38:50 +08:00
insistence 71b4a10991 perf: 优化用户管理dao层status和del_flag类型与数据库保持一致 2024-07-01 21:56:17 +08:00
insistence 0d2aa3e8b8 perf: 优化角色管理dao层status和del_flag类型与数据库保持一致 2024-07-01 21:50:15 +08:00
insistence d7d8dcda1c perf: 优化岗位管理dao层status类型与数据库保持一致 2024-07-01 21:48:14 +08:00
insistence 1fa14118ff perf: 优化菜单管理dao层status和del_flag类型与数据库保持一致 2024-07-01 21:46:43 +08:00
insistence 7bb947c14a perf: 优化登录模块dao层status和del_flag类型与数据库保持一致 2024-07-01 21:44:04 +08:00
insistence d8011c8738 perf: 优化定时任务管理dao层status类型与数据库保持一致 2024-07-01 21:41:10 +08:00
insistence adb6d63eb4 perf: 优化字典管理dao层status类型与数据库保持一致 2024-07-01 21:39:22 +08:00
insistence f1baf96f95 perf: 优化部门管理dao层status和del_flag类型与数据库保持一致 2024-07-01 21:35:29 +08:00
insistence d0e82b56ab fix: 修复移动端左侧菜单无法显示的问题 2024-06-30 21:55:37 +08:00
insistence c64519d8f9 chore: 升级前端组件依赖到最新版本 2024-06-30 21:40:35 +08:00
insistence 77b4eb10b1 perf: 优化@log_decorator参数的类型提示 2024-06-30 21:04:50 +08:00
insistence e705a1c32b !13 RuoYi-Vue3-FastAPI v1.2.2
Merge pull request !13 from insistence/develop
2024-06-29 12:16:55 +00:00
insistence 114cda0b76 docs: 更新README文档 2024-06-29 20:13:42 +08:00
insistence 847445df82 chore: 升级版本至1.2.2 2024-06-29 20:13:03 +08:00
insistence 2360c7a12d fix: 修复菜单生成路由时组件条件判断错误的问题 2024-06-29 19:49:52 +08:00
insistence 581ef15917 fix: 修复删除定时任务时未移除调度中任务的问题 2024-06-29 17:03:28 +08:00
insistence 3180da36da !12 RuoYi-Vue3-FastAPI v1.2.1
Merge pull request !12 from insistence/develop
2024-06-28 06:40:53 +00:00
insistence ad20a5da70 docs: 更新README文档 2024-06-28 14:35:18 +08:00
insistence b04debcf75 chore: 升级版本至1.2.1 2024-06-28 14:31:48 +08:00
insistence 2cf5268f7d perf: 修改代理localhost为127.0.0.1以适配部分设备解析localhost异常的问题 2024-06-28 14:31:22 +08:00
insistence 43f903c6e0 fix: 修复菜单挂载到根目录时路由加载异常等一系列相关问题 2024-06-27 21:51:01 +08:00
insistence d08fe67ab8 fix: 修复各模块新增数据时创建时间记录异常的问题 2024-06-26 21:05:25 +08:00
insistence 88f9574aaa chore: 回退通用上传接口返回url修复 2024-06-16 15:23:28 +08:00
insistence 2387c2e7df fix: 修复通用上传接口返回url异常的问题 2024-06-15 15:44:50 +08:00
insistence 1dd4d8296b !11 RuoYi-Vue3-FastAPI v1.2.0
Merge pull request !11 from insistence/async-database
2024-05-17 08:17:47 +00:00
insistence b1e3fcdc30 docs: 更新README文档 2024-05-17 16:04:06 +08:00
insistence 6064535c22 chore: 升级版本至1.2.0 2024-05-17 16:03:40 +08:00
insistence 8b3d04ca06 feat: 新增SnakeCaseUtil工具类,将原CamelCaseUtil工具类的camel_to_snake函数迁移至SnakeCaseUtil工具类 2024-05-17 15:59:12 +08:00
insistence 4b13e492fc style: 修复.env.prod文件书写错误 2024-05-17 15:16:59 +08:00
insistence 5319327c37 chore: 增加asyncmy依赖用于支持orm异步操作mysql 2024-05-17 15:11:14 +08:00
insistence 3b13237ad6 fix: 修复清空定时任务日志异常的问题 2024-05-17 15:07:17 +08:00
insistence ef39f5aa1e fix: 修复用户管理模块重置用户密码时会异常重置用户岗位和角色的问题 2024-05-17 15:05:34 +08:00
insistence 1972f1cb6d refactor: 重构数据库orm为异步 #I9MRQS 2024-05-17 15:04:30 +08:00
insistence 18103e3d38 !10 RuoYi-Vue3-FastAPI v1.1.3
Merge pull request !10 from insistence/develop
2024-05-13 01:20:40 +00:00
insistence cb96c878bf docs: 更新README文档 2024-05-13 09:16:04 +08:00
insistence 266b3e3b5c chore: 升级版本至1.1.3 2024-05-13 09:15:01 +08:00
insistence 6ea8ada989 fix: 修复个人中心修改基本资料后端异常的问题 2024-05-11 21:15:14 +08:00
insistence 901a66bafa feat: 用户密码新增非法字符验证 2024-05-11 14:58:02 +08:00
insistence c9cb8c6542 fix: 修复通知公告列表查询前后端字段不一致的问题 2024-05-11 09:05:07 +08:00
399 changed files with 41374 additions and 8349 deletions
+1
View File
@@ -0,0 +1 @@
github: [insistence]
+170
View File
@@ -0,0 +1,170 @@
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
sed "s/FROM python:3.10/FROM python:${{ matrix.python-version }}/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.my
cp ruoyi-fastapi-backend/Dockerfile.my ruoyi-fastapi-backend/Dockerfile.my.bak
# Create temporary Dockerfile with target Python version
sed "s/FROM python:3.10/FROM python:${{ matrix.python-version }}/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.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.my
if: always()
run: |
# Restore original Dockerfile.my after test
mv ruoyi-fastapi-backend/Dockerfile.my.bak ruoyi-fastapi-backend/Dockerfile.my
+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
+17
View File
@@ -18,6 +18,7 @@ lib64/
parts/
sdist/
var/
vf_admin/
wheels/
share/python-wheels/
*.egg-info/
@@ -135,3 +136,19 @@ dmypy.json
# Cython debug symbols
cython_debug/
# PyCharm
.idea/
# VSCode
.vscode/
# AI Editor
.agent/
.claude/
.codebuddy/
.codex/
.cursor/
.opencode/
.qoder/
.trae/
+547
View File
@@ -0,0 +1,547 @@
# 更新日志
## 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接口文档。
+195 -60
View File
@@ -1,105 +1,191 @@
<p align="center">
<img alt="logo" src="https://oscimg.oschina.net/oscnet/up-d3d0a9303e11d522a06cd263f3079027715.png">
</p>
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">RuoYi-Vue3-FastAPI v1.1.2</h1>
<h1 align="center">
<img alt="logo" src="https://oscimg.oschina.net/oscnet/up-d3d0a9303e11d522a06cd263f3079027715.png">
</h1>
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">RuoYi-Vue3-FastAPI</h1>
<h4 align="center">基于RuoYi-Vue3+FastAPI前后端分离的快速开发框架</h4>
<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://github.com/insistence/RuoYi-Vue3-FastAPI"><img src="https://img.shields.io/github/stars/insistence/RuoYi-Vue3-FastAPI?style=social"></a>
<a href="https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI"><img src="https://img.shields.io/badge/RuoYiVue3FastAPI-v1.1.2-brightgreen.svg"></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>
<img src="https://img.shields.io/badge/python-≥3.8-blue">
<img src="https://img.shields.io/badge/MySQL-≥5.7-blue">
<a href="https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/stargazers">
<img alt="Gitee" src="https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/badge/star.svg?theme=dark">
</a>
<a href="https://github.com/insistence/RuoYi-Vue3-FastAPI">
<img alt="Github" src="https://img.shields.io/github/stars/insistence/RuoYi-Vue3-FastAPI?style=social">
</a>
<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.9.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>
## 平台简介
RuoYi-Vue3-FastAPI是一套全部开源的快速开发平台,毫无保留给个人及企业免费使用。
* 前端采用Vue、Element Plus,基于<u>[RuoYi-Vue3](https://github.com/yangzongzhuan/RuoYi-Vue3)</u>前端项目修改。
* 端采用FastAPI、sqlalchemy、MySQL、Redis、OAuth2 & Jwt
* 前端采用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。
* 权限认证使用OAuth2 & Jwt,支持多终端认证系统。
* 支持加载动态权限菜单,多方式轻松权限控制。
* Vue2版本:
- Gitte仓库地址:https://gitee.com/insistence2022/RuoYi-Vue-FastAPI
- GitHub仓库地址:https://github.com/insistence/RuoYi-Vue-FastAPI
* Gitte仓库地址:<https://gitee.com/insistence2022/RuoYi-Vue-FastAPI>
* GitHub仓库地址:<https://github.com/insistence/RuoYi-Vue-FastAPI>
* 纯Python版本:
- Gitte仓库地址:https://gitee.com/insistence2022/dash-fastapi-admin
- GitHub仓库地址:https://github.com/insistence/Dash-FastAPI-Admin
* 特别鸣谢:<u>[RuoYi-Vue3](https://github.com/yangzongzhuan/RuoYi-Vue3)</u>
* Gitte仓库地址:<https://gitee.com/insistence2022/dash-fastapi-admin>
* GitHub仓库地址:<https://github.com/insistence/Dash-FastAPI-Admin>
* 特别鸣谢:<u>[RuoYi-Vue3](https://github.com/yangzongzhuan/RuoYi-Vue3)</u>、<u>[RuoYi-App](https://github.com/yangzongzhuan/RuoYi-App)</u>
## 内置功能
1. 用户管理:用户是系统操作者,该功能主要完成系统用户配置。
2. 角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分。
3. 菜单管理:配置系统菜单,操作权限,按钮权限标识等。
4. 部门管理:配置系统组织机构(公司、部门、小组)。
5. 岗位管理:配置系统用户所属担任职务。
6. 字典管理:对系统中经常使用的一些较为固定的数据进行维护。
7. 参数管理:对系统动态配置常用参数。
8. 通知公告:系统通知公告信息发布维护。
9. 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
1. 用户管理:用户是系统操作者,该功能主要完成系统用户配置。
2. 角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分。
3. 菜单管理:配置系统菜单,操作权限,按钮权限标识等。
4. 部门管理:配置系统组织机构(公司、部门、小组)。
5. 岗位管理:配置系统用户所属担任职务。
6. 字典管理:对系统中经常使用的一些较为固定的数据进行维护。
7. 参数管理:对系统动态配置常用参数。
8. 通知公告:系统通知公告信息发布维护。
9. 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
10. 登录日志:系统登录日志记录查询包含登录异常。
11. 在线用户:当前系统中活跃用户状态监控。
12. 定时任务:在线(添加、修改、删除)任务调度包含执行结果日志。
13. 服务监控:监视当前系统CPU、内存、磁盘、堆栈等相关信息。
14. 缓存监控:对系统的缓存信息查询,命令统计等。
15. 系统接口:根据业务代码自动生成相关的api接口文档
15. 在线构建器:拖动表单元素生成相应的HTML代码
16. 系统接口:根据业务代码自动生成相关的api接口文档。
17. 代码生成:配置数据库表信息一键生成前后端代码(python、sql、vue、js),支持下载。
18. AI管理:提供AI模型管理和AI对话功能。
## 演示图
<table>
<tr>
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/login.png"/></td>
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/dashboard.png"/></td>
<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>
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/user.png"/></td>
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/role.png"/></td>
<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>
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/menu.png"/></td>
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/dept.png"/></td>
<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>
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/post.png"/></td>
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/dict.png"/></td>
<td>
<img alt=""post src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/post.png">
</td>
<td>
<img alt="dict" src="https://gitee.com/insistence2022/ruoyi-fastapi-pictures/raw/master/vue3/web/dict.png">
</td>
</tr>
<tr>
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/config.png"/></td>
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/notice.png"/></td>
<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>
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/operLog.png"/></td>
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/loginLog.png"/></td>
<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>
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/online.png"/></td>
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/job.png"/></td>
<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>
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/server.png"/></td>
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/cache.png"/></td>
<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>
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/cacheList.png"></td>
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/api.png"></td>
<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>
<td><img src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/profile.png"/></td>
<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>
<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="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>
</table>
## 在线体验
- *账号:admin*
- *密码admin123*
- 演示地址:<a href="https://vfadmin.insistence.tech">vfadmin管理系统<a>
* *账号admin*
* *密码:admin123*
* 演示地址:<a href="https://vfadmin.insistence.tech">vfadmin管理系统<a>
## 项目开发及发布相关
@@ -114,6 +200,7 @@ cd RuoYi-Vue3-FastAPI
```
#### 前端
```bash
# 进入前端目录
cd ruoyi-fastapi-frontend
@@ -128,26 +215,49 @@ npm install --registry=https://registry.npmmirror.com
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
# 进入后端目录
cd ruoyi-fastapi-backend
# 安装项目依赖环境
# 如果使用的是MySQL数据库,请执行以下命令安装项目依赖环境
pip3 install -r requirements.txt
# 如果使用的是PostgreSQL数据库,请执行以下命令安装项目依赖环境
pip3 install -r requirements-pg.txt
# 配置环境
在.env.dev文件中配置开发环境的数据库和redis
# 运行sql文件
1.新建数据库ruoyi-fastapi(默认,可修改)
2.使用命令或数据库连接工具运行sql文件夹下的ruoyi-fastapi.sql
2.如果使用的是MySQL数据库,使用命令或数据库连接工具运行sql文件夹下的ruoyi-fastapi.sql;如果使用的是PostgreSQL数据库,使用命令或数据库连接工具运行sql文件夹下的ruoyi-fastapi-pg.sql
# 运行后端
python3 app.py --env=dev
```
#### 访问
```bash
# 默认账号密码
账号:admin
@@ -160,6 +270,7 @@ python3 app.py --env=dev
### 发布
#### 前端
```bash
# 构建测试环境
npm run build:stage 或 yarn build:stage
@@ -169,6 +280,7 @@ npm run build:prod 或 yarn build:prod
```
#### 后端
```bash
# 配置环境
在.env.prod文件中配置生产环境的数据库和redis
@@ -177,14 +289,37 @@ npm run build:prod 或 yarn build:prod
python3 app.py --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即可进群。
<table>
<tr>
<td><img alt="zsxq" src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/zsxq.jpg"></td>
<td><img alt="zanzhu" src="https://gitee.com/insistence2022/RuoYi-Vue-FastAPI/raw/master/demo-pictures/zanzhu.jpg"></td>
<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>
<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>
</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
+97
View File
@@ -0,0 +1,97 @@
{
"name": "ruoyi-fastapi-app",
"version": "1.9.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",
"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.9.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.9.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;
}
}
+74
View File
@@ -0,0 +1,74 @@
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";
let timeout = 10000;
const baseUrl = config.baseUrl;
const request = (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
.request({
method: config.method || "get",
timeout: config.timeout || timeout,
url: config.baseUrl || baseUrl + config.url,
data: config.data,
header: config.header,
dataType: "json",
})
.then((response) => {
const res = response;
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((res) => {
uni.reLaunch({ url: "/pages/login" });
});
}
});
reject("无效的会话,或者会话已过期,请重新登录。");
} else if (code === 500) {
toast(msg);
reject("500");
} else if (code !== 200) {
toast(msg);
reject(code);
}
resolve(res.data);
})
.catch((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);
});
});
};
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;
+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"]
}
+39
View File
@@ -0,0 +1,39 @@
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";
// 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(),
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"],
},
},
},
};
});
+57 -1
View File
@@ -10,13 +10,21 @@ APP_HOST = '0.0.0.0'
# 应用端口
APP_PORT = 9099
# 应用版本
APP_VERSION= '1.1.2'
APP_VERSION= '1.9.0'
# 应用是否开启热重载
APP_RELOAD = true
# 应用工作进程数
APP_WORKERS = 1
# 应用是否开启IP归属区域查询
APP_IP_LOCATION_QUERY = true
# 应用是否允许账号同时登录
APP_SAME_TIME_LOGIN = true
# 应用是否为演示模式
APP_DEMO_MODE = false
# 应用是否禁用Swagger文档
APP_DISABLE_SWAGGER = false
# 应用是否禁用ReDoc文档
APP_DISABLE_REDOC = false
# -------- Jwt配置 --------
# Jwt秘钥
@@ -30,6 +38,8 @@ JWT_REDIS_EXPIRE_MINUTES = 30
# -------- 数据库配置 --------
# 数据库类型,可选的有'mysql'、'postgresql',默认为'mysql'
DB_TYPE = 'mysql'
# 数据库主机
DB_HOST = '127.0.0.1'
# 数据库端口
@@ -62,3 +72,49 @@ REDIS_USERNAME = ''
REDIS_PASSWORD = ''
# Redis数据库
REDIS_DATABASE = 2
# -------- 日志配置 --------
# 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'
+120
View File
@@ -0,0 +1,120 @@
# -------- 应用配置 --------
# 应用运行环境
APP_ENV = 'prod'
# 应用名称
APP_NAME = 'RuoYi-FastAPI'
# 应用代理路径
APP_ROOT_PATH = '/docker-api'
# 应用主机
APP_HOST = '0.0.0.0'
# 应用端口
APP_PORT = 9099
# 应用版本
APP_VERSION= '1.9.0'
# 应用是否开启热重载
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
# -------- Jwt配置 --------
# Jwt秘钥
JWT_SECRET_KEY = 'b01c66dc2c58dc6a0aabfe2144256be36226de378bf87f72c0c795dda67f4d55'
# 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
# -------- 日志配置 --------
# 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'
+120
View File
@@ -0,0 +1,120 @@
# -------- 应用配置 --------
# 应用运行环境
APP_ENV = 'prod'
# 应用名称
APP_NAME = 'RuoYi-FastAPI'
# 应用代理路径
APP_ROOT_PATH = '/docker-api'
# 应用主机
APP_HOST = '0.0.0.0'
# 应用端口
APP_PORT = 9099
# 应用版本
APP_VERSION= '1.9.0'
# 应用是否开启热重载
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
# -------- Jwt配置 --------
# Jwt秘钥
JWT_SECRET_KEY = 'b01c66dc2c58dc6a0aabfe2144256be36226de378bf87f72c0c795dda67f4d55'
# 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
# -------- 日志配置 --------
# 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'
+58 -2
View File
@@ -10,13 +10,21 @@ APP_HOST = '0.0.0.0'
# 应用端口
APP_PORT = 9099
# 应用版本
APP_VERSION= '1.1.2'
APP_VERSION= '1.9.0'
# 应用是否开启热重载
APP_RELOAD = false
# 应用工作进程数
APP_WORKERS = 1
# 应用是否开启IP归属区域查询
APP_IP_LOCATION_QUERY = true
# 应用是否允许账号同时登录
APP_SAMETIME_LOGIN = true
APP_SAME_TIME_LOGIN = true
# 应用是否为演示模式
APP_DEMO_MODE = false
# 应用是否禁用Swagger文档
APP_DISABLE_SWAGGER = true
# 应用是否禁用ReDoc文档
APP_DISABLE_REDOC = true
# -------- Jwt配置 --------
# Jwt秘钥
@@ -30,6 +38,8 @@ JWT_REDIS_EXPIRE_MINUTES = 30
# -------- 数据库配置 --------
# 数据库类型,可选的有'mysql'、'postgresql',默认为'mysql'
DB_TYPE = 'mysql'
# 数据库主机
DB_HOST = '127.0.0.1'
# 数据库端口
@@ -62,3 +72,49 @@ REDIS_USERNAME = ''
REDIS_PASSWORD = ''
# Redis数据库
REDIS_DATABASE = 2
# -------- 日志配置 --------
# 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'
+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 ["python", "app.py", "--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 ["python", "app.py", "--env=dockerpg"]
+153
View File
@@ -0,0 +1,153 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts.
# this is typically a path given in POSIX (e.g. forward slashes)
# format, relative to the token %(here)s which refers to the location of this
# ini file
script_location = %(here)s/alembic
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
# for all available tokens
file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory. for multiple paths, the path separator
# is defined by "path_separator" below.
prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python>=3.9 or backports.zoneinfo library and tzdata library.
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
# string value is passed to ZoneInfo()
# leave blank for localtime
# timezone =
# max length of characters to apply to the "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; This defaults
# to <script_location>/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "path_separator"
# below.
# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions
# path_separator; This indicates what character is used to split lists of file
# paths, including version_locations and prepend_sys_path within configparser
# files such as alembic.ini.
# The default rendered in new alembic.ini files is "os", which uses os.pathsep
# to provide os-dependent path splitting.
#
# Note that in order to support legacy alembic.ini files, this default does NOT
# take place if path_separator is not present in alembic.ini. If this
# option is omitted entirely, fallback logic is as follows:
#
# 1. Parsing of the version_locations option falls back to using the legacy
# "version_path_separator" key, which if absent then falls back to the legacy
# behavior of splitting on spaces and/or commas.
# 2. Parsing of the prepend_sys_path option falls back to the legacy
# behavior of splitting on spaces, commas, or colons.
#
# Valid values for path_separator are:
#
# path_separator = :
# path_separator = ;
# path_separator = space
# path_separator = newline
#
# Use os.pathsep. Default configuration used for new projects.
path_separator = os
# set to 'true' to search source files recursively
# in each "version_locations" directory
# new in Alembic version 1.10
# recursive_version_locations = false
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
# database URL. This is consumed by the user-maintained env.py script only.
# other means of configuring database URLs may be customized within the env.py
# file.
sqlalchemy.url = driver://user:pass@localhost/dbname
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module
# hooks = ruff
# ruff.type = module
# ruff.module = ruff
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
# Alternatively, use the exec runner to execute a binary found on your PATH
# hooks = ruff
# ruff.type = exec
# ruff.executable = ruff
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
# Logging configuration. This is also consumed by the user-maintained
# env.py script only.
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARNING
handlers = console
qualname =
[logger_sqlalchemy]
level = WARNING
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
[settings]
# This section is used to set environment variables for the alembic.
# The env option is used to specify the environment for the alembic.
# It can be set to dev or prod.
env = dev
+1
View File
@@ -0,0 +1 @@
Generic single-database configuration with an async dbapi.
+135
View File
@@ -0,0 +1,135 @@
import asyncio
import os
from collections.abc import Iterable, MutableMapping
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.engine import Connection
from sqlalchemy.ext.asyncio import async_engine_from_config
from config.database import ASYNC_SQLALCHEMY_DATABASE_URL, Base
from utils.import_util import ImportUtil
# 判断vesrions目录是否存在,如果不存在则创建
alembic_veresions_path = 'alembic/versions'
if not os.path.exists(alembic_veresions_path):
os.makedirs(alembic_veresions_path)
# 自动查找所有模型
found_models = ImportUtil.find_models(Base)
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
alembic_config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
if alembic_config.config_file_name is not None:
fileConfig(alembic_config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
target_metadata = Base.metadata
# ASYNC_SQLALCHEMY_DATABASE_URL = 'mysql+asyncmy://root:mysqlroot@127.0.0.1:3306/ruoyi-fastapi'
# other values from the config, defined by the needs of env.py,
alembic_config.set_main_option('sqlalchemy.url', ASYNC_SQLALCHEMY_DATABASE_URL)
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = alembic_config.get_main_option('sqlalchemy.url')
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={'paramstyle': 'named'},
)
with context.begin_transaction():
context.run_migrations()
def do_run_migrations(connection: Connection) -> None:
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]
# 检查所有操作集是否为空
all_empty = all(ops.is_empty() for ops in script.upgrade_ops_list)
if all_empty:
# 如果没有实际变更,不生成迁移文件
directives[:] = []
print('❎️ 未检测到模型变更,不生成迁移文件')
else:
print('✅️ 检测到模型变更,生成迁移文件')
context.configure(
connection=connection,
target_metadata=target_metadata,
compare_type=True,
compare_server_default=True,
transaction_per_migration=True,
include_name=include_name,
process_revision_directives=process_revision_directives,
)
with context.begin_transaction():
context.run_migrations()
async def run_async_migrations() -> None:
"""In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = async_engine_from_config(
alembic_config.get_section(alembic_config.config_ini_section, {}),
prefix='sqlalchemy.',
poolclass=pool.NullPool,
)
async with connectable.connect() as connection:
await connection.run_sync(do_run_migrations)
await connectable.dispose()
def run_migrations_online() -> None:
"""Run migrations in 'online' mode."""
asyncio.run(run_async_migrations())
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
@@ -0,0 +1,28 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
def upgrade() -> None:
"""Upgrade schema."""
${upgrades if upgrades else "pass"}
def downgrade() -> None:
"""Downgrade schema."""
${downgrades if downgrades else "pass"}
+9 -3
View File
@@ -1,12 +1,18 @@
import uvicorn
from server import app, AppConfig
from config.env import AppConfig
from server import create_app
if __name__ != '__main__':
app = create_app()
if __name__ == '__main__':
uvicorn.run(
app='app:app',
app='server:create_app',
host=AppConfig.app_host,
port=AppConfig.app_port,
root_path=AppConfig.app_root_path,
reload=AppConfig.app_reload
reload=AppConfig.app_reload,
workers=AppConfig.app_workers,
factory=True,
)
@@ -0,0 +1,412 @@
import inspect
import json
import time
from collections.abc import Awaitable, Callable
from datetime import datetime
from functools import wraps
from typing import Any, Literal, TypeVar
import httpx
from async_lru import alru_cache
from fastapi import Request
from fastapi.responses import JSONResponse, ORJSONResponse, UJSONResponse
from starlette.status import HTTP_200_OK
from typing_extensions import ParamSpec
from user_agents import parse
from common.context import RequestContext
from common.enums import BusinessType
from config.env import AppConfig
from exceptions.exception import LoginException, ServiceException, ServiceWarning
from module_admin.entity.vo.log_vo import LogininforModel, OperLogModel
from module_admin.service.log_service import LogQueueService
from utils.dependency_util import DependencyUtil
from utils.log_util import logger
from utils.response_util import ResponseUtil
P = ParamSpec('P')
R = TypeVar('R')
class Log:
"""
日志装饰器
"""
def __init__(
self,
title: str,
business_type: BusinessType,
log_type: Literal['login', 'operation'] | None = 'operation',
) -> None:
"""
日志装饰器
:param title: 当前日志装饰器装饰的模块标题
:param business_type: 业务类型(OTHER其它 INSERT新增 UPDATE修改 DELETE删除 GRANT授权 EXPORT导出 IMPORT导入 FORCE强退 GENCODE生成代码 CLEAN清空数据)
:param log_type: 日志类型(login表示登录日志,operation表示为操作日志)
:return:
"""
self.title = title
self.business_type = business_type.value
self.log_type = log_type
self._oper_param_len = 2000
def __call__(self, func: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[R]]:
@wraps(func)
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
start_time = time.perf_counter()
# 获取当前被装饰函数所在路径
func_path = self._get_decorator_func_path(func)
# 获取上下文信息
request_name_list = get_function_parameters_name_by_type(func, Request)
request = get_function_parameters_value_by_name(func, request_name_list[0], *args, **kwargs)
DependencyUtil.check_exclude_routes(request, err_msg='当前路由不在认证规则内,不可使用Log装饰器')
request_method = request.method
user_agent = request.headers.get('User-Agent')
# 获取操作类型
operator_type = self._get_oper_type(user_agent)
# 获取请求的url
oper_url = request.url.path
# 获取请求ip
oper_ip = request.headers.get('X-Forwarded-For')
# 获取请求ip归属区域
oper_location = await self._get_oper_location(oper_ip)
# 获取请求参数
oper_param = await self._get_request_params(request)
# 日志表请求参数字段长度最大为2000,因此在此处判断长度
if len(oper_param) > self._oper_param_len:
oper_param = '请求参数过长'
# 获取操作时间
oper_time = datetime.now()
# 此处在登录之前向原始函数传递一些登录信息,用于监测在线用户的相关信息
login_log = self._get_login_log(user_agent, oper_ip, oper_location, oper_time, kwargs)
try:
# 调用原始函数
result = await func(*args, **kwargs)
except (LoginException, ServiceWarning) as e:
logger.warning(e.message)
result = ResponseUtil.failure(data=e.data, msg=e.message)
except ServiceException as e:
logger.error(e.message)
result = ResponseUtil.error(data=e.data, msg=e.message)
except Exception as e:
logger.exception(e)
result = ResponseUtil.error(msg=str(e))
# 获取请求耗时
cost_time = float(time.perf_counter() - start_time) * 1000
# 判断请求是否来自api文档
request_from_swagger, request_from_redoc = self._is_request_from_swagger_or_redoc(request)
# 根据响应结果的类型使用不同的方法获取响应结果参数
result_dict = self._get_result_dict(result, request_from_swagger, request_from_redoc)
json_result = json.dumps(result_dict, ensure_ascii=False)
# 根据响应结果获取响应状态及异常信息
status, error_msg = self._get_status_and_error_msg(result_dict)
# 根据日志类型向对应的日志表插入数据
if self.log_type == 'login':
# 登录请求来自于api文档时不记录登录日志,其余情况则记录
if request_from_swagger or request_from_redoc:
pass
else:
user = kwargs.get('form_data')
login_log.update(
{
'loginTime': oper_time,
'userName': user.username,
'status': str(status),
'msg': result_dict.get('msg'),
}
)
await LogQueueService.enqueue_login_log(request, LogininforModel(**login_log), func_path)
else:
current_user = RequestContext.get_current_user()
oper_name = current_user.user.user_name
dept_name = current_user.user.dept.dept_name if current_user.user.dept else None
operation_log = OperLogModel(
title=self.title,
businessType=self.business_type,
method=func_path,
requestMethod=request_method,
operatorType=operator_type,
operName=oper_name,
deptName=dept_name,
operUrl=oper_url,
operIp=oper_ip,
operLocation=oper_location,
operParam=oper_param,
jsonResult=json_result,
status=status,
errorMsg=error_msg,
operTime=oper_time,
costTime=int(cost_time),
)
await LogQueueService.enqueue_operation_log(request, operation_log, func_path)
return result
return wrapper
def _get_decorator_func_path(self, func: Callable) -> str:
"""
获取被装饰函数所在路径
:param func: 被装饰函数
:return: 被装饰函数所在路径
"""
# 获取被装饰函数所在的模块
module = inspect.getmodule(func)
# 获取完整模块路径
module_path = module.__name__ if module else ''
# 获取当前被装饰函数所在路径
func_path = f'{module_path}.{func.__name__}()'
return func_path
def _get_oper_type(self, user_agent: Any) -> int:
"""
获取操作类型
:param user_agent: 用户代理字符串
:return: 操作类型
"""
operator_type = 0
if 'Windows' in user_agent or 'Macintosh' in user_agent or 'Linux' in user_agent:
operator_type = 1
if 'Mobile' in user_agent or 'Android' in user_agent or 'iPhone' in user_agent:
operator_type = 2
return operator_type
async def _get_oper_location(self, oper_ip: str) -> str:
"""
获取请求IP归属区域
:param oper_ip: 请求IP
:return: 请求IP归属区域
"""
oper_location = '内网IP'
if AppConfig.app_ip_location_query:
oper_location = await get_ip_location(oper_ip)
return oper_location
async def _get_request_params(self, request: Request) -> str:
"""
获取请求参数
:param request: Request对象
:return: 格式化后的请求参数字符串
"""
params = {}
# 路径和查询参数
path_params = dict(request.path_params)
query_params = dict(request.query_params)
params.update({k: v for k, v in {'path_params': path_params, 'query_params': query_params}.items() if v})
# 请求体处理
content_type = request.headers.get('Content-Type', '')
# JSON请求
if 'application/json' in content_type:
json_body = await request.json()
if json_body:
params['json_body'] = json_body
# 表单数据
elif 'multipart/form-data' in content_type or 'application/x-www-form-urlencoded' in content_type:
form_data = await request.form()
if form_data:
# 过滤掉文件对象,只保留普通表单字段
form_dict = {key: value for key, value in form_data.items() if not hasattr(value, 'filename')}
if form_dict:
params['form_data'] = form_dict
# 仅在multipart时尝试处理文件
if 'multipart/form-data' in content_type:
file_info = {}
for key, value in form_data.items():
if hasattr(value, 'filename'):
file_info[key] = {
'filename': value.filename,
'content_type': value.content_type,
'size': value.size,
'headers': dict(value.headers),
}
if file_info:
params['files'] = file_info
# 其他文本请求
elif 'application/octet-stream' not in content_type:
body = await request.body()
if body:
params['raw_body'] = body.decode('utf-8')
return json.dumps(params, ensure_ascii=False, indent=2) if params else ''
def _get_login_log(
self, user_agent: Any, oper_ip: str, oper_location: str, oper_time: datetime, origin_kwargs: dict
) -> dict:
"""
获取登录日志信息
:param user_agent: 用户代理字符串
:param oper_ip: 操作ip
:param oper_location: 操作区域
:param oper_time: 操作时间
:param origin_kwargs: 原始函数参数
:return: 登录日志信息
"""
login_log = {}
if self.log_type == 'login':
user_agent_info = parse(user_agent)
browser = f'{user_agent_info.browser.family}'
system_os = f'{user_agent_info.os.family}'
if user_agent_info.browser.version != ():
browser += f' {user_agent_info.browser.version[0]}'
if user_agent_info.os.version != ():
system_os += f' {user_agent_info.os.version[0]}'
login_log = {
'ipaddr': oper_ip,
'loginLocation': oper_location,
'browser': browser,
'os': system_os,
'loginTime': oper_time.strftime('%Y-%m-%d %H:%M:%S'),
}
self._set_login_data(login_log, origin_kwargs)
return login_log
def _set_login_data(self, login_log: dict, origin_kwargs: dict) -> None:
"""
设置登录日志数据
:param login_log: 登录日志信息
:param origin_kwargs: 原始函数参数
:return: None
"""
if 'form_data' in origin_kwargs:
origin_kwargs['form_data'].login_info = login_log
def _get_status_and_error_msg(self, result_dict: dict) -> tuple[int, str]:
"""
获取操作状态和错误信息
:param result_dict: 操作结果字典
:return: 操作状态和错误信息元组
"""
status = 1
error_msg = ''
if result_dict.get('code') == HTTP_200_OK:
status = 0
else:
error_msg = result_dict.get('msg')
return status, error_msg
def _is_request_from_swagger_or_redoc(self, request: Request) -> tuple[bool, bool]:
"""
判断请求是否来自swagger或redoc
:param request: Request对象
:return: 是否来自swagger请求和是否来自redoc请求元组
"""
request_from_swagger = (
request.headers.get('referer').endswith('docs') if request.headers.get('referer') else False
)
request_from_redoc = (
request.headers.get('referer').endswith('redoc') if request.headers.get('referer') else False
)
return request_from_swagger, request_from_redoc
def _get_result_dict(self, result: Any, request_from_swagger: bool, request_from_redoc: bool) -> dict:
"""
获取操作结果字典
:param result: 操作结果
:param request_from_swagger: 是否来自swagger请求
:param request_from_redoc: 是否来自redoc请求
:return: 操作结果字典
"""
if isinstance(result, (JSONResponse, ORJSONResponse, UJSONResponse)):
result_dict = json.loads(str(result.body, 'utf-8'))
elif request_from_swagger or request_from_redoc:
result_dict = {}
elif result.status_code == HTTP_200_OK:
result_dict = {'code': result.status_code, 'message': '获取成功'}
else:
result_dict = {'code': result.status_code, 'message': '获取失败'}
return result_dict
@alru_cache()
async def get_ip_location(oper_ip: str) -> str:
"""
查询ip归属区域
:param oper_ip: 需要查询的ip
:return: ip归属区域
"""
oper_location = '内网IP'
try:
if oper_ip not in ['127.0.0.1', 'localhost']:
oper_location = '未知'
async with httpx.AsyncClient() as client:
ip_result = await client.get(f'https://qifu-api.baidubce.com/ip/geo/v1/district?ip={oper_ip}')
if ip_result.status_code == HTTP_200_OK:
prov = ip_result.json().get('data', {}).get('prov')
city = ip_result.json().get('data', {}).get('city')
if prov or city:
oper_location = f'{prov}-{city}'
except Exception as e:
oper_location = '未知'
print(e)
return oper_location
def get_function_parameters_name_by_type(func: Callable, param_type: Any) -> list:
"""
获取函数指定类型的参数名称
:param func: 函数
:param arg_type: 参数类型
:return: 函数指定类型的参数名称
"""
# 获取函数的参数信息
parameters = inspect.signature(func).parameters
# 找到指定类型的参数名称
parameters_name_list = []
# 遍历所有参数
for name, param in parameters.items():
# 处理参数注解
annotation = param.annotation
# 检查参数类型是否匹配
# 1. 直接匹配
# 2. 检查是否为Annotated类型(通过类型名称判断)
if annotation == param_type or (
hasattr(annotation, '__class__')
and annotation.__class__.__name__ == '_AnnotatedAlias'
and annotation.__origin__ == param_type
):
parameters_name_list.append(name)
return parameters_name_list
def get_function_parameters_value_by_name(func: Callable, name: str, *args, **kwargs) -> Any:
"""
获取函数指定参数的值
:param func: 函数
:param name: 参数名
:return: 参数值
"""
# 获取参数值
bound_parameters = inspect.signature(func).bind(*args, **kwargs)
bound_parameters.apply_defaults()
parameters_value = bound_parameters.arguments.get(name)
return parameters_value
@@ -1,27 +1,31 @@
import inspect
from typing import Type
from typing import TYPE_CHECKING, TypeVar
from fastapi import Query, Form
from fastapi import Form, Query
from pydantic import BaseModel
from pydantic.fields import FieldInfo
if TYPE_CHECKING:
from pydantic.fields import FieldInfo
BaseModelVar = TypeVar('BaseModelVar', bound=BaseModel)
def as_query(cls: Type[BaseModel]):
def as_query(cls: type[BaseModelVar]) -> type[BaseModelVar]:
"""
pydantic模型查询参数装饰器将pydantic模型用于接收查询参数
"""
new_parameters = []
for field_name, model_field in cls.model_fields.items():
model_field: FieldInfo # type: ignore
for model_field in cls.model_fields.values():
model_field: FieldInfo
if not model_field.is_required():
new_parameters.append(
inspect.Parameter(
model_field.alias,
inspect.Parameter.POSITIONAL_ONLY,
default=Query(model_field.default),
annotation=model_field.annotation
default=Query(default=model_field.default, description=model_field.description),
annotation=model_field.annotation,
)
)
else:
@@ -29,37 +33,37 @@ def as_query(cls: Type[BaseModel]):
inspect.Parameter(
model_field.alias,
inspect.Parameter.POSITIONAL_ONLY,
default=Query(...),
annotation=model_field.annotation
default=Query(..., description=model_field.description),
annotation=model_field.annotation,
)
)
async def as_query_func(**data):
async def as_query_func(**data) -> type[BaseModelVar]:
return cls(**data)
sig = inspect.signature(as_query_func)
sig = sig.replace(parameters=new_parameters)
as_query_func.__signature__ = sig # type: ignore
setattr(cls, 'as_query', as_query_func)
as_query_func.__signature__ = sig
cls.as_query = as_query_func
return cls
def as_form(cls: Type[BaseModel]):
def as_form(cls: type[BaseModelVar]) -> type[BaseModelVar]:
"""
pydantic模型表单参数装饰器将pydantic模型用于接收表单参数
"""
new_parameters = []
for field_name, model_field in cls.model_fields.items():
model_field: FieldInfo # type: ignore
for model_field in cls.model_fields.values():
model_field: FieldInfo
if not model_field.is_required():
new_parameters.append(
inspect.Parameter(
model_field.alias,
inspect.Parameter.POSITIONAL_ONLY,
default=Form(model_field.default),
annotation=model_field.annotation
default=Form(default=model_field.default, description=model_field.description),
annotation=model_field.annotation,
)
)
else:
@@ -67,16 +71,16 @@ def as_form(cls: Type[BaseModel]):
inspect.Parameter(
model_field.alias,
inspect.Parameter.POSITIONAL_ONLY,
default=Form(...),
annotation=model_field.annotation
default=Form(..., description=model_field.description),
annotation=model_field.annotation,
)
)
async def as_form_func(**data):
async def as_form_func(**data) -> type[BaseModelVar]:
return cls(**data)
sig = inspect.signature(as_form_func)
sig = sig.replace(parameters=new_parameters)
as_form_func.__signature__ = sig # type: ignore
setattr(cls, 'as_form', as_form_func)
as_form_func.__signature__ = sig
cls.as_form = as_form_func
return cls
@@ -0,0 +1,112 @@
from fastapi import Depends, Request, params
from sqlalchemy import ColumnElement, func, or_, select
from common.context import RequestContext
from config.database import Base
from module_admin.entity.do.dept_do import SysDept
from module_admin.entity.do.role_do import SysRoleDept
from utils.dependency_util import DependencyUtil
class GetDataScope:
"""
获取当前用户数据权限对应的查询sql语句
"""
DATA_SCOPE_ALL = '1'
DATA_SCOPE_CUSTOM = '2'
DATA_SCOPE_DEPT = '3'
DATA_SCOPE_DEPT_AND_CHILD = '4'
DATA_SCOPE_SELF = '5'
def __init__(
self,
query_alias: Base,
user_alias: str | None = 'user_id',
dept_alias: str | None = 'dept_id',
) -> None:
"""
获取当前用户数据权限对应的查询sql语句
:param query_alias: 所要查询表对应的sqlalchemy模型类,不可为空
:param user_alias: 用户id字段别名,默认为'user_id'
:param dept_alias: 部门id字段别名,默认为'dept_id'
"""
self.query_alias = query_alias
self.user_alias = user_alias
self.dept_alias = dept_alias
def __call__(self, request: Request) -> ColumnElement:
DependencyUtil.check_exclude_routes(request, err_msg='当前路由不在认证规则内,不可使用GetDataScope依赖项')
current_user = RequestContext.get_current_user()
user_id = current_user.user.user_id
dept_id = current_user.user.dept_id
custom_data_scope_role_id_list = [
item.role_id for item in current_user.user.role if item.data_scope == self.DATA_SCOPE_CUSTOM
]
param_sql_list = []
for role in current_user.user.role:
if current_user.user.admin or role.data_scope == self.DATA_SCOPE_ALL:
param_sql_list = [True]
break
if role.data_scope == self.DATA_SCOPE_CUSTOM:
if len(custom_data_scope_role_id_list) > 1:
param_sql_list.append(
getattr(self.query_alias, self.dept_alias).in_(
select(SysRoleDept.dept_id).where(SysRoleDept.role_id.in_(custom_data_scope_role_id_list))
)
if hasattr(self.query_alias, self.dept_alias)
else False
)
else:
param_sql_list.append(
getattr(self.query_alias, self.dept_alias).in_(
select(SysRoleDept.dept_id).where(SysRoleDept.role_id == role.role_id)
)
if hasattr(self.query_alias, self.dept_alias)
else False
)
elif role.data_scope == self.DATA_SCOPE_DEPT:
param_sql_list.append(
getattr(self.query_alias, self.dept_alias) == dept_id
if hasattr(self.query_alias, self.dept_alias)
else False
)
elif role.data_scope == self.DATA_SCOPE_DEPT_AND_CHILD:
param_sql_list.append(
getattr(self.query_alias, self.dept_alias).in_(
select(SysDept.dept_id).where(
or_(SysDept.dept_id == dept_id, func.find_in_set(dept_id, SysDept.ancestors))
)
)
if hasattr(self.query_alias, self.dept_alias)
else False
)
elif role.data_scope == self.DATA_SCOPE_SELF:
param_sql_list.append(
getattr(self.query_alias, self.user_alias) == user_id
if hasattr(self.query_alias, self.user_alias)
else False
)
else:
param_sql_list.append(False)
param_sql_list = list(dict.fromkeys(param_sql_list))
param_sql = or_(*param_sql_list)
return param_sql
def DataScopeDependency( # noqa: N802
query_alias: Base,
user_alias: str | None = 'user_id',
dept_alias: str | None = 'dept_id',
) -> params.Depends:
"""
当前用户数据权限依赖
:param query_alias: 所要查询表对应的sqlalchemy模型类,不可为空
:param user_alias: 用户id字段别名,默认为'user_id'
:param dept_alias: 部门id字段别名,默认为'dept_id'
:return: 当前用户数据权限依赖
"""
return Depends(GetDataScope(query_alias, user_alias, dept_alias))
@@ -0,0 +1,12 @@
from fastapi import Depends, params
from config.get_db import get_db
def DBSessionDependency() -> params.Depends: # noqa: N802
"""
数据库会话依赖
:return: 数据库会话依赖
"""
return Depends(get_db)
@@ -0,0 +1,94 @@
from fastapi import Depends, Request, params
from common.context import RequestContext
from exceptions.exception import PermissionException
from utils.dependency_util import DependencyUtil
class CheckUserInterfaceAuth:
"""
校验当前用户是否具有相应的接口权限
"""
def __init__(self, perm: str | list, is_strict: bool = False) -> None:
"""
校验当前用户是否具有相应的接口权限
:param perm: 权限标识
:param is_strict: 当传入的权限标识是list类型时,是否开启严格模式,开启表示会校验列表中的每一个权限标识,所有的校验结果都需要为True才会通过
"""
self.perm = perm
self.is_strict = is_strict
def __call__(self, request: Request) -> bool:
DependencyUtil.check_exclude_routes(
request, err_msg='当前路由不在认证规则内,不可使用CheckUserInterfaceAuth依赖项'
)
current_user = RequestContext.get_current_user()
user_auth_list = current_user.permissions
if '*:*:*' in user_auth_list:
return True
if isinstance(self.perm, str) and self.perm in user_auth_list:
return True
if isinstance(self.perm, list):
if self.is_strict:
if all(perm_str in user_auth_list for perm_str in self.perm):
return True
elif any(perm_str in user_auth_list for perm_str in self.perm):
return True
raise PermissionException(data='', message='该用户无此接口权限')
class CheckRoleInterfaceAuth:
"""
根据角色校验当前用户是否具有相应的接口权限
"""
def __init__(self, role_key: str | list, is_strict: bool = False) -> None:
"""
根据角色校验当前用户是否具有相应的接口权限
:param role_key: 角色标识
:param is_strict: 当传入的角色标识是list类型时,是否开启严格模式,开启表示会校验列表中的每一个角色标识,所有的校验结果都需要为True才会通过
"""
self.role_key = role_key
self.is_strict = is_strict
def __call__(self, request: Request) -> bool:
DependencyUtil.check_exclude_routes(
request, err_msg='当前路由不在认证规则内,不可使用CheckRoleInterfaceAuth依赖项'
)
current_user = RequestContext.get_current_user()
user_role_list = current_user.user.role
user_role_key_list = [role.role_key for role in user_role_list]
if isinstance(self.role_key, str) and self.role_key in user_role_key_list:
return True
if isinstance(self.role_key, list):
if self.is_strict:
if all(role_key_str in user_role_key_list for role_key_str in self.role_key):
return True
elif any(role_key_str in user_role_key_list for role_key_str in self.role_key):
return True
raise PermissionException(data='', message='该用户无此接口权限')
def UserInterfaceAuthDependency(perm: str | list, is_strict: bool = False) -> params.Depends: # noqa: N802
"""
根据权限标识校验当前用户接口权限依赖
:param perm: 权限标识
:param is_strict: 当传入的权限标识是list类型时,是否开启严格模式,开启表示会校验列表中的每一个权限标识,所有的校验结果都需要为True才会通过
:return: 根据权限标识校验当前用户接口权限依赖
"""
return Depends(CheckUserInterfaceAuth(perm, is_strict))
def RoleInterfaceAuthDependency(role_key: str | list, is_strict: bool = False) -> params.Depends: # noqa: N802
"""
根据角色校验当前用户接口权限依赖
:param role_key: 角色标识
:param is_strict: 当传入的角色标识是list类型时,是否开启严格模式,开启表示会校验列表中的每一个角色标识,所有的校验结果都需要为True才会通过
:return: 根据角色校验当前用户接口权限依赖
"""
return Depends(CheckRoleInterfaceAuth(role_key, is_strict))
@@ -0,0 +1,146 @@
import re
from typing import Literal, TypedDict
from fastapi import Depends, Request, params
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.ext.asyncio import AsyncSession
from common.context import RequestContext
from config.env import AppConfig
from config.get_db import get_db
from exceptions.exception import AuthException
from module_admin.entity.vo.user_vo import CurrentUserModel
from module_admin.service.login_service import LoginService
# 定义排除路由的字典结构
class ExcludeRoute(TypedDict, total=False):
"""
排除路由的字典结构
:param path: 路由路径(必填)
:param methods: HTTP方法列表,空列表表示所有方法(可选,默认为[])
:param ignore_paths: 需要忽略的特定路径列表,即使匹配通配符也不排除(可选,默认为[])
"""
path: str
methods: list[Literal['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']]
ignore_paths: list[str]
# 创建OAuth2PasswordBearer对象
oauth2_scheme = OAuth2PasswordBearer(tokenUrl='/login')
class PreAuth:
"""
登录认证前置校验依赖类
"""
def __init__(self, exclude_routes: list[ExcludeRoute] | None = None) -> None:
"""
初始化登录认证前置校验依赖
:param exclude_routes: 需要排除的路由列表,格式为:
[{'path': '/path1', 'methods': ['GET', 'POST']}, {'path': '/path2/{param}', 'methods': ['GET']}]
methods 可以是字符串或列表,空列表表示所有方法
"""
self.exclude_routes = exclude_routes or []
# 编译排除路径为正则表达式模式,并存储方法信息
self.exclude_patterns = []
for route in self.exclude_routes:
# 使用TypedDict,确保路由字典包含path字段
path = route.get('path', '')
methods = route.get('methods', [])
ignore_paths = route.get('ignore_paths', [])
# 编译路径为正则表达式
pattern = self._compile_path_pattern(path)
# 存储编译后的模式和方法信息
self.exclude_patterns.append(
{
'pattern': pattern,
'methods': [method.upper() for method in methods],
'original_path': path,
'ignore_paths': ignore_paths,
}
)
def _compile_path_pattern(self, path: str) -> re.Pattern:
"""
将FastAPI路径转换为正则表达式模式
:param path: FastAPI路径(如 /configKey/{config_key}
:return: 编译后的正则表达式模式
"""
# 将FastAPI路径参数转换为正则表达式
# 例如:/configKey/{config_key} -> /configKey/[^/]+
pattern_str = re.sub(r'\{[^}]+\}', r'[^/]+', path)
# 添加开始和结束锚点,确保精确匹配
return re.compile(f'^{pattern_str}$')
async def __call__(self, request: Request, db: AsyncSession = Depends(get_db)) -> CurrentUserModel | None:
"""
执行登录认证校验
:param request: 当前请求对象
:param db: 数据库会话
:return: 当前用户信息
"""
# 获取当前请求路径和方法
path = request.url.path
method = request.method.upper()
# 从配置中获取APP_ROOT_PATH
app_root_path = AppConfig.app_root_path
# 去掉APP_ROOT_PATH前缀
if app_root_path and path.startswith(app_root_path):
path = path[len(app_root_path) :]
# 设置上下文变量
RequestContext.set_current_exclude_patterns(self.exclude_patterns)
# 检查路径和方法是否匹配排除模式
for item in self.exclude_patterns:
pattern = item['pattern']
exclude_methods = item['methods']
ignore_paths = item['ignore_paths']
# 检查当前路径是否在忽略列表中
if path in ignore_paths:
continue
# 检查路径是否匹配,并且methods为空列表(匹配所有方法)或者当前方法在允许列表中
if pattern.match(path) and (not exclude_methods or method in exclude_methods):
# 跳过认证
return None
# 否则执行正常认证
token = request.headers.get('Authorization')
if not token:
raise AuthException(data='', message='用户未登录,请先完成登录')
current_user = await LoginService.get_current_user(request, token, db)
return current_user
def PreAuthDependency(exclude_routes: list[ExcludeRoute] | None = None) -> params.Depends: # noqa: N802
"""
登录认证前置校验依赖
:param exclude_routes: 需要排除的路由列表,格式为:
[{'path': '/path1', 'methods': ['GET', 'POST']}, {'path': '/path2/{param}', 'methods': ['GET']}]
methods 可以是字符串或列表,空列表表示所有方法
:return: 登录认证前置校验依赖
"""
return Depends(PreAuth(exclude_routes))
def CurrentUserDependency() -> params.Depends: # noqa: N802
"""
当前登录用户信息依赖
:return: 当前登录用户信息依赖
"""
return Depends(LoginService.get_current_user)
+495
View File
@@ -0,0 +1,495 @@
from config.env import DataBaseConfig
class CommonConstant:
"""
常用常量
PASSWORD_ERROR_COUNT: 密码错误次数
WWW: www主域
HTTP: http请求
HTTPS: https请求
LOOKUP_RMI: RMI远程方法调用
LOOKUP_LDAP: LDAP远程方法调用
LOOKUP_LDAPS: LDAPS远程方法调用
YES: 是否为系统默认(是)
NO: 是否为系统默认(否)
DEPT_NORMAL: 部门正常状态
DEPT_DISABLE: 部门停用状态
UNIQUE: 校验是否唯一的返回标识(是)
NOT_UNIQUE: 校验是否唯一的返回标识(否)
"""
PASSWORD_ERROR_COUNT = 5
WWW = 'www.'
HTTP = 'http://'
HTTPS = 'https://'
LOOKUP_RMI = 'rmi:'
LOOKUP_LDAP = 'ldap:'
LOOKUP_LDAPS = 'ldaps:'
YES = 'Y'
NO = 'N'
DEPT_NORMAL = '0'
DEPT_DISABLE = '1'
UNIQUE = True
NOT_UNIQUE = False
class HttpStatusConstant:
"""
返回状态码
SUCCESS: 操作成功
CREATED: 对象创建成功
ACCEPTED: 请求已经被接受
NO_CONTENT: 操作已经执行成功,但是没有返回数据
MOVED_PERM: 资源已被移除
SEE_OTHER: 重定向
NOT_MODIFIED: 资源没有被修改
BAD_REQUEST: 参数列表错误(缺少,格式不匹配)
UNAUTHORIZED: 未授权
FORBIDDEN: 访问受限,授权过期
NOT_FOUND: 资源,服务未找到
BAD_METHOD: 不允许的http方法
CONFLICT: 资源冲突,或者资源被锁
UNSUPPORTED_TYPE: 不支持的数据,媒体类型
ERROR: 系统内部错误
NOT_IMPLEMENTED: 接口未实现
WARN: 系统警告消息
"""
SUCCESS = 200
CREATED = 201
ACCEPTED = 202
NO_CONTENT = 204
MOVED_PERM = 301
SEE_OTHER = 303
NOT_MODIFIED = 304
BAD_REQUEST = 400
UNAUTHORIZED = 401
FORBIDDEN = 403
NOT_FOUND = 404
BAD_METHOD = 405
CONFLICT = 409
UNSUPPORTED_TYPE = 415
ERROR = 500
NOT_IMPLEMENTED = 501
WARN = 601
class JobConstant:
"""
定时任务常量
JOB_ERROR_LIST: 定时任务禁止调用模块及违规字符串列表
JOB_WHITE_LIST: 定时任务允许调用模块列表
"""
JOB_ERROR_LIST = [
'app',
'config',
'exceptions',
'import ',
'middlewares',
'module_admin',
'open(',
'os.',
'server',
'sub_applications',
'subprocess.',
'sys.',
'utils',
'while ',
'__import__',
'"',
"'",
',',
'?',
':',
';',
'/',
'|',
'+',
'-',
'=',
'~',
'!',
'#',
'$',
'%',
'^',
'&',
'*',
'<',
'>',
'(',
')',
'[',
']',
'{',
'}',
' ',
]
JOB_WHITE_LIST = ['module_task']
class LockConstant:
"""
分布式锁常量
"""
APP_STARTUP_LOCK_KEY = 'app:startup:lock'
LOCK_EXPIRE_SECONDS = 60
LOCK_RENEWAL_INTERVAL = 20
class MenuConstant:
"""
菜单常量
TYPE_DIR: 菜单类型(目录)
TYPE_MENU: 菜单类型(菜单)
TYPE_BUTTON: 菜单类型(按钮)
YES_FRAME: 是否菜单外链(是)
NO_FRAME: 是否菜单外链(否)
LAYOUT: Layout组件标识
PARENT_VIEW: ParentView组件标识
INNER_LINK: InnerLink组件标识
"""
TYPE_DIR = 'M'
TYPE_MENU = 'C'
TYPE_BUTTON = 'F'
YES_FRAME = 0
NO_FRAME = 1
LAYOUT = 'Layout'
PARENT_VIEW = 'ParentView'
INNER_LINK = 'InnerLink'
class GenConstant:
"""
代码生成常量
TPL_CRUD: 单表(增删改查
TPL_TREE: 树表(增删改查)
TPL_SUB: 主子表(增删改查)
TREE_CODE: 树编码字段
TREE_PARENT_CODE: 树父编码字段
TREE_NAME: 树名称字段
PARENT_MENU_ID: 上级菜单ID字段
PARENT_MENU_NAME: 上级菜单名称字段
COLUMNTYPE_STR: 数据库字符串类型
COLUMNTYPE_TEXT: 数据库文本类型
COLUMNTYPE_TIME: 数据库时间类型
COLUMNTYPE_GEOMETRY: 数据库字空间类型
COLUMNTYPE_NUMBER: 数据库数字类型
COLUMNNAME_NOT_EDIT: 页面不需要编辑字段
COLUMNNAME_NOT_LIST: 页面不需要显示的列表字段
COLUMNNAME_NOT_QUERY: 页面不需要查询字段
BASE_ENTITY: Entity基类字段
TREE_ENTITY: Tree基类字段
HTML_INPUT: 文本框
HTML_TEXTAREA: 文本域
HTML_SELECT: 下拉框
HTML_RADIO: 单选框
HTML_CHECKBOX: 复选框
HTML_DATETIME: 日期控件
HTML_IMAGE_UPLOAD: 图片上传控件
HTML_FILE_UPLOAD: 文件上传控件
HTML_EDITOR: 富文本控件
TYPE_DECIMAL: 高精度计算类型
TYPE_DATE: 时间类型
QUERY_LIKE: 模糊查询
QUERY_EQ: 相等查询
REQUIRE: 需要
DB_TO_SQLALCHEMY_TYPE_MAPPING: 数据库类型与sqlalchemy类型映射
DB_TO_PYTHON_TYPE_MAPPING: 数据库类型与python类型映射
"""
TPL_CRUD = 'crud'
TPL_TREE = 'tree'
TPL_SUB = 'sub'
TREE_CODE = 'treeCode'
TREE_PARENT_CODE = 'treeParentCode'
TREE_NAME = 'treeName'
PARENT_MENU_ID = 'parentMenuId'
PARENT_MENU_NAME = 'parentMenuName'
COLUMNTYPE_STR = (
['character varying', 'varchar', 'character', 'char']
if DataBaseConfig.db_type == 'postgresql'
else ['char', 'varchar', 'nvarchar', 'varchar2']
)
COLUMNTYPE_TEXT = (
['text', 'citext'] if DataBaseConfig.db_type == 'postgresql' else ['tinytext', 'text', 'mediumtext', 'longtext']
)
COLUMNTYPE_TIME = (
[
'date',
'time',
'time with time zone',
'time without time zone',
'timestamp',
'timestamp with time zone',
'timestamp without time zone',
'interval',
]
if DataBaseConfig.db_type == 'postgresql'
else ['datetime', 'time', 'date', 'timestamp']
)
COLUMNTYPE_GEOMETRY = (
['point', 'line', 'lseg', 'box', 'path', 'polygon', 'circle']
if DataBaseConfig.db_type == 'postgresql'
else [
'geometry',
'point',
'linestring',
'polygon',
'multipoint',
'multilinestring',
'multipolygon',
'geometrycollection',
]
)
COLUMNTYPE_NUMBER = [
'tinyint',
'smallint',
'mediumint',
'int',
'number',
'integer',
'bit',
'bigint',
'float',
'double',
'decimal',
]
COLUMNNAME_NOT_ADD_SHOW = ['create_by', 'create_time']
COLUMNNAME_NOT_EDIT_SHOW = ['update_by', 'update_time']
COLUMNNAME_NOT_EDIT = ['id', 'create_by', 'create_time', 'del_flag']
COLUMNNAME_NOT_LIST = ['id', 'create_by', 'create_time', 'del_flag', 'update_by', 'update_time']
COLUMNNAME_NOT_QUERY = ['id', 'create_by', 'create_time', 'del_flag', 'update_by', 'update_time', 'remark']
BASE_ENTITY = ['createBy', 'createTime', 'updateBy', 'updateTime', 'remark']
TREE_ENTITY = ['parentName', 'parentId', 'orderNum', 'ancestors', 'children']
HTML_INPUT = 'input'
HTML_TEXTAREA = 'textarea'
HTML_SELECT = 'select'
HTML_RADIO = 'radio'
HTML_CHECKBOX = 'checkbox'
HTML_DATETIME = 'datetime'
HTML_IMAGE_UPLOAD = 'imageUpload'
HTML_FILE_UPLOAD = 'fileUpload'
HTML_EDITOR = 'editor'
TYPE_DECIMAL = 'Decimal'
TYPE_DATE = ['date', 'time', 'datetime']
QUERY_LIKE = 'LIKE'
QUERY_EQ = 'EQ'
REQUIRE = '1'
DB_TO_SQLALCHEMY_TYPE_MAPPING = (
{
'boolean': 'Boolean',
'smallint': 'SmallInteger',
'integer': 'Integer',
'bigint': 'BigInteger',
'real': 'Float',
'double precision': 'Float',
'numeric': 'Numeric',
'character varying': 'String',
'character': 'String',
'text': 'Text',
'bytea': 'LargeBinary',
'date': 'Date',
'time': 'Time',
'time with time zone': 'Time',
'time without time zone': 'Time',
'timestamp': 'DateTime',
'timestamp with time zone': 'DateTime',
'timestamp without time zone': 'DateTime',
'interval': 'Interval',
'json': 'JSON',
'jsonb': 'JSONB',
'uuid': 'Uuid',
'inet': 'INET',
'cidr': 'CIDR',
'macaddr': 'MACADDR',
'point': 'Geometry',
'line': 'Geometry',
'lseg': 'Geometry',
'box': 'Geometry',
'path': 'Geometry',
'polygon': 'Geometry',
'circle': 'Geometry',
'bit': 'Bit',
'bit varying': 'Bit',
'tsvector': 'TSVECTOR',
'tsquery': 'TSQUERY',
'xml': 'String',
'array': 'ARRAY',
'composite': 'JSON',
'enum': 'Enum',
'range': 'Range',
'money': 'Numeric',
'pg_lsn': 'BigInteger',
'txid_snapshot': 'String',
'oid': 'BigInteger',
'regproc': 'String',
'regclass': 'String',
'regtype': 'String',
'regrole': 'String',
'regnamespace': 'String',
'int2vector': 'ARRAY',
'oidvector': 'ARRAY',
'pg_node_tree': 'Text',
}
if DataBaseConfig.db_type == 'postgresql'
else {
# 数值类型
'TINYINT': 'SmallInteger',
'SMALLINT': 'SmallInteger',
'MEDIUMINT': 'Integer',
'INT': 'Integer',
'INTEGER': 'Integer',
'BIGINT': 'BigInteger',
'FLOAT': 'Float',
'DOUBLE': 'Float',
'DECIMAL': 'DECIMAL',
'BIT': 'Integer',
# 日期和时间类型
'DATE': 'Date',
'TIME': 'Time',
'DATETIME': 'DateTime',
'TIMESTAMP': 'TIMESTAMP',
'YEAR': 'Integer',
# 字符串类型
'CHAR': 'CHAR',
'VARCHAR': 'String',
'TINYTEXT': 'Text',
'TEXT': 'Text',
'MEDIUMTEXT': 'Text',
'LONGTEXT': 'Text',
'BINARY': 'BINARY',
'VARBINARY': 'VARBINARY',
'TINYBLOB': 'LargeBinary',
'BLOB': 'LargeBinary',
'MEDIUMBLOB': 'LargeBinary',
'LONGBLOB': 'LargeBinary',
# 枚举和集合类型
'ENUM': 'Enum',
'SET': 'String',
# JSON 类型
'JSON': 'JSON',
# 空间数据类型(需要扩展支持,如 GeoAlchemy2
'GEOMETRY': 'Geometry', # 需要安装 geoalchemy2
'POINT': 'Geometry',
'LINESTRING': 'Geometry',
'POLYGON': 'Geometry',
'MULTIPOINT': 'Geometry',
'MULTILINESTRING': 'Geometry',
'MULTIPOLYGON': 'Geometry',
'GEOMETRYCOLLECTION': 'Geometry',
}
)
DB_TO_PYTHON_TYPE_MAPPING = (
{
'boolean': 'bool',
'smallint': 'int',
'integer': 'int',
'bigint': 'int',
'real': 'float',
'double precision': 'float',
'numeric': 'Decimal',
'character varying': 'str',
'character': 'str',
'text': 'str',
'bytea': 'bytes',
'date': 'date',
'time': 'time',
'time with time zone': 'time',
'time without time zone': 'time',
'timestamp': 'datetime',
'timestamp with time zone': 'datetime',
'timestamp without time zone': 'datetime',
'interval': 'timedelta',
'json': 'dict',
'jsonb': 'dict',
'uuid': 'str',
'inet': 'str',
'cidr': 'str',
'macaddr': 'str',
'point': 'list',
'line': 'list',
'lseg': 'list',
'box': 'list',
'path': 'list',
'polygon': 'list',
'circle': 'list',
'bit': 'int',
'bit varying': 'int',
'tsvector': 'str',
'tsquery': 'str',
'xml': 'str',
'array': 'list',
'composite': 'dict',
'enum': 'str',
'range': 'list',
'money': 'Decimal',
'pg_lsn': 'int',
'txid_snapshot': 'str',
'oid': 'int',
'regproc': 'str',
'regclass': 'str',
'regtype': 'str',
'regrole': 'str',
'regnamespace': 'str',
'int2vector': 'list',
'oidvector': 'list',
'pg_node_tree': 'str',
}
if DataBaseConfig.db_type == 'postgresql'
else {
# 数值类型
'TINYINT': 'int',
'SMALLINT': 'int',
'MEDIUMINT': 'int',
'INT': 'int',
'INTEGER': 'int',
'BIGINT': 'int',
'FLOAT': 'float',
'DOUBLE': 'float',
'DECIMAL': 'Decimal',
'BIT': 'int',
# 日期和时间类型
'DATE': 'date',
'TIME': 'time',
'DATETIME': 'datetime',
'TIMESTAMP': 'datetime',
'YEAR': 'int',
# 字符串类型
'CHAR': 'str',
'VARCHAR': 'str',
'TINYTEXT': 'str',
'TEXT': 'str',
'MEDIUMTEXT': 'str',
'LONGTEXT': 'str',
'BINARY': 'bytes',
'VARBINARY': 'bytes',
'TINYBLOB': 'bytes',
'BLOB': 'bytes',
'MEDIUMBLOB': 'bytes',
'LONGBLOB': 'bytes',
# 枚举和集合类型
'ENUM': 'str',
'SET': 'str',
# JSON 类型
'JSON': 'dict',
# 空间数据类型(通常需要特殊处理)
'GEOMETRY': 'bytes',
'POINT': 'bytes',
'LINESTRING': 'bytes',
'POLYGON': 'bytes',
'MULTIPOINT': 'bytes',
'MULTILINESTRING': 'bytes',
'MULTIPOLYGON': 'bytes',
'GEOMETRYCOLLECTION': 'bytes',
}
)

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