Compare commits

...
35 Commits
Author SHA1 Message Date
insistence 6dd08421b6 chore: 发布1.9.0版本 2026-02-13 16:50:52 +08:00
insistence a45594a4bc docs: 新增1.9.0版本更新日志 2026-02-13 16:50:23 +08:00
insistence d0127ca07c chore: 升级依赖 2026-02-13 16:47:36 +08:00
insistence b342b24aa5 perf: 优化ci测试稳定性 (#90) 2026-02-13 16:43:23 +08:00
董煜andsmock 76343f5838 perf: 优化验证码计算结果为非负数 (#88)
* 非负数结果的验证码更友好一些

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

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

This reverts commit 9d3eb66741.

---------

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

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

* rollback: 恢复导入策略

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

* perf: 优化DNS探测

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

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

* perf: 优化AI SDK为懒加载

---------

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

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

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

* chore: 升级前端依赖

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

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

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

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

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

* chore: 更新.gitignore规则

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

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

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

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

* perf: 优化函数无用参数

* perf: 优化env配置

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

* feat: 新增server_util

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

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

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

* perf: 优化ai_chat_controller的order_num

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

* feat: 新增加解密工具类

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

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

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

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

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

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

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

* docs: 新增更新日志

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

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

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

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

* fix: 增加mysql服务检查

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

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

* feat: 新增pg测试

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

* perf: 优化测试

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

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

+9
View File
@@ -0,0 +1,9 @@
import { createPinia } from "pinia";
import { useUserStore } from "./modules/user";
import { useConfigStore } from "./modules/config";
const pinia = createPinia();
export default pinia;
export { useUserStore, useConfigStore };
@@ -0,0 +1,13 @@
import { defineStore } from "pinia";
import { ref } from "vue";
export const useConfigStore = defineStore("config", () => {
const config = ref();
const setConfig = (val) => {
config.value = val;
};
return {
config,
setConfig,
};
});
@@ -0,0 +1,56 @@
import { defineStore } from "pinia";
const useDictStore = defineStore("dict", {
state: () => ({
dict: new Array(),
}),
actions: {
// 获取字典
getDict(_key) {
if (_key == null && _key == "") {
return null;
}
try {
for (let i = 0; i < this.dict.length; i++) {
if (this.dict[i].key == _key) {
return this.dict[i].value;
}
}
} catch (e) {
return null;
}
},
// 设置字典
setDict(_key, value) {
if (_key !== null && _key !== "") {
this.dict.push({
key: _key,
value: value,
});
}
},
// 删除字典
removeDict(_key) {
var bln = false;
try {
for (let i = 0; i < this.dict.length; i++) {
if (this.dict[i].key == _key) {
this.dict.splice(i, 1);
return true;
}
}
} catch (e) {
bln = false;
}
return bln;
},
// 清空字典
cleanDict() {
this.dict = new Array();
},
// 初始字典
initDict() {},
},
});
export default useDictStore;
+125
View File
@@ -0,0 +1,125 @@
import { defineStore } from "pinia";
import { ref } from "vue";
import config from "@/config";
import storage from "@/utils/storage";
import constant from "@/utils/constant";
import { isHttp, isEmpty } from "@/utils/validate";
import { getInfo, login, logout } from "@/api/login";
import { getToken, removeToken, setToken } from "@/utils/auth";
import defAva from "@/static/images/profile.jpg";
const baseUrl = config.baseUrl;
export const useUserStore = defineStore("user", () => {
const token = ref(getToken());
const id = ref(storage.get(constant.id));
const name = ref(storage.get(constant.name));
const avatar = ref(storage.get(constant.avatar));
const roles = ref(storage.get(constant.roles));
const permissions = ref(storage.get(constant.permissions));
const SET_TOKEN = (val) => {
token.value = val;
};
const SET_ID = (val) => {
id.value = val;
storage.set(constant.id, val);
};
const SET_NAME = (val) => {
name.value = val;
storage.set(constant.name, val);
};
const SET_AVATAR = (val) => {
avatar.value = val;
storage.set(constant.avatar, val);
};
const SET_ROLES = (val) => {
roles.value = val;
storage.set(constant.roles, val);
};
const SET_PERMISSIONS = (val) => {
permissions.value = val;
storage.set(constant.permissions, val);
};
// 登录
const loginAction = (userInfo) => {
const username = userInfo.username.trim();
const password = userInfo.password;
const code = userInfo.code;
const uuid = userInfo.uuid;
return new Promise((resolve, reject) => {
login(username, password, code, uuid)
.then((res) => {
setToken(res.token);
SET_TOKEN(res.token);
resolve();
})
.catch((error) => {
reject(error);
});
});
};
// 获取用户信息
const getInfoAction = () => {
return new Promise((resolve, reject) => {
getInfo()
.then((res) => {
const user = res.user;
let avatar = user.avatar || "";
if (!isHttp(avatar)) {
avatar = isEmpty(avatar) ? defAva : baseUrl + avatar;
}
const userid =
isEmpty(user) || isEmpty(user.userId) ? "" : user.userId;
const username =
isEmpty(user) || isEmpty(user.userName) ? "" : user.userName;
if (res.roles && res.roles.length > 0) {
SET_ROLES(res.roles);
SET_PERMISSIONS(res.permissions);
} else {
SET_ROLES(["ROLE_DEFAULT"]);
}
SET_ID(userid);
SET_NAME(username);
SET_AVATAR(avatar);
resolve(res);
})
.catch((error) => {
reject(error);
});
});
};
// 退出系统
const logOutAction = () => {
return new Promise((resolve, reject) => {
logout(token.value)
.then(() => {
SET_TOKEN("");
SET_ROLES([]);
SET_PERMISSIONS([]);
removeToken();
storage.clean();
resolve();
})
.catch((error) => {
reject(error);
});
});
};
return {
token,
id,
name,
avatar,
roles,
permissions,
SET_AVATAR,
login: loginAction,
getInfo: getInfoAction,
logOut: logOutAction,
};
});
+10
View File
@@ -0,0 +1,10 @@
{
"light": {
"navBgColor": "#f6f6f6",
"navTxtStyle": "black"
},
"dark": {
"navBgColor": "#191919",
"navTxtStyle": "white"
}
}
View File
+13
View File
@@ -0,0 +1,13 @@
const TokenKey = "App-Token";
export function getToken() {
return uni.getStorageSync(TokenKey);
}
export function setToken(token) {
return uni.setStorageSync(TokenKey, token);
}
export function removeToken() {
return uni.removeStorageSync(TokenKey);
}
+58
View File
@@ -0,0 +1,58 @@
/**
* 显示消息提示框
* @param content 提示的标题
*/
export function toast(content) {
uni.showToast({
icon: "none",
title: content,
});
}
/**
* 显示模态弹窗
* @param content 提示的标题
*/
export function showConfirm(content) {
return new Promise((resolve, reject) => {
uni.showModal({
title: "提示",
content: content,
cancelText: "取消",
confirmText: "确定",
success: function (res) {
resolve(res);
},
});
});
}
/**
* 参数处理
* @param params 参数
*/
export function tansParams(params) {
let result = "";
for (const propName of Object.keys(params)) {
const value = params[propName];
var part = encodeURIComponent(propName) + "=";
if (value !== null && value !== "" && typeof value !== "undefined") {
if (typeof value === "object") {
for (const key of Object.keys(value)) {
if (
value[key] !== null &&
value[key] !== "" &&
typeof value[key] !== "undefined"
) {
let params = propName + "[" + key + "]";
var subPart = encodeURIComponent(params) + "=";
result += subPart + encodeURIComponent(value[key]) + "&";
}
}
} else {
result += part + encodeURIComponent(value) + "&";
}
}
}
return result;
}
+9
View File
@@ -0,0 +1,9 @@
const constant = {
avatar: "user_avatar",
id: "user_id",
name: "user_name",
roles: "user_roles",
permissions: "user_permissions",
};
export default constant;
+30
View File
@@ -0,0 +1,30 @@
import useDictStore from "@/store/modules/dict";
import { getDicts } from "@/api/system/dict/data";
import { ref, toRefs } from "vue";
/**
* 获取字典数据
*/
export function useDict(...args) {
const res = ref({});
return (() => {
args.forEach((dictType, index) => {
res.value[dictType] = [];
const dicts = useDictStore().getDict(dictType);
if (dicts) {
res.value[dictType] = dicts;
} else {
getDicts(dictType).then((resp) => {
res.value[dictType] = resp.data.map((p) => ({
label: p.dictLabel,
value: p.dictValue,
elTagType: p.listClass,
elTagClass: p.cssClass,
}));
useDictStore().setDict(dictType, res.value[dictType]);
});
}
});
return toRefs(res.value);
})();
}
+6
View File
@@ -0,0 +1,6 @@
export default {
401: "认证失败,无法访问系统资源",
403: "当前操作没有权限",
404: "访问资源不存在",
default: "系统未知错误,请反馈给管理员",
};
+55
View File
@@ -0,0 +1,55 @@
import store from "@/store";
/**
* 字符权限校验
* @param {Array} value 校验值
* @returns {Boolean}
*/
export function checkPermi(value) {
if (value && value instanceof Array && value.length > 0) {
const permissions = store.getters && store.getters.permissions;
const permissionDatas = value;
const all_permission = "*:*:*";
const hasPermission = permissions.some((permission) => {
return (
all_permission === permission || permissionDatas.includes(permission)
);
});
if (!hasPermission) {
return false;
}
return true;
} else {
console.error(
`need roles! Like checkPermi="['system:user:add','system:user:edit']"`,
);
return false;
}
}
/**
* 角色权限校验
* @param {Array} value 校验值
* @returns {Boolean}
*/
export function checkRole(value) {
if (value && value instanceof Array && value.length > 0) {
const roles = store.getters && store.getters.roles;
const permissionRoles = value;
const super_admin = "admin";
const hasRole = roles.some((role) => {
return super_admin === role || permissionRoles.includes(role);
});
if (!hasRole) {
return false;
}
return true;
} else {
console.error(`need roles! Like checkRole="['admin','editor']"`);
return false;
}
}
+74
View File
@@ -0,0 +1,74 @@
import config from "@/config";
import { getToken } from "@/utils/auth";
import errorCode from "@/utils/errorCode";
import { useUserStore } from "@/store/modules/user";
import { toast, showConfirm, tansParams } from "@/utils/common";
let timeout = 10000;
const baseUrl = config.baseUrl;
const request = (config) => {
// 是否需要设置 token
const isToken = (config.headers || {}).isToken === false;
config.header = config.header || {};
if (getToken() && !isToken) {
config.header["Authorization"] = "Bearer " + getToken();
}
// get请求映射params参数
if (config.params) {
let url = config.url + "?" + tansParams(config.params);
url = url.slice(0, -1);
config.url = url;
}
return new Promise((resolve, reject) => {
uni
.request({
method: config.method || "get",
timeout: config.timeout || timeout,
url: config.baseUrl || baseUrl + config.url,
data: config.data,
header: config.header,
dataType: "json",
})
.then((response) => {
const res = response;
const code = res.data.code || 200;
const msg = errorCode[code] || res.data.msg || errorCode["default"];
if (code === 401) {
showConfirm(
"登录状态已过期,您可以继续留在该页面,或者重新登录?",
).then((res) => {
if (res.confirm) {
useUserStore()
.logOut()
.then((res) => {
uni.reLaunch({ url: "/pages/login" });
});
}
});
reject("无效的会话,或者会话已过期,请重新登录。");
} else if (code === 500) {
toast(msg);
reject("500");
} else if (code !== 200) {
toast(msg);
reject(code);
}
resolve(res.data);
})
.catch((error) => {
let { message } = error;
if (message === "Network Error") {
message = "后端接口连接异常";
} else if (message.includes("timeout")) {
message = "系统接口请求超时";
} else if (message.includes("Request failed with status code")) {
message = "系统接口" + message.substr(message.length - 3) + "异常";
}
toast(message);
reject(error);
});
});
};
export default request;
+38
View File
@@ -0,0 +1,38 @@
import constant from "./constant";
// 存储变量名
let storageKey = "storage_data";
// 存储节点变量名
let storageNodeKeys = [
constant.avatar,
constant.id,
constant.name,
constant.roles,
constant.permissions,
];
const storage = {
set: function (key, value) {
if (storageNodeKeys.indexOf(key) != -1) {
let tmp = uni.getStorageSync(storageKey);
tmp = tmp ? tmp : {};
tmp[key] = value;
uni.setStorageSync(storageKey, tmp);
}
},
get: function (key) {
let storageData = uni.getStorageSync(storageKey) || {};
return storageData[key] || "";
},
remove: function (key) {
let storageData = uni.getStorageSync(storageKey) || {};
delete storageData[key];
uni.setStorageSync(storageKey, storageData);
},
clean: function () {
uni.removeStorageSync(storageKey);
},
};
export default storage;
+72
View File
@@ -0,0 +1,72 @@
import { useUserStore } from "@/store";
import config from "@/config";
import { getToken } from "@/utils/auth";
import errorCode from "@/utils/errorCode";
import { toast, showConfirm, tansParams } from "@/utils/common";
let timeout = 10000;
const baseUrl = config.baseUrl;
export default function upload(config) {
// 是否需要设置 token
const isToken = (config.headers || {}).isToken === false;
config.header = config.header || {};
if (getToken() && !isToken) {
config.header["Authorization"] = "Bearer " + getToken();
}
// get请求映射params参数
if (config.params) {
let url = config.url + "?" + tansParams(config.params);
url = url.slice(0, -1);
config.url = url;
}
return new Promise((resolve, reject) => {
uni.uploadFile({
timeout: config.timeout || timeout,
url: baseUrl + config.url,
filePath: config.filePath,
name: config.name || "file",
header: config.header,
formData: config.formData,
success: (res) => {
let result = JSON.parse(res.data);
const code = result.code || 200;
const msg = errorCode[code] || result.msg || errorCode["default"];
if (code === 200) {
resolve(result);
} else if (code == 401) {
showConfirm(
"登录状态已过期,您可以继续留在该页面,或者重新登录?",
).then((res) => {
if (res.confirm) {
useUserStore()
.logOut()
.then((res) => {
uni.reLaunch({ url: "/pages/login/login" });
});
}
});
reject("无效的会话,或者会话已过期,请重新登录。");
} else if (code === 500) {
toast(msg);
reject("500");
} else if (code !== 200) {
toast(msg);
reject(code);
}
},
fail: (error) => {
let { message } = error;
if (message == "Network Error") {
message = "后端接口连接异常";
} else if (message.includes("timeout")) {
message = "系统接口请求超时";
} else if (message.includes("Request failed with status code")) {
message = "系统接口" + message.substr(message.length - 3) + "异常";
}
toast(message);
reject(error);
},
});
});
}
+124
View File
@@ -0,0 +1,124 @@
/**
* 路径匹配器
* @param {string} pattern
* @param {string} path
* @returns {Boolean}
*/
export function isPathMatch(pattern, path) {
const regexPattern = pattern
.replace(/\//g, "\\/")
.replace(/\*\*/g, ".*")
.replace(/\*/g, "[^\\/]*");
const regex = new RegExp(`^${regexPattern}$`);
return regex.test(path);
}
/**
* 判断value字符串是否为空
* @param {string} value
* @returns {Boolean}
*/
export function isEmpty(value) {
if (
value == null ||
value == "" ||
value == undefined ||
value == "undefined"
) {
return true;
}
return false;
}
/**
* 判断url是否是http或https
* @param {string} url
* @returns {Boolean}
*/
export function isHttp(url) {
return url.indexOf("http://") !== -1 || url.indexOf("https://") !== -1;
}
/**
* 判断path是否为外链
* @param {string} path
* @returns {Boolean}
*/
export function isExternal(path) {
return /^(https?:|mailto:|tel:)/.test(path);
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function validUsername(str) {
const valid_map = ["admin", "editor"];
return valid_map.indexOf(str.trim()) >= 0;
}
/**
* @param {string} url
* @returns {Boolean}
*/
export function validURL(url) {
const reg =
/^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/;
return reg.test(url);
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function validLowerCase(str) {
const reg = /^[a-z]+$/;
return reg.test(str);
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function validUpperCase(str) {
const reg = /^[A-Z]+$/;
return reg.test(str);
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function validAlphabets(str) {
const reg = /^[A-Za-z]+$/;
return reg.test(str);
}
/**
* @param {string} email
* @returns {Boolean}
*/
export function validEmail(email) {
const reg =
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return reg.test(email);
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function isString(str) {
return typeof str === "string" || str instanceof String;
}
/**
* @param {Array} arg
* @returns {Boolean}
*/
export function isArray(arg) {
if (typeof Array.isArray === "undefined") {
return Object.prototype.toString.call(arg) === "[object Array]";
}
return Array.isArray(arg);
}
+3
View File
@@ -0,0 +1,3 @@
import { icebreaker } from "@icebreakers/stylelint-config";
export default icebreaker();
+53
View File
@@ -0,0 +1,53 @@
import type { Config } from "tailwindcss";
import { getIconCollections, iconsPlugin } from "@egoist/tailwindcss-icons";
import cssMacro from "weapp-tailwindcss/css-macro";
import { isMp } from "./platform";
export default <Config>{
content: ["./index.html", "./src/**/*.{html,js,ts,jsx,tsx,vue}"],
theme: {
extend: {
// colors: {
// // 你可以在这里进行颜色的扩展
// primary: {
// 'DEFAULT': 'var(--color-primary, #0089FF)',
// 'light-3': 'var(--color-primary-light-3, rgb(85, 199, 255))',
// 'light-5': 'var(--color-primary-light-5, rgb(130, 217, 255))',
// 'light-7': 'var(--color-primary-light-7, rgb(175, 235, 255))',
// 'light-9': 'var(--color-primary-light-9, rgb(219, 252, 255))',
// 'dark-2': 'var(--color-primary-dark-2, rgb(0, 135, 204))',
// },
// },
},
},
// https://tw.icebreaker.top/docs/quick-start/uni-app-css-macro
plugins: [
cssMacro({
variantsMap: {
wx: "MP-WEIXIN",
"-wx": {
value: "MP-WEIXIN",
negative: true,
},
// 定义多个条件判断
// mv: {
// value: 'H5 || MP-WEIXIN'
// },
// '-mv': {
// value: 'H5 || MP-WEIXIN',
// negative: true
// }
},
}),
iconsPlugin({
// 在这里可以选择你要使用的 icon, 更多详见:
// https://icon-sets.iconify.design/
collections: getIconCollections(["svg-spinners", "mdi"]),
}),
],
corePlugins: {
// 小程序去使用 h5 的 preflight 和响应式 container 没有意义
preflight: !isMp,
container: !isMp,
},
};
+22
View File
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "esnext",
"outDir": "true",
"jsx": "preserve",
"lib": ["esnext", "dom"],
"useDefineForClassFields": true,
"baseUrl": ".",
"module": "esnext",
"moduleResolution": "bundler",
"paths": {
"@/*": ["src/*"]
},
"resolveJsonModule": true,
"types": ["@dcloudio/types"],
"allowJs": true,
"strict": true,
"sourceMap": true,
"esModuleInterop": true
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}
+39
View File
@@ -0,0 +1,39 @@
import uni from "@dcloudio/vite-plugin-uni";
import { defineConfig } from "vite";
import { UnifiedViteWeappTailwindcssPlugin } from "weapp-tailwindcss/vite";
import { WeappTailwindcssDisabled } from "./platform";
import postcssPlugins from "./postcss.config";
// https://vitejs.dev/config/
export default defineConfig(async () => {
// 新版本的 unplugin-auto-import 改成了只有 esm 格式的产物,而 uni-app 目前必须 cjs 格式
// 所以需要改成动态 import 的写法来进行引入
// 详见 https://github.com/sonofmagic/uni-app-vite-vue3-tailwind-vscode-template/issues/29
const { default: AutoImport } = await import("unplugin-auto-import/vite");
return {
// uvtw 一定要放在 uni 后面
plugins: [
uni(),
UnifiedViteWeappTailwindcssPlugin({
rem2rpx: true,
disabled: WeappTailwindcssDisabled,
}),
AutoImport({
imports: ["vue", "uni-app", "pinia"],
dts: "./src/auto-imports.d.ts",
}),
],
// 内联 postcss 注册 tailwindcss
css: {
postcss: {
plugins: postcssPlugins,
},
// https://vitejs.dev/config/shared-options.html#css-preprocessoroptions
preprocessorOptions: {
scss: {
silenceDeprecations: ["legacy-js-api"],
},
},
},
};
});
+56 -2
View File
@@ -10,13 +10,21 @@ APP_HOST = '0.0.0.0'
# 应用端口 # 应用端口
APP_PORT = 9099 APP_PORT = 9099
# 应用版本 # 应用版本
APP_VERSION= '1.8.0' APP_VERSION= '1.9.0'
# 应用是否开启热重载 # 应用是否开启热重载
APP_RELOAD = true APP_RELOAD = true
# 应用工作进程数
APP_WORKERS = 1
# 应用是否开启IP归属区域查询 # 应用是否开启IP归属区域查询
APP_IP_LOCATION_QUERY = true APP_IP_LOCATION_QUERY = true
# 应用是否允许账号同时登录 # 应用是否允许账号同时登录
APP_SAME_TIME_LOGIN = true APP_SAME_TIME_LOGIN = true
# 应用是否为演示模式
APP_DEMO_MODE = false
# 应用是否禁用Swagger文档
APP_DISABLE_SWAGGER = false
# 应用是否禁用ReDoc文档
APP_DISABLE_REDOC = false
# -------- Jwt配置 -------- # -------- Jwt配置 --------
# Jwt秘钥 # Jwt秘钥
@@ -63,4 +71,50 @@ REDIS_USERNAME = ''
# Redis密码 # Redis密码
REDIS_PASSWORD = '' REDIS_PASSWORD = ''
# Redis数据库 # Redis数据库
REDIS_DATABASE = 2 REDIS_DATABASE = 2
# -------- 日志配置 --------
# Redis Stream Key
LOG_STREAM_KEY = 'log:stream'
# Redis Stream 消费组名称
LOG_STREAM_GROUP = 'log_aggregator'
# Redis Stream 消费者名称前缀
LOG_STREAM_CONSUMER_PREFIX = 'worker'
# 每次读取的最大消息数量
LOG_STREAM_BATCH_SIZE = 100
# 阻塞读取等待时间(毫秒)
LOG_STREAM_BLOCK_MS = 2000
# Stream 最大长度(近似裁剪)
LOG_STREAM_MAXLEN = 100000
# Pending 回收最小空闲时间(毫秒)
LOG_STREAM_CLAIM_IDLE_MS = 60000
# Pending 回收检查间隔(毫秒)
LOG_STREAM_CLAIM_INTERVAL_MS = 5000
# 每次回收的最大消息数量
LOG_STREAM_CLAIM_BATCH_SIZE = 100
# 去重 Key 过期时间(秒)
LOG_STREAM_DEDUP_TTL = 3600
# 去重 Key 前缀
LOG_STREAM_DEDUP_PREFIX = 'log:dedup'
# stdout 输出是否为 JSON
LOGURU_JSON = false
# Loguru 最低输出级别
LOGURU_LEVEL = 'INFO'
# 是否输出到 stdout
LOGURU_STDOUT = true
# 是否启用文件日志
LOG_FILE_ENABLED = true
# 文件日志根目录
LOG_FILE_BASE_DIR = 'logs'
# 文件滚动策略
LOGURU_ROTATION = '50MB'
# 文件保留策略
LOGURU_RETENTION = '30 days'
# 文件压缩格式
LOGURU_COMPRESSION = 'zip'
# 实例标识(用于区分实例)
LOG_INSTANCE_ID = 'dev'
# 服务名称(用于统一标识服务)
LOG_SERVICE_NAME = 'ruoyi-fastapi-backend'
# Worker 标识(auto 自动生成)
LOG_WORKER_ID = 'auto'
+56 -2
View File
@@ -10,13 +10,21 @@ APP_HOST = '0.0.0.0'
# 应用端口 # 应用端口
APP_PORT = 9099 APP_PORT = 9099
# 应用版本 # 应用版本
APP_VERSION= '1.8.0' APP_VERSION= '1.9.0'
# 应用是否开启热重载 # 应用是否开启热重载
APP_RELOAD = false APP_RELOAD = false
# 应用工作进程数
APP_WORKERS = 1
# 应用是否开启IP归属区域查询 # 应用是否开启IP归属区域查询
APP_IP_LOCATION_QUERY = true APP_IP_LOCATION_QUERY = true
# 应用是否允许账号同时登录 # 应用是否允许账号同时登录
APP_SAME_TIME_LOGIN = true APP_SAME_TIME_LOGIN = true
# 应用是否为演示模式
APP_DEMO_MODE = false
# 应用是否禁用Swagger文档
APP_DISABLE_SWAGGER = true
# 应用是否禁用ReDoc文档
APP_DISABLE_REDOC = true
# -------- Jwt配置 -------- # -------- Jwt配置 --------
# Jwt秘钥 # Jwt秘钥
@@ -63,4 +71,50 @@ REDIS_USERNAME = ''
# Redis密码 # Redis密码
REDIS_PASSWORD = '' REDIS_PASSWORD = ''
# Redis数据库 # Redis数据库
REDIS_DATABASE = 2 REDIS_DATABASE = 2
# -------- 日志配置 --------
# Redis Stream Key
LOG_STREAM_KEY = 'log:stream'
# Redis Stream 消费组名称
LOG_STREAM_GROUP = 'log_aggregator'
# Redis Stream 消费者名称前缀
LOG_STREAM_CONSUMER_PREFIX = 'worker'
# 每次读取的最大消息数量
LOG_STREAM_BATCH_SIZE = 100
# 阻塞读取等待时间(毫秒)
LOG_STREAM_BLOCK_MS = 2000
# Stream 最大长度(近似裁剪)
LOG_STREAM_MAXLEN = 100000
# Pending 回收最小空闲时间(毫秒)
LOG_STREAM_CLAIM_IDLE_MS = 60000
# Pending 回收检查间隔(毫秒)
LOG_STREAM_CLAIM_INTERVAL_MS = 5000
# 每次回收的最大消息数量
LOG_STREAM_CLAIM_BATCH_SIZE = 100
# 去重 Key 过期时间(秒)
LOG_STREAM_DEDUP_TTL = 3600
# 去重 Key 前缀
LOG_STREAM_DEDUP_PREFIX = 'log:dedup'
# stdout 输出是否为 JSON
LOGURU_JSON = false
# Loguru 最低输出级别
LOGURU_LEVEL = 'INFO'
# 是否输出到 stdout
LOGURU_STDOUT = true
# 是否启用文件日志
LOG_FILE_ENABLED = true
# 文件日志根目录
LOG_FILE_BASE_DIR = 'logs'
# 文件滚动策略
LOGURU_ROTATION = '50MB'
# 文件保留策略
LOGURU_RETENTION = '30 days'
# 文件压缩格式
LOGURU_COMPRESSION = 'zip'
# 实例标识(用于区分实例)
LOG_INSTANCE_ID = 'dockermy'
# 服务名称(用于统一标识服务)
LOG_SERVICE_NAME = 'ruoyi-fastapi-backend'
# Worker 标识(auto 自动生成)
LOG_WORKER_ID = 'auto'
+56 -2
View File
@@ -10,13 +10,21 @@ APP_HOST = '0.0.0.0'
# 应用端口 # 应用端口
APP_PORT = 9099 APP_PORT = 9099
# 应用版本 # 应用版本
APP_VERSION= '1.8.0' APP_VERSION= '1.9.0'
# 应用是否开启热重载 # 应用是否开启热重载
APP_RELOAD = false APP_RELOAD = false
# 应用工作进程数
APP_WORKERS = 1
# 应用是否开启IP归属区域查询 # 应用是否开启IP归属区域查询
APP_IP_LOCATION_QUERY = true APP_IP_LOCATION_QUERY = true
# 应用是否为演示模式
APP_DEMO_MODE = false
# 应用是否允许账号同时登录 # 应用是否允许账号同时登录
APP_SAME_TIME_LOGIN = true APP_SAME_TIME_LOGIN = true
# 应用是否禁用Swagger文档
APP_DISABLE_SWAGGER = true
# 应用是否禁用ReDoc文档
APP_DISABLE_REDOC = true
# -------- Jwt配置 -------- # -------- Jwt配置 --------
# Jwt秘钥 # Jwt秘钥
@@ -63,4 +71,50 @@ REDIS_USERNAME = ''
# Redis密码 # Redis密码
REDIS_PASSWORD = '' REDIS_PASSWORD = ''
# Redis数据库 # Redis数据库
REDIS_DATABASE = 2 REDIS_DATABASE = 2
# -------- 日志配置 --------
# Redis Stream Key
LOG_STREAM_KEY = 'log:stream'
# Redis Stream 消费组名称
LOG_STREAM_GROUP = 'log_aggregator'
# Redis Stream 消费者名称前缀
LOG_STREAM_CONSUMER_PREFIX = 'worker'
# 每次读取的最大消息数量
LOG_STREAM_BATCH_SIZE = 100
# 阻塞读取等待时间(毫秒)
LOG_STREAM_BLOCK_MS = 2000
# Stream 最大长度(近似裁剪)
LOG_STREAM_MAXLEN = 100000
# Pending 回收最小空闲时间(毫秒)
LOG_STREAM_CLAIM_IDLE_MS = 60000
# Pending 回收检查间隔(毫秒)
LOG_STREAM_CLAIM_INTERVAL_MS = 5000
# 每次回收的最大消息数量
LOG_STREAM_CLAIM_BATCH_SIZE = 100
# 去重 Key 过期时间(秒)
LOG_STREAM_DEDUP_TTL = 3600
# 去重 Key 前缀
LOG_STREAM_DEDUP_PREFIX = 'log:dedup'
# stdout 输出是否为 JSON
LOGURU_JSON = false
# Loguru 最低输出级别
LOGURU_LEVEL = 'INFO'
# 是否输出到 stdout
LOGURU_STDOUT = true
# 是否启用文件日志
LOG_FILE_ENABLED = true
# 文件日志根目录
LOG_FILE_BASE_DIR = 'logs'
# 文件滚动策略
LOGURU_ROTATION = '50MB'
# 文件保留策略
LOGURU_RETENTION = '30 days'
# 文件压缩格式
LOGURU_COMPRESSION = 'zip'
# 实例标识(用于区分实例)
LOG_INSTANCE_ID = 'dockerpg'
# 服务名称(用于统一标识服务)
LOG_SERVICE_NAME = 'ruoyi-fastapi-backend'
# Worker 标识(auto 自动生成)
LOG_WORKER_ID = 'auto'
+56 -2
View File
@@ -10,13 +10,21 @@ APP_HOST = '0.0.0.0'
# 应用端口 # 应用端口
APP_PORT = 9099 APP_PORT = 9099
# 应用版本 # 应用版本
APP_VERSION= '1.8.0' APP_VERSION= '1.9.0'
# 应用是否开启热重载 # 应用是否开启热重载
APP_RELOAD = false APP_RELOAD = false
# 应用工作进程数
APP_WORKERS = 1
# 应用是否开启IP归属区域查询 # 应用是否开启IP归属区域查询
APP_IP_LOCATION_QUERY = true APP_IP_LOCATION_QUERY = true
# 应用是否允许账号同时登录 # 应用是否允许账号同时登录
APP_SAME_TIME_LOGIN = true APP_SAME_TIME_LOGIN = true
# 应用是否为演示模式
APP_DEMO_MODE = false
# 应用是否禁用Swagger文档
APP_DISABLE_SWAGGER = true
# 应用是否禁用ReDoc文档
APP_DISABLE_REDOC = true
# -------- Jwt配置 -------- # -------- Jwt配置 --------
# Jwt秘钥 # Jwt秘钥
@@ -63,4 +71,50 @@ REDIS_USERNAME = ''
# Redis密码 # Redis密码
REDIS_PASSWORD = '' REDIS_PASSWORD = ''
# Redis数据库 # Redis数据库
REDIS_DATABASE = 2 REDIS_DATABASE = 2
# -------- 日志配置 --------
# Redis Stream Key
LOG_STREAM_KEY = 'log:stream'
# Redis Stream 消费组名称
LOG_STREAM_GROUP = 'log_aggregator'
# Redis Stream 消费者名称前缀
LOG_STREAM_CONSUMER_PREFIX = 'worker'
# 每次读取的最大消息数量
LOG_STREAM_BATCH_SIZE = 100
# 阻塞读取等待时间(毫秒)
LOG_STREAM_BLOCK_MS = 2000
# Stream 最大长度(近似裁剪)
LOG_STREAM_MAXLEN = 100000
# Pending 回收最小空闲时间(毫秒)
LOG_STREAM_CLAIM_IDLE_MS = 60000
# Pending 回收检查间隔(毫秒)
LOG_STREAM_CLAIM_INTERVAL_MS = 5000
# 每次回收的最大消息数量
LOG_STREAM_CLAIM_BATCH_SIZE = 100
# 去重 Key 过期时间(秒)
LOG_STREAM_DEDUP_TTL = 3600
# 去重 Key 前缀
LOG_STREAM_DEDUP_PREFIX = 'log:dedup'
# stdout 输出是否为 JSON
LOGURU_JSON = false
# Loguru 最低输出级别
LOGURU_LEVEL = 'INFO'
# 是否输出到 stdout
LOGURU_STDOUT = true
# 是否启用文件日志
LOG_FILE_ENABLED = true
# 文件日志根目录
LOG_FILE_BASE_DIR = 'logs'
# 文件滚动策略
LOGURU_ROTATION = '50MB'
# 文件保留策略
LOGURU_RETENTION = '30 days'
# 文件压缩格式
LOGURU_COMPRESSION = 'zip'
# 实例标识(用于区分实例)
LOG_INSTANCE_ID = 'prod'
# 服务名称(用于统一标识服务)
LOG_SERVICE_NAME = 'ruoyi-fastapi-backend'
# Worker 标识(auto 自动生成)
LOG_WORKER_ID = 'auto'
+13 -3
View File
@@ -1,8 +1,8 @@
import asyncio import asyncio
import os import os
from collections.abc import Iterable from collections.abc import Iterable, MutableMapping
from logging.config import fileConfig from logging.config import fileConfig
from typing import Optional, Union from typing import Literal
from alembic import context from alembic import context
from alembic.migration import MigrationContext from alembic.migration import MigrationContext
@@ -65,9 +65,18 @@ def run_migrations_offline() -> None:
def do_run_migrations(connection: Connection) -> None: 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( def process_revision_directives(
context: MigrationContext, context: MigrationContext,
revision: Union[str, Iterable[Optional[str]], Iterable[str]], revision: str | Iterable[str | None] | Iterable[str],
directives: list[MigrationScript], directives: list[MigrationScript],
) -> None: ) -> None:
script = directives[0] script = directives[0]
@@ -88,6 +97,7 @@ def do_run_migrations(connection: Connection) -> None:
compare_type=True, compare_type=True,
compare_server_default=True, compare_server_default=True,
transaction_per_migration=True, transaction_per_migration=True,
include_name=include_name,
process_revision_directives=process_revision_directives, process_revision_directives=process_revision_directives,
) )
+5 -2
View File
@@ -3,13 +3,16 @@ import uvicorn
from config.env import AppConfig from config.env import AppConfig
from server import create_app from server import create_app
app = create_app() if __name__ != '__main__':
app = create_app()
if __name__ == '__main__': if __name__ == '__main__':
uvicorn.run( uvicorn.run(
app='app:app', app='server:create_app',
host=AppConfig.app_host, host=AppConfig.app_host,
port=AppConfig.app_port, port=AppConfig.app_port,
root_path=AppConfig.app_root_path, root_path=AppConfig.app_root_path,
reload=AppConfig.app_reload, reload=AppConfig.app_reload,
workers=AppConfig.app_workers,
factory=True,
) )
@@ -1,16 +1,15 @@
import inspect import inspect
import json import json
import time import time
from collections.abc import Awaitable from collections.abc import Awaitable, Callable
from datetime import datetime from datetime import datetime
from functools import wraps from functools import wraps
from typing import Any, Callable, Literal, Optional, TypeVar from typing import Any, Literal, TypeVar
import httpx import httpx
from async_lru import alru_cache from async_lru import alru_cache
from fastapi import Request from fastapi import Request
from fastapi.responses import JSONResponse, ORJSONResponse, UJSONResponse from fastapi.responses import JSONResponse, ORJSONResponse, UJSONResponse
from sqlalchemy.ext.asyncio import AsyncSession
from starlette.status import HTTP_200_OK from starlette.status import HTTP_200_OK
from typing_extensions import ParamSpec from typing_extensions import ParamSpec
from user_agents import parse from user_agents import parse
@@ -20,7 +19,7 @@ from common.enums import BusinessType
from config.env import AppConfig from config.env import AppConfig
from exceptions.exception import LoginException, ServiceException, ServiceWarning from exceptions.exception import LoginException, ServiceException, ServiceWarning
from module_admin.entity.vo.log_vo import LogininforModel, OperLogModel from module_admin.entity.vo.log_vo import LogininforModel, OperLogModel
from module_admin.service.log_service import LoginLogService, OperationLogService from module_admin.service.log_service import LogQueueService
from utils.dependency_util import DependencyUtil from utils.dependency_util import DependencyUtil
from utils.log_util import logger from utils.log_util import logger
from utils.response_util import ResponseUtil from utils.response_util import ResponseUtil
@@ -38,7 +37,7 @@ class Log:
self, self,
title: str, title: str,
business_type: BusinessType, business_type: BusinessType,
log_type: Optional[Literal['login', 'operation']] = 'operation', log_type: Literal['login', 'operation'] | None = 'operation',
) -> None: ) -> None:
""" """
日志装饰器 日志装饰器
@@ -63,8 +62,6 @@ class Log:
request_name_list = get_function_parameters_name_by_type(func, Request) request_name_list = get_function_parameters_name_by_type(func, Request)
request = get_function_parameters_value_by_name(func, request_name_list[0], *args, **kwargs) request = get_function_parameters_value_by_name(func, request_name_list[0], *args, **kwargs)
DependencyUtil.check_exclude_routes(request, err_msg='当前路由不在认证规则内,不可使用Log装饰器') DependencyUtil.check_exclude_routes(request, err_msg='当前路由不在认证规则内,不可使用Log装饰器')
session_name_list = get_function_parameters_name_by_type(func, AsyncSession)
query_db = get_function_parameters_value_by_name(func, session_name_list[0], *args, **kwargs)
request_method = request.method request_method = request.method
user_agent = request.headers.get('User-Agent') user_agent = request.headers.get('User-Agent')
# 获取操作类型 # 获取操作类型
@@ -122,7 +119,7 @@ class Log:
} }
) )
await LoginLogService.add_login_log_services(query_db, LogininforModel(**login_log)) await LogQueueService.enqueue_login_log(request, LogininforModel(**login_log), func_path)
else: else:
current_user = RequestContext.get_current_user() current_user = RequestContext.get_current_user()
oper_name = current_user.user.user_name oper_name = current_user.user.user_name
@@ -145,7 +142,7 @@ class Log:
operTime=oper_time, operTime=oper_time,
costTime=int(cost_time), costTime=int(cost_time),
) )
await OperationLogService.add_operation_log_services(query_db, operation_log) await LogQueueService.enqueue_operation_log(request, operation_log, func_path)
return result return result
@@ -1,8 +1,10 @@
from typing import Optional
from fastapi import Depends, Request, params from fastapi import Depends, Request, params
from sqlalchemy import ColumnElement, func, or_, select
from common.context import RequestContext from common.context import RequestContext
from config.database import Base
from module_admin.entity.do.dept_do import SysDept
from module_admin.entity.do.role_do import SysRoleDept
from utils.dependency_util import DependencyUtil from utils.dependency_util import DependencyUtil
@@ -19,25 +21,22 @@ class GetDataScope:
def __init__( def __init__(
self, self,
query_alias: Optional[str] = '', query_alias: Base,
db_alias: Optional[str] = 'db', user_alias: str | None = 'user_id',
user_alias: Optional[str] = 'user_id', dept_alias: str | None = 'dept_id',
dept_alias: Optional[str] = 'dept_id',
) -> None: ) -> None:
""" """
获取当前用户数据权限对应的查询sql语句 获取当前用户数据权限对应的查询sql语句
:param query_alias: 所要查询表对应的sqlalchemy模型名称默认为'' :param query_alias: 所要查询表对应的sqlalchemy模型不可为空
:param db_alias: orm对象别名默认为'db'
:param user_alias: 用户id字段别名默认为'user_id' :param user_alias: 用户id字段别名默认为'user_id'
:param dept_alias: 部门id字段别名默认为'dept_id' :param dept_alias: 部门id字段别名默认为'dept_id'
""" """
self.query_alias = query_alias self.query_alias = query_alias
self.db_alias = db_alias
self.user_alias = user_alias self.user_alias = user_alias
self.dept_alias = dept_alias self.dept_alias = dept_alias
def __call__(self, request: Request) -> str: def __call__(self, request: Request) -> ColumnElement:
DependencyUtil.check_exclude_routes(request, err_msg='当前路由不在认证规则内,不可使用GetDataScope依赖项') DependencyUtil.check_exclude_routes(request, err_msg='当前路由不在认证规则内,不可使用GetDataScope依赖项')
current_user = RequestContext.get_current_user() current_user = RequestContext.get_current_user()
user_id = current_user.user.user_id user_id = current_user.user.user_id
@@ -48,50 +47,66 @@ class GetDataScope:
param_sql_list = [] param_sql_list = []
for role in current_user.user.role: for role in current_user.user.role:
if current_user.user.admin or role.data_scope == self.DATA_SCOPE_ALL: if current_user.user.admin or role.data_scope == self.DATA_SCOPE_ALL:
param_sql_list = ['1 == 1'] param_sql_list = [True]
break break
if role.data_scope == self.DATA_SCOPE_CUSTOM: if role.data_scope == self.DATA_SCOPE_CUSTOM:
if len(custom_data_scope_role_id_list) > 1: if len(custom_data_scope_role_id_list) > 1:
param_sql_list.append( param_sql_list.append(
f"{self.query_alias}.{self.dept_alias}.in_(select(SysRoleDept.dept_id).where(SysRoleDept.role_id.in_({custom_data_scope_role_id_list}))) if hasattr({self.query_alias}, '{self.dept_alias}') else 1 == 0" getattr(self.query_alias, self.dept_alias).in_(
select(SysRoleDept.dept_id).where(SysRoleDept.role_id.in_(custom_data_scope_role_id_list))
)
if hasattr(self.query_alias, self.dept_alias)
else False
) )
else: else:
param_sql_list.append( param_sql_list.append(
f"{self.query_alias}.{self.dept_alias}.in_(select(SysRoleDept.dept_id).where(SysRoleDept.role_id == {role.role_id})) if hasattr({self.query_alias}, '{self.dept_alias}') else 1 == 0" getattr(self.query_alias, self.dept_alias).in_(
select(SysRoleDept.dept_id).where(SysRoleDept.role_id == role.role_id)
)
if hasattr(self.query_alias, self.dept_alias)
else False
) )
elif role.data_scope == self.DATA_SCOPE_DEPT: elif role.data_scope == self.DATA_SCOPE_DEPT:
param_sql_list.append( param_sql_list.append(
f"{self.query_alias}.{self.dept_alias} == {dept_id} if hasattr({self.query_alias}, '{self.dept_alias}') else 1 == 0" getattr(self.query_alias, self.dept_alias) == dept_id
if hasattr(self.query_alias, self.dept_alias)
else False
) )
elif role.data_scope == self.DATA_SCOPE_DEPT_AND_CHILD: elif role.data_scope == self.DATA_SCOPE_DEPT_AND_CHILD:
param_sql_list.append( param_sql_list.append(
f"{self.query_alias}.{self.dept_alias}.in_(select(SysDept.dept_id).where(or_(SysDept.dept_id == {dept_id}, func.find_in_set({dept_id}, SysDept.ancestors)))) if hasattr({self.query_alias}, '{self.dept_alias}') else 1 == 0" getattr(self.query_alias, self.dept_alias).in_(
select(SysDept.dept_id).where(
or_(SysDept.dept_id == dept_id, func.find_in_set(dept_id, SysDept.ancestors))
)
)
if hasattr(self.query_alias, self.dept_alias)
else False
) )
elif role.data_scope == self.DATA_SCOPE_SELF: elif role.data_scope == self.DATA_SCOPE_SELF:
param_sql_list.append( param_sql_list.append(
f"{self.query_alias}.{self.user_alias} == {user_id} if hasattr({self.query_alias}, '{self.user_alias}') else 1 == 0" getattr(self.query_alias, self.user_alias) == user_id
if hasattr(self.query_alias, self.user_alias)
else False
) )
else: else:
param_sql_list.append('1 == 0') param_sql_list.append(False)
param_sql_list = list(dict.fromkeys(param_sql_list)) param_sql_list = list(dict.fromkeys(param_sql_list))
param_sql = f'or_({", ".join(param_sql_list)})' param_sql = or_(*param_sql_list)
return param_sql return param_sql
def DataScopeDependency( # noqa: N802 def DataScopeDependency( # noqa: N802
query_alias: Optional[str] = '', query_alias: Base,
db_alias: Optional[str] = 'db', user_alias: str | None = 'user_id',
user_alias: Optional[str] = 'user_id', dept_alias: str | None = 'dept_id',
dept_alias: Optional[str] = 'dept_id',
) -> params.Depends: ) -> params.Depends:
""" """
当前用户数据权限依赖 当前用户数据权限依赖
:param query_alias: 所要查询表对应的sqlalchemy模型名称默认为'' :param query_alias: 所要查询表对应的sqlalchemy模型不可为空
:param db_alias: orm对象别名默认为'db'
:param user_alias: 用户id字段别名默认为'user_id' :param user_alias: 用户id字段别名默认为'user_id'
:param dept_alias: 部门id字段别名默认为'dept_id' :param dept_alias: 部门id字段别名默认为'dept_id'
:return: 当前用户数据权限依赖 :return: 当前用户数据权限依赖
""" """
return Depends(GetDataScope(query_alias, db_alias, user_alias, dept_alias)) return Depends(GetDataScope(query_alias, user_alias, dept_alias))
@@ -1,5 +1,3 @@
from typing import Union
from fastapi import Depends, Request, params from fastapi import Depends, Request, params
from common.context import RequestContext from common.context import RequestContext
@@ -12,7 +10,7 @@ class CheckUserInterfaceAuth:
校验当前用户是否具有相应的接口权限 校验当前用户是否具有相应的接口权限
""" """
def __init__(self, perm: Union[str, list], is_strict: bool = False) -> None: def __init__(self, perm: str | list, is_strict: bool = False) -> None:
""" """
校验当前用户是否具有相应的接口权限 校验当前用户是否具有相应的接口权限
@@ -46,7 +44,7 @@ class CheckRoleInterfaceAuth:
根据角色校验当前用户是否具有相应的接口权限 根据角色校验当前用户是否具有相应的接口权限
""" """
def __init__(self, role_key: Union[str, list], is_strict: bool = False) -> None: def __init__(self, role_key: str | list, is_strict: bool = False) -> None:
""" """
根据角色校验当前用户是否具有相应的接口权限 根据角色校验当前用户是否具有相应的接口权限
@@ -74,7 +72,7 @@ class CheckRoleInterfaceAuth:
raise PermissionException(data='', message='该用户无此接口权限') raise PermissionException(data='', message='该用户无此接口权限')
def UserInterfaceAuthDependency(perm: Union[str, list], is_strict: bool = False) -> params.Depends: # noqa: N802 def UserInterfaceAuthDependency(perm: str | list, is_strict: bool = False) -> params.Depends: # noqa: N802
""" """
根据权限标识校验当前用户接口权限依赖 根据权限标识校验当前用户接口权限依赖
@@ -85,7 +83,7 @@ def UserInterfaceAuthDependency(perm: Union[str, list], is_strict: bool = False)
return Depends(CheckUserInterfaceAuth(perm, is_strict)) return Depends(CheckUserInterfaceAuth(perm, is_strict))
def RoleInterfaceAuthDependency(role_key: Union[str, list], is_strict: bool = False) -> params.Depends: # noqa: N802 def RoleInterfaceAuthDependency(role_key: str | list, is_strict: bool = False) -> params.Depends: # noqa: N802
""" """
根据角色校验当前用户接口权限依赖 根据角色校验当前用户接口权限依赖
@@ -1,5 +1,5 @@
import re import re
from typing import Literal, Optional, TypedDict, Union from typing import Literal, TypedDict
from fastapi import Depends, Request, params from fastapi import Depends, Request, params
from fastapi.security import OAuth2PasswordBearer from fastapi.security import OAuth2PasswordBearer
@@ -37,7 +37,7 @@ class PreAuth:
登录认证前置校验依赖类 登录认证前置校验依赖类
""" """
def __init__(self, exclude_routes: Optional[list[ExcludeRoute]] = None) -> None: def __init__(self, exclude_routes: list[ExcludeRoute] | None = None) -> None:
""" """
初始化登录认证前置校验依赖 初始化登录认证前置校验依赖
@@ -80,7 +80,7 @@ class PreAuth:
# 添加开始和结束锚点,确保精确匹配 # 添加开始和结束锚点,确保精确匹配
return re.compile(f'^{pattern_str}$') return re.compile(f'^{pattern_str}$')
async def __call__(self, request: Request, db: AsyncSession = Depends(get_db)) -> Union[CurrentUserModel, None]: async def __call__(self, request: Request, db: AsyncSession = Depends(get_db)) -> CurrentUserModel | None:
""" """
执行登录认证校验 执行登录认证校验
@@ -125,7 +125,7 @@ class PreAuth:
return current_user return current_user
def PreAuthDependency(exclude_routes: Optional[list[ExcludeRoute]] = None) -> params.Depends: # noqa: N802 def PreAuthDependency(exclude_routes: list[ExcludeRoute] | None = None) -> params.Depends: # noqa: N802
""" """
登录认证前置校验依赖 登录认证前置校验依赖
+10
View File
@@ -133,6 +133,16 @@ class JobConstant:
JOB_WHITE_LIST = ['module_task'] JOB_WHITE_LIST = ['module_task']
class LockConstant:
"""
分布式锁常量
"""
APP_STARTUP_LOCK_KEY = 'app:startup:lock'
LOCK_EXPIRE_SECONDS = 60
LOCK_RENEWAL_INTERVAL = 20
class MenuConstant: class MenuConstant:
""" """
菜单常量 菜单常量
+5 -9
View File
@@ -1,6 +1,6 @@
import re import re
from contextvars import ContextVar, Token from contextvars import ContextVar, Token
from typing import Literal, Optional, Union from typing import Literal
from exceptions.exception import LoginException from exceptions.exception import LoginException
from module_admin.entity.vo.user_vo import CurrentUserModel from module_admin.entity.vo.user_vo import CurrentUserModel
@@ -8,14 +8,10 @@ from module_admin.entity.vo.user_vo import CurrentUserModel
# 定义上下文变量 # 定义上下文变量
# 存储当前请求的编译后的排除路由模式列表 # 存储当前请求的编译后的排除路由模式列表
current_exclude_patterns: ContextVar[ current_exclude_patterns: ContextVar[
Optional[ list[dict[str, str | list[Literal['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']] | re.Pattern]] | None
list[
dict[str, Union[str, list[Literal['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']], re.Pattern]]
]
]
] = ContextVar('current_exclude_patterns', default=None) ] = ContextVar('current_exclude_patterns', default=None)
# 存储当前用户信息 # 存储当前用户信息
current_user: ContextVar[Optional[CurrentUserModel]] = ContextVar('current_user', default=None) current_user: ContextVar[CurrentUserModel | None] = ContextVar('current_user', default=None)
class RequestContext: class RequestContext:
@@ -26,7 +22,7 @@ class RequestContext:
@staticmethod @staticmethod
def set_current_exclude_patterns( def set_current_exclude_patterns(
exclude_patterns: list[ exclude_patterns: list[
dict[str, Union[str, list[Literal['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']], re.Pattern]] dict[str, str | list[Literal['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']] | re.Pattern]
], ],
) -> Token: ) -> Token:
""" """
@@ -39,7 +35,7 @@ class RequestContext:
@staticmethod @staticmethod
def get_current_exclude_patterns() -> list[ def get_current_exclude_patterns() -> list[
dict[str, Union[str, list[Literal['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']], re.Pattern]] dict[str, str | list[Literal['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']] | re.Pattern]
]: ]:
""" """
获取当前请求的编译后的排除路由模式列表 获取当前请求的编译后的排除路由模式列表
+2 -3
View File
@@ -1,5 +1,4 @@
from enum import Enum from enum import Enum
from typing import Union
class BusinessType(Enum): class BusinessType(Enum):
@@ -36,11 +35,11 @@ class RedisInitKeyConfig(Enum):
""" """
@property @property
def key(self) -> Union[str, None]: def key(self) -> str | None:
return self.value.get('key') return self.value.get('key')
@property @property
def remark(self) -> Union[str, None]: def remark(self) -> str | None:
return self.value.get('remark') return self.value.get('remark')
ACCESS_TOKEN = {'key': 'access_token', 'remark': '登录令牌信息'} ACCESS_TOKEN = {'key': 'access_token', 'remark': '登录令牌信息'}
+27 -39
View File
@@ -1,9 +1,10 @@
import glob
import importlib import importlib
import os import os
import sys import sys
from collections.abc import Sequence from collections.abc import Callable, Sequence
from enum import Enum from enum import Enum
from typing import Annotated, Any, Callable, Literal, Optional, Union from typing import Annotated, Any, Literal
from annotated_doc import Doc from annotated_doc import Doc
from fastapi import FastAPI, params from fastapi import FastAPI, params
@@ -51,7 +52,7 @@ class APIRouterPro(APIRouter):
order_num: Annotated[int, Doc('An optional order number for the router.')] = 100, order_num: Annotated[int, Doc('An optional order number for the router.')] = 100,
auto_register: Annotated[bool, Doc('An optional auto register flag for the router.')] = True, auto_register: Annotated[bool, Doc('An optional auto register flag for the router.')] = True,
tags: Annotated[ tags: Annotated[
Optional[list[Union[str, Enum]]], list[str | Enum] | None,
Doc( Doc(
""" """
A list of tags to be applied to all the *path operations* in this A list of tags to be applied to all the *path operations* in this
@@ -65,7 +66,7 @@ class APIRouterPro(APIRouter):
), ),
] = None, ] = None,
dependencies: Annotated[ dependencies: Annotated[
Optional[Sequence[params.Depends]], Sequence[params.Depends] | None,
Doc( Doc(
""" """
A list of dependencies (using `Depends()`) to be applied to all the A list of dependencies (using `Depends()`) to be applied to all the
@@ -88,7 +89,7 @@ class APIRouterPro(APIRouter):
), ),
] = Default(JSONResponse), ] = Default(JSONResponse),
responses: Annotated[ responses: Annotated[
Optional[dict[Union[int, str], dict[str, Any]]], dict[int | str, dict[str, Any]] | None,
Doc( Doc(
""" """
Additional responses to be shown in OpenAPI. Additional responses to be shown in OpenAPI.
@@ -104,7 +105,7 @@ class APIRouterPro(APIRouter):
), ),
] = None, ] = None,
callbacks: Annotated[ callbacks: Annotated[
Optional[list[BaseRoute]], list[BaseRoute] | None,
Doc( Doc(
""" """
OpenAPI callbacks that should apply to all *path operations* in this OpenAPI callbacks that should apply to all *path operations* in this
@@ -118,7 +119,7 @@ class APIRouterPro(APIRouter):
), ),
] = None, ] = None,
routes: Annotated[ routes: Annotated[
Optional[list[BaseRoute]], list[BaseRoute] | None,
Doc( Doc(
""" """
**Note**: you probably shouldn't use this parameter, it is inherited **Note**: you probably shouldn't use this parameter, it is inherited
@@ -149,7 +150,7 @@ class APIRouterPro(APIRouter):
), ),
] = True, ] = True,
default: Annotated[ default: Annotated[
Optional[ASGIApp], ASGIApp | None,
Doc( Doc(
""" """
Default function handler for this router. Used to handle Default function handler for this router. Used to handle
@@ -158,7 +159,7 @@ class APIRouterPro(APIRouter):
), ),
] = None, ] = None,
dependency_overrides_provider: Annotated[ dependency_overrides_provider: Annotated[
Optional[Any], Any | None,
Doc( Doc(
""" """
Only used internally by FastAPI to handle dependency overrides. Only used internally by FastAPI to handle dependency overrides.
@@ -180,7 +181,7 @@ class APIRouterPro(APIRouter):
), ),
] = APIRoute, ] = APIRoute,
on_startup: Annotated[ on_startup: Annotated[
Optional[Sequence[Callable[[], Any]]], Sequence[Callable[[], Any]] | None,
Doc( Doc(
""" """
A list of startup event handler functions. A list of startup event handler functions.
@@ -192,7 +193,7 @@ class APIRouterPro(APIRouter):
), ),
] = None, ] = None,
on_shutdown: Annotated[ on_shutdown: Annotated[
Optional[Sequence[Callable[[], Any]]], Sequence[Callable[[], Any]] | None,
Doc( Doc(
""" """
A list of shutdown event handler functions. A list of shutdown event handler functions.
@@ -207,7 +208,7 @@ class APIRouterPro(APIRouter):
# the generic to Lifespan[AppType] is the type of the top level application # the generic to Lifespan[AppType] is the type of the top level application
# which the router cannot know statically, so we use typing.Any # which the router cannot know statically, so we use typing.Any
lifespan: Annotated[ lifespan: Annotated[
Optional[Lifespan[Any]], Lifespan[Any] | None,
Doc( Doc(
""" """
A `Lifespan` context manager handler. This replaces `startup` and A `Lifespan` context manager handler. This replaces `startup` and
@@ -219,7 +220,7 @@ class APIRouterPro(APIRouter):
), ),
] = None, ] = None,
deprecated: Annotated[ deprecated: Annotated[
Optional[bool], bool | None,
Doc( Doc(
""" """
Mark all *path operations* in this router as deprecated. Mark all *path operations* in this router as deprecated.
@@ -306,17 +307,8 @@ class RouterRegister:
:return: py文件路径列表 :return: py文件路径列表
""" """
controller_files = [] pattern = os.path.join(self.project_root, '*', 'controller', '[!_]*.py')
# 遍历所有目录,查找controller目录 return sorted(glob.glob(pattern))
for root, _dirs, files in os.walk(self.project_root):
# 检查当前目录是否为controller目录
if os.path.basename(root) == 'controller':
# 遍历controller目录下的所有py文件
for file in files:
if file.endswith('.py') and not file.startswith('__'):
file_path = os.path.join(root, file)
controller_files.append(file_path)
return controller_files
def _import_module_and_get_routers(self, controller_files: list[str]) -> list[tuple[str, APIRouter]]: def _import_module_and_get_routers(self, controller_files: list[str]) -> list[tuple[str, APIRouter]]:
""" """
@@ -331,21 +323,17 @@ class RouterRegister:
relative_path = os.path.relpath(file_path, self.project_root) relative_path = os.path.relpath(file_path, self.project_root)
module_name = relative_path.replace(os.sep, '.')[:-3] module_name = relative_path.replace(os.sep, '.')[:-3]
try: # 动态导入模块
# 动态导入模块 module = importlib.import_module(module_name)
module = importlib.import_module(module_name) # 直接遍历模块__dict__,只检查模块自身定义的属性
# 遍历模块属性,寻找APIRouter和APIRouterPro实例 for attr_name, attr in module.__dict__.items():
for attr_name in dir(module): # 对于APIRouterPro实例,只有当auto_register=True时才添加
attr = getattr(module, attr_name) if isinstance(attr, APIRouterPro):
# 对于APIRouterPro实例,只有当auto_register=True时才添加 if attr.auto_register:
if isinstance(attr, APIRouterPro):
if attr.auto_register:
routers.append((attr_name, attr))
# 对于APIRouter实例,直接添加
elif isinstance(attr, APIRouter):
routers.append((attr_name, attr)) routers.append((attr_name, attr))
except Exception as e: # 对于APIRouter实例,直接添加
print(f'Error importing module {module_name}: {e}') elif isinstance(attr, APIRouter):
routers.append((attr_name, attr))
return routers return routers
def _sort_routers(self, routers: list[tuple[str, APIRouter]]) -> list[tuple[str, APIRouter]]: def _sort_routers(self, routers: list[tuple[str, APIRouter]]) -> list[tuple[str, APIRouter]]:
@@ -357,7 +345,7 @@ class RouterRegister:
""" """
# 按规则排序路由 # 按规则排序路由
def sort_key(item: tuple[str, APIRouter]) -> Union[tuple[Literal[0], int, str], tuple[Literal[1], str]]: def sort_key(item: tuple[str, APIRouter]) -> tuple[Literal[0], int, str] | tuple[Literal[1], str]:
attr_name, router = item attr_name, router = item
# APIRouterPro实例按order_num排序,序号越小越靠前 # APIRouterPro实例按order_num排序,序号越小越靠前
if isinstance(router, APIRouterPro): if isinstance(router, APIRouterPro):
+3 -3
View File
@@ -1,5 +1,5 @@
from datetime import datetime from datetime import datetime
from typing import Any, Generic, Optional, TypeVar, Union from typing import Any, Generic, TypeVar
from pydantic import BaseModel, ConfigDict, Field, create_model from pydantic import BaseModel, ConfigDict, Field, create_model
from pydantic.alias_generators import to_camel from pydantic.alias_generators import to_camel
@@ -17,7 +17,7 @@ class CrudResponseModel(BaseModel):
is_success: bool = Field(description='操作是否成功') is_success: bool = Field(description='操作是否成功')
message: str = Field(description='响应信息') message: str = Field(description='响应信息')
result: Optional[Any] = Field(default=None, description='响应结果') result: Any | None = Field(default=None, description='响应结果')
class ResponseBaseModel(BaseModel): class ResponseBaseModel(BaseModel):
@@ -38,7 +38,7 @@ class DynamicResponseModel(ResponseBaseModel, Generic[T]):
model_config = ConfigDict(alias_generator=to_camel) model_config = ConfigDict(alias_generator=to_camel)
def __class_getitem__(cls, item: Any) -> Union[Any, Self]: def __class_getitem__(cls, item: Any) -> Any | Self:
""" """
当使用 DynamicResponseModel[Item] 语法时动态创建一个包含所有字段的新模型 当使用 DynamicResponseModel[Item] 语法时动态创建一个包含所有字段的新模型
""" """
+97 -18
View File
@@ -1,29 +1,108 @@
from urllib.parse import quote_plus from urllib.parse import quote_plus
from sqlalchemy.ext.asyncio import AsyncAttrs, async_sessionmaker, create_async_engine from sqlalchemy import Engine, create_engine
from sqlalchemy.orm import DeclarativeBase from sqlalchemy.ext.asyncio import AsyncAttrs, AsyncEngine, async_sessionmaker, create_async_engine
from sqlalchemy.orm import DeclarativeBase, sessionmaker
from config.env import DataBaseConfig from config.env import DataBaseConfig
ASYNC_SQLALCHEMY_DATABASE_URL = (
f'mysql+asyncmy://{DataBaseConfig.db_username}:{quote_plus(DataBaseConfig.db_password)}@' def build_async_sqlalchemy_database_url() -> str:
f'{DataBaseConfig.db_host}:{DataBaseConfig.db_port}/{DataBaseConfig.db_database}' """
) 构建异步 SQLAlchemy 数据库连接 URL
if DataBaseConfig.db_type == 'postgresql':
ASYNC_SQLALCHEMY_DATABASE_URL = ( :return: 异步 SQLAlchemy 数据库连接 URL
f'postgresql+asyncpg://{DataBaseConfig.db_username}:{quote_plus(DataBaseConfig.db_password)}@' """
if DataBaseConfig.db_type == 'postgresql':
return (
f'postgresql+asyncpg://{DataBaseConfig.db_username}:{quote_plus(DataBaseConfig.db_password)}@'
f'{DataBaseConfig.db_host}:{DataBaseConfig.db_port}/{DataBaseConfig.db_database}'
)
return (
f'mysql+asyncmy://{DataBaseConfig.db_username}:{quote_plus(DataBaseConfig.db_password)}@'
f'{DataBaseConfig.db_host}:{DataBaseConfig.db_port}/{DataBaseConfig.db_database}' f'{DataBaseConfig.db_host}:{DataBaseConfig.db_port}/{DataBaseConfig.db_database}'
) )
async_engine = create_async_engine(
ASYNC_SQLALCHEMY_DATABASE_URL, ASYNC_SQLALCHEMY_DATABASE_URL = build_async_sqlalchemy_database_url()
echo=DataBaseConfig.db_echo,
max_overflow=DataBaseConfig.db_max_overflow,
pool_size=DataBaseConfig.db_pool_size, def build_sync_sqlalchemy_database_url() -> str:
pool_recycle=DataBaseConfig.db_pool_recycle, """
pool_timeout=DataBaseConfig.db_pool_timeout, 构建同步 SQLAlchemy 数据库连接 URL
)
AsyncSessionLocal = async_sessionmaker(autocommit=False, autoflush=False, bind=async_engine) :return: 同步 SQLAlchemy 数据库连接 URL
"""
if DataBaseConfig.db_type == 'postgresql':
return (
f'postgresql+psycopg2://{DataBaseConfig.db_username}:{quote_plus(DataBaseConfig.db_password)}@'
f'{DataBaseConfig.db_host}:{DataBaseConfig.db_port}/{DataBaseConfig.db_database}'
)
return (
f'mysql+pymysql://{DataBaseConfig.db_username}:{quote_plus(DataBaseConfig.db_password)}@'
f'{DataBaseConfig.db_host}:{DataBaseConfig.db_port}/{DataBaseConfig.db_database}'
)
SYNC_SQLALCHEMY_DATABASE_URL = build_sync_sqlalchemy_database_url()
def create_async_db_engine(echo: bool | None = None) -> AsyncEngine:
"""
创建异步 SQLAlchemy Engine
:param echo: 可选是否输出 SQLAlchemy SQL 日志
:return: 异步 SQLAlchemy Engine
"""
return create_async_engine(
ASYNC_SQLALCHEMY_DATABASE_URL,
echo=DataBaseConfig.db_echo if echo is None else echo,
max_overflow=DataBaseConfig.db_max_overflow,
pool_size=DataBaseConfig.db_pool_size,
pool_recycle=DataBaseConfig.db_pool_recycle,
pool_timeout=DataBaseConfig.db_pool_timeout,
)
def create_sync_db_engine(echo: bool | None = None) -> Engine:
"""
创建同步 SQLAlchemy Engine
:param echo: 可选是否输出 SQLAlchemy SQL 日志
:return: 同步 SQLAlchemy Engine
"""
return create_engine(
SYNC_SQLALCHEMY_DATABASE_URL,
echo=DataBaseConfig.db_echo if echo is None else echo,
max_overflow=DataBaseConfig.db_max_overflow,
pool_size=DataBaseConfig.db_pool_size,
pool_recycle=DataBaseConfig.db_pool_recycle,
pool_timeout=DataBaseConfig.db_pool_timeout,
)
def create_async_session_local(engine: AsyncEngine) -> async_sessionmaker:
"""
创建异步 Session 工厂
:param engine: 异步 SQLAlchemy Engine
:return: 异步 Session 工厂
"""
return async_sessionmaker(autocommit=False, autoflush=False, bind=engine)
def create_sync_session_local(engine: Engine) -> sessionmaker:
"""
创建同步 Session 工厂
:param engine: 同步 SQLAlchemy Engine
:return: 同步 Session 工厂
"""
return sessionmaker(autocommit=False, autoflush=False, bind=engine)
async_engine = create_async_db_engine()
AsyncSessionLocal = create_async_session_local(async_engine)
class Base(AsyncAttrs, DeclarativeBase): class Base(AsyncAttrs, DeclarativeBase):
+42
View File
@@ -21,8 +21,12 @@ class AppSettings(BaseSettings):
app_port: int = 9099 app_port: int = 9099
app_version: str = '1.0.0' app_version: str = '1.0.0'
app_reload: bool = True app_reload: bool = True
app_workers: int = 1
app_ip_location_query: bool = True app_ip_location_query: bool = True
app_same_time_login: bool = True app_same_time_login: bool = True
app_demo_mode: bool = False
app_disable_swagger: bool = False
app_disable_redoc: bool = False
class JwtSettings(BaseSettings): class JwtSettings(BaseSettings):
@@ -73,6 +77,36 @@ class RedisSettings(BaseSettings):
redis_database: int = 2 redis_database: int = 2
class LogSettings(BaseSettings):
"""
日志与队列配置
"""
log_stream_key: str = 'log:stream'
log_stream_group: str = 'log_aggregator'
log_stream_consumer_prefix: str = 'worker'
log_stream_batch_size: int = 100
log_stream_block_ms: int = 2000
log_stream_maxlen: int = 100000
log_stream_claim_idle_ms: int = 60000
log_stream_claim_interval_ms: int = 5000
log_stream_claim_batch_size: int = 100
log_stream_dedup_ttl: int = 3600
log_stream_dedup_prefix: str = 'log:dedup'
loguru_json: bool = False
loguru_level: str = 'INFO'
loguru_stdout: bool = True
log_file_enabled: bool = True
log_file_base_dir: str = 'logs'
loguru_rotation: str = '50MB'
loguru_retention: str = '30 days'
loguru_compression: str = 'zip'
log_instance_id: str = 'prod'
log_service_name: str = 'ruoyi-fastapi-backend'
log_worker_id: str = 'auto'
class GenSettings: class GenSettings:
""" """
代码生成配置 代码生成配置
@@ -182,6 +216,12 @@ class GetConfig:
# 实例化Redis配置模型 # 实例化Redis配置模型
return RedisSettings() return RedisSettings()
def get_log_config(self) -> LogSettings:
"""
获取日志配置
"""
return LogSettings()
def get_gen_config(self) -> GenSettings: def get_gen_config(self) -> GenSettings:
""" """
获取代码生成配置 获取代码生成配置
@@ -241,6 +281,8 @@ JwtConfig = get_config.get_jwt_config()
DataBaseConfig = get_config.get_database_config() DataBaseConfig = get_config.get_database_config()
# Redis配置 # Redis配置
RedisConfig = get_config.get_redis_config() RedisConfig = get_config.get_redis_config()
# 日志配置
LogConfig = get_config.get_log_config()
# 代码生成配置 # 代码生成配置
GenConfig = get_config.get_gen_config() GenConfig = get_config.get_gen_config()
# 上传配置 # 上传配置
+9
View File
@@ -26,3 +26,12 @@ async def init_create_table() -> None:
async with async_engine.begin() as conn: async with async_engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all) await conn.run_sync(Base.metadata.create_all)
logger.info('✅️ 数据库连接成功') logger.info('✅️ 数据库连接成功')
async def close_async_engine() -> None:
"""
应用关闭时释放数据库连接池
:return:
"""
await async_engine.dispose()
+33 -6
View File
@@ -16,13 +16,14 @@ class RedisUtil:
""" """
@classmethod @classmethod
async def create_redis_pool(cls) -> aioredis.Redis: async def create_redis_pool(cls, log_enabled: bool = True, log_start_enabled: bool | None = None) -> aioredis.Redis:
""" """
应用启动时初始化redis连接 应用启动时初始化redis连接
:param log_enabled: 是否输出日志
:param log_start_enabled: 是否输出开始连接日志
:return: Redis连接对象 :return: Redis连接对象
""" """
logger.info('🔎 开始连接redis...')
redis = await aioredis.from_url( redis = await aioredis.from_url(
url=f'redis://{RedisConfig.redis_host}', url=f'redis://{RedisConfig.redis_host}',
port=RedisConfig.redis_port, port=RedisConfig.redis_port,
@@ -32,19 +33,45 @@ class RedisUtil:
encoding='utf-8', encoding='utf-8',
decode_responses=True, decode_responses=True,
) )
if log_start_enabled is None:
log_start_enabled = log_enabled
if log_enabled or log_start_enabled:
await cls.check_redis_connection(redis, log_enabled=log_enabled, log_start_enabled=log_start_enabled)
return redis
@classmethod
async def check_redis_connection(
cls, redis: aioredis.Redis, log_enabled: bool = True, log_start_enabled: bool | None = None
) -> None:
"""
检查redis连接状态
:param redis: redis对象
:param log_enabled: 是否输出日志
:param log_start_enabled: 是否输出开始连接日志
:return: None
"""
if log_start_enabled is None:
log_start_enabled = log_enabled
if log_start_enabled:
logger.info('🔎 开始连接redis...')
try: try:
connection = await redis.ping() connection = await redis.ping()
if not log_enabled:
return
if connection: if connection:
logger.info('✅️ redis连接成功') logger.info('✅️ redis连接成功')
else: else:
logger.error('❌️ redis连接失败') logger.error('❌️ redis连接失败')
except AuthenticationError as e: except AuthenticationError as e:
logger.error(f'❌️ redis用户名或密码错误,详细错误信息:{e}') if log_enabled:
logger.error(f'❌️ redis用户名或密码错误,详细错误信息:{e}')
except RedisTimeoutError as e: except RedisTimeoutError as e:
logger.error(f'❌️ redis连接超时,详细错误信息:{e}') if log_enabled:
logger.error(f'❌️ redis连接超时,详细错误信息:{e}')
except RedisError as e: except RedisError as e:
logger.error(f'❌️ redis连接错误,详细错误信息:{e}') if log_enabled:
return redis logger.error(f'❌️ redis连接错误,详细错误信息:{e}')
@classmethod @classmethod
async def close_redis_pool(cls, app: FastAPI) -> None: async def close_redis_pool(cls, app: FastAPI) -> None:

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