初始提交

This commit is contained in:
焦龙言 2026-07-24 09:43:10 +08:00
commit b5b4721456
793 changed files with 116305 additions and 0 deletions

10
.idea/.gitignore vendored Normal file
View 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
View 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>

View 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>

View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,168 @@
# ShiErFeng
基于 Vue3 与 FastAPI 的前后端分离快速开发框架。
## 平台简介
施尔丰是一套全部开源的快速开发平台,供个人及企业免费使用。
* 前端采用Vue3、Element Plus。
* 移动端采用uni-app、Vue3、Vite内置tailwindcss。
* 后端采用FastAPI、sqlalchemy、MySQLPostgreSQL、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
```

View File

@ -0,0 +1,12 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false

27
shierfeng-fastapi-app/.gitignore vendored Normal file
View File

@ -0,0 +1,27 @@
.DS_Store
node_modules/
unpackage/
dist/
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.project
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw*
# 要提交 .env 来确保 jit v2 的开发 watch mode
!.env
src/ignore
pnpm-lock.yaml

View File

@ -0,0 +1,7 @@
# 设置国内镜像地址
registry=http://registry.npmmirror.com/
# registry=http://registry.npmjs.com/
# 这个是给 pnpm 用的
shamefully-hoist=true
ignore-engines=true

View 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` 中设置推荐

View File

@ -0,0 +1,29 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script>
var coverSupport =
'CSS' in window &&
typeof CSS.supports === 'function' &&
(CSS.supports('top: env(a)') || CSS.supports('top: constant(a)'))
document.write(
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
(coverSupport ? ', viewport-fit=cover' : '') +
'" />',
)
</script>
<style>
html {
font-size: 16px !important;
}
</style>
<title></title>
<!--preload-links-->
<!--app-context-->
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@ -0,0 +1,13 @@
# netlify 部署文件
# 用于部署此项目的 h5 产物,可删除
[build]
publish = "dist/build/h5"
command = "pnpm run build:h5"
[build.environment]
NODE_VERSION = "20"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200

View 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"
]
}
}

View File

@ -0,0 +1,8 @@
import process from "node:process";
const isH5 = process.env.UNI_PLATFORM === "h5";
const isApp = process.env.UNI_PLATFORM === "app";
const WeappTailwindcssDisabled = isH5 || isApp;
const isMp = !isH5 && !isApp;
export { isApp, isH5, isMp, WeappTailwindcssDisabled };

View File

@ -0,0 +1,13 @@
import type { AcceptedPlugin } from "postcss";
import autoprefixer from "autoprefixer";
import tailwindcss from "tailwindcss";
import cssMacro from "weapp-tailwindcss/css-macro/postcss";
const plugins: AcceptedPlugin[] = [tailwindcss(), autoprefixer()];
// 可以使用 postcss-pxtransform 来进行 px 转 rpx 的功能
// 详见: https://tw.icebreaker.top/docs/quick-start/css-unit-transform#px-%E8%BD%AC-rpx
plugins.push(cssMacro);
export default plugins;

View File

@ -0,0 +1,79 @@
<script setup>
import config from "./config";
import { getToken } from "@/utils/auth";
import { useConfigStore } from "@/store";
import { getCurrentInstance } from "vue";
import { onLaunch } from "@dcloudio/uni-app";
const { proxy } = getCurrentInstance();
onLaunch(() => {
initApp();
});
//
function initApp() {
//
initConfig();
//
//#ifdef H5
checkLogin();
//#endif
}
function initConfig() {
useConfigStore().setConfig(config);
}
function checkLogin() {
if (!getToken()) {
proxy.$tab.reLaunch("/pages/login");
}
}
</script>
<style>
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Global Reset for UniApp/Mobile */
page,
body {
height: 100%;
min-height: 100%;
overflow-x: hidden;
/* Prevent default bounce effect on iOS if needed, or just handle overflow */
}
/* Global box-sizing for consistency with Tailwind preflight */
page,
view,
scroll-view,
image,
text,
button,
input,
textarea,
label,
navigator {
box-sizing: border-box;
}
/* 修复 H5 端 uni.showToast 图标在引入 Tailwind 后可能偏左的问题 */
/* #ifdef H5 */
uni-toast img,
uni-toast svg {
display: inline-block !important;
}
/* #endif */
/* Hide scrollbar for Chrome/Safari/Webkit */
::-webkit-scrollbar {
display: none;
width: 0 !important;
height: 0 !important;
-webkit-appearance: none;
background: transparent;
}
</style>

View File

@ -0,0 +1,62 @@
import request from "@/utils/request";
// 登录方法
export function login(username, password, code, uuid) {
const data = {
username,
password,
code,
uuid,
};
return request({
url: "/login",
header: {
"Content-Type": "application/x-www-form-urlencoded",
},
headers: {
isToken: false,
},
method: "post",
data: data,
});
}
// 注册方法
export function register(data) {
return request({
url: "/register",
headers: {
isToken: false,
},
method: "post",
data: data,
});
}
// 获取用户详细信息
export function getInfo() {
return request({
url: "/getInfo",
method: "get",
});
}
// 退出方法
export function logout() {
return request({
url: "/logout",
method: "post",
});
}
// 获取验证码
export function getCodeImg() {
return request({
url: "/captchaImage",
headers: {
isToken: false,
},
method: "get",
timeout: 20000,
});
}

View 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",
});
}

View 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",
});
}

View File

@ -0,0 +1,41 @@
import upload from "@/utils/upload";
import request from "@/utils/request";
// 用户密码重置
export function updateUserPwd(oldPassword, newPassword) {
const data = {
oldPassword,
newPassword,
};
return request({
url: "/system/user/profile/updatePwd",
method: "put",
data: data,
});
}
// 查询用户个人信息
export function getUserProfile() {
return request({
url: "/system/user/profile",
method: "get",
});
}
// 修改用户个人信息
export function updateUserProfile(data) {
return request({
url: "/system/user/profile",
method: "put",
data: data,
});
}
// 用户头像上传
export function uploadAvatar(data) {
return upload({
url: "/system/user/profile/avatar",
name: data.name,
filePath: data.filePath,
});
}

View File

