初始提交
10
.idea/.gitignore
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Ignored default folder with query files
|
||||
/queries/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
10
.idea/ShiErFeng.iml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/venv" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Python 3.10 (ShiErFeng)" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
25
.idea/inspectionProfiles/Project_Default.xml
Normal file
@ -0,0 +1,25 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||
<option name="ignoredErrors">
|
||||
<list>
|
||||
<option value="N806" />
|
||||
<option value="N803" />
|
||||
<option value="N802" />
|
||||
<option value="N801" />
|
||||
</list>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PyUnresolvedReferencesInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="ignoredIdentifiers">
|
||||
<list>
|
||||
<option value="list.to_numpy" />
|
||||
<option value="str.__or__" />
|
||||
<option value="str.like" />
|
||||
<option value="nacos.client" />
|
||||
</list>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
||||
6
.idea/inspectionProfiles/profiles_settings.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
||||
7
.idea/misc.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="Python 3.11 (TavernProgram)" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (ShiErFeng)" project-jdk-type="Python SDK" />
|
||||
</project>
|
||||
8
.idea/modules.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/ShiErFeng.iml" filepath="$PROJECT_DIR$/.idea/ShiErFeng.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/vcs.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
547
CHANGELOG.md
Normal file
@ -0,0 +1,547 @@
|
||||
# 更新日志
|
||||
|
||||
## ShiErFeng v1.9.0
|
||||
|
||||
### 项目依赖
|
||||
|
||||
前后端依赖均有升级,请升级依赖或重新创建环境。
|
||||
|
||||
### 新增功能
|
||||
|
||||
1.新增AI管理模块 (#69)。
|
||||
2.新增移动端模块 (#73)。
|
||||
3.新增多worker运行支持 (#76)。
|
||||
4.应用新增演示模式 (#78)。
|
||||
|
||||
### BUG修复
|
||||
|
||||
1.修复代码生成controller模板删除接口query_db参数异常的问题 (#63)。
|
||||
2.修复登录接口response_model声明错误 (#71)。
|
||||
3.修复无法直接通过后端地址访问API文档的问题 (#74)。
|
||||
4.修复create_app重复执行的问题 (#84)。
|
||||
|
||||
### 代码重构
|
||||
|
||||
1.移除对python3.9的支持 (#67)。
|
||||
|
||||
### 代码优化
|
||||
|
||||
1.优化alembic处理表模型逻辑,避免无关表影响 (#68)。
|
||||
2.优化代码生成后端模板 (#72)。
|
||||
3.自动注册路由出错时抛出异常以便于调试 (#79)。
|
||||
4.优化部分页面字段tooltip说明 (#80)。
|
||||
5.优化项目启动速度 (#82)。
|
||||
6.优化暗黑模式切换效果 (#83)。
|
||||
7.优化热重载模式或单worker下scheduler的任务状态同步机制 (#85)。
|
||||
8.优化防重提交间隔时间可自定义 (#87)。
|
||||
9.优化验证码计算结果为非负数 (#88)。
|
||||
10.优化ci测试稳定性 (#90)。
|
||||
|
||||
## ShiErFeng v1.8.1
|
||||
|
||||
### 新增功能
|
||||
|
||||
1.新增E2E测试 (#57)。
|
||||
|
||||
### BUG修复
|
||||
|
||||
1.修复DictTag组件渲染异常的问题 (#59)。
|
||||
|
||||
### 代码优化
|
||||
|
||||
1.优化数据权限依赖 (#55)。
|
||||
2.动态导入定时任务函数,移除eval (#56)。
|
||||
3.优化pg版本的docker compose配置文件 (#61)。
|
||||
|
||||
## ShiErFeng v1.8.0
|
||||
|
||||
### 项目依赖
|
||||
|
||||
#### 后端
|
||||
|
||||
1.后端依赖升级到最新版本,请升级依赖或重新创建环境。
|
||||
|
||||
### 新增功能
|
||||
|
||||
1.新增请求上下文管理类。
|
||||
2.新增`PreAuthDependency`、`CurrentUserDependency`、`DataScopeDependency`、`DBSessionDependency`、`UserInterfaceAuthDependency`和`RoleInterfaceAuthDependency`依赖函数。
|
||||
3.新增上下文清理中间件。
|
||||
4.新增公共vo模块。
|
||||
5.新增配置文档静态资源方法。
|
||||
6.新增自动注册路由功能。
|
||||
7.新增docker compose部署方式。
|
||||
8.菜单导航设置支持纯顶部。
|
||||
|
||||
### BUG修复
|
||||
|
||||
1.修复单账号登录模式下强退功能失效的问题 #52。
|
||||
2.确保ApschedulerJobs字段类型与apscheduler默认创建的表字段类型一致 #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.默认固定头部。
|
||||
|
||||
## ShiErFeng v1.7.1
|
||||
|
||||
### 项目依赖
|
||||
|
||||
1.后端依赖移除passlib,直接使用bcrypt。
|
||||
|
||||
### BUG修复
|
||||
|
||||
1.修复代码生成controller模板编辑接口异常生成字段的问题。
|
||||
2.移除passlib直接使用bcrypt修复密码校验异常的问题 #48 #49。
|
||||
|
||||
### 代码优化
|
||||
|
||||
1.代码生成do模板补充表描述。
|
||||
|
||||
## ShiErFeng 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。
|
||||
2.修复上传组件被多次引用拖动仅对第一个有效的问题。
|
||||
|
||||
### 代码优化
|
||||
|
||||
1.优化接口耗时计算。
|
||||
2.优化启动信息显示。
|
||||
3.优化前端处理路由函数代码。
|
||||
4.登录页和注册页表头使用VITE_APP_BASE_API配置值。
|
||||
5.优化角色禁用不允许分配。
|
||||
6.优化富文本控制台警告异常。
|
||||
7.优化checkbox废弃API。
|
||||
8.优化导航栏显示昵称&设置。
|
||||
|
||||
### 代码重构
|
||||
|
||||
1.重构IP归属区域查询为异步调用。
|
||||
2.调整do与sql使其相互适配以支持alembic。
|
||||
3.富文本复制粘贴图片上传至url。
|
||||
|
||||
## ShiErFeng v1.6.2
|
||||
|
||||
### 新增功能
|
||||
|
||||
1.文件上传组件新增disabled属性。
|
||||
2.文件上传组件新增类型。
|
||||
|
||||
### BUG修复
|
||||
|
||||
1.修复日志管理时间查询报错 #27。
|
||||
2.修复定时任务状态暂停时执行单次任务会触发cron表达式的问题 #31。
|
||||
3.修复修改字典类型时获取dict_code异常的问题。
|
||||
4.修复修改字典类型时字典数据更新时间异常的问题。
|
||||
5.修复代码生成模板时间查询问题 #28。
|
||||
6.修复用户导出缺失部门名称的问题。
|
||||
|
||||
### 代码优化
|
||||
|
||||
1.优化代码生成新增和编辑字段显示和渲染。
|
||||
2.pagination更换成flex布局。
|
||||
3.优化代码生成vue模板 #23。
|
||||
|
||||
## ShiErFeng 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的问题。
|
||||
|
||||
## ShiErFeng 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。
|
||||
5.修复个人中心特殊字符密码修改失败问题。
|
||||
|
||||
### 代码优化
|
||||
|
||||
1.优化导出方法。
|
||||
2.参数键值更换为多行文本。
|
||||
3.优化日志中操作方法显示。
|
||||
4.优化日志装饰器获取核心参数的方式。
|
||||
5.用户管理过滤掉已禁用部门。
|
||||
6.优化TopNav内链菜单点击没有高亮。
|
||||
7.ResponseUtil补充完整参数。
|
||||
|
||||
## ShiErFeng v1.5.1
|
||||
|
||||
### 新增功能
|
||||
|
||||
1.定时任务新增支持调用异步函数。
|
||||
|
||||
### 代码优化
|
||||
|
||||
1.优化字典数组条件判断。
|
||||
2.校检文件名是否包含特殊字符。
|
||||
3.移除已弃用的log_decorator装饰器。
|
||||
|
||||
## ShiErFeng v1.5.0
|
||||
|
||||
### 新增功能
|
||||
|
||||
1.新增对PostgreSQL数据库的支持。
|
||||
|
||||
### BUG修复
|
||||
|
||||
1.修复DictTag组件控制台抛异常的问题 #IAYSVZ。
|
||||
2.修复登录日志导出文件名称错误的问题。
|
||||
|
||||
### 代码回滚
|
||||
|
||||
1.因fastapi查询参数模型底层存在bug,回滚查询参数模型声明方式为as_query。
|
||||
|
||||
### 代码优化
|
||||
|
||||
1.优化CamelCaseUtil和SnakeCaseUtil以兼容更多转换场景。
|
||||
2.优化列表查询排序。
|
||||
3.优化参数设置页面。
|
||||
4.优化上传图片带域名时不增加前缀。
|
||||
|
||||
## ShiErFeng 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.优化前端登录请求方法。
|
||||
|
||||
## ShiErFeng v1.3.3
|
||||
|
||||
### 项目依赖
|
||||
|
||||
#### 后端
|
||||
|
||||
1.更新pydantic-validation-decorator版本为0.1.4,修复了一些底层bug。
|
||||
|
||||
### BUG修复
|
||||
|
||||
1.修复在线用户模块条件查询无效的问题。
|
||||
|
||||
### 代码优化
|
||||
|
||||
1.优化在线用户模块前后端字段描述一致。
|
||||
2.日志装饰器异常处理增加logger打印日志。
|
||||
|
||||
## ShiErFeng v1.3.2
|
||||
|
||||
### 新增功能
|
||||
|
||||
1.新增gzip压缩中间件。
|
||||
|
||||
### BUG修复
|
||||
|
||||
1.修复分页函数计算has_next错误的问题 #10。
|
||||
2.修复定时任务监听函数中事件没有job_id报错的问题。
|
||||
|
||||
### 代码优化
|
||||
|
||||
1.优化添加中间件函数注释。
|
||||
|
||||
## ShiErFeng v1.3.1
|
||||
|
||||
### BUG修复
|
||||
|
||||
1.修复1.3.0版本采用新的异常处理机制后日志装饰器无法记录异常日志的问题。
|
||||
|
||||
### 代码优化
|
||||
|
||||
1.补充定时任务违规字符串。
|
||||
|
||||
## ShiErFeng 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文档字段描述显示。
|
||||
|
||||
## ShiErFeng v1.2.2
|
||||
|
||||
### BUG修复
|
||||
|
||||
1.修复删除定时任务时未移除调度中任务的问题。
|
||||
2.修复菜单生成路由时组件条件判断错误的问题。
|
||||
|
||||
## ShiErFeng v1.2.1
|
||||
|
||||
### BUG修复
|
||||
|
||||
1.修复各模块新增数据时创建时间记录异常的问题。
|
||||
2.修复菜单挂载到根目录时路由加载异常等一系列相关问题。
|
||||
|
||||
### 代码及性能优化
|
||||
|
||||
1.修改代理localhost为127.0.0.1以适配部分设备解析localhost异常的问题。
|
||||
|
||||
## ShiErFeng 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.修复清空定时任务日志异常的问题。
|
||||
|
||||
## ShiErFeng v1.1.3
|
||||
|
||||
### 新增功能
|
||||
|
||||
1.用户密码新增非法字符验证。
|
||||
|
||||
### BUG修复
|
||||
|
||||
1.修复通知公告列表查询前后端字段不一致的问题。
|
||||
2.修复个人中心修改基本资料后端异常的问题。
|
||||
|
||||
## ShiErFeng v1.1.2
|
||||
|
||||
### 新增功能
|
||||
|
||||
1.配置文件新增数据库连接池相关配置。
|
||||
|
||||
### BUG修复
|
||||
|
||||
1.修复个人中心修改密码后端异常的问题。
|
||||
|
||||
### 代码及性能优化
|
||||
|
||||
1.使用@lru_cache缓存ip归属区域查询结果,避免重复调用ip归属区域查询接口以优化性能。
|
||||
|
||||
## ShiErFeng v1.1.1
|
||||
|
||||
### BUG修复
|
||||
|
||||
1.修复编辑定时任务时更新的信息未同步至scheduler的问题 #I9EK56。
|
||||
2.修复编辑角色数据权限时后端异常的问题 #I9ENQN。
|
||||
3.修复菜单配置路由参数不生效的问题。
|
||||
4.修复获取路由信息时菜单排序不生效的问题。
|
||||
5.修复添加菜单时是否外链和是否缓存回显异常的问题。
|
||||
|
||||
## ShiErFeng v1.1.0
|
||||
|
||||
### 新增功能
|
||||
|
||||
1.后端配置文件新增sqlalchemy日志开关配置。
|
||||
2.后端配置文件新增IP归属区域查询开关配置。
|
||||
3.后端配置文件新增账号同时登录开关配置。
|
||||
|
||||
### BUG修复
|
||||
|
||||
1.修复token本身过期时退出登录接口异常的问题 #I9CBWT。
|
||||
2.修复系统版本号或浏览器版本号无法获取时登录异常的问题 #I9CYNM。
|
||||
|
||||
## ShiErFeng v1.0.3
|
||||
|
||||
### 新增功能
|
||||
|
||||
1.账号密码登录新增IP黑名单校验。
|
||||
|
||||
### BUG修复
|
||||
|
||||
1.修复外链菜单无法打开的问题 #I95KBK。
|
||||
2.修复添加和编辑菜单页面中是否缓存和是否外链字段回显异常的问题 #I95KBK。
|
||||
|
||||
## ShiErFeng v1.0.2
|
||||
|
||||
### 新增功能
|
||||
|
||||
1.用户接口权限校验增加列表接收参数,实现同一接口支持多个权限标识校验。
|
||||
2.新增按角色校验接口权限依赖
|
||||
|
||||
### BUG修复
|
||||
|
||||
1.修复用户管理和部门管理模块数据权限异常的问题。
|
||||
|
||||
### 代码及性能优化
|
||||
|
||||
1.调整参数设置、部门管理、字典管理、定时任务、日志管理、角色管理、菜单管理模块部分接口权限标识。
|
||||
|
||||
## ShiErFeng v1.0.1
|
||||
|
||||
### 项目依赖
|
||||
|
||||
#### 后端
|
||||
|
||||
1.更新fastapi版本为0.109.1,修复一些安全性问题,命令:
|
||||
|
||||
```bash
|
||||
pip install fastapi[all]==0.109.1 -i https://mirrors.aliyun.com/pypi/simple/
|
||||
```
|
||||
|
||||
### 新增功能
|
||||
|
||||
1.日志管理模块新增字段排序查询。
|
||||
|
||||
## ShiErFeng v1.0.0
|
||||
|
||||
ShiErFeng第一个版本发布啦!
|
||||
此版本功能如下:
|
||||
1.用户管理:用户是系统操作者,该功能主要完成系统用户配置。
|
||||
2.角色管理:角色菜单权限分配。
|
||||
3.菜单管理:配置系统菜单,操作权限,按钮权限标识等。
|
||||
4.部门管理:配置系统组织机构(公司、部门、小组)。
|
||||
5.岗位管理:配置系统用户所属担任职务。
|
||||
6.字典管理:对系统中经常使用的一些较为固定的数据进行维护。
|
||||
7.参数管理:对系统动态配置常用参数。
|
||||
8.通知公告:系统通知公告信息发布维护。
|
||||
9.操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
|
||||
10.登录日志:系统登录日志记录查询包含登录异常。
|
||||
11.在线用户:当前系统中活跃用户状态监控。
|
||||
12.定时任务:在线(添加、修改、删除)任务调度包含执行结果日志。
|
||||
13.服务监控:监视当前系统CPU、内存、磁盘、堆栈等相关信息。
|
||||
14.缓存监控:对系统的缓存信息查询,命令统计等。
|
||||
15.系统接口:根据业务代码自动生成相关的api接口文档。
|
||||
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 insistence
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
168
README.md
Normal file
@ -0,0 +1,168 @@
|
||||
# ShiErFeng
|
||||
|
||||
基于 Vue3 与 FastAPI 的前后端分离快速开发框架。
|
||||
|
||||
## 平台简介
|
||||
|
||||
施尔丰是一套全部开源的快速开发平台,供个人及企业免费使用。
|
||||
|
||||
* 前端采用Vue3、Element Plus。
|
||||
* 移动端采用uni-app、Vue3、Vite,内置tailwindcss。
|
||||
* 后端采用FastAPI、sqlalchemy、MySQL(PostgreSQL)、Redis、OAuth2 & Jwt。
|
||||
* 权限认证使用OAuth2 & Jwt,支持多终端认证系统。
|
||||
* 支持加载动态权限菜单,多方式轻松权限控制。
|
||||
* 纯Python版本:
|
||||
* Gitte仓库地址:<https://gitee.com/insistence2022/dash-fastapi-admin>
|
||||
* GitHub仓库地址:<https://github.com/insistence/Dash-FastAPI-Admin>
|
||||
|
||||
## 内置功能
|
||||
|
||||
1. 用户管理:用户是系统操作者,该功能主要完成系统用户配置。
|
||||
2. 角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分。
|
||||
3. 菜单管理:配置系统菜单,操作权限,按钮权限标识等。
|
||||
4. 部门管理:配置系统组织机构(公司、部门、小组)。
|
||||
5. 岗位管理:配置系统用户所属担任职务。
|
||||
6. 字典管理:对系统中经常使用的一些较为固定的数据进行维护。
|
||||
7. 参数管理:对系统动态配置常用参数。
|
||||
8. 通知公告:系统通知公告信息发布维护。
|
||||
9. 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
|
||||
10. 登录日志:系统登录日志记录查询包含登录异常。
|
||||
11. 在线用户:当前系统中活跃用户状态监控。
|
||||
12. 定时任务:在线(添加、修改、删除)任务调度包含执行结果日志。
|
||||
13. 服务监控:监视当前系统CPU、内存、磁盘、堆栈等相关信息。
|
||||
14. 缓存监控:对系统的缓存信息查询,命令统计等。
|
||||
15. 传输加密:支持前后端请求加密、响应解密、公钥轮换、运行策略下发与监控统计。
|
||||
16. 在线构建器:拖动表单元素生成相应的HTML代码。
|
||||
17. 系统接口:根据业务代码自动生成相关的api接口文档。
|
||||
18. 代码生成:配置数据库表信息一键生成前后端代码(python、sql、vue、js),支持下载。
|
||||
19. AI管理:提供AI模型管理和AI对话功能。
|
||||
|
||||
## 在线体验
|
||||
|
||||
* *账号:admin*
|
||||
* *密码:admin123*
|
||||
* 演示地址:<a href="https://vfadmin.insistence.tech">vfadmin管理系统<a>
|
||||
|
||||
## 项目开发及发布相关
|
||||
|
||||
### 传输层加解密配置说明
|
||||
|
||||
后端密钥配置与轮换说明:[shierfeng-fastapi-backend/docs/transport_crypto_config.md](./shierfeng-fastapi-backend/docs/transport_crypto_config.md)
|
||||
|
||||
### 开发
|
||||
|
||||
```bash
|
||||
# 克隆项目
|
||||
git clone https://git.yujiangrubber.cn/niuniu/ShiErFeng.git
|
||||
|
||||
# 进入项目根目录
|
||||
cd ShiErFeng
|
||||
```
|
||||
|
||||
#### 前端
|
||||
|
||||
```bash
|
||||
# 进入前端目录
|
||||
cd shierfeng-fastapi-frontend
|
||||
|
||||
# 安装依赖
|
||||
npm install 或 yarn --registry=https://registry.npmmirror.com
|
||||
|
||||
# 建议不要直接使用 cnpm 安装依赖,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题
|
||||
npm install --registry=https://registry.npmmirror.com
|
||||
|
||||
# 启动服务
|
||||
npm run dev 或 yarn dev
|
||||
```
|
||||
|
||||
#### 移动端
|
||||
|
||||
```bash
|
||||
# 进入移动端目录
|
||||
cd shierfeng-fastapi-app
|
||||
|
||||
# 安装依赖
|
||||
npm install -g pnpm
|
||||
pnpm install
|
||||
|
||||
# 启动 H5
|
||||
pnpm dev:h5
|
||||
|
||||
# 启动微信小程序
|
||||
pnpm dev:mp-weixin
|
||||
```
|
||||
|
||||
移动端详细文档请参考:[shierfeng-fastapi-app/README.md](./shierfeng-fastapi-app/README.md)
|
||||
|
||||
#### 后端
|
||||
|
||||
```bash
|
||||
# 进入后端目录
|
||||
cd shierfeng-fastapi-backend
|
||||
|
||||
# 如果使用的是MySQL数据库,请执行以下命令安装项目依赖环境
|
||||
pip3 install -r requirements.txt
|
||||
# 如果使用的是PostgreSQL数据库,请执行以下命令安装项目依赖环境
|
||||
pip3 install -r requirements-pg.txt
|
||||
|
||||
# 配置环境
|
||||
在.env.dev文件中配置开发环境的数据库和redis
|
||||
|
||||
# 运行sql文件
|
||||
1.新建数据库shierfeng-fastapi(默认,可修改)
|
||||
2.如果使用的是MySQL数据库,使用命令或数据库连接工具运行sql文件夹下的shierfeng-fastapi.sql;如果使用的是PostgreSQL数据库,使用命令或数据库连接工具运行sql文件夹下的shierfeng-fastapi-pg.sql
|
||||
|
||||
# 运行后端
|
||||
shierfeng app run --env=dev
|
||||
```
|
||||
|
||||
后端 CLI 使用说明请参考:[shierfeng-fastapi-backend/docs/cli_usage.md](./shierfeng-fastapi-backend/docs/cli_usage.md)
|
||||
|
||||
#### 访问
|
||||
|
||||
```bash
|
||||
# 默认账号密码
|
||||
账号:admin
|
||||
密码:admin123
|
||||
|
||||
# 浏览器访问
|
||||
地址:http://localhost:80
|
||||
```
|
||||
|
||||
### 发布
|
||||
|
||||
#### 前端
|
||||
|
||||
```bash
|
||||
# 构建测试环境
|
||||
npm run build:stage 或 yarn build:stage
|
||||
|
||||
# 构建生产环境
|
||||
npm run build:prod 或 yarn build:prod
|
||||
```
|
||||
|
||||
#### 后端
|
||||
|
||||
```bash
|
||||
# 配置环境
|
||||
在.env.prod文件中配置生产环境的数据库和redis
|
||||
|
||||
# 运行后端
|
||||
shierfeng app run --env=prod
|
||||
```
|
||||
|
||||
### Docker Compose部署方式
|
||||
|
||||
> ⚠️ **警告:** 默认未做数据持久化配置,请注意数据备份或自行配置持久化
|
||||
|
||||
#### MySQL版本
|
||||
|
||||
```bash
|
||||
docker compose -f shierfeng-fastapi-test/docker-compose.test.my.yml up -d --build
|
||||
```
|
||||
|
||||
#### PostgreSQL版本
|
||||
|
||||
```bash
|
||||
docker compose -f shierfeng-fastapi-test/docker-compose.test.pg.yml up -d --build
|
||||
```
|
||||
12
shierfeng-fastapi-app/.editorconfig
Normal 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
shierfeng-fastapi-app/.gitignore
vendored
Normal 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
shierfeng-fastapi-app/.npmrc
Normal file
@ -0,0 +1,7 @@
|
||||
# 设置国内镜像地址
|
||||
registry=http://registry.npmmirror.com/
|
||||
# registry=http://registry.npmjs.com/
|
||||
# 这个是给 pnpm 用的
|
||||
shamefully-hoist=true
|
||||
|
||||
ignore-engines=true
|
||||
78
shierfeng-fastapi-app/README.md
Normal file
@ -0,0 +1,78 @@
|
||||
# ShiErFeng 移动端
|
||||
|
||||
基于 `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
shierfeng-fastapi-app/index.html
Normal 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
shierfeng-fastapi-app/netlify.toml
Normal file
@ -0,0 +1,13 @@
|
||||
# netlify 部署文件
|
||||
# 用于部署此项目的 h5 产物,可删除
|
||||
[build]
|
||||
publish = "dist/build/h5"
|
||||
command = "pnpm run build:h5"
|
||||
|
||||
[build.environment]
|
||||
NODE_VERSION = "20"
|
||||
|
||||
[[redirects]]
|
||||
from = "/*"
|
||||
to = "/index.html"
|
||||
status = 200
|
||||
98
shierfeng-fastapi-app/package.json
Normal file
@ -0,0 +1,98 @@
|
||||
{
|
||||
"name": "shierfeng-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",
|
||||
"node-forge": "^1.4.0",
|
||||
"pinia": "2.2.4",
|
||||
"vue": "^3.4.21",
|
||||
"vue-i18n": "^9.1.9",
|
||||
"vuex": "^4.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@dcloudio/types": "^3.4.8",
|
||||
"@dcloudio/uni-automator": "3.0.0-4080720251210001",
|
||||
"@dcloudio/uni-cli-shared": "3.0.0-4080720251210001",
|
||||
"@dcloudio/uni-stacktracey": "3.0.0-4080720251210001",
|
||||
"@dcloudio/vite-plugin-uni": "3.0.0-4080720251210001",
|
||||
"@egoist/tailwindcss-icons": "^1.9.2",
|
||||
"@icebreakers/stylelint-config": "^1.2.5",
|
||||
"@iconify-json/mdi": "^1.2.3",
|
||||
"@iconify-json/svg-spinners": "^1.2.4",
|
||||
"@types/node": "^24.10.1",
|
||||
"@vue/runtime-core": "^3.5.27",
|
||||
"autoprefixer": "^10.4.24",
|
||||
"postcss": "^8.5.6",
|
||||
"sass": "^1.97.3",
|
||||
"tailwindcss": "^3.4.19",
|
||||
"typescript": "^5.9.3",
|
||||
"unplugin-auto-import": "^20.3.0",
|
||||
"vite": "5.4.21",
|
||||
"weapp-ide-cli": "^5.0.1",
|
||||
"weapp-tailwindcss": "^4.9.8"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"weapp-tailwindcss",
|
||||
"@weapp-tailwindcss/merge-v3"
|
||||
]
|
||||
}
|
||||
}
|
||||
8
shierfeng-fastapi-app/platform.ts
Normal 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
shierfeng-fastapi-app/postcss.config.ts
Normal 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
shierfeng-fastapi-app/src/App.vue
Normal 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
shierfeng-fastapi-app/src/api/login.js
Normal 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,
|
||||
});
|
||||
}
|
||||
52
shierfeng-fastapi-app/src/api/system/dict/data.js
Normal file
@ -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",
|
||||
});
|
||||
}
|
||||
60
shierfeng-fastapi-app/src/api/system/dict/type.js
Normal file
@ -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
shierfeng-fastapi-app/src/api/system/user.js
Normal 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
shierfeng-fastapi-app/src/auto-imports.d.ts
vendored
Normal 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
shierfeng-fastapi-app/src/config.js
Normal file
@ -0,0 +1,26 @@
|
||||
// 应用全局配置
|
||||
export default {
|
||||
baseUrl: "http://localhost:9099",
|
||||
// 应用信息
|
||||
appInfo: {
|
||||
// 应用名称
|
||||
name: "ShiErFeng-FastAPI-APP",
|
||||
// 应用版本
|
||||
version: "1.9.0",
|
||||
// 应用logo
|
||||
logo: "/static/logo.png",
|
||||
// 官方网站
|
||||
site_url: "https://vfadmin.insistence.tech",
|
||||
// 政策协议
|
||||
agreements: [
|
||||
{
|
||||
title: "隐私政策",
|
||||
url: "/pages/common/privacy/index",
|
||||
},
|
||||
{
|
||||
title: "用户服务协议",
|
||||
url: "/pages/common/agreement/index",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
8
shierfeng-fastapi-app/src/env.d.ts
vendored
Normal 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
shierfeng-fastapi-app/src/main.ts
Normal 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
shierfeng-fastapi-app/src/manifest.json
Normal file
@ -0,0 +1,70 @@
|
||||
{
|
||||
"name": "ShiErFeng-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": "ShiErFeng-FastAPI-APP",
|
||||
"router": {
|
||||
"mode": "hash",
|
||||
"base": "./"
|
||||
}
|
||||
}
|
||||
}
|
||||
132
shierfeng-fastapi-app/src/pages.json
Normal file
@ -0,0 +1,132 @@
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/login",
|
||||
"style": {
|
||||
"navigationBarTitleText": "登录"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/register",
|
||||
"style": {
|
||||
"navigationBarTitleText": "注册"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "ShiErFeng-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": "ShiErFeng",
|
||||
"navigationBarBackgroundColor": "#FFFFFF"
|
||||
}
|
||||
}
|
||||
63
shierfeng-fastapi-app/src/pages/common/agreement/index.vue
Normal file
@ -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
|
||||
>欢迎使用ShiErFeng-FastAPI移动端框架(以下简称“本产品”)。本产品的服务所有权和运作权归ShiErFeng-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
|
||||
>本产品包含的所有内容(包括但不限于文字、图片、音频、视频、软件代码等)的知识产权归ShiErFeng-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>
|
||||
87
shierfeng-fastapi-app/src/pages/common/privacy/index.vue
Normal file
@ -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
|
||||
>ShiErFeng-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>
|
||||
37
shierfeng-fastapi-app/src/pages/common/textview/index.vue
Normal file
@ -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>
|
||||
33
shierfeng-fastapi-app/src/pages/common/webview/index.vue
Normal file
@ -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
shierfeng-fastapi-app/src/pages/index.vue
Normal 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">ShiErFeng-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
shierfeng-fastapi-app/src/pages/login.vue
Normal 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"
|
||||
>ShiErFeng-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>
|
||||
68
shierfeng-fastapi-app/src/pages/mine/about/index.vue
Normal file
@ -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"
|
||||
>ShiErFeng-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 © 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>
|
||||
678
shierfeng-fastapi-app/src/pages/mine/avatar/index.vue
Normal file
@ -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>
|
||||
91
shierfeng-fastapi-app/src/pages/mine/help/index.vue
Normal file
@ -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: "ShiErFeng-FastAPI问题",
|
||||
childList: [
|
||||
{
|
||||
title: "ShiErFeng-FastAPI开源吗?",
|
||||
content: "开源",
|
||||
},
|
||||
{
|
||||
title: "ShiErFeng-FastAPI可以商用吗?",
|
||||
content: "可以",
|
||||
},
|
||||
{
|
||||
title: "ShiErFeng-FastAPI官网地址多少?",
|
||||
content: "https://vfadmin.insistence.tech",
|
||||
},
|
||||
{
|
||||
title: "ShiErFeng-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
shierfeng-fastapi-app/src/pages/mine/index.vue
Normal 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>
|
||||
145
shierfeng-fastapi-app/src/pages/mine/info/edit.vue
Normal file
@ -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>
|
||||
121
shierfeng-fastapi-app/src/pages/mine/info/index.vue
Normal file
@ -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>
|
||||
90
shierfeng-fastapi-app/src/pages/mine/pwd/index.vue
Normal file
@ -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>
|
||||
87
shierfeng-fastapi-app/src/pages/mine/setting/index.vue
Normal file
@ -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
shierfeng-fastapi-app/src/pages/register.vue
Normal 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"
|
||||
>ShiErFeng-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
shierfeng-fastapi-app/src/pages/work/index.vue
Normal 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
shierfeng-fastapi-app/src/permission.js
Normal 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
shierfeng-fastapi-app/src/plugins/auth.js
Normal 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
shierfeng-fastapi-app/src/plugins/index.js
Normal 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
shierfeng-fastapi-app/src/plugins/modal.js
Normal 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
shierfeng-fastapi-app/src/plugins/tab.js
Normal 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();
|
||||
},
|
||||
};
|
||||
BIN
shierfeng-fastapi-app/src/static/favicon.ico
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
shierfeng-fastapi-app/src/static/images/banner/banner01.jpg
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
shierfeng-fastapi-app/src/static/images/banner/banner02.jpg
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
shierfeng-fastapi-app/src/static/images/banner/banner03.jpg
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
shierfeng-fastapi-app/src/static/images/profile.jpg
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
shierfeng-fastapi-app/src/static/images/tabbar/home.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
shierfeng-fastapi-app/src/static/images/tabbar/home_.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
shierfeng-fastapi-app/src/static/images/tabbar/mine.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
shierfeng-fastapi-app/src/static/images/tabbar/mine_.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
shierfeng-fastapi-app/src/static/images/tabbar/work.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
shierfeng-fastapi-app/src/static/images/tabbar/work_.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
shierfeng-fastapi-app/src/static/logo.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
shierfeng-fastapi-app/src/static/logo200.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
9
shierfeng-fastapi-app/src/store/index.js
Normal 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 };
|
||||
13
shierfeng-fastapi-app/src/store/modules/config.js
Normal file
@ -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,
|
||||
};
|
||||
});
|
||||
56
shierfeng-fastapi-app/src/store/modules/dict.js
Normal file
@ -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
shierfeng-fastapi-app/src/store/modules/user.js
Normal 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
shierfeng-fastapi-app/src/theme.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"light": {
|
||||
"navBgColor": "#f6f6f6",
|
||||
"navTxtStyle": "black"
|
||||
},
|
||||
"dark": {
|
||||
"navBgColor": "#191919",
|
||||
"navTxtStyle": "white"
|
||||
}
|
||||
}
|
||||
0
shierfeng-fastapi-app/src/uni.scss
Normal file
13
shierfeng-fastapi-app/src/utils/auth.js
Normal 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
shierfeng-fastapi-app/src/utils/common.js
Normal 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
shierfeng-fastapi-app/src/utils/constant.js
Normal 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
shierfeng-fastapi-app/src/utils/dict.js
Normal 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
shierfeng-fastapi-app/src/utils/errorCode.js
Normal file
@ -0,0 +1,6 @@
|
||||
export default {
|
||||
401: "认证失败,无法访问系统资源",
|
||||
403: "当前操作没有权限",
|
||||
404: "访问资源不存在",
|
||||
default: "系统未知错误,请反馈给管理员",
|
||||
};
|
||||
55
shierfeng-fastapi-app/src/utils/permission.js
Normal file
@ -0,0 +1,55 @@
|
||||
import store from "@/store";
|
||||
|
||||
/**
|
||||
* 字符权限校验
|
||||
* @param {Array} value 校验值
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function checkPermi(value) {
|
||||
if (value && value instanceof Array && value.length > 0) {
|
||||
const permissions = store.getters && store.getters.permissions;
|
||||
const permissionDatas = value;
|
||||
const all_permission = "*:*:*";
|
||||
|
||||
const hasPermission = permissions.some((permission) => {
|
||||
return (
|
||||
all_permission === permission || permissionDatas.includes(permission)
|
||||
);
|
||||
});
|
||||
|
||||
if (!hasPermission) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
console.error(
|
||||
`need roles! Like checkPermi="['system:user:add','system:user:edit']"`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 角色权限校验
|
||||
* @param {Array} value 校验值
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function checkRole(value) {
|
||||
if (value && value instanceof Array && value.length > 0) {
|
||||
const roles = store.getters && store.getters.roles;
|
||||
const permissionRoles = value;
|
||||
const super_admin = "admin";
|
||||
|
||||
const hasRole = roles.some((role) => {
|
||||
return super_admin === role || permissionRoles.includes(role);
|
||||
});
|
||||
|
||||
if (!hasRole) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
console.error(`need roles! Like checkRole="['admin','editor']"`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
131
shierfeng-fastapi-app/src/utils/request.js
Normal file
@ -0,0 +1,131 @@
|
||||
import config from "@/config";
|
||||
import { getToken } from "@/utils/auth";
|
||||
import errorCode from "@/utils/errorCode";
|
||||
import { useUserStore } from "@/store/modules/user";
|
||||
import { toast, showConfirm, tansParams } from "@/utils/common";
|
||||
import {
|
||||
decryptTransportErrorResponse,
|
||||
decryptTransportResponse,
|
||||
encryptTransportRequest,
|
||||
invalidateTransportKeyMeta,
|
||||
resetTransportRequestConfig,
|
||||
shouldRetryTransportWithFreshKey,
|
||||
} from "@/utils/transportCrypto";
|
||||
|
||||
let timeout = 10000;
|
||||
const baseUrl = config.baseUrl;
|
||||
|
||||
const request = async (config) => {
|
||||
// 是否需要设置 token
|
||||
const isToken = (config.headers || {}).isToken === false;
|
||||
config.header = config.header || {};
|
||||
config.headers = config.headers || {};
|
||||
if (getToken() && !isToken) {
|
||||
config.header["Authorization"] = "Bearer " + getToken();
|
||||
}
|
||||
|
||||
try {
|
||||
config = await encryptTransportRequest(config);
|
||||
|
||||
// get请求映射params参数
|
||||
if (config.params) {
|
||||
let url = config.url + "?" + tansParams(config.params);
|
||||
url = url.slice(0, -1);
|
||||
config.url = url;
|
||||
}
|
||||
|
||||
return await new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
method: config.method || "get",
|
||||
timeout: config.timeout || timeout,
|
||||
url: config.baseUrl || baseUrl + config.url,
|
||||
data: config.data,
|
||||
header: config.header,
|
||||
dataType: "json",
|
||||
success: async (response) => {
|
||||
try {
|
||||
const res = await decryptTransportResponse(response, config);
|
||||
|
||||
if (shouldRetryTransportWithFreshKey(res) && !config.__transportRetried) {
|
||||
invalidateTransportKeyMeta();
|
||||
config.__transportRetried = true;
|
||||
config.headers.repeatSubmit = false;
|
||||
resetTransportRequestConfig(config);
|
||||
resolve(await request(config));
|
||||
return;
|
||||
}
|
||||
|
||||
const code = res.data.code || 200;
|
||||
const msg = errorCode[code] || res.data.msg || errorCode["default"];
|
||||
if (code === 401) {
|
||||
showConfirm(
|
||||
"登录状态已过期,您可以继续留在该页面,或者重新登录?",
|
||||
).then((res) => {
|
||||
if (res.confirm) {
|
||||
useUserStore()
|
||||
.logOut()
|
||||
.then(() => {
|
||||
uni.reLaunch({ url: "/pages/login" });
|
||||
});
|
||||
}
|
||||
});
|
||||
const error = new Error("无效的会话,或者会话已过期,请重新登录。");
|
||||
error.response = res;
|
||||
reject(error);
|
||||
} else if (code === 500) {
|
||||
const error = new Error(msg);
|
||||
error.response = res;
|
||||
reject(error);
|
||||
} else if (code !== 200) {
|
||||
const error = new Error(msg);
|
||||
error.response = res;
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(res.data);
|
||||
}
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
},
|
||||
fail: reject,
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
error = await decryptTransportErrorResponse(error, config);
|
||||
if (shouldRetryTransportWithFreshKey(error) && !config.__transportRetried) {
|
||||
invalidateTransportKeyMeta();
|
||||
config.__transportRetried = true;
|
||||
config.headers.repeatSubmit = false;
|
||||
resetTransportRequestConfig(config);
|
||||
return request(config);
|
||||
}
|
||||
|
||||
const response = error.response;
|
||||
const responseStatus = response?.status ?? response?.statusCode;
|
||||
const responseCode = response?.data?.code;
|
||||
const responseMsg = response?.data?.msg;
|
||||
if (responseMsg) {
|
||||
uni.showToast({
|
||||
title: responseMsg,
|
||||
icon: "none",
|
||||
duration: responseStatus === 429 || responseCode === 429 ? 5000 : 3000,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
let { message } = error;
|
||||
if (message === "Network Error") {
|
||||
message = "后端接口连接异常";
|
||||
} else if (message && message.includes("timeout")) {
|
||||
message = "系统接口请求超时";
|
||||
} else if (message && message.includes("Request failed with status code")) {
|
||||
message = "系统接口" + message.substr(message.length - 3) + "异常";
|
||||
}
|
||||
if (message) {
|
||||
toast(message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export default request;
|
||||
38
shierfeng-fastapi-app/src/utils/storage.js
Normal 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;
|
||||
1236
shierfeng-fastapi-app/src/utils/transportCrypto.js
Normal file
436
shierfeng-fastapi-app/src/utils/transportCryptoPolicy.js
Normal file
@ -0,0 +1,436 @@
|
||||
import config from "@/config";
|
||||
|
||||
const TRANSPORT_BASE_URL = config.baseUrl;
|
||||
const EXCLUDED_URL_PATTERNS = [
|
||||
"/transport/crypto/frontend-config",
|
||||
"/transport/crypto/public-key",
|
||||
"/common/download",
|
||||
"/common/download/resource",
|
||||
];
|
||||
const TRANSPORT_FRONTEND_CONFIG_CACHE_KEY = "transportCryptoFrontendConfig";
|
||||
const TRANSPORT_FRONTEND_CONFIG_URL = "/transport/crypto/frontend-config";
|
||||
const TRANSPORT_FRONTEND_CONFIG_FALLBACK_TTL_SECONDS = 60;
|
||||
const DEFAULT_TRANSPORT_ENVELOPE_VERSION = "1";
|
||||
const DEFAULT_REQUEST_ENVELOPE_ALGORITHM = "RSA_OAEP_AES_256_GCM";
|
||||
const DEFAULT_RESPONSE_ENVELOPE_ALGORITHM = "AES_256_GCM";
|
||||
const DEFAULT_TRANSPORT_MAX_GET_URL_LENGTH = 4096;
|
||||
|
||||
let cachedTransportPolicy = null;
|
||||
let inflightTransportPolicyPromise = null;
|
||||
|
||||
/**
|
||||
* 获取当前 Unix 秒级时间戳。
|
||||
*
|
||||
* @returns {number} 当前时间戳
|
||||
*/
|
||||
function getNowTimestamp() {
|
||||
return Math.floor(Date.now() / 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断请求地址是否命中固定排除名单。
|
||||
*
|
||||
* @param {string} url 请求地址
|
||||
* @returns {boolean} 是否命中排除规则
|
||||
*/
|
||||
function matchExcludedUrl(url = "") {
|
||||
return EXCLUDED_URL_PATTERNS.some((pattern) => url.includes(pattern));
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断请求路径是否命中路径前缀列表。
|
||||
*
|
||||
* @param {string} path 待匹配路径
|
||||
* @param {string[]} pathPatterns 路径前缀集合
|
||||
* @returns {boolean} 是否匹配成功
|
||||
*/
|
||||
function matchPathPrefix(path = "", pathPatterns = []) {
|
||||
return pathPatterns.some(
|
||||
(pattern) => path === pattern || path.startsWith(`${pattern}/`),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从绝对地址中提取 pathname,或直接返回相对地址的路径部分。
|
||||
*
|
||||
* @param {string} url 请求地址
|
||||
* @returns {string} 标准化后的路径
|
||||
*/
|
||||
function parseAbsoluteUrlPath(url = "") {
|
||||
const normalizedUrl = String(url || "");
|
||||
if (!normalizedUrl) {
|
||||
return "/";
|
||||
}
|
||||
if (
|
||||
!normalizedUrl.startsWith("http://") &&
|
||||
!normalizedUrl.startsWith("https://")
|
||||
) {
|
||||
return normalizedUrl.split("?")[0] || "/";
|
||||
}
|
||||
const pathMatch = normalizedUrl.match(/^https?:\/\/[^/]+(\/[^?#]*)?/i);
|
||||
return pathMatch?.[1] || "/";
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析基础 API 地址对应的路径前缀。
|
||||
*
|
||||
* @param {string} baseUrl 基础 API 地址
|
||||
* @returns {string} 基础路径前缀
|
||||
*/
|
||||
function getBaseApiPath(baseUrl = TRANSPORT_BASE_URL) {
|
||||
if (!baseUrl) {
|
||||
return "";
|
||||
}
|
||||
const baseApiPath = parseAbsoluteUrlPath(baseUrl);
|
||||
return baseApiPath === "/" ? "" : baseApiPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算后端用于 AAD 与策略匹配的标准请求路径。
|
||||
*
|
||||
* @param {string} url 请求地址
|
||||
* @param {string} baseUrl 基础 API 地址
|
||||
* @returns {string} 标准化请求路径
|
||||
*/
|
||||
function getRequestPath(url = "", baseUrl = TRANSPORT_BASE_URL) {
|
||||
const baseApiPath = getBaseApiPath(baseUrl);
|
||||
const pathname = parseAbsoluteUrlPath(url);
|
||||
if (baseApiPath && pathname.startsWith(baseApiPath)) {
|
||||
const normalizedPath = pathname.slice(baseApiPath.length);
|
||||
return normalizedPath || "/";
|
||||
}
|
||||
return pathname || "/";
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准化后端下发的路径列表配置。
|
||||
*
|
||||
* @param {Array} paths 原始路径集合
|
||||
* @returns {string[]} 标准化后的路径数组
|
||||
*/
|
||||
function normalizePaths(paths) {
|
||||
if (!Array.isArray(paths)) {
|
||||
return [];
|
||||
}
|
||||
return paths.map((path) => String(path || "").trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求头对象中读取指定字段。
|
||||
*
|
||||
* @param {Object} headers 请求头对象
|
||||
* @param {string} name 请求头名称
|
||||
* @returns {*} 请求头值
|
||||
*/
|
||||
function getHeaderValue(headers, name) {
|
||||
if (!headers) {
|
||||
return undefined;
|
||||
}
|
||||
return headers[name] ?? headers[name.toLowerCase()] ?? headers[name.toUpperCase()];
|
||||
}
|
||||
|
||||
/**
|
||||
* 将后端配置响应转换为前端统一使用的策略对象。
|
||||
*
|
||||
* @param {Object} payload 后端返回的配置数据
|
||||
* @returns {Object} 标准化后的传输加密策略
|
||||
*/
|
||||
function normalizeTransportPolicy(payload) {
|
||||
return {
|
||||
transportCryptoEnabled: Boolean(payload?.transportCryptoEnabled),
|
||||
transportCryptoMode: String(payload?.transportCryptoMode || "off"),
|
||||
transportCryptoActive: Boolean(payload?.transportCryptoActive),
|
||||
envelopeVersion: String(
|
||||
payload?.envelopeVersion || DEFAULT_TRANSPORT_ENVELOPE_VERSION,
|
||||
),
|
||||
publicKeyUrl: String(payload?.publicKeyUrl || "/transport/crypto/public-key"),
|
||||
requestEnvelopeAlgorithm: String(
|
||||
payload?.requestEnvelopeAlgorithm || DEFAULT_REQUEST_ENVELOPE_ALGORITHM,
|
||||
),
|
||||
responseEnvelopeAlgorithm: String(
|
||||
payload?.responseEnvelopeAlgorithm || DEFAULT_RESPONSE_ENVELOPE_ALGORITHM,
|
||||
),
|
||||
enabledPaths: normalizePaths(payload?.enabledPaths),
|
||||
requiredPaths: normalizePaths(payload?.requiredPaths),
|
||||
excludePaths: normalizePaths(payload?.excludePaths),
|
||||
maxEncryptedGetUrlLength: Number(
|
||||
payload?.maxEncryptedGetUrlLength || DEFAULT_TRANSPORT_MAX_GET_URL_LENGTH,
|
||||
),
|
||||
configExpireAt: Number(payload?.configExpireAt || 0),
|
||||
retryAt: Number(payload?.retryAt || payload?.configExpireAt || 0),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建不可用场景下的本地兜底策略。
|
||||
*
|
||||
* @returns {Object} 明文回退策略
|
||||
*/
|
||||
function buildFallbackTransportPolicy() {
|
||||
const nowTimestamp = getNowTimestamp();
|
||||
return {
|
||||
transportCryptoEnabled: false,
|
||||
transportCryptoMode: "off",
|
||||
transportCryptoActive: false,
|
||||
envelopeVersion: DEFAULT_TRANSPORT_ENVELOPE_VERSION,
|
||||
publicKeyUrl: "/transport/crypto/public-key",
|
||||
requestEnvelopeAlgorithm: DEFAULT_REQUEST_ENVELOPE_ALGORITHM,
|
||||
responseEnvelopeAlgorithm: DEFAULT_RESPONSE_ENVELOPE_ALGORITHM,
|
||||
enabledPaths: [],
|
||||
requiredPaths: [],
|
||||
excludePaths: [...EXCLUDED_URL_PATTERNS],
|
||||
maxEncryptedGetUrlLength: DEFAULT_TRANSPORT_MAX_GET_URL_LENGTH,
|
||||
configExpireAt:
|
||||
nowTimestamp + TRANSPORT_FRONTEND_CONFIG_FALLBACK_TTL_SECONDS,
|
||||
retryAt: nowTimestamp + TRANSPORT_FRONTEND_CONFIG_FALLBACK_TTL_SECONDS,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于旧策略生成短期可重试的缓存策略。
|
||||
*
|
||||
* @param {Object} policy 旧的策略对象
|
||||
* @returns {Object} 可重试策略
|
||||
*/
|
||||
function buildRetryableTransportPolicy(policy) {
|
||||
const normalizedPolicy = normalizeTransportPolicy(policy);
|
||||
return {
|
||||
...normalizedPolicy,
|
||||
retryAt: getNowTimestamp() + TRANSPORT_FRONTEND_CONFIG_FALLBACK_TTL_SECONDS,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前策略是否仍在可用期内。
|
||||
*
|
||||
* @param {Object} policy 待校验策略
|
||||
* @returns {boolean} 是否可继续使用
|
||||
*/
|
||||
function isUsableTransportPolicy(policy) {
|
||||
if (!policy || !policy.publicKeyUrl || !policy.retryAt) {
|
||||
return false;
|
||||
}
|
||||
return policy.retryAt > getNowTimestamp();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从本地缓存加载最近一次持久化的策略。
|
||||
*
|
||||
* @returns {Object|null} 缓存策略
|
||||
*/
|
||||
function loadPersistedTransportPolicy() {
|
||||
const persistedTransportPolicy = uni.getStorageSync(
|
||||
TRANSPORT_FRONTEND_CONFIG_CACHE_KEY,
|
||||
);
|
||||
if (!persistedTransportPolicy) {
|
||||
return null;
|
||||
}
|
||||
return normalizeTransportPolicy(persistedTransportPolicy);
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求后端传输加密前端配置。
|
||||
* 这里直接使用原始 uni.request,避免策略初始化阶段反向依赖统一 request
|
||||
* 包装器,导致“是否加密尚未判定时又要先走加密请求”的循环依赖。
|
||||
*
|
||||
* @returns {Promise<Object>} Uni 请求响应结果
|
||||
*/
|
||||
function requestFrontendConfig() {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: `${TRANSPORT_BASE_URL}${TRANSPORT_FRONTEND_CONFIG_URL}`,
|
||||
method: "GET",
|
||||
timeout: 10000,
|
||||
success: resolve,
|
||||
fail: reject,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取请求加密使用的标准路径。
|
||||
*
|
||||
* @param {string} url 请求地址
|
||||
* @param {string} baseUrl 基础 API 地址
|
||||
* @returns {string} 标准请求路径
|
||||
*/
|
||||
export function getTransportRequestPath(
|
||||
url = "",
|
||||
baseUrl = TRANSPORT_BASE_URL,
|
||||
) {
|
||||
return getRequestPath(url, baseUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前生效的传输加密策略。
|
||||
*
|
||||
* @returns {Object} 当前策略对象
|
||||
*/
|
||||
export function getTransportCryptoPolicy() {
|
||||
return cachedTransportPolicy || buildFallbackTransportPolicy();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空当前策略缓存与持久化数据。
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
export function invalidateTransportCryptoPolicy() {
|
||||
cachedTransportPolicy = null;
|
||||
inflightTransportPolicyPromise = null;
|
||||
uni.removeStorageSync(TRANSPORT_FRONTEND_CONFIG_CACHE_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保本地已加载一份可用的传输加密策略。
|
||||
*
|
||||
* @param {boolean} forceRefresh 是否强制从后端刷新
|
||||
* @returns {Promise<Object>} 当前可用策略
|
||||
*/
|
||||
export async function ensureTransportCryptoPolicyLoaded(forceRefresh = false) {
|
||||
if (!forceRefresh && !cachedTransportPolicy) {
|
||||
const persistedTransportPolicy = loadPersistedTransportPolicy();
|
||||
if (isUsableTransportPolicy(persistedTransportPolicy)) {
|
||||
cachedTransportPolicy = persistedTransportPolicy;
|
||||
}
|
||||
}
|
||||
|
||||
if (!forceRefresh && isUsableTransportPolicy(cachedTransportPolicy)) {
|
||||
return cachedTransportPolicy;
|
||||
}
|
||||
|
||||
if (inflightTransportPolicyPromise) {
|
||||
return inflightTransportPolicyPromise;
|
||||
}
|
||||
|
||||
inflightTransportPolicyPromise = requestFrontendConfig()
|
||||
.then((response) => {
|
||||
const payload = normalizeTransportPolicy(response?.data?.data || {});
|
||||
cachedTransportPolicy = payload;
|
||||
uni.setStorageSync(TRANSPORT_FRONTEND_CONFIG_CACHE_KEY, payload);
|
||||
inflightTransportPolicyPromise = null;
|
||||
return cachedTransportPolicy;
|
||||
})
|
||||
.catch((error) => {
|
||||
const staleTransportPolicy =
|
||||
cachedTransportPolicy || loadPersistedTransportPolicy();
|
||||
inflightTransportPolicyPromise = null;
|
||||
cachedTransportPolicy = staleTransportPolicy
|
||||
? buildRetryableTransportPolicy(staleTransportPolicy)
|
||||
: buildFallbackTransportPolicy();
|
||||
uni.setStorageSync(
|
||||
TRANSPORT_FRONTEND_CONFIG_CACHE_KEY,
|
||||
cachedTransportPolicy,
|
||||
);
|
||||
if (staleTransportPolicy) {
|
||||
console.warn(
|
||||
"加载传输加密前端配置失败,当前继续沿用最近一次后端策略",
|
||||
error,
|
||||
);
|
||||
} else {
|
||||
console.warn("加载传输加密前端配置失败,当前回退为明文请求策略", error);
|
||||
}
|
||||
return cachedTransportPolicy;
|
||||
});
|
||||
|
||||
return inflightTransportPolicyPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前请求是否需要执行请求体加密。
|
||||
*
|
||||
* @param {Object} requestConfig 请求配置
|
||||
* @param {Object} transportPolicy 传输加密策略
|
||||
* @returns {boolean} 是否启用请求加密
|
||||
*/
|
||||
export function shouldEncryptRequest(
|
||||
requestConfig,
|
||||
transportPolicy = getTransportCryptoPolicy(),
|
||||
) {
|
||||
if (!transportPolicy.transportCryptoActive) {
|
||||
return false;
|
||||
}
|
||||
const requestPath = getRequestPath(
|
||||
requestConfig.url,
|
||||
requestConfig.baseUrl || TRANSPORT_BASE_URL,
|
||||
);
|
||||
if (matchPathPrefix(requestPath, transportPolicy.excludePaths || [])) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
(transportPolicy.enabledPaths || []).length &&
|
||||
!matchPathPrefix(requestPath, transportPolicy.enabledPaths || [])
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if ((requestConfig.headers || {}).encrypt === false) {
|
||||
return false;
|
||||
}
|
||||
if (matchExcludedUrl(requestConfig.url)) {
|
||||
return false;
|
||||
}
|
||||
const contentType =
|
||||
getHeaderValue(requestConfig.header, "Content-Type") ||
|
||||
getHeaderValue(requestConfig.headers, "Content-Type") ||
|
||||
"";
|
||||
if (String(contentType).includes("multipart/form-data")) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前响应是否需要执行自动解密。
|
||||
*
|
||||
* @param {Object} requestConfig 请求配置
|
||||
* @param {Object} transportPolicy 传输加密策略
|
||||
* @returns {boolean} 是否启用响应解密
|
||||
*/
|
||||
export function shouldEncryptResponse(
|
||||
requestConfig,
|
||||
transportPolicy = getTransportCryptoPolicy(),
|
||||
) {
|
||||
const requestPath = getRequestPath(
|
||||
requestConfig.url,
|
||||
requestConfig.baseUrl || TRANSPORT_BASE_URL,
|
||||
);
|
||||
if (matchPathPrefix(requestPath, transportPolicy.excludePaths || [])) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
(transportPolicy.enabledPaths || []).length &&
|
||||
!matchPathPrefix(requestPath, transportPolicy.enabledPaths || [])
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if ((requestConfig.headers || {}).encryptResponse === false) {
|
||||
return false;
|
||||
}
|
||||
if (matchExcludedUrl(requestConfig.url)) {
|
||||
return false;
|
||||
}
|
||||
if (requestConfig.__transportCryptoEnabledForRequest === true) {
|
||||
return true;
|
||||
}
|
||||
if (requestConfig.__transportCryptoEnabledForRequest === false) {
|
||||
return false;
|
||||
}
|
||||
return transportPolicy.transportCryptoActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断查询参数是否需要封装为加密信封。
|
||||
*
|
||||
* @param {Object} requestConfig 请求配置
|
||||
* @param {Object} transportPolicy 传输加密策略
|
||||
* @returns {boolean} 是否启用查询参数加密
|
||||
*/
|
||||
export function shouldEncryptQuery(
|
||||
requestConfig,
|
||||
transportPolicy = getTransportCryptoPolicy(),
|
||||
) {
|
||||
if ((requestConfig.headers || {}).encryptQuery === false) {
|
||||
return false;
|
||||
}
|
||||
return shouldEncryptRequest(requestConfig, transportPolicy);
|
||||
}
|
||||
214
shierfeng-fastapi-app/src/utils/transportForge.js
Normal file
@ -0,0 +1,214 @@
|
||||
import forge from "node-forge/lib/forge";
|
||||
import "node-forge/lib/util";
|
||||
import "node-forge/lib/asn1";
|
||||
import "node-forge/lib/oids";
|
||||
import "node-forge/lib/cipher";
|
||||
import "node-forge/lib/cipherModes";
|
||||
import "node-forge/lib/aes";
|
||||
import "node-forge/lib/jsbn";
|
||||
import "node-forge/lib/pkcs1";
|
||||
import "node-forge/lib/prime";
|
||||
import "node-forge/lib/random";
|
||||
import "node-forge/lib/md";
|
||||
import "node-forge/lib/sha256";
|
||||
import "node-forge/lib/mgf1";
|
||||
import "node-forge/lib/pem";
|
||||
import "node-forge/lib/pki";
|
||||
|
||||
let randomBytePool = "";
|
||||
const FORGE_RANDOM_REFILL_BYTES = 4096;
|
||||
|
||||
/**
|
||||
* 将 Uint8Array 转为 Forge 兼容的二进制字符串。
|
||||
*
|
||||
* @param {Uint8Array} uint8Array 字节数组
|
||||
* @returns {string} 二进制字符串
|
||||
*/
|
||||
function uint8ArrayToBytes(uint8Array) {
|
||||
return Array.from(uint8Array, (item) => String.fromCharCode(item)).join("");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前运行时的全局对象引用。
|
||||
*
|
||||
* @returns {Object|undefined} 全局对象
|
||||
*/
|
||||
function getRuntimeGlobal() {
|
||||
if (typeof globalThis !== "undefined") {
|
||||
return globalThis;
|
||||
}
|
||||
if (typeof self !== "undefined") {
|
||||
return self;
|
||||
}
|
||||
if (typeof window !== "undefined") {
|
||||
return window;
|
||||
}
|
||||
if (typeof global !== "undefined") {
|
||||
return global;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 Web Crypto 同步获取随机字节。
|
||||
*
|
||||
* @param {number} length 需要的字节长度
|
||||
* @returns {string|null} 随机字节串,当前运行环境不支持时返回 null
|
||||
*/
|
||||
function getWebCryptoRandomBytes(length) {
|
||||
const runtimeCrypto = getRuntimeGlobal()?.crypto;
|
||||
if (!runtimeCrypto?.getRandomValues) {
|
||||
return null;
|
||||
}
|
||||
const bytes = new Uint8Array(length);
|
||||
runtimeCrypto.getRandomValues(bytes);
|
||||
return uint8ArrayToBytes(bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 app-plus iOS 端通过原生 NSUUID 获取随机字节兜底。
|
||||
*
|
||||
* @param {number} length 需要的字节长度
|
||||
* @returns {string|null} 随机字节串,当前运行环境不支持时返回 null
|
||||
*/
|
||||
function getAppPlusIosRandomBytes(length) {
|
||||
// #ifdef APP-PLUS
|
||||
if (typeof plus === "undefined") {
|
||||
return null;
|
||||
}
|
||||
const osName = String(plus.os?.name || "").toLowerCase();
|
||||
if (osName !== "ios" || typeof plus.ios?.importClass !== "function") {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const uuidClass = plus.ios.importClass("NSUUID");
|
||||
let randomBytes = "";
|
||||
while (randomBytes.length < length) {
|
||||
const uuidObject = plus.ios.invoke(uuidClass, "UUID");
|
||||
const uuidText = String(plus.ios.invoke(uuidObject, "UUIDString") || "")
|
||||
.replace(/-/g, "")
|
||||
.toLowerCase();
|
||||
plus.ios.deleteObject(uuidObject);
|
||||
if (!uuidText) {
|
||||
return null;
|
||||
}
|
||||
for (
|
||||
let index = 0;
|
||||
index < uuidText.length && randomBytes.length < length;
|
||||
index += 2
|
||||
) {
|
||||
const byteValue = parseInt(uuidText.slice(index, index + 2), 16);
|
||||
if (isNaN(byteValue)) {
|
||||
return null;
|
||||
}
|
||||
randomBytes += String.fromCharCode(byteValue);
|
||||
}
|
||||
}
|
||||
return randomBytes;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
// #endif
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 app-plus Android 端通过原生 SecureRandom 获取随机字节兜底。
|
||||
*
|
||||
* @param {number} length 需要的字节长度
|
||||
* @returns {string|null} 随机字节串,当前运行环境不支持时返回 null
|
||||
*/
|
||||
function getAppPlusAndroidRandomBytes(length) {
|
||||
// #ifdef APP-PLUS
|
||||
if (typeof plus === "undefined") {
|
||||
return null;
|
||||
}
|
||||
const osName = String(plus.os?.name || "").toLowerCase();
|
||||
if (osName !== "android" || typeof plus.android?.importClass !== "function") {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
plus.android.importClass("java.security.SecureRandom");
|
||||
const secureRandom =
|
||||
typeof plus.android.newObject === "function"
|
||||
? plus.android.newObject("java.security.SecureRandom")
|
||||
: null;
|
||||
if (!secureRandom) {
|
||||
return null;
|
||||
}
|
||||
plus.android.importClass(secureRandom);
|
||||
let randomBytes = "";
|
||||
for (let index = 0; index < length; index += 1) {
|
||||
randomBytes += String.fromCharCode(secureRandom.nextInt(256));
|
||||
}
|
||||
if (typeof plus.android.autoCollection === "function") {
|
||||
plus.android.autoCollection(secureRandom);
|
||||
}
|
||||
return randomBytes;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
// #endif
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在随机池不足时尝试同步补充一段新的随机字节。
|
||||
*
|
||||
* @param {number} length 当前至少需要的字节长度
|
||||
* @returns {void}
|
||||
*/
|
||||
function refillRandomBytePool(length) {
|
||||
const refillLength = Math.max(length, FORGE_RANDOM_REFILL_BYTES);
|
||||
const randomBytes =
|
||||
getWebCryptoRandomBytes(refillLength) ||
|
||||
getAppPlusAndroidRandomBytes(refillLength) ||
|
||||
getAppPlusIosRandomBytes(refillLength);
|
||||
if (randomBytes) {
|
||||
randomBytePool += randomBytes;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从随机数池中按需提取字节串。
|
||||
*
|
||||
* @param {number} length 需要提取的字节长度
|
||||
* @returns {string} Forge 使用的二进制字符串
|
||||
*/
|
||||
function consumeRandomBytes(length) {
|
||||
if (randomBytePool.length < length) {
|
||||
refillRandomBytePool(length);
|
||||
}
|
||||
if (randomBytePool.length < length) {
|
||||
throw new Error("传输加密随机数池不足,且当前运行环境无法同步补充安全随机数");
|
||||
}
|
||||
const bytes = randomBytePool.slice(0, length);
|
||||
randomBytePool = randomBytePool.slice(length);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
forge.random.getBytesSync = function getBytesSync(length) {
|
||||
return consumeRandomBytes(length);
|
||||
};
|
||||
|
||||
forge.random.getBytes = function getBytes(length, callback) {
|
||||
const bytes = consumeRandomBytes(length);
|
||||
if (typeof callback === "function") {
|
||||
callback(null, bytes);
|
||||
}
|
||||
return bytes;
|
||||
};
|
||||
|
||||
/**
|
||||
* 向 Forge 随机数池预填充平台侧生成的随机字节。
|
||||
*
|
||||
* @param {string} bytes 待注入的二进制字符串
|
||||
* @returns {void}
|
||||
*/
|
||||
export function primeForgeRandomBytes(bytes) {
|
||||
randomBytePool += String(bytes || "");
|
||||
}
|
||||
|
||||
export default forge;
|
||||
72
shierfeng-fastapi-app/src/utils/upload.js
Normal 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
shierfeng-fastapi-app/src/utils/validate.js
Normal 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
shierfeng-fastapi-app/stylelint.config.mjs
Normal file
@ -0,0 +1,3 @@
|
||||
import { icebreaker } from "@icebreakers/stylelint-config";
|
||||
|
||||
export default icebreaker();
|
||||
53
shierfeng-fastapi-app/tailwind.config.ts
Normal 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
shierfeng-fastapi-app/tsconfig.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"outDir": "true",
|
||||
"jsx": "preserve",
|
||||
"lib": ["esnext", "dom"],
|
||||
"useDefineForClassFields": true,
|
||||
"baseUrl": ".",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"resolveJsonModule": true,
|
||||
"types": ["@dcloudio/types"],
|
||||
"allowJs": true,
|
||||
"strict": true,
|
||||
"sourceMap": true,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||
}
|
||||
70
shierfeng-fastapi-app/vite.config.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import uni from "@dcloudio/vite-plugin-uni";
|
||||
import { defineConfig } from "vite";
|
||||
import { UnifiedViteWeappTailwindcssPlugin } from "weapp-tailwindcss/vite";
|
||||
import { WeappTailwindcssDisabled } from "./platform";
|
||||
import postcssPlugins from "./postcss.config";
|
||||
|
||||
function patchNodeForgeMiniProgramRuntime() {
|
||||
return {
|
||||
name: "patch-node-forge-mini-program-runtime",
|
||||
enforce: "pre",
|
||||
transform(code: string, id: string) {
|
||||
if (id.includes("node-forge/lib/util.js")) {
|
||||
return {
|
||||
code: code.replace(
|
||||
/return typeof self === 'undefined' \? window : self;/,
|
||||
"return typeof globalThis !== 'undefined' ? globalThis : (typeof self === 'undefined' ? (typeof window === 'undefined' ? {} : window) : self);",
|
||||
),
|
||||
map: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (id.includes("node-forge/lib/random.js")) {
|
||||
return {
|
||||
code: code.replace(
|
||||
/var _crypto = globalScope\.crypto \|\| globalScope\.msCrypto;/,
|
||||
"var _crypto = (globalScope && (globalScope.crypto || globalScope.msCrypto)) || null;",
|
||||
),
|
||||
map: null,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(async () => {
|
||||
// 新版本的 unplugin-auto-import 改成了只有 esm 格式的产物,而 uni-app 目前必须 cjs 格式
|
||||
// 所以需要改成动态 import 的写法来进行引入
|
||||
// 详见 https://github.com/sonofmagic/uni-app-vite-vue3-tailwind-vscode-template/issues/29
|
||||
const { default: AutoImport } = await import("unplugin-auto-import/vite");
|
||||
return {
|
||||
// uvtw 一定要放在 uni 后面
|
||||
plugins: [
|
||||
uni(),
|
||||
patchNodeForgeMiniProgramRuntime(),
|
||||
UnifiedViteWeappTailwindcssPlugin({
|
||||
rem2rpx: true,
|
||||
disabled: WeappTailwindcssDisabled,
|
||||
}),
|
||||
AutoImport({
|
||||
imports: ["vue", "uni-app", "pinia"],
|
||||
dts: "./src/auto-imports.d.ts",
|
||||
}),
|
||||
],
|
||||
// 内联 postcss 注册 tailwindcss
|
||||
css: {
|
||||
postcss: {
|
||||
plugins: postcssPlugins,
|
||||
},
|
||||
// https://vitejs.dev/config/shared-options.html#css-preprocessoroptions
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
silenceDeprecations: ["legacy-js-api"],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
173
shierfeng-fastapi-backend/.env.dev
Normal file
@ -0,0 +1,173 @@
|
||||
# -------- 应用配置 --------
|
||||
# 应用运行环境
|
||||
APP_ENV = 'dev'
|
||||
# 应用名称
|
||||
APP_NAME = 'ShiErFeng-FastAPI'
|
||||
# 应用代理路径
|
||||
APP_ROOT_PATH = '/dev-api'
|
||||
# 应用主机
|
||||
APP_HOST = '0.0.0.0'
|
||||
# 应用端口
|
||||
APP_PORT = 9099
|
||||
# 应用版本
|
||||
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
|
||||
# 可信代理IP列表,多个值使用逗号分隔
|
||||
APP_TRUSTED_PROXY_IPS = '127.0.0.1,::1'
|
||||
# 可信代理跳数,单层Nginx代理通常为1
|
||||
APP_TRUSTED_PROXY_HOPS = 1
|
||||
|
||||
# -------- 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 = '127.0.0.1'
|
||||
# 数据库端口
|
||||
DB_PORT = 3306
|
||||
# 数据库用户名
|
||||
DB_USERNAME = 'root'
|
||||
# 数据库密码
|
||||
DB_PASSWORD = 'mysqlroot'
|
||||
# 数据库名称
|
||||
DB_DATABASE = 'shierfeng-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 = '127.0.0.1'
|
||||
# Redis端口
|
||||
REDIS_PORT = 6379
|
||||
# Redis用户名
|
||||
REDIS_USERNAME = ''
|
||||
# Redis密码
|
||||
REDIS_PASSWORD = ''
|
||||
# Redis数据库
|
||||
REDIS_DATABASE = 2
|
||||
|
||||
# -------- 日志配置 --------
|
||||
# 是否启用日志脱敏
|
||||
LOG_MASK_ENABLED = false
|
||||
# 日志脱敏占位符
|
||||
LOG_MASK_PLACEHOLDER = '******'
|
||||
# 以下三项为互补关系(非互斥):
|
||||
# 1) LOG_MASK_FIELDS:命中后执行全量脱敏
|
||||
# 2) LOG_PARTIAL_MASK_FIELDS:命中后执行部分脱敏
|
||||
# 3) LOG_CONFIG_SECRET_PATTERNS:按 configKey 关键词决定是否脱敏 configValue
|
||||
# 全量脱敏字段,多个值使用逗号分隔
|
||||
LOG_MASK_FIELDS = 'password,old_password,new_password,confirm_password,api_key,token,access_token,refresh_token,authorization,client_secret,secret,secret_key,private_key,private_key_pem,credential,credentials,sms_code,captcha_code,system_prompt'
|
||||
# 部分脱敏字段,多个值使用逗号分隔
|
||||
# 当前默认不对IP脱敏;如后期需要开启,可直接追加 ip,ipaddr,oper_ip,login_ip 并重启服务
|
||||
LOG_PARTIAL_MASK_FIELDS = 'phonenumber,phone,mobile,email'
|
||||
# 按配置键名识别敏感配置的关键词,多个值使用逗号分隔
|
||||
LOG_CONFIG_SECRET_PATTERNS = 'password,token,secret,key,private,credential,access,jwt,captcha,sms'
|
||||
# Redis Stream Key
|
||||
LOG_STREAM_KEY = 'log:stream'
|
||||
# Redis Stream 消费组名称
|
||||
LOG_STREAM_GROUP = 'log_aggregator'
|
||||
# Redis Stream 消费者名称前缀
|
||||
LOG_STREAM_CONSUMER_PREFIX = 'worker'
|
||||
# 每次读取的最大消息数量
|
||||
LOG_STREAM_BATCH_SIZE = 100
|
||||
# 阻塞读取等待时间(毫秒)
|
||||
LOG_STREAM_BLOCK_MS = 2000
|
||||
# Stream 最大长度(近似裁剪)
|
||||
LOG_STREAM_MAXLEN = 100000
|
||||
# Pending 回收最小空闲时间(毫秒)
|
||||
LOG_STREAM_CLAIM_IDLE_MS = 60000
|
||||
# Pending 回收检查间隔(毫秒)
|
||||
LOG_STREAM_CLAIM_INTERVAL_MS = 5000
|
||||
# 每次回收的最大消息数量
|
||||
LOG_STREAM_CLAIM_BATCH_SIZE = 100
|
||||
# 去重 Key 过期时间(秒)
|
||||
LOG_STREAM_DEDUP_TTL = 3600
|
||||
# 去重 Key 前缀
|
||||
LOG_STREAM_DEDUP_PREFIX = 'log:dedup'
|
||||
# stdout 输出是否为 JSON
|
||||
LOGURU_JSON = false
|
||||
# Loguru 最低输出级别
|
||||
LOGURU_LEVEL = 'INFO'
|
||||
# 是否输出到 stdout
|
||||
LOGURU_STDOUT = true
|
||||
# 是否启用文件日志
|
||||
LOG_FILE_ENABLED = true
|
||||
# 文件日志根目录
|
||||
LOG_FILE_BASE_DIR = 'logs'
|
||||
# 文件滚动策略
|
||||
LOGURU_ROTATION = '50MB'
|
||||
# 文件保留策略
|
||||
LOGURU_RETENTION = '30 days'
|
||||
# 文件压缩格式
|
||||
LOGURU_COMPRESSION = 'zip'
|
||||
# 实例标识(用于区分实例)
|
||||
LOG_INSTANCE_ID = 'dev'
|
||||
# 服务名称(用于统一标识服务)
|
||||
LOG_SERVICE_NAME = 'shierfeng-fastapi-backend'
|
||||
# Worker 标识(auto 自动生成)
|
||||
LOG_WORKER_ID = 'auto'
|
||||
|
||||
# -------- 传输层加解密配置 --------
|
||||
# 是否启用传输层加解密
|
||||
TRANSPORT_CRYPTO_ENABLED = false
|
||||
# 传输层加解密模式:off=关闭,optional=明文/密文兼容,required=命中接口强制加密
|
||||
TRANSPORT_CRYPTO_MODE = 'off'
|
||||
# 传输层加解密算法标识
|
||||
TRANSPORT_CRYPTO_ALGORITHM = 'RSA_OAEP_AES_256_GCM'
|
||||
# 当前启用的密钥版本标识
|
||||
TRANSPORT_CRYPTO_KID = 'default'
|
||||
# 传输层RSA位数,需与下方密钥对匹配
|
||||
TRANSPORT_CRYPTO_RSA_KEY_SIZE = 4096
|
||||
# 传输层公钥,默认提供一套可用示例值;团队使用时可按需替换
|
||||
TRANSPORT_CRYPTO_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAwA2ooWpsxLzIVMJp7Wcv\nvR0Bu8paFn8NVPpzz+wGpUlwP5DGK6pBGItsglNMZx56MSYMp3zVyqB95XUVZ6Ub\nQVyHJ6BXXIMs/BpzcHCbyXR/wWG+pKxQ8UEwaHVhK8X21xW1R0kzzwhgMw51unEs\nA81kskVlDdGeNyaOJg5QuofEErCfR1y0e+iqd1PBpIwdEY5L+BzvbVtyhsPz6dBu\n9YbEbKs7JvNle5vjc72ebbMIeGejHFQRYxihuamPCAEylj1qqpHk8U+r+3icxQsj\n8F/TySLuOy37iVWfD+5ikLyou4ZDI3hOYnIYHl194ZM5xVyOGBD/xdqZadXTLqL6\nSrldwZfZDBl3EGgRby8yJqO6SqGgyvyGWXZAaHoPMmF+quu/nUooqnp0fpl+LCqX\nhK67kbYaA9xJeHRaP04cg16imB7sBIXBqzkyZtkhC2BtlL0h5X7dRlAddrQ23z6d\nWdJA3qe3iBSSM8pmedv+Jgfau/PFam2051HqFxvJEh/jnc6rq1aIjL/d4Kk5imIa\nQ6acv+v5N6QVFptlcx6I7j8yZJ7WUHZlB1IxVqfPb69+985eknZyLul60gyu0kPG\nYUecypUu2wiNDBAErZlUxEujMWgJDFYCSonHxtwr88gDInjP0lvwd/OgqjDQ7hSo\nRYxPhTbwo68RJDpImBjn+YsCAwEAAQ==\n-----END PUBLIC KEY-----'
|
||||
# 传输层私钥,必须与上方公钥成对使用
|
||||
TRANSPORT_CRYPTO_PRIVATE_KEY = '-----BEGIN RSA PRIVATE KEY-----\n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\n-----END RSA PRIVATE KEY-----'
|
||||
# 历史密钥对配置,JSON数组格式
|
||||
TRANSPORT_CRYPTO_LEGACY_KEY_PAIRS = '[]'
|
||||
# 公钥缓存有效期(秒)
|
||||
TRANSPORT_CRYPTO_PUBLIC_KEY_TTL_SECONDS = 3600
|
||||
# 前端传输加密策略缓存有效期(秒)
|
||||
TRANSPORT_CRYPTO_FRONTEND_CONFIG_TTL_SECONDS = 300
|
||||
# GET/DELETE 请求参数加密后的最大 URL 长度
|
||||
TRANSPORT_CRYPTO_MAX_GET_URL_LENGTH = 4096
|
||||
# 请求时间窗允许偏差(秒)
|
||||
TRANSPORT_CRYPTO_CLOCK_SKEW_SECONDS = 120
|
||||
# 防重放随机数有效期(秒)
|
||||
TRANSPORT_CRYPTO_REPLAY_TTL_SECONDS = 300
|
||||
# 启用传输层加密的路径列表,多个值使用逗号分隔,留空表示默认全部启用
|
||||
TRANSPORT_CRYPTO_ENABLED_PATHS = ''
|
||||
# 强制要求传输层加密的路径列表,多个值使用逗号分隔
|
||||
TRANSPORT_CRYPTO_REQUIRED_PATHS = ''
|
||||
# 排除传输层加密的路径列表,多个值使用逗号分隔
|
||||
TRANSPORT_CRYPTO_EXCLUDE_PATHS = '/openapi.json,/docs,/docs/oauth2-redirect,/redoc,/transport/crypto/frontend-config,/transport/crypto/public-key,/common/download,/common/download/resource'
|
||||
173
shierfeng-fastapi-backend/.env.dockermy
Normal file
@ -0,0 +1,173 @@
|
||||
# -------- 应用配置 --------
|
||||
# 应用运行环境
|
||||
APP_ENV = 'prod'
|
||||
# 应用名称
|
||||
APP_NAME = 'ShiErFeng-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
|
||||
# 可信代理IP列表,多个值使用逗号分隔
|
||||
APP_TRUSTED_PROXY_IPS = '127.0.0.1,::1'
|
||||
# 可信代理跳数,单层Nginx代理通常为1
|
||||
APP_TRUSTED_PROXY_HOPS = 1
|
||||
|
||||
# -------- 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 = 'shierfeng-mysql'
|
||||
# 数据库端口
|
||||
DB_PORT = 3306
|
||||
# 数据库用户名
|
||||
DB_USERNAME = 'root'
|
||||
# 数据库密码
|
||||
DB_PASSWORD = 'root'
|
||||
# 数据库名称
|
||||
DB_DATABASE = 'shierfeng-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 = 'shierfeng-redis'
|
||||
# Redis端口
|
||||
REDIS_PORT = 6379
|
||||
# Redis用户名
|
||||
REDIS_USERNAME = ''
|
||||
# Redis密码
|
||||
REDIS_PASSWORD = ''
|
||||
# Redis数据库
|
||||
REDIS_DATABASE = 2
|
||||
|
||||
# -------- 日志配置 --------
|
||||
# 是否启用日志脱敏
|
||||
LOG_MASK_ENABLED = true
|
||||
# 日志脱敏占位符
|
||||
LOG_MASK_PLACEHOLDER = '******'
|
||||
# 以下三项为互补关系(非互斥):
|
||||
# 1) LOG_MASK_FIELDS:命中后执行全量脱敏
|
||||
# 2) LOG_PARTIAL_MASK_FIELDS:命中后执行部分脱敏
|
||||
# 3) LOG_CONFIG_SECRET_PATTERNS:按 configKey 关键词决定是否脱敏 configValue
|
||||
# 全量脱敏字段,多个值使用逗号分隔
|
||||
LOG_MASK_FIELDS = 'password,old_password,new_password,confirm_password,api_key,token,access_token,refresh_token,authorization,client_secret,secret,secret_key,private_key,private_key_pem,credential,credentials,sms_code,captcha_code,system_prompt'
|
||||
# 部分脱敏字段,多个值使用逗号分隔
|
||||
# 当前默认不对IP脱敏;如后期需要开启,可直接追加 ip,ipaddr,oper_ip,login_ip 并重启服务
|
||||
LOG_PARTIAL_MASK_FIELDS = 'phonenumber,phone,mobile,email'
|
||||
# 按配置键名识别敏感配置的关键词,多个值使用逗号分隔
|
||||
LOG_CONFIG_SECRET_PATTERNS = 'password,token,secret,key,private,credential,access,jwt,captcha,sms'
|
||||
# Redis Stream Key
|
||||
LOG_STREAM_KEY = 'log:stream'
|
||||
# Redis Stream 消费组名称
|
||||
LOG_STREAM_GROUP = 'log_aggregator'
|
||||
# Redis Stream 消费者名称前缀
|
||||
LOG_STREAM_CONSUMER_PREFIX = 'worker'
|
||||
# 每次读取的最大消息数量
|
||||
LOG_STREAM_BATCH_SIZE = 100
|
||||
# 阻塞读取等待时间(毫秒)
|
||||
LOG_STREAM_BLOCK_MS = 2000
|
||||
# Stream 最大长度(近似裁剪)
|
||||
LOG_STREAM_MAXLEN = 100000
|
||||
# Pending 回收最小空闲时间(毫秒)
|
||||
LOG_STREAM_CLAIM_IDLE_MS = 60000
|
||||
# Pending 回收检查间隔(毫秒)
|
||||
LOG_STREAM_CLAIM_INTERVAL_MS = 5000
|
||||
# 每次回收的最大消息数量
|
||||
LOG_STREAM_CLAIM_BATCH_SIZE = 100
|
||||
# 去重 Key 过期时间(秒)
|
||||
LOG_STREAM_DEDUP_TTL = 3600
|
||||
# 去重 Key 前缀
|
||||
LOG_STREAM_DEDUP_PREFIX = 'log:dedup'
|
||||
# stdout 输出是否为 JSON
|
||||
LOGURU_JSON = false
|
||||
# Loguru 最低输出级别
|
||||
LOGURU_LEVEL = 'INFO'
|
||||
# 是否输出到 stdout
|
||||
LOGURU_STDOUT = true
|
||||
# 是否启用文件日志
|
||||
LOG_FILE_ENABLED = true
|
||||
# 文件日志根目录
|
||||
LOG_FILE_BASE_DIR = 'logs'
|
||||
# 文件滚动策略
|
||||
LOGURU_ROTATION = '50MB'
|
||||
# 文件保留策略
|
||||
LOGURU_RETENTION = '30 days'
|
||||
# 文件压缩格式
|
||||
LOGURU_COMPRESSION = 'zip'
|
||||
# 实例标识(用于区分实例)
|
||||
LOG_INSTANCE_ID = 'dockermy'
|
||||
# 服务名称(用于统一标识服务)
|
||||
LOG_SERVICE_NAME = 'shierfeng-fastapi-backend'
|
||||
# Worker 标识(auto 自动生成)
|
||||
LOG_WORKER_ID = 'auto'
|
||||
|
||||
# -------- 传输层加解密配置 --------
|
||||
# 是否启用传输层加解密
|
||||
TRANSPORT_CRYPTO_ENABLED = true
|
||||
# 传输层加解密模式:off=关闭,optional=明文/密文兼容,required=命中接口强制加密
|
||||
TRANSPORT_CRYPTO_MODE = 'optional'
|
||||
# 传输层加解密算法标识
|
||||
TRANSPORT_CRYPTO_ALGORITHM = 'RSA_OAEP_AES_256_GCM'
|
||||
# 当前启用的密钥版本标识
|
||||
TRANSPORT_CRYPTO_KID = 'default'
|
||||
# 传输层RSA位数,需与下方密钥对匹配
|
||||
TRANSPORT_CRYPTO_RSA_KEY_SIZE = 4096
|
||||
# 传输层公钥,默认提供一套可用示例值;Docker 部署前请替换为正式密钥
|
||||
TRANSPORT_CRYPTO_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAwA2ooWpsxLzIVMJp7Wcv\nvR0Bu8paFn8NVPpzz+wGpUlwP5DGK6pBGItsglNMZx56MSYMp3zVyqB95XUVZ6Ub\nQVyHJ6BXXIMs/BpzcHCbyXR/wWG+pKxQ8UEwaHVhK8X21xW1R0kzzwhgMw51unEs\nA81kskVlDdGeNyaOJg5QuofEErCfR1y0e+iqd1PBpIwdEY5L+BzvbVtyhsPz6dBu\n9YbEbKs7JvNle5vjc72ebbMIeGejHFQRYxihuamPCAEylj1qqpHk8U+r+3icxQsj\n8F/TySLuOy37iVWfD+5ikLyou4ZDI3hOYnIYHl194ZM5xVyOGBD/xdqZadXTLqL6\nSrldwZfZDBl3EGgRby8yJqO6SqGgyvyGWXZAaHoPMmF+quu/nUooqnp0fpl+LCqX\nhK67kbYaA9xJeHRaP04cg16imB7sBIXBqzkyZtkhC2BtlL0h5X7dRlAddrQ23z6d\nWdJA3qe3iBSSM8pmedv+Jgfau/PFam2051HqFxvJEh/jnc6rq1aIjL/d4Kk5imIa\nQ6acv+v5N6QVFptlcx6I7j8yZJ7WUHZlB1IxVqfPb69+985eknZyLul60gyu0kPG\nYUecypUu2wiNDBAErZlUxEujMWgJDFYCSonHxtwr88gDInjP0lvwd/OgqjDQ7hSo\nRYxPhTbwo68RJDpImBjn+YsCAwEAAQ==\n-----END PUBLIC KEY-----'
|
||||
# 传输层私钥,必须与上方公钥成对使用;Docker 部署前请替换为正式密钥
|
||||
TRANSPORT_CRYPTO_PRIVATE_KEY = '-----BEGIN RSA PRIVATE KEY-----\n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\n-----END RSA PRIVATE KEY-----'
|
||||
# 历史密钥对配置,JSON数组格式,密钥轮换说明见 docs/transport_crypto_deployment.md
|
||||
TRANSPORT_CRYPTO_LEGACY_KEY_PAIRS = '[]'
|
||||
# 公钥缓存有效期(秒)
|
||||
TRANSPORT_CRYPTO_PUBLIC_KEY_TTL_SECONDS = 3600
|
||||
# 前端传输加密策略缓存有效期(秒)
|
||||
TRANSPORT_CRYPTO_FRONTEND_CONFIG_TTL_SECONDS = 300
|
||||
# GET/DELETE 请求参数加密后的最大 URL 长度
|
||||
TRANSPORT_CRYPTO_MAX_GET_URL_LENGTH = 4096
|
||||
# 请求时间窗允许偏差(秒)
|
||||
TRANSPORT_CRYPTO_CLOCK_SKEW_SECONDS = 120
|
||||
# 防重放随机数有效期(秒)
|
||||
TRANSPORT_CRYPTO_REPLAY_TTL_SECONDS = 300
|
||||
# 启用传输层加密的路径列表,多个值使用逗号分隔,留空表示默认全部启用
|
||||
TRANSPORT_CRYPTO_ENABLED_PATHS = ''
|
||||
# 强制要求传输层加密的路径列表,多个值使用逗号分隔
|
||||
TRANSPORT_CRYPTO_REQUIRED_PATHS = ''
|
||||
# 排除传输层加密的路径列表,多个值使用逗号分隔
|
||||
TRANSPORT_CRYPTO_EXCLUDE_PATHS = '/openapi.json,/docs,/docs/oauth2-redirect,/redoc,/transport/crypto/frontend-config,/transport/crypto/public-key,/common/download,/common/download/resource'
|
||||
173
shierfeng-fastapi-backend/.env.dockerpg
Normal file
@ -0,0 +1,173 @@
|
||||
# -------- 应用配置 --------
|
||||
# 应用运行环境
|
||||
APP_ENV = 'prod'
|
||||
# 应用名称
|
||||
APP_NAME = 'ShiErFeng-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
|
||||
# 可信代理IP列表,多个值使用逗号分隔
|
||||
APP_TRUSTED_PROXY_IPS = '127.0.0.1,::1'
|
||||
# 可信代理跳数,单层Nginx代理通常为1
|
||||
APP_TRUSTED_PROXY_HOPS = 1
|
||||
|
||||
# -------- 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 = 'shierfeng-pg'
|
||||
# 数据库端口
|
||||
DB_PORT = 5432
|
||||
# 数据库用户名
|
||||
DB_USERNAME = 'postgres'
|
||||
# 数据库密码
|
||||
DB_PASSWORD = 'root'
|
||||
# 数据库名称
|
||||
DB_DATABASE = 'shierfeng-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 = 'shierfeng-redis'
|
||||
# Redis端口
|
||||
REDIS_PORT = 6379
|
||||
# Redis用户名
|
||||
REDIS_USERNAME = ''
|
||||
# Redis密码
|
||||
REDIS_PASSWORD = ''
|
||||
# Redis数据库
|
||||
REDIS_DATABASE = 2
|
||||
|
||||
# -------- 日志配置 --------
|
||||
# 是否启用日志脱敏
|
||||
LOG_MASK_ENABLED = true
|
||||
# 日志脱敏占位符
|
||||
LOG_MASK_PLACEHOLDER = '******'
|
||||
# 以下三项为互补关系(非互斥):
|
||||
# 1) LOG_MASK_FIELDS:命中后执行全量脱敏
|
||||
# 2) LOG_PARTIAL_MASK_FIELDS:命中后执行部分脱敏
|
||||
# 3) LOG_CONFIG_SECRET_PATTERNS:按 configKey 关键词决定是否脱敏 configValue
|
||||
# 全量脱敏字段,多个值使用逗号分隔
|
||||
LOG_MASK_FIELDS = 'password,old_password,new_password,confirm_password,api_key,token,access_token,refresh_token,authorization,client_secret,secret,secret_key,private_key,private_key_pem,credential,credentials,sms_code,captcha_code,system_prompt'
|
||||
# 部分脱敏字段,多个值使用逗号分隔
|
||||
# 当前默认不对IP脱敏;如后期需要开启,可直接追加 ip,ipaddr,oper_ip,login_ip 并重启服务
|
||||
LOG_PARTIAL_MASK_FIELDS = 'phonenumber,phone,mobile,email'
|
||||
# 按配置键名识别敏感配置的关键词,多个值使用逗号分隔
|
||||
LOG_CONFIG_SECRET_PATTERNS = 'password,token,secret,key,private,credential,access,jwt,captcha,sms'
|
||||
# Redis Stream Key
|
||||
LOG_STREAM_KEY = 'log:stream'
|
||||
# Redis Stream 消费组名称
|
||||
LOG_STREAM_GROUP = 'log_aggregator'
|
||||
# Redis Stream 消费者名称前缀
|
||||
LOG_STREAM_CONSUMER_PREFIX = 'worker'
|
||||
# 每次读取的最大消息数量
|
||||
LOG_STREAM_BATCH_SIZE = 100
|
||||
# 阻塞读取等待时间(毫秒)
|
||||
LOG_STREAM_BLOCK_MS = 2000
|
||||
# Stream 最大长度(近似裁剪)
|
||||
LOG_STREAM_MAXLEN = 100000
|
||||
# Pending 回收最小空闲时间(毫秒)
|
||||
LOG_STREAM_CLAIM_IDLE_MS = 60000
|
||||
# Pending 回收检查间隔(毫秒)
|
||||
LOG_STREAM_CLAIM_INTERVAL_MS = 5000
|
||||
# 每次回收的最大消息数量
|
||||
LOG_STREAM_CLAIM_BATCH_SIZE = 100
|
||||
# 去重 Key 过期时间(秒)
|
||||
LOG_STREAM_DEDUP_TTL = 3600
|
||||
# 去重 Key 前缀
|
||||
LOG_STREAM_DEDUP_PREFIX = 'log:dedup'
|
||||
# stdout 输出是否为 JSON
|
||||
LOGURU_JSON = false
|
||||
# Loguru 最低输出级别
|
||||
LOGURU_LEVEL = 'INFO'
|
||||
# 是否输出到 stdout
|
||||
LOGURU_STDOUT = true
|
||||
# 是否启用文件日志
|
||||
LOG_FILE_ENABLED = true
|
||||
# 文件日志根目录
|
||||
LOG_FILE_BASE_DIR = 'logs'
|
||||
# 文件滚动策略
|
||||
LOGURU_ROTATION = '50MB'
|
||||
# 文件保留策略
|
||||
LOGURU_RETENTION = '30 days'
|
||||
# 文件压缩格式
|
||||
LOGURU_COMPRESSION = 'zip'
|
||||
# 实例标识(用于区分实例)
|
||||
LOG_INSTANCE_ID = 'dockerpg'
|
||||
# 服务名称(用于统一标识服务)
|
||||
LOG_SERVICE_NAME = 'shierfeng-fastapi-backend'
|
||||
# Worker 标识(auto 自动生成)
|
||||
LOG_WORKER_ID = 'auto'
|
||||
|
||||
# -------- 传输层加解密配置 --------
|
||||
# 是否启用传输层加解密
|
||||
TRANSPORT_CRYPTO_ENABLED = true
|
||||
# 传输层加解密模式:off=关闭,optional=明文/密文兼容,required=命中接口强制加密
|
||||
TRANSPORT_CRYPTO_MODE = 'optional'
|
||||
# 传输层加解密算法标识
|
||||
TRANSPORT_CRYPTO_ALGORITHM = 'RSA_OAEP_AES_256_GCM'
|
||||
# 当前启用的密钥版本标识
|
||||
TRANSPORT_CRYPTO_KID = 'default'
|
||||
# 传输层RSA位数,需与下方密钥对匹配
|
||||
TRANSPORT_CRYPTO_RSA_KEY_SIZE = 4096
|
||||
# 传输层公钥,默认提供一套可用示例值;Docker 部署前请替换为正式密钥
|
||||
TRANSPORT_CRYPTO_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAwA2ooWpsxLzIVMJp7Wcv\nvR0Bu8paFn8NVPpzz+wGpUlwP5DGK6pBGItsglNMZx56MSYMp3zVyqB95XUVZ6Ub\nQVyHJ6BXXIMs/BpzcHCbyXR/wWG+pKxQ8UEwaHVhK8X21xW1R0kzzwhgMw51unEs\nA81kskVlDdGeNyaOJg5QuofEErCfR1y0e+iqd1PBpIwdEY5L+BzvbVtyhsPz6dBu\n9YbEbKs7JvNle5vjc72ebbMIeGejHFQRYxihuamPCAEylj1qqpHk8U+r+3icxQsj\n8F/TySLuOy37iVWfD+5ikLyou4ZDI3hOYnIYHl194ZM5xVyOGBD/xdqZadXTLqL6\nSrldwZfZDBl3EGgRby8yJqO6SqGgyvyGWXZAaHoPMmF+quu/nUooqnp0fpl+LCqX\nhK67kbYaA9xJeHRaP04cg16imB7sBIXBqzkyZtkhC2BtlL0h5X7dRlAddrQ23z6d\nWdJA3qe3iBSSM8pmedv+Jgfau/PFam2051HqFxvJEh/jnc6rq1aIjL/d4Kk5imIa\nQ6acv+v5N6QVFptlcx6I7j8yZJ7WUHZlB1IxVqfPb69+985eknZyLul60gyu0kPG\nYUecypUu2wiNDBAErZlUxEujMWgJDFYCSonHxtwr88gDInjP0lvwd/OgqjDQ7hSo\nRYxPhTbwo68RJDpImBjn+YsCAwEAAQ==\n-----END PUBLIC KEY-----'
|
||||
# 传输层私钥,必须与上方公钥成对使用;Docker 部署前请替换为正式密钥
|
||||
TRANSPORT_CRYPTO_PRIVATE_KEY = '-----BEGIN RSA PRIVATE KEY-----\n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\n-----END RSA PRIVATE KEY-----'
|
||||
# 历史密钥对配置,JSON数组格式,密钥轮换说明见 docs/transport_crypto_deployment.md
|
||||
TRANSPORT_CRYPTO_LEGACY_KEY_PAIRS = '[]'
|
||||
# 公钥缓存有效期(秒)
|
||||
TRANSPORT_CRYPTO_PUBLIC_KEY_TTL_SECONDS = 3600
|
||||
# 前端传输加密策略缓存有效期(秒)
|
||||
TRANSPORT_CRYPTO_FRONTEND_CONFIG_TTL_SECONDS = 300
|
||||
# GET/DELETE 请求参数加密后的最大 URL 长度
|
||||
TRANSPORT_CRYPTO_MAX_GET_URL_LENGTH = 4096
|
||||
# 请求时间窗允许偏差(秒)
|
||||
TRANSPORT_CRYPTO_CLOCK_SKEW_SECONDS = 120
|
||||
# 防重放随机数有效期(秒)
|
||||
TRANSPORT_CRYPTO_REPLAY_TTL_SECONDS = 300
|
||||
# 启用传输层加密的路径列表,多个值使用逗号分隔,留空表示默认全部启用
|
||||
TRANSPORT_CRYPTO_ENABLED_PATHS = ''
|
||||
# 强制要求传输层加密的路径列表,多个值使用逗号分隔
|
||||
TRANSPORT_CRYPTO_REQUIRED_PATHS = ''
|
||||
# 排除传输层加密的路径列表,多个值使用逗号分隔
|
||||
TRANSPORT_CRYPTO_EXCLUDE_PATHS = '/openapi.json,/docs,/docs/oauth2-redirect,/redoc,/transport/crypto/frontend-config,/transport/crypto/public-key,/common/download,/common/download/resource'
|
||||
173
shierfeng-fastapi-backend/.env.prod
Normal file
@ -0,0 +1,173 @@
|
||||
# -------- 应用配置 --------
|
||||
# 应用运行环境
|
||||
APP_ENV = 'prod'
|
||||
# 应用名称
|
||||
APP_NAME = 'ShiErFeng-FastAPI'
|
||||
# 应用代理路径
|
||||
APP_ROOT_PATH = '/prod-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
|
||||
# 可信代理IP列表,多个值使用逗号分隔
|
||||
APP_TRUSTED_PROXY_IPS = '127.0.0.1,::1'
|
||||
# 可信代理跳数,单层Nginx代理通常为1
|
||||
APP_TRUSTED_PROXY_HOPS = 1
|
||||
|
||||
# -------- 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 = '127.0.0.1'
|
||||
# 数据库端口
|
||||
DB_PORT = 3306
|
||||
# 数据库用户名
|
||||
DB_USERNAME = 'root'
|
||||
# 数据库密码
|
||||
DB_PASSWORD = 'root'
|
||||
# 数据库名称
|
||||
DB_DATABASE = 'shierfeng-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 = '127.0.0.1'
|
||||
# Redis端口
|
||||
REDIS_PORT = 6379
|
||||
# Redis用户名
|
||||
REDIS_USERNAME = ''
|
||||
# Redis密码
|
||||
REDIS_PASSWORD = ''
|
||||
# Redis数据库
|
||||
REDIS_DATABASE = 2
|
||||
|
||||
# -------- 日志配置 --------
|
||||
# 是否启用日志脱敏
|
||||
LOG_MASK_ENABLED = true
|
||||
# 日志脱敏占位符
|
||||
LOG_MASK_PLACEHOLDER = '******'
|
||||
# 以下三项为互补关系(非互斥):
|
||||
# 1) LOG_MASK_FIELDS:命中后执行全量脱敏
|
||||
# 2) LOG_PARTIAL_MASK_FIELDS:命中后执行部分脱敏
|
||||
# 3) LOG_CONFIG_SECRET_PATTERNS:按 configKey 关键词决定是否脱敏 configValue
|
||||
# 全量脱敏字段,多个值使用逗号分隔
|
||||
LOG_MASK_FIELDS = 'password,old_password,new_password,confirm_password,api_key,token,access_token,refresh_token,authorization,client_secret,secret,secret_key,private_key,private_key_pem,credential,credentials,sms_code,captcha_code,system_prompt'
|
||||
# 部分脱敏字段,多个值使用逗号分隔
|
||||
# 当前默认不对IP脱敏;如后期需要开启,可直接追加 ip,ipaddr,oper_ip,login_ip 并重启服务
|
||||
LOG_PARTIAL_MASK_FIELDS = 'phonenumber,phone,mobile,email'
|
||||
# 按配置键名识别敏感配置的关键词,多个值使用逗号分隔
|
||||
LOG_CONFIG_SECRET_PATTERNS = 'password,token,secret,key,private,credential,access,jwt,captcha,sms'
|
||||
# Redis Stream Key
|
||||
LOG_STREAM_KEY = 'log:stream'
|
||||
# Redis Stream 消费组名称
|
||||
LOG_STREAM_GROUP = 'log_aggregator'
|
||||
# Redis Stream 消费者名称前缀
|
||||
LOG_STREAM_CONSUMER_PREFIX = 'worker'
|
||||
# 每次读取的最大消息数量
|
||||
LOG_STREAM_BATCH_SIZE = 100
|
||||
# 阻塞读取等待时间(毫秒)
|
||||
LOG_STREAM_BLOCK_MS = 2000
|
||||
# Stream 最大长度(近似裁剪)
|
||||
LOG_STREAM_MAXLEN = 100000
|
||||
# Pending 回收最小空闲时间(毫秒)
|
||||
LOG_STREAM_CLAIM_IDLE_MS = 60000
|
||||
# Pending 回收检查间隔(毫秒)
|
||||
LOG_STREAM_CLAIM_INTERVAL_MS = 5000
|
||||
# 每次回收的最大消息数量
|
||||
LOG_STREAM_CLAIM_BATCH_SIZE = 100
|
||||
# 去重 Key 过期时间(秒)
|
||||
LOG_STREAM_DEDUP_TTL = 3600
|
||||
# 去重 Key 前缀
|
||||
LOG_STREAM_DEDUP_PREFIX = 'log:dedup'
|
||||
# stdout 输出是否为 JSON
|
||||
LOGURU_JSON = false
|
||||
# Loguru 最低输出级别
|
||||
LOGURU_LEVEL = 'INFO'
|
||||
# 是否输出到 stdout
|
||||
LOGURU_STDOUT = true
|
||||
# 是否启用文件日志
|
||||
LOG_FILE_ENABLED = true
|
||||
# 文件日志根目录
|
||||
LOG_FILE_BASE_DIR = 'logs'
|
||||
# 文件滚动策略
|
||||
LOGURU_ROTATION = '50MB'
|
||||
# 文件保留策略
|
||||
LOGURU_RETENTION = '30 days'
|
||||
# 文件压缩格式
|
||||
LOGURU_COMPRESSION = 'zip'
|
||||
# 实例标识(用于区分实例)
|
||||
LOG_INSTANCE_ID = 'prod'
|
||||
# 服务名称(用于统一标识服务)
|
||||
LOG_SERVICE_NAME = 'shierfeng-fastapi-backend'
|
||||
# Worker 标识(auto 自动生成)
|
||||
LOG_WORKER_ID = 'auto'
|
||||
|
||||
# -------- 传输层加解密配置 --------
|
||||
# 是否启用传输层加解密
|
||||
TRANSPORT_CRYPTO_ENABLED = true
|
||||
# 传输层加解密模式:off=关闭,optional=明文/密文兼容,required=命中接口强制加密
|
||||
TRANSPORT_CRYPTO_MODE = 'optional'
|
||||
# 传输层加解密算法标识
|
||||
TRANSPORT_CRYPTO_ALGORITHM = 'RSA_OAEP_AES_256_GCM'
|
||||
# 当前启用的密钥版本标识
|
||||
TRANSPORT_CRYPTO_KID = 'default'
|
||||
# 传输层RSA位数,需与下方密钥对匹配
|
||||
TRANSPORT_CRYPTO_RSA_KEY_SIZE = 4096
|
||||
# 传输层公钥,默认提供一套可用示例值;生产部署前请替换为正式密钥
|
||||
TRANSPORT_CRYPTO_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAwA2ooWpsxLzIVMJp7Wcv\nvR0Bu8paFn8NVPpzz+wGpUlwP5DGK6pBGItsglNMZx56MSYMp3zVyqB95XUVZ6Ub\nQVyHJ6BXXIMs/BpzcHCbyXR/wWG+pKxQ8UEwaHVhK8X21xW1R0kzzwhgMw51unEs\nA81kskVlDdGeNyaOJg5QuofEErCfR1y0e+iqd1PBpIwdEY5L+BzvbVtyhsPz6dBu\n9YbEbKs7JvNle5vjc72ebbMIeGejHFQRYxihuamPCAEylj1qqpHk8U+r+3icxQsj\n8F/TySLuOy37iVWfD+5ikLyou4ZDI3hOYnIYHl194ZM5xVyOGBD/xdqZadXTLqL6\nSrldwZfZDBl3EGgRby8yJqO6SqGgyvyGWXZAaHoPMmF+quu/nUooqnp0fpl+LCqX\nhK67kbYaA9xJeHRaP04cg16imB7sBIXBqzkyZtkhC2BtlL0h5X7dRlAddrQ23z6d\nWdJA3qe3iBSSM8pmedv+Jgfau/PFam2051HqFxvJEh/jnc6rq1aIjL/d4Kk5imIa\nQ6acv+v5N6QVFptlcx6I7j8yZJ7WUHZlB1IxVqfPb69+985eknZyLul60gyu0kPG\nYUecypUu2wiNDBAErZlUxEujMWgJDFYCSonHxtwr88gDInjP0lvwd/OgqjDQ7hSo\nRYxPhTbwo68RJDpImBjn+YsCAwEAAQ==\n-----END PUBLIC KEY-----'
|
||||
# 传输层私钥,必须与上方公钥成对使用;生产部署前请替换为正式密钥
|
||||
TRANSPORT_CRYPTO_PRIVATE_KEY = '-----BEGIN RSA PRIVATE KEY-----\nMIIJKAIBAAKCAgEAwA2ooWpsxLzIVMJp7WcvvR0Bu8paFn8NVPpzz+wGpUlwP5DG\nK6pBGItsglNMZx56MSYMp3zVyqB95XUVZ6UbQVyHJ6BXXIMs/BpzcHCbyXR/wWG+\npKxQ8UEwaHVhK8X21xW1R0kzzwhgMw51unEsA81kskVlDdGeNyaOJg5QuofEErCf\nR1y0e+iqd1PBpIwdEY5L+BzvbVtyhsPz6dBu9YbEbKs7JvNle5vjc72ebbMIeGej\nHFQRYxihuamPCAEylj1qqpHk8U+r+3icxQsj8F/TySLuOy37iVWfD+5ikLyou4ZD\nI3hOYnIYHl194ZM5xVyOGBD/xdqZadXTLqL6SrldwZfZDBl3EGgRby8yJqO6SqGg\nyvyGWXZAaHoPMmF+quu/nUooqnp0fpl+LCqXhK67kbYaA9xJeHRaP04cg16imB7s\nBIXBqzkyZtkhC2BtlL0h5X7dRlAddrQ23z6dWdJA3qe3iBSSM8pmedv+Jgfau/PF\nam2051HqFxvJEh/jnc6rq1aIjL/d4Kk5imIaQ6acv+v5N6QVFptlcx6I7j8yZJ7W\nUHZlB1IxVqfPb69+985eknZyLul60gyu0kPGYUecypUu2wiNDBAErZlUxEujMWgJ\nDFYCSonHxtwr88gDInjP0lvwd/OgqjDQ7hSoRYxPhTbwo68RJDpImBjn+YsCAwEA\nAQKCAgEAtH7hwjp8WPY3rPk+hqEYy7psO6q0ujnUNM5hc8MWh9caSJNuu/D51vCK\nHX3d63hITNK+x9ZaM2Jcj/9XC56vm+EzILngARFDgPf3EHC06UO1IfEUND3CoMqs\njS/sEDCwiXpccc/JgkUT7EgluwDL5hLuFWGE1NTxxlFU/U0s7/HTA+G9AFuPQHjt\nZNJxxozscOh0W44OM4/jdebJ4TBEaDqtdpgDMttDwEVFIizOrbi6ODbmpCQaZzDq\nJczGoTZG7c5w5jfRnD1NnDzB0apjr3DQYVOT5EiWA39Vy6o2NmMojX1ksfxKZtll\n83vYZ91vSR5waQvo5nFJB5+vJ0CCy4BZaV9/gQBP/OSElELfD0bGYidO7NURAJMW\njGc2B6uIog8BZ3ix02FO/30i5rWeDWM7McLol8+IP0Qcg473YvrCgmRZpprdc9HT\nHnB6BgIwu0ZBoP7CxW7pBm0ApwmOf7OvUdAu68kFQ4KxABuv+Qegu0vSsktBtmIs\ngUDzu/ekoayyOvFHVwVTd4QKOSa2gXb3TS7PP22j6QLL1OPy2fK2NBW0xrRDIgtw\nSx3Yo0dxCYkxJWz1nQYsdIjZbz2/xQPZZQ5yNeePwxaOKyDuA50elN86I9RR2gjL\nh9QMrvPG7DOlgmBG9+JsydQZf00G+x45Su/1fydA09X3AFtZLkkCggEBAOj8NY/x\n3G3/1qAkrcQYLwB0mjLNkgKchRIP8NMildk0nSub5SRUxleW2wp0kM0WqkFTXLMa\nFR0qCRjiyPOtBBZxLobIfGc6568LTKfg4N7VsCeAcTjFjSOE57iX5XzwhTy6ZCKe\nZFGFWfhj/a9lNQHneTcklDHq/oHjLJQ1FacbWNycIOX2Hk7SZKyJvyMutAem3BI+\nm5SLyj5648N2ylmz6f3eaTn8CyNCrUKWnPriQZfV0XF/Aalh4sq/I3fI6pqnYkHK\nuSoG9YHujGXKN2iOV/0krCiGj94l7JqvKGs4ZRoZdD+s1EugGD7qBK1wGATuEseg\nR6mS74ylgD88JPcCggEBANMGWrqhHOAtGeIewLbNob54+nqdwU/O+zk0RcTZErcD\naRZMTLPRlpdbgWbNgdfjFnbUx0WmIeTIboD2MaW5KOnDfV/x+eRhGtEmtNRvHA/o\nHK6uYzok7Ekm7oc298CYrfRbEWVx3m4+mHmU+rsvBeof664zpEEIsSwm+NaAnjz8\nru0U8xw8KWLB8F/Q4J4p4D0yVKCmAXmrrTgEYm1H355JlAlmiu6t6YPS2Xd+gNFD\ntWBsQXYnxEZyPhoD1Q4SQjQmQMEN7+lIHm8f/nMwwUkAd3y6D1w/Uu81Gqa/9eAh\nz3woK++tj/U1CMMTWXJumIyJdH28CWQJa9VdJBoFbw0CggEAdIBhPDhh1DNhHDUb\nGvpIzn5/+LVotJuVwwFrl/gsXC9+BCdxPmiRwYyyvRiqq5MQ0ZegvAJn2myBngsR\nFyBF8f7omAc6hdgjsBkDXNMLPwI1kpscgpnuSHuV720tGPugdExz9Ael/EmlQpql\naQY/qlMX/uXwoMF0QHjbesTMrgHzvmTn4nXek+WK5+f9Rtd8uHLMiub5nx8Do5iJ\nudz1tENN119W2OLaougdgTWVC7MFv6nwkENFDnDfGijX0HcMMQdQD7wSORy/uRgS\n5ndmm4bKItAqsh0PZbMrC/JYUL0jeRiPU6PViHdmiFc2vY9Ww2hUxbO1Aetyk67S\nYUxu4wKCAQAvSOQ5n3JV9SUwms156LfOc1NE+GZhmLKITeM4SZ+87IG2omAphtQk\nlDd5rqDinBrjg6gnPiOoHRVNxly3krbNMqW8Qv9Iok2dAfxRAZibI9qRdbf8Rlu/\nxH58Q9/eAxgvgdxzC8HYmCQYUj6ghNfhb0ejICU1AVqZ1x25CtbqOYCE1UXoVL69\nR1GyVp6OMjnx3H5EBQ6dkc9dlvXrIMjFshz+wkBtXQDPLgbWlL0OpwpUUc2VSTHc\nyyqJL8Skw4icINovqAzTC/rt0ZB1hT46OmWLyDE67WGLAi43oRnaIBla67F0okJ4\nomqVM5e+YXPDQeWdau37wXStOZKmVgNdAoIBADojGo0h9mBgnr+u1oYB+mKx6LVD\n3TTy45IB1ikqprBnSjgXNbQycsTbl+qDo5ge+KqlPNk6Scvn0L7k43/VB9Y5qK2s\nBgxa1KgdeC5WUNU0rs/1UKIODA5SlWIk6JqekiAE+glljVZ5E53l93gPL3uRcvr5\nSD5CPe+qqoBh9nYIRJDogP0e2xV11EuE10j1WxuLkHL5//hePGPpgnb0/rZkP+vb\nkzwz1fTv88kQDnRk6uIe0L78iFTpEwAnlDmuMJ4KafKnujaR5VVCoI7bwryCp4+f\n8zyU7ZpKo/2EE8bYmxHMDPiYByJxCwAbm2Xn7Kw4H/17MLJOxg8685A8jyA=\n-----END RSA PRIVATE KEY-----'
|
||||
# 历史密钥对配置,JSON数组格式,密钥轮换说明见 docs/transport_crypto_deployment.md
|
||||
TRANSPORT_CRYPTO_LEGACY_KEY_PAIRS = '[]'
|
||||
# 公钥缓存有效期(秒)
|
||||
TRANSPORT_CRYPTO_PUBLIC_KEY_TTL_SECONDS = 3600
|
||||
# 前端传输加密策略缓存有效期(秒)
|
||||
TRANSPORT_CRYPTO_FRONTEND_CONFIG_TTL_SECONDS = 300
|
||||
# GET/DELETE 请求参数加密后的最大 URL 长度
|
||||
TRANSPORT_CRYPTO_MAX_GET_URL_LENGTH = 4096
|
||||
# 请求时间窗允许偏差(秒)
|
||||
TRANSPORT_CRYPTO_CLOCK_SKEW_SECONDS = 120
|
||||
# 防重放随机数有效期(秒)
|
||||
TRANSPORT_CRYPTO_REPLAY_TTL_SECONDS = 300
|
||||
# 启用传输层加密的路径列表,多个值使用逗号分隔,留空表示默认全部启用
|
||||
TRANSPORT_CRYPTO_ENABLED_PATHS = ''
|
||||
# 强制要求传输层加密的路径列表,多个值使用逗号分隔
|
||||
TRANSPORT_CRYPTO_REQUIRED_PATHS = ''
|
||||
# 排除传输层加密的路径列表,多个值使用逗号分隔
|
||||
TRANSPORT_CRYPTO_EXCLUDE_PATHS = '/openapi.json,/docs,/docs/oauth2-redirect,/redoc,/transport/crypto/frontend-config,/transport/crypto/public-key,/common/download,/common/download/resource'
|
||||
14
shierfeng-fastapi-backend/Dockerfile.my
Normal 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 ["shierfeng", "app", "run", "--env=dockermy"]
|
||||
14
shierfeng-fastapi-backend/Dockerfile.pg
Normal 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 ["shierfeng", "app", "run", "--env=dockerpg"]
|
||||
153
shierfeng-fastapi-backend/alembic.ini
Normal 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
shierfeng-fastapi-backend/alembic/README
Normal file
@ -0,0 +1 @@
|
||||
Generic single-database configuration with an async dbapi.
|
||||
135
shierfeng-fastapi-backend/alembic/env.py
Normal 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/shierfeng-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()
|
||||
28
shierfeng-fastapi-backend/alembic/script.py.mako
Normal file
@ -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"}
|
||||
18
shierfeng-fastapi-backend/app.py
Normal file
@ -0,0 +1,18 @@
|
||||
import uvicorn
|
||||
|
||||
from config.env import AppConfig
|
||||
from server import create_app
|
||||
|
||||
if __name__ != '__main__':
|
||||
app = create_app()
|
||||
|
||||
if __name__ == '__main__':
|
||||
uvicorn.run(
|
||||
app='server:create_app',
|
||||
host=AppConfig.app_host,
|
||||
port=AppConfig.app_port,
|
||||
root_path=AppConfig.app_root_path,
|
||||
reload=AppConfig.app_reload,
|
||||
workers=AppConfig.app_workers,
|
||||
factory=True,
|
||||
)
|
||||