commit 8e1b750300433893abd7a2fa35f4998ae8415b6e Author: 27942 Date: Thu Jan 22 18:26:47 2026 +0800 haha diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a8487f7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,47 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +venv/ +env/ +ENV/ +*.egg-info/ +dist/ +build/ + +# Flask +instance/ +.webassets-cache + +# Database +*.db +*.sqlite +*.sqlite3 + +# Environment +.env +.env.local + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# Node +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# Frontend build +frontend/dist/ +frontend/.vite/ + +# OS +.DS_Store +Thumbs.db diff --git a/111 b/111 new file mode 100644 index 0000000..98f77eb --- /dev/null +++ b/111 @@ -0,0 +1,18 @@ +cd backend +python app.py + + + +cd frontend +npm install +npm run dev + + + + + + + + + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..17a6217 --- /dev/null +++ b/README.md @@ -0,0 +1,389 @@ +# Claude Code 蓝星 - 完整网站项目 + +这是一个基于 Vue 3 + Flask 的完整网站项目,包含用户注册登录、令牌管理、数据看板、使用日志、充值等功能。 + +## 项目结构 + +``` +. +├── backend/ # Flask 后端 +│ ├── app.py # 应用入口 +│ ├── config.py # 配置文件 +│ ├── requirements.txt # Python 依赖 +│ ├── models/ # 数据模型 +│ │ ├── user.py # 用户模型 +│ │ ├── token.py # 令牌模型 +│ │ ├── usage_log.py # 使用日志模型 +│ │ └── ... +│ └── routes/ # API 路由 +│ ├── auth.py # 认证相关 +│ ├── tokens.py # 令牌管理 +│ ├── dashboard.py # 数据看板 +│ └── ... +├── frontend/ # Vue 3 前端 +│ ├── src/ +│ │ ├── api/ # API 接口 +│ │ ├── views/ # 页面组件 +│ │ ├── components/ # 通用组件 +│ │ ├── stores/ # Pinia 状态管理 +│ │ ├── router/ # 路由配置 +│ │ └── ... +│ ├── package.json +│ └── vite.config.js +└── README.md +``` + +## 功能特性 + +### 前端功能 +- ✅ 首页展示(服务介绍、分组选择、功能特色) +- ✅ 用户注册/登录 +- ✅ 模型广场(模型浏览、筛选、搜索) +- ✅ 控制台数据看板 +- ✅ 令牌管理(增删改查、批量操作) +- ✅ 使用日志查询 +- ✅ 账户充值(支付宝/微信、兑换码) +- ✅ 邀请奖励系统 +- ✅ 个人设置 + +### 后端功能 +- ✅ JWT 认证系统 +- ✅ 用户注册/登录 API +- ✅ 模型管理 API(列表、筛选、搜索) +- ✅ 令牌管理 API +- ✅ 数据统计 API +- ✅ 使用日志 API +- ✅ 充值订单 API +- ✅ 邀请奖励 API + +## 技术栈 + +### 前端 +- Vue 3 (Composition API) +- Vue Router 4 +- Pinia (状态管理) +- Element Plus (UI 组件库) +- Axios (HTTP 客户端) +- Vite (构建工具) + +### 后端 +- Flask +- Flask-SQLAlchemy (ORM) +- Flask-JWT-Extended (JWT 认证) +- Flask-CORS (跨域支持) +- bcrypt (密码加密) +- SQLite/MySQL (数据库) + +## 快速开始 + +### 后端设置 + +1. 进入后端目录: +```bash +cd backend +``` + +2. 创建虚拟环境(推荐): +```bash +python -m venv venv +# Windows +venv\Scripts\activate +# Linux/Mac +source venv/bin/activate +``` + +3. 安装依赖: +```bash +pip install -r requirements.txt +``` + +4. 配置环境变量(可选): +创建 `.env` 文件: +``` +SECRET_KEY=your-secret-key +JWT_SECRET_KEY=your-jwt-secret-key +DATABASE_URL=sqlite:///claude_code.db +CORS_ORIGINS=http://localhost:5173 +``` + +5. 运行后端服务: +```bash +python app.py +``` + +后端服务将在 `http://localhost:5000` 启动。 + +### 前端设置 + +1. 进入前端目录: +```bash +cd frontend +``` + +2. 安装依赖: +```bash +npm install +``` + +3. 启动开发服务器: +```bash +npm run dev +``` + +前端服务将在 `http://localhost:5173` 启动,并且已经配置为允许所有IP访问。 + +**从其他设备访问:** +- 确保前端和后端服务器在同一网络下 +- 使用服务器IP地址访问:`http://<服务器IP>:5173` +- 例如:`http://192.168.1.100:5173` +- 后端API已配置为允许跨域访问 + +## API 文档 + +### 认证相关 + +#### 用户注册 +``` +POST /api/auth/register +Body: { + "username": "string", + "password": "string", + "confirm_password": "string", + "invite_code": "string" (可选) +} +``` + +#### 用户登录 +``` +POST /api/auth/login +Body: { + "username": "string", + "password": "string" +} +``` + +#### 获取当前用户 +``` +GET /api/auth/me +Headers: { + "Authorization": "Bearer {token}" +} +``` + +### 令牌管理 + +#### 获取令牌列表 +``` +GET /api/tokens?keyword=xxx&key=xxx +Headers: { + "Authorization": "Bearer {token}" +} +``` + +#### 创建令牌 +``` +POST /api/tokens +Headers: { + "Authorization": "Bearer {token}" +} +Body: { + "name": "string", + "group": "string", + "remaining_quota": number (可选), + "total_quota": number (可选), + "ip_restriction": "string" (可选) +} +``` + +#### 更新令牌 +``` +PUT /api/tokens/{id} +Headers: { + "Authorization": "Bearer {token}" +} +Body: { + "name": "string", + ... +} +``` + +#### 删除令牌 +``` +DELETE /api/tokens/{id} +Headers: { + "Authorization": "Bearer {token}" +} +``` + +### 数据看板 + +#### 获取统计数据 +``` +GET /api/dashboard/stats +Headers: { + "Authorization": "Bearer {token}" +} +``` + +### 使用日志 + +#### 获取使用日志 +``` +GET /api/logs/usage?page=1&per_page=20&start_date=xxx&end_date=xxx&token_name=xxx&model_name=xxx&group=xxx +Headers: { + "Authorization": "Bearer {token}" +} +``` + +### 模型广场 + +#### 获取模型列表 +``` +GET /api/models?page=1&per_page=20&provider=xxx&tags=xxx&token_group=xxx&billing_type=xxx&endpoint_type=xxx&search=xxx&show_recharge_price=true&show_multiplier=true +``` + +#### 获取单个模型详情 +``` +GET /api/models/{id} +``` + +#### 获取筛选选项 +``` +GET /api/models/filters +``` + +### 充值相关 + +#### 获取充值信息 +``` +GET /api/recharge/info +Headers: { + "Authorization": "Bearer {token}" +} +``` + +#### 创建充值订单 +``` +POST /api/recharge/create +Headers: { + "Authorization": "Bearer {token}" +} +Body: { + "amount": number, + "payment_method": "alipay" | "wechat" +} +``` + +#### 兑换码充值 +``` +POST /api/recharge/exchange +Headers: { + "Authorization": "Bearer {token}" +} +Body: { + "code": "string" +} +``` + +## 数据库模型 + +### User (用户) +- id: 主键 +- username: 用户名(唯一) +- email: 邮箱(可选) +- password_hash: 密码哈希 +- balance: 账户余额 +- total_consumption: 历史消耗 +- request_count: 请求次数 +- user_group: 用户分组 +- invite_code: 邀请码 +- invited_by: 邀请人ID + +### Token (令牌) +- id: 主键 +- user_id: 用户ID +- name: 令牌名称 +- key: API密钥 +- status: 状态(enabled/disabled) +- remaining_quota: 剩余额度 +- total_quota: 总额度 +- group: 分组 +- available_models: 可用模型 +- ip_restriction: IP限制 +- expires_at: 过期时间 + +### UsageLog (使用日志) +- id: 主键 +- user_id: 用户ID +- token_id: 令牌ID +- log_type: 日志类型 +- model: 模型名称 +- input_tokens: 输入Token数 +- output_tokens: 输出Token数 +- cost: 花费 +- created_at: 创建时间 + +### Model (模型) +- id: 主键 +- name: 模型名称(唯一) +- provider: 供应商(Anthropic、OpenAI等) +- description: 模型描述 +- input_price: 输入价格(每100万tokens) +- output_price: 输出价格(每100万tokens) +- billing_type: 计费类型(pay_as_you_go按量计费、per_request按次计费) +- endpoint_type: 端点类型(anthropic、openai) +- tags: 标签(JSON数组) +- available_groups: 可用分组(JSON数组) +- multiplier: 倍率 +- is_active: 是否激活 + +## 开发说明 + +### 初始化模型数据 + +运行以下命令初始化示例模型数据: + +```bash +cd backend +python init_models.py +``` + +这将创建6个示例模型供测试使用。 + +### 数据库迁移 + +项目使用 SQLite 作为默认数据库,首次运行会自动创建表结构。 + +如需使用 MySQL,修改 `config.py` 中的 `SQLALCHEMY_DATABASE_URI`。 + +### 生产环境部署 + +1. 修改后端配置: + - 设置强密码的 `SECRET_KEY` 和 `JWT_SECRET_KEY` + - 配置生产数据库 + - 设置正确的 `CORS_ORIGINS` + +2. 构建前端: +```bash +cd frontend +npm run build +``` + +3. 部署: + - 后端:使用 Gunicorn 或 uWSGI + - 前端:将 `dist` 目录部署到 Nginx 或其他静态文件服务器 + +## 注意事项 + +1. 默认使用 SQLite 数据库,生产环境建议使用 MySQL 或 PostgreSQL +2. JWT token 默认 24 小时过期,可在 `config.py` 中调整 +3. 充值功能需要集成真实的支付接口(支付宝、微信等) +4. 邀请奖励的返佣比例需要在业务逻辑中实现 + +## 许可证 + +MIT License + +## 联系方式 + +如有问题,请联系管理员微信: cursor2028 diff --git a/backend/app.py b/backend/app.py new file mode 100644 index 0000000..66da846 --- /dev/null +++ b/backend/app.py @@ -0,0 +1,94 @@ +from flask import Flask, jsonify, request +from flask_cors import CORS +from flask_jwt_extended import JWTManager, get_jwt_identity, verify_jwt_in_request +from flask_jwt_extended.exceptions import JWTDecodeError, NoAuthorizationError, InvalidHeaderError +from config import Config +from models import db +from routes.auth import auth_bp +from routes.tokens import tokens_bp +from routes.dashboard import dashboard_bp +from routes.logs import logs_bp +from routes.recharge import recharge_bp +from routes.user import user_bp +from routes.models import models_bp +import logging + +# 配置日志 +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +def create_app(): + app = Flask(__name__) + app.config.from_object(Config) + + # 请求前钩子:记录请求信息(用于调试) + @app.before_request + def log_request(): + if request.path.startswith('/api/') and request.path != '/api/auth/login' and request.path != '/api/auth/register': + auth_header = request.headers.get('Authorization', 'None') + logger.info(f"[{request.method}] {request.path}") + logger.info(f"Authorization Header: {auth_header[:80] if auth_header != 'None' else 'Missing'}") + + # 初始化扩展 + db.init_app(app) + # CORS配置:允许所有来源,并支持credentials + CORS(app, + resources={r"/api/*": { + "origins": "*", + "methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"], + "allow_headers": ["Content-Type", "Authorization"], + "supports_credentials": True + }}, + supports_credentials=True) + jwt = JWTManager(app) + + # JWT错误处理器 + @jwt.expired_token_loader + def expired_token_callback(jwt_header, jwt_payload): + logger.warning(f"Token expired: {jwt_payload}") + return jsonify({'error': 'Token已过期,请重新登录'}), 401 + + @jwt.invalid_token_loader + def invalid_token_callback(error): + auth_header = request.headers.get('Authorization', 'None') + logger.error(f"Invalid token error: {str(error)}") + logger.error(f"Authorization header: {auth_header[:100] if auth_header != 'None' else 'Missing'}") + return jsonify({ + 'error': '无效的Token', + 'detail': str(error), + 'hint': '请检查Authorization头格式是否正确(应为: Bearer )' + }), 422 + + @jwt.unauthorized_loader + def missing_token_callback(error): + logger.warning(f"Missing token: {str(error)}") + auth_header = request.headers.get('Authorization', 'None') + logger.warning(f"Authorization header: {auth_header[:100] if auth_header != 'None' else 'Missing'}") + return jsonify({ + 'error': '缺少认证Token,请先登录', + 'detail': str(error) + }), 401 + + @jwt.needs_fresh_token_loader + def token_not_fresh_callback(jwt_header, jwt_payload): + logger.warning(f"Token not fresh: {jwt_payload}") + return jsonify({'error': 'Token需要刷新'}), 401 + + # 注册蓝图 + app.register_blueprint(auth_bp, url_prefix='/api/auth') + app.register_blueprint(tokens_bp, url_prefix='/api/tokens') + app.register_blueprint(dashboard_bp, url_prefix='/api/dashboard') + app.register_blueprint(logs_bp, url_prefix='/api/logs') + app.register_blueprint(recharge_bp, url_prefix='/api/recharge') + app.register_blueprint(user_bp, url_prefix='/api/user') + app.register_blueprint(models_bp, url_prefix='/api/models') + + # 创建数据库表 + with app.app_context(): + db.create_all() + + return app + +if __name__ == '__main__': + app = create_app() + app.run(debug=True, host='0.0.0.0', port=5000) diff --git a/backend/config.py b/backend/config.py new file mode 100644 index 0000000..8282260 --- /dev/null +++ b/backend/config.py @@ -0,0 +1,22 @@ +import os +from datetime import timedelta + +class Config: + SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production' + SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///claude_code.db' + SQLALCHEMY_TRACK_MODIFICATIONS = False + JWT_SECRET_KEY = os.environ.get('JWT_SECRET_KEY') or 'jwt-secret-key-change-in-production' + JWT_ACCESS_TOKEN_EXPIRES = timedelta(hours=24) + JWT_REFRESH_TOKEN_EXPIRES = timedelta(days=30) + JWT_ALGORITHM = 'HS256' + # Flask-JWT-Extended默认配置,通常不需要显式设置 + # JWT_HEADER_NAME = 'Authorization' # 默认值 + # JWT_HEADER_TYPE = 'Bearer' # 默认值 + JWT_TOKEN_LOCATION = ['headers'] # 从请求头获取token + # CORS配置:如果设置了CORS_ORIGINS环境变量则使用,否则允许所有来源(开发环境) + cors_origins_env = os.environ.get('CORS_ORIGINS') + if cors_origins_env: + CORS_ORIGINS = cors_origins_env.split(',') + else: + # 开发环境:允许所有来源 + CORS_ORIGINS = ['*'] diff --git a/backend/env.example.txt b/backend/env.example.txt new file mode 100644 index 0000000..6ba4544 --- /dev/null +++ b/backend/env.example.txt @@ -0,0 +1,4 @@ +SECRET_KEY=your-secret-key-here +JWT_SECRET_KEY=your-jwt-secret-key-here +DATABASE_URL=sqlite:///claude_code.db +CORS_ORIGINS=http://localhost:5173 diff --git a/backend/init_models.py b/backend/init_models.py new file mode 100644 index 0000000..bb40b14 --- /dev/null +++ b/backend/init_models.py @@ -0,0 +1,114 @@ +""" +初始化模型数据脚本 +运行此脚本可以添加一些示例模型数据 +""" +from app import create_app +from models import db +from models.model import Model +import json + +def init_models(): + app = create_app() + with app.app_context(): + # 检查是否已有数据 + if Model.query.count() > 0: + print("模型数据已存在,跳过初始化") + return + + # 示例模型数据 + models_data = [ + { + 'name': 'claude-haiku-4-5-20251001', + 'provider': 'Anthropic', + 'description': 'Claude Haiku 4.5 是由 anthropic 提供的人工智能模型。', + 'input_price': 0.3000, + 'output_price': 1.5000, + 'billing_type': 'pay_as_you_go', + 'endpoint_type': 'anthropic', + 'tags': ['200k', '工具'], + 'available_groups': ['Claude Code 官方编程模型', 'Claude Code 企业专线'], + 'multiplier': 1.00 + }, + { + 'name': 'claude-sonnet-4-5-20250929', + 'provider': 'Anthropic', + 'description': 'Claude Sonnet 4.5 是由 anthropic 提供的人工智能模型。', + 'input_price': 3.0000, + 'output_price': 15.0000, + 'billing_type': 'pay_as_you_go', + 'endpoint_type': 'anthropic', + 'tags': ['200k', '推理', '工具', '文件'], + 'available_groups': ['Claude Code 官方编程模型', 'Claude Code 企业专线'], + 'multiplier': 1.00 + }, + { + 'name': 'claude-opus-4-5-20251101-thinking', + 'provider': 'Anthropic', + 'description': 'Claude Opus 4.5 with thinking 是由 anthropic 提供的人工智能模型。', + 'input_price': 15.0000, + 'output_price': 75.0000, + 'billing_type': 'pay_as_you_go', + 'endpoint_type': 'anthropic', + 'tags': ['200k', '推理', '工具', '文件'], + 'available_groups': ['Claude Code 企业专线'], + 'multiplier': 1.00 + }, + { + 'name': 'claude-3-5-sonnet-20241022', + 'provider': 'Anthropic', + 'description': 'Claude 3.5 Sonnet 是由 anthropic 提供的人工智能模型。', + 'input_price': 3.0000, + 'output_price': 15.0000, + 'billing_type': 'pay_as_you_go', + 'endpoint_type': 'anthropic', + 'tags': ['200k', '多模态', '工具'], + 'available_groups': ['Claude Code 官方编程模型', 'Claude Code 企业专线'], + 'multiplier': 1.00 + }, + { + 'name': 'claude-3-opus-20240229', + 'provider': 'Anthropic', + 'description': 'Claude 3 Opus 是由 anthropic 提供的人工智能模型。', + 'input_price': 15.0000, + 'output_price': 75.0000, + 'billing_type': 'pay_as_you_go', + 'endpoint_type': 'anthropic', + 'tags': ['200k', '推理', '工具'], + 'available_groups': ['Claude Code 企业专线'], + 'multiplier': 1.00 + }, + { + 'name': 'claude-3-5-haiku-20241022', + 'provider': 'Anthropic', + 'description': 'Claude 3.5 Haiku 是由 anthropic 提供的人工智能模型。', + 'input_price': 0.3000, + 'output_price': 1.5000, + 'billing_type': 'pay_as_you_go', + 'endpoint_type': 'anthropic', + 'tags': ['200k', '工具'], + 'available_groups': ['Claude Code 官方编程模型'], + 'multiplier': 1.00 + } + ] + + for model_data in models_data: + model = Model( + name=model_data['name'], + provider=model_data['provider'], + description=model_data['description'], + input_price=model_data['input_price'], + output_price=model_data['output_price'], + billing_type=model_data['billing_type'], + endpoint_type=model_data['endpoint_type'], + tags=json.dumps(model_data['tags'], ensure_ascii=False), + available_groups=json.dumps(model_data['available_groups'], ensure_ascii=False), + multiplier=model_data['multiplier'], + is_active=True + ) + db.session.add(model) + + db.session.commit() + print(f"成功初始化 {len(models_data)} 个模型") + +if __name__ == '__main__': + init_models() diff --git a/backend/middleware.py b/backend/middleware.py new file mode 100644 index 0000000..f10f8e9 --- /dev/null +++ b/backend/middleware.py @@ -0,0 +1,14 @@ +""" +中间件:用于调试和日志记录 +""" +from flask import request +import logging + +logger = logging.getLogger(__name__) + +def log_request_info(): + """记录请求信息(用于调试)""" + if request.path.startswith('/api/'): + auth_header = request.headers.get('Authorization', 'None') + logger.info(f"Request: {request.method} {request.path}") + logger.info(f"Authorization: {auth_header[:50] if auth_header != 'None' else 'None'}") diff --git a/backend/models/__init__.py b/backend/models/__init__.py new file mode 100644 index 0000000..57dd877 --- /dev/null +++ b/backend/models/__init__.py @@ -0,0 +1,10 @@ +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() + +from .user import User +from .token import Token +from .usage_log import UsageLog +from .recharge_record import RechargeRecord +from .invite_reward import InviteReward +from .model import Model diff --git a/backend/models/invite_reward.py b/backend/models/invite_reward.py new file mode 100644 index 0000000..fee75d3 --- /dev/null +++ b/backend/models/invite_reward.py @@ -0,0 +1,23 @@ +from models import db +from datetime import datetime + +class InviteReward(db.Model): + __tablename__ = 'invite_rewards' + + id = db.Column(db.Integer, primary_key=True) + inviter_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) + invitee_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) + reward_amount = db.Column(db.Numeric(10, 4), nullable=False) # 奖励金额(美元) + status = db.Column(db.String(20), default='pending') # pending, used, transferred + related_recharge_id = db.Column(db.Integer, db.ForeignKey('recharge_records.id'), nullable=True) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + transferred_at = db.Column(db.DateTime, nullable=True) + + def to_dict(self): + """转换为字典""" + return { + 'id': self.id, + 'reward_amount': float(self.reward_amount), + 'status': self.status, + 'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S') if self.created_at else None, + } diff --git a/backend/models/model.py b/backend/models/model.py new file mode 100644 index 0000000..15805d2 --- /dev/null +++ b/backend/models/model.py @@ -0,0 +1,64 @@ +from models import db +from datetime import datetime + +class Model(db.Model): + __tablename__ = 'models' + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(200), nullable=False, unique=True, index=True) + provider = db.Column(db.String(100), nullable=False, index=True) # Anthropic, OpenAI等 + description = db.Column(db.Text, nullable=True) + input_price = db.Column(db.Numeric(10, 6), nullable=False) # 输入价格 per 1M tokens + output_price = db.Column(db.Numeric(10, 6), nullable=False) # 输出价格 per 1M tokens + billing_type = db.Column(db.String(50), default='pay_as_you_go') # pay_as_you_go, per_request + endpoint_type = db.Column(db.String(50), default='anthropic') # anthropic, openai + tags = db.Column(db.Text, nullable=True) # JSON字符串,存储标签数组 + available_groups = db.Column(db.Text, nullable=True) # JSON字符串,存储可用分组 + multiplier = db.Column(db.Numeric(5, 2), default=1.00) # 倍率 + is_active = db.Column(db.Boolean, default=True) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + def to_dict(self, show_recharge_price=False, show_multiplier=False): + """转换为字典""" + import json + + tags = [] + if self.tags: + try: + tags = json.loads(self.tags) + except: + tags = [] + + available_groups = [] + if self.available_groups: + try: + available_groups = json.loads(self.available_groups) + except: + available_groups = [] + + result = { + 'id': self.id, + 'name': self.name, + 'provider': self.provider, + 'description': self.description, + 'input_price': float(self.input_price), + 'output_price': float(self.output_price), + 'billing_type': self.billing_type, + 'billing_type_display': '按量计费' if self.billing_type == 'pay_as_you_go' else '按次计费', + 'endpoint_type': self.endpoint_type, + 'tags': tags, + 'available_groups': available_groups, + 'is_active': self.is_active, + 'created_at': self.created_at.isoformat() if self.created_at else None, + } + + if show_recharge_price: + # 显示充值价格(假设充值比例0.65) + result['recharge_input_price'] = float(self.input_price) * 0.65 + result['recharge_output_price'] = float(self.output_price) * 0.65 + + if show_multiplier: + result['multiplier'] = float(self.multiplier) + + return result diff --git a/backend/models/recharge_record.py b/backend/models/recharge_record.py new file mode 100644 index 0000000..914052f --- /dev/null +++ b/backend/models/recharge_record.py @@ -0,0 +1,28 @@ +from models import db +from datetime import datetime + +class RechargeRecord(db.Model): + __tablename__ = 'recharge_records' + + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) + amount = db.Column(db.Numeric(10, 2), nullable=False) # 美元金额 + actual_amount = db.Column(db.Numeric(10, 2), nullable=False) # 实际支付金额(人民币) + payment_method = db.Column(db.String(50), nullable=False) # alipay, wechat等 + payment_status = db.Column(db.String(20), default='pending') # pending, completed, failed + exchange_code = db.Column(db.String(50), nullable=True) # 兑换码 + transaction_id = db.Column(db.String(100), nullable=True) # 第三方交易ID + created_at = db.Column(db.DateTime, default=datetime.utcnow) + completed_at = db.Column(db.DateTime, nullable=True) + + def to_dict(self): + """转换为字典""" + return { + 'id': self.id, + 'amount': float(self.amount), + 'actual_amount': float(self.actual_amount), + 'payment_method': self.payment_method, + 'payment_status': self.payment_status, + 'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S') if self.created_at else None, + 'completed_at': self.completed_at.strftime('%Y-%m-%d %H:%M:%S') if self.completed_at else None, + } diff --git a/backend/models/token.py b/backend/models/token.py new file mode 100644 index 0000000..53fd009 --- /dev/null +++ b/backend/models/token.py @@ -0,0 +1,66 @@ +from models import db +from datetime import datetime +import secrets +import string + +class Token(db.Model): + __tablename__ = 'tokens' + + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) + name = db.Column(db.String(100), nullable=False) + key = db.Column(db.String(255), unique=True, nullable=False, index=True) + status = db.Column(db.String(20), default='enabled') # enabled, disabled + remaining_quota = db.Column(db.Numeric(15, 2), nullable=True) # None表示无限额度 + total_quota = db.Column(db.Numeric(15, 2), nullable=True) + group = db.Column(db.String(100), default='Claude Code 官方编程模型') + available_models = db.Column(db.Text, nullable=True) # JSON字符串或None表示无限制 + ip_restriction = db.Column(db.String(500), nullable=True) # IP白名单,逗号分隔 + expires_at = db.Column(db.DateTime, nullable=True) # None表示永不过期 + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + # 关系 + usage_logs = db.relationship('UsageLog', backref='token', lazy=True) + + @staticmethod + def generate_key(): + """生成API密钥""" + return 'sk-' + ''.join(secrets.choice(string.ascii_letters + string.digits) for _ in range(40)) + + def to_dict(self, show_key=False): + """转换为字典""" + return { + 'id': self.id, + 'user_id': self.user_id, + 'name': self.name, + 'key': self.key if show_key else self.masked_key, + 'status': self.status, + 'remaining_quota': float(self.remaining_quota) if self.remaining_quota else None, + 'total_quota': float(self.total_quota) if self.total_quota else None, + 'quota_display': self.quota_display, + 'group': self.group, + 'available_models': self.available_models, + 'ip_restriction': self.ip_restriction, + 'expires_at': self.expires_at.isoformat() if self.expires_at else None, + 'expires_display': '永不过期' if not self.expires_at else self.expires_at.strftime('%Y-%m-%d %H:%M:%S'), + 'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S') if self.created_at else None, + } + + @property + def masked_key(self): + """掩码显示的密钥""" + if len(self.key) > 10: + return self.key[:5] + '*' * 10 + self.key[-5:] + return '*' * len(self.key) + + @property + def quota_display(self): + """额度显示文本""" + if self.remaining_quota is None and self.total_quota is None: + return '无限额度' + if self.remaining_quota is None: + return f'无限额度 / {float(self.total_quota):.2f}' + if self.total_quota is None: + return f'{float(self.remaining_quota):.2f} / 无限额度' + return f'{float(self.remaining_quota):.2f} / {float(self.total_quota):.2f}' diff --git a/backend/models/usage_log.py b/backend/models/usage_log.py new file mode 100644 index 0000000..83fc7f6 --- /dev/null +++ b/backend/models/usage_log.py @@ -0,0 +1,49 @@ +from models import db +from datetime import datetime + +class UsageLog(db.Model): + __tablename__ = 'usage_logs' + + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) + token_id = db.Column(db.Integer, db.ForeignKey('tokens.id'), nullable=False) + token_name = db.Column(db.String(100), nullable=True) + group = db.Column(db.String(100), nullable=True) + log_type = db.Column(db.String(50), nullable=False) # chat, image, task等 + model = db.Column(db.String(100), nullable=False) + duration = db.Column(db.Float, nullable=True) # 用时(秒) + first_token_time = db.Column(db.Float, nullable=True) # 首字时间(秒) + input_tokens = db.Column(db.Integer, default=0) + output_tokens = db.Column(db.Integer, default=0) + cost = db.Column(db.Numeric(10, 4), default=0.0000) + ip_address = db.Column(db.String(50), nullable=True) + details = db.Column(db.Text, nullable=True) # JSON字符串,存储详细信息 + created_at = db.Column(db.DateTime, default=datetime.utcnow, index=True) + + def to_dict(self): + """转换为字典""" + return { + 'id': self.id, + 'token_name': self.token_name, + 'group': self.group, + 'log_type': self.log_type, + 'model': self.model, + 'duration': self.duration, + 'first_token_time': self.first_token_time, + 'time_display': self.time_display, + 'input_tokens': self.input_tokens, + 'output_tokens': self.output_tokens, + 'cost': float(self.cost), + 'ip_address': self.ip_address, + 'details': self.details, + 'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S') if self.created_at else None, + } + + @property + def time_display(self): + """时间显示文本""" + if self.duration is not None: + return f'{self.duration:.2f}s' + if self.first_token_time is not None: + return f'{self.first_token_time:.2f}s' + return '-' diff --git a/backend/models/user.py b/backend/models/user.py new file mode 100644 index 0000000..5de40c8 --- /dev/null +++ b/backend/models/user.py @@ -0,0 +1,46 @@ +from models import db +from datetime import datetime +import bcrypt + +class User(db.Model): + __tablename__ = 'users' + + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(80), unique=True, nullable=False, index=True) + email = db.Column(db.String(120), unique=True, nullable=True) + password_hash = db.Column(db.String(255), nullable=False) + balance = db.Column(db.Numeric(10, 2), default=0.00) + total_consumption = db.Column(db.Numeric(10, 2), default=0.00) + request_count = db.Column(db.Integer, default=0) + user_group = db.Column(db.String(50), default='default') + invite_code = db.Column(db.String(20), unique=True, nullable=True) + invited_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + # 关系 + tokens = db.relationship('Token', backref='user', lazy=True, cascade='all, delete-orphan') + usage_logs = db.relationship('UsageLog', backref='user', lazy=True) + recharge_records = db.relationship('RechargeRecord', backref='user', lazy=True) + + def set_password(self, password): + """设置密码""" + self.password_hash = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8') + + def check_password(self, password): + """验证密码""" + return bcrypt.checkpw(password.encode('utf-8'), self.password_hash.encode('utf-8')) + + def to_dict(self): + """转换为字典""" + return { + 'id': self.id, + 'username': self.username, + 'email': self.email, + 'balance': float(self.balance), + 'total_consumption': float(self.total_consumption), + 'request_count': self.request_count, + 'user_group': self.user_group, + 'invite_code': self.invite_code, + 'created_at': self.created_at.isoformat() if self.created_at else None, + } diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..6dbf6ff --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,10 @@ +Flask==3.0.0 +Flask-SQLAlchemy==3.1.1 +Flask-CORS==4.0.0 +Flask-JWT-Extended==4.6.0 +Flask-Migrate==4.0.5 +Werkzeug==3.0.1 +python-dotenv==1.0.0 +bcrypt==4.1.2 +PyMySQL==1.1.0 +cryptography==41.0.7 diff --git a/backend/routes/__init__.py b/backend/routes/__init__.py new file mode 100644 index 0000000..d212dab --- /dev/null +++ b/backend/routes/__init__.py @@ -0,0 +1 @@ +# Routes package diff --git a/backend/routes/auth.py b/backend/routes/auth.py new file mode 100644 index 0000000..273b586 --- /dev/null +++ b/backend/routes/auth.py @@ -0,0 +1,109 @@ +from flask import Blueprint, request, jsonify +from flask_jwt_extended import create_access_token, create_refresh_token, jwt_required, get_jwt_identity +from models import db +from models.user import User +import secrets +import string + +auth_bp = Blueprint('auth', __name__) + +@auth_bp.route('/register', methods=['POST']) +def register(): + """用户注册""" + data = request.get_json() + username = data.get('username') + password = data.get('password') + confirm_password = data.get('confirm_password') + invite_code = data.get('invite_code') + + # 验证输入 + if not username or not password: + return jsonify({'error': '用户名和密码不能为空'}), 400 + + if len(password) < 8 or len(password) > 20: + return jsonify({'error': '密码长度必须在8-20位之间'}), 400 + + if password != confirm_password: + return jsonify({'error': '两次输入的密码不一致'}), 400 + + # 检查用户名是否已存在 + if User.query.filter_by(username=username).first(): + return jsonify({'error': '用户名已存在'}), 400 + + # 处理邀请码 + inviter = None + if invite_code: + inviter = User.query.filter_by(invite_code=invite_code).first() + + # 创建用户 + user = User(username=username) + user.set_password(password) + + # 生成邀请码 + user.invite_code = ''.join(secrets.choice(string.ascii_letters + string.digits) for _ in range(6)) + if inviter: + user.invited_by = inviter.id + + try: + db.session.add(user) + db.session.commit() + + # 生成token + access_token = create_access_token(identity=str(user.id)) + refresh_token = create_refresh_token(identity=str(user.id)) + + return jsonify({ + 'message': '注册成功', + 'access_token': access_token, + 'refresh_token': refresh_token, + 'user': user.to_dict() + }), 201 + except Exception as e: + db.session.rollback() + return jsonify({'error': '注册失败: ' + str(e)}), 500 + +@auth_bp.route('/login', methods=['POST']) +def login(): + """用户登录""" + data = request.get_json() + username = data.get('username') + password = data.get('password') + + if not username or not password: + return jsonify({'error': '用户名和密码不能为空'}), 400 + + user = User.query.filter_by(username=username).first() + + if not user or not user.check_password(password): + return jsonify({'error': '用户名或密码错误'}), 401 + + # 生成token + access_token = create_access_token(identity=str(user.id)) + refresh_token = create_refresh_token(identity=str(user.id)) + + return jsonify({ + 'message': '登录成功', + 'access_token': access_token, + 'refresh_token': refresh_token, + 'user': user.to_dict() + }), 200 + +@auth_bp.route('/refresh', methods=['POST']) +@jwt_required(refresh=True) +def refresh(): + """刷新token""" + current_user_id = get_jwt_identity() + new_token = create_access_token(identity=str(current_user_id)) + return jsonify({'access_token': new_token}), 200 + +@auth_bp.route('/me', methods=['GET']) +@jwt_required() +def get_current_user(): + """获取当前用户信息""" + current_user_id = int(get_jwt_identity()) + user = User.query.get(current_user_id) + + if not user: + return jsonify({'error': '用户不存在'}), 404 + + return jsonify({'user': user.to_dict()}), 200 diff --git a/backend/routes/dashboard.py b/backend/routes/dashboard.py new file mode 100644 index 0000000..eee5f5f --- /dev/null +++ b/backend/routes/dashboard.py @@ -0,0 +1,128 @@ +from flask import Blueprint, jsonify +from flask_jwt_extended import jwt_required, get_jwt_identity +from models import db +from models.user import User +from models.token import Token +from models.usage_log import UsageLog +from datetime import datetime, timedelta +import logging +from sqlalchemy import func, and_ + +dashboard_bp = Blueprint('dashboard', __name__) +logger = logging.getLogger(__name__) + +@dashboard_bp.route('/stats', methods=['GET']) +@jwt_required() +def get_dashboard_stats(): + """获取数据看板统计信息""" + current_user_id = int(get_jwt_identity()) + user = User.query.get(current_user_id) + + if not user: + return jsonify({'error': '用户不存在'}), 404 + + # 获取今日和7天的日期范围 + today_start = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0) + seven_days_ago = today_start - timedelta(days=7) + + # Token统计(今日) + today_logs = UsageLog.query.filter( + and_( + UsageLog.user_id == current_user_id, + UsageLog.created_at >= today_start + ) + ).all() + + today_input = sum(log.input_tokens for log in today_logs) + today_output = sum(log.output_tokens for log in today_logs) + today_cache_create = 0 # 需要根据实际业务逻辑计算 + today_cache_read = 0 # 需要根据实际业务逻辑计算 + today_total = today_input + today_output + + # Token统计(7天) + week_logs = UsageLog.query.filter( + and_( + UsageLog.user_id == current_user_id, + UsageLog.created_at >= seven_days_ago + ) + ).all() + + week_input = sum(log.input_tokens for log in week_logs) + week_output = sum(log.output_tokens for log in week_logs) + week_total = week_input + week_output + + # 性能指标 + if today_logs: + total_duration = sum(log.duration or 0 for log in today_logs) + total_requests = len(today_logs) + avg_rpm = total_requests / (total_duration / 60) if total_duration > 0 else 0 + avg_tpm = today_total / (total_duration / 60) if total_duration > 0 else 0 + else: + avg_rpm = 0 + avg_tpm = 0 + + # 模型消耗分布(示例数据,实际需要从数据库查询) + model_distribution = [] + model_stats = db.session.query( + UsageLog.model, + func.sum(UsageLog.cost).label('total_cost') + ).filter( + and_( + UsageLog.user_id == current_user_id, + UsageLog.created_at >= seven_days_ago + ) + ).group_by(UsageLog.model).all() + + for model, cost in model_stats: + model_distribution.append({ + 'model': model, + 'cost': float(cost) if cost else 0 + }) + + return jsonify({ + 'account': { + 'balance': float(user.balance), + 'total_consumption': float(user.total_consumption), + 'request_count': user.request_count + }, + 'usage': { + 'request_count': user.request_count, + 'stat_count': len(today_logs) # 统计次数 + }, + 'token_stats': { + 'today': { + 'input': today_input, + 'output': today_output, + 'cache_create': today_cache_create, + 'cache_read': today_cache_read, + 'total': today_total + }, + 'week': { + 'input': week_input, + 'output': week_output, + 'total': week_total + } + }, + 'performance': { + 'avg_rpm': round(avg_rpm, 3), + 'avg_tpm': round(avg_tpm, 3) + }, + 'model_distribution': model_distribution + }), 200 + +@dashboard_bp.route('/server-info', methods=['GET']) +@jwt_required() +def get_server_info(): + """获取服务器信息""" + # 验证token(用于调试) + try: + current_user_id = int(get_jwt_identity()) + logger.info(f"Server info requested by user: {current_user_id}") + except Exception as e: + logger.error(f"JWT verification failed: {str(e)}") + + return jsonify({ + 'name': '蓝星中国服务器', + 'url': 'https://cc.honoursoft.cn', + 'description': 'cn2网络回国优化,支持海外,国内双向访问。' + }), 200 diff --git a/backend/routes/logs.py b/backend/routes/logs.py new file mode 100644 index 0000000..fc2c338 --- /dev/null +++ b/backend/routes/logs.py @@ -0,0 +1,90 @@ +from flask import Blueprint, request, jsonify +from flask_jwt_extended import jwt_required, get_jwt_identity +from models import db +from models.usage_log import UsageLog +from datetime import datetime, timedelta +from sqlalchemy import and_, or_, func + +logs_bp = Blueprint('logs', __name__) + +@logs_bp.route('/usage', methods=['GET']) +@jwt_required() +def get_usage_logs(): + """获取使用日志""" + current_user_id = int(get_jwt_identity()) + + # 获取查询参数 + page = request.args.get('page', 1, type=int) + per_page = request.args.get('per_page', 20, type=int) + start_date = request.args.get('start_date') + end_date = request.args.get('end_date') + token_name = request.args.get('token_name', '') + model_name = request.args.get('model_name', '') + group = request.args.get('group', '') + + # 构建查询 + query = UsageLog.query.filter_by(user_id=current_user_id) + + # 日期范围过滤 + if start_date: + try: + start_dt = datetime.fromisoformat(start_date.replace('Z', '+00:00')) + query = query.filter(UsageLog.created_at >= start_dt) + except: + pass + + if end_date: + try: + end_dt = datetime.fromisoformat(end_date.replace('Z', '+00:00')) + query = query.filter(UsageLog.created_at <= end_dt) + except: + pass + + # 其他过滤条件 + if token_name: + query = query.filter(UsageLog.token_name.like(f'%{token_name}%')) + + if model_name: + query = query.filter(UsageLog.model.like(f'%{model_name}%')) + + if group: + query = query.filter(UsageLog.group == group) + + # 统计信息 + stats_query = query + total_cost = db.session.query(func.sum(UsageLog.cost)).filter( + stats_query.whereclause + ).scalar() or 0 + + total_requests = stats_query.count() + + # 计算RPM和TPM(基于查询结果) + logs_for_stats = stats_query.all() + if logs_for_stats: + total_duration = sum(log.duration or 0 for log in logs_for_stats) + total_tokens = sum(log.input_tokens + log.output_tokens for log in logs_for_stats) + rpm = total_requests / (total_duration / 60) if total_duration > 0 else 0 + tpm = total_tokens / (total_duration / 60) if total_duration > 0 else 0 + else: + rpm = 0 + tpm = 0 + + # 分页 + pagination = query.order_by(UsageLog.created_at.desc()).paginate( + page=page, per_page=per_page, error_out=False + ) + + return jsonify({ + 'logs': [log.to_dict() for log in pagination.items], + 'pagination': { + 'page': page, + 'per_page': per_page, + 'total': pagination.total, + 'pages': pagination.pages + }, + 'stats': { + 'total_cost': float(total_cost), + 'rpm': round(rpm, 2), + 'tpm': round(tpm, 2) + } + }), 200 diff --git a/backend/routes/models.py b/backend/routes/models.py new file mode 100644 index 0000000..f8474c3 --- /dev/null +++ b/backend/routes/models.py @@ -0,0 +1,147 @@ +from flask import Blueprint, request, jsonify +from models import db +from models.model import Model +from sqlalchemy import or_, and_ + +models_bp = Blueprint('models', __name__) + +@models_bp.route('', methods=['GET']) +def get_models(): + """获取模型列表(支持筛选、搜索、分页)""" + # 获取查询参数 + page = request.args.get('page', 1, type=int) + per_page = request.args.get('per_page', 20, type=int) + provider = request.args.get('provider', '') + tags = request.args.getlist('tags') # 支持多个标签 + token_group = request.args.get('token_group', '') + billing_type = request.args.get('billing_type', '') + endpoint_type = request.args.get('endpoint_type', '') + search_query = request.args.get('search', '') + show_recharge_price = request.args.get('show_recharge_price', 'false').lower() == 'true' + show_multiplier = request.args.get('show_multiplier', 'false').lower() == 'true' + + # 构建查询 + query = Model.query.filter_by(is_active=True) + + # 供应商筛选 + if provider and provider != 'all': + query = query.filter(Model.provider == provider) + + # 标签筛选 + if tags and 'all' not in tags: + import json + for tag in tags: + query = query.filter( + or_( + Model.tags.like(f'%"{tag}"%'), + Model.tags.like(f'%{tag}%') + ) + ) + + # 分组筛选 + if token_group and token_group != 'all': + import json + query = query.filter( + or_( + Model.available_groups.like(f'%"{token_group}"%'), + Model.available_groups.like(f'%{token_group}%') + ) + ) + + # 计费类型筛选 + if billing_type and billing_type != 'all': + query = query.filter(Model.billing_type == billing_type) + + # 端点类型筛选 + if endpoint_type and endpoint_type != 'all': + query = query.filter(Model.endpoint_type == endpoint_type) + + # 搜索 + if search_query: + query = query.filter( + or_( + Model.name.like(f'%{search_query}%'), + Model.description.like(f'%{search_query}%') + ) + ) + + # 分页 + pagination = query.order_by(Model.created_at.desc()).paginate( + page=page, per_page=per_page, error_out=False + ) + + # 获取筛选选项(用于前端显示) + all_providers = db.session.query(Model.provider).filter_by(is_active=True).distinct().all() + providers = [p[0] for p in all_providers] + + # 获取所有标签 + all_tags = set() + all_models = Model.query.filter_by(is_active=True).all() + for model in all_models: + if model.tags: + import json + try: + tags_list = json.loads(model.tags) + all_tags.update(tags_list) + except: + pass + + return jsonify({ + 'models': [model.to_dict(show_recharge_price, show_multiplier) for model in pagination.items], + 'pagination': { + 'page': page, + 'per_page': per_page, + 'total': pagination.total, + 'pages': pagination.pages + }, + 'filters': { + 'providers': providers, + 'tags': sorted(list(all_tags)), + 'billing_types': ['pay_as_you_go', 'per_request'], + 'endpoint_types': ['anthropic', 'openai'] + } + }), 200 + +@models_bp.route('/', methods=['GET']) +def get_model(model_id): + """获取单个模型详情""" + model = Model.query.get_or_404(model_id) + return jsonify({'model': model.to_dict(show_recharge_price=True, show_multiplier=True)}), 200 + +@models_bp.route('/filters', methods=['GET']) +def get_filters(): + """获取筛选选项""" + # 获取所有分组(从Token表) + from models.token import Token + all_groups = db.session.query(Token.group).distinct().all() + groups = [g[0] for g in all_groups if g[0]] + + # 获取所有供应商 + all_providers = db.session.query(Model.provider).filter_by(is_active=True).distinct().all() + providers = [p[0] for p in all_providers] + + # 获取所有标签 + all_tags = set() + all_models = Model.query.filter_by(is_active=True).all() + for model in all_models: + if model.tags: + import json + try: + tags_list = json.loads(model.tags) + all_tags.update(tags_list) + except: + pass + + return jsonify({ + 'providers': providers, + 'tags': sorted(list(all_tags)), + 'groups': groups, + 'billing_types': [ + {'value': 'pay_as_you_go', 'label': '按量计费'}, + {'value': 'per_request', 'label': '按次计费'} + ], + 'endpoint_types': [ + {'value': 'anthropic', 'label': 'anthropic'}, + {'value': 'openai', 'label': 'openai'} + ] + }), 200 diff --git a/backend/routes/recharge.py b/backend/routes/recharge.py new file mode 100644 index 0000000..c1f1d03 --- /dev/null +++ b/backend/routes/recharge.py @@ -0,0 +1,178 @@ +from flask import Blueprint, request, jsonify +from flask_jwt_extended import jwt_required, get_jwt_identity +from models import db +from models.user import User +from models.recharge_record import RechargeRecord +from models.invite_reward import InviteReward +from decimal import Decimal +from datetime import datetime +from sqlalchemy import func + +recharge_bp = Blueprint('recharge', __name__) + +# 充值比例 +RECHARGE_RATE = 0.65 # 0.65元 = 1美元 + +@recharge_bp.route('/info', methods=['GET']) +@jwt_required() +def get_recharge_info(): + """获取充值信息""" + current_user_id = int(get_jwt_identity()) + user = User.query.get(current_user_id) + + if not user: + return jsonify({'error': '用户不存在'}), 404 + + # 计算邀请奖励 + pending_rewards = InviteReward.query.filter_by( + inviter_id=current_user_id, + status='pending' + ).all() + + total_pending_reward = sum(float(reward.reward_amount) for reward in pending_rewards) + total_reward = db.session.query(func.sum(InviteReward.reward_amount)).filter_by( + inviter_id=current_user_id + ).scalar() or 0 + + invite_count = User.query.filter_by(invited_by=current_user_id).count() + + return jsonify({ + 'balance': float(user.balance), + 'total_consumption': float(user.total_consumption), + 'request_count': user.request_count, + 'recharge_rate': RECHARGE_RATE, + 'invite_reward': { + 'pending': float(total_pending_reward), + 'total': float(total_reward), + 'invite_count': invite_count, + 'invite_code': user.invite_code, + 'invite_url': f'https://cc.honoursoft.cn/register?aff={user.invite_code}' + } + }), 200 + +@recharge_bp.route('/create', methods=['POST']) +@jwt_required() +def create_recharge(): + """创建充值订单""" + current_user_id = int(get_jwt_identity()) + data = request.get_json() + + amount = Decimal(str(data.get('amount', 0))) # 美元 + payment_method = data.get('payment_method', 'alipay') + + if amount <= 0: + return jsonify({'error': '充值金额必须大于0'}), 400 + + # 计算实际支付金额(人民币) + actual_amount = amount * Decimal(str(RECHARGE_RATE)) + + # 创建充值记录 + recharge = RechargeRecord( + user_id=current_user_id, + amount=amount, + actual_amount=actual_amount, + payment_method=payment_method, + payment_status='pending' + ) + + try: + db.session.add(recharge) + db.session.commit() + + # 这里应该调用第三方支付接口,生成支付链接 + # 示例:返回订单信息 + return jsonify({ + 'message': '订单创建成功', + 'order_id': recharge.id, + 'amount': float(amount), + 'actual_amount': float(actual_amount), + 'payment_method': payment_method, + 'payment_url': f'/payment/{recharge.id}' # 实际应该返回真实的支付链接 + }), 201 + except Exception as e: + db.session.rollback() + return jsonify({'error': '创建订单失败: ' + str(e)}), 500 + +@recharge_bp.route('/exchange', methods=['POST']) +@jwt_required() +def exchange_code(): + """兑换码充值""" + current_user_id = int(get_jwt_identity()) + data = request.get_json() + code = data.get('code', '').strip() + + if not code: + return jsonify({'error': '兑换码不能为空'}), 400 + + # 这里应该实现兑换码验证逻辑 + # 示例:假设兑换码格式为 EXCHANGE-{amount} + # 实际应该从数据库查询有效的兑换码 + + # 示例实现 + if code.startswith('EXCHANGE-'): + try: + amount = Decimal(code.replace('EXCHANGE-', '')) + user = User.query.get(current_user_id) + user.balance += amount + + # 创建充值记录 + recharge = RechargeRecord( + user_id=current_user_id, + amount=amount, + actual_amount=Decimal('0'), + payment_method='exchange', + payment_status='completed', + exchange_code=code + ) + + db.session.add(recharge) + db.session.commit() + + return jsonify({ + 'message': '兑换成功', + 'amount': float(amount) + }), 200 + except: + return jsonify({'error': '无效的兑换码'}), 400 + else: + return jsonify({'error': '无效的兑换码'}), 400 + +@recharge_bp.route('/transfer-reward', methods=['POST']) +@jwt_required() +def transfer_reward(): + """将邀请奖励转入余额""" + current_user_id = int(get_jwt_identity()) + user = User.query.get(current_user_id) + + if not user: + return jsonify({'error': '用户不存在'}), 404 + + # 获取待使用的奖励 + pending_rewards = InviteReward.query.filter_by( + inviter_id=current_user_id, + status='pending' + ).all() + + if not pending_rewards: + return jsonify({'error': '没有可转移的奖励'}), 400 + + total_reward = sum(float(reward.reward_amount) for reward in pending_rewards) + + try: + # 更新用户余额 + user.balance += Decimal(str(total_reward)) + + # 更新奖励状态 + for reward in pending_rewards: + reward.status = 'transferred' + reward.transferred_at = datetime.utcnow() + + db.session.commit() + + return jsonify({ + 'message': '转移成功', + 'amount': total_reward + }), 200 + except Exception as e: + db.session.rollback() + return jsonify({'error': '转移失败: ' + str(e)}), 500 diff --git a/backend/routes/tokens.py b/backend/routes/tokens.py new file mode 100644 index 0000000..c24256f --- /dev/null +++ b/backend/routes/tokens.py @@ -0,0 +1,160 @@ +from flask import Blueprint, request, jsonify +from flask_jwt_extended import jwt_required, get_jwt_identity +from models import db +from models.token import Token +from models.user import User + +tokens_bp = Blueprint('tokens', __name__) + +@tokens_bp.route('', methods=['GET']) +@jwt_required() +def get_tokens(): + """获取用户的令牌列表""" + current_user_id = int(get_jwt_identity()) + search_keyword = request.args.get('keyword', '') + search_key = request.args.get('key', '') + + query = Token.query.filter_by(user_id=current_user_id) + + if search_keyword: + query = query.filter(Token.name.like(f'%{search_keyword}%')) + + if search_key: + query = query.filter(Token.key.like(f'%{search_key}%')) + + tokens = query.order_by(Token.created_at.desc()).all() + + return jsonify({ + 'tokens': [token.to_dict() for token in tokens] + }), 200 + +@tokens_bp.route('', methods=['POST']) +@jwt_required() +def create_token(): + """创建新令牌""" + current_user_id = int(get_jwt_identity()) + data = request.get_json() + + name = data.get('name', '新令牌') + group = data.get('group', 'Claude Code 官方编程模型') + remaining_quota = data.get('remaining_quota') + total_quota = data.get('total_quota') + available_models = data.get('available_models') + ip_restriction = data.get('ip_restriction') + expires_at = data.get('expires_at') + + token = Token( + user_id=current_user_id, + name=name, + key=Token.generate_key(), + group=group, + remaining_quota=remaining_quota, + total_quota=total_quota, + available_models=available_models, + ip_restriction=ip_restriction, + expires_at=expires_at + ) + + try: + db.session.add(token) + db.session.commit() + return jsonify({ + 'message': '令牌创建成功', + 'token': token.to_dict(show_key=True) + }), 201 + except Exception as e: + db.session.rollback() + return jsonify({'error': '创建失败: ' + str(e)}), 500 + +@tokens_bp.route('/', methods=['GET']) +@jwt_required() +def get_token(token_id): + """获取单个令牌详情""" + current_user_id = int(get_jwt_identity()) + token = Token.query.filter_by(id=token_id, user_id=current_user_id).first() + + if not token: + return jsonify({'error': '令牌不存在'}), 404 + + return jsonify({'token': token.to_dict(show_key=True)}), 200 + +@tokens_bp.route('/', methods=['PUT']) +@jwt_required() +def update_token(token_id): + """更新令牌""" + current_user_id = int(get_jwt_identity()) + token = Token.query.filter_by(id=token_id, user_id=current_user_id).first() + + if not token: + return jsonify({'error': '令牌不存在'}), 404 + + data = request.get_json() + token.name = data.get('name', token.name) + token.group = data.get('group', token.group) + token.remaining_quota = data.get('remaining_quota', token.remaining_quota) + token.total_quota = data.get('total_quota', token.total_quota) + token.available_models = data.get('available_models', token.available_models) + token.ip_restriction = data.get('ip_restriction', token.ip_restriction) + token.expires_at = data.get('expires_at', token.expires_at) + + try: + db.session.commit() + return jsonify({ + 'message': '更新成功', + 'token': token.to_dict(show_key=True) + }), 200 + except Exception as e: + db.session.rollback() + return jsonify({'error': '更新失败: ' + str(e)}), 500 + +@tokens_bp.route('/', methods=['DELETE']) +@jwt_required() +def delete_token(token_id): + """删除令牌""" + current_user_id = int(get_jwt_identity()) + token = Token.query.filter_by(id=token_id, user_id=current_user_id).first() + + if not token: + return jsonify({'error': '令牌不存在'}), 404 + + try: + db.session.delete(token) + db.session.commit() + return jsonify({'message': '删除成功'}), 200 + except Exception as e: + db.session.rollback() + return jsonify({'error': '删除失败: ' + str(e)}), 500 + +@tokens_bp.route('/batch', methods=['POST']) +@jwt_required() +def batch_operation(): + """批量操作令牌""" + current_user_id = int(get_jwt_identity()) + data = request.get_json() + token_ids = data.get('token_ids', []) + operation = data.get('operation') # copy, delete, enable, disable + + tokens = Token.query.filter( + Token.id.in_(token_ids), + Token.user_id == current_user_id + ).all() + + if not tokens: + return jsonify({'error': '未找到有效的令牌'}), 404 + + try: + if operation == 'delete': + for token in tokens: + db.session.delete(token) + elif operation == 'enable': + for token in tokens: + token.status = 'enabled' + elif operation == 'disable': + for token in tokens: + token.status = 'disabled' + + db.session.commit() + return jsonify({'message': '操作成功'}), 200 + except Exception as e: + db.session.rollback() + return jsonify({'error': '操作失败: ' + str(e)}), 500 diff --git a/backend/routes/user.py b/backend/routes/user.py new file mode 100644 index 0000000..0318172 --- /dev/null +++ b/backend/routes/user.py @@ -0,0 +1,79 @@ +from flask import Blueprint, request, jsonify +from flask_jwt_extended import jwt_required, get_jwt_identity +from models import db +from models.user import User +from datetime import datetime + +user_bp = Blueprint('user', __name__) + +@user_bp.route('/profile', methods=['GET']) +@jwt_required() +def get_profile(): + """获取用户资料""" + current_user_id = int(get_jwt_identity()) + user = User.query.get(current_user_id) + + if not user: + return jsonify({'error': '用户不存在'}), 404 + + return jsonify({'user': user.to_dict()}), 200 + +@user_bp.route('/profile', methods=['PUT']) +@jwt_required() +def update_profile(): + """更新用户资料""" + current_user_id = int(get_jwt_identity()) + user = User.query.get(current_user_id) + + if not user: + return jsonify({'error': '用户不存在'}), 404 + + data = request.get_json() + + # 更新邮箱 + if 'email' in data: + email = data['email'] + if email and User.query.filter_by(email=email).filter(User.id != current_user_id).first(): + return jsonify({'error': '邮箱已被使用'}), 400 + user.email = email + + # 更新用户组 + if 'user_group' in data: + user.user_group = data['user_group'] + + try: + db.session.commit() + return jsonify({ + 'message': '更新成功', + 'user': user.to_dict() + }), 200 + except Exception as e: + db.session.rollback() + return jsonify({'error': '更新失败: ' + str(e)}), 500 + +@user_bp.route('/settings', methods=['GET']) +@jwt_required() +def get_settings(): + """获取用户设置""" + current_user_id = int(get_jwt_identity()) + # 这里可以从数据库读取用户设置,示例返回默认设置 + return jsonify({ + 'notification': { + 'method': 'email', # email, webhook, bark, gotify + 'threshold': 500000, + 'email': '' + }, + 'price': {}, + 'privacy': {}, + 'sidebar': {} + }), 200 + +@user_bp.route('/settings', methods=['PUT']) +@jwt_required() +def update_settings(): + """更新用户设置""" + current_user_id = int(get_jwt_identity()) + data = request.get_json() + + # 这里应该保存到数据库,示例只返回成功 + return jsonify({'message': '设置保存成功'}), 200 diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..4bda4cc --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Claude Code 蓝星 + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..280654a --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,1828 @@ +{ + "name": "claude-code-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "claude-code-frontend", + "version": "1.0.0", + "dependencies": { + "@element-plus/icons-vue": "^2.3.1", + "axios": "^1.6.2", + "dayjs": "^1.11.10", + "echarts": "^5.4.3", + "element-plus": "^2.4.4", + "pinia": "^2.1.7", + "vue": "^3.3.4", + "vue-echarts": "^6.6.1", + "vue-router": "^4.2.5" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^4.5.0", + "vite": "^5.0.8" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", + "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@element-plus/icons-vue": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz", + "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==", + "license": "MIT", + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@popperjs/core": { + "name": "@sxzz/popperjs-es", + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz", + "integrity": "sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.56.0.tgz", + "integrity": "sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.56.0.tgz", + "integrity": "sha512-lfbVUbelYqXlYiU/HApNMJzT1E87UPGvzveGg2h0ktUNlOCxKlWuJ9jtfvs1sKHdwU4fzY7Pl8sAl49/XaEk6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.56.0.tgz", + "integrity": "sha512-EgxD1ocWfhoD6xSOeEEwyE7tDvwTgZc8Bss7wCWe+uc7wO8G34HHCUH+Q6cHqJubxIAnQzAsyUsClt0yFLu06w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.56.0.tgz", + "integrity": "sha512-1vXe1vcMOssb/hOF8iv52A7feWW2xnu+c8BV4t1F//m9QVLTfNVpEdja5ia762j/UEJe2Z1jAmEqZAK42tVW3g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.56.0.tgz", + "integrity": "sha512-bof7fbIlvqsyv/DtaXSck4VYQ9lPtoWNFCB/JY4snlFuJREXfZnm+Ej6yaCHfQvofJDXLDMTVxWscVSuQvVWUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.56.0.tgz", + "integrity": "sha512-KNa6lYHloW+7lTEkYGa37fpvPq+NKG/EHKM8+G/g9WDU7ls4sMqbVRV78J6LdNuVaeeK5WB9/9VAFbKxcbXKYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.56.0.tgz", + "integrity": "sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.56.0.tgz", + "integrity": "sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.56.0.tgz", + "integrity": "sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.56.0.tgz", + "integrity": "sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.56.0.tgz", + "integrity": "sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.56.0.tgz", + "integrity": "sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.56.0.tgz", + "integrity": "sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.56.0.tgz", + "integrity": "sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.56.0.tgz", + "integrity": "sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.56.0.tgz", + "integrity": "sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.56.0.tgz", + "integrity": "sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.56.0.tgz", + "integrity": "sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.56.0.tgz", + "integrity": "sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.56.0.tgz", + "integrity": "sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.56.0.tgz", + "integrity": "sha512-LhN/Reh+7F3RCgQIRbgw8ZMwUwyqJM+8pXNT6IIJAqm2IdKkzpCh/V9EdgOMBKuebIrzswqy4ATlrDgiOwbRcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.56.0.tgz", + "integrity": "sha512-kbFsOObXp3LBULg1d3JIUQMa9Kv4UitDmpS+k0tinPBz3watcUiV2/LUDMMucA6pZO3WGE27P7DsfaN54l9ing==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.56.0.tgz", + "integrity": "sha512-vSSgny54D6P4vf2izbtFm/TcWYedw7f8eBrOiGGecyHyQB9q4Kqentjaj8hToe+995nob/Wv48pDqL5a62EWtg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.56.0.tgz", + "integrity": "sha512-FeCnkPCTHQJFbiGG49KjV5YGW/8b9rrXAM2Mz2kiIoktq2qsJxRD5giEMEOD2lPdgs72upzefaUvS+nc8E3UzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.56.0.tgz", + "integrity": "sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==", + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz", + "integrity": "sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.0.0 || ^5.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.27.tgz", + "integrity": "sha512-gnSBQjZA+//qDZen+6a2EdHqJ68Z7uybrMf3SPjEGgG4dicklwDVmMC1AeIHxtLVPT7sn6sH1KOO+tS6gwOUeQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/shared": "3.5.27", + "entities": "^7.0.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.27.tgz", + "integrity": "sha512-oAFea8dZgCtVVVTEC7fv3T5CbZW9BxpFzGGxC79xakTr6ooeEqmRuvQydIiDAkglZEAd09LgVf1RoDnL54fu5w==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.27", + "@vue/shared": "3.5.27" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.27.tgz", + "integrity": "sha512-sHZu9QyDPeDmN/MRoshhggVOWE5WlGFStKFwu8G52swATgSny27hJRWteKDSUUzUH+wp+bmeNbhJnEAel/auUQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/compiler-core": "3.5.27", + "@vue/compiler-dom": "3.5.27", + "@vue/compiler-ssr": "3.5.27", + "@vue/shared": "3.5.27", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.27.tgz", + "integrity": "sha512-Sj7h+JHt512fV1cTxKlYhg7qxBvack+BGncSpH+8vnN+KN95iPIcqB5rsbblX40XorP+ilO7VIKlkuu3Xq2vjw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.27", + "@vue/shared": "3.5.27" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.27.tgz", + "integrity": "sha512-vvorxn2KXfJ0nBEnj4GYshSgsyMNFnIQah/wczXlsNXt+ijhugmW+PpJ2cNPe4V6jpnBcs0MhCODKllWG+nvoQ==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.27" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.27.tgz", + "integrity": "sha512-fxVuX/fzgzeMPn/CLQecWeDIFNt3gQVhxM0rW02Tvp/YmZfXQgcTXlakq7IMutuZ/+Ogbn+K0oct9J3JZfyk3A==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.27", + "@vue/shared": "3.5.27" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.27.tgz", + "integrity": "sha512-/QnLslQgYqSJ5aUmb5F0z0caZPGHRB8LEAQ1s81vHFM5CBfnun63rxhvE/scVb/j3TbBuoZwkJyiLCkBluMpeg==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.27", + "@vue/runtime-core": "3.5.27", + "@vue/shared": "3.5.27", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.27.tgz", + "integrity": "sha512-qOz/5thjeP1vAFc4+BY3Nr6wxyLhpeQgAE/8dDtKo6a6xdk+L4W46HDZgNmLOBUDEkFXV3G7pRiUqxjX0/2zWA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.27", + "@vue/shared": "3.5.27" + }, + "peerDependencies": { + "vue": "3.5.27" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.27.tgz", + "integrity": "sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ==", + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz", + "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "10.11.1", + "@vueuse/shared": "10.11.1", + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/metadata": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz", + "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz", + "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==", + "license": "MIT", + "dependencies": { + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/echarts": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz", + "integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "2.3.0", + "zrender": "5.6.1" + } + }, + "node_modules/element-plus": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.13.1.tgz", + "integrity": "sha512-eG4BDBGdAsUGN6URH1PixzZb0ngdapLivIk1meghS1uEueLvQ3aljSKrCt5x6sYb6mUk8eGtzTQFgsPmLavQcA==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^3.4.1", + "@element-plus/icons-vue": "^2.3.2", + "@floating-ui/dom": "^1.0.1", + "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7", + "@types/lodash": "^4.17.20", + "@types/lodash-es": "^4.17.12", + "@vueuse/core": "^10.11.0", + "async-validator": "^4.2.5", + "dayjs": "^1.11.19", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "lodash-unified": "^1.0.3", + "memoize-one": "^6.0.0", + "normalize-wheel-es": "^1.2.0" + }, + "peerDependencies": { + "vue": "^3.3.0" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", + "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", + "license": "MIT" + }, + "node_modules/lodash-unified": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.3.tgz", + "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", + "license": "MIT", + "peerDependencies": { + "@types/lodash-es": "*", + "lodash": "*", + "lodash-es": "*" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/normalize-wheel-es": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", + "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==", + "license": "BSD-3-Clause" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/pinia": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz", + "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/resize-detector": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/resize-detector/-/resize-detector-0.3.0.tgz", + "integrity": "sha512-R/tCuvuOHQ8o2boRP6vgx8hXCCy87H1eY9V5imBYeVNyNVpuL9ciReSccLj2gDcax9+2weXy3bc8Vv+NRXeEvQ==", + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.56.0.tgz", + "integrity": "sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.56.0", + "@rollup/rollup-android-arm64": "4.56.0", + "@rollup/rollup-darwin-arm64": "4.56.0", + "@rollup/rollup-darwin-x64": "4.56.0", + "@rollup/rollup-freebsd-arm64": "4.56.0", + "@rollup/rollup-freebsd-x64": "4.56.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.56.0", + "@rollup/rollup-linux-arm-musleabihf": "4.56.0", + "@rollup/rollup-linux-arm64-gnu": "4.56.0", + "@rollup/rollup-linux-arm64-musl": "4.56.0", + "@rollup/rollup-linux-loong64-gnu": "4.56.0", + "@rollup/rollup-linux-loong64-musl": "4.56.0", + "@rollup/rollup-linux-ppc64-gnu": "4.56.0", + "@rollup/rollup-linux-ppc64-musl": "4.56.0", + "@rollup/rollup-linux-riscv64-gnu": "4.56.0", + "@rollup/rollup-linux-riscv64-musl": "4.56.0", + "@rollup/rollup-linux-s390x-gnu": "4.56.0", + "@rollup/rollup-linux-x64-gnu": "4.56.0", + "@rollup/rollup-linux-x64-musl": "4.56.0", + "@rollup/rollup-openbsd-x64": "4.56.0", + "@rollup/rollup-openharmony-arm64": "4.56.0", + "@rollup/rollup-win32-arm64-msvc": "4.56.0", + "@rollup/rollup-win32-ia32-msvc": "4.56.0", + "@rollup/rollup-win32-x64-gnu": "4.56.0", + "@rollup/rollup-win32-x64-msvc": "4.56.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.27.tgz", + "integrity": "sha512-aJ/UtoEyFySPBGarREmN4z6qNKpbEguYHMmXSiOGk69czc+zhs0NF6tEFrY8TZKAl8N/LYAkd4JHVd5E/AsSmw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.27", + "@vue/compiler-sfc": "3.5.27", + "@vue/runtime-dom": "3.5.27", + "@vue/server-renderer": "3.5.27", + "@vue/shared": "3.5.27" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-echarts": { + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/vue-echarts/-/vue-echarts-6.7.3.tgz", + "integrity": "sha512-vXLKpALFjbPphW9IfQPOVfb1KjGZ/f8qa/FZHi9lZIWzAnQC1DgnmEK3pJgEkyo6EP7UnX6Bv/V3Ke7p+qCNXA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "resize-detector": "^0.3.0", + "vue-demi": "^0.13.11" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.5", + "@vue/runtime-core": "^3.0.0", + "echarts": "^5.4.1", + "vue": "^2.6.12 || ^3.1.1" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + }, + "@vue/runtime-core": { + "optional": true + } + } + }, + "node_modules/vue-echarts/node_modules/vue-demi": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.11.tgz", + "integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/zrender": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz", + "integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==", + "license": "BSD-3-Clause", + "dependencies": { + "tslib": "2.3.0" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..34ac543 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,25 @@ +{ + "name": "claude-code-frontend", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "vue": "^3.3.4", + "vue-router": "^4.2.5", + "pinia": "^2.1.7", + "axios": "^1.6.2", + "element-plus": "^2.4.4", + "@element-plus/icons-vue": "^2.3.1", + "echarts": "^5.4.3", + "vue-echarts": "^6.6.1", + "dayjs": "^1.11.10" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^4.5.0", + "vite": "^5.0.8" + } +} diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..2850ba5 --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/frontend/src/api/auth.js b/frontend/src/api/auth.js new file mode 100644 index 0000000..999f66f --- /dev/null +++ b/frontend/src/api/auth.js @@ -0,0 +1,16 @@ +import api from './index' + +export const authApi = { + register(data) { + return api.post('/auth/register', data) + }, + login(data) { + return api.post('/auth/login', data) + }, + getCurrentUser() { + return api.get('/auth/me') + }, + refreshToken() { + return api.post('/auth/refresh') + } +} diff --git a/frontend/src/api/dashboard.js b/frontend/src/api/dashboard.js new file mode 100644 index 0000000..accab9d --- /dev/null +++ b/frontend/src/api/dashboard.js @@ -0,0 +1,10 @@ +import api from './index' + +export const dashboardApi = { + getStats() { + return api.get('/dashboard/stats') + }, + getServerInfo() { + return api.get('/dashboard/server-info') + } +} diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js new file mode 100644 index 0000000..03f02a8 --- /dev/null +++ b/frontend/src/api/index.js @@ -0,0 +1,65 @@ +import axios from 'axios' +import { useAuthStore } from '@/stores/auth' + +const api = axios.create({ + baseURL: '/api', + timeout: 10000 +}) + +// 请求拦截器 +api.interceptors.request.use( + config => { + const authStore = useAuthStore() + if (authStore.token) { + // 确保token格式正确:Bearer + const token = authStore.token.trim() + if (!token.startsWith('Bearer ')) { + config.headers.Authorization = `Bearer ${token}` + } else { + config.headers.Authorization = token + } + // 调试日志 + if (process.env.NODE_ENV === 'development') { + console.log(`[API Request] ${config.method?.toUpperCase()} ${config.url}`) + console.log(`[Token] ${token.substring(0, 20)}...`) + } + } else { + console.warn('请求缺少Token:', config.url) + } + return config + }, + error => { + return Promise.reject(error) + } +) + +// 响应拦截器 +api.interceptors.response.use( + response => { + return response.data + }, + error => { + const authStore = useAuthStore() + + // 401: 未授权,token无效或过期 + if (error.response?.status === 401) { + authStore.logout() + window.location.href = '/login' + return Promise.reject(error) + } + + // 422: Token验证失败 + if (error.response?.status === 422) { + const errorMsg = error.response?.data?.error || 'Token验证失败,请重新登录' + console.error('JWT验证失败:', errorMsg) + // 清除无效的token + authStore.logout() + // 不自动跳转,让调用方决定如何处理 + return Promise.reject(error) + } + + return Promise.reject(error) + } +) + +export default api diff --git a/frontend/src/api/logs.js b/frontend/src/api/logs.js new file mode 100644 index 0000000..36dca9c --- /dev/null +++ b/frontend/src/api/logs.js @@ -0,0 +1,7 @@ +import api from './index' + +export const logsApi = { + getUsageLogs(params) { + return api.get('/logs/usage', { params }) + } +} diff --git a/frontend/src/api/models.js b/frontend/src/api/models.js new file mode 100644 index 0000000..51066d5 --- /dev/null +++ b/frontend/src/api/models.js @@ -0,0 +1,13 @@ +import api from './index' + +export const modelsApi = { + getModels(params) { + return api.get('/models', { params }) + }, + getModel(id) { + return api.get(`/models/${id}`) + }, + getFilters() { + return api.get('/models/filters') + } +} diff --git a/frontend/src/api/recharge.js b/frontend/src/api/recharge.js new file mode 100644 index 0000000..ec2f02a --- /dev/null +++ b/frontend/src/api/recharge.js @@ -0,0 +1,16 @@ +import api from './index' + +export const rechargeApi = { + getRechargeInfo() { + return api.get('/recharge/info') + }, + createRecharge(data) { + return api.post('/recharge/create', data) + }, + exchangeCode(data) { + return api.post('/recharge/exchange', data) + }, + transferReward() { + return api.post('/recharge/transfer-reward') + } +} diff --git a/frontend/src/api/tokens.js b/frontend/src/api/tokens.js new file mode 100644 index 0000000..3a0712e --- /dev/null +++ b/frontend/src/api/tokens.js @@ -0,0 +1,22 @@ +import api from './index' + +export const tokensApi = { + getTokens(params) { + return api.get('/tokens', { params }) + }, + createToken(data) { + return api.post('/tokens', data) + }, + getToken(id) { + return api.get(`/tokens/${id}`) + }, + updateToken(id, data) { + return api.put(`/tokens/${id}`, data) + }, + deleteToken(id) { + return api.delete(`/tokens/${id}`) + }, + batchOperation(data) { + return api.post('/tokens/batch', data) + } +} diff --git a/frontend/src/api/user.js b/frontend/src/api/user.js new file mode 100644 index 0000000..7a9e891 --- /dev/null +++ b/frontend/src/api/user.js @@ -0,0 +1,16 @@ +import api from './index' + +export const userApi = { + getProfile() { + return api.get('/user/profile') + }, + updateProfile(data) { + return api.put('/user/profile', data) + }, + getSettings() { + return api.get('/user/settings') + }, + updateSettings(data) { + return api.put('/user/settings', data) + } +} diff --git a/frontend/src/components/Header.vue b/frontend/src/components/Header.vue new file mode 100644 index 0000000..d76ff4f --- /dev/null +++ b/frontend/src/components/Header.vue @@ -0,0 +1,231 @@ + + + + + diff --git a/frontend/src/layouts/ConsoleLayout.vue b/frontend/src/layouts/ConsoleLayout.vue new file mode 100644 index 0000000..b8f23b4 --- /dev/null +++ b/frontend/src/layouts/ConsoleLayout.vue @@ -0,0 +1,433 @@ + + + + + diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000..e04162a --- /dev/null +++ b/frontend/src/main.js @@ -0,0 +1,23 @@ +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import ElementPlus from 'element-plus' +import 'element-plus/dist/index.css' +import * as ElementPlusIconsVue from '@element-plus/icons-vue' +import zhCn from 'element-plus/dist/locale/zh-cn.mjs' +import App from './App.vue' +import router from './router' +import './style.css' + +const app = createApp(App) +const pinia = createPinia() + +// 注册所有图标 +for (const [key, component] of Object.entries(ElementPlusIconsVue)) { + app.component(key, component) +} + +app.use(pinia) +app.use(router) +app.use(ElementPlus, { locale: zhCn }) + +app.mount('#app') diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js new file mode 100644 index 0000000..4e466a0 --- /dev/null +++ b/frontend/src/router/index.js @@ -0,0 +1,118 @@ +import { createRouter, createWebHistory } from 'vue-router' +import { useAuthStore } from '@/stores/auth' + +const routes = [ + { + path: '/', + name: 'Home', + component: () => import('@/views/Home.vue') + }, + { + path: '/login', + name: 'Login', + component: () => import('@/views/Login.vue'), + meta: { requiresGuest: true } + }, + { + path: '/register', + name: 'Register', + component: () => import('@/views/Register.vue'), + meta: { requiresGuest: true } + }, + { + path: '/models', + name: 'ModelSquare', + component: () => import('@/views/ModelSquare.vue') + }, + { + path: '/console', + component: () => import('@/layouts/ConsoleLayout.vue'), + meta: { requiresAuth: true }, + children: [ + { + path: '', + name: 'Dashboard', + component: () => import('@/views/console/Dashboard.vue') + }, + { + path: 'tokens', + name: 'Tokens', + component: () => import('@/views/console/Tokens.vue') + }, + { + path: 'logs', + name: 'Logs', + component: () => import('@/views/console/Logs.vue') + }, + { + path: 'draw-logs', + name: 'DrawLogs', + component: () => import('@/views/console/DrawLogs.vue') + }, + { + path: 'task-logs', + name: 'TaskLogs', + component: () => import('@/views/console/TaskLogs.vue') + }, + { + path: 'recharge', + name: 'Recharge', + component: () => import('@/views/console/Recharge.vue') + }, + { + path: 'settings', + name: 'Settings', + component: () => import('@/views/console/Settings.vue') + } + ] + } +] + +const router = createRouter({ + history: createWebHistory(), + routes +}) + +// 缓存用户信息获取,避免重复请求 +let userFetchPromise = null + +router.beforeEach(async (to, from, next) => { + const authStore = useAuthStore() + + if (to.meta.requiresAuth) { + if (!authStore.isAuthenticated) { + // 未登录,跳转到登录页 + next('/login') + return + } + + // 如果token存在但用户信息不存在,尝试获取用户信息 + // 使用缓存避免重复请求 + if (!authStore.user && authStore.token) { + try { + // 如果已经有正在进行的请求,等待它完成 + if (!userFetchPromise) { + userFetchPromise = authStore.fetchUser() + } + await userFetchPromise + userFetchPromise = null + } catch (error) { + // 获取用户信息失败,清除token并跳转登录 + console.error('获取用户信息失败:', error) + userFetchPromise = null + authStore.logout() + next('/login') + return + } + } + } + + if (to.meta.requiresGuest && authStore.isAuthenticated) { + next('/console') + return + } + + next() +}) + +export default router diff --git a/frontend/src/stores/auth.js b/frontend/src/stores/auth.js new file mode 100644 index 0000000..ca53f24 --- /dev/null +++ b/frontend/src/stores/auth.js @@ -0,0 +1,70 @@ +import { defineStore } from 'pinia' +import { authApi } from '@/api/auth' +import router from '@/router' + +export const useAuthStore = defineStore('auth', { + state: () => ({ + token: localStorage.getItem('token') || '', + refreshToken: localStorage.getItem('refreshToken') || '', + user: null + }), + + getters: { + isAuthenticated: (state) => !!state.token, + username: (state) => state.user?.username || '' + }, + + actions: { + async login(credentials) { + try { + const response = await authApi.login(credentials) + // 确保token格式正确(不包含Bearer前缀) + this.token = response.access_token?.replace(/^Bearer\s+/, '') || response.access_token + this.refreshToken = response.refresh_token?.replace(/^Bearer\s+/, '') || response.refresh_token + this.user = response.user + localStorage.setItem('token', this.token) + localStorage.setItem('refreshToken', this.refreshToken) + console.log('登录成功,Token已保存:', this.token.substring(0, 20) + '...') + return response + } catch (error) { + throw error + } + }, + + async register(data) { + try { + const response = await authApi.register(data) + // 确保token格式正确(不包含Bearer前缀) + this.token = response.access_token?.replace(/^Bearer\s+/, '') || response.access_token + this.refreshToken = response.refresh_token?.replace(/^Bearer\s+/, '') || response.refresh_token + this.user = response.user + localStorage.setItem('token', this.token) + localStorage.setItem('refreshToken', this.refreshToken) + console.log('注册成功,Token已保存:', this.token.substring(0, 20) + '...') + return response + } catch (error) { + throw error + } + }, + + async fetchUser() { + try { + const response = await authApi.getCurrentUser() + this.user = response.user + return response + } catch (error) { + this.logout() + throw error + } + }, + + logout() { + this.token = '' + this.refreshToken = '' + this.user = null + localStorage.removeItem('token') + localStorage.removeItem('refreshToken') + router.push('/login') + } + } +}) diff --git a/frontend/src/style.css b/frontend/src/style.css new file mode 100644 index 0000000..dc10196 --- /dev/null +++ b/frontend/src/style.css @@ -0,0 +1,222 @@ +:root { + --primary-color: #409eff; + --success-color: #67c23a; + --warning-color: #e6a23c; + --danger-color: #f56c6c; + --info-color: #909399; + --brand-1: #5b8cff; + --brand-2: #7a5cff; + --bg: #f5f7fb; + --surface: #ffffff; + --text-primary: #1f2a44; + --text-secondary: #5b6b8a; + --border-color: #e6ebf5; + --shadow-sm: 0 6px 18px rgba(19, 35, 74, 0.08); + --shadow-md: 0 14px 34px rgba(19, 35, 74, 0.12); + --glow-primary: 0 0 0 rgba(91, 140, 255, 0.0), 0 0 18px rgba(91, 140, 255, 0.35); + --glow-accent: 0 0 0 rgba(122, 92, 255, 0.0), 0 0 22px rgba(122, 92, 255, 0.35); + --grid-line: rgba(91, 140, 255, 0.08); +} + +body { + background: var(--bg); + color: var(--text-primary); + position: relative; +} + +body::after { + content: ''; + position: fixed; + inset: 0; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.04), transparent 35%), + repeating-linear-gradient(0deg, rgba(91, 140, 255, 0.04) 0 1px, transparent 1px 3px); + opacity: 0.35; + pointer-events: none; + z-index: 0; +} + +@keyframes neonPulse { + 0% { + box-shadow: 0 8px 24px rgba(91, 140, 255, 0.25), var(--glow-primary); + } + 100% { + box-shadow: 0 10px 30px rgba(122, 92, 255, 0.35), var(--glow-accent); + } +} + +@keyframes shimmer { + 0% { + background-position: 0% 50%; + } + 100% { + background-position: 100% 50%; + } +} + +a { + color: inherit; + text-decoration: none; +} + +button, +.el-button { + border-radius: 10px; + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.el-button--primary { + background: linear-gradient(135deg, var(--brand-1), var(--brand-2), #4ee3ff); + background-size: 200% 200%; + border: none; + box-shadow: var(--glow-primary); + animation: shimmer 6s ease infinite; +} + +.el-button--primary:hover { + box-shadow: 0 8px 24px rgba(91, 140, 255, 0.35), var(--glow-accent); + transform: translateY(-1px); +} + +.el-button:active { + transform: translateY(1px); +} + +.el-card { + border-radius: 16px; + border-color: var(--border-color); + box-shadow: var(--shadow-sm); + transition: transform 0.2s ease, box-shadow 0.2s ease; + background: var(--surface); + position: relative; + overflow: hidden; +} + +.el-card:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-md), 0 0 24px rgba(91, 140, 255, 0.18); +} + +.el-card::after { + content: ''; + position: absolute; + inset: 0; + border-radius: 16px; + background: radial-gradient(circle at 20% 10%, rgba(91, 140, 255, 0.16), transparent 35%); + opacity: 0.6; + pointer-events: none; +} + +.el-input__wrapper, +.el-textarea__inner { + border-radius: 10px; +} + +.el-input__wrapper:focus-within, +.el-textarea__inner:focus-within { + box-shadow: 0 0 0 1px rgba(91, 140, 255, 0.45), 0 0 18px rgba(91, 140, 255, 0.15); +} + +.el-table th.el-table__cell { + background: linear-gradient(90deg, rgba(91, 140, 255, 0.12), rgba(122, 92, 255, 0.12)); +} + +.el-table tr:hover > td.el-table__cell { + background: rgba(91, 140, 255, 0.06); +} + +/* 细腻滚动条 */ +*::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +*::-webkit-scrollbar-thumb { + background: rgba(91, 140, 255, 0.35); + border-radius: 999px; +} + +*::-webkit-scrollbar-track { + background: rgba(91, 140, 255, 0.08); +} + +/* 全局响应式样式 */ +@media (max-width: 768px) { + /* 表格在小屏幕上横向滚动 */ + .el-table { + overflow-x: auto; + } + + /* 卡片在小屏幕上全宽 */ + .el-card { + margin-bottom: 12px; + } + + /* 按钮组在小屏幕上换行 */ + .el-button-group { + flex-wrap: wrap; + } + + /* 表单在小屏幕上垂直排列 */ + .el-form-item { + margin-bottom: 16px; + } + + /* 对话框在小屏幕上全屏 */ + .el-dialog { + width: 95% !important; + margin: 5vh auto !important; + } + + /* 抽屉在小屏幕上全屏 */ + .el-drawer { + width: 85% !important; + } +} + +@media (max-width: 480px) { + /* 超小屏幕进一步优化 */ + .el-dialog { + width: 100% !important; + margin: 0 !important; + border-radius: 0; + } + + .el-drawer { + width: 100% !important; + } + + /* 按钮在小屏幕上全宽 */ + .el-button { + width: 100%; + margin-bottom: 8px; + } + + .el-button + .el-button { + margin-left: 0; + } +} + +/* 确保所有图片在小屏幕上响应式 */ +img { + max-width: 100%; + height: auto; +} + +/* 文本在小屏幕上更易读 */ +@media (max-width: 768px) { + body { + font-size: 14px; + } + + h1 { + font-size: 24px; + } + + h2 { + font-size: 20px; + } + + h3 { + font-size: 18px; + } +} diff --git a/frontend/src/views/Home.vue b/frontend/src/views/Home.vue new file mode 100644 index 0000000..e3300ea --- /dev/null +++ b/frontend/src/views/Home.vue @@ -0,0 +1,504 @@ + + + + + diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue new file mode 100644 index 0000000..f8d7c19 --- /dev/null +++ b/frontend/src/views/Login.vue @@ -0,0 +1,185 @@ + + + + + diff --git a/frontend/src/views/ModelSquare.vue b/frontend/src/views/ModelSquare.vue new file mode 100644 index 0000000..3e578e3 --- /dev/null +++ b/frontend/src/views/ModelSquare.vue @@ -0,0 +1,898 @@ + + + + + diff --git a/frontend/src/views/Register.vue b/frontend/src/views/Register.vue new file mode 100644 index 0000000..bb97d74 --- /dev/null +++ b/frontend/src/views/Register.vue @@ -0,0 +1,212 @@ + + + + + diff --git a/frontend/src/views/console/Dashboard.vue b/frontend/src/views/console/Dashboard.vue new file mode 100644 index 0000000..a758748 --- /dev/null +++ b/frontend/src/views/console/Dashboard.vue @@ -0,0 +1,344 @@ + + + + + diff --git a/frontend/src/views/console/DrawLogs.vue b/frontend/src/views/console/DrawLogs.vue new file mode 100644 index 0000000..0321613 --- /dev/null +++ b/frontend/src/views/console/DrawLogs.vue @@ -0,0 +1,19 @@ + + + + + diff --git a/frontend/src/views/console/Logs.vue b/frontend/src/views/console/Logs.vue new file mode 100644 index 0000000..28be4e6 --- /dev/null +++ b/frontend/src/views/console/Logs.vue @@ -0,0 +1,234 @@ + + + + + diff --git a/frontend/src/views/console/Recharge.vue b/frontend/src/views/console/Recharge.vue new file mode 100644 index 0000000..e05d9b8 --- /dev/null +++ b/frontend/src/views/console/Recharge.vue @@ -0,0 +1,473 @@ + + + + + diff --git a/frontend/src/views/console/Settings.vue b/frontend/src/views/console/Settings.vue new file mode 100644 index 0000000..c93f9e4 --- /dev/null +++ b/frontend/src/views/console/Settings.vue @@ -0,0 +1,319 @@ + + + + + diff --git a/frontend/src/views/console/TaskLogs.vue b/frontend/src/views/console/TaskLogs.vue new file mode 100644 index 0000000..6501a6f --- /dev/null +++ b/frontend/src/views/console/TaskLogs.vue @@ -0,0 +1,19 @@ + + + + + diff --git a/frontend/src/views/console/Tokens.vue b/frontend/src/views/console/Tokens.vue new file mode 100644 index 0000000..82703a9 --- /dev/null +++ b/frontend/src/views/console/Tokens.vue @@ -0,0 +1,366 @@ + + + + + diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..878b3a3 --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,24 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import path from 'path' + +export default defineConfig({ + plugins: [vue()], + resolve: { + alias: { + '@': path.resolve(__dirname, 'src') + } + }, + server: { + host: '0.0.0.0', // 允许所有IP访问 + port: 5173, + strictPort: false, // 如果端口被占用,自动尝试下一个可用端口 + proxy: { + '/api': { + target: 'http://localhost:5000', + changeOrigin: true, + secure: false + } + } + } +})