@ -0,0 +1,109 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
const EffectScope: typeof import('vue').EffectScope
const acceptHMRUpdate: typeof import('pinia').acceptHMRUpdate
const computed: typeof import('vue').computed
const createApp: typeof import('vue').createApp
const createPinia: typeof import('pinia').createPinia
const customRef: typeof import('vue').customRef
const defineAsyncComponent: typeof import('vue').defineAsyncComponent
const defineComponent: typeof import('vue').defineComponent
const defineStore: typeof import('pinia').defineStore
const effectScope: typeof import('vue').effectScope
const getActivePinia: typeof import('pinia').getActivePinia
const getCurrentInstance: typeof import('vue').getCurrentInstance
const getCurrentScope: typeof import('vue').getCurrentScope
const getCurrentWatcher: typeof import('vue').getCurrentWatcher
const h: typeof import('vue').h
const inject: typeof import('vue').inject
const isProxy: typeof import('vue').isProxy
const isReactive: typeof import('vue').isReactive
const isReadonly: typeof import('vue').isReadonly
const isRef: typeof import('vue').isRef
const isShallow: typeof import('vue').isShallow
const mapActions: typeof import('pinia').mapActions
const mapGetters: typeof import('pinia').mapGetters
const mapState: typeof import('pinia').mapState
const mapStores: typeof import('pinia').mapStores
const mapWritableState: typeof import('pinia').mapWritableState
const markRaw: typeof import('vue').markRaw
const nextTick: typeof import('vue').nextTick
const onActivated: typeof import('vue').onActivated
const onAddToFavorites: typeof import('@dcloudio/uni-app').onAddToFavorites
const onBackPress: typeof import('@dcloudio/uni-app').onBackPress
const onBeforeMount: typeof import('vue').onBeforeMount
const onBeforeUnmount: typeof import('vue').onBeforeUnmount
const onBeforeUpdate: typeof import('vue').onBeforeUpdate
const onDeactivated: typeof import('vue').onDeactivated
const onError: typeof import('@dcloudio/uni-app').onError
const onErrorCaptured: typeof import('vue').onErrorCaptured
const onHide: typeof import('@dcloudio/uni-app').onHide
const onLaunch: typeof import('@dcloudio/uni-app').onLaunch
const onLoad: typeof import('@dcloudio/uni-app').onLoad
const onMounted: typeof import('vue').onMounted
const onNavigationBarButtonTap: typeof import('@dcloudio/uni-app').onNavigationBarButtonTap
const onNavigationBarSearchInputChanged: typeof import('@dcloudio/uni-app').onNavigationBarSearchInputChanged
const onNavigationBarSearchInputClicked: typeof import('@dcloudio/uni-app').onNavigationBarSearchInputClicked
const onNavigationBarSearchInputConfirmed: typeof import('@dcloudio/uni-app').onNavigationBarSearchInputConfirmed
const onNavigationBarSearchInputFocusChanged: typeof import('@dcloudio/uni-app').onNavigationBarSearchInputFocusChanged
const onPageNotFound: typeof import('@dcloudio/uni-app').onPageNotFound
const onPageScroll: typeof import('@dcloudio/uni-app').onPageScroll
const onPullDownRefresh: typeof import('@dcloudio/uni-app').onPullDownRefresh
const onReachBottom: typeof import('@dcloudio/uni-app').onReachBottom
const onReady: typeof import('@dcloudio/uni-app').onReady
const onRenderTracked: typeof import('vue').onRenderTracked
const onRenderTriggered: typeof import('vue').onRenderTriggered
const onResize: typeof import('@dcloudio/uni-app').onResize
const onScopeDispose: typeof import('vue').onScopeDispose
const onServerPrefetch: typeof import('vue').onServerPrefetch
const onShareAppMessage: typeof import('@dcloudio/uni-app').onShareAppMessage
const onShareTimeline: typeof import('@dcloudio/uni-app').onShareTimeline
const onShow: typeof import('@dcloudio/uni-app').onShow
const onTabItemTap: typeof import('@dcloudio/uni-app').onTabItemTap
const onThemeChange: typeof import('@dcloudio/uni-app').onThemeChange
const onUnhandledRejection: typeof import('@dcloudio/uni-app').onUnhandledRejection
const onUnload: typeof import('@dcloudio/uni-app').onUnload
const onUnmounted: typeof import('vue').onUnmounted
const onUpdated: typeof import('vue').onUpdated
const onWatcherCleanup: typeof import('vue').onWatcherCleanup
const provide: typeof import('vue').provide
const reactive: typeof import('vue').reactive
const readonly: typeof import('vue').readonly
const ref: typeof import('vue').ref
const resolveComponent: typeof import('vue').resolveComponent
const setActivePinia: typeof import('pinia').setActivePinia
const setMapStoreSuffix: typeof import('pinia').setMapStoreSuffix
const shallowReactive: typeof import('vue').shallowReactive
const shallowReadonly: typeof import('vue').shallowReadonly
const shallowRef: typeof import('vue').shallowRef
const storeToRefs: typeof import('pinia').storeToRefs
const toRaw: typeof import('vue').toRaw
const toRef: typeof import('vue').toRef
const toRefs: typeof import('vue').toRefs
const toValue: typeof import('vue').toValue
const triggerRef: typeof import('vue').triggerRef
const unref: typeof import('vue').unref
const useAttrs: typeof import('vue').useAttrs
const useCssModule: typeof import('vue').useCssModule
const useCssVars: typeof import('vue').useCssVars
const useId: typeof import('vue').useId
const useModel: typeof import('vue').useModel
const useSlots: typeof import('vue').useSlots
const useTemplateRef: typeof import('vue').useTemplateRef
const watch: typeof import('vue').watch
const watchEffect: typeof import('vue').watchEffect
const watchPostEffect: typeof import('vue').watchPostEffect
const watchSyncEffect: typeof import('vue').watchSyncEffect
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
}

View 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
View File

@ -0,0 +1,8 @@
/// <reference types="vite/client" />
declare module "*.vue" {
import type { DefineComponent } from "vue";
const component: DefineComponent<object, object, any>;
export default component;
}

View File

@ -0,0 +1,17 @@
// @ts-nocheck
import { createSSRApp } from "vue";
import App from "./App.vue";
import store from "./store"; // store
import { install } from "./plugins"; // plugins
import "./permission"; // permission
import { useDict } from "@/utils/dict";
export function createApp() {
const app = createSSRApp(App);
app.use(store);
app.config.globalProperties.useDict = useDict;
install(app);
return {
app,
};
}

View 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": "./"
}
}
}

View 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"
}
}

View 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>

View 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>

View 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>

View 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>

View File

@ -0,0 +1,208 @@
<template>
<view class="flex h-full flex-col bg-gray-50">
<!-- Header Area -->
<view class="bg-white pb-6 rounded-b-3xl shadow-sm z-50">
<!-- Title -->
<view class="pt-12 pb-4 px-4 flex justify-center items-center">
<text class="text-lg font-bold text-gray-800">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>

View File

@ -0,0 +1,224 @@
<template>
<view
class="flex h-full flex-col items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100 px-6 pb-20 overflow-hidden"
>
<!-- Logo Section -->
<view class="mb-6 flex flex-col items-center">
<view
class="mb-4 flex size-16 items-center justify-center rounded-2xl bg-white shadow-lg"
>
<image
class="size-10"
:src="globalConfig.appInfo.logo"
mode="widthFix"
/>
</view>
<text class="text-xl font-bold tracking-wide text-gray-800"
>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>

View 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 &copy; 2026 insistence.tech All Rights Reserved.
</view>
</view>
</template>
<script setup>
import { useConfigStore } from "@/store";
const url = useConfigStore().config.appInfo.site_url;
const version = useConfigStore().config.appInfo.version;
</script>
<style>
page {
background-color: #f9fafb;
}
</style>

View 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>

View 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>

View File

