mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-20 20:39:55 +00:00
docs(backend): 重构后端README文档,完善项目文档与开发指南
- 全面更新README,添加项目特性、架构设计与技术栈介绍 - 详细补充项目结构说明及模块设计规范 - 增加快速开始步骤,包括环境配置、数据库初始化和服务启动 - 补充主要API模块路径和认证授权使用示例 - 添加开发指南、数据库迁移与测试方法 - 集成监控、日志级别说明及性能监控内容 - 完善Docker、传统部署及Nginx配置示例 - 添加贡献指南和代码规范说明 - 新增MCP模块概述及智能对话API接口文档 - 清理和移除module_ai中旧的mcp_server相关实现代码 - 在api/v1初始化文件中注册AI模块路由 - 修改resource模块,增强资源路径安全检查和文件类型检测逻辑 - 增强资源搜索和上传服务的健壮性及安全性
This commit is contained in:
+363
-72
@@ -1,105 +1,396 @@
|
||||
# backend
|
||||
# FastAPI Vue3 Admin - Backend
|
||||
|
||||
## 项目结构
|
||||
一个基于 FastAPI 的现代化后端管理系统,为前端 Vue3 管理系统提供完整的 API 服务支持。
|
||||
|
||||
```sh
|
||||
fastapi_project/backend
|
||||
├─ app # 项目核心代码
|
||||
│ ├─ almbic # 数据库迁移文件
|
||||
│ ├─ api # 接口模块
|
||||
│ │ └─ v1 # 接口版本模块
|
||||
│ │ ├─ controllers # 控制器模块
|
||||
│ │ ├─ crud # 数据库操作模块
|
||||
│ │ ├─ models # orm模型模块
|
||||
│ │ ├─ params # 参数模块
|
||||
│ │ ├─ schemas # pydantic模型模块
|
||||
│ │ ├─ services # 业务模块
|
||||
│ │ └─ urls # 路由模块
|
||||
│ ├─ common # 公共模块
|
||||
│ ├─ config # 项目配置文件
|
||||
│ ├─ core # 项目核心模块
|
||||
│ ├─ module_task # 项目任务模块
|
||||
│ ├─ plugin # 项目插件模块
|
||||
│ ├─ scripts # 项目初始化模块
|
||||
│ └─ utils # 工具模块
|
||||
├─ env # 项目环境配置文件
|
||||
├─ logs # 项目日志文件
|
||||
├─ sql # 项目数据库文件
|
||||
├─ static # 项目静态文件
|
||||
├─ main.py # 项目启动文件
|
||||
├─ alembic.ini # alembic配置文件
|
||||
├─ requirements.txt # 项目依赖文件
|
||||
├─ mkdocs.yml # mkdocs配置文件
|
||||
├─ dev_sql.db # 项目数据库文件
|
||||
└─ README.md # 项目说明文档
|
||||
## 🚀 项目特性
|
||||
|
||||
- **现代技术栈**: FastAPI + SQLAlchemy 2.0 + Pydantic 2.x
|
||||
- **多数据库支持**: MySQL、PostgreSQL、SQLite
|
||||
- **异步架构**: 支持高并发异步数据库操作
|
||||
- **权限管理**: 完整的 RBAC 权限控制体系
|
||||
- **任务调度**: 基于 APScheduler 的定时任务系统
|
||||
- **日志监控**: 完整的操作日志和系统监控
|
||||
- **代码生成**: 智能化代码生成工具
|
||||
- **AI 集成**: 支持 OpenAI 大模型调用
|
||||
- **云存储**: 支持阿里云 OSS 对象存储
|
||||
|
||||
## 🏗️ 系统架构
|
||||
|
||||
### 技术栈
|
||||
|
||||
| 技术 | 版本 | 说明 |
|
||||
|------|------|------|
|
||||
| FastAPI | 0.115.2 | 现代 Web 框架 |
|
||||
| SQLAlchemy | 2.0.36 | ORM 框架 |
|
||||
| Alembic | 1.15.1 | 数据库迁移工具 |
|
||||
| Pydantic | 2.x | 数据验证与序列化 |
|
||||
| APScheduler | 3.11.0 | 定时任务调度 |
|
||||
| Redis | 5.2.1 | 缓存与会话存储 |
|
||||
| Uvicorn | 0.30.6 | ASGI 服务器 |
|
||||
| Python | 3.10+ | 运行环境 |
|
||||
|
||||
### 架构设计
|
||||
|
||||
```txt
|
||||
📦 分层架构 (MVC)
|
||||
├── 🎯 Controller # 控制器层 - 处理HTTP请求
|
||||
├── 🏢 Service # 业务层 - 核心业务逻辑
|
||||
├── 💾 CRUD # 数据访问层 - 数据库操作
|
||||
└── 📊 Model # 模型层 - 数据模型定义
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
## 📁 项目结构
|
||||
|
||||
```txt
|
||||
fastapi_vue3_admin/backend/
|
||||
├── 📁 app/ # 项目核心代码
|
||||
│ ├── 💾 alembic/ # 数据库迁移管理
|
||||
│ ├── 🌐 api/ # API 接口模块
|
||||
│ │ └── v1/ # API v1 版本
|
||||
│ │ ├── module_system/ # 系统管理模块
|
||||
│ │ ├── module_monitor/ # 系统监控模块
|
||||
│ │ ├── module_ai/ # AI 功能模块
|
||||
│ │ └── module_*/ # 其他业务模块
|
||||
│ ├── 📄 common/ # 公共组件(常量、枚举、响应封装)
|
||||
│ ├── ⚙️ config/ # 项目配置文件
|
||||
│ ├── 💖 core/ # 核心模块(数据库、中间件、安全)
|
||||
│ ├── ⏰ module_task/ # 定时任务模块
|
||||
│ ├── 🔌 plugin/ # 插件模块
|
||||
│ ├── 📜 scripts/ # 初始化脚本和数据
|
||||
│ └── 🛠️ utils/ # 工具类(验证码、文件上传等)
|
||||
├── 🌍 env/ # 环境配置文件
|
||||
├── 📄 logs/ # 日志输出目录
|
||||
├── 📊 sql/ # SQL 初始化脚本
|
||||
├── 📷 static/ # 静态资源文件
|
||||
├── 🚀 main.py # 项目启动入口
|
||||
├── 📄 alembic.ini # Alembic 迁移配置
|
||||
├── 📎 requirements.txt # Python 依赖包
|
||||
└── 📝 README.md # 项目说明文档
|
||||
```
|
||||
|
||||
### 模块设计
|
||||
|
||||
每个业务模块采用统一的分层结构:
|
||||
|
||||
```txt
|
||||
module_*/
|
||||
├── controller.py # 控制器 - HTTP 请求处理
|
||||
├── service.py # 服务层 - 业务逻辑处理
|
||||
├── crud.py # 数据层 - 数据库操作
|
||||
├── model.py # ORM 模型 - 数据库表定义
|
||||
├── schema.py # Pydantic 模型 - 数据验证
|
||||
└── param.py # 参数模型 - 请求参数
|
||||
```
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 环境要求
|
||||
|
||||
- **Python**: 3.10+
|
||||
- **数据库**: MySQL 8.0+ / PostgreSQL 13+ / SQLite 3.x
|
||||
- **Redis**: 6.0+ (可选)
|
||||
|
||||
### 安装与运行
|
||||
|
||||
#### 1. 项目初始化
|
||||
|
||||
```bash
|
||||
# 克隆项目
|
||||
git clone <repository-url>
|
||||
cd fastapi_vue3_admin/backend
|
||||
|
||||
# 创建虚拟环境(推荐)
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate # Windows: venv\Scripts\activate
|
||||
|
||||
```sh
|
||||
# 进入后端工程目录
|
||||
cd backend
|
||||
# 安装依赖
|
||||
pip3 install -r requirements.txt
|
||||
# 启动后端服务
|
||||
python3 main.py run
|
||||
或
|
||||
python3 main.py run--env=dev
|
||||
# 生成迁移文件
|
||||
python3 main.py revision "初始化迁移" --env=dev(不加默认为dev)
|
||||
# 应用迁移
|
||||
python3 main.py upgrade --env=dev(不加默认为dev)
|
||||
```
|
||||
|
||||
## Markdown转静态网站(mkdocs-material使用)介绍
|
||||
#### 2. 环境配置
|
||||
|
||||
- 名称:mkdocs-material
|
||||
- 官网:<https://squidfunk.github.io/mkdocs-material/>
|
||||
- 完整文档信息清访问 [mkdocs官网](https://www.mkdocs.org).
|
||||
复制并编辑环境配置文件:
|
||||
|
||||
## 工具安装
|
||||
|
||||
```sh
|
||||
pip install mkdocs-material
|
||||
```bash
|
||||
cp env/dev.env.example env/dev.env
|
||||
# 编辑 env/dev.env 文件,配置数据库连接和其他参数
|
||||
```
|
||||
|
||||
## 工具使用
|
||||
主要配置项:
|
||||
|
||||
### 创建项目
|
||||
- `DATABASE_URL`: 数据库连接地址
|
||||
- `SECRET_KEY`: JWT 加密密钥
|
||||
- `REDIS_URL`: Redis 连接地址(可选)
|
||||
|
||||
```sh
|
||||
mkdocs new 项目名称
|
||||
#### 3. 数据库初始化
|
||||
|
||||
```bash
|
||||
# 生成迁移文件(仅首次或模型变更时)
|
||||
python3 main.py revision "初始化迁移" --env=dev
|
||||
|
||||
# 应用数据库迁移
|
||||
python3 main.py upgrade --env=dev
|
||||
|
||||
# 初始化数据(可选,系统会自动初始化)
|
||||
python3 main.py init-data --env=dev
|
||||
```
|
||||
|
||||
### 编辑构建文档
|
||||
#### 4. 启动服务
|
||||
|
||||
```yaml
|
||||
theme:
|
||||
name: material
|
||||
```bash
|
||||
# 开发环境启动
|
||||
python3 main.py run --env=dev
|
||||
|
||||
# 生产环境启动
|
||||
python3 main.py run --env=prod
|
||||
|
||||
# 或使用 uvicorn 直接启动
|
||||
uvicorn main:app --host 0.0.0.0 --port 8000 --reload
|
||||
```
|
||||
|
||||
### 启动服务
|
||||
服务成功启动后,您可以访问:
|
||||
|
||||
```sh
|
||||
mkdocs serve
|
||||
- **API 文档**: [http://localhost:8000/docs](http://localhost:8000/docs) (Swagger UI)
|
||||
- **替代文档**: [http://localhost:8000/redoc](http://localhost:8000/redoc) (ReDoc)
|
||||
- **健康检查**: [http://localhost:8000/health](http://localhost:8000/health)
|
||||
|
||||
## 📚 API 文档
|
||||
|
||||
### 主要接口模块
|
||||
|
||||
| 模块 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| 用户管理 | `/api/v1/system/user` | 用户增删改查、角色分配 |
|
||||
| 角色管理 | `/api/v1/system/role` | 角色管理、权限分配 |
|
||||
| 菜单管理 | `/api/v1/system/menu` | 系统菜单、权限节点 |
|
||||
| 部门管理 | `/api/v1/system/dept` | 组织架构管理 |
|
||||
| 岗位管理 | `/api/v1/system/position` | 岗位信息管理 |
|
||||
| 系统监控 | `/api/v1/monitor/*` | 系统性能、日志监控 |
|
||||
| 任务调度 | `/api/v1/monitor/job` | 定时任务管理 |
|
||||
| 文件管理 | `/api/v1/common/file` | 文件上传下载 |
|
||||
| 代码生成 | `/api/v1/generator/*` | 代码生成工具 |
|
||||
|
||||
### 认证授权
|
||||
|
||||
系统使用 JWT Bearer Token 进行身份验证:
|
||||
|
||||
```bash
|
||||
# 登录获取 Token
|
||||
curl -X POST "http://localhost:8000/api/v1/auth/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username": "admin", "password": "123456"}'
|
||||
|
||||
# 使用 Token 访问受保护的资源
|
||||
curl -X GET "http://localhost:8000/api/v1/system/user/list" \
|
||||
-H "Authorization: Bearer YOUR_TOKEN_HERE"
|
||||
```
|
||||
|
||||
### 构建静态站点
|
||||
## 🛠️ 开发指南
|
||||
|
||||
```sh
|
||||
mkdocs build
|
||||
### 项目配置
|
||||
|
||||
主要配置文件位于 `app/config/setting.py`,支持通过环境变量进行覆盖。
|
||||
|
||||
### 数据库迁移
|
||||
|
||||
```bash
|
||||
# 查看当前迁移状态
|
||||
python3 main.py current --env=dev
|
||||
|
||||
# 查看迁移历史
|
||||
python3 main.py history --env=dev
|
||||
|
||||
# 回滚到上一个版本
|
||||
python3 main.py downgrade -1 --env=dev
|
||||
```
|
||||
|
||||
### 获取帮助
|
||||
### 添加新模块
|
||||
|
||||
```sh
|
||||
mkdocs -h
|
||||
1. 在 `app/api/v1/` 下创建新的模块目录
|
||||
2. 按照现有模块结构创建文件:
|
||||
- `model.py` - SQLAlchemy ORM 模型
|
||||
- `schema.py` - Pydantic 数据模型
|
||||
- `crud.py` - 数据库操作层
|
||||
- `service.py` - 业务逻辑层
|
||||
- `controller.py` - API 控制器
|
||||
- `param.py` - 请求参数模型
|
||||
3. 在主路由中注册新模块
|
||||
|
||||
### 测试
|
||||
|
||||
```bash
|
||||
# 运行单元测试
|
||||
pytest tests/
|
||||
|
||||
# 运行指定测试文件
|
||||
pytest tests/test_user.py -v
|
||||
|
||||
# 生成测试覆盖率报告
|
||||
pytest --cov=app tests/
|
||||
```
|
||||
|
||||
### 生成所有依赖
|
||||
## 📊 监控与日志
|
||||
|
||||
```sh
|
||||
pip freeze > requirements.txt
|
||||
### 日志级别
|
||||
|
||||
- **DEBUG**: 详细的调试信息
|
||||
- **INFO**: 一般信息(默认级别)
|
||||
- **WARNING**: 警告信息
|
||||
- **ERROR**: 错误信息
|
||||
- **CRITICAL**: 严重错误
|
||||
|
||||
### 性能监控
|
||||
|
||||
系统内置了完整的性能监控功能:
|
||||
|
||||
- API 响应时间监控
|
||||
- 数据库连接池监控
|
||||
- 内存与 CPU 使用率监控
|
||||
- 自定义业务指标监控
|
||||
|
||||
## 🚀 部署指南
|
||||
|
||||
### Docker 部署
|
||||
|
||||
```bash
|
||||
# 构建镜像
|
||||
docker build -t fastapi-admin .
|
||||
|
||||
# 运行容器
|
||||
docker run -d \
|
||||
--name fastapi-admin \
|
||||
-p 8000:8000 \
|
||||
-e DATABASE_URL="postgresql://user:pass@host:5432/db" \
|
||||
fastapi-admin
|
||||
```
|
||||
|
||||
### 传统部署
|
||||
|
||||
```bash
|
||||
# 使用 Gunicorn 作为 WSGI 服务器
|
||||
gunicorn main:app \
|
||||
--workers 4 \
|
||||
--worker-class uvicorn.workers.UvicornWorker \
|
||||
--bind 0.0.0.0:8000 \
|
||||
--access-logfile - \
|
||||
--error-logfile -
|
||||
```
|
||||
|
||||
### Nginx 配置
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🤝 贡献指南
|
||||
|
||||
欢迎提交 Issue 和 Pull Request!
|
||||
|
||||
### 开发流程
|
||||
|
||||
1. Fork 项目
|
||||
2. 创建特性分支 (`git checkout -b feature/amazing-feature`)
|
||||
3. 提交更改 (`git commit -m 'Add some amazing feature'`)
|
||||
4. 推送到分支 (`git push origin feature/amazing-feature`)
|
||||
5. 发起 Pull Request
|
||||
|
||||
### 代码规范
|
||||
|
||||
- 遵循 PEP 8 Python 编码规范
|
||||
- 使用类型注解 (Type Hints)
|
||||
- 编写单元测试
|
||||
- 添加必要的注释和文档
|
||||
|
||||
## 📝 更新日志
|
||||
|
||||
### v1.0.0 (2024-09-06)
|
||||
|
||||
- ✨ 初始版本发布
|
||||
- ✨ 完整的用户权限管理系统
|
||||
- ✨ 支持多数据库类型
|
||||
- ✨ 定时任务调度功能
|
||||
- ✨ 代码生成工具
|
||||
- ✨ AI 集成功能
|
||||
|
||||
## 📜 相关链接
|
||||
|
||||
- **FastAPI 官方文档**: [https://fastapi.tiangolo.com/](https://fastapi.tiangolo.com/)
|
||||
- **SQLAlchemy 文档**: [https://docs.sqlalchemy.org/](https://docs.sqlalchemy.org/)
|
||||
- **Pydantic 文档**: [https://pydantic-docs.helpmanual.io/](https://pydantic-docs.helpmanual.io/)
|
||||
|
||||
## 💬 支持与反馈
|
||||
|
||||
如果您在使用过程中遇到问题或有任何建议,请通过以下方式联系我们:
|
||||
|
||||
- 🐛 **Bug 报告**: 请在 GitHub Issues 中提交
|
||||
- 💡 **功能建议**: 请在 GitHub Discussions 中讨论
|
||||
- 💬 **技术交流**: 欢迎参与项目讨论
|
||||
|
||||
---
|
||||
|
||||
❤️ **感谢您的关注和支持!** 如果这个项目对您有帮助,请给我们一个 ⭐️ Star!
|
||||
|
||||
|
||||
# MCP 模块
|
||||
|
||||
## 概述
|
||||
|
||||
MCP (Model Context Protocol) 模块为系统提供与AI模型交互的能力,基于FastAPI-MCP实现。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 智能对话接口
|
||||
- 流式和非流式响应支持
|
||||
- WebSocket聊天支持
|
||||
- MCP服务器状态监控
|
||||
|
||||
## API接口
|
||||
|
||||
### 智能对话
|
||||
|
||||
```
|
||||
POST /api/v1/mcp/chat
|
||||
```
|
||||
|
||||
请求参数:
|
||||
- `message` (string, required): 聊天消息
|
||||
- `stream` (boolean, optional): 是否流式返回,默认为false
|
||||
|
||||
### 获取服务器状态
|
||||
|
||||
```
|
||||
GET /api/v1/mcp/status
|
||||
```
|
||||
|
||||
### WebSocket聊天
|
||||
|
||||
```
|
||||
GET /api/v1/mcp/ws/chat
|
||||
```
|
||||
|
||||
## MCP集成
|
||||
|
||||
系统已集成FastAPI-MCP,可通过以下URL访问MCP服务器:
|
||||
|
||||
```
|
||||
http://localhost:8000/mcp
|
||||
```
|
||||
|
||||
## 配置
|
||||
|
||||
在系统配置中设置以下参数:
|
||||
|
||||
- `QWEN_API_KEY`: Qwen API密钥
|
||||
- `QWEN_BASE_URL`: Qwen API基础URL
|
||||
- `QWEN_MODEL`: Qwen模型名称
|
||||
@@ -26,6 +26,8 @@ from .module_example.demo.controller import DemoRouter
|
||||
|
||||
from .module_application.myapp.controller import MyAppRouter
|
||||
|
||||
from .module_ai.mcp.controller import MCPRouter
|
||||
|
||||
from .module_resource.resource.controller import ResourceRouter
|
||||
|
||||
|
||||
@@ -58,6 +60,8 @@ EXAMPLE_MODULES = [{"router": DemoRouter}]
|
||||
|
||||
APPLICATION_MODULES = [{"router": MyAppRouter}]
|
||||
|
||||
AI_MODULES = [{"router": MCPRouter}]
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
for module in SYSTEM_MODULES:
|
||||
@@ -88,4 +92,9 @@ for module in APPLICATION_MODULES:
|
||||
for module in RESOURCE_MODULES:
|
||||
router.include_router(
|
||||
router=module["router"], prefix="/resource"
|
||||
)
|
||||
|
||||
for module in AI_MODULES:
|
||||
router.include_router(
|
||||
router=module["router"], prefix="/ai"
|
||||
)
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
AI模块初始化文件
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
AI模块初始化文件
|
||||
"""
|
||||
|
||||
from .controller import MCPRouter
|
||||
@@ -0,0 +1,50 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from fastapi import APIRouter, Depends, WebSocket
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
|
||||
from app.common.response import StreamResponse, SuccessResponse
|
||||
from app.core.dependencies import AuthPermission
|
||||
from app.core.router_class import OperationLogRoute
|
||||
from app.core.logger import logger
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from .service import MCPService
|
||||
from .schema import ChatQuerySchema
|
||||
|
||||
|
||||
MCPRouter = APIRouter(route_class=OperationLogRoute, prefix="/mcp", tags=["MCP智能助手"])
|
||||
|
||||
|
||||
@MCPRouter.post("/chat", summary="智能对话", description="与MCP智能助手进行对话")
|
||||
async def chat_controller(
|
||||
query: ChatQuerySchema,
|
||||
auth: AuthSchema = Depends(AuthPermission(permissions=["ai:mcp:chat"]))
|
||||
) -> StreamingResponse:
|
||||
"""智能对话接口"""
|
||||
logger.info(f"用户 {auth.user.name} 发起智能对话: {query.message[:50]}...")
|
||||
|
||||
async def generate_response():
|
||||
async for chunk in MCPService.chat_query(query.message):
|
||||
yield chunk
|
||||
|
||||
return StreamingResponse(generate_response(), media_type="text/plain")
|
||||
|
||||
|
||||
@MCPRouter.websocket("/ws/chat", name="WebSocket聊天")
|
||||
async def websocket_chat_controller(
|
||||
websocket: WebSocket,
|
||||
):
|
||||
"""WebSocket聊天接口
|
||||
|
||||
ws://127.0.0.1:8001/api/v1/ai/mcp/ws/chat
|
||||
"""
|
||||
await websocket.accept()
|
||||
try:
|
||||
while True:
|
||||
data = await websocket.receive_text()
|
||||
# 流式发送响应
|
||||
async for chunk in MCPService.chat_query(data):
|
||||
await websocket.send_text(chunk)
|
||||
except Exception as e:
|
||||
logger.error(f"WebSocket聊天出错: {str(e)}")
|
||||
await websocket.close()
|
||||
@@ -0,0 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class ChatQuerySchema(BaseModel):
|
||||
"""聊天查询模型"""
|
||||
message: str = Field(..., min_length=1, max_length=4000, description="聊天消息")
|
||||
@@ -0,0 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from app.utils.ai_util import AIClient
|
||||
|
||||
|
||||
class MCPService:
|
||||
"""MCP服务层 - 适配FastAPI-MCP"""
|
||||
|
||||
@classmethod
|
||||
async def chat_query(cls, message: str):
|
||||
"""处理聊天查询"""
|
||||
# 创建MCP客户端实例
|
||||
mcp_client = AIClient()
|
||||
# 处理消息
|
||||
async for response in mcp_client.process(message):
|
||||
yield response
|
||||
@@ -1,37 +0,0 @@
|
||||
from fastapi import FastAPI
|
||||
from starlette.websockets import WebSocket, WebSocketDisconnect
|
||||
|
||||
from mcp_server.mcp_client import MCPClient
|
||||
|
||||
|
||||
async def init_ai_websocket(app: FastAPI):
|
||||
|
||||
@app.websocket("/ws/chat")
|
||||
async def websocket_endpoint(websocket: WebSocket):
|
||||
await websocket.accept()
|
||||
user_id = id(websocket)
|
||||
user_contexts = {}
|
||||
user_contexts[user_id] = [{"role": "system", "content": "你是一个有帮助的助手。"}]
|
||||
|
||||
client = MCPClient()
|
||||
await client.connect_to_server('mcp_server/mcp_server.py')
|
||||
try:
|
||||
while True:
|
||||
user_msg = await websocket.receive_text()
|
||||
user_contexts[user_id].append({"role": "user", "content": user_msg})
|
||||
await websocket.send_json({"role": "user", "content": user_msg})
|
||||
|
||||
assistant_reply = ""
|
||||
response = client.put_query(user_msg)
|
||||
await websocket.send_json({"start": True})
|
||||
async for content_piece in response:
|
||||
assistant_reply += content_piece
|
||||
await websocket.send_json({"role": "assistant", "content": content_piece})
|
||||
|
||||
await websocket.send_json({"done": True})
|
||||
|
||||
except WebSocketDisconnect:
|
||||
print("WebSocket 断开连接")
|
||||
# 清理上下文
|
||||
user_contexts.pop(user_id, None)
|
||||
await client.cleanup()
|
||||
@@ -1,215 +0,0 @@
|
||||
import argparse
|
||||
import asyncio
|
||||
import os
|
||||
import json
|
||||
from typing import Optional
|
||||
from contextlib import AsyncExitStack
|
||||
|
||||
from click import argument
|
||||
from openai import AsyncOpenAI
|
||||
from dotenv import load_dotenv
|
||||
from mcp import ClientSession, StdioServerParameters
|
||||
from mcp.client.stdio import stdio_client
|
||||
|
||||
|
||||
|
||||
|
||||
class MCPClient:
|
||||
def __init__(self):
|
||||
"""初始化 MCP 客户端"""
|
||||
self.exit_stack = AsyncExitStack()
|
||||
self.openai_api_key = os.getenv("OPENAI_API_KEY") # 读取 OpenAI API Key
|
||||
self.base_url = os.getenv("OPENAI_API_URL") # 读取 BASE YRL
|
||||
self.model = os.getenv("OPENAI_API_MODEL") # 读取 model
|
||||
if not self.openai_api_key:
|
||||
raise ValueError("❌ 未找到 OpenAI API Key,请在 .env 文件中设置 OPENAI_API_KEY")
|
||||
self.client = AsyncOpenAI(api_key=self.openai_api_key, base_url=self.base_url) # 创建OpenAI client
|
||||
self.session: Optional[ClientSession] = None
|
||||
self.exit_stack = AsyncExitStack()
|
||||
self.messages = []
|
||||
|
||||
async def connect_to_server(self, server_script_path: str):
|
||||
"""连接到 MCP 服务器并列出可用工具"""
|
||||
is_python = server_script_path.endswith('.py')
|
||||
is_js = server_script_path.endswith('.js')
|
||||
if not (is_python or is_js):
|
||||
raise ValueError("服务器脚本必须是 .py 或 .js 文件")
|
||||
|
||||
# 必须设置项目根目录,否则无法获取到其他引用代码文件
|
||||
project_root = os.path.abspath(os.getcwd())
|
||||
python_cmd_path = os.getenv("PYTHON_PATH")
|
||||
command = python_cmd_path if is_python else "node"
|
||||
|
||||
parser = argparse.ArgumentParser(description='命令行参数')
|
||||
parser.add_argument('--env', type=str, default='', help='运行环境')
|
||||
args, unknown = parser.parse_known_args()
|
||||
|
||||
server_params = StdioServerParameters(
|
||||
command=command,
|
||||
args=[server_script_path, f'--env={args.env}'],
|
||||
env={"PYTHONPATH": project_root}
|
||||
)
|
||||
|
||||
# 启动 MCP 服务器并建立通信
|
||||
stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
|
||||
self.stdio, self.write = stdio_transport
|
||||
self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write))
|
||||
|
||||
await self.session.initialize()
|
||||
|
||||
# 列出 MCP 服务器上的工具
|
||||
response = await self.session.list_tools()
|
||||
tools = response.tools
|
||||
print("\n已连接到服务器,支持以下工具:", [tool.name for tool in tools])
|
||||
|
||||
|
||||
|
||||
async def process_query(self, query: str):
|
||||
"""
|
||||
使用大模型处理查询并调用可用的 MCP 工具 (Function Calling)
|
||||
"""
|
||||
self.messages.append({"role": "user", "content": query})
|
||||
|
||||
response = await self.session.list_tools()
|
||||
|
||||
available_tools = [{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tool.name,
|
||||
"description": tool.description,
|
||||
"input_schema": tool.inputSchema
|
||||
}
|
||||
} for tool in response.tools]
|
||||
# print(available_tools)
|
||||
|
||||
response = await self.client.chat.completions.create(
|
||||
model=self.model,
|
||||
messages=self.messages,
|
||||
stream=True,
|
||||
tools=available_tools
|
||||
)
|
||||
is_tool_call = False
|
||||
tool_name = None
|
||||
tool_args = ''
|
||||
tool_call_id = None
|
||||
content = ''
|
||||
yield f'🤖AI:'
|
||||
async for chunk in response:
|
||||
print(chunk)
|
||||
if chunk.choices and chunk.choices[0].delta.tool_calls:
|
||||
#调用工具
|
||||
tool_call = chunk.choices[0].delta.tool_calls[0]
|
||||
if tool_call.id:
|
||||
is_tool_call = True
|
||||
tool_name = tool_call.function.name
|
||||
tool_call_id = tool_call.id
|
||||
yield f'开始调用工具【{tool_call.function.name}】,参数为'
|
||||
if tool_call.function:
|
||||
tool_args += tool_call.function.arguments
|
||||
print(f'tool_args==={tool_args}')
|
||||
yield tool_call.function.arguments
|
||||
elif tool_call.function:
|
||||
tool_args += tool_call.function.arguments
|
||||
print(f'tool_args==={tool_args}')
|
||||
yield tool_call.function.arguments
|
||||
elif chunk.choices and chunk.choices[0].delta.content:
|
||||
# 大模型解答
|
||||
content += chunk.choices[0].delta.content
|
||||
yield chunk.choices[0].delta.content
|
||||
elif chunk.choices and chunk.choices[0].finish_reason == 'tool_calls':
|
||||
# 参数处理完毕
|
||||
pass
|
||||
elif chunk.choices and chunk.choices[0].finish_reason == 'stop':
|
||||
self.messages.append({
|
||||
"role": "assistant",
|
||||
"content": content
|
||||
})
|
||||
pass
|
||||
# 处理返回的内容
|
||||
if is_tool_call:
|
||||
# 如何是需要使用工具,就解析工具
|
||||
# 执行工具
|
||||
print(f"\n\n[Calling tool {tool_name} with args {tool_args}]\n\n")
|
||||
result = await self.session.call_tool(tool_name, json.loads(tool_args))
|
||||
print(result)
|
||||
# 将模型返回的调用哪个工具数据和工具执行完成后的数据都存入messages中
|
||||
self.messages.append({
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"index": 0,
|
||||
"tool_calls": [{
|
||||
"id": tool_call_id,
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tool_name,
|
||||
"arguments": tool_args
|
||||
}
|
||||
}]
|
||||
})
|
||||
self.messages.append({
|
||||
"role": "tool",
|
||||
"content": result.content[0].text,
|
||||
"tool_call_id": tool_call_id,
|
||||
})
|
||||
|
||||
# 将上面的结果再返回给大模型用于生产最终的结果
|
||||
result_response = await self.client.chat.completions.create(
|
||||
model=self.model,
|
||||
messages=self.messages,
|
||||
stream=True,
|
||||
)
|
||||
result_content = ''
|
||||
async for chunk in result_response:
|
||||
if chunk.choices and chunk.choices[0].delta.content:
|
||||
result_content += chunk.choices[0].delta.content
|
||||
yield chunk.choices[0].delta.content
|
||||
self.messages.append({
|
||||
"role": "assistant",
|
||||
'content': result_content,
|
||||
})
|
||||
return
|
||||
|
||||
async def put_query(self, query: str):
|
||||
print(f"\n🤖 OpenAI: ", end="", flush=True)
|
||||
response = self.process_query(query) # 发送用户输入到 OpenAI API
|
||||
async for value in response:
|
||||
print(value, end="", flush=True)
|
||||
yield value
|
||||
|
||||
async def chat_loop(self):
|
||||
"""运行交互式聊天循环"""
|
||||
print("\n🤖 MCP 客户端已启动!输入 'quit' 退出")
|
||||
|
||||
while True:
|
||||
try:
|
||||
query = input("\n你: ").strip()
|
||||
if query.lower() == 'quit':
|
||||
break
|
||||
|
||||
|
||||
print(f"\n🤖 OpenAI: ", end="", flush=True)
|
||||
response = self.process_query(query) # 发送用户输入到 OpenAI API
|
||||
async for value in response:
|
||||
print(value, end="", flush=True)
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n⚠️ 发生错误: {str(e)}")
|
||||
|
||||
async def cleanup(self):
|
||||
"""清理资源"""
|
||||
await self.exit_stack.aclose()
|
||||
|
||||
|
||||
async def main(server_script_path: str):
|
||||
|
||||
client = MCPClient()
|
||||
try:
|
||||
await client.connect_to_server(server_script_path)
|
||||
await client.chat_loop()
|
||||
finally:
|
||||
await client.cleanup()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
asyncio.run(main('mcp_server.py'))
|
||||
@@ -1,39 +0,0 @@
|
||||
|
||||
from typing import Literal
|
||||
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
from tool_table import TableTool
|
||||
from tool_weather import WeatherTool
|
||||
|
||||
# 初始化 MCP 服务器
|
||||
mcp = FastMCP("FluxMcpServer")
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def query_weather(city: str) -> str:
|
||||
"""
|
||||
输入指定城市的英文名称,返回今日天气查询结果。
|
||||
:param city: 城市名称(需使用英文)
|
||||
:return: 格式化后的天气信息
|
||||
"""
|
||||
data = await WeatherTool.fetch_weather(city)
|
||||
return WeatherTool.format_weather(data)
|
||||
|
||||
@mcp.tool()
|
||||
async def query_table(table_name: Literal["car_driver", "student_info"]) -> str:
|
||||
"""
|
||||
输入指定表名,获取表内的数据。
|
||||
Args:
|
||||
table_name: 表名选项:
|
||||
- car_driver: 司机信息
|
||||
- student_info: 学生信息表
|
||||
return: 数据表内容
|
||||
"""
|
||||
data = await TableTool.fetch_table_data(table_name)
|
||||
return data
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 以标准 I/O 方式运行 MCP 服务器
|
||||
mcp.run(transport='stdio')
|
||||
@@ -1,30 +0,0 @@
|
||||
import json
|
||||
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from sqlalchemy import select
|
||||
from config.database import Base
|
||||
from config.get_db import get_db
|
||||
import logging
|
||||
|
||||
from module_admin.entity.do.car_driver_do import CarDriver
|
||||
from module_admin.entity.do.student_info_do import StudentInfo
|
||||
|
||||
class TableTool:
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
# 因为mcp服务是在另外进程里面,需要导入模型,否则Base.registry.mappers是空的
|
||||
support_modules = [CarDriver, StudentInfo]
|
||||
|
||||
@classmethod
|
||||
async def fetch_table_data(cls, table_name: str) -> str:
|
||||
async for query_db in get_db():
|
||||
for mapper in Base.registry.mappers:
|
||||
table_cls = mapper.class_
|
||||
if hasattr(table_cls, '__tablename__') and table_cls.__tablename__ == table_name:
|
||||
result = await query_db.execute(select(table_cls))
|
||||
data = result.scalars().all()
|
||||
json_str = json.dumps(jsonable_encoder(data), ensure_ascii=False)
|
||||
return json_str
|
||||
raise ValueError(f"No model found for table name: {table_name},to check if you have imported it")
|
||||
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
|
||||
class WeatherTool:
|
||||
# OpenWeather API 配置
|
||||
OPENWEATHER_API_BASE = "https://api.openweathermap.org/data/2.5/weather"
|
||||
API_KEY = "146d600baa0f4f7a7687bdb573fb9138" # 请替换为你自己的 OpenWeather API Key
|
||||
USER_AGENT = "weather-app/1.0"
|
||||
|
||||
@classmethod
|
||||
async def fetch_weather(cls, city: str) -> dict[str, Any] | None:
|
||||
"""
|
||||
从 OpenWeather API 获取天气信息。
|
||||
:param city: 城市名称(需使用英文,如 Beijing)
|
||||
:return: 天气数据字典;若出错返回包含 error 信息的字典
|
||||
"""
|
||||
params = {
|
||||
"q": city,
|
||||
"appid": cls.API_KEY,
|
||||
"units": "metric",
|
||||
"lang": "zh_cn"
|
||||
}
|
||||
headers = {"User-Agent": cls.USER_AGENT}
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
response = await client.get(cls.OPENWEATHER_API_BASE, params=params, headers=headers, timeout=30.0)
|
||||
response.raise_for_status()
|
||||
return response.json() # 返回字典类型
|
||||
except httpx.HTTPStatusError as e:
|
||||
return {"error": f"HTTP 错误: {e.response.status_code}"}
|
||||
except Exception as e:
|
||||
return {"error": f"请求失败: {str(e)}"}
|
||||
|
||||
@classmethod
|
||||
def format_weather(cls, data: dict[str, Any] | str) -> str:
|
||||
"""
|
||||
将天气数据格式化为易读文本。
|
||||
:param data: 天气数据(可以是字典或 JSON 字符串)
|
||||
:return: 格式化后的天气信息字符串
|
||||
"""
|
||||
# 如果传入的是字符串,则先转换为字典
|
||||
if isinstance(data, str):
|
||||
try:
|
||||
data = json.loads(data)
|
||||
except Exception as e:
|
||||
return f"无法解析天气数据: {e}"
|
||||
|
||||
# 如果数据中包含错误信息,直接返回错误提示
|
||||
if "error" in data:
|
||||
return f"⚠️ {data['error']}"
|
||||
|
||||
# 提取数据时做容错处理
|
||||
city = data.get("name", "未知")
|
||||
country = data.get("sys", {}).get("country", "未知")
|
||||
temp = data.get("main", {}).get("temp", "N/A")
|
||||
humidity = data.get("main", {}).get("humidity", "N/A")
|
||||
wind_speed = data.get("wind", {}).get("speed", "N/A")
|
||||
# weather 可能为空列表,因此用 [0] 前先提供默认字典
|
||||
weather_list = data.get("weather", [{}])
|
||||
description = weather_list[0].get("description", "未知")
|
||||
|
||||
return (
|
||||
f"🌍 {city}, {country}\n"
|
||||
f"🌡 温度: {temp}°C\n"
|
||||
f"💧 湿度: {humidity}%\n"
|
||||
f"🌬 风速: {wind_speed} m/s\n"
|
||||
f"🌤 天气: {description}\n"
|
||||
)
|
||||
@@ -19,6 +19,8 @@ class ResourceType(Enum):
|
||||
|
||||
class ResourceItemSchema(BaseModel):
|
||||
"""资源项目模型"""
|
||||
model_config = ConfigDict(from_attributes=True, use_enum_values=True)
|
||||
|
||||
name: str = Field(..., description="文件名")
|
||||
path: str = Field(..., description="文件路径")
|
||||
relative_path: str = Field(..., description="相对路径")
|
||||
@@ -37,6 +39,8 @@ class ResourceItemSchema(BaseModel):
|
||||
|
||||
class ResourceDirectorySchema(BaseModel):
|
||||
"""资源目录模型"""
|
||||
model_config = ConfigDict(from_attributes=True, use_enum_values=True)
|
||||
|
||||
path: str = Field(..., description="目录路径")
|
||||
name: str = Field(..., description="目录名称")
|
||||
items: List[ResourceItemSchema] = Field(default_factory=list, description="目录项")
|
||||
@@ -47,6 +51,8 @@ class ResourceDirectorySchema(BaseModel):
|
||||
|
||||
class ResourceStatsSchema(BaseModel):
|
||||
"""资源统计模型"""
|
||||
model_config = ConfigDict(from_attributes=True, use_enum_values=True)
|
||||
|
||||
mount_point: str = Field(..., description="挂载点")
|
||||
total_files: int = Field(0, description="文件总数")
|
||||
total_dirs: int = Field(0, description="目录总数")
|
||||
@@ -60,6 +66,8 @@ class ResourceStatsSchema(BaseModel):
|
||||
|
||||
class ResourceSearchSchema(BaseModel):
|
||||
"""资源搜索模型"""
|
||||
model_config = ConfigDict(from_attributes=True, use_enum_values=True)
|
||||
|
||||
keyword: Optional[str] = Field(None, description="关键词")
|
||||
file_type: Optional[str] = Field(None, description="文件类型")
|
||||
resource_type: Optional[ResourceType] = Field(None, description="资源类型")
|
||||
@@ -74,6 +82,8 @@ class ResourceSearchSchema(BaseModel):
|
||||
|
||||
class ResourceUploadSchema(BaseModel):
|
||||
"""资源上传响应模型"""
|
||||
model_config = ConfigDict(from_attributes=True, use_enum_values=True)
|
||||
|
||||
filename: str = Field(..., description="文件名")
|
||||
file_path: str = Field(..., description="文件路径")
|
||||
file_url: str = Field(..., description="访问URL")
|
||||
|
||||
@@ -10,6 +10,8 @@ from pathlib import Path
|
||||
|
||||
from fastapi import UploadFile
|
||||
from PIL import Image
|
||||
|
||||
# 尝试导入 magic 库,如果失败则标记为不可用
|
||||
try:
|
||||
import magic
|
||||
MAGIC_AVAILABLE = True
|
||||
@@ -18,6 +20,10 @@ except ImportError:
|
||||
|
||||
from app.core.exceptions import CustomException
|
||||
from app.core.logger import logger
|
||||
|
||||
# 如果 magic 不可用,记录日志
|
||||
if not MAGIC_AVAILABLE:
|
||||
logger.info("没有找到 python-magic 库,将使用基于扩展名的文件类型检测")
|
||||
from app.utils.excel_util import ExcelUtil
|
||||
from app.config.setting import settings
|
||||
from ...module_system.auth.schema import AuthSchema
|
||||
@@ -38,32 +44,48 @@ from .schema import (
|
||||
|
||||
class ResourceService:
|
||||
"""
|
||||
资源管理模块服务层 - 直接操作文件系统
|
||||
资源管理模块服务层 - 管理系统静态文件目录
|
||||
"""
|
||||
|
||||
# 默认挂载点配置
|
||||
DEFAULT_MOUNT_POINT = getattr(settings, 'RESOURCE_MOUNT_POINT', '/Users/tao/workspace/fastapi_vue3_admin/backend/static/upload')
|
||||
ALLOWED_MOUNT_POINTS = getattr(settings, 'ALLOWED_MOUNT_POINTS', ['/Users/tao/workspace/fastapi_vue3_admin/backend/static', '/Users/tao/workspace/fastapi_vue3_admin/backend/static/upload'])
|
||||
# 配置常量
|
||||
MAX_UPLOAD_SIZE = 100 * 1024 * 1024 # 100MB
|
||||
MAX_SEARCH_RESULTS = 1000 # 最大搜索结果数
|
||||
MAX_PATH_DEPTH = 20 # 最大路径深度
|
||||
|
||||
@classmethod
|
||||
def _get_safe_path(cls, path: str) -> str:
|
||||
def _get_resource_root(cls) -> str:
|
||||
"""获取资源管理根目录"""
|
||||
if not settings.STATIC_ENABLE:
|
||||
raise CustomException(msg='静态文件服务未启用')
|
||||
return str(settings.STATIC_ROOT)
|
||||
|
||||
@classmethod
|
||||
def _get_safe_path(cls, path: str = None) -> str:
|
||||
"""获取安全的文件路径"""
|
||||
if not path:
|
||||
return cls.DEFAULT_MOUNT_POINT
|
||||
|
||||
# 规范化路径
|
||||
safe_path = os.path.normpath(os.path.abspath(path))
|
||||
resource_root = cls._get_resource_root()
|
||||
|
||||
# 检查路径是否在允许的挂载点内
|
||||
allowed = False
|
||||
for mount_point in cls.ALLOWED_MOUNT_POINTS:
|
||||
mount_abs = os.path.normpath(os.path.abspath(mount_point))
|
||||
if safe_path.startswith(mount_abs):
|
||||
allowed = True
|
||||
break
|
||||
|
||||
if not allowed:
|
||||
if not path:
|
||||
return resource_root
|
||||
|
||||
# 清理路径,移除危险字符
|
||||
path = path.strip().replace('..', '').replace('//', '/')
|
||||
|
||||
# 规范化路径
|
||||
if os.path.isabs(path):
|
||||
safe_path = os.path.normpath(path)
|
||||
else:
|
||||
safe_path = os.path.normpath(os.path.join(resource_root, path))
|
||||
|
||||
# 检查路径是否在允许的范围内
|
||||
resource_root_abs = os.path.normpath(os.path.abspath(resource_root))
|
||||
safe_path_abs = os.path.normpath(os.path.abspath(safe_path))
|
||||
|
||||
if not safe_path_abs.startswith(resource_root_abs):
|
||||
raise CustomException(msg=f'访问路径不在允许范围内: {path}')
|
||||
|
||||
# 防止路径遍历攻击
|
||||
if '..' in safe_path or safe_path.count('/') > cls.MAX_PATH_DEPTH: # 限制最大目录深度
|
||||
raise CustomException(msg=f'不安全的路径格式: {path}')
|
||||
|
||||
return safe_path
|
||||
|
||||
@@ -86,16 +108,45 @@ class ResourceService:
|
||||
|
||||
stat = os.stat(safe_path)
|
||||
path_obj = Path(safe_path)
|
||||
resource_root = cls._get_resource_root()
|
||||
|
||||
# 获取文件扩展名和类型
|
||||
file_extension = path_obj.suffix.lower() if path_obj.suffix else None
|
||||
file_type = cls._get_mime_type_from_extension(file_extension) if file_extension else None
|
||||
|
||||
# 优先使用 magic 库检测 MIME 类型
|
||||
file_type = None
|
||||
if MAGIC_AVAILABLE and os.path.isfile(safe_path):
|
||||
try:
|
||||
file_type = magic.from_file(safe_path, mime=True)
|
||||
except Exception as e:
|
||||
logger.debug(f"magic 库检测文件类型失败: {e}")
|
||||
|
||||
# 如果 magic 检测失败或不可用,使用扩展名检测
|
||||
if not file_type and file_extension:
|
||||
file_type = cls._get_mime_type_from_extension(file_extension)
|
||||
|
||||
# 如果仍然没有类型,使用默认值
|
||||
if not file_type:
|
||||
file_type = 'application/octet-stream' if os.path.isfile(safe_path) else None
|
||||
|
||||
resource_type = cls._determine_resource_type(file_type, file_extension)
|
||||
|
||||
# 计算相对路径
|
||||
try:
|
||||
relative_path = os.path.relpath(safe_path, resource_root)
|
||||
except ValueError:
|
||||
relative_path = os.path.basename(safe_path)
|
||||
|
||||
# 计算深度
|
||||
try:
|
||||
depth = len(Path(safe_path).relative_to(resource_root).parts)
|
||||
except ValueError:
|
||||
depth = 0
|
||||
|
||||
return {
|
||||
'name': path_obj.name,
|
||||
'path': safe_path,
|
||||
'relative_path': os.path.relpath(safe_path, cls.DEFAULT_MOUNT_POINT),
|
||||
'relative_path': relative_path,
|
||||
'is_file': os.path.isfile(safe_path),
|
||||
'is_dir': os.path.isdir(safe_path),
|
||||
'size': stat.st_size if os.path.isfile(safe_path) else None,
|
||||
@@ -106,7 +157,7 @@ class ResourceService:
|
||||
'modified_time': datetime.fromtimestamp(stat.st_mtime),
|
||||
'accessed_time': datetime.fromtimestamp(stat.st_atime),
|
||||
'parent_path': str(path_obj.parent),
|
||||
'depth': len(path_obj.relative_to(cls.DEFAULT_MOUNT_POINT).parts) if cls.DEFAULT_MOUNT_POINT in safe_path else 0
|
||||
'depth': depth
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f'获取文件信息失败: {str(e)}')
|
||||
@@ -122,8 +173,11 @@ class ResourceService:
|
||||
) -> Dict:
|
||||
"""获取目录列表"""
|
||||
try:
|
||||
target_path = path or cls.DEFAULT_MOUNT_POINT
|
||||
safe_path = cls._get_safe_path(target_path)
|
||||
# 如果没有指定路径,使用静态文件根目录
|
||||
if path is None:
|
||||
safe_path = cls._get_resource_root()
|
||||
else:
|
||||
safe_path = cls._get_safe_path(path)
|
||||
|
||||
if not os.path.exists(safe_path):
|
||||
raise CustomException(msg='目录不存在')
|
||||
@@ -171,7 +225,7 @@ class ResourceService:
|
||||
total_files=total_files,
|
||||
total_dirs=total_dirs,
|
||||
total_size=total_size
|
||||
).model_dump()
|
||||
).model_dump(mode='json')
|
||||
|
||||
except CustomException:
|
||||
raise
|
||||
@@ -214,27 +268,52 @@ class ResourceService:
|
||||
) -> List[Dict]:
|
||||
"""搜索资源"""
|
||||
try:
|
||||
mount_point = cls.DEFAULT_MOUNT_POINT
|
||||
# 使用静态文件根目录作为搜索起点
|
||||
search_root = cls._get_resource_root()
|
||||
results = []
|
||||
|
||||
for root, dirs, files in os.walk(mount_point):
|
||||
for root, dirs, files in os.walk(search_root):
|
||||
# 控制搜索深度
|
||||
depth = len(Path(root).relative_to(mount_point).parts)
|
||||
try:
|
||||
depth = len(Path(root).relative_to(search_root).parts)
|
||||
except ValueError:
|
||||
depth = 0
|
||||
|
||||
if depth > search.max_depth:
|
||||
dirs.clear() # 阻止进一步深入
|
||||
continue
|
||||
|
||||
# 过滤隐藏文件夹
|
||||
# 过滤隐藏文件夹(性能优化)
|
||||
if not search.include_hidden:
|
||||
dirs[:] = [d for d in dirs if not d.startswith('.')]
|
||||
files = [f for f in files if not f.startswith('.')]
|
||||
|
||||
# 优化:先过滤文件名,再进行详细检查
|
||||
if search.keyword:
|
||||
files = [f for f in files if search.keyword.lower() in f.lower()]
|
||||
|
||||
# 搜索文件
|
||||
for file in files:
|
||||
file_path = os.path.join(root, file)
|
||||
|
||||
# 优化:先进行快速检查
|
||||
if search.extensions:
|
||||
file_ext = os.path.splitext(file)[1].lower()
|
||||
if file_ext not in search.extensions:
|
||||
continue
|
||||
|
||||
file_info = cls._get_file_info(file_path)
|
||||
|
||||
if cls._match_search_criteria(file_info, search):
|
||||
results.append(file_info)
|
||||
|
||||
# 限制结果数量防止内存溢出
|
||||
if len(results) >= cls.MAX_SEARCH_RESULTS:
|
||||
logger.warning(f"搜索结果过多,已截断到前{cls.MAX_SEARCH_RESULTS}个")
|
||||
break
|
||||
|
||||
if len(results) >= cls.MAX_SEARCH_RESULTS:
|
||||
break
|
||||
|
||||
# 排序结果
|
||||
return cls._sort_results(results, search)
|
||||
@@ -314,10 +393,21 @@ class ResourceService:
|
||||
if not file or not file.filename:
|
||||
raise CustomException(msg="请选择要上传的文件")
|
||||
|
||||
# 文件名安全检查
|
||||
if '..' in file.filename or '/' in file.filename or '\\' in file.filename:
|
||||
raise CustomException(msg="文件名包含不安全字符")
|
||||
|
||||
try:
|
||||
# 确定上传目录
|
||||
upload_dir = target_path or cls.DEFAULT_MOUNT_POINT
|
||||
safe_dir = cls._get_safe_path(upload_dir)
|
||||
# 检查文件大小
|
||||
content = await file.read()
|
||||
if len(content) > cls.MAX_UPLOAD_SIZE:
|
||||
raise CustomException(msg=f"文件太大,最大支持{cls.MAX_UPLOAD_SIZE // (1024*1024)}MB")
|
||||
|
||||
# 确定上传目录,如果没有指定目标路径,使用静态文件根目录
|
||||
if target_path is None:
|
||||
safe_dir = cls._get_resource_root()
|
||||
else:
|
||||
safe_dir = cls._get_safe_path(target_path)
|
||||
|
||||
# 创建目录(如果不存在)
|
||||
os.makedirs(safe_dir, exist_ok=True)
|
||||
@@ -337,24 +427,34 @@ class ResourceService:
|
||||
counter += 1
|
||||
filename = os.path.basename(file_path)
|
||||
|
||||
# 保存文件
|
||||
content = await file.read()
|
||||
# 保存文件(使用已读取的内容)
|
||||
with open(file_path, 'wb') as f:
|
||||
f.write(content)
|
||||
|
||||
# 获取文件信息
|
||||
file_info = cls._get_file_info(file_path)
|
||||
|
||||
# 生成相对于资源根目录的URL路径
|
||||
resource_root = cls._get_resource_root()
|
||||
try:
|
||||
relative_path = os.path.relpath(file_path, resource_root)
|
||||
# 确保路径使用正斜杠(URL格式)
|
||||
file_url_path = relative_path.replace(os.sep, '/')
|
||||
file_url = f"/resource/download?path={file_url_path}"
|
||||
except ValueError:
|
||||
# 如果无法计算相对路径,使用文件名
|
||||
file_url = f"/resource/download?path={filename}"
|
||||
|
||||
logger.info(f"文件上传成功: {filename}")
|
||||
|
||||
return ResourceUploadSchema(
|
||||
filename=filename,
|
||||
file_path=file_path,
|
||||
file_url=f"/resource/download?path={file_path}",
|
||||
file_url=file_url,
|
||||
file_size=file_info.get('size', 0),
|
||||
resource_type=file_info.get('resource_type', ResourceType.OTHER),
|
||||
upload_time=datetime.now()
|
||||
).model_dump()
|
||||
).model_dump(mode='json')
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"文件上传失败: {str(e)}")
|
||||
@@ -531,10 +631,11 @@ class ResourceService:
|
||||
async def get_stats_service(cls, auth: AuthSchema) -> Dict:
|
||||
"""获取资源统计信息"""
|
||||
try:
|
||||
mount_point = cls.DEFAULT_MOUNT_POINT
|
||||
# 使用静态文件根目录
|
||||
stats_root = cls._get_resource_root()
|
||||
|
||||
# 获取磁盘空间信息
|
||||
statvfs = os.statvfs(mount_point)
|
||||
statvfs = os.statvfs(stats_root)
|
||||
total_space = statvfs.f_frsize * statvfs.f_blocks
|
||||
free_space = statvfs.f_frsize * statvfs.f_bavail
|
||||
used_space = total_space - free_space
|
||||
@@ -546,7 +647,7 @@ class ResourceService:
|
||||
type_stats = {}
|
||||
extension_stats = {}
|
||||
|
||||
for root, dirs, files in os.walk(mount_point):
|
||||
for root, dirs, files in os.walk(stats_root):
|
||||
total_dirs += len(dirs)
|
||||
|
||||
for file in files:
|
||||
@@ -572,7 +673,7 @@ class ResourceService:
|
||||
continue
|
||||
|
||||
return ResourceStatsSchema(
|
||||
mount_point=mount_point,
|
||||
mount_point=stats_root,
|
||||
total_files=total_files,
|
||||
total_dirs=total_dirs,
|
||||
total_size=total_size,
|
||||
@@ -581,7 +682,7 @@ class ResourceService:
|
||||
total_space=total_space,
|
||||
type_stats=type_stats,
|
||||
extension_stats=extension_stats
|
||||
).model_dump()
|
||||
).model_dump(mode='json')
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取统计信息失败: {str(e)}")
|
||||
@@ -658,36 +759,39 @@ class ResourceService:
|
||||
if not file_extension:
|
||||
return 'application/octet-stream'
|
||||
|
||||
# 扩展更全面的MIME类型映射
|
||||
mime_types = {
|
||||
'.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.png': 'image/png',
|
||||
'.gif': 'image/gif',
|
||||
'.bmp': 'image/bmp',
|
||||
'.webp': 'image/webp',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.mp4': 'video/mp4',
|
||||
'.avi': 'video/x-msvideo',
|
||||
'.mov': 'video/quicktime',
|
||||
'.wmv': 'video/x-ms-wmv',
|
||||
'.flv': 'video/x-flv',
|
||||
'.mp3': 'audio/mpeg',
|
||||
'.wav': 'audio/wav',
|
||||
'.aac': 'audio/aac',
|
||||
'.ogg': 'audio/ogg',
|
||||
'.pdf': 'application/pdf',
|
||||
'.doc': 'application/msword',
|
||||
# 图片类型
|
||||
'.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png',
|
||||
'.gif': 'image/gif', '.bmp': 'image/bmp', '.webp': 'image/webp',
|
||||
'.svg': 'image/svg+xml', '.ico': 'image/x-icon', '.tiff': 'image/tiff',
|
||||
|
||||
# 视频类型
|
||||
'.mp4': 'video/mp4', '.avi': 'video/x-msvideo', '.mov': 'video/quicktime',
|
||||
'.wmv': 'video/x-ms-wmv', '.flv': 'video/x-flv', '.webm': 'video/webm',
|
||||
'.mkv': 'video/x-matroska', '.m4v': 'video/x-m4v',
|
||||
|
||||
# 音频类型
|
||||
'.mp3': 'audio/mpeg', '.wav': 'audio/wav', '.aac': 'audio/aac',
|
||||
'.ogg': 'audio/ogg', '.flac': 'audio/flac', '.m4a': 'audio/mp4',
|
||||
|
||||
# 文档类型
|
||||
'.pdf': 'application/pdf', '.doc': 'application/msword',
|
||||
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'.xls': 'application/vnd.ms-excel',
|
||||
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'.ppt': 'application/vnd.ms-powerpoint',
|
||||
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
'.txt': 'text/plain',
|
||||
'.csv': 'text/csv',
|
||||
'.zip': 'application/zip',
|
||||
'.rar': 'application/x-rar-compressed',
|
||||
'.7z': 'application/x-7z-compressed',
|
||||
'.tar': 'application/x-tar',
|
||||
'.gz': 'application/gzip'
|
||||
'.txt': 'text/plain', '.csv': 'text/csv', '.rtf': 'application/rtf',
|
||||
|
||||
# 代码文件
|
||||
'.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript',
|
||||
'.json': 'application/json', '.xml': 'application/xml',
|
||||
'.py': 'text/x-python', '.java': 'text/x-java-source',
|
||||
|
||||
# 压缩文件
|
||||
'.zip': 'application/zip', '.rar': 'application/x-rar-compressed',
|
||||
'.7z': 'application/x-7z-compressed', '.tar': 'application/x-tar',
|
||||
'.gz': 'application/gzip', '.bz2': 'application/x-bzip2'
|
||||
}
|
||||
return mime_types.get(file_extension.lower(), 'application/octet-stream')
|
||||
@@ -260,6 +260,14 @@ class Settings(BaseSettings):
|
||||
# if not os.path.exists(self.GEN_PATH):
|
||||
# os.makedirs(self.GEN_PATH)
|
||||
|
||||
# ================================================= #
|
||||
# ******************* AI大模型配置 ****************** #
|
||||
# ================================================= #
|
||||
# https://bailian.console.aliyun.com/?spm=5176.29619931.J_AHgvE-XDhTWrtotIBlDQQ.13.74cd521clrmQ7o&tab=api#/api/?type=model&url=https%3A%2F%2Fhelp.aliyun.com%2Fdocument_detail%2F2712576.html&renderType=iframe
|
||||
QWEN_BASE_URL: str
|
||||
QWEN_API_KEY: str
|
||||
QWEN_MODEL: str
|
||||
|
||||
# ================================================= #
|
||||
# ******************* 其他配置 ******************* #
|
||||
# ================================================= #
|
||||
|
||||
@@ -24,7 +24,7 @@ from apscheduler.triggers.interval import IntervalTrigger
|
||||
from pymongo import MongoClient
|
||||
|
||||
from app.config.setting import settings
|
||||
from app.core.database import engine, session_connect, SessionLocal, async_session
|
||||
from app.core.database import engine, session_connect, SessionLocal, AsyncSessionLocal
|
||||
from app.core.exceptions import CustomException
|
||||
from app.core.logger import logger
|
||||
|
||||
@@ -41,7 +41,10 @@ job_stores = {
|
||||
)),
|
||||
}
|
||||
# 配置执行器
|
||||
executors = {'default': AsyncIOExecutor(), 'processpool': ProcessPoolExecutor(5)}
|
||||
executors = {
|
||||
'default': AsyncIOExecutor(),
|
||||
'processpool': ProcessPoolExecutor(max_workers=1) # 减少进程数量以减少资源消耗
|
||||
}
|
||||
# 配置默认参数
|
||||
job_defaults = {
|
||||
'coalesce': False, # 是否合并执行
|
||||
@@ -110,7 +113,7 @@ class SchedulerUtil:
|
||||
exception_info=exception_info,
|
||||
create_time=datetime.now(),
|
||||
)
|
||||
session = async_session()
|
||||
session = SessionLocal()
|
||||
JobLogCRUD(AuthSchema(db=session)).create_obj_log_crud(data=job_log)
|
||||
session.close()
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from redis import asyncio as aioredis
|
||||
from motor.motor_asyncio import AsyncIOMotorClient
|
||||
from fastapi import FastAPI
|
||||
from sqlalchemy import create_engine, inspect, text
|
||||
from sqlalchemy import create_engine, text
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy.ext.asyncio import (
|
||||
create_async_engine,
|
||||
@@ -37,26 +37,21 @@ engine = create_engine(
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
# 异步数据库引擎
|
||||
def async_db_engine() -> AsyncEngine:
|
||||
"""创建异步数据库引擎"""
|
||||
# 创建数据库引擎
|
||||
async_engine = create_async_engine(
|
||||
url=settings.ASYNC_DB_URI,
|
||||
echo=settings.DATABASE_ECHO,
|
||||
echo_pool=settings.ECHO_POOL,
|
||||
pool_pre_ping=settings.POOL_PRE_PING,
|
||||
future=settings.FUTURE,
|
||||
pool_recycle=settings.POOL_RECYCLE,
|
||||
# pool_size=settings.POOL_SIZE, # sqlite 不支持
|
||||
# max_overflow=settings.MAX_OVERFLOW, # sqlite 不支持
|
||||
# pool_timeout=settings.POOL_TIMEOUT, # sqlite 不支持
|
||||
)
|
||||
async_engine: AsyncEngine = create_async_engine(
|
||||
url=settings.ASYNC_DB_URI,
|
||||
echo=settings.DATABASE_ECHO,
|
||||
echo_pool=settings.ECHO_POOL,
|
||||
pool_pre_ping=settings.POOL_PRE_PING,
|
||||
future=settings.FUTURE,
|
||||
pool_recycle=settings.POOL_RECYCLE,
|
||||
# pool_size=settings.POOL_SIZE, # sqlite 不支持
|
||||
# max_overflow=settings.MAX_OVERFLOW, # sqlite 不支持
|
||||
# pool_timeout=settings.POOL_TIMEOUT, # sqlite 不支持
|
||||
)
|
||||
|
||||
return async_engine
|
||||
|
||||
# 异步数据库会话工厂
|
||||
async_session = async_sessionmaker(
|
||||
bind=async_db_engine(),
|
||||
AsyncSessionLocal = async_sessionmaker(
|
||||
bind=async_engine,
|
||||
autocommit=settings.AUTOCOMMIT,
|
||||
autoflush=settings.AUTOFETCH,
|
||||
expire_on_commit=settings.EXPIRE_ON_COMMIT,
|
||||
@@ -68,7 +63,7 @@ def session_connect() -> AsyncSession:
|
||||
try:
|
||||
if not settings.SQL_DB_ENABLE:
|
||||
raise CustomException(msg="请先开启数据库连接", data="请启用 app/config/setting.py: SQL_DB_ENABLE")
|
||||
return async_session()
|
||||
return AsyncSessionLocal()
|
||||
except Exception as e:
|
||||
raise CustomException(msg=f"数据库连接失败: {e}")
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import asyncio
|
||||
from typing import Any, AsyncGenerator
|
||||
from fastapi_mcp import FastApiMCP
|
||||
from fastapi import FastAPI
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.concurrency import asynccontextmanager
|
||||
@@ -10,7 +10,6 @@ from fastapi.openapi.docs import (
|
||||
get_swagger_ui_html,
|
||||
get_swagger_ui_oauth2_redirect_html
|
||||
)
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.config.setting import settings
|
||||
from app.core.ap_scheduler import SchedulerUtil
|
||||
@@ -103,6 +102,20 @@ def register_routers(app: FastAPI) -> None:
|
||||
"""
|
||||
app.include_router(router=router)
|
||||
|
||||
def register_fastapi_mcp(app: FastAPI) -> None:
|
||||
"""
|
||||
注册FastAPI-MCP路由
|
||||
"""
|
||||
mcp = FastApiMCP(
|
||||
app,
|
||||
name="FastAPI Vue3 Admin MCP",
|
||||
description="MCP server for the FastAPI Vue3 Admin system",
|
||||
describe_full_response_schema=True,
|
||||
describe_all_responses=True,
|
||||
)
|
||||
mcp.mount()
|
||||
# mcp.mount_http()
|
||||
|
||||
def register_files(app: FastAPI) -> None:
|
||||
"""
|
||||
注册文件相关配置
|
||||
@@ -140,4 +153,4 @@ def reset_api_docs(app: FastAPI) -> None:
|
||||
title=app.title + " - ReDoc",
|
||||
redoc_js_url=settings.REDOC_JS_URL,
|
||||
redoc_favicon_url=settings.FAVICON_URL,
|
||||
)
|
||||
)
|
||||
@@ -2118,5 +2118,68 @@
|
||||
"affix": false,
|
||||
"redirect": null,
|
||||
"description": "初始化数据"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 102,
|
||||
"name": "AI大模型",
|
||||
"type": 1,
|
||||
"icon": "el-icon-DataLine",
|
||||
"order": 8,
|
||||
"permission": null,
|
||||
"route_name": "AI",
|
||||
"route_path": "/ai",
|
||||
"component_path": null,
|
||||
"parent_id": null,
|
||||
"status": true,
|
||||
"keep_alive": false,
|
||||
"hidden": false,
|
||||
"always_show": false,
|
||||
"title": "AI大模型",
|
||||
"params": null,
|
||||
"affix": false,
|
||||
"redirect": "/ai/mcp",
|
||||
"description": "AI大模型管理"
|
||||
},
|
||||
{
|
||||
"id": 103,
|
||||
"name": "MCP智能助手",
|
||||
"type": 2,
|
||||
"icon": "el-icon-DataLine",
|
||||
"order": 1,
|
||||
"permission": "ai:mcp:chat",
|
||||
"route_name": "MCP",
|
||||
"route_path": "/ai/mcp",
|
||||
"component_path": "ai/mcp/index",
|
||||
"parent_id": 102,
|
||||
"status": true,
|
||||
"keep_alive": true,
|
||||
"hidden": false,
|
||||
"always_show": false,
|
||||
"title": "MCP智能助手",
|
||||
"params": null,
|
||||
"affix": false,
|
||||
"redirect": null,
|
||||
"description": "MCP智能助手"
|
||||
},
|
||||
{
|
||||
"id": 104,
|
||||
"name": "智能对话",
|
||||
"type": 3,
|
||||
"icon": null,
|
||||
"order": 1,
|
||||
"permission": "ai:mcp:chat",
|
||||
"route_name": null,
|
||||
"route_path": null,
|
||||
"component_path": null,
|
||||
"parent_id": 103,
|
||||
"status": true,
|
||||
"keep_alive": true,
|
||||
"hidden": false,
|
||||
"always_show": false,
|
||||
"title": "智能对话",
|
||||
"params": null,
|
||||
"affix": false,
|
||||
"redirect": null,
|
||||
"description": "智能对话"
|
||||
}
|
||||
]
|
||||
@@ -403,6 +403,18 @@
|
||||
"role_id": 1,
|
||||
"menu_id": 101
|
||||
},
|
||||
{
|
||||
"role_id": 1,
|
||||
"menu_id": 102
|
||||
},
|
||||
{
|
||||
"role_id": 1,
|
||||
"menu_id": 103
|
||||
},
|
||||
{
|
||||
"role_id": 1,
|
||||
"menu_id": 104
|
||||
},
|
||||
{
|
||||
"role_id": 2,
|
||||
"menu_id": 1
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from openai import AsyncOpenAI, OpenAI
|
||||
from openai.types.chat.chat_completion import ChatCompletion
|
||||
|
||||
from app.config.setting import settings
|
||||
from app.core.logger import logger
|
||||
|
||||
|
||||
class AIClient:
|
||||
|
||||
def __init__(self):
|
||||
self.model = settings.QWEN_MODEL
|
||||
# 使用默认的http客户端,避免资源管理问题
|
||||
self.client = AsyncOpenAI(
|
||||
api_key=settings.QWEN_API_KEY,
|
||||
base_url=settings.QWEN_BASE_URL,
|
||||
)
|
||||
|
||||
async def process(self, query: str):
|
||||
"""处理查询并返回流式响应"""
|
||||
system_prompt = """你是一个有用的AI助手,可以帮助用户回答问题和提供帮助。请用中文回答用户的问题。"""
|
||||
|
||||
try:
|
||||
# 使用 await 调用异步客户端
|
||||
response = await self.client.chat.completions.create(
|
||||
model=self.model,
|
||||
messages=[
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": query}
|
||||
],
|
||||
stream=True
|
||||
)
|
||||
|
||||
# 流式返回响应
|
||||
async for chunk in response:
|
||||
if chunk.choices[0].delta.content is not None:
|
||||
yield chunk.choices[0].delta.content
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"AI处理查询失败: {str(e)}")
|
||||
yield f"抱歉,处理您的请求时出现了错误: {str(e)}"
|
||||
@@ -86,7 +86,7 @@ class IpLocalUtil:
|
||||
|
||||
try:
|
||||
# 使用ip-api.com API获取IP归属地信息
|
||||
async with httpx.AsyncClient() as client:
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
# 尝试使用 ip9.com.cn API
|
||||
url = f'https://ip9.com.cn/get?ip={ip}'
|
||||
response = await cls._make_api_request(client, url)
|
||||
|
||||
Vendored
+5
@@ -65,3 +65,8 @@ MONGO_DB_NAME = "admin"
|
||||
|
||||
# 日志配置
|
||||
LOGGER_LEVEL = 'DEBUG' # 日志级别
|
||||
|
||||
# https://bailian.console.aliyun.com/?spm=5176.29619931.J_AHgvE-XDhTWrtotIBlDQQ.13.74cd521clrmQ7o&tab=api#/api/?type=model&url=https%3A%2F%2Fhelp.aliyun.com%2Fdocument_detail%2F2712576.html&renderType=iframe
|
||||
QWEN_BASE_URL = https://dashscope.aliyuncs.com/compatible-mode/v1
|
||||
QWEN_API_KEY = sk-e688534f2d984e7fa2eb46add409422f
|
||||
QWEN_MODEL = qwen-plus
|
||||
Vendored
+6
@@ -59,3 +59,9 @@ MONGO_DB_NAME = "fastapiadmin"
|
||||
|
||||
# 日志配置
|
||||
LOGGER_LEVEL = 'INFO' # 日志级别
|
||||
|
||||
|
||||
# https://bailian.console.aliyun.com/?spm=5176.29619931.J_AHgvE-XDhTWrtotIBlDQQ.13.74cd521clrmQ7o&tab=api#/api/?type=model&url=https%3A%2F%2Fhelp.aliyun.com%2Fdocument_detail%2F2712576.html&renderType=iframe
|
||||
QWEN_BASE_URL = https://dashscope.aliyuncs.com/compatible-mode/v1
|
||||
QWEN_API_KEY = sk-e688534f2d984e7fa2eb46add409422f
|
||||
QWEN_MODEL = qwen-plus
|
||||
@@ -21,6 +21,7 @@ def create_app() -> FastAPI:
|
||||
register_middlewares,
|
||||
register_exceptions,
|
||||
register_routers,
|
||||
register_fastapi_mcp,
|
||||
register_files,
|
||||
reset_api_docs,
|
||||
lifespan
|
||||
@@ -38,6 +39,8 @@ def create_app() -> FastAPI:
|
||||
register_files(app)
|
||||
# 重设API文档
|
||||
reset_api_docs(app)
|
||||
# 注册FastAPI-MCP
|
||||
register_fastapi_mcp(app)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
alembic==1.15.1 # 数据库迁移
|
||||
APScheduler==3.11.0 # 定时任务
|
||||
fastapi==0.115.2
|
||||
fastapi-mcp==0.4.0
|
||||
typer==0.7.0
|
||||
click==8.1.7
|
||||
uvicorn==0.30.6 # uvicorn web 框架
|
||||
@@ -23,8 +24,7 @@ python-multipart==0.0.9 # request.form() 对表单进行「解析」时安
|
||||
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+的版本
|
||||
redis==5.2.1 # redis 同步操作数据库(用户celery配套使用)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 驱动
|
||||
@@ -34,3 +34,4 @@ PyMySQL==1.1.2 # mysql 异步操作数据库基于 pymysql:aiomys
|
||||
cryptography==45.0.2 # mysql8 密码加密
|
||||
openai==1.55.2 # ai 大模型
|
||||
oss2==2.18.4 # 阿里云对象存储
|
||||
python-magic==0.4.27 # 文件类型识别
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,8 @@ arrowParens: "always"
|
||||
bracketSameLine: false
|
||||
# 对象字面量中的括号之间添加空格
|
||||
bracketSpacing: true
|
||||
# 在对象字面量中,如果属性名需要引号,则使用单引号
|
||||
singleAttributePerLine: false
|
||||
# 自动格式化嵌入的代码(如 Markdown 和 HTML 内的代码)
|
||||
embeddedLanguageFormatting: "auto"
|
||||
# 忽略 HTML 空白敏感度,将空白视为非重要内容
|
||||
@@ -12,8 +14,8 @@ htmlWhitespaceSensitivity: "ignore"
|
||||
insertPragma: false
|
||||
# 在 JSX 中使用双引号
|
||||
jsxSingleQuote: false
|
||||
# 每行代码的最大长度限制为 100 字符
|
||||
printWidth: 100
|
||||
# 每行代码的最大长度限制为 200 字符
|
||||
printWidth: 200
|
||||
# 在 Markdown 中保留原有的换行格式
|
||||
proseWrap: "preserve"
|
||||
# 仅在必要时添加对象属性的引号
|
||||
@@ -34,8 +36,15 @@ useTabs: false
|
||||
vueIndentScriptAndStyle: false
|
||||
# 根据系统自动检测换行符
|
||||
endOfLine: "auto"
|
||||
# 对 HTML 文件应用特定格式化规则
|
||||
# 对特定文件类型应用更宽松的格式化规则
|
||||
overrides:
|
||||
- files: "*.html"
|
||||
options:
|
||||
parser: "html"
|
||||
- files: "*.vue"
|
||||
options:
|
||||
printWidth: 300
|
||||
singleAttributePerLine: false
|
||||
- files: "*.{js,ts}"
|
||||
options:
|
||||
printWidth: 250
|
||||
|
||||
Vendored
+11
@@ -17,19 +17,27 @@ 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']
|
||||
WdAvatar: typeof import('wot-design-uni/components/wd-avatar/wd-avatar.vue')['default']
|
||||
WdBackTop: typeof import('wot-design-uni/components/wd-back-top/wd-back-top.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']
|
||||
WdCol: typeof import('wot-design-uni/components/wd-col/wd-col.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']
|
||||
WdFab: typeof import('wot-design-uni/components/wd-fab/wd-fab.vue')['default']
|
||||
WdFloatingPanel: typeof import('wot-design-uni/components/wd-floating-panel/wd-floating-panel.vue')['default']
|
||||
WdForm: typeof import('wot-design-uni/components/wd-form/wd-form.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']
|
||||
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']
|
||||
WdLoading: typeof import('wot-design-uni/components/wd-loading/wd-loading.vue')['default']
|
||||
WdMessageBox: typeof import('wot-design-uni/components/wd-message-box/wd-message-box.vue')['default']
|
||||
@@ -38,6 +46,9 @@ declare module 'vue' {
|
||||
WdPopup: typeof import('wot-design-uni/components/wd-popup/wd-popup.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']
|
||||
WdRow: typeof import('wot-design-uni/components/wd-row/wd-row.vue')['default']
|
||||
WdSearch: typeof import('wot-design-uni/components/wd-search/wd-search.vue')['default']
|
||||
WdSkeleton: typeof import('wot-design-uni/components/wd-skeleton/wd-skeleton.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']
|
||||
|
||||
@@ -1,301 +0,0 @@
|
||||
# 主题系统使用指南
|
||||
|
||||
## 概述
|
||||
|
||||
本项目基于 Wot Design Uni 的 ConfigProvider 组件实现了完整的主题系统,支持:
|
||||
|
||||
- 🌙 暗黑/浅色模式切换
|
||||
- 🎨 12种预设主题色
|
||||
- 🎯 自定义主题色
|
||||
- 💾 主题设置持久化存储
|
||||
- 📱 多平台兼容(H5、小程序)
|
||||
|
||||
## 核心文件
|
||||
|
||||
### 1. useTheme Composable (`src/composables/useTheme.ts`)
|
||||
|
||||
主题管理的核心逻辑,提供:
|
||||
|
||||
```typescript
|
||||
const {
|
||||
theme, // 当前主题模式 'light' | 'dark'
|
||||
themeVars, // ConfigProviderThemeVars 主题变量
|
||||
toggleTheme, // 切换主题模式
|
||||
setThemeColor, // 设置主题色
|
||||
resetTheme, // 重置主题
|
||||
initTheme, // 初始化主题
|
||||
colorColumns, // 预设主题色列表
|
||||
} = useTheme();
|
||||
```
|
||||
|
||||
### 2. 布局文件
|
||||
|
||||
#### Tabbar 布局 (`src/layouts/tabbar.vue`)
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<wd-config-provider :theme="theme" :theme-vars="themeVars" custom-style="min-height: 100vh" :class="{ 'wot-theme-dark': theme === 'dark' }">
|
||||
<!-- 页面内容 -->
|
||||
</wd-config-provider>
|
||||
</template>
|
||||
```
|
||||
|
||||
#### Default 布局 (`src/layouts/default.vue`)
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<wd-config-provider :theme="theme" :theme-vars="themeVars" custom-style="background-color: #f5f5f5;min-height: 100vh" :class="{ 'wot-theme-dark': theme === 'dark' }">
|
||||
<!-- 页面内容 -->
|
||||
</wd-config-provider>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 3. 主题设置页面 (`src/pages/mine/settings/theme/index.vue`)
|
||||
|
||||
提供用户界面来:
|
||||
|
||||
- 切换暗黑/浅色模式
|
||||
- 选择预设主题色
|
||||
- 输入自定义主题色
|
||||
- 预览主题效果
|
||||
- 重置主题设置
|
||||
|
||||
## 主题变量
|
||||
|
||||
### 支持的 ConfigProviderThemeVars
|
||||
|
||||
根据 Wot Design Uni 文档,主要使用:
|
||||
|
||||
```typescript
|
||||
interface ConfigProviderThemeVars {
|
||||
colorTheme?: string; // 主题色
|
||||
buttonPrimaryBgColor?: string; // 主按钮背景色
|
||||
buttonPrimaryColor?: string; // 主按钮文字色
|
||||
// ... 更多变量
|
||||
}
|
||||
```
|
||||
|
||||
### 预设主题色
|
||||
|
||||
```typescript
|
||||
const colorColumns = [
|
||||
{ value: "#165DFF", label: "蓝色" },
|
||||
{ value: "#0FC6C2", label: "青绿色" },
|
||||
{ value: "#722ED1", label: "紫色" },
|
||||
{ value: "#F5222D", label: "红色" },
|
||||
{ value: "#FA8C16", label: "橙色" },
|
||||
{ value: "#FADB14", label: "黄色" },
|
||||
{ value: "#52C41A", label: "绿色" },
|
||||
{ value: "#EB2F96", label: "粉色" },
|
||||
{ value: "#13C2C2", label: "青色" },
|
||||
{ value: "#1890FF", label: "天蓝色" },
|
||||
{ value: "#CD5C5C", label: "经典红" },
|
||||
{ value: "#228B22", label: "自然绿" },
|
||||
];
|
||||
```
|
||||
|
||||
## 暗黑模式实现
|
||||
|
||||
### 1. ConfigProvider 主题切换
|
||||
|
||||
```vue
|
||||
<wd-config-provider :theme="theme">
|
||||
<!-- theme 为 'dark' 时自动应用暗黑模式 -->
|
||||
</wd-config-provider>
|
||||
```
|
||||
|
||||
### 2. 全局样式适配
|
||||
|
||||
在 `src/uni.scss` 中定义:
|
||||
|
||||
```scss
|
||||
.wot-theme-dark {
|
||||
background-color: #1a1a1a !important;
|
||||
color: #f5f5f5 !important;
|
||||
|
||||
/* H5 环境 body 样式 */
|
||||
body {
|
||||
background-color: #1a1a1a !important;
|
||||
color: #f5f5f5 !important;
|
||||
}
|
||||
|
||||
/* 其他组件暗黑模式适配 */
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 动态 Body 样式
|
||||
|
||||
在 `useTheme` 中自动处理:
|
||||
|
||||
```typescript
|
||||
const applyDarkModeBodyStyle = (isDark: boolean) => {
|
||||
// #ifdef H5
|
||||
if (typeof document !== "undefined") {
|
||||
const body = document.body;
|
||||
if (isDark) {
|
||||
body.style.backgroundColor = "#1a1a1a";
|
||||
body.style.color = "#f5f5f5";
|
||||
body.classList.add("wot-theme-dark");
|
||||
} else {
|
||||
body.style.backgroundColor = "#f8f8f8";
|
||||
body.style.color = "#333";
|
||||
body.classList.remove("wot-theme-dark");
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
};
|
||||
```
|
||||
|
||||
## 持久化存储
|
||||
|
||||
主题设置自动保存到本地存储,无需手动调用:
|
||||
|
||||
```typescript
|
||||
// 存储键名
|
||||
const THEME_STORAGE_KEY = "app_theme_mode";
|
||||
const THEME_COLOR_STORAGE_KEY = "app_theme_color";
|
||||
|
||||
// 主题设置自动持久化,在 useTheme 中已处理
|
||||
// 包含主题模式、主题色、是否跟随系统、用户设置状态
|
||||
```
|
||||
|
||||
主题设置会在以下情况自动保存:
|
||||
|
||||
- 切换主题模式时
|
||||
- 设置主题色时
|
||||
- 重置主题时
|
||||
- 跟随系统设置改变时
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 在页面中使用主题
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<view>
|
||||
<wd-button type="primary">主题色按钮</wd-button>
|
||||
<text :style="{ color: themeVars.colorTheme }">主题色文本</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useTheme } from "@/composables/useTheme";
|
||||
|
||||
const { theme, themeVars, toggleTheme, setThemeColor } = useTheme();
|
||||
|
||||
// 切换主题模式
|
||||
const handleToggleTheme = () => {
|
||||
toggleTheme();
|
||||
};
|
||||
|
||||
// 设置主题色
|
||||
const handleSetColor = (color: string) => {
|
||||
setThemeColor(color);
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
### 在组件中响应主题变化
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<view :class="{ 'dark-mode': isDark }">
|
||||
<!-- 组件内容 -->
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { useTheme } from "@/composables/useTheme";
|
||||
|
||||
const { theme } = useTheme();
|
||||
|
||||
const isDark = computed(() => theme.value === "dark");
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.dark-mode {
|
||||
background-color: #2a2a2a;
|
||||
color: #f5f5f5;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 组件开发
|
||||
|
||||
- 使用 Wot Design Uni 组件时,主题色会自动应用
|
||||
- 自定义组件需要手动适配暗黑模式
|
||||
- 使用 `:class="{ 'wot-theme-dark': theme === 'dark' }"` 来应用暗黑模式样式
|
||||
|
||||
### 2. 样式编写
|
||||
|
||||
```scss
|
||||
.my-component {
|
||||
background-color: #fff;
|
||||
color: #333;
|
||||
|
||||
// 暗黑模式适配
|
||||
.wot-theme-dark & {
|
||||
background-color: #2a2a2a;
|
||||
color: #f5f5f5;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 主题色使用
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<!-- 直接使用主题色 -->
|
||||
<view :style="{ borderColor: themeVars.colorTheme }">
|
||||
<!-- 内容 -->
|
||||
</view>
|
||||
</template>
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **ConfigProvider 包裹**:确保页面被 ConfigProvider 包裹才能应用主题
|
||||
2. **样式优先级**:暗黑模式样式需要足够的优先级,必要时使用 `!important`
|
||||
3. **平台兼容**:小程序和 H5 的样式处理略有不同,使用条件编译处理
|
||||
4. **性能考虑**:主题切换时避免频繁的 DOM 操作
|
||||
|
||||
## 扩展功能
|
||||
|
||||
### 添加新的主题色
|
||||
|
||||
在 `colorColumns` 中添加新的颜色:
|
||||
|
||||
```typescript
|
||||
export const colorColumns = [
|
||||
// ... 现有颜色
|
||||
{ value: "#FF6B6B", label: "珊瑚红" },
|
||||
{ value: "#4ECDC4", label: "薄荷绿" },
|
||||
];
|
||||
```
|
||||
|
||||
### 添加更多主题变量
|
||||
|
||||
```typescript
|
||||
const themeVars = ref<ConfigProviderThemeVars>({
|
||||
colorTheme: getStoredThemeColor(),
|
||||
buttonPrimaryBgColor: getStoredThemeColor(),
|
||||
// 添加更多变量
|
||||
});
|
||||
```
|
||||
|
||||
### 自定义暗黑模式样式
|
||||
|
||||
在 `uni.scss` 中添加更多组件的暗黑模式适配:
|
||||
|
||||
```scss
|
||||
.wot-theme-dark {
|
||||
// 新组件的暗黑模式样式
|
||||
.my-custom-component {
|
||||
background-color: #2a2a2a;
|
||||
color: #f5f5f5;
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,427 +0,0 @@
|
||||
# uni-mini-router 在UniApp中的整合教程
|
||||
|
||||
## 一、uni-mini-router简介
|
||||
|
||||
uni-mini-router是一个轻量级的路由管理库,专为uni-app设计,解决了uni-app原生路由系统中没有路由拦截等关键功能的问题。它提供了类似Vue Router的API体验,使得在uni-app项目中实现更加灵活和强大的路由管理成为可能。
|
||||
|
||||
### 主要特点
|
||||
|
||||
1. **Vue Router风格API**:提供与Vue Router相似的API,降低学习成本
|
||||
2. **路由拦截功能**:支持全局导航守卫,可以在路由跳转前后执行逻辑
|
||||
3. **优雅的参数传递**:支持params和query方式传参
|
||||
4. **命名路由**:支持通过路由名称进行导航
|
||||
5. **类型支持**:完整的TypeScript类型定义
|
||||
6. **轻量级**:体积小,性能高效
|
||||
|
||||
## 二、安装与基本配置
|
||||
|
||||
### 1. 安装uni-mini-router
|
||||
|
||||
使用npm或yarn安装uni-mini-router:
|
||||
|
||||
```bash
|
||||
pnpm add - uni-mini-router
|
||||
```
|
||||
|
||||
### 2. 初始化路由
|
||||
|
||||
在项目中创建router目录并初始化路由配置:
|
||||
|
||||
```typescript
|
||||
// src/router/index.ts
|
||||
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 };
|
||||
});
|
||||
|
||||
// 处理分包路由
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
return routes;
|
||||
}
|
||||
|
||||
// 创建路由实例
|
||||
const router = createRouter({
|
||||
routes: generateRoutes(),
|
||||
});
|
||||
|
||||
export default router;
|
||||
```
|
||||
|
||||
### 3. 在main.ts中挂载路由
|
||||
|
||||
```typescript
|
||||
// src/main.ts
|
||||
import { createSSRApp } from "vue";
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
|
||||
export function createApp() {
|
||||
const app = createSSRApp(App);
|
||||
|
||||
// 使用路由
|
||||
app.use(router);
|
||||
|
||||
return {
|
||||
app,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 配置自动导入(可选,推荐)
|
||||
|
||||
使用unplugin-auto-import插件可以自动导入路由相关hooks,无需每次手动导入:
|
||||
|
||||
```typescript
|
||||
// vite.config.ts
|
||||
import AutoImport from "unplugin-auto-import/vite";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
AutoImport({
|
||||
imports: [
|
||||
"vue",
|
||||
{
|
||||
from: "uni-mini-router",
|
||||
imports: ["createRouter", "useRouter", "useRoute"],
|
||||
},
|
||||
],
|
||||
dts: "src/auto-imports.d.ts",
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
## 三、路由基本用法
|
||||
|
||||
### 1. 编程式导航
|
||||
|
||||
uni-mini-router提供了多种导航方法:
|
||||
|
||||
```typescript
|
||||
const router = useRouter();
|
||||
|
||||
// 字符串路径导航
|
||||
router.push("/pages/index/index");
|
||||
|
||||
// 对象导航(通过路径)
|
||||
router.push({ path: "/pages/index/index" });
|
||||
|
||||
// 对象导航(通过名称)
|
||||
router.push({ name: "index" });
|
||||
|
||||
// 携带参数
|
||||
router.push({
|
||||
path: "/pages/detail/index",
|
||||
query: { id: 10 },
|
||||
});
|
||||
|
||||
// 通过名称 + 参数
|
||||
router.push({
|
||||
name: "detail",
|
||||
params: { id: 10 },
|
||||
});
|
||||
|
||||
// Tab页面导航
|
||||
router.pushTab("/pages/home/index");
|
||||
|
||||
// 关闭当前页面并跳转
|
||||
router.replace("/pages/index/index");
|
||||
|
||||
// 关闭所有页面并跳转
|
||||
router.replaceAll("/pages/index/index");
|
||||
|
||||
// 返回上一级
|
||||
router.back();
|
||||
|
||||
// 返回多级
|
||||
router.back(2);
|
||||
```
|
||||
|
||||
### 2. 获取和使用路由信息
|
||||
|
||||
```typescript
|
||||
const route = useRoute();
|
||||
|
||||
// 访问当前路由信息
|
||||
console.log(route.path); // 当前路由路径
|
||||
console.log(route.name); // 当前路由名称
|
||||
console.log(route.query); // 查询参数
|
||||
console.log(route.params); // 路由参数
|
||||
```
|
||||
|
||||
### 3. 接收页面参数
|
||||
|
||||
在页面组件中接收传递的参数:
|
||||
|
||||
```typescript
|
||||
<script setup lang="ts">
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// 方法1: 直接从route对象获取
|
||||
const id = route.query.id || route.params.id
|
||||
|
||||
// 方法2: 通过onLoad生命周期获取
|
||||
onLoad((option) => {
|
||||
console.log('页面参数:', option)
|
||||
// option中包含所有传递的参数
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
> ⚠️ **重要说明**:在uni-mini-router中,params和query参数都会转换为查询字符串放在URL中,两者在实际效果上没有区别。这种设计是为了与Vue Router保持API一致性。
|
||||
|
||||
## 四、导航守卫
|
||||
|
||||
uni-mini-router提供了全局导航守卫功能,可以在路由跳转前后执行自定义逻辑。
|
||||
|
||||
### 1. 全局前置守卫
|
||||
|
||||
```typescript
|
||||
// src/router/index.ts
|
||||
router.beforeEach((to, from, next) => {
|
||||
console.log("路由跳转:", from.path, "->", to.path);
|
||||
|
||||
// 检查是否需要登录
|
||||
if (to.meta && to.meta.requireAuth) {
|
||||
// 检查登录状态
|
||||
const isLoggedIn = uni.getStorageSync("token");
|
||||
|
||||
if (!isLoggedIn) {
|
||||
// 未登录,跳转到登录页
|
||||
uni.showToast({ title: "请先登录", icon: "none" });
|
||||
next("/pages/login/index");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 继续导航
|
||||
next();
|
||||
});
|
||||
```
|
||||
|
||||
### 2. 全局后置守卫
|
||||
|
||||
```typescript
|
||||
// src/router/index.ts
|
||||
router.afterEach((to, from) => {
|
||||
console.log("路由跳转完成:", to.path);
|
||||
|
||||
// 可以在这里做一些统计或记录
|
||||
});
|
||||
```
|
||||
|
||||
### 3. 路由元数据配置
|
||||
|
||||
可以在页面文件中使用`<route>`自定义块来定义路由元数据:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<!-- 页面内容 -->
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 页面逻辑
|
||||
</script>
|
||||
|
||||
<route lang="json">
|
||||
{
|
||||
"name": "protected-page",
|
||||
"meta": {
|
||||
"requireAuth": true,
|
||||
"title": "需要登录的页面"
|
||||
}
|
||||
}
|
||||
</route>
|
||||
```
|
||||
|
||||
## 五、实战示例:登录权限控制
|
||||
|
||||
### 1. 定义带有权限控制的路由
|
||||
|
||||
```typescript
|
||||
// src/router/index.ts
|
||||
import { createRouter } from "uni-mini-router";
|
||||
|
||||
const router = createRouter({
|
||||
routes: generateRoutes(),
|
||||
});
|
||||
|
||||
// 全局前置守卫
|
||||
router.beforeEach((to, from, next) => {
|
||||
// 检查页面是否需要登录
|
||||
if (to.meta && to.meta.requireAuth) {
|
||||
const token = uni.getStorageSync("token");
|
||||
|
||||
if (!token) {
|
||||
// 显示登录提示
|
||||
uni.showModal({
|
||||
title: "提示",
|
||||
content: "该功能需要登录后使用",
|
||||
confirmText: "去登录",
|
||||
cancelText: "返回",
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 记住原来要去的页面
|
||||
uni.setStorageSync("redirect", to.fullPath);
|
||||
next("/pages/login/index");
|
||||
} else {
|
||||
// 取消则返回首页
|
||||
next("/pages/index/index");
|
||||
}
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 继续导航
|
||||
next();
|
||||
});
|
||||
|
||||
export default router;
|
||||
```
|
||||
|
||||
### 2. 登录成功后跳转回原页面
|
||||
|
||||
```vue
|
||||
<!-- src/pages/login/index.vue -->
|
||||
<script setup>
|
||||
const router = useRouter();
|
||||
|
||||
function handleLogin() {
|
||||
// 模拟登录请求
|
||||
setTimeout(() => {
|
||||
// 登录成功,存储token
|
||||
uni.setStorageSync("token", "user_token_example");
|
||||
|
||||
// 获取之前要去的页面
|
||||
const redirect = uni.getStorageSync("redirect") || "/pages/index/index";
|
||||
uni.removeStorageSync("redirect");
|
||||
|
||||
// 跳转回原来的页面
|
||||
router.replaceAll(redirect);
|
||||
}, 1000);
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## 六、最佳实践与性能优化
|
||||
|
||||
### 1. 合理使用跳转方式
|
||||
|
||||
- **router.push**:需要保留当前页面、可返回时使用
|
||||
- **router.replace**:不需要返回当前页面时使用
|
||||
- **router.replaceAll**:需要清除所有页面栈时使用(如登录后)
|
||||
- **router.pushTab**:跳转到tabBar页面时使用
|
||||
|
||||
### 2. 参数传递最佳实践
|
||||
|
||||
- 对于简单数据,直接使用参数传递
|
||||
- 对于复杂数据或对象,可使用以下方法:
|
||||
|
||||
```typescript
|
||||
// 传递复杂对象
|
||||
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)) },
|
||||
});
|
||||
|
||||
// 接收页面
|
||||
onLoad((option) => {
|
||||
if (option.data) {
|
||||
try {
|
||||
const data = JSON.parse(decodeURIComponent(option.data));
|
||||
console.log(data);
|
||||
} catch (e) {
|
||||
console.error("参数解析错误", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 方法2: 对于非常大的数据,考虑使用全局状态管理或本地存储
|
||||
```
|
||||
|
||||
### 3. 路由懒加载
|
||||
|
||||
uni-mini-router自动支持小程序的分包加载特性,可以在pages.json中配置分包:
|
||||
|
||||
```json
|
||||
{
|
||||
"pages": [
|
||||
// 主包页面
|
||||
],
|
||||
"subPackages": [
|
||||
{
|
||||
"root": "pages/module",
|
||||
"pages": [
|
||||
{
|
||||
"path": "detail/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "详情页"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 七、路由调试与测试
|
||||
|
||||
### 1. 路由日志记录
|
||||
|
||||
```typescript
|
||||
// src/router/index.ts
|
||||
router.beforeEach((to, from, next) => {
|
||||
console.log(`[Router] ${from.path || "初始页面"} -> ${to.path}`, {
|
||||
params: to.params,
|
||||
query: to.query,
|
||||
});
|
||||
next();
|
||||
});
|
||||
```
|
||||
|
||||
### 2. 常见问题解决
|
||||
|
||||
1. **路由参数获取不到**:
|
||||
- 检查传参方式是否正确
|
||||
- 使用`console.log`打印完整的option对象
|
||||
- 尝试同时检查route.query和route.params
|
||||
|
||||
2. **页面未注册**:
|
||||
- 确保页面已在pages.json中正确注册
|
||||
- 检查路径大小写是否正确
|
||||
|
||||
3. **导航守卫不生效**:
|
||||
- 确保在路由配置后调用守卫
|
||||
- 检查是否正确调用next()函数
|
||||
|
||||
## 总结
|
||||
|
||||
uni-mini-router为uni-app提供了Vue Router风格的路由解决方案,特别是增加了路由拦截功能,解决了uni-app原生路由的限制。通过简单配置,就能在uni-app中实现更加灵活的路由管理,包括权限控制、参数传递和路由拦截等高级功能。
|
||||
|
||||
使用uni-mini-router可以让你的uni-app项目路由管理更加规范化和工程化,提升开发效率和代码质量。
|
||||
|
||||
参考资料:
|
||||
|
||||
- [uni-mini-router GitHub仓库](https://github.com/Moonofweisheng/uni-mini-router)
|
||||
- [uni-mini-router官方文档](https://moonofweisheng.github.io/uni-mini-router/)
|
||||
- [uni-app官方路由文档](https://uniapp.dcloud.net.cn/tutorial/page.html)
|
||||
@@ -19,17 +19,7 @@ const autoImportConfig = JSON.parse(fs.readFileSync(".eslintrc-auto-import.json"
|
||||
export default [
|
||||
// 忽略指定文件
|
||||
{
|
||||
ignores: [
|
||||
"node_modules/**",
|
||||
"dist/**",
|
||||
"auto-imports.d.ts",
|
||||
"unpackage/**",
|
||||
"public/**",
|
||||
"static/**",
|
||||
"**/u-charts/**",
|
||||
"**/qiun-**/**",
|
||||
"**/auto-imports.d.ts",
|
||||
],
|
||||
ignores: ["node_modules/**", "dist/**", "auto-imports.d.ts", "unpackage/**", "public/**", "static/**", "**/u-charts/**", "**/qiun-**/**", "**/auto-imports.d.ts"],
|
||||
},
|
||||
// 检查文件的配置
|
||||
{
|
||||
@@ -58,7 +48,7 @@ export default [
|
||||
rules: {
|
||||
...configPrettier.rules, // 关闭与 Prettier 冲突的规则
|
||||
...pluginPrettier.configs.recommended.rules, // 启用 Prettier 规则
|
||||
"prettier/prettier": "error", // 强制 Prettier 格式化
|
||||
"prettier/prettier": "off", // 完全关闭 Prettier 格式化检查
|
||||
"no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
@@ -121,9 +111,7 @@ export default [
|
||||
},
|
||||
rules: {
|
||||
// 禁用所有规则
|
||||
...Object.fromEntries(
|
||||
Object.keys(pluginVue.configs["vue3-recommended"].rules || {}).map((key) => [key, "off"])
|
||||
),
|
||||
...Object.fromEntries(Object.keys(pluginVue.configs["vue3-recommended"].rules || {}).map((key) => [key, "off"])),
|
||||
"no-unused-vars": "off",
|
||||
"no-prototype-builtins": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
|
||||
+26
-26
@@ -100,44 +100,43 @@
|
||||
"commit": "git-cz"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dcloudio/uni-app": "3.0.0-4070520250711001",
|
||||
"@dcloudio/uni-app-harmony": "3.0.0-4070520250711001",
|
||||
"@dcloudio/uni-app-plus": "3.0.0-4070520250711001",
|
||||
"@dcloudio/uni-components": "3.0.0-4070520250711001",
|
||||
"@dcloudio/uni-h5": "3.0.0-4070520250711001",
|
||||
"@dcloudio/uni-mp-alipay": "3.0.0-4070520250711001",
|
||||
"@dcloudio/uni-mp-baidu": "3.0.0-4070520250711001",
|
||||
"@dcloudio/uni-mp-harmony": "3.0.0-4070520250711001",
|
||||
"@dcloudio/uni-mp-jd": "3.0.0-4070520250711001",
|
||||
"@dcloudio/uni-mp-kuaishou": "3.0.0-4070520250711001",
|
||||
"@dcloudio/uni-mp-lark": "3.0.0-4070520250711001",
|
||||
"@dcloudio/uni-mp-qq": "3.0.0-4070520250711001",
|
||||
"@dcloudio/uni-mp-toutiao": "3.0.0-4070520250711001",
|
||||
"@dcloudio/uni-mp-weixin": "3.0.0-4070520250711001",
|
||||
"@dcloudio/uni-mp-xhs": "3.0.0-4070520250711001",
|
||||
"@dcloudio/uni-quickapp-webview": "3.0.0-4070520250711001",
|
||||
"@dcloudio/uni-app": "3.0.0-4070620250821001",
|
||||
"@dcloudio/uni-app-harmony": "3.0.0-4070620250821001",
|
||||
"@dcloudio/uni-app-plus": "3.0.0-4070620250821001",
|
||||
"@dcloudio/uni-components": "3.0.0-4070620250821001",
|
||||
"@dcloudio/uni-h5": "3.0.0-4070620250821001",
|
||||
"@dcloudio/uni-mp-alipay": "3.0.0-4070620250821001",
|
||||
"@dcloudio/uni-mp-baidu": "3.0.0-4070620250821001",
|
||||
"@dcloudio/uni-mp-harmony": "3.0.0-4070620250821001",
|
||||
"@dcloudio/uni-mp-jd": "3.0.0-4070620250821001",
|
||||
"@dcloudio/uni-mp-kuaishou": "3.0.0-4070620250821001",
|
||||
"@dcloudio/uni-mp-lark": "3.0.0-4070620250821001",
|
||||
"@dcloudio/uni-mp-qq": "3.0.0-4070620250821001",
|
||||
"@dcloudio/uni-mp-toutiao": "3.0.0-4070620250821001",
|
||||
"@dcloudio/uni-mp-weixin": "3.0.0-4070620250821001",
|
||||
"@dcloudio/uni-mp-xhs": "3.0.0-4070620250821001",
|
||||
"@dcloudio/uni-quickapp-webview": "3.0.0-4070620250821001",
|
||||
"@stomp/stompjs": "^7.1.1",
|
||||
"@uni-helper/uni-use": "^0.19.14",
|
||||
"@vueuse/core": "9.13.0",
|
||||
"pinia": "^2.2.2",
|
||||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^9.1.9",
|
||||
"wot-design-uni": "^1.9.1"
|
||||
"vue": "^3.5.18",
|
||||
"vue-i18n": "^9.14.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^19.5.0",
|
||||
"@commitlint/config-conventional": "^19.5.0",
|
||||
"@dcloudio/types": "^3.4.8",
|
||||
"@dcloudio/uni-automator": "3.0.0-4070520250711001",
|
||||
"@dcloudio/uni-cli-shared": "3.0.0-4070520250711001",
|
||||
"@dcloudio/uni-stacktracey": "3.0.0-4070520250711001",
|
||||
"@dcloudio/vite-plugin-uni": "3.0.0-4070520250711001",
|
||||
"@dcloudio/types": "^3.4.19",
|
||||
"@dcloudio/uni-automator": "3.0.0-4070620250821001",
|
||||
"@dcloudio/uni-cli-shared": "3.0.0-4070620250821001",
|
||||
"@dcloudio/uni-stacktracey": "3.0.0-4070620250821001",
|
||||
"@dcloudio/vite-plugin-uni": "3.0.0-4070620250821001",
|
||||
"@eslint/js": "^9.10.0",
|
||||
"@uni-helper/uni-types": "1.0.0-alpha.6",
|
||||
"@uni-helper/vite-plugin-uni-components": "^0.2.0",
|
||||
"@uni-helper/vite-plugin-uni-layouts": "^0.1.10",
|
||||
"@uni-helper/vite-plugin-uni-pages": "^0.2.28",
|
||||
"@vue/runtime-core": "^3.4.21",
|
||||
"@vue/runtime-core": "^3.5.18",
|
||||
"commitizen": "^4.3.0",
|
||||
"cz-git": "^1.9.4",
|
||||
"eslint": "^9.10.0",
|
||||
@@ -168,6 +167,7 @@
|
||||
"unplugin-auto-import": "^0.18.3",
|
||||
"vite": "5.2.8",
|
||||
"vue-eslint-parser": "^9.4.3",
|
||||
"vue-tsc": "^1.0.24"
|
||||
"vue-tsc": "^1.0.24",
|
||||
"wot-design-uni": "^1.9.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
<template>
|
||||
<wd-calendar
|
||||
v-model="dateRange"
|
||||
:label="label"
|
||||
type="daterange"
|
||||
:placeholder="placeholder"
|
||||
@confirm="handleConfirm"
|
||||
/>
|
||||
<wd-calendar v-model="dateRange" :label="label" type="daterange" :placeholder="placeholder" @confirm="handleConfirm" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@@ -38,12 +32,7 @@ watch(
|
||||
} else if (typeof val === "string" && val.includes(",")) {
|
||||
const [startDate, endDate] = val.split(",");
|
||||
|
||||
if (
|
||||
startDate &&
|
||||
endDate &&
|
||||
!isNaN(new Date(startDate).getTime()) &&
|
||||
!isNaN(new Date(endDate).getTime())
|
||||
) {
|
||||
if (startDate && endDate && !isNaN(new Date(startDate).getTime()) && !isNaN(new Date(endDate).getTime())) {
|
||||
dateRange.value = [new Date(startDate).getTime(), new Date(endDate).getTime()];
|
||||
} else {
|
||||
dateRange.value = null;
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
<template>
|
||||
<wd-picker
|
||||
v-model="selectedValues"
|
||||
:columns="pickerColumns"
|
||||
:display-format="displayFormat"
|
||||
:column-change="handleColumnChange"
|
||||
:label="label"
|
||||
:placeholder="pickerColumns.length ? '请选择' : '加载中...'"
|
||||
:required="required"
|
||||
/>
|
||||
<wd-picker v-model="selectedValues" :columns="pickerColumns" :display-format="displayFormat" :column-change="handleColumnChange" :label="label" :placeholder="pickerColumns.length ? '请选择' : '加载中...'" :required="required" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -80,9 +72,7 @@ const findTreePath = (value: number | string): number[] | string[] => {
|
||||
* @param treeData 树形数据
|
||||
* @returns Picker 所需的 columns 格式
|
||||
*/
|
||||
const transformTreeToColumns = (
|
||||
treeData: OptionType[]
|
||||
): Array<Array<{ label: string; value: string | number }>> => {
|
||||
const transformTreeToColumns = (treeData: OptionType[]): Array<Array<{ label: string; value: string | number }>> => {
|
||||
const columns: Array<Array<{ label: string; value: string | number }>> = [];
|
||||
|
||||
for (let depth = 0; depth <= selectedValues.value.length; depth++) {
|
||||
@@ -117,12 +107,7 @@ watch(
|
||||
/**
|
||||
* 处理列的变化,动态更新后续列的数据
|
||||
*/
|
||||
function handleColumnChange(
|
||||
pickerView: PickerViewInstance,
|
||||
value: Record<string, any> | Record<string, any>[],
|
||||
columnIndex: number,
|
||||
resolve: () => void
|
||||
) {
|
||||
function handleColumnChange(pickerView: PickerViewInstance, value: Record<string, any> | Record<string, any>[], columnIndex: number, resolve: () => void) {
|
||||
const selectedValue = selectedValues.value[selectedValues.value.length - 1] || undefined;
|
||||
emits("update:modelValue", selectedValue);
|
||||
|
||||
@@ -134,11 +119,7 @@ function handleColumnChange(
|
||||
/**
|
||||
* 动态更新所有后续列的数据
|
||||
*/
|
||||
function updatePickerColumns(
|
||||
pickerView: PickerViewInstance,
|
||||
parentId: string | number,
|
||||
columnIndex: number
|
||||
) {
|
||||
function updatePickerColumns(pickerView: PickerViewInstance, parentId: string | number, columnIndex: number) {
|
||||
const nextColumnIndex = columnIndex + 1;
|
||||
const children = findChildren(parentId, props.data);
|
||||
|
||||
@@ -151,10 +132,7 @@ function updatePickerColumns(
|
||||
/**
|
||||
* 根据节点value查找其子节点数据
|
||||
*/
|
||||
function findChildren(
|
||||
parentId: string | number,
|
||||
list: Record<string, any>[]
|
||||
): Record<string, any>[] {
|
||||
function findChildren(parentId: string | number, list: Record<string, any>[]): Record<string, any>[] {
|
||||
for (const item of list) {
|
||||
if (item.value === parentId && item.children) {
|
||||
return item.children;
|
||||
|
||||
@@ -28,17 +28,7 @@
|
||||
<!-- APP和H5采用renderjs渲染图表 -->
|
||||
<!-- #ifdef APP-VUE || H5 -->
|
||||
<block v-if="echarts">
|
||||
<view
|
||||
v-show="showchart"
|
||||
:id="'EC' + cid"
|
||||
:style="{ background: background }"
|
||||
style="width: 100%; height: 100%"
|
||||
:data-directory="directory"
|
||||
:prop="echartsOpts"
|
||||
:change:prop="rdcharts.ecinit"
|
||||
:resize="echartsResize"
|
||||
:change:resize="rdcharts.ecresize"
|
||||
/>
|
||||
<view v-show="showchart" :id="'EC' + cid" :style="{ background: background }" style="width: 100%; height: 100%" :data-directory="directory" :prop="echartsOpts" :change:prop="rdcharts.ecinit" :resize="echartsResize" :change:resize="rdcharts.ecresize" />
|
||||
</block>
|
||||
<block v-else>
|
||||
<view
|
||||
@@ -53,14 +43,7 @@
|
||||
v-on:touchmove="rdcharts.touchMove"
|
||||
v-on:touchend="rdcharts.touchEnd"
|
||||
>
|
||||
<canvas
|
||||
v-show="showchart"
|
||||
:id="cid"
|
||||
:canvasId="cid"
|
||||
:style="{ width: cWidth + 'px', height: cHeight + 'px', background: background }"
|
||||
:disable-scroll="disableScroll"
|
||||
@error="_error"
|
||||
/>
|
||||
<canvas v-show="showchart" :id="cid" :canvasId="cid" :style="{ width: cWidth + 'px', height: cHeight + 'px', background: background }" :disable-scroll="disableScroll" @error="_error" />
|
||||
</view>
|
||||
</block>
|
||||
<!-- #endif -->
|
||||
@@ -83,72 +66,25 @@
|
||||
/>
|
||||
</block>
|
||||
<block v-if="!ontouch">
|
||||
<canvas
|
||||
v-show="showchart"
|
||||
:id="cid"
|
||||
:canvasId="cid"
|
||||
:width="cWidth * pixel"
|
||||
:height="cHeight * pixel"
|
||||
:style="{ width: cWidth + 'px', height: cHeight + 'px', background: background }"
|
||||
:disable-scroll="disScroll"
|
||||
@tap="_tap"
|
||||
@error="_error"
|
||||
/>
|
||||
<canvas v-show="showchart" :id="cid" :canvasId="cid" :width="cWidth * pixel" :height="cHeight * pixel" :style="{ width: cWidth + 'px', height: cHeight + 'px', background: background }" :disable-scroll="disScroll" @tap="_tap" @error="_error" />
|
||||
</block>
|
||||
<!-- #endif -->
|
||||
<!-- 其他小程序通过vue渲染图表 -->
|
||||
<!-- #ifdef MP-WEIXIN || MP-BAIDU || MP-QQ || MP-TOUTIAO || MP-KUAISHOU || MP-LARK || MP-JD || MP-360 -->
|
||||
<block v-if="type2d">
|
||||
<view v-if="ontouch" @tap="_tap">
|
||||
<canvas
|
||||
v-show="showchart"
|
||||
:id="cid"
|
||||
:canvasId="cid"
|
||||
:style="{ width: cWidth + 'px', height: cHeight + 'px', background: background }"
|
||||
type="2d"
|
||||
:disable-scroll="disScroll"
|
||||
@touchstart="_touchStart"
|
||||
@touchmove="_touchMove"
|
||||
@touchend="_touchEnd"
|
||||
@error="_error"
|
||||
/>
|
||||
<canvas v-show="showchart" :id="cid" :canvasId="cid" :style="{ width: cWidth + 'px', height: cHeight + 'px', background: background }" type="2d" :disable-scroll="disScroll" @touchstart="_touchStart" @touchmove="_touchMove" @touchend="_touchEnd" @error="_error" />
|
||||
</view>
|
||||
<view v-if="!ontouch" @tap="_tap">
|
||||
<canvas
|
||||
v-show="showchart"
|
||||
:id="cid"
|
||||
:canvasId="cid"
|
||||
:style="{ width: cWidth + 'px', height: cHeight + 'px', background: background }"
|
||||
type="2d"
|
||||
:disable-scroll="disScroll"
|
||||
@error="_error"
|
||||
/>
|
||||
<canvas v-show="showchart" :id="cid" :canvasId="cid" :style="{ width: cWidth + 'px', height: cHeight + 'px', background: background }" type="2d" :disable-scroll="disScroll" @error="_error" />
|
||||
</view>
|
||||
</block>
|
||||
<block v-if="!type2d">
|
||||
<view v-if="ontouch" @tap="_tap">
|
||||
<canvas
|
||||
v-if="showchart"
|
||||
:id="cid"
|
||||
:canvasId="cid"
|
||||
:style="{ width: cWidth + 'px', height: cHeight + 'px', background: background }"
|
||||
:disable-scroll="disScroll"
|
||||
@touchstart="_touchStart"
|
||||
@touchmove="_touchMove"
|
||||
@touchend="_touchEnd"
|
||||
@error="_error"
|
||||
/>
|
||||
<canvas v-if="showchart" :id="cid" :canvasId="cid" :style="{ width: cWidth + 'px', height: cHeight + 'px', background: background }" :disable-scroll="disScroll" @touchstart="_touchStart" @touchmove="_touchMove" @touchend="_touchEnd" @error="_error" />
|
||||
</view>
|
||||
<view v-if="!ontouch">
|
||||
<canvas
|
||||
v-if="showchart"
|
||||
:id="cid"
|
||||
:canvasId="cid"
|
||||
:style="{ width: cWidth + 'px', height: cHeight + 'px', background: background }"
|
||||
:disable-scroll="disScroll"
|
||||
@tap="_tap"
|
||||
@error="_error"
|
||||
/>
|
||||
<canvas v-if="showchart" :id="cid" :canvasId="cid" :style="{ width: cWidth + 'px', height: cHeight + 'px', background: background }" :disable-scroll="disScroll" @tap="_tap" @error="_error" />
|
||||
</view>
|
||||
</block>
|
||||
<!-- #endif -->
|
||||
@@ -166,10 +102,7 @@ function deepCloneAssign(origin = {}, ...args) {
|
||||
for (let i in args) {
|
||||
for (let key in args[i]) {
|
||||
if (args[i].hasOwnProperty(key)) {
|
||||
origin[key] =
|
||||
args[i][key] && typeof args[i][key] === "object"
|
||||
? deepCloneAssign(Array.isArray(args[i][key]) ? [] : {}, origin[key], args[i][key])
|
||||
: args[i][key];
|
||||
origin[key] = args[i][key] && typeof args[i][key] === "object" ? deepCloneAssign(Array.isArray(args[i][key]) ? [] : {}, origin[key], args[i][key]) : args[i][key];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -474,11 +407,7 @@ export default {
|
||||
optsProps: {
|
||||
handler(val, oldval) {
|
||||
if (typeof val === "object") {
|
||||
if (
|
||||
JSON.stringify(val) !== JSON.stringify(oldval) &&
|
||||
this.echarts === false &&
|
||||
this.optsWatch == true
|
||||
) {
|
||||
if (JSON.stringify(val) !== JSON.stringify(oldval) && this.echarts === false && this.optsWatch == true) {
|
||||
this.checkData(this.drawData);
|
||||
}
|
||||
} else {
|
||||
@@ -562,11 +491,7 @@ export default {
|
||||
}
|
||||
// #ifdef MP-WEIXIN
|
||||
this.inWx = true;
|
||||
if (
|
||||
this.canvas2d === false ||
|
||||
systemInfo.platform === "windows" ||
|
||||
systemInfo.platform === "mac"
|
||||
) {
|
||||
if (this.canvas2d === false || systemInfo.platform === "windows" || systemInfo.platform === "mac") {
|
||||
this.type2d = false;
|
||||
} else {
|
||||
this.type2d = true;
|
||||
@@ -656,12 +581,7 @@ export default {
|
||||
methods: {
|
||||
beforeInit() {
|
||||
this.mixinDatacomErrorMessage = null;
|
||||
if (
|
||||
typeof this.chartData === "object" &&
|
||||
this.chartData != null &&
|
||||
this.chartData.series !== undefined &&
|
||||
this.chartData.series.length > 0
|
||||
) {
|
||||
if (typeof this.chartData === "object" && this.chartData != null && this.chartData.series !== undefined && this.chartData.series.length > 0) {
|
||||
//拷贝一下chartData,为了opts变更后统一数据来源
|
||||
this.drawData = deepCloneAssign({}, this.chartData);
|
||||
this.mixinDatacomLoading = false;
|
||||
@@ -920,11 +840,7 @@ export default {
|
||||
ctx.canvas.willReadFrequently = true;
|
||||
cfu.option[cid].context = ctx;
|
||||
cfu.option[cid].rotateLock = cfu.option[cid].rotate;
|
||||
if (
|
||||
cfu.instance[cid] &&
|
||||
cfu.option[cid] &&
|
||||
cfu.option[cid].update === true
|
||||
) {
|
||||
if (cfu.instance[cid] && cfu.option[cid] && cfu.option[cid].update === true) {
|
||||
this._updataUChart(cid);
|
||||
} else {
|
||||
canvas.width = data.width * this.pixel;
|
||||
@@ -939,8 +855,7 @@ export default {
|
||||
}
|
||||
} else {
|
||||
this.showchart = false;
|
||||
this.mixinDatacomErrorMessage =
|
||||
"参数错误:开启2d模式后,未获取到dom节点,canvas-id:" + cid;
|
||||
this.mixinDatacomErrorMessage = "参数错误:开启2d模式后,未获取到dom节点,canvas-id:" + cid;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@@ -1086,10 +1001,7 @@ export default {
|
||||
offset: offset,
|
||||
textList: tc.textList,
|
||||
formatter: (item, category, index, opts) => {
|
||||
if (
|
||||
typeof cfu.option[cid].tooltipFormat === "string" &&
|
||||
cfu.formatter[cfu.option[cid].tooltipFormat]
|
||||
) {
|
||||
if (typeof cfu.option[cid].tooltipFormat === "string" && cfu.formatter[cfu.option[cid].tooltipFormat]) {
|
||||
return cfu.formatter[cfu.option[cid].tooltipFormat](item, category, index, opts);
|
||||
} else {
|
||||
return this._tooltipDefault(item, category, index, opts);
|
||||
@@ -1099,10 +1011,7 @@ export default {
|
||||
} else {
|
||||
cfu.instance[cid].showToolTip(e, {
|
||||
formatter: (item, category, index, opts) => {
|
||||
if (
|
||||
typeof cfu.option[cid].tooltipFormat === "string" &&
|
||||
cfu.formatter[cfu.option[cid].tooltipFormat]
|
||||
) {
|
||||
if (typeof cfu.option[cid].tooltipFormat === "string" && cfu.formatter[cfu.option[cid].tooltipFormat]) {
|
||||
return cfu.formatter[cfu.option[cid].tooltipFormat](item, category, index, opts);
|
||||
} else {
|
||||
return this._tooltipDefault(item, category, index, opts);
|
||||
@@ -1224,19 +1133,10 @@ export default {
|
||||
if (cfu.option[cid].enableScroll === true && e.changedTouches.length == 1) {
|
||||
cfu.instance[cid].scroll(e);
|
||||
}
|
||||
if (
|
||||
this.ontap === true &&
|
||||
cfu.option[cid].enableScroll === false &&
|
||||
this.onmovetip === true
|
||||
) {
|
||||
if (this.ontap === true && cfu.option[cid].enableScroll === false && this.onmovetip === true) {
|
||||
this._tap(e, true);
|
||||
}
|
||||
if (
|
||||
this.ontouch === true &&
|
||||
cfu.option[cid].enableScroll === true &&
|
||||
this.onzoom === true &&
|
||||
e.changedTouches.length == 2
|
||||
) {
|
||||
if (this.ontouch === true && cfu.option[cid].enableScroll === true && this.onzoom === true && e.changedTouches.length == 2) {
|
||||
cfu.instance[cid].dobuleZoom(e);
|
||||
}
|
||||
this.emitMsg({
|
||||
@@ -1263,11 +1163,7 @@ export default {
|
||||
opts: cfu.instance[cid].opts,
|
||||
},
|
||||
});
|
||||
if (
|
||||
this.ontap === true &&
|
||||
cfu.option[cid].enableScroll === false &&
|
||||
this.onmovetip === true
|
||||
) {
|
||||
if (this.ontap === true && cfu.option[cid].enableScroll === false && this.onmovetip === true) {
|
||||
this._tap(e, true);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -19,33 +19,11 @@
|
||||
// 通用配置项
|
||||
|
||||
// 主题颜色配置:如每个图表类型需要不同主题,请在对应图表类型上更改color属性
|
||||
const color = [
|
||||
"#1890FF",
|
||||
"#91CB74",
|
||||
"#FAC858",
|
||||
"#EE6666",
|
||||
"#73C0DE",
|
||||
"#3CA272",
|
||||
"#FC8452",
|
||||
"#9A60B4",
|
||||
"#ea7ccc",
|
||||
];
|
||||
const color = ["#1890FF", "#91CB74", "#FAC858", "#EE6666", "#73C0DE", "#3CA272", "#FC8452", "#9A60B4", "#ea7ccc"];
|
||||
|
||||
const cfe = {
|
||||
//demotype为自定义图表类型
|
||||
type: [
|
||||
"pie",
|
||||
"ring",
|
||||
"rose",
|
||||
"funnel",
|
||||
"line",
|
||||
"column",
|
||||
"area",
|
||||
"radar",
|
||||
"gauge",
|
||||
"candle",
|
||||
"demotype",
|
||||
],
|
||||
type: ["pie", "ring", "rose", "funnel", "line", "column", "area", "radar", "gauge", "candle", "demotype"],
|
||||
//增加自定义图表类型,如果需要categories,请在这里加入您的图表类型例如最后的"demotype"
|
||||
categories: ["line", "column", "area", "radar", "gauge", "candle", "demotype"],
|
||||
//instance为实例变量承载属性,option为eopts承载属性,不要删除
|
||||
|
||||
@@ -17,17 +17,7 @@
|
||||
*/
|
||||
|
||||
// 主题颜色配置:如每个图表类型需要不同主题,请在对应图表类型上更改color属性
|
||||
const color = [
|
||||
"#1890FF",
|
||||
"#91CB74",
|
||||
"#FAC858",
|
||||
"#EE6666",
|
||||
"#73C0DE",
|
||||
"#3CA272",
|
||||
"#FC8452",
|
||||
"#9A60B4",
|
||||
"#ea7ccc",
|
||||
];
|
||||
const color = ["#1890FF", "#91CB74", "#FAC858", "#EE6666", "#73C0DE", "#3CA272", "#FC8452", "#9A60B4", "#ea7ccc"];
|
||||
|
||||
//事件转换函数,主要用作格式化x轴为时间轴,根据需求自行修改
|
||||
const formatDateTime = (timeStamp, returnType) => {
|
||||
@@ -61,66 +51,11 @@ const formatDateTime = (timeStamp, returnType) => {
|
||||
|
||||
const cfu = {
|
||||
//demotype为自定义图表类型,一般不需要自定义图表类型,只需要改根节点上对应的类型即可
|
||||
type: [
|
||||
"pie",
|
||||
"ring",
|
||||
"rose",
|
||||
"word",
|
||||
"funnel",
|
||||
"map",
|
||||
"arcbar",
|
||||
"line",
|
||||
"column",
|
||||
"mount",
|
||||
"bar",
|
||||
"area",
|
||||
"radar",
|
||||
"gauge",
|
||||
"candle",
|
||||
"mix",
|
||||
"tline",
|
||||
"tarea",
|
||||
"scatter",
|
||||
"bubble",
|
||||
"demotype",
|
||||
],
|
||||
range: [
|
||||
"饼状图",
|
||||
"圆环图",
|
||||
"玫瑰图",
|
||||
"词云图",
|
||||
"漏斗图",
|
||||
"地图",
|
||||
"圆弧进度条",
|
||||
"折线图",
|
||||
"柱状图",
|
||||
"山峰图",
|
||||
"条状图",
|
||||
"区域图",
|
||||
"雷达图",
|
||||
"仪表盘",
|
||||
"K线图",
|
||||
"混合图",
|
||||
"时间轴折线",
|
||||
"时间轴区域",
|
||||
"散点图",
|
||||
"气泡图",
|
||||
"自定义类型",
|
||||
],
|
||||
type: ["pie", "ring", "rose", "word", "funnel", "map", "arcbar", "line", "column", "mount", "bar", "area", "radar", "gauge", "candle", "mix", "tline", "tarea", "scatter", "bubble", "demotype"],
|
||||
range: ["饼状图", "圆环图", "玫瑰图", "词云图", "漏斗图", "地图", "圆弧进度条", "折线图", "柱状图", "山峰图", "条状图", "区域图", "雷达图", "仪表盘", "K线图", "混合图", "时间轴折线", "时间轴区域", "散点图", "气泡图", "自定义类型"],
|
||||
//增加自定义图表类型,如果需要categories,请在这里加入您的图表类型,例如最后的"demotype"
|
||||
//自定义类型时需要注意"tline","tarea","scatter","bubble"等时间轴(矢量x轴)类图表,没有categories,不需要加入categories
|
||||
categories: [
|
||||
"line",
|
||||
"column",
|
||||
"mount",
|
||||
"bar",
|
||||
"area",
|
||||
"radar",
|
||||
"gauge",
|
||||
"candle",
|
||||
"mix",
|
||||
"demotype",
|
||||
],
|
||||
categories: ["line", "column", "mount", "bar", "area", "radar", "gauge", "candle", "mix", "demotype"],
|
||||
//instance为实例变量承载属性,不要删除
|
||||
instance: {},
|
||||
//option为opts及eopts承载属性,不要删除
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -135,10 +135,7 @@ export function useStomp(options: UseStompOptions = {}) {
|
||||
}
|
||||
|
||||
// 如果是授权问题导致的关闭,尝试重连
|
||||
if (
|
||||
(event?.code === 1000 || event?.code === 1006 || event?.code === 1008) &&
|
||||
reconnectCount.value < maxReconnectAttempts
|
||||
) {
|
||||
if ((event?.code === 1000 || event?.code === 1006 || event?.code === 1008) && reconnectCount.value < maxReconnectAttempts) {
|
||||
console.log("检测到连接异常关闭,将尝试重连");
|
||||
|
||||
// 通过 handleReconnect 统一处理重连,避免重复计数
|
||||
@@ -152,11 +149,7 @@ export function useStomp(options: UseStompOptions = {}) {
|
||||
isConnecting = false;
|
||||
|
||||
// 检查是否是授权错误
|
||||
if (
|
||||
frame.headers?.message?.includes("Unauthorized") ||
|
||||
frame.body?.includes("Unauthorized") ||
|
||||
frame.body?.includes("Token")
|
||||
) {
|
||||
if (frame.headers?.message?.includes("Unauthorized") || frame.body?.includes("Unauthorized") || frame.body?.includes("Token")) {
|
||||
console.warn("WebSocket授权错误,请检查登录状态");
|
||||
// 授权错误不进行重连
|
||||
isManualDisconnect = true;
|
||||
@@ -182,9 +175,7 @@ export function useStomp(options: UseStompOptions = {}) {
|
||||
console.log(`准备重连(${reconnectCount.value}/${maxReconnectAttempts})...`);
|
||||
|
||||
// 使用指数退避策略增加重连间隔
|
||||
const delay = useExponentialBackoff
|
||||
? Math.min(reconnectDelay * Math.pow(2, reconnectCount.value - 1), maxReconnectDelay)
|
||||
: reconnectDelay;
|
||||
const delay = useExponentialBackoff ? Math.min(reconnectDelay * Math.pow(2, reconnectCount.value - 1), maxReconnectDelay) : reconnectDelay;
|
||||
|
||||
// 清除之前的计时器
|
||||
if (reconnectTimer) {
|
||||
|
||||
@@ -1,52 +1,27 @@
|
||||
export interface TabbarItem {
|
||||
name: string;
|
||||
value: number | null;
|
||||
active: boolean;
|
||||
title: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
const tabbarItems = ref<TabbarItem[]>([
|
||||
{ name: "index", value: null, active: true, title: "首页", icon: "home" },
|
||||
{ name: "work", value: null, active: false, title: "工作台", icon: "app" },
|
||||
{ name: "mine", value: null, active: false, title: "我的", icon: "user" },
|
||||
{ name: "index", title: "首页", icon: "home" },
|
||||
{ name: "work", title: "工作台", icon: "app" },
|
||||
{ name: "mine", title: "我的", icon: "user" },
|
||||
]);
|
||||
|
||||
const activeTabbar = ref<string>("index");
|
||||
|
||||
export function useTabbar() {
|
||||
const tabbarList = computed(() => tabbarItems.value);
|
||||
|
||||
const activeTabbar = computed(() => {
|
||||
const item = tabbarItems.value.find((item) => item.active);
|
||||
return item || tabbarItems.value[0];
|
||||
});
|
||||
|
||||
const getTabbarItemValue = (name: string) => {
|
||||
const item = tabbarItems.value.find((item) => item.name === name);
|
||||
return item && item.value ? item.value : null;
|
||||
};
|
||||
|
||||
const setTabbarItem = (name: string, value: number) => {
|
||||
const tabbarItem = tabbarItems.value.find((item) => item.name === name);
|
||||
if (tabbarItem) {
|
||||
tabbarItem.value = value;
|
||||
}
|
||||
};
|
||||
|
||||
const setTabbarItemActive = (name: string) => {
|
||||
tabbarItems.value.forEach((item) => {
|
||||
if (item.name === name) {
|
||||
item.active = true;
|
||||
} else {
|
||||
item.active = false;
|
||||
}
|
||||
});
|
||||
activeTabbar.value = name;
|
||||
};
|
||||
|
||||
return {
|
||||
tabbarList,
|
||||
activeTabbar,
|
||||
getTabbarItemValue,
|
||||
setTabbarItem,
|
||||
setTabbarItemActive,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,30 @@
|
||||
<script lang="ts" setup>
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { useThemeStore } from "@/store/modules/theme.store";
|
||||
import { onMounted, onUnmounted } from "vue";
|
||||
|
||||
const useTheme = useThemeStore();
|
||||
const themeStore = useThemeStore();
|
||||
const theme = themeStore.theme;
|
||||
const themeVars = themeStore.themeVars;
|
||||
|
||||
let theme = useTheme.theme;
|
||||
let themeVars = useTheme.themeVars;
|
||||
// 监听主题变化事件
|
||||
const handleThemeChange = () => {
|
||||
// 当主题变化时强制更新组件
|
||||
// 这里可以添加一些更新逻辑
|
||||
};
|
||||
|
||||
onLoad(() => {
|
||||
theme = useTheme.theme;
|
||||
themeVars = useTheme.themeVars;
|
||||
onMounted(() => {
|
||||
// 监听主题变化事件
|
||||
uni.$on("theme-change", handleThemeChange);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
// 移除事件监听
|
||||
uni.$off("theme-change", handleThemeChange);
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
virtualHost: true,
|
||||
styleIsolation: "shared",
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<wd-config-provider :theme-vars="themeVars" :theme="theme" :custom-class="`page-wraper ${theme}`">
|
||||
<wd-config-provider :theme-vars="themeVars" :theme="theme" :custom-class="`page-wrapper theme-adaptive ${theme}`">
|
||||
<slot />
|
||||
<wd-notify />
|
||||
<wd-toast />
|
||||
@@ -33,13 +33,14 @@ export default {
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-wraper {
|
||||
.page-wrapper {
|
||||
box-sizing: border-box;
|
||||
min-height: calc(100vh - var(--window-top));
|
||||
background: #f9f9f9;
|
||||
background-color: var(--bg-color-2);
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.wot-theme-dark.page-wraper {
|
||||
background: #222;
|
||||
.wot-theme-dark.page-wrapper {
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -1,22 +1,10 @@
|
||||
<template>
|
||||
<wd-config-provider :theme-vars="themeVars" :custom-class="`page-wraper ${theme}`" :theme="theme">
|
||||
<wd-config-provider :theme-vars="themeVars" :custom-class="`page-wrapper ${theme}`" :theme="theme">
|
||||
<slot />
|
||||
<wd-tabbar
|
||||
:model-value="activeTabbar.name"
|
||||
placeholder
|
||||
bordered
|
||||
safe-area-inset-bottom
|
||||
fixed
|
||||
@change="handleTabbarChange"
|
||||
>
|
||||
<wd-tabbar-item
|
||||
v-for="(item, index) in tabbarList"
|
||||
:key="index"
|
||||
:name="item.name"
|
||||
:value="getTabbarItemValue(item.name)"
|
||||
:title="item.title"
|
||||
:icon="item.icon"
|
||||
/>
|
||||
<wd-tabbar :model-value="activeTabbar" placeholder bordered safe-area-inset-bottom fixed
|
||||
@change="handleTabbarChange">
|
||||
<wd-tabbar-item v-for="(item, index) in tabbarList" :key="index" :name="item.name" :title="item.title"
|
||||
:icon="item.icon" />
|
||||
</wd-tabbar>
|
||||
<wd-notify />
|
||||
<wd-toast />
|
||||
@@ -35,7 +23,7 @@ const router = useRouter();
|
||||
const route = useRoute();
|
||||
let theme = useTheme.theme;
|
||||
let themeVars = useTheme.themeVars;
|
||||
const { activeTabbar, getTabbarItemValue, setTabbarItemActive, tabbarList } = useTabbar();
|
||||
const { activeTabbar, setTabbarItemActive, tabbarList } = useTabbar();
|
||||
|
||||
function handleTabbarChange({ value }: { value: string }) {
|
||||
setTabbarItemActive(value);
|
||||
@@ -47,7 +35,7 @@ onMounted(() => {
|
||||
uni.hideTabBar();
|
||||
// #endif
|
||||
nextTick(() => {
|
||||
if (route.name && route.name !== activeTabbar.value.name) {
|
||||
if (route.name) {
|
||||
setTabbarItemActive(route.name);
|
||||
}
|
||||
});
|
||||
@@ -65,13 +53,14 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.page-wraper {
|
||||
.page-wrapper {
|
||||
box-sizing: border-box;
|
||||
min-height: calc(100vh - var(--window-top));
|
||||
background: #f9f9f9;
|
||||
background-color: var(--bg-color-2);
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.wot-theme-dark.page-wraper {
|
||||
background: #222;
|
||||
.wot-theme-dark.page-wrapper {
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -68,7 +68,11 @@
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/profile/index",
|
||||
"type": "page"
|
||||
"type": "page",
|
||||
"name": "profile",
|
||||
"style": {
|
||||
"navigationBarTitleText": "个人中心"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/settings/index",
|
||||
@@ -110,8 +114,7 @@
|
||||
"name": "privacy",
|
||||
"style": {
|
||||
"navigationBarTitleText": "隐私政策"
|
||||
},
|
||||
"layout": "tabbar"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/settings/theme/index",
|
||||
|
||||
@@ -1,30 +1,21 @@
|
||||
<template>
|
||||
<view style="width: 100%; height: var(--status-bar-height)" />
|
||||
<view class="app-container">
|
||||
<view class="app-container theme-adaptive">
|
||||
<wd-swiper v-model:current="current" :list="swiperList" autoplay />
|
||||
|
||||
<!-- 快捷导航 -->
|
||||
<wd-grid clickable :column="4" class="mt-2">
|
||||
<wd-grid-item
|
||||
v-for="(item, index) in navList"
|
||||
:key="index"
|
||||
use-slot
|
||||
link-type="navigateTo"
|
||||
:url="item.url"
|
||||
>
|
||||
<wd-grid-item v-for="(item, index) in navList" :key="index" use-slot link-type="navigateTo" :url="item.url">
|
||||
<view class="p-2">
|
||||
<image class="w-72rpx h-72rpx rounded-8rpx" :src="item.icon" />
|
||||
</view>
|
||||
<view class="text">{{ item.title }}</view>
|
||||
<view class="text theme-text-primary">{{ item.title }}</view>
|
||||
</wd-grid-item>
|
||||
</wd-grid>
|
||||
|
||||
<!-- 通知公告 -->
|
||||
<wd-notice-bar
|
||||
text="fastapp 是一个基于 Vue3 + UniApp 的前端模板项目,提供了一套完整的前端解决方案,包括登录、权限、字典、接口请求、状态管理、页面布局、组件封装等功能。"
|
||||
color="#34D19D"
|
||||
type="info"
|
||||
>
|
||||
<wd-notice-bar text="fastapp 是一个基于 Vue3 + UniApp 的前端模板项目,提供了一套完整的前端解决方案,包括登录、权限、字典、接口请求、状态管理、页面布局、组件封装等功能。"
|
||||
color="#34D19D" type="info">
|
||||
<template #prefix>
|
||||
<wd-tag color="#FAA21E" bg-color="#FAA21E" plain custom-style="margin-right:10rpx">
|
||||
通知公告
|
||||
@@ -39,8 +30,8 @@
|
||||
<view class="flex-center">
|
||||
<image class="w-80rpx h-80rpx rounded-8rpx" src="/static/icons/visitor.png" />
|
||||
<view class="ml-5 text-left">
|
||||
<view class="font-bold">访客数</view>
|
||||
<view class="mt-2">{{ visitStatsData.todayUvCount }}</view>
|
||||
<view class="font-bold theme-text-primary">访客数</view>
|
||||
<view class="mt-2 theme-text-primary">{{ visitStatsData.todayUvCount }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -50,8 +41,8 @@
|
||||
<view class="flex-center">
|
||||
<image class="w-80rpx h-80rpx rounded-8rpx" src="/static/icons/browser.png" />
|
||||
<view class="ml-5 text-left">
|
||||
<view class="font-bold">浏览量</view>
|
||||
<view class="mt-2">{{ visitStatsData.todayPvCount }}</view>
|
||||
<view class="font-bold theme-text-primary">浏览量</view>
|
||||
<view class="mt-2 theme-text-primary">{{ visitStatsData.todayPvCount }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -61,14 +52,9 @@
|
||||
<wd-card type="rectangle">
|
||||
<template #title>
|
||||
<view class="flex-between">
|
||||
<view>访问趋势</view>
|
||||
<view class="theme-text-primary">访问趋势</view>
|
||||
<view>
|
||||
<wd-radio-group
|
||||
:value="recentDaysRange"
|
||||
shape="button"
|
||||
inline
|
||||
@change="handleDataRangeChange"
|
||||
>
|
||||
<wd-radio-group :value="recentDaysRange" shape="button" inline @change="handleDataRangeChange">
|
||||
<wd-radio :value="7">近7天</wd-radio>
|
||||
<wd-radio :value="15">近15天</wd-radio>
|
||||
</wd-radio-group>
|
||||
@@ -252,11 +238,11 @@ onShow(() => {
|
||||
});
|
||||
</script>
|
||||
|
||||
<route lang="json">
|
||||
{
|
||||
<route lang="json">{
|
||||
"name": "index",
|
||||
"style": { "navigationStyle": "custom" },
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
},
|
||||
"layout": "tabbar"
|
||||
}
|
||||
</route>
|
||||
<style setup lang="scss"></style>
|
||||
}</route>
|
||||
<style setup lang="scss"></style>
|
||||
@@ -1,109 +1,51 @@
|
||||
<template>
|
||||
<view class="app-container">
|
||||
<view class="app-container theme-adaptive">
|
||||
<!-- 背景图 -->
|
||||
<image src="/static/images/login-bg.svg" mode="aspectFill" class="login-bg" />
|
||||
|
||||
<!-- Logo和标题区域 -->
|
||||
<view class="header">
|
||||
<wd-img src="./static/logo.png" class="logo" mode="aspectFit" />
|
||||
<text class="title">FastApp管理系统</text>
|
||||
<text class="subtitle">欢迎使用移动端管理平台</text>
|
||||
<wd-img src="/static/logo.png" class="logo" mode="aspectFit" />
|
||||
<text class="title theme-text-inverse">FastApp管理系统</text>
|
||||
<text class="subtitle theme-text-inverse">欢迎使用移动端管理平台</text>
|
||||
</view>
|
||||
|
||||
<view class="login-card">
|
||||
<view class="login-card theme-card">
|
||||
<view class="form-wrap">
|
||||
<!-- 账号密码登录表单 -->
|
||||
<wd-form v-if="loginType === 'account'" ref="loginFormRef" :model="loginFormData">
|
||||
<!-- 用户名输入框 -->
|
||||
<view class="form-item">
|
||||
<wd-icon
|
||||
name="user"
|
||||
size="22"
|
||||
:color="isDarkMode ? '#7AC5FF' : '#333'"
|
||||
class="input-icon"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="loginFormData.username"
|
||||
type="text"
|
||||
prop="username"
|
||||
class="form-input input-transparent"
|
||||
placeholder="请输入用户名"
|
||||
placeholder-class="input-placeholder"
|
||||
clearable
|
||||
clear-trigger="focus"
|
||||
/>
|
||||
<wd-icon name="user" size="22" :color="themeVars.darkColor3" class="input-icon" />
|
||||
<wd-input v-model="loginFormData.username" type="text" prop="username" class="form-input input-transparent"
|
||||
placeholder="请输入用户名" placeholder-class="input-placeholder" clearable clear-trigger="focus" />
|
||||
</view>
|
||||
|
||||
<!-- <wd-divider /> -->
|
||||
|
||||
<!-- 密码输入框 -->
|
||||
<view class="form-item">
|
||||
<wd-icon
|
||||
name="lock-on"
|
||||
size="22"
|
||||
:color="isDarkMode ? '#7AC5FF' : '#333'"
|
||||
class="input-icon"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="loginFormData.password"
|
||||
class="form-input input-transparent"
|
||||
type="text"
|
||||
prop="password"
|
||||
show-password
|
||||
placeholder="请输入密码"
|
||||
clear-trigger="focus"
|
||||
clearable
|
||||
:rules="[{ required: true, message: '请输入密码', trigger: 'blur' }]"
|
||||
/>
|
||||
<wd-icon name="lock-on" size="22" :color="themeVars.darkColor3" class="input-icon" />
|
||||
<wd-input v-model="loginFormData.password" class="form-input input-transparent" type="text" prop="password"
|
||||
show-password placeholder="请输入密码" clear-trigger="focus" clearable
|
||||
:rules="[{ required: true, message: '请输入密码', trigger: 'blur' }]" />
|
||||
</view>
|
||||
|
||||
<!-- <wd-divider /> -->
|
||||
|
||||
<!-- 验证码输入框 -->
|
||||
<view v-if="captchaState.enable" class="form-item">
|
||||
<wd-icon
|
||||
name="lock-on"
|
||||
size="22"
|
||||
:color="isDarkMode ? '#7AC5FF' : '#333'"
|
||||
class="input-icon"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="loginFormData.captcha"
|
||||
class="form-input input-transparent captcha-input"
|
||||
type="text"
|
||||
prop="captcha"
|
||||
placeholder="请输入验证码"
|
||||
clear-trigger="focus"
|
||||
clearable
|
||||
:rules="[{ required: true, message: '请输入验证码', trigger: 'blur' }]"
|
||||
/>
|
||||
<wd-img
|
||||
:src="captchaState.img_base"
|
||||
class="captcha-image"
|
||||
mode="aspectFit"
|
||||
@click="getLoginCaptcha"
|
||||
/>
|
||||
<view
|
||||
v-if="!captchaState.img_base && captchaState.enable"
|
||||
class="captcha-placeholder"
|
||||
@click="getLoginCaptcha"
|
||||
>
|
||||
<wd-icon name="lock-on" size="22" :color="themeVars.darkColor3" class="input-icon" />
|
||||
<wd-input v-model="loginFormData.captcha" class="form-input input-transparent captcha-input" type="text"
|
||||
prop="captcha" placeholder="请输入验证码" clear-trigger="focus" clearable
|
||||
:rules="[{ required: true, message: '请输入验证码', trigger: 'blur' }]" />
|
||||
<wd-img :src="captchaState.img_base" class="captcha-image" mode="aspectFit" @click="getLoginCaptcha" />
|
||||
<view v-if="!captchaState.img_base && captchaState.enable" class="captcha-placeholder"
|
||||
@click="getLoginCaptcha">
|
||||
<text>点击加载</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- <wd-divider /> -->
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<view class="footer">
|
||||
<wd-button
|
||||
class="login-btn"
|
||||
type="primary"
|
||||
size="large"
|
||||
:disabled="loading || !isFormValid"
|
||||
:style="{ opacity: loading || !isFormValid ? 0.7 : 1 }"
|
||||
block
|
||||
@click="handleAccountLogin"
|
||||
>
|
||||
<wd-button class="login-btn" type="primary" size="large" :disabled="loading || !isFormValid"
|
||||
:style="{ opacity: loading || !isFormValid ? 0.7 : 1 }" block @click="handleAccountLogin">
|
||||
<wd-loading v-if="loading" size="20" color="#fff" />
|
||||
{{ loading ? "登录中..." : "账号登录" }}
|
||||
</wd-button>
|
||||
@@ -111,31 +53,26 @@
|
||||
|
||||
<!-- 切换登录方式 -->
|
||||
<view class="switch-login-type" @click="loginType = 'phone'">
|
||||
<text>使用手机号一键登录</text>
|
||||
<wd-icon name="arrow-right" size="12" />
|
||||
<text class="text-primary">使用手机号一键登录</text>
|
||||
<wd-icon name="arrow-right" size="12" :color="themeVars.colorTheme" />
|
||||
</view>
|
||||
</wd-form>
|
||||
|
||||
<!-- 手机号登录 -->
|
||||
<view v-else class="phone-login-form">
|
||||
<view class="phone-login-title">微信一键登录</view>
|
||||
<view class="phone-login-subtitle">授权后将获取您的手机号</view>
|
||||
<view class="phone-login-title theme-text-primary">微信一键登录</view>
|
||||
<view class="phone-login-subtitle theme-text-secondary">授权后将获取您的手机号</view>
|
||||
|
||||
<wd-button
|
||||
class="wechat-phone-btn"
|
||||
:disabled="loading"
|
||||
open-type="getPhoneNumber"
|
||||
size="large"
|
||||
@getphonenumber="handleWechatPhoneLogin"
|
||||
>
|
||||
<wd-button class="wechat-phone-btn" :disabled="loading" open-type="getPhoneNumber" size="large"
|
||||
@getphonenumber="handleWechatPhoneLogin">
|
||||
<wd-icon name="weixin" size="24" color="#ffffff" />
|
||||
<text>微信一键登录</text>
|
||||
</wd-button>
|
||||
|
||||
<!-- 切换登录方式 -->
|
||||
<view class="switch-login-type" @click="loginType = 'account'">
|
||||
<text>使用账号密码登录</text>
|
||||
<wd-icon name="arrow-right" size="12" />
|
||||
<text class="text-primary">使用账号密码登录</text>
|
||||
<wd-icon name="arrow-right" size="12" :color="themeVars.colorTheme" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -143,7 +80,7 @@
|
||||
<view class="other-login">
|
||||
<view class="other-login-title">
|
||||
<view class="line"></view>
|
||||
<text class="text">其他登录方式</text>
|
||||
<text class="text theme-text-muted">其他登录方式</text>
|
||||
<view class="line"></view>
|
||||
</view>
|
||||
|
||||
@@ -157,9 +94,9 @@
|
||||
<!-- 底部协议 -->
|
||||
<view class="agreement">
|
||||
<wd-checkbox v-model="loginFormData.remember">
|
||||
<wd-text text="我已阅读并同意"></wd-text>
|
||||
<wd-text text="我已阅读并同意" class="theme-text-secondary"></wd-text>
|
||||
<wd-text type="primary" text="《用户协议》" @click="navigateToUserAgreement"></wd-text>
|
||||
<wd-text text="和"></wd-text>
|
||||
<wd-text text="和" class="theme-text-secondary"></wd-text>
|
||||
<wd-text type="primary" text="《隐私政策》" @click="navigateToPrivacy"></wd-text>
|
||||
</wd-checkbox>
|
||||
</view>
|
||||
@@ -177,14 +114,15 @@ import { useToast } from "wot-design-uni";
|
||||
import AuthAPI, { type LoginFormData, type CaptchaInfo } from "@/api/auth";
|
||||
import { useThemeStore } from "@/store/modules/theme.store";
|
||||
|
||||
const useTheme = useThemeStore();
|
||||
// 主题相关
|
||||
const themeStore = useThemeStore();
|
||||
const themeVars = themeStore.themeVars;
|
||||
|
||||
const loginFormRef = ref();
|
||||
const toast = useToast();
|
||||
const loading = ref(false);
|
||||
const userStore = useUserStore();
|
||||
const loginType = ref<"account" | "phone">("account");
|
||||
let theme = useTheme.theme;
|
||||
|
||||
// 登录表单数据
|
||||
const loginFormData = ref<LoginFormData>({
|
||||
@@ -282,8 +220,6 @@ const handleWechatLogin = async () => {
|
||||
toast.error("微信授权登录功能开发中...");
|
||||
};
|
||||
|
||||
// 是否暗黑模式
|
||||
const isDarkMode = computed(() => theme === "dark");
|
||||
|
||||
// 导航函数
|
||||
const navigateToUserAgreement = () => {
|
||||
@@ -316,21 +252,21 @@ onLoad((options) => {
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<route lang="json">
|
||||
{
|
||||
|
||||
<route lang="json">{
|
||||
"name": "login",
|
||||
"style": {
|
||||
"navigationBarTitleText": "登录"
|
||||
}
|
||||
}
|
||||
</route>
|
||||
}</route>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
overflow: hidden;
|
||||
background-color: var(--wot-color-bg-container);
|
||||
@@ -349,13 +285,12 @@ onLoad((options) => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-top: 80rpx;
|
||||
margin-top: 30rpx;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 140rpx;
|
||||
height: 140rpx;
|
||||
margin-bottom: 20rpx;
|
||||
width: 250rpx;
|
||||
height: 250rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
@@ -377,8 +312,8 @@ onLoad((options) => {
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 90%;
|
||||
margin-top: 80rpx;
|
||||
width: 95%;
|
||||
margin-top: 30rpx;
|
||||
overflow: hidden;
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
@@ -503,7 +438,7 @@ input.input-transparent {
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
margin-top: 60rpx;
|
||||
margin-top: 30rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
line-height: 88rpx;
|
||||
@@ -518,7 +453,7 @@ input.input-transparent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 30rpx;
|
||||
margin-top: 60rpx;
|
||||
font-size: 26rpx;
|
||||
color: var(--wot-color-theme);
|
||||
}
|
||||
|
||||
@@ -1,81 +1,52 @@
|
||||
<template>
|
||||
<view class="app-container dark:text-[var(--wot-dark-color)]">
|
||||
<wd-card custom-style="margin-top: 20rpx">
|
||||
<view class="about-header">
|
||||
<wd-img
|
||||
width="120rpx"
|
||||
height="120rpx"
|
||||
src="./static/logo.png"
|
||||
mode="aspectFit"
|
||||
class="about-logo"
|
||||
/>
|
||||
<view class="about-meta">
|
||||
<text class="app-title">{{ webTitle }}</text>
|
||||
<text class="app-version">版本 {{ version }}</text>
|
||||
<view class="app-container theme-adaptive">
|
||||
<wd-card custom-style="margin-top: 20rpx" class="theme-card">
|
||||
<view class="company-section flex-center flex-column">
|
||||
<view class="about-meta text-center">
|
||||
<text class="app-title theme-text-primary">{{ webTitle }}-{{ version }}</text>
|
||||
<text class="section-subtitle theme-text-secondary">{{ webDescription }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</wd-card>
|
||||
|
||||
<!-- 公司信息区域 -->
|
||||
<wd-card title="项目概述" custom-style="margin-top: 20rpx">
|
||||
<view class="company-section">
|
||||
<view class="section-title">
|
||||
<wd-icon name="home" size="18" />
|
||||
<text>{{ webTitle }}</text>
|
||||
</view>
|
||||
<text class="section-subtitle">{{ webDescription }}</text>
|
||||
</view>
|
||||
</wd-card>
|
||||
|
||||
<!-- 项目列表 -->
|
||||
<wd-card title="技术站" custom-style="margin-top: 20rpx">
|
||||
<wd-card title="技术栈" custom-style="margin-top: 20rpx" class="theme-card">
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="后台" icon="desktop">
|
||||
<view slot="label" class="project-desc">
|
||||
基于 FastAPI + Uvicorn + SQLAlchemy 构建的RBAC管理系统
|
||||
<view slot="label" class="project-desc theme-text-secondary">基于 FastAPI + Uvicorn + SQLAlchemy 构建的RBAC管理系统
|
||||
</view>
|
||||
</wd-cell>
|
||||
|
||||
<wd-cell title="web前端" icon="mobile">
|
||||
<view slot="label" class="project-desc">
|
||||
基于 Vue3 + Vite6 + TypeScript + Element-Plus + Pinia 构建的中后台管理模板
|
||||
</view>
|
||||
<view slot="label" class="project-desc theme-text-secondary">基于 Vue3 + Vite6 + TypeScript + Element-Plus +
|
||||
Pinia 构建的中后台管理模板</view>
|
||||
</wd-cell>
|
||||
|
||||
<wd-cell title="移动端" icon="server" label="">
|
||||
<view slot="label" class="project-desc">
|
||||
基于 uni-app + Vite6 + Vue 3 + TypeScript + Wot Design Uni + Pinia 构建的移动端应用模板
|
||||
</view>
|
||||
<view slot="label" class="project-desc theme-text-secondary">基于 uni-app + Vite6 + Vue 3 + TypeScript + Wot
|
||||
Design Uni + Pinia
|
||||
构建的移动端应用模板</view>
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
</wd-card>
|
||||
|
||||
<!-- 联系方式 -->
|
||||
<wd-card title="联系我们" custom-style="margin-top: 20rpx">
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="联系邮箱" value="948080782@qq.com" icon="mail" ellipsis />
|
||||
<wd-cell title="官方网站" :value="helpDoc" is-link :to="helpDoc" icon="link" ellipsis />
|
||||
<wd-cell title="源码地址" :value="gitCode" is-link :to="gitCode" icon="github" ellipsis />
|
||||
</wd-cell-group>
|
||||
</wd-card>
|
||||
|
||||
<!-- 底部版权信息 -->
|
||||
<view class="about-footer">
|
||||
<text class="copyright">{{ copyright }}</text>
|
||||
<text class="copyright copyright-sub">{{ keepRecord }}</text>
|
||||
<view class="about-footer text-center">
|
||||
<text class="copyright theme-text-secondary">{{ copyright }}</text>
|
||||
<text class="copyright copyright-sub theme-text-secondary">{{ keepRecord }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
// 系统配置数据
|
||||
const webTitle = ref("FastAPI Vue3 Admin");
|
||||
const webDescription = ref("FastAPI Vue3 Admin 是完全开源的权限管理系统");
|
||||
const version = ref("2.0.0");
|
||||
const webDescription = ref("完全开源的权限管理系统");
|
||||
const version = ref("V2.0.0");
|
||||
const keepRecord = ref("陕ICP备2025069493号-1");
|
||||
const copyright = ref("Copyright © 2025-2026 service.fastapiadmin.com 版权所有");
|
||||
const helpDoc = ref("http://service.fastapiadmin.com");
|
||||
const gitCode = ref("https://github.com/1014TaoTao/fastapi_vue3_admin.git");
|
||||
|
||||
onMounted(() => {
|
||||
// #ifdef MP-WEIXIN
|
||||
@@ -84,40 +55,30 @@ onMounted(() => {
|
||||
});
|
||||
</script>
|
||||
|
||||
<route lang="json">
|
||||
{
|
||||
<route lang="json">{
|
||||
"name": "about",
|
||||
"style": {
|
||||
"navigationBarTitleText": "关于我们"
|
||||
}
|
||||
}
|
||||
</route>
|
||||
}</route>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.about-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.about-logo {
|
||||
margin-right: 16rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.about-meta {
|
||||
flex: 1;
|
||||
}
|
||||
.app-title {
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.app-title {
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.app-version {
|
||||
font-size: 24rpx;
|
||||
color: #6b7280;
|
||||
.app-version {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.company-section {
|
||||
@@ -125,39 +86,32 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
|
||||
:deep(.wd-icon) {
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.section-subtitle {
|
||||
display: block;
|
||||
padding-left: 32rpx;
|
||||
font-size: 24rpx;
|
||||
color: #6b7280;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.project-desc {
|
||||
margin-top: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: #6b7280;
|
||||
font-size: 26rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.about-footer {
|
||||
padding: 32rpx 0;
|
||||
text-align: center;
|
||||
padding: 40rpx 20rpx;
|
||||
}
|
||||
|
||||
.copyright {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: #9ca3af;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.copyright-sub {
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
<template>
|
||||
<view class="faq-container">
|
||||
<view class="app-container theme-adaptive">
|
||||
<scroll-view class="content-scroll" scroll-y :scroll-top="scrollTop" @scroll="handleScroll">
|
||||
<view class="content-wrapper">
|
||||
<!-- 技术支持卡片 -->
|
||||
<view class="support-card">
|
||||
<view class="card-header">
|
||||
<wd-icon name="service" size="48rpx" color="#0083f0" />
|
||||
<text class="card-title">技术支持</text>
|
||||
<view class="support-card theme-card">
|
||||
<view class="card-header flex items-center">
|
||||
<wd-icon name="service" size="48rpx" :color="themeVars.colorTheme" />
|
||||
<text class="card-title theme-text-primary ml-md">技术支持</text>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<text class="card-desc">专业的技术团队为您提供7×24小时服务支持</text>
|
||||
<text class="card-desc theme-text-secondary">专业的技术团队为您提供7×24小时服务支持</text>
|
||||
<view class="support-grid">
|
||||
<view class="support-item" @tap="handleContact('email')">
|
||||
<view class="support-icon">
|
||||
<wd-icon name="mail" size="40rpx" color="#0083f0" />
|
||||
<view class="support-item theme-card" @tap="handleContact('email')">
|
||||
<view class="support-icon flex-center">
|
||||
<wd-icon name="mail" size="40rpx" :color="themeVars.colorTheme" />
|
||||
</view>
|
||||
<view class="support-info">
|
||||
<text class="support-label">邮箱支持</text>
|
||||
<text class="support-value" user-select>support@example.com</text>
|
||||
<text class="support-label theme-text-primary">邮箱支持</text>
|
||||
<text class="support-value theme-text-secondary" user-select>support@example.com</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="support-item" @tap="handleContact('phone')">
|
||||
<view class="support-icon">
|
||||
<wd-icon name="phone" size="40rpx" color="#0083f0" />
|
||||
<view class="support-item theme-card" @tap="handleContact('phone')">
|
||||
<view class="support-icon flex-center">
|
||||
<wd-icon name="phone" size="40rpx" :color="themeVars.colorTheme" />
|
||||
</view>
|
||||
<view class="support-info">
|
||||
<text class="support-label">客服热线</text>
|
||||
<text class="support-value">400-123-4567</text>
|
||||
<text class="support-label theme-text-primary">客服热线</text>
|
||||
<text class="support-value theme-text-secondary">400-123-4567</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="support-item">
|
||||
<view class="support-icon">
|
||||
<wd-icon name="clock" size="40rpx" color="#0083f0" />
|
||||
<view class="support-item theme-card">
|
||||
<view class="support-icon flex-center">
|
||||
<wd-icon name="clock" size="40rpx" :color="themeVars.colorTheme" />
|
||||
</view>
|
||||
<view class="support-info">
|
||||
<text class="support-label">服务时间</text>
|
||||
<text class="support-value">工作日 9:00-18:00</text>
|
||||
<text class="support-label theme-text-primary">服务时间</text>
|
||||
<text class="support-value theme-text-secondary">工作日 9:00-18:00</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -43,31 +43,22 @@
|
||||
</view>
|
||||
|
||||
<!-- FAQ内容区域 -->
|
||||
<view class="faq-section">
|
||||
<view class="faq-section theme-card mt-md">
|
||||
<wd-collapse v-model="activeNames" accordion>
|
||||
<wd-collapse-item title="常见问题" name="faq">
|
||||
<view class="faq-list">
|
||||
<view
|
||||
v-for="(item, index) in faqList"
|
||||
:key="index"
|
||||
class="faq-item"
|
||||
:class="{ active: currentFaq === index }"
|
||||
@tap="toggleFaq(index)"
|
||||
>
|
||||
<view class="faq-header">
|
||||
<view class="faq-question">
|
||||
<wd-icon name="question-filled" size="28rpx" color="#ff9500" />
|
||||
<text class="question-text">{{ item.question }}</text>
|
||||
<view v-for="(item, index) in faqList" :key="index" class="faq-item theme-card"
|
||||
:class="{ active: currentFaq === index }" @tap="toggleFaq(index)">
|
||||
<view class="faq-header flex justify-between items-center">
|
||||
<view class="faq-question flex items-center">
|
||||
<wd-icon name="question-filled" size="28rpx" :color="themeVars.colorTheme" />
|
||||
<text class="question-text theme-text-primary ml-sm">{{ item.question }}</text>
|
||||
</view>
|
||||
<wd-icon
|
||||
name="arrow-down-bold"
|
||||
size="24rpx"
|
||||
color="#999"
|
||||
:custom-class="currentFaq === index ? 'rotate-180' : ''"
|
||||
/>
|
||||
<wd-icon name="arrow-down-bold" size="24rpx" :color="themeVars.darkColor3"
|
||||
:custom-class="currentFaq === index ? 'rotate-180' : ''" />
|
||||
</view>
|
||||
<view v-if="currentFaq === index" class="faq-answer">
|
||||
<text class="answer-text">{{ item.answer }}</text>
|
||||
<text class="answer-text theme-text-secondary">{{ item.answer }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -75,13 +66,13 @@
|
||||
|
||||
<wd-collapse-item title="系统功能" name="features">
|
||||
<view class="feature-list">
|
||||
<view v-for="(feature, index) in featureList" :key="index" class="feature-item">
|
||||
<view class="feature-icon">
|
||||
<wd-icon :name="feature.icon" size="48rpx" :color="feature.color" />
|
||||
<view v-for="(feature, index) in featureList" :key="index" class="feature-item theme-card">
|
||||
<view class="feature-icon flex-center">
|
||||
<wd-icon :name="feature.icon" size="48rpx" :color="themeVars.colorTheme" />
|
||||
</view>
|
||||
<view class="feature-content">
|
||||
<text class="feature-name">{{ feature.name }}</text>
|
||||
<text class="feature-desc">{{ feature.desc }}</text>
|
||||
<text class="feature-name theme-text-primary">{{ feature.name }}</text>
|
||||
<text class="feature-desc theme-text-secondary">{{ feature.desc }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -89,25 +80,27 @@
|
||||
|
||||
<wd-collapse-item title="使用指南" name="guide">
|
||||
<view class="guide-section">
|
||||
<view class="guide-header">
|
||||
<wd-icon name="light" size="44rpx" color="#ff9500" />
|
||||
<text class="guide-title">快速开始</text>
|
||||
<view class="guide-header flex-center">
|
||||
<wd-icon name="light" size="44rpx" :color="themeVars.colorTheme" />
|
||||
<text class="guide-title theme-text-primary ml-md">快速开始</text>
|
||||
</view>
|
||||
<view class="guide-steps">
|
||||
<view v-for="(step, index) in guideSteps" :key="index" class="guide-step">
|
||||
<view class="step-indicator">
|
||||
<view class="step-number">{{ index + 1 }}</view>
|
||||
<view class="step-number flex-center">{{ index + 1 }}</view>
|
||||
<view v-if="index < guideSteps.length - 1" class="step-line"></view>
|
||||
</view>
|
||||
<view class="step-content">
|
||||
<text class="step-title">{{ step.title }}</text>
|
||||
<text v-if="step.desc" class="step-desc">{{ step.desc }}</text>
|
||||
<text class="step-title theme-text-primary">{{ step.title }}</text>
|
||||
<text v-if="step.desc" class="step-desc theme-text-secondary">
|
||||
{{ step.desc }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="guide-footer">
|
||||
<wd-icon name="tips" size="28rpx" color="#999" />
|
||||
<text class="footer-text">详细操作手册请查看帮助文档或联系客服获取</text>
|
||||
<view class="guide-footer theme-card flex items-center">
|
||||
<wd-icon name="tips" size="28rpx" :color="themeVars.darkColor3" />
|
||||
<text class="footer-text theme-text-secondary ml-sm">详细操作手册请查看帮助文档或联系客服获取</text>
|
||||
</view>
|
||||
</view>
|
||||
</wd-collapse-item>
|
||||
@@ -120,6 +113,10 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from "vue";
|
||||
import { useThemeStore } from "@/store/modules/theme.store";
|
||||
|
||||
const themeStore = useThemeStore();
|
||||
const themeVars = themeStore.themeVars;
|
||||
|
||||
// 响应式数据
|
||||
const activeNames = ref<string[]>(["faq"]);
|
||||
@@ -130,23 +127,27 @@ const scrollTop = ref<number>(0);
|
||||
const faqList = ref([
|
||||
{
|
||||
question: "如何重置密码?",
|
||||
answer:
|
||||
"在登录页面点击 '忘记密码',按照提示操作即可重置密码。如果无法收到验证码,请检查邮箱或手机号是否正确,或联系客服协助处理。",
|
||||
answer: "在登录页面点击 '忘记密码',按照提示操作即可重置密码。如果无法收到验证码,请检查邮箱或手机号是否正确,或联系客服协助处理。建议定期修改密码以保障账户安全。",
|
||||
},
|
||||
{
|
||||
question: "数据如何备份?",
|
||||
answer:
|
||||
"系统会自动定期备份数据,您也可以在'系统设置-数据管理'中手动导出数据。建议每月进行一次完整数据备份,确保数据安全。",
|
||||
answer: "系统会自动定期备份数据,您也可以在'系统设置-数据管理'中手动导出数据。建议每月进行一次完整数据备份,确保数据安全。重要数据请务必做好多重备份。",
|
||||
},
|
||||
{
|
||||
question: "支持哪些浏览器?",
|
||||
answer:
|
||||
"推荐使用Chrome 90+、Firefox 88+、Safari 14+、Edge 90+等现代浏览器。IE浏览器仅支持IE11及以上版本。",
|
||||
answer: "推荐使用Chrome 90+、Firefox 88+、Safari 14+、Edge 90+等现代浏览器。IE浏览器仅支持IE11及以上版本。移动端建议使用最新版本的微信、QQ浏览器等。",
|
||||
},
|
||||
{
|
||||
question: "如何联系客服?",
|
||||
answer:
|
||||
"您可以通过以下方式联系客服:1. 客服热线 400-123-4567(工作日9:00-18:00);2. 邮箱 support@example.com(7×24小时);3. 在线客服(工作日9:00-18:00)。",
|
||||
answer: "您可以通过以下方式联系客服:1. 客服热线 400-123-4567(工作日9:00-18:00);2. 邮箱 support@example.com(7×24小时);3. 在线客服(工作日9:00-18:00)。我们将尽快回复您的问题。",
|
||||
},
|
||||
{
|
||||
question: "如何反馈问题?",
|
||||
answer: "您可以通过'我的-问题反馈'页面提交问题,详细描述遇到的问题并上传相关截图。我们的技术团队会在24小时内处理您的反馈。您也可以通过客服热线直接反馈问题。",
|
||||
},
|
||||
{
|
||||
question: "如何更新应用?",
|
||||
answer: "应用会自动检测更新,有新版本时会提示您更新。您也可以在应用商店中手动检查更新。建议及时更新到最新版本以获得更好的使用体验和安全保障。",
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -154,47 +155,63 @@ const faqList = ref([
|
||||
const featureList = ref([
|
||||
{
|
||||
name: "用户管理",
|
||||
desc: "支持用户注册、登录、权限管理等功能",
|
||||
desc: "支持用户注册、登录、权限管理等功能,提供完善的用户体系",
|
||||
icon: "user",
|
||||
color: "#0083f0",
|
||||
},
|
||||
{
|
||||
name: "数据统计",
|
||||
desc: "提供实时数据分析和可视化报表",
|
||||
desc: "提供实时数据分析和可视化报表,帮助您更好地了解业务状况",
|
||||
icon: "chart",
|
||||
color: "#00c250",
|
||||
},
|
||||
{
|
||||
name: "文件管理",
|
||||
desc: "支持文件上传、下载、分类管理",
|
||||
desc: "支持文件上传、下载、分类管理,提供安全的文件存储服务",
|
||||
icon: "folder",
|
||||
color: "#ff9500",
|
||||
},
|
||||
{
|
||||
name: "消息通知",
|
||||
desc: "实时消息推送和系统通知",
|
||||
desc: "实时消息推送和系统通知,确保您不会错过重要信息",
|
||||
icon: "notification",
|
||||
color: "#ff3b30",
|
||||
},
|
||||
{
|
||||
name: "权限控制",
|
||||
desc: "基于RBAC的权限管理模型,灵活控制用户访问权限",
|
||||
icon: "secured",
|
||||
color: "#5a5a5a",
|
||||
},
|
||||
{
|
||||
name: "日志审计",
|
||||
desc: "完整的操作日志记录,便于追踪和审计用户行为",
|
||||
icon: "file",
|
||||
color: "#8a8a8a",
|
||||
},
|
||||
]);
|
||||
|
||||
// 使用指南数据
|
||||
const guideSteps = ref([
|
||||
{
|
||||
title: "注册账号并登录系统",
|
||||
desc: "使用手机号或邮箱注册账号,完成实名认证",
|
||||
desc: "使用手机号或邮箱注册账号,完成实名认证,设置安全密码",
|
||||
},
|
||||
{
|
||||
title: "完善个人或企业信息",
|
||||
desc: "填写基本信息,设置安全问题和密保邮箱",
|
||||
desc: "填写基本信息,设置安全问题和密保邮箱,绑定手机号",
|
||||
},
|
||||
{
|
||||
title: "根据需求配置功能模块",
|
||||
desc: "选择需要的功能模块,设置相关参数",
|
||||
desc: "选择需要的功能模块,设置相关参数,配置权限角色",
|
||||
},
|
||||
{
|
||||
title: "开始使用各项功能",
|
||||
desc: "完成初始化设置,开始使用系统各项功能",
|
||||
desc: "完成初始化设置,开始使用系统各项功能,定期备份数据",
|
||||
},
|
||||
{
|
||||
title: "获取技术支持",
|
||||
desc: "遇到问题时可通过客服、FAQ或在线反馈获取技术支持",
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -239,337 +256,270 @@ onMounted(() => {
|
||||
// 可以在这里添加页面埋点或初始化逻辑
|
||||
});
|
||||
</script>
|
||||
<route lang="json">
|
||||
{
|
||||
|
||||
<route lang="json">{
|
||||
"name": "faq",
|
||||
"style": {
|
||||
"navigationBarTitleText": "常见问题"
|
||||
}
|
||||
}
|
||||
</route>
|
||||
}</route>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.faq-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.content-scroll {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
background: var(--wot-color-bg);
|
||||
}
|
||||
|
||||
.content-scroll {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
.content-wrapper {
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
|
||||
// 技术支持卡片样式
|
||||
.support-card {
|
||||
.card-header {
|
||||
padding: var(--spacing-xl) var(--spacing-xl) var(--spacing-lg);
|
||||
border-bottom: 1rpx solid var(--border-color);
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
padding: 24rpx;
|
||||
.card-title {
|
||||
font-size: var(--font-lg);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
// 技术支持卡片样式
|
||||
.support-card {
|
||||
margin-bottom: 24rpx;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04);
|
||||
.card-body {
|
||||
padding: var(--spacing-xl);
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 32rpx 32rpx 24rpx;
|
||||
border-bottom: 1rpx solid var(--wot-color-border);
|
||||
|
||||
.card-title {
|
||||
margin-left: 16rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: var(--wot-color-text);
|
||||
}
|
||||
.card-desc {
|
||||
display: block;
|
||||
margin-bottom: var(--spacing-xl);
|
||||
font-size: var(--font-md);
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 32rpx;
|
||||
.support-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--spacing-lg);
|
||||
|
||||
.card-desc {
|
||||
display: block;
|
||||
margin-bottom: 32rpx;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.5;
|
||||
color: var(--wot-color-text-light);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.support-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 24rpx;
|
||||
|
||||
.support-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 32rpx;
|
||||
background: var(--wot-color-bg);
|
||||
border-radius: 16rpx;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:active {
|
||||
opacity: 0.8;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.support-icon {
|
||||
flex-shrink: 0;
|
||||
margin-right: 24rpx;
|
||||
}
|
||||
|
||||
.support-info {
|
||||
flex: 1;
|
||||
|
||||
.support-label {
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: var(--wot-color-text);
|
||||
}
|
||||
|
||||
.support-value {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: var(--wot-color-text-light);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FAQ区域样式
|
||||
.faq-section {
|
||||
:deep(.wd-collapse-item__title) {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:deep(.wd-collapse-item__content) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.faq-list {
|
||||
.faq-item {
|
||||
border-bottom: 1rpx solid var(--wot-color-border);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: rgba(0, 131, 240, 0.02);
|
||||
}
|
||||
|
||||
.faq-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 32rpx;
|
||||
|
||||
.faq-question {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
margin-right: 24rpx;
|
||||
|
||||
.question-text {
|
||||
margin-left: 16rpx;
|
||||
font-size: 30rpx;
|
||||
color: var(--wot-color-text);
|
||||
}
|
||||
}
|
||||
|
||||
.wd-icon {
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
&.rotate-180 {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.faq-answer {
|
||||
padding: 0 32rpx 32rpx 32rpx;
|
||||
border-top: 1rpx solid var(--wot-color-border);
|
||||
|
||||
.answer-text {
|
||||
display: block;
|
||||
padding: 24rpx 0;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.6;
|
||||
color: var(--wot-color-text-light);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.feature-list {
|
||||
.feature-item {
|
||||
.support-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 32rpx;
|
||||
border-bottom: 1rpx solid var(--wot-color-border);
|
||||
padding: var(--spacing-xl);
|
||||
border-radius: var(--radius-lg);
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
&:active {
|
||||
opacity: 0.8;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
.support-icon {
|
||||
flex-shrink: 0;
|
||||
margin-right: 24rpx;
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
margin-right: var(--spacing-lg);
|
||||
background-color: var(--bg-color-2);
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.feature-content {
|
||||
.support-info {
|
||||
flex: 1;
|
||||
|
||||
.feature-name {
|
||||
.support-label {
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 30rpx;
|
||||
margin-bottom: var(--spacing-xs);
|
||||
font-size: var(--font-md);
|
||||
font-weight: 600;
|
||||
color: var(--wot-color-text);
|
||||
}
|
||||
|
||||
.feature-desc {
|
||||
.support-value {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
line-height: 1.4;
|
||||
color: var(--wot-color-text-light);
|
||||
font-size: var(--font-sm);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.guide-section {
|
||||
padding: 32rpx;
|
||||
// FAQ区域样式
|
||||
.faq-section {
|
||||
:deep(.wd-collapse-item__title) {
|
||||
font-size: var(--font-lg);
|
||||
font-weight: 600;
|
||||
padding: var(--spacing-lg) var(--spacing-xl);
|
||||
}
|
||||
|
||||
.guide-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 48rpx;
|
||||
:deep(.wd-collapse-item__content) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.guide-title {
|
||||
margin-left: 16rpx;
|
||||
font-size: 32rpx;
|
||||
.faq-list {
|
||||
.faq-item {
|
||||
border-bottom: 1rpx solid var(--border-color);
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: rgba(var(--primary-color-rgb), 0.02);
|
||||
}
|
||||
|
||||
.faq-header {
|
||||
padding: var(--spacing-xl);
|
||||
|
||||
.faq-question {
|
||||
flex: 1;
|
||||
margin-right: var(--spacing-lg);
|
||||
|
||||
.question-text {
|
||||
font-size: var(--font-md);
|
||||
}
|
||||
}
|
||||
|
||||
.wd-icon {
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
&.rotate-180 {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.faq-answer {
|
||||
padding: 0 var(--spacing-xl) var(--spacing-xl);
|
||||
border-top: 1rpx solid var(--border-color);
|
||||
|
||||
.answer-text {
|
||||
display: block;
|
||||
padding: var(--spacing-lg) 0;
|
||||
font-size: var(--font-md);
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.feature-list {
|
||||
.feature-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--spacing-xl);
|
||||
border-bottom: 1rpx solid var(--border-color);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
flex-shrink: 0;
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
margin-right: var(--spacing-lg);
|
||||
background-color: var(--bg-color-2);
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.feature-content {
|
||||
flex: 1;
|
||||
|
||||
.feature-name {
|
||||
display: block;
|
||||
margin-bottom: var(--spacing-xs);
|
||||
font-size: var(--font-md);
|
||||
font-weight: 600;
|
||||
color: var(--wot-color-text);
|
||||
}
|
||||
|
||||
.feature-desc {
|
||||
display: block;
|
||||
font-size: var(--font-sm);
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.guide-steps {
|
||||
.guide-step {
|
||||
display: flex;
|
||||
margin-bottom: 48rpx;
|
||||
.guide-section {
|
||||
padding: var(--spacing-xl);
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.guide-header {
|
||||
margin-bottom: var(--spacing-xxl);
|
||||
|
||||
.step-indicator {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
margin-right: 24rpx;
|
||||
|
||||
.step-number {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
background: var(--wot-color-theme);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.step-line {
|
||||
position: absolute;
|
||||
top: 48rpx;
|
||||
left: 50%;
|
||||
width: 2rpx;
|
||||
height: 48rpx;
|
||||
background: var(--wot-color-border);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
.step-content {
|
||||
flex: 1;
|
||||
padding-top: 8rpx;
|
||||
|
||||
.step-title {
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: var(--wot-color-text);
|
||||
}
|
||||
|
||||
.step-desc {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
line-height: 1.4;
|
||||
color: var(--wot-color-text-light);
|
||||
}
|
||||
}
|
||||
}
|
||||
.guide-title {
|
||||
font-size: var(--font-lg);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.guide-footer {
|
||||
.guide-steps {
|
||||
.guide-step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 32rpx;
|
||||
margin-top: 48rpx;
|
||||
background: var(--wot-color-bg);
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: var(--spacing-xxl);
|
||||
|
||||
.footer-text {
|
||||
margin-left: 16rpx;
|
||||
font-size: 26rpx;
|
||||
color: var(--wot-color-text-light);
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.step-indicator {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
margin-right: var(--spacing-lg);
|
||||
|
||||
.step-number {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
font-size: var(--font-sm);
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
background: var(--primary-color);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.step-line {
|
||||
position: absolute;
|
||||
top: 48rpx;
|
||||
left: 50%;
|
||||
width: 2rpx;
|
||||
height: 48rpx;
|
||||
background: var(--border-color);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
.step-content {
|
||||
flex: 1;
|
||||
padding-top: var(--spacing-xs);
|
||||
|
||||
.step-title {
|
||||
display: block;
|
||||
margin-bottom: var(--spacing-xs);
|
||||
font-size: var(--font-md);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.step-desc {
|
||||
display: block;
|
||||
font-size: var(--font-sm);
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media screen and (min-width: 768px) {
|
||||
.faq-container {
|
||||
.content-wrapper {
|
||||
max-width: 750rpx;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.guide-footer {
|
||||
padding: var(--spacing-xl);
|
||||
margin-top: var(--spacing-xxl);
|
||||
border-radius: var(--radius-lg);
|
||||
|
||||
.support-card {
|
||||
.support-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
.footer-text {
|
||||
font-size: var(--font-sm);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 暗黑模式适配
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.faq-container {
|
||||
--wot-color-text: #f5f5f5;
|
||||
--wot-color-text-light: #969799;
|
||||
--wot-color-border: #3a3a3c;
|
||||
--wot-color-bg: #1c1c1e;
|
||||
|
||||
.support-card,
|
||||
.faq-section :deep(.wd-collapse) {
|
||||
color: var(--wot-color-text);
|
||||
background: #2c2c2e;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -1,49 +1,42 @@
|
||||
<template>
|
||||
<view class="app-container">
|
||||
<wd-text size="small">选填,最多上传3张图片</wd-text>
|
||||
<view class="app-container theme-adaptive">
|
||||
<wd-text size="small" class="theme-text-secondary">选填,最多上传3张图片</wd-text>
|
||||
<wd-form ref="formRef" :model="formData" :rules="rules" errorType="toast">
|
||||
<!-- 问题类型选择 -->
|
||||
<wd-cell-group title="问题类型" border>
|
||||
<wd-radio-group v-model="formData.feedbackType" inline prop="feedbackType">
|
||||
<wd-radio v-for="item in feedbackTypes" :key="item.value" :value="item.value">
|
||||
<wd-cell-group title="问题类型" custom-class="theme-card mt-md">
|
||||
<wd-radio-group v-model="formData.feedbackType" inline prop="feedbackType" custom-class="p-md">
|
||||
<wd-radio v-for="item in feedbackTypes" :key="item.value" :value="item.value" custom-class="mb-sm">
|
||||
{{ item.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-cell-group>
|
||||
|
||||
<wd-textarea
|
||||
v-model="formData.description"
|
||||
label="问题描述"
|
||||
prop="description"
|
||||
placeholder="请详细描述您遇到的问题或建议..."
|
||||
:maxlength="120"
|
||||
show-word-limit
|
||||
/>
|
||||
<!-- 问题描述 -->
|
||||
<wd-cell-group title="问题描述" custom-class="theme-card mt-md">
|
||||
<wd-textarea v-model="formData.description" prop="description" placeholder="请详细描述您遇到的问题或建议..."
|
||||
:maxlength="300" show-word-limit custom-class="p-md" />
|
||||
</wd-cell-group>
|
||||
|
||||
<wd-upload
|
||||
v-model="formData.fileList"
|
||||
label="相关截图"
|
||||
prop="fileList"
|
||||
:max-count="3"
|
||||
:before-read="beforeRead"
|
||||
@delete="handleDelete"
|
||||
/>
|
||||
<!-- 相关截图 -->
|
||||
<wd-cell-group title="相关截图" custom-class="theme-card mt-md">
|
||||
<view class="p-md">
|
||||
<wd-text size="small" custom-class="theme-text-secondary mb-sm">选填,最多上传3张图片</wd-text>
|
||||
<wd-upload v-model="formData.fileList" :max-count="3" :before-read="beforeRead"
|
||||
@delete="handleDelete" />
|
||||
</view>
|
||||
</wd-cell-group>
|
||||
|
||||
<wd-input
|
||||
v-model="formData.contact"
|
||||
label="联系方式"
|
||||
prop="contact"
|
||||
placeholder="请输入您的手机号或邮箱"
|
||||
clearable
|
||||
:border="false"
|
||||
/>
|
||||
<wd-text size="small">选填,便于我们与您联系</wd-text>
|
||||
<!-- 联系方式 -->
|
||||
<wd-cell-group title="联系方式" custom-class="theme-card mt-md">
|
||||
<view class="p-md">
|
||||
<wd-input v-model="formData.contact" prop="contact" placeholder="请输入您的手机号或邮箱" clearable />
|
||||
<wd-text size="small" custom-class="theme-text-secondary mt-sm">选填,便于我们与您联系</wd-text>
|
||||
</view>
|
||||
</wd-cell-group>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<view class="footer">
|
||||
<wd-button type="primary" size="large" :loading="submitting" block @click="handleSubmit">
|
||||
提交反馈
|
||||
</wd-button>
|
||||
<view class="footer theme-card mt-md">
|
||||
<wd-button type="primary" size="large" :loading="submitting" block custom-class="submit-btn" @click="handleSubmit">提交反馈</wd-button>
|
||||
</view>
|
||||
</wd-form>
|
||||
</view>
|
||||
@@ -79,6 +72,13 @@ const formData = reactive({
|
||||
|
||||
// 表单验证规则
|
||||
const rules: FormRules = {
|
||||
feedbackType: [
|
||||
{
|
||||
required: true,
|
||||
message: "请选择问题类型",
|
||||
trigger: "change",
|
||||
},
|
||||
],
|
||||
description: [
|
||||
{
|
||||
required: true,
|
||||
@@ -99,9 +99,7 @@ const rules: FormRules = {
|
||||
if (!value) return Promise.resolve(); // 非必填
|
||||
const emailReg = /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/;
|
||||
const phoneReg = /^1[3456789]\d{9}$/;
|
||||
return emailReg.test(value) || phoneReg.test(value)
|
||||
? Promise.resolve()
|
||||
: Promise.reject("请输入正确的手机号或邮箱");
|
||||
return emailReg.test(value) || phoneReg.test(value) ? Promise.resolve() : Promise.reject("请输入正确的手机号或邮箱");
|
||||
},
|
||||
message: "请输入正确的手机号或邮箱",
|
||||
trigger: "blur",
|
||||
@@ -145,7 +143,7 @@ const handleSubmit = async () => {
|
||||
try {
|
||||
// TODO: 调用提交反馈的接口
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500)); // 模拟提交
|
||||
toast.success("提交成功");
|
||||
toast.success("提交成功,我们会尽快处理您的反馈");
|
||||
|
||||
// 重置表单
|
||||
formRef.value.reset();
|
||||
@@ -170,6 +168,7 @@ const handleSubmit = async () => {
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<route lang="json">
|
||||
{
|
||||
"name": "feedback",
|
||||
@@ -178,12 +177,16 @@ const handleSubmit = async () => {
|
||||
}
|
||||
}
|
||||
</route>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.wd-form-item) {
|
||||
margin-bottom: 12rpx;
|
||||
.footer {
|
||||
padding: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
margin: 40rpx 30rpx;
|
||||
height: 88rpx;
|
||||
font-size: 32rpx;
|
||||
border-radius: 44rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,40 +1,30 @@
|
||||
<template>
|
||||
<view class="app-container">
|
||||
<view class="app-container theme-adaptive">
|
||||
<!-- 用户信息卡片 -->
|
||||
<view class="user-profile">
|
||||
<view class="user-profile theme-card">
|
||||
<view class="blur-bg"></view>
|
||||
<view class="user-info">
|
||||
<view class="avatar-container" @click="navigateToProfile">
|
||||
<image
|
||||
class="avatar"
|
||||
:src="isLogin ? userInfo!.avatar : defaultAvatar"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<image class="avatar" :src="isLogin ? userInfo!.avatar : defaultAvatar" mode="aspectFill" />
|
||||
</view>
|
||||
<view class="user-details">
|
||||
<block v-if="isLogin">
|
||||
<view class="nickname">{{ userInfo!.name || "匿名用户" }}</view>
|
||||
<view class="user-id">ID: {{ userInfo?.username || "0000000" }}</view>
|
||||
<view class="nickname theme-text-primary">{{ userInfo!.name || "匿名用户" }}</view>
|
||||
<view class="user-id theme-text-secondary">ID: {{ userInfo?.username || "0000000" }}</view>
|
||||
</block>
|
||||
<block v-else>
|
||||
<view class="login-prompt">立即登录获取更多功能</view>
|
||||
<wd-button
|
||||
custom-class="btn-login"
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="navigateToLoginPage"
|
||||
>
|
||||
登录/注册
|
||||
</wd-button>
|
||||
<view class="login-prompt text-white">立即登录获取更多功能</view>
|
||||
<wd-button custom-class="btn-login" size="small" type="primary"
|
||||
@click="navigateToLoginPage">登录/注册</wd-button>
|
||||
</block>
|
||||
</view>
|
||||
<view class="actions">
|
||||
<view class="action-btn" @click="navigateToSettings">
|
||||
<wd-icon name="setting1" size="22" color="#333" />
|
||||
<wd-icon name="setting1" size="22" :color="themeVars.darkColor3" />
|
||||
</view>
|
||||
<view v-if="isLogin" class="action-btn" @click="navigateToSection('messages')">
|
||||
<wd-badge v-if="true" modelValue="99+">
|
||||
<wd-icon name="notification" size="22" color="#333" />
|
||||
<wd-icon name="notification" size="22" :color="themeVars.darkColor3" />
|
||||
</wd-badge>
|
||||
</view>
|
||||
</view>
|
||||
@@ -42,20 +32,20 @@
|
||||
</view>
|
||||
|
||||
<!-- 数据统计 -->
|
||||
<view class="stats-card">
|
||||
<view class="stats-card theme-card">
|
||||
<view class="stats-item" @click="navigateToSection('wallet')">
|
||||
<view class="mb-8rpx text-36rpx font-600">0.00</view>
|
||||
<view class="text-26rpx text-gray-500">我的余额</view>
|
||||
<view class="mb-sm text-lg font-bold theme-text-primary">0.00</view>
|
||||
<view class="text-sm theme-text-secondary">我的余额</view>
|
||||
</view>
|
||||
<view class="stats-divider"></view>
|
||||
<view class="stats-item" @click="navigateToSection('favorites')">
|
||||
<view class="mb-8rpx text-36rpx font-600">0</view>
|
||||
<view class="text-26rpx text-gray-500">我的收藏</view>
|
||||
<view class="mb-sm text-lg font-bold theme-text-primary">0</view>
|
||||
<view class="text-sm theme-text-secondary">我的收藏</view>
|
||||
</view>
|
||||
<view class="stats-divider"></view>
|
||||
<view class="stats-item" @click="navigateToSection('history')">
|
||||
<view class="mb-8rpx text-36rpx font-600">0</view>
|
||||
<view class="text-26rpx text-gray-500">浏览历史</view>
|
||||
<view class="mb-sm text-lg font-bold theme-text-primary">0</view>
|
||||
<view class="text-sm theme-text-secondary">浏览历史</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -63,32 +53,32 @@
|
||||
<view class="card-container">
|
||||
<view class="card-header">
|
||||
<view class="flex-start">
|
||||
<wd-icon name="tools" size="18" :color="currentThemeColor" />
|
||||
<wd-icon name="tools" size="18" :color="themeVars.colorTheme" />
|
||||
<text class="ml-12rpx text-28rpx font-600">常用工具</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="flex flex-wrap p-20rpx pt-20rpx pb-10rpx">
|
||||
<view class="tool-item" @click="navigateToProfile">
|
||||
<view class="tool-icon">
|
||||
<wd-icon name="user" size="24" :color="currentThemeColor" />
|
||||
<wd-icon name="user" size="24" :color="currentThemeColor.primary" />
|
||||
</view>
|
||||
<view class="text-24rpx">个人资料</view>
|
||||
</view>
|
||||
<view class="tool-item" @click="navigateToFAQ">
|
||||
<view class="tool-icon">
|
||||
<wd-icon name="help-circle" size="24" :color="currentThemeColor" />
|
||||
<wd-icon name="help-circle" size="24" :color="currentThemeColor.primary" />
|
||||
</view>
|
||||
<view class="text-24rpx">常见问题</view>
|
||||
</view>
|
||||
<view class="tool-item" @click="handleQuestionFeedback">
|
||||
<view class="tool-icon">
|
||||
<wd-icon name="check-circle" size="24" :color="currentThemeColor" />
|
||||
<wd-icon name="check-circle" size="24" :color="currentThemeColor.primary" />
|
||||
</view>
|
||||
<view class="text-24rpx">问题反馈</view>
|
||||
</view>
|
||||
<view class="tool-item" @click="navigateToAbout">
|
||||
<view class="tool-icon">
|
||||
<wd-icon name="info-circle" size="24" :color="currentThemeColor" />
|
||||
<wd-icon name="info-circle" size="24" :color="currentThemeColor.primary" />
|
||||
</view>
|
||||
<view class="text-24rpx">关于我们</view>
|
||||
</view>
|
||||
@@ -99,7 +89,7 @@
|
||||
<view class="card-container">
|
||||
<view class="card-header">
|
||||
<view class="flex-start">
|
||||
<wd-icon name="star" size="18" :color="currentThemeColor" />
|
||||
<wd-icon name="star" size="18" :color="currentThemeColor.primary" />
|
||||
<text class="ml-12rpx text-28rpx font-600">推荐服务</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -107,7 +97,7 @@
|
||||
<view class="service-item" @click="navigateToSection('services', 'vip')">
|
||||
<view class="flex-start">
|
||||
<view class="service-icon">
|
||||
<wd-icon name="dong" size="22" :color="currentThemeColor" />
|
||||
<wd-icon name="dong" size="22" :color="currentThemeColor.primary" />
|
||||
</view>
|
||||
<view>
|
||||
<view class="text-28rpx font-500">会员中心</view>
|
||||
@@ -119,7 +109,7 @@
|
||||
<view class="service-item" @click="navigateToSection('services', 'coupon')">
|
||||
<view class="flex-start">
|
||||
<view class="service-icon">
|
||||
<wd-icon name="discount" size="22" :color="currentThemeColor" />
|
||||
<wd-icon name="discount" size="22" :color="currentThemeColor.primary" />
|
||||
</view>
|
||||
<view>
|
||||
<view class="text-28rpx font-500">优惠券</view>
|
||||
@@ -128,13 +118,10 @@
|
||||
</view>
|
||||
<wd-icon name="arrow-right" size="14" />
|
||||
</view>
|
||||
<view
|
||||
class="service-item service-item-last"
|
||||
@click="navigateToSection('services', 'invite')"
|
||||
>
|
||||
<view class="service-item service-item-last" @click="navigateToSection('services', 'invite')">
|
||||
<view class="flex-start">
|
||||
<view class="service-icon">
|
||||
<wd-icon name="share" size="22" :color="currentThemeColor" />
|
||||
<wd-icon name="share" size="22" :color="currentThemeColor.primary" />
|
||||
</view>
|
||||
<view>
|
||||
<view class="text-28rpx font-500">邀请有礼</view>
|
||||
@@ -150,7 +137,6 @@
|
||||
<view v-if="isLogin" class="p-30rpx">
|
||||
<wd-button custom-class="logout-button" plain @click="handleLogout">退出登录</wd-button>
|
||||
</view>
|
||||
|
||||
<wd-toast />
|
||||
</view>
|
||||
</template>
|
||||
@@ -158,13 +144,16 @@
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from "wot-design-uni";
|
||||
import { useRouter } from "uni-mini-router";
|
||||
import { useUserStore, useThemeStore } from "@/store";
|
||||
import { useUserStore } from "@/store";
|
||||
import { useThemeStore } from "@/store/modules/theme.store";
|
||||
import { computed } from "vue";
|
||||
|
||||
const toast = useToast();
|
||||
const userStore = useUserStore();
|
||||
const themeStore = useThemeStore();
|
||||
const currentThemeColor = computed(() => themeStore.themeVars.colorTheme);
|
||||
const themeVars = themeStore.themeVars;
|
||||
const currentThemeColor = themeStore.currentThemeColor;
|
||||
|
||||
const userInfo = computed(() => userStore.userInfo);
|
||||
const isLogin = computed(() => !!userInfo.value);
|
||||
const defaultAvatar = "/static/images/default-avatar.png";
|
||||
@@ -266,7 +255,7 @@ const loadUserInfo = async () => {
|
||||
// 监听用户信息变化,确保数据及时更新
|
||||
watch(
|
||||
() => userInfo.value,
|
||||
() => {},
|
||||
() => { },
|
||||
{
|
||||
deep: true,
|
||||
immediate: true,
|
||||
@@ -274,20 +263,23 @@ watch(
|
||||
);
|
||||
</script>
|
||||
|
||||
<route lang="json">
|
||||
{
|
||||
<route lang="json">{
|
||||
"name": "mine",
|
||||
"style": { "navigationStyle": "custom" },
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
},
|
||||
"layout": "tabbar"
|
||||
}
|
||||
</route>
|
||||
}</route>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// 用户信息卡片
|
||||
.user-profile {
|
||||
position: relative;
|
||||
padding: 30rpx;
|
||||
margin: 12rpx;
|
||||
overflow: hidden;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: var(--shadow-medium);
|
||||
|
||||
.blur-bg {
|
||||
position: absolute;
|
||||
@@ -360,9 +352,9 @@ watch(
|
||||
.card-container {
|
||||
margin: 24rpx 12rpx;
|
||||
overflow: hidden;
|
||||
background-color: #fff;
|
||||
background-color: var(--card-bg-color, #ffffff);
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 1rpx 3rpx rgba(0, 0, 0, 0.1);
|
||||
box-shadow: var(--shadow-medium);
|
||||
}
|
||||
|
||||
// 卡片头部
|
||||
@@ -371,16 +363,7 @@ watch(
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20rpx 24rpx;
|
||||
border-bottom: 1rpx solid #e5e7eb;
|
||||
}
|
||||
|
||||
// 暗色模式下的卡片样式
|
||||
:deep(.dark) .card-container {
|
||||
background-color: #1f2937;
|
||||
}
|
||||
|
||||
:deep(.dark) .card-header {
|
||||
border-bottom-color: #374151;
|
||||
border-bottom: 1rpx solid var(--border-color, #e5e7eb);
|
||||
}
|
||||
|
||||
// 工具项
|
||||
@@ -400,7 +383,7 @@ watch(
|
||||
width: 90rpx;
|
||||
height: 90rpx;
|
||||
margin-bottom: 12rpx;
|
||||
background-color: #f3f4f6;
|
||||
background-color: var(--bg-color-2, #f3f4f6);
|
||||
border-radius: 18rpx;
|
||||
transition: transform 0.15s ease;
|
||||
|
||||
@@ -409,24 +392,15 @@ watch(
|
||||
}
|
||||
}
|
||||
|
||||
// 暗色模式下的工具图标样式
|
||||
:deep(.dark) .tool-icon {
|
||||
background-color: #374151;
|
||||
}
|
||||
|
||||
// 数据统计卡片(减少原子类堆叠)
|
||||
// 数据统计卡片
|
||||
.stats-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
margin: 24rpx 12rpx;
|
||||
background-color: #ffffff;
|
||||
background-color: var(--card-bg-color, #ffffff);
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 1rpx 3rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
:deep(.dark) .stats-card {
|
||||
background-color: #1f2937;
|
||||
box-shadow: var(--shadow-medium);
|
||||
}
|
||||
|
||||
.stats-item {
|
||||
@@ -439,11 +413,7 @@ watch(
|
||||
.stats-divider {
|
||||
width: 1px;
|
||||
margin: 0 20rpx;
|
||||
background-color: #e5e7eb;
|
||||
}
|
||||
|
||||
:deep(.dark) .stats-divider {
|
||||
background-color: #374151;
|
||||
background-color: var(--border-color, #e5e7eb);
|
||||
}
|
||||
|
||||
// 服务项
|
||||
@@ -452,11 +422,11 @@ watch(
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx;
|
||||
border-bottom: 1rpx solid #e5e7eb;
|
||||
border-bottom: 1rpx solid var(--border-color, #e5e7eb);
|
||||
transition: background-color 0.15s ease;
|
||||
|
||||
&:active {
|
||||
background-color: #f3f4f6;
|
||||
background-color: var(--bg-color-2, #f3f4f6);
|
||||
}
|
||||
|
||||
&.service-item-last {
|
||||
@@ -464,15 +434,6 @@ watch(
|
||||
}
|
||||
}
|
||||
|
||||
// 暗色模式下的服务项样式
|
||||
:deep(.dark) .service-item {
|
||||
border-bottom-color: #374151;
|
||||
|
||||
&:active {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
// 服务图标
|
||||
.service-icon {
|
||||
display: flex;
|
||||
@@ -481,15 +442,10 @@ watch(
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
margin-right: 20rpx;
|
||||
background-color: #f3f4f6;
|
||||
background-color: var(--bg-color-2, #f3f4f6);
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
// 暗色模式下的服务图标样式
|
||||
:deep(.dark) .service-icon {
|
||||
background-color: #374151;
|
||||
}
|
||||
|
||||
// 登录按钮样式
|
||||
:deep(.btn-login) {
|
||||
font-size: 24rpx !important;
|
||||
@@ -504,4 +460,24 @@ watch(
|
||||
font-weight: bold !important;
|
||||
border-radius: 40rpx !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
// 文字白色样式(用于在蓝色背景上显示白色文字)
|
||||
.text-white {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
// 暗色模式适配
|
||||
:deep(.dark) {
|
||||
|
||||
.tool-icon,
|
||||
.service-icon,
|
||||
.service-item:active {
|
||||
background-color: var(--bg-color-2, #374151);
|
||||
}
|
||||
|
||||
.stats-divider,
|
||||
.card-header {
|
||||
border-color: var(--border-color, #374151);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,49 +1,122 @@
|
||||
<template>
|
||||
<view class="app-container">
|
||||
<wd-card v-if="userProfile" custom-style="margin-top: 20rpx">
|
||||
<view class="app-container theme-adaptive">
|
||||
<wd-card v-if="userProfile" custom-style="margin-top: 20rpx" class="theme-card">
|
||||
<wd-cell-group border>
|
||||
<wd-cell class="avatar-cell" title="头像" center is-link>
|
||||
<view class="avatar">
|
||||
<view v-if="!userProfile.avatar" class="img" @click="avatarUpload">
|
||||
<wd-icon name="fill-camera" custom-class="img-icon" />
|
||||
<wd-cell class="avatar-cell" title="头像" center is-link @click="avatarUpload">
|
||||
<template #title>
|
||||
<text class="theme-text-primary">头像</text>
|
||||
</template>
|
||||
<view class="avatar flex-center">
|
||||
<view v-if="!userProfile.avatar" class="img flex-center" @click="avatarUpload">
|
||||
<wd-icon name="fill-camera" custom-class="img-icon" :color="themeVars.darkColor3" />
|
||||
</view>
|
||||
<wd-img
|
||||
v-if="userProfile.avatar"
|
||||
round
|
||||
width="80px"
|
||||
height="80px"
|
||||
:src="userProfile.avatar"
|
||||
mode="aspectFit"
|
||||
custom-class="profile-img"
|
||||
@click="avatarUpload"
|
||||
/>
|
||||
<wd-img v-if="userProfile.avatar" round width="80px" height="80px" :src="userProfile.avatar"
|
||||
mode="aspectFit" custom-class="profile-img" @click="avatarUpload" />
|
||||
</view>
|
||||
</wd-cell>
|
||||
<wd-cell title="昵称" :value="userProfile.name" is-link @click="handleOpenDialog()" />
|
||||
<wd-cell
|
||||
title="性别"
|
||||
:value="userProfile.gender === '0' ? '男' : userProfile.gender === '1' ? '女' : '未知'"
|
||||
is-link
|
||||
@click="handleOpenDialog()"
|
||||
/>
|
||||
<wd-cell title="用户名" :value="userProfile.username" />
|
||||
<wd-cell title="昵称" :value="userProfile.name" is-link @click="handleOpenDialog('name')">
|
||||
<template #title>
|
||||
<text class="theme-text-primary">昵称</text>
|
||||
</template>
|
||||
<template #value>
|
||||
<text class="theme-text-secondary">{{ userProfile.name || '未设置' }}</text>
|
||||
</template>
|
||||
</wd-cell>
|
||||
<wd-cell title="性别" :value="userProfile.gender === '0' ? '男' : userProfile.gender === '1' ? '女' : '未知'" is-link
|
||||
@click="handleOpenDialog('gender')">
|
||||
<template #title>
|
||||
<text class="theme-text-primary">性别</text>
|
||||
</template>
|
||||
<template #value>
|
||||
<text class="theme-text-secondary">
|
||||
{{ userProfile.gender === "0" ? "男" : userProfile.gender === "1" ? "女" : "未知" }}
|
||||
</text>
|
||||
</template>
|
||||
</wd-cell>
|
||||
<wd-cell title="用户名" :value="userProfile.username">
|
||||
<template #title>
|
||||
<text class="theme-text-primary">用户名</text>
|
||||
</template>
|
||||
<template #value>
|
||||
<text class="theme-text-secondary">{{ userProfile.username }}</text>
|
||||
</template>
|
||||
</wd-cell>
|
||||
<wd-cell v-if="userProfile" title="状态" center>
|
||||
<template #title>
|
||||
<text class="theme-text-primary">状态</text>
|
||||
</template>
|
||||
<wd-tag :type="userProfile.status ? 'success' : 'danger'">
|
||||
{{ userProfile.status ? "启用" : "停用" }}
|
||||
</wd-tag>
|
||||
</wd-cell>
|
||||
<wd-cell v-if="userProfile" title="是否超管" center>
|
||||
<template #title>
|
||||
<text class="theme-text-primary">是否超管</text>
|
||||
</template>
|
||||
<wd-tag plain :type="userProfile.is_superuser ? 'primary' : 'default'">
|
||||
{{ userProfile.is_superuser ? "是" : "否" }}
|
||||
</wd-tag>
|
||||
</wd-cell>
|
||||
<wd-cell title="手机号" :value="userProfile.mobile" />
|
||||
<wd-cell title="邮箱" :value="userProfile.email" />
|
||||
<wd-cell title="部门" :value="userProfile.dept_name" />
|
||||
<wd-cell title="角色" :value="userProfile.roles?.map((item) => item.name).join(', ')" />
|
||||
<wd-cell title="岗位" :value="userProfile.positions?.map((item) => item.name).join(', ')" />
|
||||
<wd-cell title="备注" :value="userProfile.description" />
|
||||
<wd-cell title="创建日期" :value="userProfile.created_at" />
|
||||
<wd-cell title="手机号" :value="userProfile.mobile">
|
||||
<template #title>
|
||||
<text class="theme-text-primary">手机号</text>
|
||||
</template>
|
||||
<template #value>
|
||||
<text class="theme-text-secondary">{{ userProfile.mobile || '未绑定' }}</text>
|
||||
</template>
|
||||
</wd-cell>
|
||||
<wd-cell title="邮箱" :value="userProfile.email">
|
||||
<template #title>
|
||||
<text class="theme-text-primary">邮箱</text>
|
||||
</template>
|
||||
<template #value>
|
||||
<text class="theme-text-secondary">{{ userProfile.email || '未绑定' }}</text>
|
||||
</template>
|
||||
</wd-cell>
|
||||
<wd-cell title="部门" :value="userProfile.dept_name">
|
||||
<template #title>
|
||||
<text class="theme-text-primary">部门</text>
|
||||
</template>
|
||||
<template #value>
|
||||
<text class="theme-text-secondary">{{ userProfile.dept_name || '未分配' }}</text>
|
||||
</template>
|
||||
</wd-cell>
|
||||
<wd-cell title="角色" :value="userProfile.roles?.map((item) => item.name).join(', ')">
|
||||
<template #title>
|
||||
<text class="theme-text-primary">角色</text>
|
||||
</template>
|
||||
<template #value>
|
||||
<text class="theme-text-secondary">
|
||||
{{userProfile.roles?.map((item) => item.name).join(", ") || '未分配'}}
|
||||
</text>
|
||||
</template>
|
||||
</wd-cell>
|
||||
<wd-cell title="岗位" :value="userProfile.positions?.map((item) => item.name).join(', ')">
|
||||
<template #title>
|
||||
<text class="theme-text-primary">岗位</text>
|
||||
</template>
|
||||
<template #value>
|
||||
<text class="theme-text-secondary">
|
||||
{{userProfile.positions?.map((item) => item.name).join(", ") || '未分配'}}
|
||||
</text>
|
||||
</template>
|
||||
</wd-cell>
|
||||
<wd-cell title="备注" :value="userProfile.description">
|
||||
<template #title>
|
||||
<text class="theme-text-primary">备注</text>
|
||||
</template>
|
||||
<template #value>
|
||||
<text class="theme-text-secondary">{{ userProfile.description || '无' }}</text>
|
||||
</template>
|
||||
</wd-cell>
|
||||
<wd-cell title="创建日期" :value="userProfile.created_at">
|
||||
<template #title>
|
||||
<text class="theme-text-primary">创建日期</text>
|
||||
</template>
|
||||
<template #value>
|
||||
<text class="theme-text-secondary">{{ userProfile.created_at }}</text>
|
||||
</template>
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
</wd-card>
|
||||
|
||||
@@ -51,18 +124,24 @@
|
||||
<wd-img-cropper v-model="avatarShow" :img-src="originalSrc" @confirm="handleAvatarConfirm" />
|
||||
|
||||
<!--用户信息编辑弹出框-->
|
||||
<wd-popup v-model="dialog.visible" position="bottom">
|
||||
<wd-popup v-model="dialog.visible" position="bottom"
|
||||
custom-style="border-top-left-radius: 16rpx; border-top-right-radius: 16rpx;">
|
||||
<view class="popup-header">
|
||||
<wd-cell :title="dialog.title" center>
|
||||
<template #right-icon>
|
||||
<wd-icon name="close" size="20" @click="dialog.visible = false" />
|
||||
</template>
|
||||
</wd-cell>
|
||||
</view>
|
||||
|
||||
<wd-divider />
|
||||
|
||||
<wd-form ref="userProfileFormRef" :model="userProfileForm" custom-class="edit-form">
|
||||
<wd-cell-group border>
|
||||
<wd-input
|
||||
v-model="userProfileForm.name"
|
||||
label="昵称"
|
||||
label-width="160rpx"
|
||||
placeholder="请输入昵称"
|
||||
prop="name"
|
||||
:rules="rules.name"
|
||||
/>
|
||||
<wd-cell title="性别" title-width="160rpx" center prop="gender" :rules="rules.gender">
|
||||
<wd-input v-if="dialog.field === 'name'" v-model="userProfileForm.name" label="昵称" label-width="160rpx"
|
||||
placeholder="请输入昵称" prop="name" :rules="rules.name" />
|
||||
<wd-cell v-if="dialog.field === 'gender'" title="性别" title-width="160rpx" center prop="gender"
|
||||
:rules="rules.gender">
|
||||
<wd-radio-group v-model="userProfileForm.gender" shape="button" class="ef-radio-group">
|
||||
<wd-radio :value="0">男</wd-radio>
|
||||
<wd-radio :value="1">女</wd-radio>
|
||||
@@ -70,13 +149,14 @@
|
||||
</wd-radio-group>
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
<view class="footer">
|
||||
<wd-button type="primary" size="large" block @click="handleSubmit">提交</wd-button>
|
||||
<view>
|
||||
<wd-button type="primary" block @click="handleSubmit">提交</wd-button>
|
||||
</view>
|
||||
</wd-form>
|
||||
</wd-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import UserAPI, { type UserInfo } from "@/api/user";
|
||||
import FileAPI from "@/api/file";
|
||||
@@ -146,11 +226,18 @@ const userProfileFormRef = ref();
|
||||
* 打开弹窗
|
||||
* @param type 弹窗类型 ACCOUNT: 账号资料 PASSWORD: 修改密码 MOBILE: 绑定手机 EMAIL: 绑定邮箱
|
||||
*/
|
||||
const handleOpenDialog = () => {
|
||||
const handleOpenDialog = (field: string) => {
|
||||
dialog.field = field;
|
||||
dialog.visible = true;
|
||||
|
||||
// 初始化表单数据
|
||||
userProfileForm.name = userProfile.value?.name || "";
|
||||
userProfileForm.gender = userProfile.value?.gender || "0";
|
||||
if (field === "name") {
|
||||
dialog.title = "编辑昵称";
|
||||
userProfileForm.name = userProfile.value?.name || "";
|
||||
} else if (field === "gender") {
|
||||
dialog.title = "选择性别";
|
||||
userProfileForm.gender = userProfile.value?.gender || "0";
|
||||
}
|
||||
};
|
||||
|
||||
// 提交表单
|
||||
@@ -211,6 +298,16 @@ function touchmoveListener(event: TouchEvent) {
|
||||
event.preventDefault();
|
||||
}
|
||||
</script>
|
||||
|
||||
<route lang="json">
|
||||
{
|
||||
"name": "profile",
|
||||
"style": {
|
||||
"navigationBarTitleText": "个人中心"
|
||||
}
|
||||
}
|
||||
</route>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.avatar-cell {
|
||||
:deep(.wd-cell__body) {
|
||||
|
||||
@@ -1,56 +1,43 @@
|
||||
<template>
|
||||
<view class="app-container">
|
||||
<wd-card custom-style="margin-top: 20rpx">
|
||||
<view class="app-container theme-adaptive">
|
||||
<wd-card custom-style="margin-top: 20rpx" class="theme-card">
|
||||
<wd-cell-group border>
|
||||
<wd-cell
|
||||
title="账户密码"
|
||||
label="定期修改密码有助于保护账户安全"
|
||||
value="修改"
|
||||
is-link
|
||||
@click="handleOpenDialog(DialogType.PASSWORD)"
|
||||
/>
|
||||
<wd-cell title="账户密码" label="定期修改密码有助于保护账户安全" value="修改" is-link @click="handleOpenDialog(DialogType.PASSWORD)">
|
||||
<template #title>
|
||||
<text class="theme-text-primary">账户密码</text>
|
||||
</template>
|
||||
<template #label>
|
||||
<text class="theme-text-secondary">定期修改密码有助于保护账户安全</text>
|
||||
</template>
|
||||
<template #value>
|
||||
<text class="theme-text-secondary">修改</text>
|
||||
</template>
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
</wd-card>
|
||||
|
||||
<!--用户信息编辑弹出框-->
|
||||
<wd-popup v-model="dialog.visible" position="bottom">
|
||||
<wd-form
|
||||
v-if="dialog.type === DialogType.PASSWORD"
|
||||
ref="passwordChangeFormRef"
|
||||
:model="passwordChangeForm"
|
||||
custom-class="edit-form"
|
||||
>
|
||||
<wd-popup v-model="dialog.visible" position="bottom"
|
||||
custom-style="border-top-left-radius: 16rpx; border-top-right-radius: 16rpx;">
|
||||
<view class="popup-header">
|
||||
<wd-cell :title="getDialogTitle" center>
|
||||
<template #right-icon>
|
||||
<wd-icon name="close" size="20" @click="dialog.visible = false" />
|
||||
</template>
|
||||
</wd-cell>
|
||||
</view>
|
||||
|
||||
<wd-divider />
|
||||
|
||||
<wd-form v-if="dialog.type === DialogType.PASSWORD" ref="passwordChangeFormRef" :model="passwordChangeForm"
|
||||
custom-class="edit-form">
|
||||
<wd-cell-group border>
|
||||
<wd-input
|
||||
v-model="passwordChangeForm.old_password"
|
||||
label="原密码"
|
||||
label-width="160rpx"
|
||||
show-password
|
||||
clearable
|
||||
placeholder="请输入原密码"
|
||||
prop="old_password"
|
||||
:rules="rules.oldPassword"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="passwordChangeForm.new_password"
|
||||
label="新密码"
|
||||
label-width="160rpx"
|
||||
show-password
|
||||
clearable
|
||||
placeholder="请输入新密码"
|
||||
prop="new_password"
|
||||
:rules="rules.newPassword"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="passwordChangeForm.confirm_password"
|
||||
label="确认密码"
|
||||
label-width="160rpx"
|
||||
show-password
|
||||
clearable
|
||||
placeholder="请确认新密码"
|
||||
prop="confirm_password"
|
||||
:rules="rules.confirmPassword"
|
||||
/>
|
||||
<wd-input v-model="passwordChangeForm.old_password" label="原密码" label-width="160rpx" show-password clearable
|
||||
placeholder="请输入原密码" prop="old_password" :rules="rules.oldPassword" />
|
||||
<wd-input v-model="passwordChangeForm.new_password" label="新密码" label-width="160rpx" show-password clearable
|
||||
placeholder="请输入新密码" prop="new_password" :rules="rules.newPassword" />
|
||||
<wd-input v-model="passwordChangeForm.confirm_password" label="确认密码" label-width="160rpx" show-password
|
||||
clearable placeholder="请确认新密码" prop="confirm_password" :rules="rules.confirmPassword" />
|
||||
</wd-cell-group>
|
||||
<view class="p-6">
|
||||
<wd-button type="primary" size="large" block @click="handleSubmit">提交</wd-button>
|
||||
@@ -59,6 +46,7 @@
|
||||
</wd-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import UserAPI, { PasswordChangeForm, UserInfo } from "@/api/user";
|
||||
|
||||
@@ -73,6 +61,7 @@ const validatorConfirmPassword = (value: string) => {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 本页面中所有的校验规则
|
||||
const rules = reactive({
|
||||
oldPassword: [{ required: true, message: "请填写原密码" }],
|
||||
@@ -95,6 +84,19 @@ enum DialogType {
|
||||
EMAIL = "email",
|
||||
}
|
||||
|
||||
const getDialogTitle = computed(() => {
|
||||
switch (dialog.type) {
|
||||
case DialogType.PASSWORD:
|
||||
return "修改密码";
|
||||
case DialogType.MOBILE:
|
||||
return "绑定手机";
|
||||
case DialogType.EMAIL:
|
||||
return "绑定邮箱";
|
||||
default:
|
||||
return "账号设置";
|
||||
}
|
||||
});
|
||||
|
||||
const dialog = reactive({
|
||||
visible: false,
|
||||
type: "" as DialogType, // 修改账号资料,修改密码、绑定手机、绑定邮箱
|
||||
@@ -135,8 +137,10 @@ function handleSubmit() {
|
||||
passwordChangeFormRef.value.validate().then(({ valid }: { valid: boolean }) => {
|
||||
if (valid) {
|
||||
UserAPI.changeCurrentUserPassword(passwordChangeForm).then(() => {
|
||||
uni.showToast({ title: "密码修改成功", icon: "none" });
|
||||
uni.showToast({ title: "密码修改成功", icon: "success" });
|
||||
dialog.visible = false;
|
||||
}).catch((error: any) => {
|
||||
uni.showToast({ title: error?.message || "密码修改失败", icon: "error" });
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -151,8 +155,11 @@ onMounted(() => {
|
||||
<route lang="json">
|
||||
{
|
||||
"name": "account",
|
||||
"style": { "navigationBarTitleText": "账号和安全" },
|
||||
"style": {
|
||||
"navigationBarTitleText": "账号和安全"
|
||||
},
|
||||
"layout": "tabbar"
|
||||
}
|
||||
</route>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -1,26 +1,22 @@
|
||||
<template>
|
||||
<view class="app-container">
|
||||
<wd-card custom-style="margin-top: 20rpx">
|
||||
<view class="flex-col-center py-4">
|
||||
<text class="text-xl font-bold mb-2">用户协议</text>
|
||||
<text class="text-sm text-gray-500">更新日期:2024年3月15日</text>
|
||||
<view class="app-container theme-adaptive">
|
||||
<wd-card custom-style="margin-top: 20rpx" class="theme-card">
|
||||
<view class="flex-col-center py-lg">
|
||||
<text class="text-xl font-bold mb-md theme-text-primary">用户协议</text>
|
||||
<text class="text-sm theme-text-secondary">更新日期:2024年3月15日</text>
|
||||
</view>
|
||||
</wd-card>
|
||||
|
||||
<wd-collapse v-model="activeNames" accordion>
|
||||
<wd-collapse-item
|
||||
v-for="(section, index) in agreementContent"
|
||||
:key="index"
|
||||
:title="section.title"
|
||||
:name="String(index)"
|
||||
>
|
||||
<view class="py-3 px-4">
|
||||
<text class="text-base leading-relaxed text-gray-600">{{ section.content }}</text>
|
||||
<wd-collapse v-model="activeNames" accordion class="theme-card mt-md">
|
||||
<wd-collapse-item v-for="(section, index) in agreementContent" :key="index" :title="section.title"
|
||||
:name="String(index)">
|
||||
<view class="py-md px-lg">
|
||||
<text class="text-base leading-relaxed theme-text-secondary">{{ section.content }}</text>
|
||||
</view>
|
||||
</wd-collapse-item>
|
||||
</wd-collapse>
|
||||
|
||||
<view class="mt-6 px-4">
|
||||
<view class="mt-lg px-lg theme-card">
|
||||
<wd-button type="primary" block @click="handleAgree">我已阅读并同意</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
@@ -32,33 +28,35 @@ const activeNames = ref(["0"]); // 默认展开第一项
|
||||
const agreementContent = [
|
||||
{
|
||||
title: "1. 协议的范围",
|
||||
content:
|
||||
"本协议是您与我们之间关于使用本应用服务所订立的协议。您在使用本应用服务时,须完全接受本协议所有条款。",
|
||||
content: "本协议是您与我们之间关于使用本应用服务所订立的协议。您在使用本应用服务时,须完全接受本协议所有条款。本协议适用于本应用提供的所有服务。如果您不同意本协议的任何条款,请立即停止使用本应用。本协议构成您与我们之间的法律协议,具有法律约束力。",
|
||||
},
|
||||
{
|
||||
title: "2. 服务内容",
|
||||
content:
|
||||
"本应用向您提供以下服务:网络状态检测、网络性能测试以及其他相关服务。我们将持续优化和更新服务内容,为您提供更好的使用体验。",
|
||||
content: "本应用向您提供以下服务:网络状态检测、网络性能测试、数据统计分析以及其他相关服务。我们将持续优化和更新服务内容,为您提供更好的使用体验。我们保留在任何时候修改或终止部分或全部服务的权利,且无需提前通知用户。",
|
||||
},
|
||||
{
|
||||
title: "3. 用户隐私",
|
||||
content:
|
||||
"我们重视用户的隐私保护,收集信息仅用于提供网络测试服务、改善用户体验和必要的系统维护。我们承诺对您的信息进行严格保密。",
|
||||
content: "我们重视用户的隐私保护,收集信息仅用于提供网络测试服务、改善用户体验和必要的系统维护。我们承诺对您的信息进行严格保密,不会向任何第三方出售、出租或分享您的个人信息。我们采用行业标准的安全措施保护您的数据安全。",
|
||||
},
|
||||
{
|
||||
title: "4. 用户行为规范",
|
||||
content:
|
||||
"您在使用本服务时必须遵守中华人民共和国相关法律法规。您不得利用本服务从事违法违规活动。如发现违规行为,我们有权终止服务。",
|
||||
content: "您在使用本服务时必须遵守中华人民共和国相关法律法规。您不得利用本服务从事违法违规活动,包括但不限于:传播违法信息、侵犯他人合法权益、破坏网络安全等。如发现违规行为,我们有权终止服务并保留追究法律责任的权利。",
|
||||
},
|
||||
{
|
||||
title: "5. 免责声明",
|
||||
content:
|
||||
"由于网络服务的特殊性,本应用不保证服务一定能满足用户的所有要求。对于因网络状态、通信线路等不可控因素导致的服务中断或其他缺陷,本应用不承担任何责任。",
|
||||
content: "由于网络服务的特殊性,本应用不保证服务一定能满足用户的所有要求。对于因网络状态、通信线路等不可控因素导致的服务中断或其他缺陷,本应用不承担任何责任。我们不对任何间接、偶然、特殊或后果性损害承担责任。",
|
||||
},
|
||||
{
|
||||
title: "6. 协议修改",
|
||||
content:
|
||||
"我们保留随时修改本协议的权利。协议修改后,如果您继续使用本应用服务,即视为您已接受修改后的协议。我们建议您定期查看本协议以了解任何变更。",
|
||||
content: "我们保留随时修改本协议的权利。协议修改后,如果您继续使用本应用服务,即视为您已接受修改后的协议。我们建议您定期查看本协议以了解任何变更。重大修改将通过适当方式通知用户。",
|
||||
},
|
||||
{
|
||||
title: "7. 知识产权",
|
||||
content: "本应用的所有内容,包括但不限于文字、图片、音频、视频、软件等,均受知识产权法律法规保护。未经我们书面许可,您不得以任何方式使用、复制、传播或修改本应用的任何内容。用户在使用过程中产生的合法内容,其知识产权归用户所有。",
|
||||
},
|
||||
{
|
||||
title: "8. 争议解决",
|
||||
content: "因本协议引起的或与本协议有关的任何争议,双方应首先通过友好协商解决。协商不成的,任何一方均可向我们所在地有管辖权的人民法院提起诉讼。本协议的解释、效力和执行均适用中华人民共和国法律。",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -73,10 +71,13 @@ const handleAgree = () => {
|
||||
}, 1500);
|
||||
};
|
||||
</script>
|
||||
|
||||
<route lang="json">
|
||||
{
|
||||
"name": "agreement",
|
||||
"style": { "navigationBarTitleText": "用户协议" },
|
||||
"style": {
|
||||
"navigationBarTitleText": "用户协议"
|
||||
},
|
||||
"layout": "tabbar"
|
||||
}
|
||||
</route>
|
||||
|
||||
@@ -1,43 +1,61 @@
|
||||
<template>
|
||||
<view class="app-container dark:text-[var(--wot-dark-color)]">
|
||||
<view class="app-container theme-adaptive">
|
||||
<!-- 网络状态展示 -->
|
||||
<wd-card custom-style="margin: 20rpx">
|
||||
<wd-card custom-style="margin: 20rpx" class="theme-card">
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="网络状态">
|
||||
<wd-tag :type="networkType ? 'success' : 'danger'" size="small">
|
||||
<template #title>
|
||||
<text class="theme-text-primary">网络状态</text>
|
||||
</template>
|
||||
<wd-tag :type="networkType ? 'success' : 'danger'">
|
||||
{{ networkType ? "在线" : "离线" }}
|
||||
</wd-tag>
|
||||
</wd-cell>
|
||||
<wd-cell title="网络类型" :value="networkType || '未知'" />
|
||||
<wd-cell title="网络强度" :value="signalStrength" />
|
||||
<wd-cell title="网络类型" :value="networkType || '未知'">
|
||||
<template #title>
|
||||
<text class="theme-text-primary">网络类型</text>
|
||||
</template>
|
||||
<template #value>
|
||||
<text class="theme-text-secondary">{{ networkType || "未知" }}</text>
|
||||
</template>
|
||||
</wd-cell>
|
||||
<wd-cell title="网络强度" :value="signalStrength">
|
||||
<template #title>
|
||||
<text class="theme-text-primary">网络强度</text>
|
||||
</template>
|
||||
<template #value>
|
||||
<text class="theme-text-secondary">{{ signalStrength }}</text>
|
||||
</template>
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
</wd-card>
|
||||
|
||||
<!-- 网络测试 -->
|
||||
<wd-card title="网络测试" custom-style="margin: 20rpx">
|
||||
<wd-card title="网络测试" custom-style="margin: 20rpx" class="theme-card">
|
||||
<template #title>
|
||||
<text class="theme-text-primary">网络测试</text>
|
||||
</template>
|
||||
<view slot="extra">
|
||||
<text class="text-gray-500 text-sm">测试服务器连接情况</text>
|
||||
<text class="theme-text-secondary text-sm">测试服务器连接情况</text>
|
||||
</view>
|
||||
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="延迟">
|
||||
<template #title>
|
||||
<text class="theme-text-primary">延迟</text>
|
||||
</template>
|
||||
<view class="flex items-center">
|
||||
<text class="mr-10">{{ pingResult.delay }}ms</text>
|
||||
<wd-tag v-if="getPingStatus" :type="getPingStatusType" size="small">
|
||||
<text class="mr-md theme-text-secondary">{{ pingResult.delay }}ms</text>
|
||||
<wd-tag v-if="getPingStatus" :type="getPingStatusType">
|
||||
{{ pingResult.status }}
|
||||
</wd-tag>
|
||||
</view>
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
|
||||
<wd-progress
|
||||
v-if="testing"
|
||||
:percentage="progress"
|
||||
stroke-width="4"
|
||||
custom-style="margin: 30rpx 0"
|
||||
/>
|
||||
<wd-progress v-if="testing" :percentage="progress" stroke-width="6" custom-style="margin: 30rpx 0" color="#4D7FFF" />
|
||||
|
||||
<wd-button block type="primary" :loading="testing" @click="startTest">
|
||||
<wd-button block type="primary" :loading="testing" custom-class="test-btn" @click="startTest">
|
||||
{{ testing ? "测试中..." : "开始测试" }}
|
||||
</wd-button>
|
||||
</wd-card>
|
||||
@@ -104,9 +122,7 @@ const getNetworkType = async () => {
|
||||
|
||||
// H5环境
|
||||
// #ifdef H5
|
||||
signalStrength.value = (navigator as any).connection
|
||||
? `${(navigator as any).connection.effectiveType || "未知"}`
|
||||
: "不支持";
|
||||
signalStrength.value = (navigator as any).connection ? `${(navigator as any).connection.effectiveType || "未知"}` : "不支持";
|
||||
// #endif
|
||||
} catch {
|
||||
networkType.value = "获取失败";
|
||||
|
||||
@@ -1,26 +1,21 @@
|
||||
<template>
|
||||
<view class="app-container">
|
||||
<wd-card custom-style="margin-top: 20rpx">
|
||||
<view class="flex-col-center py-4">
|
||||
<text class="text-xl font-bold mb-2">隐私政策</text>
|
||||
<text class="text-sm text-gray-500">更新日期:2024年3月15日</text>
|
||||
<view class="app-container theme-adaptive">
|
||||
<wd-card custom-style="margin-top: 20rpx" class="theme-card">
|
||||
<view class="flex-col-center py-lg">
|
||||
<text class="text-xl font-bold mb-md theme-text-primary">隐私政策</text>
|
||||
<text class="text-sm theme-text-secondary">更新日期:2024年3月15日</text>
|
||||
</view>
|
||||
</wd-card>
|
||||
|
||||
<wd-collapse v-model="activeNames" accordion>
|
||||
<wd-collapse-item
|
||||
v-for="(section, index) in privacyContent"
|
||||
:key="index"
|
||||
:title="section.title"
|
||||
:name="String(index)"
|
||||
>
|
||||
<view class="py-3 px-4">
|
||||
<text class="text-base leading-relaxed text-gray-600">{{ section.content }}</text>
|
||||
<wd-collapse v-model="activeNames" accordion class="theme-card mt-md">
|
||||
<wd-collapse-item v-for="(section, index) in privacyContent" :key="index" :title="section.title" :name="String(index)">
|
||||
<view class="py-md px-lg">
|
||||
<text class="text-base leading-relaxed theme-text-secondary">{{ section.content }}</text>
|
||||
</view>
|
||||
</wd-collapse-item>
|
||||
</wd-collapse>
|
||||
|
||||
<view class="mt-6 px-4">
|
||||
<view class="mt-lg px-lg theme-card">
|
||||
<wd-button type="primary" block @click="handleAgree">我已阅读并同意</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
@@ -32,38 +27,35 @@ const activeNames = ref(["0"]); // 默认展开第一项
|
||||
const privacyContent = ref([
|
||||
{
|
||||
title: "1. 信息收集",
|
||||
content:
|
||||
"我们可能收集您的基本信息(如设备信息、操作日志等)以提供更好的服务体验。我们承诺对这些信息进行严格保密,并只用于改善产品服务。",
|
||||
content: "我们可能收集您的基本信息(如设备信息、操作日志等)以提供更好的服务体验。我们承诺对这些信息进行严格保密,并只用于改善产品服务。收集的信息包括但不限于:设备型号、操作系统版本、网络状态、使用习惯等。",
|
||||
},
|
||||
{
|
||||
title: "2. 信息使用",
|
||||
content:
|
||||
"收集的信息将用于:优化用户体验、提供客户支持、发送重要通知、保障账号安全等。未经您的同意,我们不会向第三方分享您的个人信息。",
|
||||
content: "收集的信息将用于:优化用户体验、提供客户支持、发送重要通知、保障账号安全等。未经您的同意,我们不会向第三方分享您的个人信息。我们可能会对数据进行统计分析,但不会泄露任何个人身份信息。",
|
||||
},
|
||||
{
|
||||
title: "3. 信息安全",
|
||||
content:
|
||||
"我们采用业界标准的安全技术和程序来保护您的个人信息,防止未经授权的访问、使用或泄露。我们定期审查信息收集、存储和处理实践。",
|
||||
content: "我们采用业界标准的安全技术和程序来保护您的个人信息,防止未经授权的访问、使用或泄露。我们定期审查信息收集、存储和处理实践,确保数据安全。我们建立了完善的安全管理制度和技术防护措施。",
|
||||
},
|
||||
{
|
||||
title: "4. Cookie 使用",
|
||||
content:
|
||||
"我们使用 Cookie 和类似技术来提供、保护和改进我们的产品和服务。这些技术帮助我们了解用户行为,告诉我们哪些功能最受欢迎。",
|
||||
content: "我们使用 Cookie 和类似技术来提供、保护和改进我们的产品和服务。这些技术帮助我们了解用户行为,告诉我们哪些功能最受欢迎。您可以通过浏览器设置管理 Cookie,但可能会影响部分功能的正常使用。",
|
||||
},
|
||||
{
|
||||
title: "5. 第三方服务",
|
||||
content:
|
||||
"我们的应用可能包含第三方服务。这些第三方服务有自己的隐私政策,我们建议您查看这些政策。我们不对第三方的隐私实践负责。",
|
||||
content: "我们的应用可能包含第三方服务。这些第三方服务有自己的隐私政策,我们建议您查看这些政策。我们不对第三方的隐私实践负责。在使用第三方服务时,请注意查看其隐私条款和使用协议。",
|
||||
},
|
||||
{
|
||||
title: "6. 未成年人保护",
|
||||
content:
|
||||
"我们非常重视对未成年人个人信息的保护。若您是未成年人,请在监护人指导下使用我们的服务。如果您是监护人,当您对您所监护的未成年人的个人信息有疑问时,请联系我们。",
|
||||
content: "我们非常重视对未成年人个人信息的保护。若您是未成年人,请在监护人指导下使用我们的服务。如果您是监护人,当您对您所监护的未成年人的个人信息有疑问时,请联系我们。我们不会故意收集未成年人的个人信息。",
|
||||
},
|
||||
{
|
||||
title: "7. 隐私政策更新",
|
||||
content:
|
||||
"我们可能会不时更新本隐私政策。当我们更新隐私政策时,我们会在本页面上发布更新后的版本并修改更新日期。建议您定期查看本页面。",
|
||||
content: "我们可能会不时更新本隐私政策。当我们更新隐私政策时,我们会在本页面上发布更新后的版本并修改更新日期。建议您定期查看本页面。重大变更将通过适当方式通知用户,确保您了解我们的数据处理方式。",
|
||||
},
|
||||
{
|
||||
title: "8. 联系我们",
|
||||
content: "如果您对本隐私政策有任何疑问或建议,请通过以下方式联系我们:邮箱:privacy@fastapiadmin.com;电话:400-123-4567。我们将尽快回复您的询问,并尽力解决您的问题。您也可以通过应用内的反馈功能提交您的意见。",
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -78,11 +70,14 @@ const handleAgree = () => {
|
||||
}, 1500);
|
||||
};
|
||||
</script>
|
||||
|
||||
<route lang="json">
|
||||
{
|
||||
"name": "privacy",
|
||||
"style": { "navigationBarTitleText": "隐私政策" },
|
||||
"layout": "tabbar"
|
||||
"style": {
|
||||
"navigationBarTitleText": "隐私政策"
|
||||
}
|
||||
}
|
||||
</route>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@@ -7,238 +7,100 @@
|
||||
</view>
|
||||
|
||||
<!-- 暗黑模式设置 -->
|
||||
<wd-card class="setting-section">
|
||||
<view class="section-header">
|
||||
<wd-icon name="moon" size="20" :color="isDarkMode ? '#FFD700' : '#666'" />
|
||||
<text class="section-title">外观模式</text>
|
||||
</view>
|
||||
<wd-card title="外观模式">
|
||||
<wd-cell title="暗黑模式" :value="isDarkMode ? '已开启' : '已关闭'">
|
||||
<wd-switch :model-value="isDarkMode" @change="handleToggleDarkMode" />
|
||||
<wd-switch v-model="isDarkMode" custom-class="theme-switch" @change="handleToggleDarkMode" />
|
||||
</wd-cell>
|
||||
</wd-card>
|
||||
|
||||
<!-- 主题色设置 -->
|
||||
<wd-card class="setting-section">
|
||||
<view class="section-header">
|
||||
<wd-icon name="palette" size="20" color="#666" />
|
||||
<text class="section-title">主题色彩</text>
|
||||
</view>
|
||||
|
||||
<wd-card title="预设主题色">
|
||||
<!-- 预设颜色选择 -->
|
||||
<view class="color-section">
|
||||
<text class="color-label">预设主题色</text>
|
||||
<view class="color-grid">
|
||||
<view
|
||||
v-for="(color, index) in useTheme.themeColorOptions"
|
||||
:key="index"
|
||||
class="color-item"
|
||||
:class="{ active: color.primary }"
|
||||
@click="handleSelectColor(color)"
|
||||
>
|
||||
<view
|
||||
class="color-preview"
|
||||
:style="{
|
||||
backgroundColor: color.primary,
|
||||
border:
|
||||
currentThemeColor === color.primary ? '3px solid #fff' : '1px solid #e0e0e0',
|
||||
}"
|
||||
>
|
||||
<text v-if="currentThemeColor === color.primary" class="check-icon">✓</text>
|
||||
<view v-for="(color, index) in themeOptions" :key="index" class="color-item"
|
||||
:class="{ active: isActiveColor(color.primary) }" @click="selectColor(color)">
|
||||
<view class="color-preview" :style="{ backgroundColor: color.primary }">
|
||||
<wd-icon v-if="isActiveColor(color.primary)" name="check" size="20" color="#fff" />
|
||||
</view>
|
||||
<text class="color-name">{{ color.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 当前主题色显示 -->
|
||||
<view class="current-theme-section">
|
||||
<view class="current-theme-item">
|
||||
<text class="current-theme-label">当前主题色</text>
|
||||
<view class="current-theme-value">
|
||||
<view
|
||||
class="current-color-preview"
|
||||
:style="{ backgroundColor: currentThemeColor }"
|
||||
></view>
|
||||
<text class="current-color-text">{{ currentThemeColor }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 自定义颜色 -->
|
||||
<wd-cell title="自定义颜色" is-link @click="showCustomInput">
|
||||
<wd-icon name="edit" size="16" color="#999" />
|
||||
</wd-cell>
|
||||
</wd-card>
|
||||
|
||||
<wd-divider />
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<wd-card class="action-section">
|
||||
<wd-button size="large" block @click="handleResetTheme">重置为默认主题</wd-button>
|
||||
</wd-card>
|
||||
|
||||
<!-- 自定义颜色输入弹窗 -->
|
||||
<wd-popup v-model="showCustomColorInput" position="bottom" :safe-area-inset-bottom="true">
|
||||
<view class="custom-color-popup">
|
||||
<view class="popup-header">
|
||||
<text class="popup-title">自定义主题色</text>
|
||||
<wd-icon name="close" size="20" color="#999" @click="showCustomColorInput = false" />
|
||||
</view>
|
||||
|
||||
<wd-divider />
|
||||
|
||||
<view class="color-input-section">
|
||||
<view class="input-label">请输入十六进制颜色值</view>
|
||||
<view class="input-container">
|
||||
<view class="color-preview-small" :style="{ backgroundColor: customColor }"></view>
|
||||
<wd-input
|
||||
v-model="customColor"
|
||||
placeholder="例如: #165DFF"
|
||||
:maxlength="7"
|
||||
class="color-input"
|
||||
/>
|
||||
</view>
|
||||
<view class="input-tip">支持格式:#RGB 或 #RRGGBB</view>
|
||||
</view>
|
||||
|
||||
<wd-divider />
|
||||
|
||||
<view class="popup-actions">
|
||||
<wd-button type="info" size="large" @click="showCustomColorInput = false">取消</wd-button>
|
||||
<wd-button type="primary" size="large" @click="applyCustomColor">应用</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
<view class="action-section">
|
||||
<wd-button block @click="resetTheme">
|
||||
重置为默认主题
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch } from "vue";
|
||||
import { useThemeStore } from "@/store/modules/theme.store";
|
||||
|
||||
const useTheme = useThemeStore();
|
||||
|
||||
// 创建响应式的计算属性
|
||||
const isDarkMode = computed(() => useTheme.isDark);
|
||||
|
||||
// 自定义颜色输入
|
||||
const customColor = ref("");
|
||||
const showCustomColorInput = ref(false);
|
||||
|
||||
// 当前选中的主题色
|
||||
const currentThemeColor = computed(() => {
|
||||
return useTheme.themeVars.colorTheme || useTheme.themeColorOptions[0].primary;
|
||||
const themeStore = useThemeStore();
|
||||
const isDarkMode = computed({
|
||||
get: () => themeStore.isDark,
|
||||
set: (value) => {
|
||||
themeStore.toggleTheme(value ? 'dark' : 'light');
|
||||
}
|
||||
});
|
||||
const themeOptions = themeStore.themeColorOptions;
|
||||
|
||||
// 选择预设颜色
|
||||
const handleSelectColor = (color: (typeof useTheme.themeColorOptions)[0]) => {
|
||||
useTheme.setCurrentThemeColor(color);
|
||||
customColor.value = color.primary;
|
||||
// 检查颜色是否为当前选中颜色
|
||||
const isActiveColor = (color: string): boolean => {
|
||||
return themeStore.currentThemeColor.primary.toLowerCase() === color.toLowerCase();
|
||||
};
|
||||
|
||||
// 提示
|
||||
// 切换暗黑模式
|
||||
const handleToggleDarkMode = (value: boolean) => {
|
||||
// toggleTheme已经在computed的setter中调用了
|
||||
uni.showToast({
|
||||
title: "主题色已更新",
|
||||
title: value ? "暗黑模式" : "浅色模式",
|
||||
icon: "success",
|
||||
duration: 1500,
|
||||
duration: 1000,
|
||||
});
|
||||
};
|
||||
|
||||
// 显示自定义颜色输入
|
||||
const showCustomInput = () => {
|
||||
showCustomColorInput.value = true;
|
||||
customColor.value = currentThemeColor.value;
|
||||
// 选择主题色
|
||||
const selectColor = (color: typeof themeOptions[0]) => {
|
||||
themeStore.setCurrentThemeColor(color);
|
||||
uni.showToast({ title: "主题色已更新", icon: "success", duration: 1000 });
|
||||
};
|
||||
|
||||
// 应用自定义颜色
|
||||
const applyCustomColor = () => {
|
||||
// 验证颜色格式
|
||||
const colorRegex = /^#([0-9A-F]{6}|[0-9A-F]{3})$/i;
|
||||
if (!colorRegex.test(customColor.value)) {
|
||||
uni.showToast({
|
||||
title: "请输入有效的颜色值",
|
||||
icon: "none",
|
||||
duration: 2000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 转换3位颜色值为6位
|
||||
let color = customColor.value;
|
||||
if (color.length === 4) {
|
||||
color = "#" + color[1] + color[1] + color[2] + color[2] + color[3] + color[3];
|
||||
}
|
||||
|
||||
// 创建自定义主题色选项
|
||||
const customColorOption = {
|
||||
name: "自定义",
|
||||
value: "custom",
|
||||
primary: color,
|
||||
};
|
||||
|
||||
useTheme.setCurrentThemeColor(customColorOption);
|
||||
showCustomColorInput.value = false;
|
||||
|
||||
// 提示
|
||||
uni.showToast({
|
||||
title: "自定义主题色已应用",
|
||||
icon: "success",
|
||||
duration: 1500,
|
||||
});
|
||||
};
|
||||
|
||||
// 重置为默认主题
|
||||
const handleResetTheme = () => {
|
||||
// 重置主题
|
||||
const resetTheme = () => {
|
||||
uni.showModal({
|
||||
title: "确认重置",
|
||||
content: "确定要重置为默认主题吗?",
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
useTheme.setCurrentThemeColor(useTheme.themeColorOptions[0]);
|
||||
customColor.value = useTheme.themeColorOptions[0].primary;
|
||||
|
||||
uni.showToast({
|
||||
title: "已重置为默认主题",
|
||||
icon: "success",
|
||||
duration: 1500,
|
||||
});
|
||||
themeStore.setCurrentThemeColor(themeOptions[0]);
|
||||
uni.showToast({ title: "主题已重置", icon: "success", duration: 1000 });
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 切换暗黑模式
|
||||
const handleToggleDarkMode = () => {
|
||||
useTheme.toggleTheme();
|
||||
nextTick(() => {
|
||||
uni.showToast({
|
||||
title: `已切换到${isDarkMode.value ? "暗黑" : "浅色"}模式`,
|
||||
icon: "success",
|
||||
duration: 1500,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
onLoad(() => {
|
||||
customColor.value = currentThemeColor.value;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
customColor.value = currentThemeColor.value;
|
||||
});
|
||||
|
||||
// 页面显示时确保主题色同步
|
||||
onShow(() => {
|
||||
customColor.value = currentThemeColor.value;
|
||||
// 监听主题变化,强制更新页面
|
||||
watch(() => themeStore.isDark, () => {
|
||||
// 触发页面重新渲染
|
||||
setTimeout(() => {
|
||||
// 强制更新页面
|
||||
}, 100);
|
||||
});
|
||||
</script>
|
||||
|
||||
<route lang="json">
|
||||
{
|
||||
<route lang="json">{
|
||||
"name": "theme",
|
||||
"style": {
|
||||
"navigationBarTitleText": "主题设置"
|
||||
}
|
||||
}
|
||||
</route>
|
||||
}</route>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// 基础布局
|
||||
@@ -273,28 +135,16 @@ onShow(() => {
|
||||
padding: 30rpx 30rpx 20rpx;
|
||||
border-bottom: 1rpx solid var(--wot-color-border, #f0f0f0);
|
||||
|
||||
.section-title {
|
||||
margin-left: 12rpx;
|
||||
.setting-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: var(--wot-color-text, #333);
|
||||
}
|
||||
}
|
||||
|
||||
// 颜色选择区域
|
||||
.color-section {
|
||||
padding: 30rpx;
|
||||
|
||||
.color-label {
|
||||
margin-bottom: 20rpx;
|
||||
font-size: 28rpx;
|
||||
color: var(--wot-color-text-secondary, #666);
|
||||
}
|
||||
|
||||
.color-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20rpx;
|
||||
gap: 24rpx;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
@@ -303,137 +153,25 @@ onShow(() => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: calc(25% - 15rpx);
|
||||
padding: 10rpx;
|
||||
width: calc(33.33% - 16rpx);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
.color-preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
margin-bottom: 8rpx;
|
||||
border-radius: 12rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
.check-icon {
|
||||
font-size: 24rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
text-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
width: 70rpx;
|
||||
height: 70rpx;
|
||||
margin-bottom: 12rpx;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.color-name {
|
||||
font-size: 22rpx;
|
||||
color: var(--wot-color-text-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&.active .color-preview {
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.2);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
&:active .color-preview {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
// 当前主题色显示
|
||||
.current-theme-section {
|
||||
padding: 30rpx;
|
||||
|
||||
.current-theme-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.current-theme-label {
|
||||
font-size: 28rpx;
|
||||
color: var(--wot-color-text-secondary, #666);
|
||||
}
|
||||
|
||||
.current-theme-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.current-color-preview {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border: 2rpx solid var(--wot-color-border, #f0f0f0);
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.current-color-text {
|
||||
margin-left: 10rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义颜色弹窗
|
||||
.custom-color-popup {
|
||||
padding: 40rpx 30rpx;
|
||||
background: var(--wot-popup-bg-color, #fff);
|
||||
border-radius: 20rpx 20rpx 0 0;
|
||||
|
||||
.popup-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 30rpx;
|
||||
|
||||
.popup-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: var(--wot-color-text, #333);
|
||||
}
|
||||
}
|
||||
|
||||
.color-input-section {
|
||||
margin-bottom: 40rpx;
|
||||
|
||||
.input-label {
|
||||
margin-bottom: 20rpx;
|
||||
font-size: 28rpx;
|
||||
color: var(--wot-color-text-secondary, #666);
|
||||
}
|
||||
|
||||
.input-container {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
align-items: center;
|
||||
margin-bottom: 10rpx;
|
||||
|
||||
.color-preview-small {
|
||||
flex-shrink: 0;
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border: 2rpx solid var(--wot-color-border, #f0f0f0);
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.color-input {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.input-tip {
|
||||
margin-left: 80rpx;
|
||||
font-size: 24rpx;
|
||||
color: var(--wot-color-text-placeholder, #999);
|
||||
}
|
||||
}
|
||||
|
||||
.popup-actions {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
}
|
||||
.action-section {
|
||||
padding: 0 30rpx 30rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
+158
-276
@@ -1,128 +1,117 @@
|
||||
<template>
|
||||
<view class="workbench-container">
|
||||
<!-- 顶部统计卡片 -->
|
||||
<view class="stats-section">
|
||||
<view class="app-container theme-adaptive">
|
||||
<!-- 页面标题 -->
|
||||
<view class="page-header">
|
||||
<text class="page-title theme-text-primary">工作台</text>
|
||||
<text class="page-subtitle theme-text-secondary">欢迎回来~</text>
|
||||
</view>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<wd-card class="stats-section">
|
||||
<view class="stats-grid">
|
||||
<view v-for="(stat, index) in statsData" :key="index" class="stat-card">
|
||||
<view class="stat-icon" :style="{ backgroundColor: stat.color + '20' }">
|
||||
<wd-icon :name="stat.icon" :color="stat.color" size="24" />
|
||||
<view v-for="(item, index) in statsData" :key="index" class="stats-card theme-card"
|
||||
@click="handleStatClick(item)">
|
||||
<view class="stats-icon" :style="{ background: item.iconBgColor }">
|
||||
<wd-icon :name="item.icon" size="24" />
|
||||
</view>
|
||||
<view class="stat-content">
|
||||
<text class="stat-number">{{ stat.number }}</text>
|
||||
<text class="stat-label">{{ stat.label }}</text>
|
||||
<view class="stats-content">
|
||||
<text class="stats-number theme-text-primary">{{ item.number }}</text>
|
||||
<text class="stats-label theme-text-secondary">{{ item.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</wd-card>
|
||||
|
||||
<!-- 快捷入口 -->
|
||||
<view class="quick-actions-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">快捷入口</text>
|
||||
<text class="section-subtitle">常用功能一键直达</text>
|
||||
</view>
|
||||
<view class="actions-grid">
|
||||
<view
|
||||
v-for="(action, index) in quickActions"
|
||||
:key="index"
|
||||
class="action-item"
|
||||
@tap="handleQuickAction(action)"
|
||||
>
|
||||
<view class="action-icon" :style="{ backgroundColor: action.color + '15' }">
|
||||
<wd-icon :name="action.icon" :color="action.color" size="28" />
|
||||
</view>
|
||||
<text class="action-name">{{ action.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 快捷操作 -->
|
||||
<wd-card title="快捷操作">
|
||||
<wd-grid :column="4" border>
|
||||
<wd-grid-item v-for="(action, index) in quickActions" :key="index" :icon="action.icon" :text="action.name"
|
||||
icon-size="20" custom-class="custom-item" @click="handleActionClick(action)">
|
||||
</wd-grid-item>
|
||||
</wd-grid>
|
||||
</wd-card>
|
||||
|
||||
<!-- 最近动态 -->
|
||||
<wd-card title="最近动态">
|
||||
<wd-cell-group>
|
||||
<wd-cell v-for="(activity, index) in recentActivities" :key="index" :title="activity.title"
|
||||
:label="activity.label" :value="activity.value" custom-class="activity-cell">
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
</wd-card>
|
||||
|
||||
<!-- 待办事项 -->
|
||||
<view class="todo-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">待办事项</text>
|
||||
<text class="section-subtitle">{{ pendingTodos.length }} 项待处理</text>
|
||||
</view>
|
||||
<view class="todo-list">
|
||||
<view
|
||||
v-for="(todo, index) in pendingTodos"
|
||||
:key="index"
|
||||
class="todo-item"
|
||||
@tap="handleTodoItem(todo)"
|
||||
>
|
||||
<view class="todo-priority" :class="'priority-' + todo.priority">
|
||||
<view class="priority-dot"></view>
|
||||
</view>
|
||||
<view class="todo-content">
|
||||
<text class="todo-title">{{ todo.title }}</text>
|
||||
<text class="todo-time">{{ todo.time }}</text>
|
||||
</view>
|
||||
<wd-icon name="arrow-right" color="#999" size="16" />
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="pendingTodos.length === 0" class="empty-todos">
|
||||
<wd-icon name="smile" color="#999" size="48" />
|
||||
<text class="empty-text">暂无待办事项</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
// 统计数据
|
||||
const statsData = ref([
|
||||
{ icon: "user", number: "1,234", label: "用户总数", color: "#165DFF" },
|
||||
{ icon: "chart-bar", number: "856", label: "今日活跃", color: "#00B42A" },
|
||||
{ icon: "calendar", number: "42", label: "待处理订单", color: "#FF7D00" },
|
||||
{ icon: "warning", number: "8", label: "系统告警", color: "#F53F3F" },
|
||||
]);
|
||||
{
|
||||
number: '1,234',
|
||||
label: '用户总数',
|
||||
color: '#165DFF',
|
||||
iconBgColor: '#e1efff',
|
||||
icon: 'user',
|
||||
path: '/pages/user/index'
|
||||
},
|
||||
{
|
||||
number: '856',
|
||||
label: '今日活跃',
|
||||
color: '#00B42A',
|
||||
iconBgColor: '#e1fcef',
|
||||
icon: 'app',
|
||||
path: '/pages/user/active'
|
||||
},
|
||||
{
|
||||
number: '42',
|
||||
label: '待处理',
|
||||
color: '#FF7D00',
|
||||
iconBgColor: '#fff0e1',
|
||||
icon: 'chart',
|
||||
path: '/pages/order/pending'
|
||||
},
|
||||
{
|
||||
number: '8',
|
||||
label: '系统消息',
|
||||
color: '#F53F3F',
|
||||
iconBgColor: '#ffe1e1',
|
||||
icon: 'warning',
|
||||
path: '/pages/message/index'
|
||||
}
|
||||
])
|
||||
|
||||
// 快捷入口
|
||||
// 快捷操作
|
||||
const quickActions = ref([
|
||||
{ name: "用户管理", icon: "user", color: "#165DFF", path: "/pages/work/user/index" },
|
||||
{ name: "角色管理", icon: "usergroup", color: "#00B42A", path: "/pages/work/role/index" },
|
||||
{ name: "菜单管理", icon: "app", color: "#FF7D00", path: "/pages/work/menu/index" },
|
||||
{ name: "部门管理", icon: "fork", color: "#FFC53D", path: "/pages/work/department/index" },
|
||||
{ name: "岗位管理", icon: "user-avatar", color: "#52C41A", path: "/pages/work/job/index" },
|
||||
{ name: "日志管理", icon: "link", color: "#13C2C2", path: "/pages/work/log/index" },
|
||||
{ name: "系统配置", icon: "setting", color: "#722ED1", path: "/pages/work/settings/index" },
|
||||
{ name: "字典管理", icon: "books", color: "#FA541C", path: "/pages/work/dictionary/index" },
|
||||
{ name: "任务管理", icon: "clock", color: "#F5222D", path: "/pages/work/task/index" },
|
||||
{ name: "通知公告", icon: "notification", color: "#FAAD14", path: "/pages/work/notice/index" },
|
||||
{ name: "帮助中心", icon: "help", color: "#2F54EB", path: "/pages/work/help/index" },
|
||||
{ name: "系统监控", icon: "dashboard", color: "#B37FEB", path: "/pages/work/monitor/index" },
|
||||
]);
|
||||
{ name: '用户管理', icon: 'user', path: '/pages/user/index' },
|
||||
{ name: '角色管理', icon: 'usergroup', path: '/pages/role/index' },
|
||||
{ name: '部门管理', icon: 'fork', path: '/pages/dept/index' },
|
||||
{ name: '系统设置', icon: 'setting', path: '/pages/settings/index' }
|
||||
])
|
||||
|
||||
// 待办事项
|
||||
const pendingTodos = ref([
|
||||
{ title: "审核新用户注册申请", time: "2小时前", priority: "high", type: "user" },
|
||||
{ title: "处理订单退款请求", time: "3小时前", priority: "medium", type: "order" },
|
||||
{ title: "更新系统权限配置", time: "1天前", priority: "low", type: "config" },
|
||||
{ title: "回复用户反馈", time: "2天前", priority: "medium", type: "feedback" },
|
||||
]);
|
||||
// 最近动态
|
||||
const recentActivities = ref([
|
||||
{ title: '新用户注册', label: '李四', value: '刚刚' },
|
||||
{ title: '订单完成', label: '宫保鸡丁', value: '3分钟前' },
|
||||
{ title: '系统更新', label: '版本v2.1.1', value: '8分钟前' },
|
||||
{ title: '用户反馈', label: '界面优化建议', value: '12分钟前' }
|
||||
])
|
||||
|
||||
// 处理方法
|
||||
const handleQuickAction = (action: any) => {
|
||||
uni.navigateTo({
|
||||
url: action.path,
|
||||
});
|
||||
};
|
||||
|
||||
const handleTodoItem = (todo: any) => {
|
||||
const handleStatClick = (item: any) => {
|
||||
uni.showToast({
|
||||
title: `处理:${todo.title}`,
|
||||
icon: "none",
|
||||
});
|
||||
};
|
||||
title: `点击了 ${item.label}`,
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
const loadWorkbenchData = async () => {
|
||||
// 这里可以添加实际的API调用
|
||||
console.log("加载工作台数据...");
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadWorkbenchData();
|
||||
});
|
||||
const handleActionClick = (action: any) => {
|
||||
uni.showToast({
|
||||
title: `跳转到 ${action.name}`,
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<route lang="json">
|
||||
@@ -139,181 +128,74 @@ onMounted(() => {
|
||||
</route>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.workbench-container {
|
||||
min-height: 100vh;
|
||||
padding: 20rpx;
|
||||
background-color: #f5f5f5;
|
||||
.page-header {
|
||||
margin-bottom: 40rpx;
|
||||
text-align: center;
|
||||
|
||||
.page-title {
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.stats-section {
|
||||
margin-bottom: 30rpx;
|
||||
|
||||
.stats-grid {
|
||||
padding: 12rpx;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
background: var(--card-bg);
|
||||
border: 1rpx solid var(--border-color);
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.stats-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
margin-right: 20rpx;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.stats-content {
|
||||
flex: 1;
|
||||
|
||||
.stats-number {
|
||||
display: block;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: var(--text-color);
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.stats-label {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-color-2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 20rpx;
|
||||
.quick-actions {
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
margin-right: 20rpx;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.section-subtitle {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.quick-actions-section,
|
||||
.todo-section,
|
||||
.recent-section {
|
||||
padding: 30rpx;
|
||||
margin-bottom: 30rpx;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.actions-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 30rpx;
|
||||
}
|
||||
|
||||
.action-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20rpx 0;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
margin-bottom: 16rpx;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.action-name {
|
||||
font-size: 24rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.todo-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.todo-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.todo-priority {
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.priority-dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.priority-high .priority-dot {
|
||||
background-color: #f53f3f;
|
||||
}
|
||||
|
||||
.priority-medium .priority-dot {
|
||||
background-color: #ff7d00;
|
||||
}
|
||||
|
||||
.priority-low .priority-dot {
|
||||
background-color: #00b42a;
|
||||
}
|
||||
|
||||
.todo-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.todo-title {
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.todo-time {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.empty-todos {
|
||||
padding: 60rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
display: block;
|
||||
margin-top: 20rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
// 深色模式适配
|
||||
:deep(.dark) .workbench-container {
|
||||
background-color: #1a1a1a;
|
||||
}
|
||||
|
||||
:deep(.dark) .stat-card,
|
||||
:deep(.dark) .quick-actions-section,
|
||||
:deep(.dark) .todo-section {
|
||||
background-color: #2a2a2a;
|
||||
}
|
||||
|
||||
:deep(.dark) .todo-item {
|
||||
background-color: #333;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -32,7 +32,19 @@ const router = createRouter({
|
||||
|
||||
// 全局前置守卫
|
||||
router.beforeEach((to, from, next) => {
|
||||
console.log("🚀 路由守卫触发:", {
|
||||
to: to.path,
|
||||
from: from.path,
|
||||
requireAuth: to.meta?.requireAuth,
|
||||
});
|
||||
|
||||
if (to.meta && to.meta.requireAuth && !isLoggedIn()) {
|
||||
console.log("🔒 需要登录,但用户未登录");
|
||||
|
||||
// 先阻止当前导航
|
||||
next(false);
|
||||
|
||||
// 然后显示登录提示
|
||||
uni.showModal({
|
||||
title: "提示",
|
||||
content: "该功能需要登录后使用",
|
||||
@@ -53,9 +65,11 @@ router.beforeEach((to, from, next) => {
|
||||
});
|
||||
}
|
||||
},
|
||||
// 确保在取消弹窗时也能调用 next
|
||||
fail: () => {
|
||||
next(false);
|
||||
// 失败时也返回首页
|
||||
uni.switchTab({
|
||||
url: "/pages/index/index",
|
||||
});
|
||||
},
|
||||
});
|
||||
} else {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.0 MiB After Width: | Height: | Size: 3.3 MiB |
@@ -1,14 +1,7 @@
|
||||
import { defineStore } from "pinia";
|
||||
import UserAPI, { type UserInfo } from "@/api/user";
|
||||
import AuthAPI, { type LoginFormData, type LoginResult, type LogoutBody } from "@/api/auth";
|
||||
import {
|
||||
getAccessToken,
|
||||
setAccessToken,
|
||||
setRefreshToken,
|
||||
clearAll,
|
||||
getUserInfo,
|
||||
setUserInfo,
|
||||
} from "@/utils/auth";
|
||||
import { getAccessToken, setAccessToken, setRefreshToken, clearAll, getUserInfo, setUserInfo } from "@/utils/auth";
|
||||
|
||||
export const useUserStore = defineStore("appUserInfo", () => {
|
||||
const userInfo = ref<UserInfo | undefined>(getUserInfo());
|
||||
|
||||
+258
-216
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* 主样式文件
|
||||
* 统一管理所有样式,保持简洁清晰
|
||||
*/
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
@@ -7,256 +12,293 @@ body,
|
||||
}
|
||||
|
||||
.app-container {
|
||||
padding: 10rpx 10rpx;
|
||||
padding: 16rpx;
|
||||
|
||||
:deep(.custom-item) {
|
||||
height: 80px !important;
|
||||
height: 100px !important;
|
||||
}
|
||||
:deep(.wd-card) {
|
||||
margin: 10rpx 0 !important;
|
||||
margin: 16rpx 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局样式变量
|
||||
*/
|
||||
|
||||
/*
|
||||
* 主题颜色 - 会被theme.ts中的动态设置覆盖
|
||||
* 这里作为默认值和IDE提示
|
||||
*/
|
||||
/* ===== 全局主题变量 ===== */
|
||||
:root {
|
||||
/* 主色 */
|
||||
/* 主题色系 */
|
||||
--primary-color: #165dff;
|
||||
--primary-color-light: #94bfff;
|
||||
--primary-color-dark: #0e3c9b;
|
||||
--wot-color-theme: #165dff;
|
||||
|
||||
/* 功能色 */
|
||||
--success-color: #0fc6c2;
|
||||
/* 功能色系 */
|
||||
--success-color: #07c160;
|
||||
--warning-color: #ff7d00;
|
||||
--danger-color: #f5222d;
|
||||
--danger-color: #f53f3f;
|
||||
--info-color: #86909c;
|
||||
|
||||
/* 文字颜色 */
|
||||
--text-primary: #1d2129;
|
||||
--text-regular: #4e5969;
|
||||
--text-secondary: #86909c;
|
||||
--text-placeholder: #c9cdd4;
|
||||
--text-inverse: #ffffff;
|
||||
--text-color: #1d2129;
|
||||
--text-color-2: #4e5969;
|
||||
--text-color-3: #86909c;
|
||||
--text-color-placeholder: #c9cdd4;
|
||||
--text-color-inverse: #ffffff;
|
||||
|
||||
/* 背景颜色 */
|
||||
--bg-color: #ffffff;
|
||||
--bg-color-2: #f8f8f8;
|
||||
--bg-color-3: #f2f3f5;
|
||||
--card-bg-color: #ffffff;
|
||||
|
||||
/* 边框颜色 */
|
||||
--border-color: #e5e6eb;
|
||||
--border-light: #f2f3f5;
|
||||
--border-color-light: #f2f3f5;
|
||||
|
||||
/* 背景颜色 */
|
||||
--bg-white: #ffffff;
|
||||
--bg-light: #f2f3f5;
|
||||
--bg-gray: #f7f8fa;
|
||||
/* 阴影 */
|
||||
--shadow-light: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||
--shadow-medium: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
|
||||
--shadow-heavy: 0 8rpx 32rpx rgba(0, 0, 0, 0.15);
|
||||
|
||||
/* 圆角 */
|
||||
--radius-sm: 4rpx;
|
||||
--radius-md: 8rpx;
|
||||
--radius-lg: 16rpx;
|
||||
--radius-xl: 24rpx;
|
||||
--radius-full: 9999rpx;
|
||||
|
||||
/* 间距 */
|
||||
--spacing-xs: 8rpx;
|
||||
--spacing-sm: 16rpx;
|
||||
--spacing-md: 24rpx;
|
||||
--spacing-lg: 32rpx;
|
||||
--spacing-xl: 48rpx;
|
||||
--spacing-xxl: 64rpx;
|
||||
|
||||
/* 字体大小 */
|
||||
--font-xs: 24rpx;
|
||||
--font-sm: 28rpx;
|
||||
--font-md: 32rpx;
|
||||
--font-lg: 36rpx;
|
||||
--font-xl: 40rpx;
|
||||
--font-xxl: 48rpx;
|
||||
--font-xxxl: 56rpx;
|
||||
|
||||
/* 动画 */
|
||||
--transition-fast: 0.15s ease;
|
||||
--transition-normal: 0.3s ease;
|
||||
--transition-slow: 0.5s ease;
|
||||
}
|
||||
|
||||
/**
|
||||
* 主题相关的通用类
|
||||
*/
|
||||
/* ===== 暗黑模式变量 ===== */
|
||||
.dark {
|
||||
--text-color: #ffffff;
|
||||
--text-color-2: #e0e0e0;
|
||||
--text-color-3: #a0a0a0;
|
||||
--text-color-placeholder: #606060;
|
||||
--text-color-inverse: #1d2129;
|
||||
|
||||
/* 主色文本 */
|
||||
--bg-color: #0f0f0f;
|
||||
--bg-color-2: #1a1a1a;
|
||||
--bg-color-3: #242424;
|
||||
--card-bg-color: #1a1a1a;
|
||||
|
||||
--border-color: #2f2f2f;
|
||||
--border-color-light: #3d3d3d;
|
||||
|
||||
--shadow-light: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
|
||||
--shadow-medium: 0 4rpx 16rpx rgba(0, 0, 0, 0.4);
|
||||
--shadow-heavy: 0 8rpx 32rpx rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
/* ===== 主题适配类 ===== */
|
||||
.theme-adaptive {
|
||||
color: var(--text-color);
|
||||
background-color: var(--bg-color);
|
||||
border-color: var(--border-color);
|
||||
transition:
|
||||
color var(--transition-normal),
|
||||
background-color var(--transition-normal),
|
||||
border-color var(--transition-normal);
|
||||
}
|
||||
|
||||
.theme-card {
|
||||
background-color: var(--card-bg-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-light);
|
||||
transition: all var(--transition-normal);
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--shadow-medium);
|
||||
}
|
||||
}
|
||||
|
||||
.theme-text-primary {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.theme-text-secondary {
|
||||
color: var(--text-color-2);
|
||||
}
|
||||
|
||||
.theme-text-tertiary {
|
||||
color: var(--text-color-3);
|
||||
}
|
||||
|
||||
.theme-text-muted {
|
||||
color: var(--text-color-placeholder);
|
||||
}
|
||||
|
||||
.theme-text-inverse {
|
||||
color: var(--text-color-inverse);
|
||||
}
|
||||
|
||||
/* ===== 常用工具类 ===== */
|
||||
.text-primary {
|
||||
color: var(--primary-color) !important;
|
||||
}
|
||||
.text-success {
|
||||
color: var(--success-color) !important;
|
||||
}
|
||||
.text-warning {
|
||||
color: var(--warning-color) !important;
|
||||
}
|
||||
.text-danger {
|
||||
color: var(--danger-color) !important;
|
||||
}
|
||||
.text-info {
|
||||
color: var(--info-color) !important;
|
||||
}
|
||||
.text-muted {
|
||||
color: var(--text-color-3) !important;
|
||||
}
|
||||
|
||||
/* 主色背景 */
|
||||
.bg-primary {
|
||||
color: #fff;
|
||||
color: var(--text-color-inverse);
|
||||
background-color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
/* 主色边框 */
|
||||
.border-primary {
|
||||
border-color: var(--primary-color) !important;
|
||||
.bg-card {
|
||||
background-color: var(--card-bg-color) !important;
|
||||
}
|
||||
.bg-light {
|
||||
background-color: var(--bg-color-2) !important;
|
||||
}
|
||||
|
||||
/* 主色按钮样式 */
|
||||
.btn-primary {
|
||||
color: #fff;
|
||||
background-color: var(--primary-color);
|
||||
border: none;
|
||||
border-radius: 8rpx;
|
||||
transition: opacity 0.3s;
|
||||
|
||||
&:active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
/* 圆角按钮 */
|
||||
.btn-rounded {
|
||||
border-radius: 45rpx !important;
|
||||
}
|
||||
|
||||
/* 次级按钮 */
|
||||
.btn-secondary {
|
||||
color: var(--primary-color);
|
||||
background-color: #fff;
|
||||
border: 1px solid var(--primary-color);
|
||||
border-radius: 8rpx;
|
||||
transition: background-color 0.3s;
|
||||
|
||||
&:active {
|
||||
background-color: rgba(22, 93, 255, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 字体大小
|
||||
*/
|
||||
.font-xs {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.font-sm {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.font-md {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.font-lg {
|
||||
font-size: 36rpx;
|
||||
}
|
||||
|
||||
.font-xl {
|
||||
font-size: 40rpx;
|
||||
}
|
||||
|
||||
/**
|
||||
* 边距辅助类
|
||||
*/
|
||||
.mt-10 {
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
.mt-20 {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
.mt-30 {
|
||||
margin-top: 30rpx;
|
||||
}
|
||||
.mt-40 {
|
||||
margin-top: 40rpx;
|
||||
}
|
||||
|
||||
.mb-10 {
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
.mb-20 {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
.mb-30 {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
.mb-40 {
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.ml-10 {
|
||||
margin-left: 10rpx;
|
||||
}
|
||||
.ml-20 {
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
.ml-30 {
|
||||
margin-left: 30rpx;
|
||||
}
|
||||
.ml-40 {
|
||||
margin-left: 40rpx;
|
||||
}
|
||||
|
||||
.mr-10 {
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
.mr-20 {
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
.mr-30 {
|
||||
margin-right: 30rpx;
|
||||
}
|
||||
.mr-40 {
|
||||
margin-right: 40rpx;
|
||||
}
|
||||
|
||||
.p-10 {
|
||||
padding: 10rpx;
|
||||
}
|
||||
.p-20 {
|
||||
padding: 20rpx;
|
||||
}
|
||||
.p-30 {
|
||||
padding: 30rpx;
|
||||
}
|
||||
.p-40 {
|
||||
padding: 40rpx;
|
||||
}
|
||||
|
||||
/**
|
||||
* 布局辅助类
|
||||
*/
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
.flex-wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.flex-column {
|
||||
flex-direction: column;
|
||||
}
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
.items-start {
|
||||
align-items: flex-start;
|
||||
}
|
||||
.items-end {
|
||||
align-items: flex-end;
|
||||
}
|
||||
.justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
.justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.justify-around {
|
||||
justify-content: space-around;
|
||||
}
|
||||
.justify-start {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.justify-end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/**
|
||||
* 其他辅助类
|
||||
*/
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.rounded-sm {
|
||||
border-radius: 4rpx;
|
||||
}
|
||||
.rounded {
|
||||
border-radius: 8rpx;
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
.rounded-lg {
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
.rounded-xl {
|
||||
border-radius: 24rpx;
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
.rounded-full {
|
||||
border-radius: 9999rpx;
|
||||
border-radius: var(--radius-full);
|
||||
}
|
||||
|
||||
.shadow {
|
||||
box-shadow: var(--shadow-light);
|
||||
}
|
||||
.shadow-lg {
|
||||
box-shadow: var(--shadow-medium);
|
||||
}
|
||||
|
||||
.transition {
|
||||
transition: all var(--transition-normal);
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex !important;
|
||||
}
|
||||
.items-center {
|
||||
align-items: center !important;
|
||||
}
|
||||
.justify-center {
|
||||
justify-content: center !important;
|
||||
}
|
||||
.justify-between {
|
||||
justify-content: space-between !important;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center !important;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
|
||||
/* ===== 间距工具类 ===== */
|
||||
.p-xs {
|
||||
padding: var(--spacing-xs) !important;
|
||||
}
|
||||
.p-sm {
|
||||
padding: var(--spacing-sm) !important;
|
||||
}
|
||||
.p-md {
|
||||
padding: var(--spacing-md) !important;
|
||||
}
|
||||
.p-lg {
|
||||
padding: var(--spacing-lg) !important;
|
||||
}
|
||||
.p-xl {
|
||||
padding: var(--spacing-xl) !important;
|
||||
}
|
||||
|
||||
.m-xs {
|
||||
margin: var(--spacing-xs) !important;
|
||||
}
|
||||
.m-sm {
|
||||
margin: var(--spacing-sm) !important;
|
||||
}
|
||||
.m-md {
|
||||
margin: var(--spacing-md) !important;
|
||||
}
|
||||
.m-lg {
|
||||
margin: var(--spacing-lg) !important;
|
||||
}
|
||||
.m-xl {
|
||||
margin: var(--spacing-xl) !important;
|
||||
}
|
||||
|
||||
.mt-sm {
|
||||
margin-top: var(--spacing-sm) !important;
|
||||
}
|
||||
.mt-md {
|
||||
margin-top: var(--spacing-md) !important;
|
||||
}
|
||||
.mt-lg {
|
||||
margin-top: var(--spacing-lg) !important;
|
||||
}
|
||||
.mt-xl {
|
||||
margin-top: var(--spacing-xl) !important;
|
||||
}
|
||||
|
||||
.mb-sm {
|
||||
margin-bottom: var(--spacing-sm) !important;
|
||||
}
|
||||
.mb-md {
|
||||
margin-bottom: var(--spacing-md) !important;
|
||||
}
|
||||
.mb-lg {
|
||||
margin-bottom: var(--spacing-lg) !important;
|
||||
}
|
||||
.mb-xl {
|
||||
margin-bottom: var(--spacing-xl) !important;
|
||||
}
|
||||
|
||||
/* ===== 字体大小类 ===== */
|
||||
.text-xs {
|
||||
font-size: var(--font-xs) !important;
|
||||
}
|
||||
.text-sm {
|
||||
font-size: var(--font-sm) !important;
|
||||
}
|
||||
.text-md {
|
||||
font-size: var(--font-md) !important;
|
||||
}
|
||||
.text-lg {
|
||||
font-size: var(--font-lg) !important;
|
||||
}
|
||||
.text-xl {
|
||||
font-size: var(--font-xl) !important;
|
||||
}
|
||||
.text-2xl {
|
||||
font-size: var(--font-xxl) !important;
|
||||
}
|
||||
|
||||
Vendored
+10
@@ -10,6 +10,7 @@ declare global {
|
||||
const EffectScope: typeof import('vue')['EffectScope']
|
||||
const Storage: typeof import('../utils/storage')['Storage']
|
||||
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
|
||||
const applyCSSVariables: typeof import('../utils/theme')['applyCSSVariables']
|
||||
const applyThemeOnPageShow: typeof import('../utils/theme')['applyThemeOnPageShow']
|
||||
const applyThemeToMiniProgram: typeof import('../utils/theme')['applyThemeToMiniProgram']
|
||||
const auth: typeof import('../api/auth')['default']
|
||||
@@ -35,25 +36,33 @@ declare global {
|
||||
const effectScope: typeof import('vue')['effectScope']
|
||||
const extendedColorOptions: typeof import('../composables/useTheme')['extendedColorOptions']
|
||||
const file: typeof import('../api/file')['default']
|
||||
const generateCSSVariables: typeof import('../utils/theme')['generateCSSVariables']
|
||||
const generateColorVariants: typeof import('../utils/theme')['generateColorVariants']
|
||||
const generateGradientBackground: typeof import('../utils/theme')['generateGradientBackground']
|
||||
const getAccessToken: typeof import('../utils/auth')['getAccessToken']
|
||||
const getActivePinia: typeof import('pinia')['getActivePinia']
|
||||
const getContrastColor: typeof import('../utils/theme')['getContrastColor']
|
||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||
const getDarkerColor: typeof import('../utils/colorUtils')['getDarkerColor']
|
||||
const getLighterColor: typeof import('../utils/colorUtils')['getLighterColor']
|
||||
const getRefreshToken: typeof import('../utils/auth')['getRefreshToken']
|
||||
const getThemeClassName: typeof import('../utils/theme')['getThemeClassName']
|
||||
const getThemeColor: typeof import('../utils/theme')['getThemeColor']
|
||||
const getThemeColors: typeof import('../utils/theme')['getThemeColors']
|
||||
const getToken: typeof import('../utils/storage')['getToken']
|
||||
const getUserInfo: typeof import('../utils/auth')['getUserInfo']
|
||||
const guessSerializerType: typeof import('@uni-helper/uni-use')['guessSerializerType']
|
||||
const h: typeof import('vue')['h']
|
||||
const initTheme: typeof import('../composables/useTheme')['initTheme']
|
||||
const inject: typeof import('vue')['inject']
|
||||
const isDarkMode: typeof import('../utils/theme')['isDarkMode']
|
||||
const isLoggedIn: typeof import('../utils/auth')['isLoggedIn']
|
||||
const isProxy: typeof import('vue')['isProxy']
|
||||
const isReactive: typeof import('vue')['isReactive']
|
||||
const isReadonly: typeof import('vue')['isReadonly']
|
||||
const isRef: typeof import('vue')['isRef']
|
||||
const isValidColor: typeof import('../utils/theme')['isValidColor']
|
||||
const log: typeof import('../api/log')['default']
|
||||
const mapActions: typeof import('pinia')['mapActions']
|
||||
const mapGetters: typeof import('pinia')['mapGetters']
|
||||
@@ -63,6 +72,7 @@ declare global {
|
||||
const markRaw: typeof import('vue')['markRaw']
|
||||
const menu: typeof import('../api/menu')['default']
|
||||
const nextTick: typeof import('vue')['nextTick']
|
||||
const normalizeColor: typeof import('../utils/theme')['normalizeColor']
|
||||
const notice: typeof import('../api/notice')['default']
|
||||
const onActivated: typeof import('vue')['onActivated']
|
||||
const onAddToFavorites: typeof import('@dcloudio/uni-app')['onAddToFavorites']
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"light": {
|
||||
"bgColor": "#F8F8F8",
|
||||
"bgColorBottom": "#F8F8F8",
|
||||
"bgColorTop": "#F8F8F8",
|
||||
"bgTxtStyle": "dark",
|
||||
"navBgColor": "#FFF",
|
||||
"navTxtStyle": "black",
|
||||
"tabBgColor": "#ffffff",
|
||||
"tabBorderStyle": "black",
|
||||
"tabColor": "#bfbfbf",
|
||||
"tabSelectedColor": "#0165FF"
|
||||
},
|
||||
"dark": {
|
||||
"bgColor": "#000",
|
||||
"bgColorBottom": "#000",
|
||||
"bgColorTop": "#000",
|
||||
"bgTxtStyle": "light",
|
||||
"navBgColor": "#000000",
|
||||
"navTxtStyle": "white",
|
||||
"tabBgColor": "#1a1a1a",
|
||||
"tabBorderStyle": "white",
|
||||
"tabColor": "#bfbfbf",
|
||||
"tabSelectedColor": "#0165FF"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 235 KiB After Width: | Height: | Size: 199 KiB |
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<dev>
|
||||
开发中...
|
||||
</dev>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
Reference in New Issue
Block a user