diff --git a/.gitignore b/.gitignore index 6d1f2781..d4cb9f30 100755 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,8 @@ frontend/node_modules frontend/dist frontend/dist frontend/.husky + +devops/nginx/fastapp/dist +devops/nginx/fastdocs/dist +devops/nginx/frontend/dist +devops/nginx/ssl diff --git a/PROJECT_DOCUMENTATION.md b/PROJECT_DOCUMENTATION.md new file mode 100644 index 00000000..f049340e --- /dev/null +++ b/PROJECT_DOCUMENTATION.md @@ -0,0 +1,403 @@ +# FastAPI Vue3 Admin 项目详细说明文档 + +
+

FastAPI Vue3 Admin

+

现代化全栈中后台管理系统

+

基于 FastAPI + Vue3 + TypeScript + Element Plus 的企业级后台管理解决方案

+
+ +## 📋 目录 + +- [项目概述](#项目概述) +- [技术架构](#技术架构) +- [功能特性](#功能特性) +- [项目结构](#项目结构) +- [核心模块](#核心模块) +- [技术栈详解](#技术栈详解) +- [环境要求](#环境要求) +- [快速开始](#快速开始) +- [部署指南](#部署指南) +- [API文档](#api文档) +- [开发规范](#开发规范) + +## 📖 项目概述 + +**FastAPI Vue3 Admin** 是一套完全开源、高度模块化、技术先进的现代化快速开发平台。该项目采用前后端分离架构,融合了 Python 后端框架 FastAPI 和前端主流框架 Vue3,为企业级中后台系统开发提供了一站式解决方案。 + +### 🎯 设计理念 + +- **模块化设计**:系统功能高度解耦,便于扩展和维护 +- **开箱即用**:提供完整的基础功能模块,快速启动项目开发 +- **技术先进**:采用最新的技术栈,保证项目的先进性和可维护性 +- **开发友好**:完善的中文文档和开发工具链,降低学习成本 +- **企业级**:满足企业级应用的性能、安全、稳定性要求 + +### 🌟 核心优势 + +1. **全栈整合**:前后端技术栈统一,开发效率高 +2. **高性能**:异步框架 + Redis 缓存,响应速度快 +3. **安全可靠**:JWT 认证 + RBAC 权限模型,安全性强 +4. **易于扩展**:模块化架构,便于功能扩展 +5. **部署简单**:支持 Docker 容器化部署 +6. **文档完善**:提供详细的开发文档和 API 文档 + +## 🏗️ 技术架构 + +### 整体架构图 + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ 前端应用层 │ │ 移动端应用 │ │ 管理后台 │ +│ (Vue3 SPA) │ │ (UniApp H5) │ │ (Element Plus)│ +└─────────────────┘ └─────────────────┘ └─────────────────┘ + │ │ │ + └───────────────────────┼───────────────────────┘ + │ +┌─────────────────────────────────┼─────────────────────────────────┐ +│ API 网关层 │ │ +│ (Nginx) │ │ +└─────────────────────────────────┼─────────────────────────────────┘ + │ +┌─────────────────────────────────┼─────────────────────────────────┐ +│ 应用服务层 │ │ +│ (FastAPI) │ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ 控制器层 │ │ 服务层 │ │ 数据访问层 │ │ +│ │ Controller │ │ Service │ │ CRUD │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +└─────────────────────────────────┼─────────────────────────────────┘ + │ +┌─────────────────────────────────┼─────────────────────────────────┐ +│ 数据存储层 │ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ MySQL │ │ Redis │ │ 文件存储 │ │ +│ │ 主数据库 │ │ 缓存/会话 │ │ 静态资源 │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 前后端分离架构 + +- **前端**:Vue3 + TypeScript + Element Plus + Vite +- **后端**:FastAPI + SQLAlchemy + Pydantic + Alembic +- **数据库**:MySQL 8.0+ (主数据库) + Redis 7.0+ (缓存) +- **部署**:Docker + Nginx + Docker Compose + +## ✨ 功能特性 + +### 🔐 认证授权 +- JWT Token 认证机制 +- OAuth2 标准实现 +- 用户登录/登出 +- 密码加密存储 +- 会话管理 + +### 👥 用户管理 +- 用户信息管理 +- 用户状态控制 +- 密码重置 +- 个人资料编辑 +- 头像上传 + +### 🛡️ 权限管理 (RBAC) +- 基于角色的访问控制 +- 菜单权限控制 +- 按钮权限控制 +- 数据权限控制 +- 角色分配管理 + +### 📊 系统监控 +- 在线用户监控 +- 服务器性能监控 +- Redis 缓存监控 +- 系统日志管理 +- 操作日志记录 + +### ⚙️ 系统配置 +- 系统参数配置 +- 字典数据管理 +- 公告通知管理 +- 定时任务管理 +- 代码生成器 + +### 🎨 界面特性 +- 响应式布局设计 +- 多主题切换 +- 国际化支持 +- 暗黑模式 +- 全屏功能 +- 面包屑导航 + +## 📁 项目结构 + +``` +fastapi_vue3_admin/ +├── backend/ # 后端工程 +│ ├── app/ # 应用核心代码 +│ │ ├── v1/ # API v1 版本 +│ │ │ ├── controllers/ # 控制器层 +│ │ │ ├── services/ # 业务逻辑层 +│ │ │ ├── cruds/ # 数据访问层 +│ │ │ ├── models/ # 数据模型 +│ │ │ ├── schemas/ # 数据验证模式 +│ │ │ ├── params/ # 请求参数模型 +│ │ │ └── urls/ # 路由配置 +│ │ ├── core/ # 核心配置 +│ │ ├── middleware/ # 中间件 +│ │ ├── utils/ # 工具函数 +│ │ └── scripts/ # 初始化脚本 +│ ├── env/ # 环境配置 +│ ├── static/ # 静态文件 +│ ├── sql/ # SQL 脚本 +│ ├── main.py # 应用入口 +│ └── requirements.txt # Python 依赖 +├── frontend/ # 前端工程 +│ ├── src/ # 源代码 +│ │ ├── api/ # API 接口 +│ │ ├── assets/ # 静态资源 +│ │ ├── components/ # 公共组件 +│ │ ├── layouts/ # 布局组件 +│ │ ├── router/ # 路由配置 +│ │ ├── store/ # 状态管理 +│ │ ├── styles/ # 样式文件 +│ │ ├── utils/ # 工具函数 +│ │ ├── views/ # 页面组件 +│ │ └── main.ts # 应用入口 +│ ├── public/ # 公共资源 +│ ├── package.json # 依赖配置 +│ └── vite.config.ts # 构建配置 +├── fastapp/ # 移动端工程 (UniApp) +├── fastdocs/ # 文档工程 +├── devops/ # 部署配置 +│ ├── nginx/ # Nginx 配置 +│ ├── backend/ # 后端部署配置 +│ └── redis/ # Redis 配置 +├── docker-compose.yaml # Docker 编排文件 +└── deploy.sh # 部署脚本 +``` + +## 🧩 核心模块 + +### 仪表盘模块 +- **工作台**:常用功能快捷入口,系统概览 +- **分析页**:数据统计图表,业务指标展示 + +### 系统管理模块 +- **用户管理**:用户信息CRUD,状态管理 +- **角色管理**:角色权限分配,角色层级管理 +- **菜单管理**:系统菜单配置,权限绑定 +- **部门管理**:组织架构管理,部门层级 +- **岗位管理**:职位信息管理 +- **字典管理**:系统字典数据维护 +- **配置管理**:系统参数配置 +- **公告管理**:系统公告发布 +- **日志管理**:操作日志记录查询 + +### 监控管理模块 +- **在线用户**:当前在线用户监控 +- **定时任务**:系统定时任务管理 +- **服务器监控**:服务器性能指标监控 +- **缓存监控**:Redis缓存状态监控 + +### 代码生成模块 +- **代码生成器**:根据数据表自动生成CRUD代码 + +### 接口文档模块 +- **Swagger文档**:自动生成的API文档 +- **Redoc文档**:美化版API文档 + +## 💻 技术栈详解 + +### 前端技术栈 + +| 技术 | 版本 | 描述 | +|------|------|------| +| Vue3 | ^3.5.17 | 渐进式JavaScript框架 | +| TypeScript | ^5.8.3 | JavaScript的超集,提供类型支持 | +| Vite | ^6.3.5 | 新一代前端构建工具 | +| Element Plus | ^2.10.4 | Vue3组件库 | +| Vue Router | ^4.5.1 | Vue官方路由管理器 | +| Pinia | ^3.0.3 | Vue状态管理库 | +| Axios | ^1.10.0 | HTTP客户端 | +| ECharts | ^5.6.0 | 数据可视化图表库 | +| UnoCSS | 66.2.3 | 原子化CSS引擎 | +| Vue I18n | ^11.1.10 | 国际化插件 | + +### 后端技术栈 + +| 技术 | 描述 | +|------|------| +| FastAPI | 现代、高性能的Python Web框架 | +| SQLAlchemy 2.0 | Python SQL工具包和ORM | +| Pydantic 2.0 | 数据验证和设置管理 | +| Alembic | 数据库迁移工具 | +| PyJWT | JWT令牌处理 | +| APScheduler | 定时任务调度器 | +| Redis | 内存数据库,用于缓存和会话 | +| MySQL | 关系型数据库 | + +### 开发工具 + +| 工具 | 描述 | +|------|------| +| ESLint | JavaScript代码检查工具 | +| Prettier | 代码格式化工具 | +| Husky | Git hooks工具 | +| Commitizen | 规范化提交信息 | +| Docker | 容器化部署 | +| Nginx | Web服务器和反向代理 | + +## 🔧 环境要求 + +### 开发环境 + +| 环境 | 版本要求 | +|------|----------| +| Node.js | >= 20.0 | +| Python | >= 3.10 | +| MySQL | >= 8.0 | +| Redis | >= 7.0 | +| pnpm | >= 8.1.0 | + +### 生产环境 + +| 环境 | 版本要求 | +|------|----------| +| Docker | >= 20.0 | +| Docker Compose | >= 2.0 | +| Nginx | >= 1.20 | + +## 🚀 快速开始 + +### 1. 克隆项目 + +```bash +git clone https://gitee.com/tao__tao/fastapi_vue3_admin.git +cd fastapi_vue3_admin +``` + +### 2. 后端启动 + +```bash +cd backend +pip install -r requirements.txt +python main.py run --env=dev +``` + +### 3. 前端启动 + +```bash +cd frontend +pnpm install +pnpm run dev +``` + +### 4. 访问系统 + +- 前端地址:http://localhost:5180/web +- 后端API:http://localhost:8000 +- API文档:http://localhost:8000/docs + +### 5. 默认账号 + +- 管理员:admin / 123456 +- 演示账号:demo / 123456 + +## 📦 部署指南 + +### Docker 部署 + +```bash +# 一键部署 +chmod +x deploy.sh +./deploy.sh + +# 查看服务状态 +docker-compose ps + +# 查看日志 +docker logs -f <容器名> +``` + +### 手动部署 + +1. **数据库配置** + - 创建MySQL数据库 + - 配置Redis服务 + +2. **后端部署** + - 配置环境变量 + - 安装Python依赖 + - 运行数据库迁移 + - 启动FastAPI服务 + +3. **前端部署** + - 构建前端项目 + - 配置Nginx + - 部署静态文件 + +## 📚 API文档 + +### 接口规范 + +- 基础路径:`/api/v1` +- 认证方式:Bearer Token (JWT) +- 数据格式:JSON +- 字符编码:UTF-8 + +### 响应格式 + +```json +{ + "code": 200, + "message": "success", + "data": {}, + "timestamp": 1640995200 +} +``` + +### 状态码说明 + +| 状态码 | 说明 | +|--------|------| +| 200 | 请求成功 | +| 400 | 请求参数错误 | +| 401 | 未授权 | +| 403 | 禁止访问 | +| 404 | 资源不存在 | +| 500 | 服务器内部错误 | + +## 📝 开发规范 + +### 代码规范 + +- 使用ESLint + Prettier进行代码格式化 +- 遵循Vue3 Composition API规范 +- 使用TypeScript进行类型约束 +- 组件命名采用PascalCase +- 文件命名采用kebab-case + +### Git规范 + +- 使用Conventional Commits规范 +- 分支命名:feature/功能名、bugfix/问题描述 +- 提交信息:type(scope): description + +### 目录规范 + +- 组件按功能模块组织 +- 公共组件放在components目录 +- 页面组件放在views目录 +- 工具函数放在utils目录 + +--- + +## 📞 联系方式 + +- 项目地址:https://gitee.com/tao__tao/fastapi_vue3_admin +- 在线演示:https://service.fastapiadmin.com/web +- 问题反馈:https://gitee.com/tao__tao/fastapi_vue3_admin/issues + +## 📄 许可证 + +本项目基于 [MIT](./LICENSE) 许可证开源。 diff --git a/README.en.md b/README.en.md index b522b681..c1b685b4 100644 --- a/README.en.md +++ b/README.en.md @@ -71,17 +71,17 @@ fastapi_vue3_admin ## 🛠️ Technology Stack Overview | Type | Technology Selection | Description | -|----------|---------------------------------|---------------------------------| -| Backend Framework | FastAPI / Uvicorn / Pydantic 2.0 / Alembic | A modern, high-performance asynchronous framework with enforced type constraints and data migration capabilities. | +|----------|---------------------------------|-------------| +| Backend Framework | FastAPI / Uvicorn / Pydantic 2.0 / Alembic | A modern, high-performance asynchronous framework with强制类型约束 (mandatory type constraints) and data migration capabilities. | | ORM | SQLAlchemy 2.0 | A powerful ORM library. | | Scheduled Tasks | APScheduler | Easily implement scheduled tasks. | -| Authentication | PyJWT | Implement JWT authentication. | -| Frontend Framework | Vue3 / Vite5 / Pinia / TypeScript | Quickly develop Vue3 applications. | -| UI Library | ElementPlus | Quickly develop beautiful UI components. | +| Permission Authentication | PyJWT | Implement JWT authentication. | +| Frontend Framework | Uni App / Vue3 / Vite5 / Pinia / TypeScript | Rapidly develop Vue3 applications. | +| UI Library | ElementPlus / Wot Design Uni | Quickly develop beautiful UI components. | | Database | MySQL / MongoDB | Powerful databases. | -| Cache | Redis | A powerful caching database. | +| Cache | Redis | A powerful cache database. | | Documentation | Swagger / Redoc | Automatically generate API documentation. | -| Deployment | Docker / Nginx / Docker Compose | Quickly deploy projects. | +| Deployment | Docker / Nginx / Docker Compose | Rapidly deploy projects. | --- @@ -117,6 +117,9 @@ fastapi_vue3_admin | Frontend | Node.js | >= 20.0 (It is recommended to use the latest version.) | | Frontend | npm | 16.14 | | Frontend | Vue3 | 3.3 | +| Web UI | ElementPlus | 2.10.4 | +| Mobile | Uni App | 3.0.0 | +| App UI | Wot Design Uni | 1.9.1 | | Database | MySQL | 8.0 (It is recommended to use the latest version.) | | Middleware | Redis | 7.0 (It is recommended to use the latest version.) | @@ -267,10 +270,10 @@ fastapi_vue3_admin/devops/devops/nginx/nginx.conf ### 移动端 -| Module Name | Screenshot | -|----------|------| -| Login | Under development... (To be completed) | - +| Module
| Details | Module
| Details | Module
| Details | +|----------|------|----------|------|----------|------| +| Login | ![Mobile Login](./fastdocs/src/public/app_login.png) | Home | ![Mobile Home](./fastdocs/src/public/app_home.png) | Profile | ![Mobile Profile](./fastdocs/src/public/app_mine.png) | +| Personal | ![Mobile Personal Info](./fastdocs/src/public/app_profile.png) | Settings | ![Mobile Settings](./fastdocs/src/public/app_setting.png) | Workbench | ![Mobile Workbench](./fastdocs/src/public/app_work.png) | --- ## 🛠️ Secondary Development Tutorial @@ -293,6 +296,12 @@ fastapi_vue3_admin/devops/devops/nginx/nginx.conf 1. **Configure the Frontend to Access the Backend Interface Address**: Configure it in `frontend/src/api/demo/example.ts`. 2. **Write the Frontend Page**: Write it in `frontend/src/views/demo/example/index.vue`. +### Mobile Part + +1. **Configure the mobile access address for backend interfaces**: Write the code in `fastapp/src/api`. +2. **Write mobile pages**: Write the code in `fastapp/src/pages`. + + --- ## 🙏 Thanks diff --git a/README.md b/README.md index 0ee229a5..5cf2df29 100644 --- a/README.md +++ b/README.md @@ -78,8 +78,8 @@ fastapi_vue3_admin | ORM | SQLAlchemy 2.0 | 强大的 ORM 库。 | | 定时任务 | APScheduler | 轻松实现定时任务。 | | 权限认证 | PyJWT | 实现 JWT 认证。 | -| 前端框架 | Vue3 / Vite5 / Pinia / TypeScript | 快速开发 Vue3 应用。 | -| UI 库 | ElementPlus | 快速开发美观的 UI 组件。 | +| 前端框架 | Uni App / Vue3 / Vite5 / Pinia / TypeScript | 快速开发 Vue3 应用。 | +| UI 库 | ElementPlus / Wot Design Uni | 快速开发美观的 UI 组件。 | | 数据库 | MySQL / MongoDB | 强大的数据库。 | | 缓存 | Redis | 强大的缓存数据库。 | | 文档 | Swagger / Redoc | 自动生成 API 文档。 | @@ -114,13 +114,16 @@ fastapi_vue3_admin | 类型 | 技术栈 | 版本 | |----------|------------|------------| -| 后端 | Python | >=3.100 | -| 后端 | FastAPI | 0.109 | -| 前端 | Node.js | >= 20.0(推荐使用最新版)| -| 前端 | npm | 16.14 | -| 前端 | Vue3 | 3.3 | -| 数据库 | MySQL | 8.0 (推荐使用最新版)| -| 中间件 | Redis | 7.0 (推荐使用最新版)| +| 后端 | Python | >=3.10 | +| 后端 | FastAPI | 0.109 | +| 前端 | Node.js | >= 20.0(推荐使用最新版)| +| 前端 | npm | 16.14 | +| 前端 | Vue3 | 3.3 | +| Web UI | ElementPlus | 2.10.4 | +| 移动端 | Uni App | 3.0.0 | +| App UI | Wot Design Uni | 1.9.1 | +| 数据库 | MySQL | 8.0 (推荐使用最新版)| +| 中间件 | Redis | 7.0 (推荐使用最新版)| --- @@ -271,9 +274,10 @@ fastapi_vue3_amdin/devops/devops/nginx/nginx.conf ### 移动端 -| 模块名 | 截图 | -|----------|------| -| 登录 | 开发中... (待完成) | +| 模块
| 详情 | 模块
| 详情 | 模块
| 详情 | +|----------|------|----------|------|----------|------| +| 登录 | ![移动端登录](./fastdocs/src/public/app_login.png) | 首页 | ![移动端首页](./fastdocs/src/public/app_home.png) | 我的 | ![移动端个人中心](./fastdocs/src/public/app_mine.png) | +| 个人 | ![移动端个人信息](./fastdocs/src/public/app_profile.png) | 设置 | ![移动端设置](./fastdocs/src/public/app_setting.png) | 工作台 | ![移动端工作台](./fastdocs/src/public/app_work.png) | --- @@ -297,6 +301,11 @@ fastapi_vue3_amdin/devops/devops/nginx/nginx.conf 1. **前端接入后端接口地址**:在 `frontend/src/api/demo/example.ts` 中配置 2. **编写前端页面**:在 `frontend/src/views/demo/example/index.vue` 中编写 +### 移动端部分 + +1. **移动端接入后端接口地址**:在 `fastapp/src/api` 中编写 +2. **编写移动端页面**:在 `fastapp/src/pages` 中编写 + --- ## 🙏 特别鸣谢 diff --git a/backend/app/api/v1/schemas/monitor/online_schema.py b/backend/app/api/v1/schemas/monitor/online_schema.py index 11a26d52..4a643314 100644 --- a/backend/app/api/v1/schemas/monitor/online_schema.py +++ b/backend/app/api/v1/schemas/monitor/online_schema.py @@ -22,4 +22,5 @@ class OnlineOutSchema(BaseModel): os: Optional[str] = Field(default=None, description='操作系统') browser: Optional[str] = Field(default=None, description='浏览器') login_time: Optional[DateTimeStr] = Field(default=None, description='登录时间') + login_type: Optional[str] = Field(default=None, description='登录类型 PC端 | 移动端') diff --git a/backend/app/api/v1/services/system/auth_service.py b/backend/app/api/v1/services/system/auth_service.py index 25ff704a..ffe1aeab 100644 --- a/backend/app/api/v1/services/system/auth_service.py +++ b/backend/app/api/v1/services/system/auth_service.py @@ -83,12 +83,12 @@ class LoginService: user = await UserCRUD(auth).update_last_login_crud(id=user.id) # 创建token - token = await cls.create_token_service(request=request, redis=redis, user=user) + token = await cls.create_token_service(request=request, redis=redis, user=user, login_type=login_form.login_type) return token @classmethod - async def create_token_service(cls, request: Request, redis: Redis, user: UserModel) -> JWTOutSchema: + async def create_token_service(cls, request: Request, redis: Redis, user: UserModel, login_type: str) -> JWTOutSchema: """ 创建访问令牌和刷新令牌 @@ -114,6 +114,9 @@ class LoginService: request_ip = request.client.host login_location = await IpLocalUtil.get_ip_location(request_ip) request.scope["login_location"] = login_location + + # 确保在请求上下文中设置用户名 + request.scope["user_username"] = user.username access_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) refresh_expires = timedelta(minutes=settings.REFRESH_TOKEN_EXPIRE_MINUTES) @@ -129,6 +132,7 @@ class LoginService: os=user_agent.os.family, browser = user_agent.browser.family, login_time=user.last_login.isoformat() if isinstance(user.last_login, datetime) else str(user.last_login), + login_type=login_type ).model_dump_json() access_token = create_access_token(payload=JWTPayloadSchema( diff --git a/backend/app/common/constant.py b/backend/app/common/constant.py index 9d684662..501a78d4 100644 --- a/backend/app/common/constant.py +++ b/backend/app/common/constant.py @@ -200,6 +200,19 @@ class RET(Enum): class CommonConstant: """ 常用常量 + + WWW: www主域名 + HTTP: http请求 + HTTPS: https请求 + LOOKUP_RMI: RMI远程方法调用 + LOOKUP_LDAP: LDAP远程方法调用 + LOOKUP_LDAPS: LDAPS远程方法调用 + YES: 是否为系统默认(是) + NO: 是否为系统默认(否) + DEPT_NORMAL: 部门正常状态 + DEPT_DISABLE: 部门停用状态 + UNIQUE: 校验是否唯一的返回标识(是) + NOT_UNIQUE: 校验是否唯一的返回标识(否) """ # 域名相关 @@ -281,5 +294,28 @@ class JobConstant: JOB_WHITE_LIST = ['module_task'] +class MenuConstant: + """ + 菜单常量 + + TYPE_DIR: 菜单类型(目录) + TYPE_MENU: 菜单类型(菜单) + TYPE_BUTTON: 菜单类型(按钮) + YES_FRAME: 是否菜单外链(是) + NO_FRAME: 是否菜单外链(否) + LAYOUT: Layout组件标识 + PARENT_VIEW: ParentView组件标识 + INNER_LINK: InnerLink组件标识 + """ + + TYPE_DIR = 'M' + TYPE_MENU = 'C' + TYPE_BUTTON = 'F' + YES_FRAME = 0 + NO_FRAME = 1 + LAYOUT = 'Layout' + PARENT_VIEW = 'ParentView' + INNER_LINK = 'InnerLink' + # if __name__ == "__main__": # print(RET.OK.msg) # 输出: 成功 diff --git a/backend/app/common/enums.py b/backend/app/common/enums.py index f9e06c5a..8557ee98 100644 --- a/backend/app/common/enums.py +++ b/backend/app/common/enums.py @@ -9,16 +9,31 @@ class EnvironmentEnum(str, Enum): @unique class BusinessType(Enum): - """业务操作类型枚举""" - GET = 0 # 查询 - CREATE = 1 # 新增 - UPDATE = 2 # 修改 - DELETE = 3 # 删除 - GRANT = 4 # 授权 - EXPORT = 5 # 导出 - IMPORT = 6 # 导入 - FORCE = 7 # 强退 + """ + 业务操作类型 + OTHER: 其它 + INSERT: 新增 + UPDATE: 修改 + DELETE: 删除 + GRANT: 授权 + EXPORT: 导出 + IMPORT: 导入 + FORCE: 强退 + GENCODE: 生成代码 + CLEAN: 清空数据 + """ + + OTHER = 0 + INSERT = 1 + UPDATE = 2 + DELETE = 3 + GRANT = 4 + EXPORT = 5 + IMPORT = 6 + FORCE = 7 + GENCODE = 8 + CLEAN = 9 @unique diff --git a/backend/app/config/setting.py b/backend/app/config/setting.py index 2304f6c5..f7934d43 100755 --- a/backend/app/config/setting.py +++ b/backend/app/config/setting.py @@ -99,7 +99,7 @@ class Settings(BaseSettings): EXPIRE_ON_COMMIT: bool = False # 是否在提交时过期 # SQLite数据库连接 - DB_DRIVER: str + DB_DRIVER: Literal['sqlite','mysql', 'postgresql'] = 'sqlite' SQLITE_DB_NAME: str # MySQL数据库连接 @@ -109,6 +109,13 @@ class Settings(BaseSettings): MYSQL_PORT: int MYSQL_DB_NAME: str + # PostgreSQL数据库连接 + POSTGRESQL_USER: str + POSTGRESQL_PASSWORD: str + POSTGRESQL_HOST: str + POSTGRESQL_PORT: int + POSTGRESQL_DB_NAME: str + # ================================================= # # ******************** MongoDB配置 ******************* # # ================================================= # @@ -175,10 +182,19 @@ class Settings(BaseSettings): "/api/v1/system/auth/token/refresh", "/api/v1/system/auth/captcha/get", "/api/v1/system/auth/logout", + "/api/v1/system/config/info", + "/api/v1/system/user/current/info", + "/api/v1/system/notice/available", ] DEMO_BLACK_LIST_PATH: List[str] = [ # 演示黑名单 "/auth/login" ] + DEMO_IP_WHITE_LIST: List[str] = [ # 演示白名单IP + "127.0.0.1" + ] + DEMO_USER_WHITE_LIST: List[str] = [ # 演示白名单用户 + "superadmin", + ] # ================================================= # # ***************** 静态文件配置 ***************** # @@ -246,30 +262,40 @@ class Settings(BaseSettings): @property def DB_URI(self) -> str: """获取数据库连接""" - supported_db_drivers = ("sqlite", "mysql") + + supported_db_drivers = ("sqlite", "mysql", "postgresql") if settings.DB_DRIVER not in supported_db_drivers: raise ValueError(f"数据库驱动不支持: {settings.DB_DRIVER}, 请选择 {supported_db_drivers}") if settings.DB_DRIVER == "mysql": MYSQL_URI: MySQLDsn = f"mysql+asyncmy://{settings.MYSQL_USER}:{settings.MYSQL_PASSWORD}@{settings.MYSQL_HOST}:{settings.MYSQL_PORT}/{settings.MYSQL_DB_NAME}?charset=utf8mb4" return MYSQL_URI - else: + elif settings.DB_DRIVER == "sqlite": SQLITE_URI: str = f"sqlite+aiosqlite:///{settings.BASE_DIR.joinpath(settings.SQLITE_DB_NAME)}?characterEncoding=UTF-8" return SQLITE_URI - + elif settings.DB_DRIVER == "postgresql": + POSRGRES_URI: PostgresDsn = f"postgresql+asyncpg://{settings.POSTGRESQL_USER}:{settings.POSTGRESQL_PASSWORD}@{settings.POSTGRESQL_HOST}:{settings.POSTGRESQL_PORT}/{settings.POSTGRESQL_DB_NAME}" + return POSRGRES_URI + else: + raise ValueError(f"数据库驱动不支持: {settings.DB_DRIVER}, 请选择 {supported_db_drivers}") @property def DATABASES_URI(self) -> str: """获取数据库连接""" - supported_db_drivers = ("sqlite", "mysql") + supported_db_drivers = ("sqlite", "mysql", "postgresql") if settings.DB_DRIVER not in supported_db_drivers: raise ValueError(f"数据库驱动不支持: {settings.DB_DRIVER}, 请选择 {supported_db_drivers}") if settings.DB_DRIVER == "mysql": MYSQL_URI: MySQLDsn = f"mysql+pymysql://{settings.MYSQL_USER}:{settings.MYSQL_PASSWORD}@{settings.MYSQL_HOST}:{settings.MYSQL_PORT}/{settings.MYSQL_DB_NAME}?charset=utf8mb4" return MYSQL_URI - else: + elif settings.DB_DRIVER == "postgresql": + POSRGRES_URI: PostgresDsn = f"postgresql://{settings.POSTGRESQL_USER}:{settings.POSTGRESQL_PASSWORD}@{settings.POSTGRESQL_HOST}:{settings.POSTGRESQL_PORT}/{settings.POSTGRESQL_DB_NAME}" + return POSRGRES_URI + elif settings.DB_DRIVER == "sqlite": SQLITE_URI: str = f"sqlite:///{settings.BASE_DIR.joinpath(settings.SQLITE_DB_NAME)}?characterEncoding=UTF-8" return SQLITE_URI + else: + raise ValueError(f"数据库驱动不支持: {settings.DB_DRIVER}, 请选择 {supported_db_drivers}") @@ -356,6 +382,45 @@ class Settings(BaseSettings): "factory": self.FACTORY, } + +class WxMiniConfig(BaseSettings): + + WX_MINI_APPID: str = "xxxx" + WX_MINI_APPID_SECRET: str = "xxxx" + + WX_MINI_TOKEN_URL: str = ("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={}&secret={}" + .format(WX_MINI_APPID, WX_MINI_APPID_SECRET)) + WX_MINI_GET_PHONE_URL: str = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={}" + WX_MINI_LOGIN: str = ("https://api.weixin.qq.com/sns/jscode2session?grant_type=authorization_code&appid={}" + "&secret={}&js_code={}") + # 获取无限小程序码 + WX_MINI_GEN_PATH_CODE: str = 'https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token={}' + # 微信小程序发送订阅消息 + WX_MINI_SEND_SUBSCRIBE_MSG: str = 'https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token={}' + # 微信商户ID,后续支付使用 + WXPAY_MCHID: str= "xxxx" + # 微信支付 API v3秘钥 + WXPAY_APIV3_KEY: str = "xxxx" + # 微信支付结果回调接口 + WXPAY_NOTIFYURL: str = "微信支付结果回调接口" + # 商户证书序列号 + WXPAY_SERIALNO: str = "xxxx" + # 商户私钥 + WXPAY_CLIENT_PRIKEY: str = "wxmini/wepay_cert/apiclient_key.pem" + # 商品描述(统一下单接口用到) + WXPAY_PAY_DESC: str = "商品描述(统一下单接口用到)" + # 回调地址,也可以在调用接口的时候覆盖 + NOTIFY_URL: str = 'https://xxxx.xxxxx.cn/api/v1/wepay/notify' + # 微信支付平台证书缓存目录,减少证书下载调用次数,首次使用确保此目录为空目录. + # 初始调试时可不设置,调试通过后再设置,示例值:'./cert' + CERT_DIR: str = './cert' + + SubscribeTemplate: Any = { + # 微信小程序通知模板ID + "rest_minute": "xxxxx" + } + pass + @lru_cache(maxsize=1) def get_settings() -> Settings: """获取配置实例""" diff --git a/backend/app/core/ap_scheduler.py b/backend/app/core/ap_scheduler.py index 8c240a7a..fe3e220a 100644 --- a/backend/app/core/ap_scheduler.py +++ b/backend/app/core/ap_scheduler.py @@ -25,7 +25,7 @@ from pymongo import MongoClient from app.api.v1.schemas.system.auth_schema import AuthSchema from app.config.setting import settings -from app.core.database import session_connect +from app.core.database import engine, session_connect, SessionLocal from app.core.exceptions import CustomException from app.core.logger import logger from app.api.v1.cruds.monitor.job_crud import JobCRUD @@ -46,6 +46,7 @@ if settings.REDIS_USER: job_stores = { 'default': MemoryJobStore(), + 'sqlalchemy': SQLAlchemyJobStore(url=settings.DATABASES_URI, engine=engine), 'redis': RedisJobStore(**redis_config), } # 配置执行器 @@ -71,7 +72,9 @@ class SchedulerUtil: @classmethod def scheduler_event_listener(cls, event: JobEvent | JobExecutionEvent): + # 获取事件类型和任务ID event_type = event.__class__.__name__ + # 初始化任务状态 status = True exception_info = '' if event_type == 'JobExecutionEvent' and event.exception: @@ -80,10 +83,29 @@ class SchedulerUtil: if hasattr(event, 'job_id'): job_id = event.job_id query_job = cls.get_job(job_id=job_id) - job_group = query_job._jobstore_alias # 获取任务组 - job_message = f"事件类型: {event_type}, 任务ID: {job_id}, 状态: {status}, 任务组: {job_group}, 错误详情: {exception_info}" - logger.error(job_message) - # JobCRUD(AuthSchema(db=session)).set_obj_field_crud(ids=[job_id], status=status, message=job_message) + if query_job: + query_job_info = query_job.__getstate__() + # 获取任务名称 + job_name = query_job_info.get('name') + # 获取任务组名 + job_group = query_job._jobstore_alias + # # 获取任务执行器 + # job_executor = query_job_info.get('executor') + # # 获取调用目标字符串 + # invoke_target = query_job_info.get('func') + # # 获取调用函数位置参数 + # job_args = ','.join(query_job_info.get('args')) + # # 获取调用函数关键字参数 + # job_kwargs = json.dumps(query_job_info.get('kwargs')) + # # 获取任务触发器 + # job_trigger = str(query_job_info.get('trigger')) + # 构造日志消息 + job_message = f"事件类型: {event_type}, 任务ID: {job_id}, 任务名称: {job_name}, 状态: {status}, 任务组: {job_group}, 错误详情: {exception_info}, 执行于{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + + logger.error(job_message) + session = SessionLocal() + JobCRUD(AuthSchema(db=session)).set_obj_field_crud(ids=[job_id], status=status, message=job_message) + session.close() @classmethod async def init_system_scheduler(cls, db): diff --git a/backend/app/core/database.py b/backend/app/core/database.py index 530c4ce3..1a414bd1 100644 --- a/backend/app/core/database.py +++ b/backend/app/core/database.py @@ -28,8 +28,15 @@ from app.core.logger import logger from app.config.setting import settings from app.core.exceptions import CustomException +# 同步数据库引擎 +engine = create_engine( + url=settings.DATABASES_URI, + echo=settings.DATABASE_ECHO, +) +# 同步数据库会话工厂 +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) - +# 异步数据库引擎 def async_db_engine() -> AsyncEngine: """创建异步数据库引擎""" # 创建数据库引擎 @@ -47,8 +54,7 @@ def async_db_engine() -> AsyncEngine: return async_engine - -# 创建会话工厂 +# 异步数据库会话工厂 async_session = async_sessionmaker( bind=async_db_engine(), autocommit=settings.AUTOCOMMIT, @@ -57,7 +63,6 @@ async_session = async_sessionmaker( class_=AsyncSession ) - def session_connect() -> AsyncSession: """获取数据库会话""" try: @@ -126,7 +131,6 @@ async def redis_connect(app: FastAPI, status: bool) -> aioredis.Redis: await app.state.redis.close() logger.info('Redis连接已关闭') - async def mongodb_connect(app: FastAPI, status: bool) -> AsyncIOMotorClient: """创建或关闭MongoDB连接""" if not settings.MONGO_DB_ENABLE: diff --git a/backend/app/core/dependencies.py b/backend/app/core/dependencies.py index 9395310f..beb7ef36 100644 --- a/backend/app/core/dependencies.py +++ b/backend/app/core/dependencies.py @@ -58,7 +58,7 @@ async def get_current_user( # 解析token payload = decode_access_token(token) if not payload or not hasattr(payload, 'is_refresh') or payload.is_refresh: - raise CustomException(msg="非法凭证", status_code=401) + raise CustomException(msg="非法凭证", code=10401, status_code=401) online_user_info = payload.sub # 从Redis中获取用户信息 @@ -66,23 +66,23 @@ async def get_current_user( session_id = user_info.get("session_id") if not session_id: - raise CustomException(msg="认证已失效", status_code=401) + raise CustomException(msg="认证已失效", code=10401, status_code=401) # 检查用户是否在线 online_ok = await RedisCURD(redis).exists(key=f'{RedisInitKeyConfig.ACCESS_TOKEN.key}:{session_id}') if not online_ok: - raise CustomException(msg="认证已失效", status_code=401) + raise CustomException(msg="认证已失效", code=10401, status_code=401) auth = AuthSchema(db=db) username = user_info.get("user_name") if not username: - raise CustomException(msg="认证已失效", status_code=401) + raise CustomException(msg="认证已失效", code=10401, status_code=401) # 获取用户信息 user = await UserCRUD(auth).get_by_username_crud(username=username) if not user: - raise CustomException(msg="用户不存在", status_code=401) + raise CustomException(msg="用户不存在", code=10401, status_code=401) if not user.status: - raise CustomException(msg="用户已被停用", status_code=401) + raise CustomException(msg="用户已被停用", code=10401, status_code=401) # 设置请求上下文 request.scope["user_id"] = user.id @@ -156,11 +156,11 @@ class AuthPermission: # 严格模式:要求所有权限都满足 if not all(perm in user_permissions for perm in self.permissions): logger.error(f"用户缺少所需的权限: {self.permissions}") - raise CustomException(msg="无权限操作", status_code=403) + raise CustomException(msg="无权限操作", code=10403, status_code=403) else: # 非严格模式:满足任一权限即可 if not any(perm in user_permissions for perm in self.permissions): logger.error(f"用户缺少任何所需的权限: {self.permissions}") - raise CustomException(msg="无权限操作", status_code=403) + raise CustomException(msg="无权限操作", code=10403, status_code=403) return auth diff --git a/backend/app/core/middlewares.py b/backend/app/core/middlewares.py index 09c3c141..a487a004 100644 --- a/backend/app/core/middlewares.py +++ b/backend/app/core/middlewares.py @@ -77,12 +77,32 @@ class DemoEnvMiddleware(BaseHTTPMiddleware): async def dispatch( self, request: Request, call_next: RequestResponseEndpoint ) -> Response: + if settings.DEMO_ENABLE and request.method != "GET": path = request.scope.get("path") - if path in settings.DEMO_BLACK_LIST_PATH: - return ErrorResponse(msg="演示环境,禁止操作",) - elif path not in settings.DEMO_WHITE_LIST_PATH: - return ErrorResponse(msg="演示环境,禁止操作",) + client_ip = request.client.host + user_username = request.scope.get("user_username") + + # 检查IP是否在白名单,或路径是否在白名单,或用户是否在白名单 + if (client_ip in settings.DEMO_IP_WHITE_LIST or + path in settings.DEMO_WHITE_LIST_PATH or + (user_username and user_username in settings.DEMO_USER_WHITE_LIST)): + return await call_next(request) + + else: + try: + response = await call_next(request) + + # 现在检查用户是否有权限执行操作 + user_username = request.scope.get("user_username") + if user_username and user_username not in settings.DEMO_USER_WHITE_LIST: + # 如果用户没有权限,返回错误响应 + return ErrorResponse(msg="演示环境,禁止操作") + + return response + except Exception as e: + # 处理可能的异常 + raise e return await call_next(request) diff --git a/backend/app/core/security.py b/backend/app/core/security.py index c719c77b..ee520d2f 100644 --- a/backend/app/core/security.py +++ b/backend/app/core/security.py @@ -55,6 +55,7 @@ class CustomOAuth2PasswordRequestForm(OAuth2PasswordRequestForm): password: str = Form(), captcha_key: Optional[str] = Form(default=""), captcha: Optional[str] = Form(default=""), + login_type: Optional[str] = Form(default="PC端", description="PC端 | 移动端") ): super().__init__( grant_type=grant_type, @@ -62,10 +63,11 @@ class CustomOAuth2PasswordRequestForm(OAuth2PasswordRequestForm): client_id=client_id, client_secret=client_secret, username=username, - password=password + password=password, ) self.captcha_key = captcha_key self.captcha = captcha + self.login_type = login_type # OAuth2认证配置 @@ -88,7 +90,7 @@ def create_access_token(payload: JWTPayloadSchema) -> str: def decode_access_token(token: str) -> JWTPayloadSchema: """解析JWT访问令牌""" if not token: - raise CustomException(msg="认证不存在,请重新登录", status_code=401) + raise CustomException(msg="认证不存在,请重新登录", code=10401, status_code=401) try: payload = jwt.decode( @@ -99,15 +101,15 @@ def decode_access_token(token: str) -> JWTPayloadSchema: online_user_info = payload.get("sub") if not online_user_info: - raise CustomException(msg="无效认证,请重新登录", status_code=401) + raise CustomException(msg="无效认证,请重新登录", code=10401, status_code=401) return JWTPayloadSchema(**payload) except (jwt.InvalidSignatureError, jwt.DecodeError): - raise CustomException(msg="无效认证,请重新登录", status_code=401) + raise CustomException(msg="无效认证,请重新登录", code=10401, status_code=401) except jwt.ExpiredSignatureError: - raise CustomException(msg="认证已过期,请重新登录", status_code=401) + raise CustomException(msg="认证已过期,请重新登录", code=10401, status_code=401) except jwt.InvalidTokenError: - raise CustomException(msg="token已失效,请重新登录", status_code=401) + raise CustomException(msg="token已失效,请重新登录", code=10401, status_code=401) diff --git a/backend/app/scripts/data/system_config.json b/backend/app/scripts/data/system_config.json index d84139ea..2b4d3747 100644 --- a/backend/app/scripts/data/system_config.json +++ b/backend/app/scripts/data/system_config.json @@ -66,7 +66,7 @@ "id": 8, "config_name": "帮助文档", "config_key": "sys_help_doc", - "config_value": "https://service.fastapiadmin.com/site/index.html", + "config_value": "https://service.fastapiadmin.com", "config_type": true, "description": "帮助文档", "creator_id": 1 diff --git a/backend/env/.env.dev b/backend/env/.env.dev index 4c4b3218..e7b29412 100644 --- a/backend/env/.env.dev +++ b/backend/env/.env.dev @@ -29,7 +29,7 @@ ROOT_PATH = "/api/v1" # API路由前缀 DEMO_ENABLE = False # 是否启用演示模式 # 数据库配置 -DB_DRIVER = "sqlite" # sqlite、mysql +DB_DRIVER = "sqlite" # sqlite、mysql、postgresql # SQLite配置 SQLITE_DB_NAME = "dev_sql.db" @@ -41,6 +41,13 @@ MYSQL_PASSWORD = "ServBay.dev" MYSQL_PORT = 3306 MYSQL_DB_NAME = "fastapi_vue_admin" +# PostgreSQL配置 +POSTGRESQL_HOST = "localhost" +POSTGRESQL_USER = "postgres" +POSTGRESQL_PASSWORD = 123456 +POSTGRESQL_PORT = 5432 +POSTGRESQL_DB_NAME = "fastapi_vue_admin" + # Redis配置 REDIS_ENABLE = True REDIS_HOST = "localhost" diff --git a/backend/env/.env.prod b/backend/env/.env.prod index 3fe2f1db..6e6cad8a 100644 --- a/backend/env/.env.prod +++ b/backend/env/.env.prod @@ -41,6 +41,13 @@ MYSQL_PASSWORD = "FastApi123abc" MYSQL_PORT = 3306 MYSQL_DB_NAME = "fastapi_vue3_admin" +# PostgreSQL配置 +POSTGRESQL_HOST = "172.18.52.77" +POSTGRESQL_USER = "postgres" +POSTGRESQL_PASSWORD = "FastApi123abc" +POSTGRESQL_PORT = 5432 +POSTGRESQL_DB_NAME = "fastapi_vue3_admin" + # Redis配置 REDIS_ENABLE = True REDIS_HOST = "172.18.52.77" diff --git a/backend/requirements.txt b/backend/requirements.txt index 16f31532..83cd65d6 100755 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -24,10 +24,10 @@ greenlet==3.1.1 # 协程框架 bcrypt==4.0.1 # 密码加密解析 aiofiles==24.1.0 # 文件操作 redis==5.2.1 # redis 同步操作数据库(用户celery配套使用) -# aioredis==2.0.1 # redis 异步操作数据库 redis已经完全具备了aioredis的功能,无需重复安全,且aioredis已经不再维护也不兼容3.10+的版本 +# aioredis==2.0.1 # redis 异步操作数据库 redis已经完全具备了aioredis的功能,无需重复安全,且aioredis已经不再维护也不兼容3.10+的版本 aiosqlite==0.17.0 # sqlite 异步操作数据库 asyncmy==0.2.9 # mysql 异步操作数据库:基于 mysqlclient:asyncmy 是 mysqlclient 的异步版本,mysqlclient 是一个 C 语言编写的 MySQL 客户端,性能较高。性能:asyncmy 通常在性能上优于 aiomysql,特别是在高并发和大数据量的场景下。 motor==3.6.0 # mongodb 驱动 -# asyncpg==0.30.0 # postgresql 异步操作数据库基于 psycopg2:asyncpg 是 psycopg2 的异步版本,psycopg2 是一个 pure-Python PostgreSQL 数据库适配器。性能:asyncpg 通常在性能上优于 psycopg2,特别是在高并发和大数据量的场景下。 -# aiomysql==0.2.0 # mysql 异步操作数据库基于 pymysql:aiomysql 是 pymysql 的异步版本,pymysql 是一个纯 Python 实现的 MySQL 客户端。成熟度:aiomysql 相对较为成熟,社区支持较好,文档也比较完善。 +asyncpg==0.30.0 # postgresql 异步操作数据库基于 psycopg2:asyncpg 是 psycopg2 的异步版本,psycopg2 是一个 pure-Python PostgreSQL 数据库适配器。性能:asyncpg 通常在性能上优于 psycopg2,特别是在高并发和大数据量的场景下。 +# aiomysql==0.2.0 # mysql 异步操作数据库基于 pymysql:aiomysql 是 pymysql 的异步版本,pymysql 是一个纯 Python 实现的 MySQL 客户端。成熟度:aiomysql 相对较为成熟,社区支持较好,文档也比较完善。 cryptography==45.0.2 # mysql8 密码加密 \ No newline at end of file diff --git a/backend/static/image/favicon.png b/backend/static/image/favicon.png index 8c016668..43eeb762 100644 Binary files a/backend/static/image/favicon.png and b/backend/static/image/favicon.png differ diff --git a/backend/static/image/group.jpg b/backend/static/image/group.jpg deleted file mode 100644 index 8e14219d..00000000 Binary files a/backend/static/image/group.jpg and /dev/null differ diff --git a/backend/static/image/logo.png b/backend/static/image/logo.png index 4a924529..81834963 100644 Binary files a/backend/static/image/logo.png and b/backend/static/image/logo.png differ diff --git a/deploy.sh b/deploy.sh index 73acd4b9..cfb4e5be 100644 --- a/deploy.sh +++ b/deploy.sh @@ -26,7 +26,7 @@ log() { # 检查权限 check_permissions() { - log "==========🔍 第一步:检查权限...==========" "INFO" + log "==========🔍 检查权限...==========" "INFO" # 检查脚本文件是否有执行权限 if [ ! -x "$0" ]; then log "⚠️ 当前脚本没有执行权限,请使用 chmod +x $0 添加执行权限" "ERROR" @@ -38,7 +38,7 @@ check_permissions() { # 检查依赖 check_dependencies() { - log "==========🔍 第二步:检查系统依赖...==========" "INFO" + log "==========🔍 检查系统依赖...==========" "INFO" local missing_deps=() for cmd in git docker node npm pnpm; do @@ -58,22 +58,32 @@ check_dependencies() { log "✅ 所有依赖检查通过" "INFO" } +# 停止项目容器 +stop_project() { + log "==========⏹️ 停止项目容器...==========" "INFO" + if [ -d "${WORK_DIR}/${PROJECT_NAME}" ]; then + cd "${WORK_DIR}/${PROJECT_NAME}" || { log "❌ 无法进入项目目录:${WORK_DIR}/${PROJECT_NAME}" "ERROR"; exit 1; } + + if [ -f "docker-compose.yaml" ] || [ -f "docker-compose.yml" ]; then + docker compose down || { log "❌ 停止容器失败" "ERROR"; exit 1; } + log "✅ 项目容器已停止并删除" "INFO" + else + log "❌ docker-compose 文件未找到" "ERROR" + exit 1 + fi + else + log "❌ 项目目录不存在:${WORK_DIR}/${PROJECT_NAME}" "ERROR" + exit 1 + fi +} + # 更新代码 update_code() { - log "==========🔍 第三步:检查项目...==========" "INFO" + log "==========🔍 更新最新代码...==========" "INFO" cd "${WORK_DIR}" || { log "❌ 无法进入工作目录:${WORK_DIR}" "ERROR"; exit 1; } if [ -d "${PROJECT_NAME}/" ]; then log "🔄 项目已存在,开始更新代码" "INFO" cd "${PROJECT_NAME}" || { log "❌ 无法进入项目目录:${PROJECT_NAME}" "ERROR"; exit 1; } - - if [ ! -f "docker-compose.yaml" ] && [ ! -f "docker-compose.yml" ]; then - log "❌ docker-compose.yaml 文件未找到" "ERROR" - exit 1 - fi - - docker compose down - log "✅ 容器已停止并删除" "INFO" - git pull --force || { log "❌ 拉取更新失败" "ERROR"; exit 1; } git log -1 || { log "❌ 获取提交信息失败" "ERROR"; exit 1; } log "✅ 代码更新成功" "INFO" @@ -86,8 +96,8 @@ update_code() { } # 打包前端 -build_image() { - log "==========🚀 第四步:打包前端...==========" "INFO" +build_frontend() { + log "==========🚀 打包前端...==========" "INFO" # 构建前端 if [ -d "frontend" ]; then @@ -125,7 +135,7 @@ build_image() { # 构建镜像&启动容器 start_containers() { - log "==========🚀 第五步:构建镜像&启动容器...==========" "INFO" + log "==========🚀 构建镜像&启动容器...==========" "INFO" docker compose build || { log "❌ 镜像构建失败" "ERROR"; exit 1; } log "✅ Docker镜像构建成功" "INFO" docker compose up -d --force-recreate || { log "❌ 容器启动失败" "ERROR"; exit 1; } @@ -136,32 +146,13 @@ start_containers() { # 显示容器日志 log "📋 获取容器日志..." "INFO" - docker compose logs || { log "❌ 容器日志获取失败" "ERROR"; exit 1; } + docker compose logs || { log "❌ 容器日志获取失败" "ERROR"; exit 1; } log "✅ 容器启动成功" "INFO" } -# 停止项目容器 -stop_project() { - log "==========⏹️ 停止项目容器...==========" "INFO" - if [ -d "${WORK_DIR}/${PROJECT_NAME}" ]; then - cd "${WORK_DIR}/${PROJECT_NAME}" || { log "❌ 无法进入项目目录:${WORK_DIR}/${PROJECT_NAME}" "ERROR"; exit 1; } - - if [ -f "docker-compose.yaml" ] || [ -f "docker-compose.yml" ]; then - docker compose down || { log "❌ 停止容器失败" "ERROR"; exit 1; } - log "✅ 项目容器已停止并删除" "INFO" - else - log "❌ docker-compose 文件未找到" "ERROR" - exit 1 - fi - else - log "❌ 项目目录不存在:${WORK_DIR}/${PROJECT_NAME}" "ERROR" - exit 1 - fi -} - # 清理旧镜像 cleanup_old_images() { - log "==========🗑️ 第六步:清理72小时前的旧镜像...==========" "INFO" + log "==========🗑️ 清理72小时前的旧镜像...==========" "INFO" # 只清理与项目相关的镜像 local project_images=$(docker images | grep ${PROJECT_NAME} | awk '{print $3}' | wc -l) if [ $project_images -gt 0 ]; then @@ -192,10 +183,17 @@ main() { check_permissions check_dependencies update_code - build_image + # build_frontend (由于本地资源较小,在服务器上构建应用改用本地构建好,上传到服务器) + stop_project start_containers cleanup_old_images - log "==========🎉 部署完成==========" "INFO" + log "🎉 部署完成!以下是访问信息:" "INFO" + log "📌 官网: https://service.fastapiadmin.com" "INFO" + log "📌 前端: https://service.fastapiadmin.com/web" "INFO" + log "📌 小程序: https://service.fastapiadmin.com/app" "INFO" + log "📌 后端接口: https://service.fastapiadmin.com/api/v1/docs" "INFO" + log "📌 登录信息: 账号 admin,密码 123456" "INFO" + } # 设置信号处理 @@ -210,14 +208,6 @@ fi while [[ $# -gt 0 ]]; do case $1 in - --debug) - LOG_LEVEL="DEBUG" - shift - ;; - --work-dir) - WORK_DIR="$2" - shift 2 - ;; --stop) stop_project exit 0 @@ -227,7 +217,7 @@ while [[ $# -gt 0 ]]; do exit 0 ;; --help|-h) - echo "Usage: $0 [--debug] [--work-dir ] [--stop] [--start]" + echo "Usage: $0 [--stop] [--start]" echo "不带参数时默认执行部署流程" exit 0 ;; diff --git a/devops/mysql/conf/mysqld.cnf b/devops/mysql/conf/mysqld.cnf new file mode 100644 index 00000000..25f1610f --- /dev/null +++ b/devops/mysql/conf/mysqld.cnf @@ -0,0 +1,31 @@ +# Copyright (c) 2014, 2016, Oracle and/or its affiliates. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 2 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +# +# The MySQL Server configuration file. +# +# For explanations see +# http://dev.mysql.com/doc/mysql/en/server-system-variables.html + +[mysqld] +pid-file = /var/run/mysqld/mysqld.pid +socket = /var/run/mysqld/mysqld.sock +datadir = /var/lib/mysql +# log-error = /var/log/mysql/error.log +# By default we only accept connections from localhost +#bind-address = 127.0.0.1 +# Disabling symbolic-links is recommended to prevent assorted security risks +symbolic-links=0 +max_connections=1000 diff --git a/docker-compose.yaml b/docker-compose.yaml index 748cd7b6..fc6451aa 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -67,9 +67,14 @@ services: - "443:443" volumes: - ./devops/nginx/nginx.conf:/etc/nginx/nginx.conf - - ./frontend/dist:/usr/share/nginx/html/frontend/dist - - ./fastapp/dist/build/h5:/usr/share/nginx/html/fastapp/dist/build/h5 - - ./fastdocs/dist:/usr/share/nginx/html/fastdocs/dist + # - ./frontend/dist:/usr/share/nginx/html/frontend/dist 如果使用服务器构建前端则不要注释这三行 + # - ./fastapp/dist/build/h5:/usr/share/nginx/html/fastapp/dist/build/h5 + # - ./fastdocs/dist:/usr/share/nginx/html/fastdocs/dist + # 下面三行是本地前端打包好放在服务器的路径,如果使用服务器打包则使用上面方式 + - /home/fastapiadmin/frontend:/usr/share/nginx/html/frontend + - /home/fastapiadmin/fastapp:/usr/share/nginx/html/fastapp + - /home/fastapiadmin/fastdocs:/usr/share/nginx/html/fastdocs + # ssl 证书 - /home/fastapiadmin/ssl:/etc/nginx/ssl:ro depends_on: - backend diff --git a/fastapp/.env.development b/fastapp/.env.development index 2a18a149..791c6403 100644 --- a/fastapp/.env.development +++ b/fastapp/.env.development @@ -1,3 +1,5 @@ +# 变量必须以 VITE_ 为前缀才能暴露给外部读取 + # 环境 VITE_APP_ENV=development @@ -17,6 +19,5 @@ VITE_APP_PORT=5180 # 超时时间 VITE_TIMEOUT=10000 - # WebSocket 服务器的 URL,不配置默认关闭 WebSocket -#VITE_APP_WS_ENDPOINT= ws://localhost:5180/ws \ No newline at end of file +VITE_APP_WS_ENDPOINT= ws://localhost:5180/ws \ No newline at end of file diff --git a/fastapp/.env.production b/fastapp/.env.production index 5d9ed9bf..9a994be0 100644 --- a/fastapp/.env.production +++ b/fastapp/.env.production @@ -1,3 +1,5 @@ +# 变量必须以 VITE_ 为前缀才能暴露给外部读取 + # 环境 VITE_APP_ENV=production @@ -17,4 +19,4 @@ VITE_APP_PORT=5180 VITE_TIMEOUT=10000 # WebSocket 服务器的 URL,不配置默认关闭 WebSocket -#VITE_APP_WS_ENDPOINT= ws://localhost:4096/ws \ No newline at end of file +VITE_APP_WS_ENDPOINT= ws://localhost:5180/ws \ No newline at end of file diff --git a/fastapp/.eslintrc-auto-import.json b/fastapp/.eslintrc-auto-import.json index 662da57f..3e525d14 100644 --- a/fastapp/.eslintrc-auto-import.json +++ b/fastapp/.eslintrc-auto-import.json @@ -93,6 +93,68 @@ "watchSyncEffect": true, "DirectiveBinding": true, "MaybeRef": true, - "MaybeRefOrGetter": true + "MaybeRefOrGetter": true, + "CommonUtil": true, + "Storage": true, + "acceptHMRUpdate": true, + "applyThemeOnPageShow": true, + "applyThemeToMiniProgram": true, + "auth": true, + "checkLogin": true, + "clearAll": true, + "clearToken": true, + "clearTokens": true, + "clearUserInfo": true, + "createPinia": true, + "createRouter": true, + "debounce": true, + "defineStore": true, + "dept": true, + "dict": true, + "file": true, + "getAccessToken": true, + "getActivePinia": true, + "getDarkerColor": true, + "getLighterColor": true, + "getRefreshToken": true, + "getToken": true, + "getUserInfo": true, + "isLoggedIn": true, + "log": true, + "mapActions": true, + "mapGetters": true, + "mapState": true, + "mapStores": true, + "mapWritableState": true, + "menu": true, + "notice": true, + "request": true, + "requireLogin": true, + "role": true, + "setAccessToken": true, + "setActivePinia": true, + "setMapStoreSuffix": true, + "setRefreshToken": true, + "setToken": true, + "setUserInfo": true, + "setupStore": true, + "store": true, + "storeToRefs": true, + "useDictStore": true, + "useMessage": true, + "useNotify": true, + "useRoute": true, + "useRouter": true, + "useThemeStore": true, + "useToast": true, + "useUserStore": true, + "user": true, + "config": true, + "extendedColorOptions": true, + "themeColorOptions": true, + "useStomp": true, + "useTabbar": true, + "useTheme": true, + "useWechat": true } } diff --git a/fastapp/.stylelintrc.json b/fastapp/.stylelintrc.json index 856b1abd..dc2cf332 100644 --- a/fastapp/.stylelintrc.json +++ b/fastapp/.stylelintrc.json @@ -1,11 +1,5 @@ { - "extends": [ - "stylelint-config-recommended", - "stylelint-config-recommended-scss", - "stylelint-config-recommended-vue/scss", - "stylelint-config-html/vue", - "stylelint-config-recess-order" - ], + "extends": ["stylelint-config-recommended", "stylelint-config-recommended-scss", "stylelint-config-recommended-vue/scss", "stylelint-config-html/vue", "stylelint-config-recess-order"], "plugins": ["stylelint-prettier"], "overrides": [ { diff --git a/fastapp/README.md b/fastapp/README.md index 5d3d8ada..c8febeed 100644 --- a/fastapp/README.md +++ b/fastapp/README.md @@ -1,10 +1,10 @@ -## 项目介绍 +# 项目介绍 基于 uni-app + Vue 3 + TypeScript 移动端跨平台开发模板,集成了 ESLint、Prettier、Stylelint、Husky 和 Commitlint 等工具,确保代码规范与质量。 ## 项目截图 -![](https://www.youlai.tech/storage/blog/2025/04/30/app.jpg) +![截图](https://www.youlai.tech/storage/blog/2025/04/30/app.jpg) ## 项目文档 @@ -14,51 +14,20 @@ 安装依赖 -``` +```node pnpm install ``` h5启动 -``` +```node pnpm run dev:h5 ``` 构建 -``` +```node pnpm run build:h5 ``` -访问 [http://localhost:4096](http://localhost:4096) - -## 组件结构 - -项目组件分为以下几类: - -- **基础组件**:`src/components` 下的通用组件 -- **业务组件**:`src/components/business` 下的业务相关组件 - - `WechatProfile.vue`: 微信小程序环境下的头像、昵称和性别选择组件 - -使用业务组件: - -```vue - - - -``` +访问 [http://localhost:5180/app](http://localhost:5180/app) diff --git a/fastapp/components.d.ts b/fastapp/components.d.ts index 2701df0b..fd8faa43 100644 --- a/fastapp/components.d.ts +++ b/fastapp/components.d.ts @@ -17,20 +17,20 @@ declare module 'vue' { QiunDataCharts: typeof import('./src/components/qiun-data-charts/qiun-data-charts.vue')['default'] QiunError: typeof import('./src/components/qiun-error/qiun-error.vue')['default'] QiunLoading: typeof import('./src/components/qiun-loading/qiun-loading.vue')['default'] + WdBadge: typeof import('wot-design-uni/components/wd-badge/wd-badge.vue')['default'] WdButton: typeof import('wot-design-uni/components/wd-button/wd-button.vue')['default'] WdCard: typeof import('wot-design-uni/components/wd-card/wd-card.vue')['default'] WdCell: typeof import('wot-design-uni/components/wd-cell/wd-cell.vue')['default'] WdCellGroup: typeof import('wot-design-uni/components/wd-cell-group/wd-cell-group.vue')['default'] + WdCheckbox: typeof import('wot-design-uni/components/wd-checkbox/wd-checkbox.vue')['default'] WdCollapse: typeof import('wot-design-uni/components/wd-collapse/wd-collapse.vue')['default'] WdCollapseItem: typeof import('wot-design-uni/components/wd-collapse-item/wd-collapse-item.vue')['default'] WdConfigProvider: typeof import('wot-design-uni/components/wd-config-provider/wd-config-provider.vue')['default'] WdDivider: typeof import('wot-design-uni/components/wd-divider/wd-divider.vue')['default'] WdForm: typeof import('wot-design-uni/components/wd-form/wd-form.vue')['default'] - WdFormItem: typeof import('wot-design-uni/components/wd-form-item/wd-form-item.vue')['default'] WdGrid: typeof import('wot-design-uni/components/wd-grid/wd-grid.vue')['default'] WdGridItem: typeof import('wot-design-uni/components/wd-grid-item/wd-grid-item.vue')['default'] WdIcon: typeof import('wot-design-uni/components/wd-icon/wd-icon.vue')['default'] - WdImage: typeof import('wot-design-uni/components/wd-image/wd-image.vue')['default'] WdImg: typeof import('wot-design-uni/components/wd-img/wd-img.vue')['default'] WdImgCropper: typeof import('wot-design-uni/components/wd-img-cropper/wd-img-cropper.vue')['default'] WdInput: typeof import('wot-design-uni/components/wd-input/wd-input.vue')['default'] @@ -39,12 +39,10 @@ declare module 'vue' { WdNavbar: typeof import('wot-design-uni/components/wd-navbar/wd-navbar.vue')['default'] WdNoticeBar: typeof import('wot-design-uni/components/wd-notice-bar/wd-notice-bar.vue')['default'] WdNotify: typeof import('wot-design-uni/components/wd-notify/wd-notify.vue')['default'] - WdPasswordInput: typeof import('wot-design-uni/components/wd-password-input/wd-password-input.vue')['default'] WdPopup: typeof import('wot-design-uni/components/wd-popup/wd-popup.vue')['default'] WdProgress: typeof import('wot-design-uni/components/wd-progress/wd-progress.vue')['default'] WdRadio: typeof import('wot-design-uni/components/wd-radio/wd-radio.vue')['default'] WdRadioGroup: typeof import('wot-design-uni/components/wd-radio-group/wd-radio-group.vue')['default'] - WdStatusTip: typeof import('wot-design-uni/components/wd-status-tip/wd-status-tip.vue')['default'] WdSwiper: typeof import('wot-design-uni/components/wd-swiper/wd-swiper.vue')['default'] WdSwitch: typeof import('wot-design-uni/components/wd-switch/wd-switch.vue')['default'] WdTabbar: typeof import('wot-design-uni/components/wd-tabbar/wd-tabbar.vue')['default'] @@ -52,7 +50,6 @@ declare module 'vue' { WdTag: typeof import('wot-design-uni/components/wd-tag/wd-tag.vue')['default'] WdText: typeof import('wot-design-uni/components/wd-text/wd-text.vue')['default'] WdTextarea: typeof import('wot-design-uni/components/wd-textarea/wd-textarea.vue')['default'] - WdTitle: typeof import('wot-design-uni/components/wd-title/wd-title.vue')['default'] WdToast: typeof import('wot-design-uni/components/wd-toast/wd-toast.vue')['default'] WdUpload: typeof import('wot-design-uni/components/wd-upload/wd-upload.vue')['default'] } diff --git a/fastapp/docs/theme-system-guide.md b/fastapp/docs/theme-system-guide.md index f67e715e..4ab07cdf 100644 --- a/fastapp/docs/theme-system-guide.md +++ b/fastapp/docs/theme-system-guide.md @@ -34,12 +34,7 @@ const { ```vue @@ -49,12 +44,7 @@ const { ```vue @@ -170,6 +160,7 @@ const THEME_COLOR_STORAGE_KEY = "app_theme_color"; ``` 主题设置会在以下情况自动保存: + - 切换主题模式时 - 设置主题色时 - 重置主题时 diff --git a/fastapp/docs/uniapp整合mini-router.md b/fastapp/docs/uniapp整合mini-router.md index b91498bc..51492094 100644 --- a/fastapp/docs/uniapp整合mini-router.md +++ b/fastapp/docs/uniapp整合mini-router.md @@ -29,55 +29,55 @@ pnpm add - uni-mini-router ```typescript // src/router/index.ts -import { createRouter } from 'uni-mini-router' -import { pages, subPackages } from 'virtual:uni-pages' +import { createRouter } from "uni-mini-router"; +import { pages, subPackages } from "virtual:uni-pages"; // 生成路由配置 function generateRoutes() { const routes = pages.map((page) => { - const newPath = `/${page.path}` - return { ...page, path: newPath } - }) + const newPath = `/${page.path}`; + return { ...page, path: newPath }; + }); // 处理分包路由 if (subPackages && subPackages.length > 0) { subPackages.forEach((subPackage) => { const subRoutes = subPackage.pages.map((page: any) => { - const newPath = `/${subPackage.root}/${page.path}` - return { ...page, path: newPath } - }) - routes.push(...subRoutes) - }) + const newPath = `/${subPackage.root}/${page.path}`; + return { ...page, path: newPath }; + }); + routes.push(...subRoutes); + }); } - return routes + return routes; } // 创建路由实例 const router = createRouter({ routes: generateRoutes(), -}) +}); -export default router +export default router; ``` ### 3. 在main.ts中挂载路由 ```typescript // src/main.ts -import { createSSRApp } from 'vue' -import App from './App.vue' -import router from './router' +import { createSSRApp } from "vue"; +import App from "./App.vue"; +import router from "./router"; export function createApp() { - const app = createSSRApp(App) + const app = createSSRApp(App); // 使用路由 - app.use(router) + app.use(router); return { - app - } + app, + }; } ``` @@ -87,22 +87,22 @@ export function createApp() { ```typescript // vite.config.ts -import AutoImport from 'unplugin-auto-import/vite' +import AutoImport from "unplugin-auto-import/vite"; export default defineConfig({ plugins: [ AutoImport({ imports: [ - 'vue', + "vue", { - from: 'uni-mini-router', - imports: ['createRouter', 'useRouter', 'useRoute'] - } + from: "uni-mini-router", + imports: ["createRouter", "useRouter", "useRoute"], + }, ], - dts: 'src/auto-imports.d.ts' - }) - ] -}) + dts: "src/auto-imports.d.ts", + }), + ], +}); ``` ## 三、路由基本用法 @@ -112,55 +112,55 @@ export default defineConfig({ uni-mini-router提供了多种导航方法: ```typescript -const router = useRouter() +const router = useRouter(); // 字符串路径导航 -router.push('/pages/index/index') +router.push("/pages/index/index"); // 对象导航(通过路径) -router.push({ path: '/pages/index/index' }) +router.push({ path: "/pages/index/index" }); // 对象导航(通过名称) -router.push({ name: 'index' }) +router.push({ name: "index" }); // 携带参数 router.push({ - path: '/pages/detail/index', - query: { id: 10 } -}) + path: "/pages/detail/index", + query: { id: 10 }, +}); // 通过名称 + 参数 router.push({ - name: 'detail', - params: { id: 10 } -}) + name: "detail", + params: { id: 10 }, +}); // Tab页面导航 -router.pushTab('/pages/home/index') +router.pushTab("/pages/home/index"); // 关闭当前页面并跳转 -router.replace('/pages/index/index') +router.replace("/pages/index/index"); // 关闭所有页面并跳转 -router.replaceAll('/pages/index/index') +router.replaceAll("/pages/index/index"); // 返回上一级 -router.back() +router.back(); // 返回多级 -router.back(2) +router.back(2); ``` ### 2. 获取和使用路由信息 ```typescript -const route = useRoute() +const route = useRoute(); // 访问当前路由信息 -console.log(route.path) // 当前路由路径 -console.log(route.name) // 当前路由名称 -console.log(route.query) // 查询参数 -console.log(route.params) // 路由参数 +console.log(route.path); // 当前路由路径 +console.log(route.name); // 当前路由名称 +console.log(route.query); // 查询参数 +console.log(route.params); // 路由参数 ``` ### 3. 接收页面参数 @@ -194,24 +194,24 @@ uni-mini-router提供了全局导航守卫功能,可以在路由跳转前后 ```typescript // src/router/index.ts router.beforeEach((to, from, next) => { - console.log('路由跳转:', from.path, '->', to.path) + console.log("路由跳转:", from.path, "->", to.path); // 检查是否需要登录 if (to.meta && to.meta.requireAuth) { // 检查登录状态 - const isLoggedIn = uni.getStorageSync('token') + const isLoggedIn = uni.getStorageSync("token"); if (!isLoggedIn) { // 未登录,跳转到登录页 - uni.showToast({ title: '请先登录', icon: 'none' }) - next('/pages/login/index') - return + uni.showToast({ title: "请先登录", icon: "none" }); + next("/pages/login/index"); + return; } } // 继续导航 - next() -}) + next(); +}); ``` ### 2. 全局后置守卫 @@ -219,10 +219,10 @@ router.beforeEach((to, from, next) => { ```typescript // src/router/index.ts router.afterEach((to, from) => { - console.log('路由跳转完成:', to.path) + console.log("路由跳转完成:", to.path); // 可以在这里做一些统计或记录 -}) +}); ``` ### 3. 路由元数据配置 @@ -255,45 +255,45 @@ router.afterEach((to, from) => { ```typescript // src/router/index.ts -import { createRouter } from 'uni-mini-router' +import { createRouter } from "uni-mini-router"; const router = createRouter({ - routes: generateRoutes() -}) + routes: generateRoutes(), +}); // 全局前置守卫 router.beforeEach((to, from, next) => { // 检查页面是否需要登录 if (to.meta && to.meta.requireAuth) { - const token = uni.getStorageSync('token') + const token = uni.getStorageSync("token"); if (!token) { // 显示登录提示 uni.showModal({ - title: '提示', - content: '该功能需要登录后使用', - confirmText: '去登录', - cancelText: '返回', + title: "提示", + content: "该功能需要登录后使用", + confirmText: "去登录", + cancelText: "返回", success: (res) => { if (res.confirm) { // 记住原来要去的页面 - uni.setStorageSync('redirect', to.fullPath) - next('/pages/login/index') + uni.setStorageSync("redirect", to.fullPath); + next("/pages/login/index"); } else { // 取消则返回首页 - next('/pages/index/index') + next("/pages/index/index"); } - } - }) - return + }, + }); + return; } } // 继续导航 - next() -}) + next(); +}); -export default router +export default router; ``` ### 2. 登录成功后跳转回原页面 @@ -301,21 +301,21 @@ export default router ```vue ``` @@ -336,25 +336,25 @@ function handleLogin() { ```typescript // 传递复杂对象 -const complexData = { name: 'product', details: { id: 1, features: ['a', 'b'] } } +const complexData = { name: "product", details: { id: 1, features: ["a", "b"] } }; // 方法1: JSON序列化 + URL编码 router.push({ - path: '/pages/detail/index', - query: { data: encodeURIComponent(JSON.stringify(complexData)) } -}) + path: "/pages/detail/index", + query: { data: encodeURIComponent(JSON.stringify(complexData)) }, +}); // 接收页面 onLoad((option) => { if (option.data) { try { - const data = JSON.parse(decodeURIComponent(option.data)) - console.log(data) + const data = JSON.parse(decodeURIComponent(option.data)); + console.log(data); } catch (e) { - console.error('参数解析错误', e) + console.error("参数解析错误", e); } } -}) +}); // 方法2: 对于非常大的数据,考虑使用全局状态管理或本地存储 ``` @@ -391,12 +391,12 @@ uni-mini-router自动支持小程序的分包加载特性,可以在pages.json ```typescript // src/router/index.ts router.beforeEach((to, from, next) => { - console.log(`[Router] ${from.path || '初始页面'} -> ${to.path}`, { + console.log(`[Router] ${from.path || "初始页面"} -> ${to.path}`, { params: to.params, query: to.query, - }) - next() -}) + }); + next(); +}); ``` ### 2. 常见问题解决 diff --git a/fastapp/eslint.config.mjs b/fastapp/eslint.config.mjs index ac8b60c9..0a1400bf 100644 --- a/fastapp/eslint.config.mjs +++ b/fastapp/eslint.config.mjs @@ -54,6 +54,9 @@ export default [ ApiResponse: "readonly", // 统一响应数据类型 creatorType: "readonly", // 创建人类型 UploadFileResult: "readonly", // 上传文件返回类型 + Todo: "readonly", // 待办事项 + TodoState: "readonly", // 待办事项状态 + plus: true, // HTML5+ 运行时环境 }, }, }, diff --git a/fastapp/package.json b/fastapp/package.json index e524fafa..3c80b88b 100644 --- a/fastapp/package.json +++ b/fastapp/package.json @@ -120,7 +120,7 @@ "@uni-helper/uni-use": "^0.19.14", "@vueuse/core": "9.13.0", "pinia": "^2.2.2", - "vue": "^3.4.21", + "vue": "^3.5.13", "vue-i18n": "^9.1.9", "wot-design-uni": "^1.9.1" }, diff --git a/fastapp/pages.config.ts b/fastapp/pages.config.ts index b5e19653..ea20c85c 100644 --- a/fastapp/pages.config.ts +++ b/fastapp/pages.config.ts @@ -41,6 +41,9 @@ export default defineUniPages({ { pagePath: "pages/index/index", }, + { + pagePath: "pages/work/index", + }, { pagePath: "pages/mine/index", }, diff --git a/fastapp/public/favicon.png b/fastapp/public/favicon.png index 8c016668..43eeb762 100644 Binary files a/fastapp/public/favicon.png and b/fastapp/public/favicon.png differ diff --git a/fastapp/public/logo.png b/fastapp/public/logo.png index 4a924529..81834963 100644 Binary files a/fastapp/public/logo.png and b/fastapp/public/logo.png differ diff --git a/fastapp/src/App.vue b/fastapp/src/App.vue index 553ca4dc..27a3c9b5 100644 --- a/fastapp/src/App.vue +++ b/fastapp/src/App.vue @@ -1,12 +1,13 @@ - + diff --git a/fastapp/src/api/auth.ts b/fastapp/src/api/auth.ts index 0f61d819..756b4920 100644 --- a/fastapp/src/api/auth.ts +++ b/fastapp/src/api/auth.ts @@ -13,9 +13,8 @@ const AuthAPI = { return request({ url: `${AUTH_BASE_URL}/login`, method: "POST", - headers: { [ApiHeader.KEY]: ApiHeader.FORM }, + header: { [ApiHeader.KEY]: ApiHeader.FORM }, data: body, - skipAuth: true, }); }, @@ -40,7 +39,6 @@ const AuthAPI = { return request({ url: `${AUTH_BASE_URL}/captcha/get`, method: "GET", - skipAuth: true, }); }, @@ -56,45 +54,10 @@ const AuthAPI = { data: body, }); }, - - /** - * 微信小程序手机号授权登录 - * @param data 包含code、encryptedData、iv等手机号相关数据 - * @returns 登录结果 - */ - loginByWxMiniAppPhone(data: WxLoginData): Promise { - return request({ - url: `${AUTH_BASE_URL}/wx/miniapp/phone-login`, - method: "POST", - data, - skipAuth: true, - }); - }, - - /** - * 微信小程序授权登录 (仅使用code获取OpenID) - * @param code 微信登录凭证 - * @returns 登录结果 - */ - loginByWxMiniAppCode(code: string): Promise { - return request({ - url: `${AUTH_BASE_URL}/wx/miniapp/code-login`, - method: "POST", - data: { code }, - skipAuth: true, - }); - }, }; export default AuthAPI; -export interface WxLoginData { - code: string; - encryptedData?: string; - iv?: string; - phoneCode?: string; -} - /** 登录表单数据 */ export interface LoginFormData { username: string; @@ -102,6 +65,7 @@ export interface LoginFormData { captcha_key: string; captcha: string; remember: boolean; + login_type: string; } // 刷新令牌 diff --git a/fastapp/src/api/file.ts b/fastapp/src/api/file.ts index ccdd1d85..ba42730e 100644 --- a/fastapp/src/api/file.ts +++ b/fastapp/src/api/file.ts @@ -1,4 +1,4 @@ -import { getToken } from "@/utils/storage"; +import { getAccessToken } from "@/utils/auth"; import { ApiCode } from "@/enums/api-code.enum"; // H5 使用 VITE_APP_BASE_API 作为代理路径,其他平台使用 VITE_API_BASE_URL 作为请求路径 @@ -25,12 +25,12 @@ const FileAPI = { filePath: filePath, name: "file", header: { - Authorization: getToken() ? `Bearer ${getToken()}` : "", + Authorization: getAccessToken() ? `Bearer ${getAccessToken()}` : "", }, formData: {}, success: (response) => { const resData = JSON.parse(response.data) as ApiResponse; - // 业务状态码 00000 表示成功 + // 业务状态码 0 表示成功 if (resData.code === ApiCode.SUCCESS) { resolve(resData.data); } else { diff --git a/fastapp/src/api/user.ts b/fastapp/src/api/user.ts index a105da96..10a31269 100644 --- a/fastapp/src/api/user.ts +++ b/fastapp/src/api/user.ts @@ -1,4 +1,5 @@ import request from "@/utils/request"; +import { ApiHeader } from "@/enums/api-header.enum"; const USER_BASE_URL = "/system/user"; @@ -26,7 +27,7 @@ const UserAPI = { url: `${USER_BASE_URL}/current/avatar/upload`, method: "POST", data: body, - headers: { "Content-Type": "multipart/form-data" }, + header: { [ApiHeader.KEY]: ApiHeader.MULTIPART }, }); }, @@ -213,7 +214,7 @@ export interface UserInfo { avatar?: string; email?: string; mobile?: string; - gender?: number; + gender?: string; password?: string; menus?: MenuTable[]; dept?: deptTreeType; @@ -290,7 +291,7 @@ export interface positionSelectorType { export interface UserProfileForm { id?: number; name?: string; - gender?: number; + gender?: string; mobile?: string; email?: string; username?: string; diff --git a/fastapp/src/components/qiun-data-charts/license.md b/fastapp/src/components/qiun-data-charts/license.md deleted file mode 100644 index c61b6639..00000000 --- a/fastapp/src/components/qiun-data-charts/license.md +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/fastapp/src/components/u-charts/license.md b/fastapp/src/components/u-charts/license.md deleted file mode 100644 index c61b6639..00000000 --- a/fastapp/src/components/u-charts/license.md +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/fastapp/src/components/u-charts/readme.md b/fastapp/src/components/u-charts/readme.md deleted file mode 100644 index b3906903..00000000 --- a/fastapp/src/components/u-charts/readme.md +++ /dev/null @@ -1,6 +0,0 @@ -# uCharts JSSDK说明 - -1、如不使用uCharts组件,可直接引用u-charts.js,打包编译后会`自动压缩`,压缩后体积约为`120kb`。 -2、如果120kb的体积仍需压缩,请手到uCharts官网通过在线定制选择您需要的图表。 -3、config-ucharts.js为uCharts组件的用户配置文件,升级前请`自行备份config-ucharts.js`文件,以免被强制覆盖。 -4、config-echarts.js为ECharts组件的用户配置文件,升级前请`自行备份config-echarts.js`文件,以免被强制覆盖。 diff --git a/fastapp/src/composables/useTheme.ts b/fastapp/src/composables/useTheme.ts index 296c3898..f341676b 100644 --- a/fastapp/src/composables/useTheme.ts +++ b/fastapp/src/composables/useTheme.ts @@ -1,345 +1,48 @@ -import type { ConfigProviderThemeVars } from "wot-design-uni"; -import { useThemeStore } from "@/store/modules/theme.store"; - -// 定义主题色选项 -export interface ThemeColorOption { - name: string; - value: string; - primary: string; -} - -// 预定义的主题色选项 -export const themeColorOptions: ThemeColorOption[] = [ - { name: "默认蓝", value: "blue", primary: "#4D7FFF" }, - { name: "活力橙", value: "orange", primary: "#FF7D00" }, - { name: "薄荷绿", value: "green", primary: "#07C160" }, - { name: "樱花粉", value: "pink", primary: "#FF69B4" }, - { name: "紫罗兰", value: "purple", primary: "#8A2BE2" }, - { name: "朱砂红", value: "red", primary: "#FF4757" }, - { name: "天空蓝", value: "sky", primary: "#00BFFF" }, - { name: "柠檬黄", value: "yellow", primary: "#FFD700" }, -]; - -// 扩展的颜色选项,用于自定义颜色选择器 -export const extendedColorOptions: ThemeColorOption[] = [ - ...themeColorOptions, - { name: "珊瑚红", value: "coral", primary: "#FF6B6B" }, - { name: "薄荷绿", value: "mint", primary: "#4ECDC4" }, - { name: "黄金黄", value: "gold", primary: "#FFD166" }, - { name: "经典红", value: "classic-red", primary: "#CD5C5C" }, - { name: "自然绿", value: "nature-green", primary: "#228B22" }, - { name: "天蓝色", value: "sky-blue", primary: "#1890FF" }, - { name: "青绿色", value: "teal", primary: "#0FC6C2" }, - { name: "深紫色", value: "deep-purple", primary: "#722ED1" }, -]; - -export interface ThemeState { - theme: "light" | "dark"; - isDark: boolean; - followSystem: boolean; - hasUserSet: boolean; - currentThemeColor: ThemeColorOption; - showThemeColorSheet: boolean; -} +import { useThemeStore, ThemeMode, ThemeColorOption } from "@/store"; export function useTheme() { - // 状态定义 - const theme = ref<"light" | "dark">("light"); - const followSystem = ref(true); // 是否跟随系统主题 - const hasUserSet = ref(false); // 用户是否手动设置过主题 - const currentThemeColor = ref(themeColorOptions[0]); - const showThemeColorSheet = ref(false); + const store = useThemeStore(); - // 主题变量 - const themeVars = reactive({ - darkBackground: "#0f0f0f", - darkBackground2: "#1a1a1a", - darkBackground3: "#242424", - darkBackground4: "#2f2f2f", - darkBackground5: "#3d3d3d", - darkBackground6: "#4a4a4a", - darkBackground7: "#606060", - darkColor: "#ffffff", - darkColor2: "#e0e0e0", - darkColor3: "#a0a0a0", - colorTheme: themeColorOptions[0].primary, - }); - - // 计算属性 - const isDark = computed(() => theme.value === "dark"); - - // 主题状态 - const themeState = computed(() => ({ - theme: theme.value, - isDark: isDark.value, - followSystem: followSystem.value, - hasUserSet: hasUserSet.value, - currentThemeColor: currentThemeColor.value, - showThemeColorSheet: showThemeColorSheet.value, - })); - - /* 手动切换主题 */ - function toggleTheme(mode?: "light" | "dark") { - theme.value = mode || (theme.value === "light" ? "dark" : "light"); - hasUserSet.value = true; - followSystem.value = false; - setNavigationBarColor(); - saveThemeSettings(); - - // 实时应用主题变化 - applyThemeModeToApp(); + /** + * 切换暗黑模式 + * @param mode 指定主题模式,不传则自动切换 + */ + function toggleTheme(mode?: ThemeMode) { + store.toggleTheme(mode); } - /* 实时应用主题模式到应用 */ - function applyThemeModeToApp() { - // 更新CSS变量 - if (typeof document !== "undefined") { - document.documentElement.setAttribute("data-theme", theme.value); - } - - // 更新导航栏颜色 - setNavigationBarColor(); + /** + * 设置主题色 + * @param option 主题色选项 + */ + function setThemeColor(option: ThemeColorOption) { + store.setCurrentThemeColor(option); } - /* 设置是否跟随系统主题 */ - function setFollowSystem(follow: boolean) { - followSystem.value = follow; - if (follow) { - hasUserSet.value = false; - initTheme(); - } - saveThemeSettings(); - - // 实时应用主题变化 - applyThemeModeToApp(); - } - - /* 设置主题色 */ - function setCurrentThemeColor(color: ThemeColorOption) { - currentThemeColor.value = color; - themeVars.colorTheme = color.primary; - hasUserSet.value = true; - saveThemeSettings(); - - // 实时应用主题色到全局 - applyThemeColorToApp(color.primary); - } - - /* 设置自定义主题色 */ - function setCustomThemeColor(color: string) { - const customTheme: ThemeColorOption = { - name: "自定义", - value: "custom", - primary: color, - }; - setCurrentThemeColor(customTheme); - } - - /* 重置主题 */ - function resetTheme() { - setCurrentThemeColor(themeColorOptions[0]); - theme.value = "light"; - followSystem.value = true; - hasUserSet.value = false; - initTheme(); - } - - /* 保存主题设置到本地存储 */ - function saveThemeSettings() { - try { - uni.setStorageSync("theme_settings", { - theme: theme.value, - currentThemeColor: currentThemeColor.value, - followSystem: followSystem.value, - hasUserSet: hasUserSet.value, - }); - } catch (error) { - console.warn("保存主题设置失败:", error); - } - } - - /* 从本地存储加载主题设置 */ - function loadThemeSettings() { - try { - const settings = uni.getStorageSync("theme_settings"); - if (settings) { - if (settings.theme && (settings.theme === "light" || settings.theme === "dark")) { - theme.value = settings.theme; - } - if (settings.currentThemeColor) { - const savedColor = - themeColorOptions.find((c) => c.value === settings.currentThemeColor.value) || - extendedColorOptions.find((c) => c.value === settings.currentThemeColor.value) || - settings.currentThemeColor; - if (savedColor) { - currentThemeColor.value = savedColor; - themeVars.colorTheme = savedColor.primary; - } - } - followSystem.value = settings.followSystem !== false; - hasUserSet.value = settings.hasUserSet === true; - } - } catch (error) { - console.warn("加载主题设置失败:", error); - } - } - - /* 获取系统主题 */ - function getSystemTheme(): "light" | "dark" { - try { - // #ifdef MP-WEIXIN - const appBaseInfo = uni.getAppBaseInfo(); - if (appBaseInfo?.theme) { - return appBaseInfo.theme as "light" | "dark"; - } - // #endif - - // #ifndef MP-WEIXIN - const systemInfo = uni.getSystemInfoSync(); - if (systemInfo?.theme) { - return systemInfo.theme as "light" | "dark"; - } - // #endif - } catch (error) { - console.warn("获取系统主题失败:", error); - } - return "light"; - } - - /* 设置导航栏颜色 */ - function setNavigationBarColor() { - // #ifndef H5 - uni.setNavigationBarColor({ - frontColor: theme.value === "light" ? "#000000" : "#ffffff", - backgroundColor: theme.value === "light" ? "#ffffff" : "#000000", - }); - // #endif - } - - /* 实时应用主题色到应用 */ - function applyThemeColorToApp(color: string) { - // 更新Wot Design组件库主题色 - themeVars.colorTheme = color; - - // 更新CSS变量 - if (typeof document !== "undefined") { - document.documentElement.style.setProperty("--wot-color-theme", color); - document.documentElement.style.setProperty("--primary-color", color); - document.documentElement.style.setProperty("--primary-color-light", color + "20"); - document.documentElement.style.setProperty("--primary-color-dark", color); - } - - // 同步到主题存储 - const themeStore = useThemeStore(); - themeStore.setPrimaryColor(color); - - // 通过事件总线通知全局主题变化 - uni.$emit("theme-color-changed", color); - - // 强制刷新页面样式 - setTimeout(() => { - uni.$emit("force-theme-refresh"); - }, 50); - } - - /* 初始化主题 */ + /** + * 初始化主题 + */ function initTheme() { - if (hasUserSet.value && !followSystem.value) { - setNavigationBarColor(); - applyThemeColorToApp(currentThemeColor.value.primary); - return; - } - - const systemTheme = getSystemTheme(); - if (!hasUserSet.value || followSystem.value) { - theme.value = systemTheme; - } - - setNavigationBarColor(); - applyThemeColorToApp(currentThemeColor.value.primary); + store.initTheme(); } - /* 主题色选择器相关 */ - function openThemeColorPicker() { - showThemeColorSheet.value = true; - } - - function closeThemeColorPicker() { - showThemeColorSheet.value = false; - } - - function selectThemeColor(option: ThemeColorOption) { - setCurrentThemeColor(option); - closeThemeColorPicker(); - } - - // 生命周期 - onBeforeMount(() => { - loadThemeSettings(); - initTheme(); - - // #ifdef MP-WEIXIN - if (uni.onThemeChange) { - uni.onThemeChange((res: any) => { - if (followSystem.value) { - theme.value = res.theme; - setNavigationBarColor(); - } - }); - } - // #endif - - // #ifndef MP-WEIXIN - if (uni.onThemeChange) { - uni.onThemeChange((res: any) => { - if (followSystem.value) { - theme.value = res.theme; - setNavigationBarColor(); - } - }); - } - // #endif - }); - - onUnmounted(() => { - // 清理监听器 - if (uni.offThemeChange) { - uni.offThemeChange(() => {}); - } - }); + // 注意:全局主题管理已在App.vue中处理 + // 包括:系统主题监听、导航栏颜色同步等 + // 组件中一般不需要再调用initTheme(),除非有特殊需求 return { // 状态 - theme: computed(() => theme.value), - isDark, - followSystem: computed(() => followSystem.value), - hasUserSet: computed(() => hasUserSet.value), - currentThemeColor: computed(() => currentThemeColor.value), - showThemeColorSheet: computed(() => showThemeColorSheet.value), - themeState, + theme: computed(() => store.theme), + isDark: computed(() => store.isDark), + currentThemeColor: computed(() => store.currentThemeColor), + themeVars: computed(() => store.themeVars), // 主题选项 - themeColorOptions, - extendedColorOptions, - - // 主题变量 - themeVars, + themeColorOptions: computed(() => store.themeColorOptions), // 方法 - toggleTheme, - setFollowSystem, - setCurrentThemeColor, - setCustomThemeColor, - resetTheme, initTheme, - - // 主题色选择器 - openThemeColorPicker, - closeThemeColorPicker, - selectThemeColor, - - // 工具方法 - saveThemeSettings, - loadThemeSettings, + toggleTheme, + setThemeColor, }; } diff --git a/fastapp/src/composables/useWechat.ts b/fastapp/src/composables/useWechat.ts deleted file mode 100644 index 871d9385..00000000 --- a/fastapp/src/composables/useWechat.ts +++ /dev/null @@ -1,159 +0,0 @@ -/** - * 微信授权服务 - * 处理微信登录授权、获取用户信息等功能 - */ - -import { ref } from "vue"; -import { getAccessToken } from "@/utils/auth"; - -export function useWechat() { - // 定义微信授权状态 - const authState = ref({ - isLogining: false, - authDenied: false, - }); - - /** - * 获取微信登录凭证code - * @returns Promise 返回登录凭证code - */ - const getLoginCode = (): Promise => { - return new Promise((resolve, reject) => { - // #ifdef MP-WEIXIN - uni.login({ - provider: "weixin", - success: (res) => { - if (res.code) { - resolve(res.code); - } else { - reject(new Error("获取微信登录凭证失败")); - } - }, - fail: (err) => { - reject(err); - }, - }); - // #endif - - // #ifndef MP-WEIXIN - reject(new Error("当前环境不支持微信登录")); - // #endif - }); - }; - - /** - * 获取微信用户手机号 - * @param e 微信授权返回的事件对象 - * @returns Promise 返回包含手机号加密数据的对象 - */ - const getPhoneNumber = ( - e: any - ): Promise<{ code: string; encryptedData?: string; iv?: string }> => { - return new Promise((resolve, reject) => { - authState.value.isLogining = true; - - // 判断授权是否成功 - if (e.detail.errMsg !== "getPhoneNumber:ok") { - authState.value.isLogining = false; - authState.value.authDenied = true; - reject(new Error("用户拒绝授权")); - return; - } - - // 获取登录凭证code - getLoginCode() - .then((code) => { - // 在微信小程序环境下,可以获取encryptedData和iv - // #ifdef MP-WEIXIN - resolve({ - code, - encryptedData: e.detail.encryptedData, - iv: e.detail.iv, - }); - // #endif - - // 其他环境或新版本接口 - // #ifndef MP-WEIXIN - resolve({ - code, - // 新版本接口在e.detail.code中包含手机号获取凭证 - ...(e.detail.code ? { phoneCode: e.detail.code } : {}), - }); - // #endif - }) - .catch((err) => { - reject(err); - }) - .finally(() => { - authState.value.isLogining = false; - }); - }); - }; - - /** - * 检查会话有效性 - * @returns Promise 返回会话是否有效 - */ - const checkSession = (): Promise => { - return new Promise((resolve) => { - const token = getAccessToken(); - - if (!token) { - resolve(false); - return; - } - - // 调用后端接口验证token有效性 - uni.request({ - url: "/api/v1/auth/check-session", - method: "GET", - header: { - Authorization: `Bearer ${token}`, - }, - success: (res: any) => { - if (res.statusCode === 200 && res.data.valid) { - resolve(true); - } else { - resolve(false); - } - }, - fail: () => { - resolve(false); - }, - }); - }); - }; - - /** - * 获取用户头像昵称 - * 注意:此接口已于2021年弃用,仅作为兼容保留 - * 推荐使用button组件的open-type="chooseAvatar"让用户选择头像 - */ - const getUserProfile = (): Promise => { - return new Promise((resolve, reject) => { - // #ifdef MP-WEIXIN - uni.getUserProfile({ - desc: "用于完善用户资料", - success: (res) => { - resolve(res.userInfo); - }, - fail: (err) => { - reject(err); - }, - }); - // #endif - - // #ifndef MP-WEIXIN - reject(new Error("当前环境不支持获取用户信息")); - // #endif - }); - }; - - return { - authState, - getLoginCode, - getPhoneNumber, - checkSession, - getUserProfile, - }; -} diff --git a/fastapp/src/constants/storage.constant.ts b/fastapp/src/constants/storage.constant.ts index 6567efa5..3c3d22f3 100644 --- a/fastapp/src/constants/storage.constant.ts +++ b/fastapp/src/constants/storage.constant.ts @@ -3,9 +3,14 @@ * 包括 localStorage、sessionStorage 等各种存储的键名 */ -// 🔐 用户认证相关 -export const ACCESS_TOKEN_KEY = "access_token"; -export const REFRESH_TOKEN_KEY = "refresh_token"; +// 访问token +export const APP_ACCESS_TOKEN_KEY = "appAccessToken"; -// 📊 用户缓存相关 -export const USER_INFO_KEY = "user_info"; +// 刷新token +export const APP_REFRESH_TOKEN_KEY = "appRefreshToken"; + +// 用户缓存相关 +export const APP_USER_INFO = "appUserInfo"; + +// 全局配置相关 +export const APP_THEME_KEY = "appTheme"; diff --git a/fastapp/src/enums/api-code.enum.ts b/fastapp/src/enums/api-code.enum.ts index 6b3f784c..94ca0dd9 100644 --- a/fastapp/src/enums/api-code.enum.ts +++ b/fastapp/src/enums/api-code.enum.ts @@ -20,12 +20,12 @@ export const enum ApiCode { /** * 未授权访问 */ - UNAUTHORIZED = 401, + UNAUTHORIZED = 10403, /** * 令牌已过期 */ - TOKEN_EXPIRED = 403, + TOKEN_EXPIRED = 10401, /** * 参数校验失败 diff --git a/fastapp/src/manifest.json b/fastapp/src/manifest.json index 687e9793..b377e4fa 100644 --- a/fastapp/src/manifest.json +++ b/fastapp/src/manifest.json @@ -15,8 +15,11 @@ "autoclose": true, "delay": 0 }, + /* 模块配置 */ "modules": {}, + /* 应用发布信息 */ "distribute": { + /* android打包配置 */ "android": { "permissions": [ "", @@ -36,11 +39,15 @@ "" ] }, + /* ios打包配置 */ "ios": {}, + /* SDK配置 */ "sdkConfigs": {} } }, + /* 快应用特有相关 */ "quickapp": {}, + /* 小程序特有相关:"appid": "wx99a151dc43d2637b" */ "mp-weixin": { "appid": "微信开发者ID", "setting": { @@ -50,12 +57,15 @@ "darkmode": true, "themeLocation": "theme.json" }, + /* 抖音小程序特有相关 */ "mp-alipay": { "usingComponents": true }, + /* 百度小程序特有相关 */ "mp-baidu": { "usingComponents": true }, + /* 字节跳动小程序特有相关 */ "mp-toutiao": { "usingComponents": true }, diff --git a/fastapp/src/pages.json b/fastapp/src/pages.json index 7e7649b7..da1d2c49 100644 --- a/fastapp/src/pages.json +++ b/fastapp/src/pages.json @@ -25,25 +25,38 @@ { "path": "pages/work/index", "type": "page", - "name": "index", + "name": "work", "style": { - "navigationBarTitleText": "工作台" + "navigationStyle": "custom" }, + "layout": "tabbar", "meta": { "requireAuth": true } }, { "path": "pages/mine/about/index", - "type": "page" + "type": "page", + "name": "about", + "style": { + "navigationBarTitleText": "关于我们" + } }, { "path": "pages/mine/faq/index", - "type": "page" + "type": "page", + "name": "faq", + "style": { + "navigationBarTitleText": "常见问题" + } }, { "path": "pages/mine/feedback/index", - "type": "page" + "type": "page", + "name": "feedback", + "style": { + "navigationBarTitleText": "问题反馈" + } }, { "path": "pages/mine/profile/complete-profile", @@ -55,23 +68,54 @@ }, { "path": "pages/mine/settings/index", - "type": "page" + "type": "page", + "name": "settings", + "style": { + "navigationBarTitleText": "设置" + } }, { "path": "pages/mine/settings/account/index", - "type": "page" + "type": "page", + "name": "account", + "style": { + "navigationBarTitleText": "账号和安全" + }, + "layout": "tabbar" }, { "path": "pages/mine/settings/agreement/index", - "type": "page" + "type": "page", + "name": "agreement", + "style": { + "navigationBarTitleText": "用户协议" + }, + "layout": "tabbar" }, { "path": "pages/mine/settings/network/index", - "type": "page" + "type": "page", + "name": "network", + "style": { + "navigationBarTitleText": "网络测试" + } + }, + { + "path": "pages/mine/settings/privacy/index", + "type": "page", + "name": "privacy", + "style": { + "navigationBarTitleText": "隐私政策" + }, + "layout": "tabbar" }, { "path": "pages/mine/settings/theme/index", - "type": "page" + "type": "page", + "name": "theme", + "style": { + "navigationBarTitleText": "主题设置" + } } ], "globalStyle": { @@ -100,6 +144,9 @@ { "pagePath": "pages/index/index" }, + { + "pagePath": "pages/work/index" + }, { "pagePath": "pages/mine/index" } diff --git a/fastapp/src/pages/index/index.vue b/fastapp/src/pages/index/index.vue index ad12355f..55cc91a6 100644 --- a/fastapp/src/pages/index/index.vue +++ b/fastapp/src/pages/index/index.vue @@ -1,4 +1,5 @@