@ -0,0 +1,209 @@
<template>
<view class="flex h-full flex-col bg-gray-50 overflow-hidden">
<!-- Header Section -->
<view
class="relative overflow-hidden bg-gradient-to-r from-blue-500 to-indigo-600 pb-20 pt-16 text-white shadow-lg"
>
<view
class="absolute -right-10 -top-10 size-64 rounded-full bg-white/10 blur-3xl"
></view>
<view
class="absolute -bottom-10 -left-10 size-40 rounded-full bg-white/10 blur-2xl"
></view>
<view class="relative z-10 flex items-center justify-between px-6">
<view class="flex items-center space-x-4">
<!-- Avatar -->
<view
class="relative overflow-hidden rounded-full border-4 border-white/30 bg-white/20 shadow-xl transition-transform active:scale-95"
>
<image
v-if="avatar"
@click="handleToAvatar"
:src="avatar"
class="size-20 object-cover"
/>
<view
v-else
class="flex size-20 items-center justify-center bg-white text-gray-400"
>
<view class="i-mdi-account text-5xl"></view>
</view>
</view>
<!-- User Info -->
<view class="flex flex-col">
<template v-if="name">
<view
class="text-xl font-bold tracking-wide"
@click="handleToInfo"
>
{{ name }}
</view>
<view
class="mt-1 flex items-center text-sm text-blue-100"
@click="handleToInfo"
>
<text>查看个人信息</text>
<view class="i-mdi-chevron-right ml-1 text-xs"></view>
</view>
</template>
<view v-else class="text-xl font-bold" @click="handleToLogin">
点击登录
</view>
</view>
</view>
</view>
</view>
<!-- Content Section -->
<view class="relative z-20 -mt-12 flex-1 px-4 overflow-y-auto">
<!-- Quick Actions -->
<view
class="mb-4 flex items-center justify-between rounded-2xl bg-white p-4 shadow-lg shadow-gray-200/50"
>
<view
class="flex flex-1 flex-col items-center justify-center space-y-2 active:opacity-70"
@click="handleJiaoLiuQun"
>
<view
class="flex size-12 items-center justify-center rounded-full bg-pink-50 text-pink-500"
>
<view class="i-mdi-account-group text-2xl"></view>
</view>
<text class="text-xs font-medium text-gray-600">交流群</text>
</view>
<view
class="flex flex-1 flex-col items-center justify-center space-y-2 active:opacity-70"
@click="handleBuilding"
>
<view
class="flex size-12 items-center justify-center rounded-full bg-blue-50 text-blue-500"
>
<view class="i-mdi-face-agent text-2xl"></view>
</view>
<text class="text-xs font-medium text-gray-600">在线客服</text>
</view>
<view
class="flex flex-1 flex-col items-center justify-center space-y-2 active:opacity-70"
@click="handleBuilding"
>
<view
class="flex size-12 items-center justify-center rounded-full bg-purple-50 text-purple-500"
>
<view class="i-mdi-forum text-2xl"></view>
</view>
<text class="text-xs font-medium text-gray-600">反馈社区</text>
</view>
<view
class="flex flex-1 flex-col items-center justify-center space-y-2 active:opacity-70"
@click="handleBuilding"
>
<view
class="flex size-12 items-center justify-center rounded-full bg-green-50 text-green-500"
>
<view class="i-mdi-thumb-up text-2xl"></view>
</view>
<text class="text-xs font-medium text-gray-600">点赞我们</text>
</view>
</view>
<!-- Menu List -->
<view
class="overflow-hidden rounded-2xl bg-white shadow-lg shadow-gray-200/50"
>
<view
class="group flex items-center justify-between border-b border-gray-100 p-4 transition-colors active:bg-gray-50"
@click="handleToEditInfo"
>
<view class="flex items-center space-x-3">
<view class="i-mdi-account-edit text-xl text-blue-500"></view>
<text class="text-base text-gray-700">编辑资料</text>
</view>
<view class="i-mdi-chevron-right text-gray-400"></view>
</view>
<view
class="group flex items-center justify-between border-b border-gray-100 p-4 transition-colors active:bg-gray-50"
@click="handleHelp"
>
<view class="flex items-center space-x-3">
<view class="i-mdi-help-circle text-xl text-orange-500"></view>
<text class="text-base text-gray-700">常见问题</text>
</view>
<view class="i-mdi-chevron-right text-gray-400"></view>
</view>
<view
class="group flex items-center justify-between border-b border-gray-100 p-4 transition-colors active:bg-gray-50"
@click="handleAbout"
>
<view class="flex items-center space-x-3">
<view class="i-mdi-heart-outline text-xl text-red-500"></view>
<text class="text-base text-gray-700">关于我们</text>
</view>
<view class="i-mdi-chevron-right text-gray-400"></view>
</view>
<view
class="group flex items-center justify-between p-4 transition-colors active:bg-gray-50"
@click="handleToSetting"
>
<view class="flex items-center space-x-3">
<view class="i-mdi-cog-outline text-xl text-gray-500"></view>
<text class="text-base text-gray-700">应用设置</text>
</view>
<view class="i-mdi-chevron-right text-gray-400"></view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { useUserStore } from "@/store";
import { computed, getCurrentInstance } from "vue";
const { proxy } = getCurrentInstance();
const userStore = useUserStore();
const name = computed(() => userStore.name);
const avatar = computed(() => userStore.avatar);
const windowHeight = computed(() => uni.getSystemInfoSync().windowHeight - 50);
function handleToInfo() {
proxy.$tab.navigateTo("/pages/mine/info/index");
}
function handleToEditInfo() {
proxy.$tab.navigateTo("/pages/mine/info/edit");
}
function handleToSetting() {
proxy.$tab.navigateTo("/pages/mine/setting/index");
}
function handleToLogin() {
proxy.$tab.reLaunch("/pages/login");
}
function handleToAvatar() {
proxy.$tab.navigateTo("/pages/mine/avatar/index");
}
function handleHelp() {
proxy.$tab.navigateTo("/pages/mine/help/index");
}
function handleAbout() {
proxy.$tab.navigateTo("/pages/mine/about/index");
}
function handleJiaoLiuQun() {
proxy.$modal.showToast("模块建设中~");
}
function handleBuilding() {
proxy.$modal.showToast("模块建设中~");
}
</script>

View 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>

View 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>

View 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>

View 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>

View File

@ -0,0 +1,206 @@
<template>
<view
class="flex h-full flex-col items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100 px-6 pb-20 overflow-hidden"
>
<!-- Logo Section -->
<view class="mb-6 flex flex-col items-center">
<view
class="mb-4 flex size-16 items-center justify-center rounded-2xl bg-white shadow-lg"
>
<image
class="size-10"
:src="globalConfig.appInfo.logo"
mode="widthFix"
/>
</view>
<text class="text-xl font-bold tracking-wide text-gray-800"
>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>

View File

@ -0,0 +1,235 @@
<template>
<view class="flex h-full flex-col bg-gray-50 overflow-hidden">
<scroll-view scroll-y class="flex-1" :show-scrollbar="false">
<view class="p-4 pb-24 space-y-6">
<!-- Banner -->
<view
class="relative h-40 w-full overflow-hidden rounded-2xl shadow-lg shadow-indigo-100"
>
<swiper
class="h-40 w-full"
:current="swiperDotIndex"
@change="changeSwiper"
autoplay
interval="3000"
circular
>
<swiper-item v-for="(item, index) in data" :key="index">
<view class="h-full w-full" @click="clickBannerItem(item)">
<image
:src="item.image"
mode="scaleToFill"
class="block h-full w-full"
/>
</view>
</swiper-item>
</swiper>
<!-- Custom Dots -->
<view
class="absolute bottom-3 right-0 left-0 flex justify-center space-x-2 pointer-events-none"
>
<view
v-for="(item, index) in data"
:key="index"
class="h-1.5 rounded-full transition-all duration-300"
:class="current === index ? 'bg-white w-4' : 'bg-white/50 w-1.5'"
>
</view>
</view>
</view>
<!-- AI Tools Section -->
<view>
<view class="mb-4 flex items-center space-x-2">
<view class="h-4 w-1 rounded-full bg-indigo-500"></view>
<text class="text-base font-bold text-gray-800">AI 生产力</text>
</view>
<view class="grid grid-cols-2 gap-4">
<view
class="group relative overflow-hidden rounded-2xl bg-white p-5 shadow-sm transition-all active:scale-95"
@click="handleToAiChat"
>
<view
class="absolute -right-4 -top-4 size-20 rounded-full bg-indigo-50 opacity-50"
></view>
<view class="relative z-10 flex flex-col">
<view
class="mb-3 flex size-10 items-center justify-center rounded-xl bg-indigo-500 text-white shadow-md shadow-indigo-200"
>
<view class="i-mdi-chat text-xl"></view>
</view>
<text class="font-bold text-gray-800">智能对话</text>
<text class="mt-1 text-xs text-gray-500">智能助手</text>
</view>
</view>
<view
class="group relative overflow-hidden rounded-2xl bg-white p-5 shadow-sm transition-all active:scale-95"
@click="handleBuilding"
>
<view
class="absolute -right-4 -top-4 size-20 rounded-full bg-purple-50 opacity-50"
></view>
<view class="relative z-10 flex flex-col">
<view
class="mb-3 flex size-10 items-center justify-center rounded-xl bg-purple-500 text-white shadow-md shadow-purple-200"
>
<view class="i-mdi-image-multiple text-xl"></view>
</view>
<text class="font-bold text-gray-800">图像生成</text>
<text class="mt-1 text-xs text-gray-500">创意工坊</text>
</view>
</view>
</view>
</view>
<!-- System Management Section -->
<view>
<view class="mb-4 flex items-center space-x-2">
<view class="h-4 w-1 rounded-full bg-blue-500"></view>
<text class="text-base font-bold text-gray-800">系统管理</text>
</view>
<view class="rounded-2xl bg-white p-5 shadow-sm">
<view class="flex flex-wrap justify-between gap-y-4">
<view
class="w-[22%] flex flex-col items-center space-y-1.5 active:opacity-60"
@click="handleToUserList"
>
<view
class="flex size-10 items-center justify-center rounded-xl bg-blue-50 text-blue-500 transition-colors group-active:bg-blue-100"
>
<view class="i-mdi-account text-xl"></view>
</view>
<text class="text-[11px] font-medium text-gray-600"
>用户管理</text
>
</view>
<view
class="w-[22%] flex flex-col items-center space-y-1.5 active:opacity-60"
@click="handleBuilding"
>
<view
class="flex size-10 items-center justify-center rounded-xl bg-orange-50 text-orange-500 transition-colors group-active:bg-orange-100"
>
<view class="i-mdi-badge-account text-xl"></view>
</view>
<text class="text-[11px] font-medium text-gray-600"
>角色管理</text
>
</view>
<view
class="w-[22%] flex flex-col items-center space-y-1.5 active:opacity-60"
@click="handleBuilding"
>
<view
class="flex size-10 items-center justify-center rounded-xl bg-green-50 text-green-500 transition-colors group-active:bg-green-100"
>
<view class="i-mdi-office-building text-xl"></view>
</view>
<text class="text-[11px] font-medium text-gray-600"
>部门管理</text
>
</view>
<view
class="w-[22%] flex flex-col items-center space-y-1.5 active:opacity-60"
@click="handleBuilding"
>
<view
class="flex size-10 items-center justify-center rounded-xl bg-red-50 text-red-500 transition-colors group-active:bg-red-100"
>
<view class="i-mdi-cog text-xl"></view>
</view>
<text class="text-[11px] font-medium text-gray-600"
>配置管理</text
>
</view>
<view
class="w-[22%] flex flex-col items-center space-y-1.5 active:opacity-60"
@click="handleBuilding"
>
<view
class="flex size-10 items-center justify-center rounded-xl bg-cyan-50 text-cyan-500 transition-colors group-active:bg-cyan-100"
>
<view class="i-mdi-book-open-page-variant text-xl"></view>
</view>
<text class="text-[11px] font-medium text-gray-600"
>字典管理</text
>
</view>
<view
class="w-[22%] flex flex-col items-center space-y-1.5 active:opacity-60"
@click="handleBuilding"
>
<view
class="flex size-10 items-center justify-center rounded-xl bg-yellow-50 text-yellow-500 transition-colors group-active:bg-yellow-100"
>
<view class="i-mdi-bullhorn text-xl"></view>
</view>
<text class="text-[11px] font-medium text-gray-600"
>通知公告</text
>
</view>
<view
class="w-[22%] flex flex-col items-center space-y-1.5 active:opacity-60"
@click="handleBuilding"
>
<view
class="flex size-10 items-center justify-center rounded-xl bg-pink-50 text-pink-500 transition-colors group-active:bg-pink-100"
>
<view class="i-mdi-file-document-outline text-xl"></view>
</view>
<text class="text-[11px] font-medium text-gray-600"
>日志管理</text
>
</view>
<view
class="w-[22%] flex flex-col items-center space-y-1.5 active:opacity-60"
@click="handleBuilding"
>
<view
class="flex size-10 items-center justify-center rounded-xl bg-gray-50 text-gray-500 transition-colors group-active:bg-gray-100"
>
<view class="i-mdi-dots-horizontal text-xl"></view>
</view>
<text class="text-[11px] font-medium text-gray-600">更多</text>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { ref, getCurrentInstance } from "vue";
const { proxy } = getCurrentInstance();
const current = ref(0);
const swiperDotIndex = ref(0);
const data = ref([
{ image: "/static/images/banner/banner01.jpg" },
{ image: "/static/images/banner/banner02.jpg" },
{ image: "/static/images/banner/banner03.jpg" },
]);
function clickBannerItem(item) {
console.info(item);
}
function changeSwiper(e) {
current.value = e.detail.current;
}
function handleToAiChat() {
handleBuilding();
}
function handleToUserList() {
handleBuilding();
}
function handleBuilding() {
proxy.$modal.msg("模块建设中~");
}
</script>

View File

@ -0,0 +1,43 @@
import { getToken } from "@/utils/auth";
// 登录页面
const loginPage = "/pages/login";
// 页面白名单
const whiteList = [
"/pages/login",
"/pages/register",
"/pages/common/webview/index",
"/pages/common/agreement/index",
"/pages/common/privacy/index",
];
// 检查地址白名单
function checkWhite(url) {
const path = url.split("?")[0];
return whiteList.indexOf(path) !== -1;
}
// 页面跳转验证拦截器
let list = ["navigateTo", "redirectTo", "reLaunch", "switchTab"];
list.forEach((item) => {
uni.addInterceptor(item, {
invoke(to) {
if (getToken()) {
if (to.url === loginPage) {
uni.reLaunch({ url: "/" });
}
return true;
} else {
if (checkWhite(to.url)) {
return true;
}
uni.reLaunch({ url: loginPage });
return false;
}
},
fail(err) {
console.log(err);
},
});
});

View File

@ -0,0 +1,60 @@
import { useUserStore } from "@/store";
function authPermission(permission) {
const all_permission = "*:*:*";
const permissions = useUserStore().permissions;
if (permission && permission.length > 0) {
return permissions.some((v) => {
return all_permission === v || v === permission;
});
} else {
return false;
}
}
function authRole(role) {
const super_admin = "admin";
const roles = useUserStore().roles;
if (role && role.length > 0) {
return roles.some((v) => {
return super_admin === v || v === role;
});
} else {
return false;
}
}
export default {
// 验证用户是否具备某权限
hasPermi(permission) {
return authPermission(permission);
},
// 验证用户是否含有指定权限,只需包含其中一个
hasPermiOr(permissions) {
return permissions.some((item) => {
return authPermission(item);
});
},
// 验证用户是否含有指定权限,必须全部拥有
hasPermiAnd(permissions) {
return permissions.every((item) => {
return authPermission(item);
});
},
// 验证用户是否具备某角色
hasRole(role) {
return authRole(role);
},
// 验证用户是否含有指定角色,只需包含其中一个
hasRoleOr(roles) {
return roles.some((item) => {
return authRole(item);
});
},
// 验证用户是否含有指定角色,必须全部拥有
hasRoleAnd(roles) {
return roles.every((item) => {
return authRole(item);
});
},
};

View File

@ -0,0 +1,12 @@
import tab from "./tab";
import auth from "./auth";
import modal from "./modal";
export function install(app) {
// 页签操作
app.config.globalProperties.$tab = tab;
// 认证对象
app.config.globalProperties.$auth = auth;
// 模态框对象
app.config.globalProperties.$modal = modal;
}

View File

@ -0,0 +1,78 @@
export default {
// 消息提示
msg(content) {
uni.showToast({
title: content,
icon: "none",
});
},
// 错误消息
msgError(content) {
uni.showToast({
title: content,
icon: "error",
});
},
// 成功消息
msgSuccess(content) {
uni.showToast({
title: content,
icon: "success",
});
},
// 隐藏消息
hideMsg(content) {
uni.hideToast();
},
// 弹出提示
alert(content, title) {
uni.showModal({
title: title || "系统提示",
content: content,
showCancel: false,
});
},
// 确认窗体
confirm(content, title) {
return new Promise((resolve, reject) => {
uni.showModal({
title: title || "系统提示",
content: content,
cancelText: "取消",
confirmText: "确定",
success: function (res) {
if (res.confirm) {
resolve(res.confirm);
}
},
});
});
},
// 提示信息
showToast(option) {
if (typeof option === "object") {
uni.showToast(option);
} else {
uni.showToast({
title: option,
icon: "none",
duration: 2500,
});
}
},
// 打开遮罩层
loading(content) {
uni.showLoading({
title: content,
icon: "none",
});
},
// 关闭遮罩层
closeLoading() {
try {
uni.hideLoading();
} catch (e) {
console.log(e);
}
},
};

View File

@ -0,0 +1,30 @@
export default {
// 关闭所有页面,打开到应用内的某个页面
reLaunch(url) {
return uni.reLaunch({
url: url,
});
},
// 跳转到tabBar页面并关闭其他所有非tabBar页面
switchTab(url) {
return uni.switchTab({
url: url,
});
},
// 关闭当前页面,跳转到应用内的某个页面
redirectTo(url) {
return uni.redirectTo({
url: url,
});
},
// 保留当前页面,跳转到应用内的某个页面
navigateTo(url) {
return uni.navigateTo({
url: url,
});
},
// 关闭当前页面,返回上一页面或多级页面
navigateBack() {
return uni.navigateBack();
},
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@ -0,0 +1,9 @@
import { createPinia } from "pinia";
import { useUserStore } from "./modules/user";
import { useConfigStore } from "./modules/config";
const pinia = createPinia();
export default pinia;
export { useUserStore, useConfigStore };

View 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,
};
});

View 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;

View File

@ -0,0 +1,125 @@
import { defineStore } from "pinia";
import { ref } from "vue";
import config from "@/config";
import storage from "@/utils/storage";
import constant from "@/utils/constant";
import { isHttp, isEmpty } from "@/utils/validate";
import { getInfo, login, logout } from "@/api/login";
import { getToken, removeToken, setToken } from "@/utils/auth";
import defAva from "@/static/images/profile.jpg";
const baseUrl = config.baseUrl;
export const useUserStore = defineStore("user", () => {
const token = ref(getToken());
const id = ref(storage.get(constant.id));
const name = ref(storage.get(constant.name));
const avatar = ref(storage.get(constant.avatar));
const roles = ref(storage.get(constant.roles));
const permissions = ref(storage.get(constant.permissions));
const SET_TOKEN = (val) => {
token.value = val;
};
const SET_ID = (val) => {
id.value = val;
storage.set(constant.id, val);
};
const SET_NAME = (val) => {
name.value = val;
storage.set(constant.name, val);
};
const SET_AVATAR = (val) => {
avatar.value = val;
storage.set(constant.avatar, val);
};
const SET_ROLES = (val) => {
roles.value = val;
storage.set(constant.roles, val);
};
const SET_PERMISSIONS = (val) => {
permissions.value = val;
storage.set(constant.permissions, val);
};
// 登录
const loginAction = (userInfo) => {
const username = userInfo.username.trim();
const password = userInfo.password;
const code = userInfo.code;
const uuid = userInfo.uuid;
return new Promise((resolve, reject) => {
login(username, password, code, uuid)
.then((res) => {
setToken(res.token);
SET_TOKEN(res.token);
resolve();
})
.catch((error) => {
reject(error);
});
});
};
// 获取用户信息
const getInfoAction = () => {
return new Promise((resolve, reject) => {
getInfo()
.then((res) => {
const user = res.user;
let avatar = user.avatar || "";
if (!isHttp(avatar)) {
avatar = isEmpty(avatar) ? defAva : baseUrl + avatar;
}
const userid =
isEmpty(user) || isEmpty(user.userId) ? "" : user.userId;
const username =
isEmpty(user) || isEmpty(user.userName) ? "" : user.userName;
if (res.roles && res.roles.length > 0) {
SET_ROLES(res.roles);
SET_PERMISSIONS(res.permissions);
} else {
SET_ROLES(["ROLE_DEFAULT"]);
}
SET_ID(userid);
SET_NAME(username);
SET_AVATAR(avatar);
resolve(res);
})
.catch((error) => {
reject(error);
});
});
};
// 退出系统
const logOutAction = () => {
return new Promise((resolve, reject) => {
logout(token.value)
.then(() => {
SET_TOKEN("");
SET_ROLES([]);
SET_PERMISSIONS([]);
removeToken();
storage.clean();
resolve();
})
.catch((error) => {
reject(error);
});
});
};
return {
token,
id,
name,
avatar,
roles,
permissions,
SET_AVATAR,
login: loginAction,
getInfo: getInfoAction,
logOut: logOutAction,
};
});

View File

@ -0,0 +1,10 @@
{
"light": {
"navBgColor": "#f6f6f6",
"navTxtStyle": "black"
},
"dark": {
"navBgColor": "#191919",
"navTxtStyle": "white"
}
}

View File

View File

@ -0,0 +1,13 @@
const TokenKey = "App-Token";
export function getToken() {
return uni.getStorageSync(TokenKey);
}
export function setToken(token) {
return uni.setStorageSync(TokenKey, token);
}
export function removeToken() {
return uni.removeStorageSync(TokenKey);
}

View File

@ -0,0 +1,58 @@
/**
* 显示消息提示框
* @param content 提示的标题
*/
export function toast(content) {
uni.showToast({
icon: "none",
title: content,
});
}
/**
* 显示模态弹窗
* @param content 提示的标题
*/
export function showConfirm(content) {
return new Promise((resolve, reject) => {
uni.showModal({
title: "提示",
content: content,
cancelText: "取消",
confirmText: "确定",
success: function (res) {
resolve(res);
},
});
});
}
/**
* 参数处理
* @param params 参数
*/
export function tansParams(params) {
let result = "";
for (const propName of Object.keys(params)) {
const value = params[propName];
var part = encodeURIComponent(propName) + "=";
if (value !== null && value !== "" && typeof value !== "undefined") {
if (typeof value === "object") {
for (const key of Object.keys(value)) {
if (
value[key] !== null &&
value[key] !== "" &&
typeof value[key] !== "undefined"
) {
let params = propName + "[" + key + "]";
var subPart = encodeURIComponent(params) + "=";
result += subPart + encodeURIComponent(value[key]) + "&";
}
}
} else {
result += part + encodeURIComponent(value) + "&";
}
}
}
return result;
}

View File

@ -0,0 +1,9 @@
const constant = {
avatar: "user_avatar",
id: "user_id",
name: "user_name",
roles: "user_roles",
permissions: "user_permissions",
};
export default constant;

View File

@ -0,0 +1,30 @@
import useDictStore from "@/store/modules/dict";
import { getDicts } from "@/api/system/dict/data";
import { ref, toRefs } from "vue";
/**
* 获取字典数据
*/
export function useDict(...args) {
const res = ref({});
return (() => {
args.forEach((dictType, index) => {
res.value[dictType] = [];
const dicts = useDictStore().getDict(dictType);
if (dicts) {
res.value[dictType] = dicts;
} else {
getDicts(dictType).then((resp) => {
res.value[dictType] = resp.data.map((p) => ({
label: p.dictLabel,
value: p.dictValue,
elTagType: p.listClass,
elTagClass: p.cssClass,
}));
useDictStore().setDict(dictType, res.value[dictType]);
});
}
});
return toRefs(res.value);
})();
}

View File

@ -0,0 +1,6 @@
export default {
401: "认证失败,无法访问系统资源",
403: "当前操作没有权限",
404: "访问资源不存在",
default: "系统未知错误,请反馈给管理员",
};

View File

@ -0,0 +1,55 @@
import store from "@/store";
/**
* 字符权限校验
* @param {Array} value 校验值
* @returns {Boolean}
*/
export function checkPermi(value) {
if (value && value instanceof Array && value.length > 0) {
const permissions = store.getters && store.getters.permissions;
const permissionDatas = value;
const all_permission = "*:*:*";
const hasPermission = permissions.some((permission) => {
return (
all_permission === permission || permissionDatas.includes(permission)
);
});
if (!hasPermission) {
return false;
}
return true;
} else {
console.error(
`need roles! Like checkPermi="['system:user:add','system:user:edit']"`,
);
return false;
}
}
/**
* 角色权限校验
* @param {Array} value 校验值
* @returns {Boolean}
*/
export function checkRole(value) {
if (value && value instanceof Array && value.length > 0) {
const roles = store.getters && store.getters.roles;
const permissionRoles = value;
const super_admin = "admin";
const hasRole = roles.some((role) => {
return super_admin === role || permissionRoles.includes(role);
});
if (!hasRole) {
return false;
}
return true;
} else {
console.error(`need roles! Like checkRole="['admin','editor']"`);
return false;
}
}

View File

@ -0,0 +1,131 @@
import config from "@/config";
import { getToken } from "@/utils/auth";
import errorCode from "@/utils/errorCode";
import { useUserStore } from "@/store/modules/user";
import { toast, showConfirm, tansParams } from "@/utils/common";
import {
decryptTransportErrorResponse,
decryptTransportResponse,
encryptTransportRequest,
invalidateTransportKeyMeta,
resetTransportRequestConfig,
shouldRetryTransportWithFreshKey,
} from "@/utils/transportCrypto";
let timeout = 10000;
const baseUrl = config.baseUrl;
const request = async (config) => {
// 是否需要设置 token
const isToken = (config.headers || {}).isToken === false;
config.header = config.header || {};
config.headers = config.headers || {};
if (getToken() && !isToken) {
config.header["Authorization"] = "Bearer " + getToken();
}
try {
config = await encryptTransportRequest(config);
// get请求映射params参数
if (config.params) {
let url = config.url + "?" + tansParams(config.params);
url = url.slice(0, -1);
config.url = url;
}
return await new Promise((resolve, reject) => {
uni.request({
method: config.method || "get",
timeout: config.timeout || timeout,
url: config.baseUrl || baseUrl + config.url,
data: config.data,
header: config.header,
dataType: "json",
success: async (response) => {
try {
const res = await decryptTransportResponse(response, config);
if (shouldRetryTransportWithFreshKey(res) && !config.__transportRetried) {
invalidateTransportKeyMeta();
config.__transportRetried = true;
config.headers.repeatSubmit = false;
resetTransportRequestConfig(config);
resolve(await request(config));
return;
}
const code = res.data.code || 200;
const msg = errorCode[code] || res.data.msg || errorCode["default"];
if (code === 401) {
showConfirm(
"登录状态已过期,您可以继续留在该页面,或者重新登录?",
).then((res) => {
if (res.confirm) {
useUserStore()
.logOut()
.then(() => {
uni.reLaunch({ url: "/pages/login" });
});
}
});
const error = new Error("无效的会话,或者会话已过期,请重新登录。");
error.response = res;
reject(error);
} else if (code === 500) {
const error = new Error(msg);
error.response = res;
reject(error);
} else if (code !== 200) {
const error = new Error(msg);
error.response = res;
reject(error);
} else {
resolve(res.data);
}
} catch (error) {
reject(error);
}
},
fail: reject,
});
});
} catch (error) {
error = await decryptTransportErrorResponse(error, config);
if (shouldRetryTransportWithFreshKey(error) && !config.__transportRetried) {
invalidateTransportKeyMeta();
config.__transportRetried = true;
config.headers.repeatSubmit = false;
resetTransportRequestConfig(config);
return request(config);
}
const response = error.response;
const responseStatus = response?.status ?? response?.statusCode;
const responseCode = response?.data?.code;
const responseMsg = response?.data?.msg;
if (responseMsg) {
uni.showToast({
title: responseMsg,
icon: "none",
duration: responseStatus === 429 || responseCode === 429 ? 5000 : 3000,
});
throw error;
}
let { message } = error;
if (message === "Network Error") {
message = "后端接口连接异常";
} else if (message && message.includes("timeout")) {
message = "系统接口请求超时";
} else if (message && message.includes("Request failed with status code")) {
message = "系统接口" + message.substr(message.length - 3) + "异常";
}
if (message) {
toast(message);
}
throw error;
}
};
export default request;

View File

@ -0,0 +1,38 @@
import constant from "./constant";
// 存储变量名
let storageKey = "storage_data";
// 存储节点变量名
let storageNodeKeys = [
constant.avatar,
constant.id,
constant.name,
constant.roles,
constant.permissions,
];
const storage = {
set: function (key, value) {
if (storageNodeKeys.indexOf(key) != -1) {
let tmp = uni.getStorageSync(storageKey);
tmp = tmp ? tmp : {};
tmp[key] = value;
uni.setStorageSync(storageKey, tmp);
}
},
get: function (key) {
let storageData = uni.getStorageSync(storageKey) || {};
return storageData[key] || "";
},
remove: function (key) {
let storageData = uni.getStorageSync(storageKey) || {};
delete storageData[key];
uni.setStorageSync(storageKey, storageData);
},
clean: function () {
uni.removeStorageSync(storageKey);
},
};
export default storage;

File diff suppressed because it is too large Load Diff

View 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);
}

View 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;

View File

@ -0,0 +1,72 @@
import { useUserStore } from "@/store";
import config from "@/config";
import { getToken } from "@/utils/auth";
import errorCode from "@/utils/errorCode";
import { toast, showConfirm, tansParams } from "@/utils/common";
let timeout = 10000;
const baseUrl = config.baseUrl;
export default function upload(config) {
// 是否需要设置 token
const isToken = (config.headers || {}).isToken === false;
config.header = config.header || {};
if (getToken() && !isToken) {
config.header["Authorization"] = "Bearer " + getToken();
}
// get请求映射params参数
if (config.params) {
let url = config.url + "?" + tansParams(config.params);
url = url.slice(0, -1);
config.url = url;
}
return new Promise((resolve, reject) => {
uni.uploadFile({
timeout: config.timeout || timeout,
url: baseUrl + config.url,
filePath: config.filePath,
name: config.name || "file",
header: config.header,
formData: config.formData,
success: (res) => {
let result = JSON.parse(res.data);
const code = result.code || 200;
const msg = errorCode[code] || result.msg || errorCode["default"];
if (code === 200) {
resolve(result);
} else if (code == 401) {
showConfirm(
"登录状态已过期,您可以继续留在该页面,或者重新登录?",
).then((res) => {
if (res.confirm) {
useUserStore()
.logOut()
.then((res) => {
uni.reLaunch({ url: "/pages/login/login" });
});
}
});
reject("无效的会话,或者会话已过期,请重新登录。");
} else if (code === 500) {
toast(msg);
reject("500");
} else if (code !== 200) {
toast(msg);
reject(code);
}
},
fail: (error) => {
let { message } = error;
if (message == "Network Error") {
message = "后端接口连接异常";
} else if (message.includes("timeout")) {
message = "系统接口请求超时";
} else if (message.includes("Request failed with status code")) {
message = "系统接口" + message.substr(message.length - 3) + "异常";
}
toast(message);
reject(error);
},
});
});
}

View File

@ -0,0 +1,124 @@
/**
* 路径匹配器
* @param {string} pattern
* @param {string} path
* @returns {Boolean}
*/
export function isPathMatch(pattern, path) {
const regexPattern = pattern
.replace(/\//g, "\\/")
.replace(/\*\*/g, ".*")
.replace(/\*/g, "[^\\/]*");
const regex = new RegExp(`^${regexPattern}$`);
return regex.test(path);
}
/**
* 判断value字符串是否为空
* @param {string} value
* @returns {Boolean}
*/
export function isEmpty(value) {
if (
value == null ||
value == "" ||
value == undefined ||
value == "undefined"
) {
return true;
}
return false;
}
/**
* 判断url是否是http或https
* @param {string} url
* @returns {Boolean}
*/
export function isHttp(url) {
return url.indexOf("http://") !== -1 || url.indexOf("https://") !== -1;
}
/**
* 判断path是否为外链
* @param {string} path
* @returns {Boolean}
*/
export function isExternal(path) {
return /^(https?:|mailto:|tel:)/.test(path);
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function validUsername(str) {
const valid_map = ["admin", "editor"];
return valid_map.indexOf(str.trim()) >= 0;
}
/**
* @param {string} url
* @returns {Boolean}
*/
export function validURL(url) {
const reg =
/^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/;
return reg.test(url);
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function validLowerCase(str) {
const reg = /^[a-z]+$/;
return reg.test(str);
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function validUpperCase(str) {
const reg = /^[A-Z]+$/;
return reg.test(str);
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function validAlphabets(str) {
const reg = /^[A-Za-z]+$/;
return reg.test(str);
}
/**
* @param {string} email
* @returns {Boolean}
*/
export function validEmail(email) {
const reg =
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return reg.test(email);
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function isString(str) {
return typeof str === "string" || str instanceof String;
}
/**
* @param {Array} arg
* @returns {Boolean}
*/
export function isArray(arg) {
if (typeof Array.isArray === "undefined") {
return Object.prototype.toString.call(arg) === "[object Array]";
}
return Array.isArray(arg);
}

View File

@ -0,0 +1,3 @@
import { icebreaker } from "@icebreakers/stylelint-config";
export default icebreaker();

View File

@ -0,0 +1,53 @@
import type { Config } from "tailwindcss";
import { getIconCollections, iconsPlugin } from "@egoist/tailwindcss-icons";
import cssMacro from "weapp-tailwindcss/css-macro";
import { isMp } from "./platform";
export default <Config>{
content: ["./index.html", "./src/**/*.{html,js,ts,jsx,tsx,vue}"],
theme: {
extend: {
// colors: {
// // 你可以在这里进行颜色的扩展
// primary: {
// 'DEFAULT': 'var(--color-primary, #0089FF)',
// 'light-3': 'var(--color-primary-light-3, rgb(85, 199, 255))',
// 'light-5': 'var(--color-primary-light-5, rgb(130, 217, 255))',
// 'light-7': 'var(--color-primary-light-7, rgb(175, 235, 255))',
// 'light-9': 'var(--color-primary-light-9, rgb(219, 252, 255))',
// 'dark-2': 'var(--color-primary-dark-2, rgb(0, 135, 204))',
// },
// },
},
},
// https://tw.icebreaker.top/docs/quick-start/uni-app-css-macro
plugins: [
cssMacro({
variantsMap: {
wx: "MP-WEIXIN",
"-wx": {
value: "MP-WEIXIN",
negative: true,
},
// 定义多个条件判断
// mv: {
// value: 'H5 || MP-WEIXIN'
// },
// '-mv': {
// value: 'H5 || MP-WEIXIN',
// negative: true
// }
},
}),
iconsPlugin({
// 在这里可以选择你要使用的 icon, 更多详见:
// https://icon-sets.iconify.design/
collections: getIconCollections(["svg-spinners", "mdi"]),
}),
],
corePlugins: {
// 小程序去使用 h5 的 preflight 和响应式 container 没有意义
preflight: !isMp,
container: !isMp,
},
};

View File

@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "esnext",
"outDir": "true",
"jsx": "preserve",
"lib": ["esnext", "dom"],
"useDefineForClassFields": true,
"baseUrl": ".",
"module": "esnext",
"moduleResolution": "bundler",
"paths": {
"@/*": ["src/*"]
},
"resolveJsonModule": true,
"types": ["@dcloudio/types"],
"allowJs": true,
"strict": true,
"sourceMap": true,
"esModuleInterop": true
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}

View File

@ -0,0 +1,70 @@
import uni from "@dcloudio/vite-plugin-uni";
import { defineConfig } from "vite";
import { UnifiedViteWeappTailwindcssPlugin } from "weapp-tailwindcss/vite";
import { WeappTailwindcssDisabled } from "./platform";
import postcssPlugins from "./postcss.config";
function patchNodeForgeMiniProgramRuntime() {
return {
name: "patch-node-forge-mini-program-runtime",
enforce: "pre",
transform(code: string, id: string) {
if (id.includes("node-forge/lib/util.js")) {
return {
code: code.replace(
/return typeof self === 'undefined' \? window : self;/,
"return typeof globalThis !== 'undefined' ? globalThis : (typeof self === 'undefined' ? (typeof window === 'undefined' ? {} : window) : self);",
),
map: null,
};
}
if (id.includes("node-forge/lib/random.js")) {
return {
code: code.replace(
/var _crypto = globalScope\.crypto \|\| globalScope\.msCrypto;/,
"var _crypto = (globalScope && (globalScope.crypto || globalScope.msCrypto)) || null;",
),
map: null,
};
}
return null;
},
};
}
// https://vitejs.dev/config/
export default defineConfig(async () => {
// 新版本的 unplugin-auto-import 改成了只有 esm 格式的产物,而 uni-app 目前必须 cjs 格式
// 所以需要改成动态 import 的写法来进行引入
// 详见 https://github.com/sonofmagic/uni-app-vite-vue3-tailwind-vscode-template/issues/29
const { default: AutoImport } = await import("unplugin-auto-import/vite");
return {
// uvtw 一定要放在 uni 后面
plugins: [
uni(),
patchNodeForgeMiniProgramRuntime(),
UnifiedViteWeappTailwindcssPlugin({
rem2rpx: true,
disabled: WeappTailwindcssDisabled,
}),
AutoImport({
imports: ["vue", "uni-app", "pinia"],
dts: "./src/auto-imports.d.ts",
}),
],
// 内联 postcss 注册 tailwindcss
css: {
postcss: {
plugins: postcssPlugins,
},
// https://vitejs.dev/config/shared-options.html#css-preprocessoroptions
preprocessorOptions: {
scss: {
silenceDeprecations: ["legacy-js-api"],
},
},
},
};
});

View 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'

View 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'

View 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'

View 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-----\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'

View File

@ -0,0 +1,14 @@
FROM python:3.10
WORKDIR /app
# 复制源代码
COPY . .
# 安装依赖
RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
# 暴露端口
EXPOSE 9099
# 启动命令
CMD ["shierfeng", "app", "run", "--env=dockermy"]

View File

@ -0,0 +1,14 @@
FROM python:3.10
WORKDIR /app
# 复制源代码
COPY . .
# 安装依赖
RUN pip install --no-cache-dir -r requirements-pg.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
# 暴露端口
EXPOSE 9099
# 启动命令
CMD ["shierfeng", "app", "run", "--env=dockerpg"]

View File

@ -0,0 +1,153 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts.
# this is typically a path given in POSIX (e.g. forward slashes)
# format, relative to the token %(here)s which refers to the location of this
# ini file
script_location = %(here)s/alembic
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
# for all available tokens
file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory. for multiple paths, the path separator
# is defined by "path_separator" below.
prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python>=3.9 or backports.zoneinfo library and tzdata library.
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
# string value is passed to ZoneInfo()
# leave blank for localtime
# timezone =
# max length of characters to apply to the "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; This defaults
# to <script_location>/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "path_separator"
# below.
# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions
# path_separator; This indicates what character is used to split lists of file
# paths, including version_locations and prepend_sys_path within configparser
# files such as alembic.ini.
# The default rendered in new alembic.ini files is "os", which uses os.pathsep
# to provide os-dependent path splitting.
#
# Note that in order to support legacy alembic.ini files, this default does NOT
# take place if path_separator is not present in alembic.ini. If this
# option is omitted entirely, fallback logic is as follows:
#
# 1. Parsing of the version_locations option falls back to using the legacy
# "version_path_separator" key, which if absent then falls back to the legacy
# behavior of splitting on spaces and/or commas.
# 2. Parsing of the prepend_sys_path option falls back to the legacy
# behavior of splitting on spaces, commas, or colons.
#
# Valid values for path_separator are:
#
# path_separator = :
# path_separator = ;
# path_separator = space
# path_separator = newline
#
# Use os.pathsep. Default configuration used for new projects.
path_separator = os
# set to 'true' to search source files recursively
# in each "version_locations" directory
# new in Alembic version 1.10
# recursive_version_locations = false
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
# database URL. This is consumed by the user-maintained env.py script only.
# other means of configuring database URLs may be customized within the env.py
# file.
sqlalchemy.url = driver://user:pass@localhost/dbname
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module
# hooks = ruff
# ruff.type = module
# ruff.module = ruff
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
# Alternatively, use the exec runner to execute a binary found on your PATH
# hooks = ruff
# ruff.type = exec
# ruff.executable = ruff
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
# Logging configuration. This is also consumed by the user-maintained
# env.py script only.
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARNING
handlers = console
qualname =
[logger_sqlalchemy]
level = WARNING
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
[settings]
# This section is used to set environment variables for the alembic.
# The env option is used to specify the environment for the alembic.
# It can be set to dev or prod.
env = dev

View File

@ -0,0 +1 @@
Generic single-database configuration with an async dbapi.

View File

@ -0,0 +1,135 @@
import asyncio
import os
from collections.abc import Iterable, MutableMapping
from logging.config import fileConfig
from typing import Literal
from alembic import context
from alembic.migration import MigrationContext
from alembic.operations.ops import MigrationScript
from sqlalchemy import pool
from sqlalchemy.engine import Connection
from sqlalchemy.ext.asyncio import async_engine_from_config
from config.database import ASYNC_SQLALCHEMY_DATABASE_URL, Base
from utils.import_util import ImportUtil
# 判断vesrions目录是否存在如果不存在则创建
alembic_veresions_path = 'alembic/versions'
if not os.path.exists(alembic_veresions_path):
os.makedirs(alembic_veresions_path)
# 自动查找所有模型
found_models = ImportUtil.find_models(Base)
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
alembic_config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
if alembic_config.config_file_name is not None:
fileConfig(alembic_config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
target_metadata = Base.metadata
# ASYNC_SQLALCHEMY_DATABASE_URL = 'mysql+asyncmy://root:mysqlroot@127.0.0.1:3306/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()

View 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"}

View 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,
)

Binary file not shown.

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