commit ee10c46aae5daba89fbbd1b8c1cd0b56db6854ab Author: Codex Date: Sun Mar 8 19:28:32 2026 +0800 chore: initialize tuhui repository diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0af2353 --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# frontend +frontend/node_modules/ +frontend/dist/ +frontend/playwright-report/ +frontend/.vite/ +frontend/.cache/ + +# backend +backend/.env +backend/__pycache__/ +backend/app/__pycache__/ +backend/*.db +backend/uploads/ +backend/.pytest_cache/ +backend/.mypy_cache/ +backend/.ruff_cache/ + +# runtime data +uploads/ +*.log +*.pid + +# local/editor +.vscode/ +.idea/ +.DS_Store +Thumbs.db + +# legacy/backup project not in active deployment +nitu/ diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..6554ee3 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.11-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple + +COPY . . + +EXPOSE 8002 + +CMD ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8002"] diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..9bb04db --- /dev/null +++ b/backend/README.md @@ -0,0 +1,128 @@ +# 爱设计后端 API + +基于 FastAPI 的付费下载平台后端 + +## 功能特性 + +- ✅ 用户注册/登录(JWT认证) +- ✅ 作品浏览/搜索 +- ✅ 付费下载(易收米支付) +- ✅ 图片水印处理 +- ✅ 缩略图生成 +- ✅ 订单管理 +- ✅ 支付回调处理 +- ✅ 订单状态查询 +- ✅ Docker 容器化部署 + +## 快速开始 + +### 1. 使用 Docker Compose(推荐) + +```bash +# 启动所有服务 +docker-compose up -d + +# 查看日志 +docker-compose logs -f + +# 停止服务 +docker-compose down +``` + +### 2. 本地开发 + +```bash +# 安装依赖 +pip install -r requirements.txt + +# 复制环境变量文件 +cp .env.example .env + +# 修改 .env 中的配置 + +# 启动服务 +uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 +``` + +## API 文档 + +启动后访问: +- Swagger UI: http://localhost:8000/docs +- ReDoc: http://localhost:8000/redoc + +## 目录结构 + +``` +backend/ +├── app/ +│ ├── api/ # API 路由 +│ ├── core/ # 核心配置 +│ ├── models/ # 数据库模型 +│ ├── schemas/ # Pydantic 模型 +│ ├── services/ # 业务逻辑 +│ ├── utils/ # 工具函数 +│ └── main.py # 入口文件 +├── uploads/ # 文件上传目录 +│ ├── original/ # 原图(高清) +│ ├── watermarked/ # 带水印图 +│ └── thumbnail/ # 缩略图 +├── requirements.txt +├── Dockerfile +└── docker-compose.yml +``` + +## 环境变量 + +查看 `.env.example` 文件了解所有配置项 + +## 部署到生产环境 + +### 使用 Caddy + +在服务器上创建 Caddyfile: + +``` +api.aishej.com { + reverse_proxy localhost:8000 +} +``` + +然后启动 Caddy: + +```bash +caddy start +``` + +## 接口列表 + +### 认证 +- POST `/api/auth/register` - 用户注册 +- POST `/api/auth/login` - 用户登录 + +### 作品 +- GET `/api/works` - 作品列表 +- GET `/api/works/{id}` - 作品详情 + +### 订单 +- POST `/api/orders/create` - 创建订单 +- POST `/api/orders/pay/{id}` - 支付订单(模拟支付,仅测试用) +- GET `/api/orders/my` - 我的订单 + +### 支付(易收米) +- POST `/api/payment/create/{order_id}` - 创建支付链接(真实支付) +- POST `/api/payment/notify` - 支付回调接口(易收米异步通知) +- GET `/api/payment/query/{order_number}` - 查询订单支付状态 +- POST `/api/payment/cancel/{order_id}` - 取消订单 + +## 后续开发 + +- [ ] 接入真实支付接口(支付宝/微信) +- [ ] 作品上传管理 +- [ ] 管理后台 +- [ ] 数据统计 +- [ ] Redis 缓存 +- [ ] 日志系统 + +## License + +MIT diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..f6287a2 --- /dev/null +++ b/backend/app/__init__.py @@ -0,0 +1,3 @@ +""" +FastAPI 后端入口文件的 __init__.py +""" diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py new file mode 100644 index 0000000..fb31fe2 --- /dev/null +++ b/backend/app/api/__init__.py @@ -0,0 +1 @@ +"""API 路由""" diff --git a/backend/app/api/__pycache__/__init__.cpython-310.pyc b/backend/app/api/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..0f1c0a4 Binary files /dev/null and b/backend/app/api/__pycache__/__init__.cpython-310.pyc differ diff --git a/backend/app/api/__pycache__/__init__.cpython-311.pyc b/backend/app/api/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..f6f40d1 Binary files /dev/null and b/backend/app/api/__pycache__/__init__.cpython-311.pyc differ diff --git a/backend/app/api/__pycache__/__init__.cpython-312.pyc b/backend/app/api/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..d3bdf51 Binary files /dev/null and b/backend/app/api/__pycache__/__init__.cpython-312.pyc differ diff --git a/backend/app/api/__pycache__/auth.cpython-310.pyc b/backend/app/api/__pycache__/auth.cpython-310.pyc new file mode 100644 index 0000000..24d6747 Binary files /dev/null and b/backend/app/api/__pycache__/auth.cpython-310.pyc differ diff --git a/backend/app/api/__pycache__/auth.cpython-311.pyc b/backend/app/api/__pycache__/auth.cpython-311.pyc new file mode 100644 index 0000000..77bc901 Binary files /dev/null and b/backend/app/api/__pycache__/auth.cpython-311.pyc differ diff --git a/backend/app/api/__pycache__/auth.cpython-312.pyc b/backend/app/api/__pycache__/auth.cpython-312.pyc new file mode 100644 index 0000000..d2b9369 Binary files /dev/null and b/backend/app/api/__pycache__/auth.cpython-312.pyc differ diff --git a/backend/app/api/__pycache__/orders.cpython-310.pyc b/backend/app/api/__pycache__/orders.cpython-310.pyc new file mode 100644 index 0000000..0b08136 Binary files /dev/null and b/backend/app/api/__pycache__/orders.cpython-310.pyc differ diff --git a/backend/app/api/__pycache__/orders.cpython-311.pyc b/backend/app/api/__pycache__/orders.cpython-311.pyc new file mode 100644 index 0000000..04c5688 Binary files /dev/null and b/backend/app/api/__pycache__/orders.cpython-311.pyc differ diff --git a/backend/app/api/__pycache__/orders.cpython-312.pyc b/backend/app/api/__pycache__/orders.cpython-312.pyc new file mode 100644 index 0000000..2806357 Binary files /dev/null and b/backend/app/api/__pycache__/orders.cpython-312.pyc differ diff --git a/backend/app/api/__pycache__/payment.cpython-310.pyc b/backend/app/api/__pycache__/payment.cpython-310.pyc new file mode 100644 index 0000000..8bcedc9 Binary files /dev/null and b/backend/app/api/__pycache__/payment.cpython-310.pyc differ diff --git a/backend/app/api/__pycache__/payment.cpython-311.pyc b/backend/app/api/__pycache__/payment.cpython-311.pyc new file mode 100644 index 0000000..7e22fb8 Binary files /dev/null and b/backend/app/api/__pycache__/payment.cpython-311.pyc differ diff --git a/backend/app/api/__pycache__/payment.cpython-312.pyc b/backend/app/api/__pycache__/payment.cpython-312.pyc new file mode 100644 index 0000000..e728f87 Binary files /dev/null and b/backend/app/api/__pycache__/payment.cpython-312.pyc differ diff --git a/backend/app/api/__pycache__/upload.cpython-311.pyc b/backend/app/api/__pycache__/upload.cpython-311.pyc new file mode 100644 index 0000000..26e3570 Binary files /dev/null and b/backend/app/api/__pycache__/upload.cpython-311.pyc differ diff --git a/backend/app/api/__pycache__/works.cpython-310.pyc b/backend/app/api/__pycache__/works.cpython-310.pyc new file mode 100644 index 0000000..ac9f98b Binary files /dev/null and b/backend/app/api/__pycache__/works.cpython-310.pyc differ diff --git a/backend/app/api/__pycache__/works.cpython-311.pyc b/backend/app/api/__pycache__/works.cpython-311.pyc new file mode 100644 index 0000000..7cc7ba9 Binary files /dev/null and b/backend/app/api/__pycache__/works.cpython-311.pyc differ diff --git a/backend/app/api/__pycache__/works.cpython-312.pyc b/backend/app/api/__pycache__/works.cpython-312.pyc new file mode 100644 index 0000000..7443921 Binary files /dev/null and b/backend/app/api/__pycache__/works.cpython-312.pyc differ diff --git a/backend/app/api/auth.py b/backend/app/api/auth.py new file mode 100644 index 0000000..5db973b --- /dev/null +++ b/backend/app/api/auth.py @@ -0,0 +1,79 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from app.core.database import get_db +from app.core.security import get_password_hash, verify_password, create_access_token +from app.models.user import User +from app.schemas.user import UserRegister, UserLogin, Token, UserResponse + +router = APIRouter(prefix="/auth", tags=["认证"]) + +@router.post("/register", response_model=Token, summary="用户注册") +def register(user_data: UserRegister, db: Session = Depends(get_db)): + """用户注册(使用手机号,无需验证码)""" + try: + # 检查手机号是否已存在 + existing_user = db.query(User).filter(User.phone == user_data.phone).first() + if existing_user: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="该手机号已被注册" + ) + + # 创建新用户 + hashed_password = get_password_hash(user_data.password) + new_user = User( + phone=user_data.phone, + password_hash=hashed_password, + nickname=user_data.nickname or f"用户{user_data.phone[-4:]}", + balance=0.0 + ) + + db.add(new_user) + db.commit() + db.refresh(new_user) + + # 生成 Token + access_token = create_access_token(data={"sub": str(new_user.id)}) + + return Token( + access_token=access_token, + user=UserResponse.from_orm(new_user) + ) + except HTTPException: + raise + except Exception as e: + print(f"[ERROR] 注册失败: {e}") + print(f"[ERROR] 错误类型: {type(e).__name__}") + import traceback + traceback.print_exc() + db.rollback() + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"注册失败: {str(e)}" + ) + +@router.post("/login", response_model=Token, summary="用户登录") +def login(credentials: UserLogin, db: Session = Depends(get_db)): + """用户登录(使用手机号)""" + # 查找用户 + user = db.query(User).filter(User.phone == credentials.phone).first() + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="手机号或密码错误" + ) + + # 验证密码 + if not verify_password(credentials.password, user.password_hash): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="手机号或密码错误" + ) + + # 生成 Token + access_token = create_access_token(data={"sub": str(user.id)}) + + return Token( + access_token=access_token, + user=UserResponse.from_orm(user) + ) diff --git a/backend/app/api/orders.py b/backend/app/api/orders.py new file mode 100644 index 0000000..4f5048f --- /dev/null +++ b/backend/app/api/orders.py @@ -0,0 +1,171 @@ +from fastapi import APIRouter, Depends, HTTPException, status, Header +from sqlalchemy.orm import Session +from datetime import datetime, timedelta +import secrets +from app.core.database import get_db +from app.core.security import decode_access_token +from app.models.user import User +from app.models.work import Work +from app.models.order import Order, OrderStatus +from app.models.download import DownloadRecord +from app.schemas.order import OrderCreate, OrderResponse, PaymentResponse + +router = APIRouter(prefix="/orders", tags=["订单"]) + +def get_current_user(authorization: str = Header(None), db: Session = Depends(get_db)): + """获取当前登录用户""" + if not authorization or not authorization.startswith("Bearer "): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="未登录" + ) + + token = authorization.replace("Bearer ", "") + payload = decode_access_token(token) + if not payload: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Token 无效或已过期" + ) + + user_id = int(payload.get("sub")) + user = db.query(User).filter(User.id == user_id).first() + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="用户不存在" + ) + + return user + +@router.post("/create", response_model=OrderResponse, summary="创建订单") +def create_order( + order_data: OrderCreate, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """创建下载订单""" + # 查找作品 + work = db.query(Work).filter(Work.id == order_data.work_id).first() + if not work: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="作品不存在" + ) + + # 检查是否已购买 + existing_order = db.query(Order).filter( + Order.user_id == current_user.id, + Order.work_id == work.id, + Order.status == OrderStatus.PAID + ).first() + + if existing_order: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="您已购买过此作品" + ) + + # 生成订单号:前缀 + (当前时间-6个月)的年月日 + 当前时间的时分秒 + now = datetime.now() + six_months_ago = now - timedelta(days=180) # 半年前 + date_part = six_months_ago.strftime('%Y%m%d') # 半年前的年月日 + time_part = now.strftime('%H%M%S') # 当前时间的时分秒 + order_no = f"ORD{date_part}{time_part}" + + # 创建订单 + new_order = Order( + order_no=order_no, + user_id=current_user.id, + work_id=work.id, + amount=work.price, + payment_method=order_data.payment_method, + status=OrderStatus.PENDING + ) + + db.add(new_order) + db.commit() + db.refresh(new_order) + + return new_order + +@router.post("/pay/{order_id}", response_model=PaymentResponse, summary="支付订单") +def pay_order( + order_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """ + 支付订单(模拟支付) + 实际生产环境需要对接真实支付接口 + """ + # 查找订单 + order = db.query(Order).filter( + Order.id == order_id, + Order.user_id == current_user.id + ).first() + + if not order: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="订单不存在" + ) + + if order.status != OrderStatus.PENDING: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="订单状态异常" + ) + + # 余额支付 + if order.payment_method == "balance": + if current_user.balance < order.amount: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="余额不足" + ) + + # 扣除余额 + current_user.balance -= order.amount + + # 更新订单状态 + order.status = OrderStatus.PAID + order.paid_at = datetime.now() + + # 创建下载记录 + download_record = DownloadRecord( + user_id=current_user.id, + work_id=order.work_id, + order_id=order.id + ) + db.add(download_record) + + # 增加作品下载量 + work = db.query(Work).filter(Work.id == order.work_id).first() + work.downloads += 1 + + db.commit() + + return PaymentResponse( + success=True, + message="支付成功", + order_no=order.order_no, + download_url=f"/api/works/{work.id}/download" + ) + + # 其他支付方式(支付宝、微信) + else: + return PaymentResponse( + success=False, + message="暂不支持该支付方式,请使用余额支付或联系管理员", + order_no=order.order_no + ) + +@router.get("/my", summary="我的订单") +def get_my_orders( + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """获取当前用户的订单列表""" + orders = db.query(Order).filter(Order.user_id == current_user.id).all() + return orders diff --git a/backend/app/api/payment.py b/backend/app/api/payment.py new file mode 100644 index 0000000..f491447 --- /dev/null +++ b/backend/app/api/payment.py @@ -0,0 +1,267 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"""支付相关接口""" + +from fastapi import APIRouter, Depends, HTTPException, Request, BackgroundTasks +from sqlalchemy.orm import Session +from sqlalchemy import and_ +from datetime import datetime +import json + +from app.core.database import get_db +from app.core.config import settings +from app.models.order import Order, OrderStatus +from app.models.work import Work +from app.models.download import DownloadRecord +from app.ysm_sdk import create_payment, query_order, PaymentNotify +from app.api.orders import get_current_user +from app.models.user import User + +router = APIRouter(prefix="/payment", tags=["支付"]) + + +@router.post("/create/{order_id}") +async def create_payment_url( + order_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """ + 创建支付链接(真实支付) + + Args: + order_id: 订单ID + + Returns: + { + "pay_url": "支付链接", + "order_number": "订单号", + "amount": 金额(分) + } + """ + # 查询订单 + order = db.query(Order).filter( + and_( + Order.id == order_id, + Order.user_id == current_user.id + ) + ).first() + + if not order: + raise HTTPException(status_code=404, detail="订单不存在") + + # 检查订单状态 + if order.status == OrderStatus.PAID: + raise HTTPException(status_code=400, detail="订单已支付") + + if order.status == OrderStatus.CANCELLED: + raise HTTPException(status_code=400, detail="订单已取消") + + # 查询作品信息 + work = db.query(Work).filter(Work.id == order.work_id).first() + if not work: + raise HTTPException(status_code=404, detail="作品不存在") + + try: + # 创建支付 + pay_url = await create_payment( + appid=settings.YSM_APPID, + appsecret=settings.YSM_APPSECRET, + order_id=order.order_no, # 使用 order_no 字段 + description=f"{work.title} - 爱设计作品购买", + amount=int(order.amount * 100), # 元转分 + notify_url=settings.YSM_NOTIFY_URL, + nopay_url=settings.YSM_NOPAY_URL, + callback_url=settings.YSM_CALLBACK_URL, + pay_type=1 # 默认微信内支付 + ) + + return { + "pay_url": pay_url, + "order_number": order.order_no, # 使用 order_no 字段 + "amount": int(order.amount * 100), + "description": f"{work.title} - 爱设计作品购买" + } + + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post("/notify") +async def payment_notify( + request: Request, + background_tasks: BackgroundTasks, + db: Session = Depends(get_db) +): + """ + 支付回调接口(易收米异步通知) + + 当用户支付成功后,支付平台会异步请求此接口 + """ + # 创建通知处理器 + notify_handler = PaymentNotify( + appid=settings.YSM_APPID, + appsecret=settings.YSM_APPSECRET + ) + + # 获取请求数据 + try: + request_data = await request.body() + data = json.loads(request_data.decode('utf-8')) + except Exception as e: + return {"error": "数据格式错误"} + + # 验证签名 + success, message = await notify_handler.process(data) + + if not success: + return {"error": message} + + # 获取商户订单号 + mch_orderid = data.get('mch_orderid') + + # 查询订单 + order = db.query(Order).filter(Order.order_no == mch_orderid).first() + + if not order: + return {"error": "订单不存在"} + + # 防止重复处理 + if order.status == OrderStatus.PAID: + return "success" + + # 更新订单状态 + if data.get('state') == 'SUCCESS': + try: + # 更新订单 + order.status = OrderStatus.PAID + order.payment_method = "ysm_wechat" # 易收米微信支付 + order.paid_at = datetime.now() + + # 创建下载记录 + download_record = DownloadRecord( + user_id=order.user_id, + work_id=order.work_id, + order_id=order.id + ) + db.add(download_record) + + # 更新作品下载次数 + work = db.query(Work).filter(Work.id == order.work_id).first() + if work: + work.downloads += 1 + + db.commit() + + return "success" + + except Exception as e: + db.rollback() + print(f"处理支付回调失败: {str(e)}") + return {"error": "处理失败"} + + return {"error": "支付未完成"} + + +@router.get("/query/{order_number}") +async def query_order_status( + order_number: str, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """ + 查询订单支付状态 + + Args: + order_number: 订单号 + + Returns: + 订单状态信息 + """ + # 查询本地订单 + order = db.query(Order).filter( + and_( + Order.order_no == order_number, + Order.user_id == current_user.id + ) + ).first() + + if not order: + raise HTTPException(status_code=404, detail="订单不存在") + + try: + # 查询易收米订单状态 + result = await query_order( + appid=settings.YSM_APPID, + mch_orderid=order_number + ) + + if result: + # 如果远程订单已支付,但本地订单未更新,则更新本地订单 + if result.get('state') == 'SUCCESS' and order.status != OrderStatus.PAID: + order.status = OrderStatus.PAID + order.payment_method = "ysm_wechat" + order.paid_at = datetime.now() + + # 创建下载记录 + download_record = DownloadRecord( + user_id=order.user_id, + work_id=order.work_id, + order_id=order.id + ) + db.add(download_record) + + # 更新作品下载次数 + work = db.query(Work).filter(Work.id == order.work_id).first() + if work: + work.downloads += 1 + + db.commit() + + return { + "order_number": order_number, + "local_status": order.status, + "remote_status": result.get('state'), + "amount": order.amount, + "paid_at": order.paid_at.isoformat() if order.paid_at else None, + "remote_info": result + } + else: + raise HTTPException(status_code=500, detail="查询订单失败") + + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post("/cancel/{order_id}") +async def cancel_order( + order_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """ + 取消订单 + + Args: + order_id: 订单ID + """ + # 查询订单 + order = db.query(Order).filter( + and_( + Order.id == order_id, + Order.user_id == current_user.id + ) + ).first() + + if not order: + raise HTTPException(status_code=404, detail="订单不存在") + + # 只能取消待支付的订单 + if order.status != OrderStatus.PENDING: + raise HTTPException(status_code=400, detail="订单状态不允许取消") + + order.status = OrderStatus.CANCELLED + db.commit() + + return {"message": "订单已取消"} diff --git a/backend/app/api/upload.py b/backend/app/api/upload.py new file mode 100644 index 0000000..5c14c6f --- /dev/null +++ b/backend/app/api/upload.py @@ -0,0 +1,250 @@ +from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File, Form, Header +from sqlalchemy.orm import Session +from typing import Optional +import os +import uuid +import shutil +from datetime import datetime +from PIL import Image +import json + +from app.core.database import get_db +from app.models.work import Work +from app.models.user import User +from app.core.security import decode_access_token + +router = APIRouter(prefix="/upload", tags=["上传"]) + +# 上传配置 +UPLOAD_BASE_DIR = "/app/uploads" +ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".webp"} +MAX_FILE_SIZE = 50 * 1024 * 1024 # 50MB + +# 图绘域名 +TUHUI_DOMAIN = "https://tuhui.cloud" + + +def get_current_user(authorization: str = Header(None), db: Session = Depends(get_db)): + """获取当前登录用户""" + if not authorization or not authorization.startswith("Bearer "): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="未登录,请先登录" + ) + + token = authorization.replace("Bearer ", "") + payload = decode_access_token(token) + if not payload: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Token 无效或已过期" + ) + + user_id = int(payload.get("sub")) + user = db.query(User).filter(User.id == user_id).first() + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="用户不存在" + ) + + return user + + +def generate_thumbnail(image_path: str, thumb_path: str, size=(400, 400)): + """生成缩略图 - 修复透明 PNG 问题""" + with Image.open(image_path) as img: + # 修复 Bug #1: 透明 PNG 转 RGB 后再保存为 JPEG + if img.mode in ('RGBA', 'LA', 'P'): + background = Image.new('RGB', img.size, (255, 255, 255)) + if img.mode == 'P': + img = img.convert('RGBA') + if img.mode in ('RGBA', 'LA'): + background.paste(img, mask=img.split()[-1]) + img = background + elif img.mode != 'RGB': + img = img.convert('RGB') + + img.thumbnail(size, Image.Resampling.LANCZOS) + img.save(thumb_path, quality=85) + + +def add_watermark(image_path: str, watermarked_path: str, watermark_text: str = "图绘"): + """添加水印 - 修复透明 PNG 问题""" + with Image.open(image_path) as img: + # 修复 Bug #1: 透明 PNG 转 RGB 后再保存为 JPEG + if img.mode in ('RGBA', 'LA', 'P'): + background = Image.new('RGB', img.size, (255, 255, 255)) + if img.mode == 'P': + img = img.convert('RGBA') + if img.mode in ('RGBA', 'LA'): + background.paste(img, mask=img.split()[-1]) + img = background + elif img.mode != 'RGB': + img = img.convert('RGB') + + width, height = img.size + from PIL import ImageDraw, ImageFont + draw = ImageDraw.Draw(img) + + try: + font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 36) + except: + font = ImageFont.load_default() + + text = watermark_text + bbox = draw.textbbox((0, 0), text, font=font) + text_width = bbox[2] - bbox[0] + text_height = bbox[3] - bbox[1] + + x = width - text_width - 20 + y = height - text_height - 20 + + draw.text((x, y), text, fill=(255, 255, 255, 128), font=font) + img.save(watermarked_path, quality=90) + + +@router.post("", summary="上传作品") +async def upload_work( + file: UploadFile = File(..., description="作品图片文件"), + title: str = Form(..., description="作品标题"), + description: Optional[str] = Form(None, description="作品描述"), + category: str = Form(..., description="作品分类"), + tags: Optional[str] = Form(None, description="标签,逗号分隔"), + price: float = Form(..., ge=0, description="作品价格"), + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """ + 📤 上传作品 API + + **需要登录**:是(Bearer Token) + + **支持格式**:jpg, jpeg, png, gif, webp + + **文件大小**:最大 50MB + """ + # 验证文件扩展名 + file_ext = os.path.splitext(file.filename)[1].lower() + if file_ext not in ALLOWED_EXTENSIONS: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"不支持的文件格式,仅支持:{', '.join(ALLOWED_EXTENSIONS)}" + ) + + # 验证文件大小 + file.file.seek(0, 2) + file_size = file.file.tell() + file.file.seek(0) + + if file_size > MAX_FILE_SIZE: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"文件过大,最大支持 {MAX_FILE_SIZE // 1024 // 1024}MB" + ) + + # 生成唯一文件名 + unique_id = str(uuid.uuid4()) + timestamp = datetime.now().strftime("%Y%m%d") + + # 创建上传目录 + upload_dir = os.path.join(UPLOAD_BASE_DIR, "original", timestamp) + thumb_dir = os.path.join(UPLOAD_BASE_DIR, "thumbnail", timestamp) + watermarked_dir = os.path.join(UPLOAD_BASE_DIR, "watermarked", timestamp) + + os.makedirs(upload_dir, exist_ok=True) + os.makedirs(thumb_dir, exist_ok=True) + os.makedirs(watermarked_dir, exist_ok=True) + + # 保存文件 + original_filename = f"{unique_id}{file_ext}" + original_path = os.path.join(upload_dir, original_filename) + thumb_filename = f"{unique_id}_thumb.jpg" + thumb_path = os.path.join(thumb_dir, thumb_filename) + watermarked_filename = f"{unique_id}_watermarked.jpg" + watermarked_path = os.path.join(watermarked_dir, watermarked_filename) + + try: + # 保存原图 + with open(original_path, "wb") as buffer: + shutil.copyfileobj(file.file, buffer) + + # 生成缩略图 + generate_thumbnail(original_path, thumb_path) + + # 生成水印图 + add_watermark(original_path, watermarked_path) + + # 解析标签 - 修复 Bug #3: tags 转字符串 + if tags: + tags_list = [tag.strip() for tag in tags.split(",")] + tags_str = ",".join(tags_list) # 转成逗号分隔的字符串 + else: + tags_str = None + + # 创建数据库记录 - 修复所有字段问题 + work = Work( + title=title, + description=description or "", + category=category, + tags=tags_str, # 修复:使用字符串而不是列表 + price=price, + designer=current_user.nickname or current_user.phone, + original_image=f"/uploads/original/{timestamp}/{original_filename}", + thumbnail_image=f"/uploads/thumbnail/{timestamp}/{thumb_filename}", + watermarked_image=f"/uploads/watermarked/{timestamp}/{watermarked_filename}" + ) + + db.add(work) + db.commit() + db.refresh(work) + + # 构建完整的图片 URL + image_url = f"{TUHUI_DOMAIN}{work.original_image}" + thumbnail_url = f"{TUHUI_DOMAIN}{work.thumbnail_image}" + watermarked_url = f"{TUHUI_DOMAIN}{work.watermarked_image}" + + return { + "success": True, + "message": "上传成功,等待审核", + "work_id": work.id, + "image_url": image_url, + "thumbnail_url": thumbnail_url, + "watermarked_url": watermarked_url + } + + except Exception as e: + # 清理已上传的文件 + for path in [original_path, thumb_path, watermarked_path]: + if os.path.exists(path): + os.remove(path) + + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"上传失败:{str(e)}" + ) + + +@router.get("/my", summary="我的上传") +def get_my_uploads( + page: int = Form(1, ge=1), + page_size: int = Form(20, ge=1, le=100), + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """获取当前用户的上传记录""" + from sqlalchemy import desc + + offset = (page - 1) * page_size + works = db.query(Work).filter( + Work.designer == current_user.phone + ).order_by(desc(Work.created_at)).offset(offset).limit(page_size).all() + + total = db.query(Work).filter(Work.designer == current_user.phone).count() + + return { + "total": total, + "page": page, + "page_size": page_size, + "items": works + } diff --git a/backend/app/api/upload.py.backup b/backend/app/api/upload.py.backup new file mode 100644 index 0000000..3531b72 --- /dev/null +++ b/backend/app/api/upload.py.backup @@ -0,0 +1,256 @@ +from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File, Form, Header +from sqlalchemy.orm import Session +from typing import Optional +import os +import uuid +import shutil +from datetime import datetime +from PIL import Image + +from app.core.database import get_db +from app.models.work import Work +from app.models.user import User +from app.core.security import decode_access_token + +router = APIRouter(prefix="/upload", tags=["上传"]) + +# 上传配置 +UPLOAD_BASE_DIR = "/var/www/tuhui_uploads" +ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".webp"} +MAX_FILE_SIZE = 50 * 1024 * 1024 # 50MB + +# 图绘域名 +TUHUI_DOMAIN = "https://tuhui.cloud" + + +def get_current_user(authorization: str = Header(None), db: Session = Depends(get_db)): + """获取当前登录用户""" + if not authorization or not authorization.startswith("Bearer "): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="未登录,请先登录" + ) + + token = authorization.replace("Bearer ", "") + payload = decode_access_token(token) + if not payload: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Token 无效或已过期" + ) + + user_id = int(payload.get("sub")) + user = db.query(User).filter(User.id == user_id).first() + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="用户不存在" + ) + + return user + + +def generate_thumbnail(image_path: str, thumb_path: str, size=(400, 400)): + """生成缩略图""" + with Image.open(image_path) as img: + img.thumbnail(size, Image.Resampling.LANCZOS) + img.save(thumb_path, quality=85) + + +def add_watermark(image_path: str, watermarked_path: str, watermark_text: str = "图绘"): + """添加水印""" + with Image.open(image_path) as img: + width, height = img.size + from PIL import ImageDraw, ImageFont + draw = ImageDraw.Draw(img) + + try: + font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 36) + except: + font = ImageFont.load_default() + + text = watermark_text + bbox = draw.textbbox((0, 0), text, font=font) + text_width = bbox[2] - bbox[0] + text_height = bbox[3] - bbox[1] + + x = width - text_width - 20 + y = height - text_height - 20 + + draw.text((x, y), text, fill=(255, 255, 255, 128), font=font) + img.save(watermarked_path, quality=90) + + +@router.post("", summary="上传作品") +async def upload_work( + file: UploadFile = File(..., description="作品图片文件"), + title: str = Form(..., description="作品标题"), + description: Optional[str] = Form(None, description="作品描述"), + category: str = Form(..., description="作品分类"), + tags: Optional[str] = Form(None, description="标签,逗号分隔"), + price: float = Form(..., ge=0, description="作品价格"), + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """ + 📤 上传作品 API + + **需要登录**:是(Bearer Token) + + **支持格式**:jpg, jpeg, png, gif, webp + + **文件大小**:最大 50MB + + **处理流程**: + 1. 验证文件类型和大小 + 2. 保存原图 + 3. 自动生成缩略图(400x400) + 4. 自动生成水印图 + 5. 创建数据库记录 + 6. 返回 work_id 和 image_url + """ + # 验证文件扩展名 + file_ext = os.path.splitext(file.filename)[1].lower() + if file_ext not in ALLOWED_EXTENSIONS: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"不支持的文件格式,仅支持:{', '.join(ALLOWED_EXTENSIONS)}" + ) + + # 验证文件大小 + file.file.seek(0, 2) + file_size = file.file.tell() + file.file.seek(0) + + if file_size > MAX_FILE_SIZE: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"文件过大,最大支持 {MAX_FILE_SIZE // 1024 // 1024}MB" + ) + + # 生成唯一文件名 + unique_id = str(uuid.uuid4()) + timestamp = datetime.now().strftime("%Y%m%d") + + # 创建上传目录 + upload_dir = os.path.join(UPLOAD_BASE_DIR, "original", timestamp) + thumb_dir = os.path.join(UPLOAD_BASE_DIR, "thumbnail", timestamp) + watermarked_dir = os.path.join(UPLOAD_BASE_DIR, "watermarked", timestamp) + + os.makedirs(upload_dir, exist_ok=True) + os.makedirs(thumb_dir, exist_ok=True) + os.makedirs(watermarked_dir, exist_ok=True) + + # 保存文件 + original_filename = f"{unique_id}{file_ext}" + original_path = os.path.join(upload_dir, original_filename) + thumb_filename = f"{unique_id}_thumb.jpg" + thumb_path = os.path.join(thumb_dir, thumb_filename) + watermarked_filename = f"{unique_id}_watermarked.jpg" + watermarked_path = os.path.join(watermarked_dir, watermarked_filename) + + try: + # 保存原图 + with open(original_path, "wb") as buffer: + shutil.copyfileobj(file.file, buffer) + + # 生成缩略图 + generate_thumbnail(original_path, thumb_path) + + # 生成水印图 + add_watermark(original_path, watermarked_path) + + # 解析标签 + tags_list = [tag.strip() for tag in tags.split(",")] if tags else [] + + # 获取图片尺寸 + with Image.open(original_path) as img: + img_width, img_height = img.size + + # 创建数据库记录 + work = Work( + title=title, + description=description or "", + category=category, + tags=tags_list, + price=price, + designer_id=current_user.id, + original_image=f"/uploads/original/{timestamp}/{original_filename}", + thumbnail_image=f"/uploads/thumbnail/{timestamp}/{thumb_filename}", + watermarked_image=f"/uploads/watermarked/{timestamp}/{watermarked_filename}", + width=img_width, + height=img_height, + file_size=file_size, + status="pending" # pending, approved, rejected + ) + + db.add(work) + db.commit() + db.refresh(work) + + # 构建完整的图片 URL + image_url = f"{TUHUI_DOMAIN}{work.original_image}" + thumbnail_url = f"{TUHUI_DOMAIN}{work.thumbnail_image}" + watermarked_url = f"{TUHUI_DOMAIN}{work.watermarked_image}" + + # 返回结果(包含 work_id 和 image_url) + return { + "success": True, + "message": "上传成功,等待审核", + "work_id": work.id, + "image_url": image_url, + "thumbnail_url": thumbnail_url, + "watermarked_url": watermarked_url, + "work": { + "id": work.id, + "title": work.title, + "description": work.description, + "category": work.category, + "tags": work.tags, + "price": work.price, + "width": work.width, + "height": work.height, + "file_size": work.file_size, + "status": work.status, + "original_image": work.original_image, + "thumbnail_image": work.thumbnail_image, + "watermarked_image": work.watermarked_image, + "created_at": work.created_at.isoformat() if work.created_at else None + } + } + + except Exception as e: + # 清理已上传的文件 + for path in [original_path, thumb_path, watermarked_path]: + if os.path.exists(path): + os.remove(path) + + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"上传失败:{str(e)}" + ) + + +@router.get("/my", summary="我的上传") +def get_my_uploads( + page: int = Form(1, ge=1), + page_size: int = Form(20, ge=1, le=100), + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """获取当前用户的上传记录""" + from sqlalchemy import desc + + offset = (page - 1) * page_size + works = db.query(Work).filter( + Work.designer_id == current_user.id + ).order_by(desc(Work.created_at)).offset(offset).limit(page_size).all() + + total = db.query(Work).filter(Work.designer_id == current_user.id).count() + + return { + "total": total, + "page": page, + "page_size": page_size, + "items": works + } diff --git a/backend/app/api/works.py b/backend/app/api/works.py new file mode 100644 index 0000000..076fc96 --- /dev/null +++ b/backend/app/api/works.py @@ -0,0 +1,157 @@ +from fastapi import APIRouter, Depends, HTTPException, status, Query, Header +from fastapi.responses import FileResponse +from sqlalchemy.orm import Session +from sqlalchemy import desc +from typing import List +import os +from app.core.database import get_db +from app.models.work import Work +from app.models.order import Order, OrderStatus +from app.models.user import User +from app.core.security import decode_access_token +from app.schemas.work import WorkResponse, WorkListResponse + +router = APIRouter(prefix="/works", tags=["作品"]) + +@router.get("", response_model=WorkListResponse, summary="获取作品列表") +def get_works( + page: int = Query(1, ge=1), + page_size: int = Query(20, ge=1, le=100), + category: str = None, + keyword: str = None, + db: Session = Depends(get_db) +): + """ + 获取作品列表 + - page: 页码 + - page_size: 每页数量 + - category: 分类筛选 + - keyword: 关键词搜索 + """ + query = db.query(Work) + + # 分类筛选 + if category: + query = query.filter(Work.category == category) + + # 关键词搜索 + if keyword: + query = query.filter(Work.title.contains(keyword)) + + # 获取总数 + total = query.count() + + # 分页查询 + offset = (page - 1) * page_size + works = query.order_by(desc(Work.created_at)).offset(offset).limit(page_size).all() + + # 修复:将 level_text 为 NULL 的转为空字符串 + for work in works: + if work.level_text is None: + work.level_text = "" + + return WorkListResponse( + total=total, + items=works + ) + +@router.get("/{work_id}", response_model=WorkResponse, summary="获取作品详情") +def get_work(work_id: int, db: Session = Depends(get_db)): + """获取作品详情""" + work = db.query(Work).filter(Work.id == work_id).first() + if not work: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="作品不存在" + ) + + # 增加浏览量 + work.views += 1 + db.commit() + + # 修复:将 level_text 为 NULL 的转为空字符串 + if work.level_text is None: + work.level_text = "" + + return work + + +def get_current_user(authorization: str = Header(None), db: Session = Depends(get_db)): + """获取当前登录用户(用于下载验证)""" + if not authorization or not authorization.startswith("Bearer "): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="未登录,请先登录" + ) + + token = authorization.replace("Bearer ", "") + payload = decode_access_token(token) + if not payload: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Token 无效或已过期" + ) + + user_id = int(payload.get("sub")) + user = db.query(User).filter(User.id == user_id).first() + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="用户不存在" + ) + + return user + + +@router.get("/{work_id}/download", summary="下载作品原图") +def download_work( + work_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """ + 下载作品原图 + - 需要登录 + - 必须已支付订单 + - 支付成功后才能下载 + """ + # 查找作品 + work = db.query(Work).filter(Work.id == work_id).first() + if not work: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="作品不存在" + ) + + # 检查是否已购买(查询已支付的订单) + paid_order = db.query(Order).filter( + Order.user_id == current_user.id, + Order.work_id == work_id, + Order.status == OrderStatus.PAID + ).first() + + if not paid_order: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="您还未购买此作品,请先完成支付" + ) + + # 构建原图文件路径 + # 假设原图存储在 uploads/original/ 目录下 + file_path = work.original_image.lstrip('/') + full_path = os.path.join(os.getcwd(), file_path) + + # 检查文件是否存在 + if not os.path.exists(full_path): + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="文件不存在" + ) + + # 返回文件 + filename = os.path.basename(full_path) + return FileResponse( + path=full_path, + filename=filename, + media_type='application/octet-stream' + ) diff --git a/backend/app/core/__init__.py b/backend/app/core/__init__.py new file mode 100644 index 0000000..9377aea --- /dev/null +++ b/backend/app/core/__init__.py @@ -0,0 +1 @@ +"""核心模块""" diff --git a/backend/app/core/__pycache__/__init__.cpython-310.pyc b/backend/app/core/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..3b4d2b1 Binary files /dev/null and b/backend/app/core/__pycache__/__init__.cpython-310.pyc differ diff --git a/backend/app/core/__pycache__/__init__.cpython-311.pyc b/backend/app/core/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..c673de9 Binary files /dev/null and b/backend/app/core/__pycache__/__init__.cpython-311.pyc differ diff --git a/backend/app/core/__pycache__/__init__.cpython-312.pyc b/backend/app/core/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..afe9cf4 Binary files /dev/null and b/backend/app/core/__pycache__/__init__.cpython-312.pyc differ diff --git a/backend/app/core/__pycache__/config.cpython-310.pyc b/backend/app/core/__pycache__/config.cpython-310.pyc new file mode 100644 index 0000000..2e65c4b Binary files /dev/null and b/backend/app/core/__pycache__/config.cpython-310.pyc differ diff --git a/backend/app/core/__pycache__/config.cpython-311.pyc b/backend/app/core/__pycache__/config.cpython-311.pyc new file mode 100644 index 0000000..61a9179 Binary files /dev/null and b/backend/app/core/__pycache__/config.cpython-311.pyc differ diff --git a/backend/app/core/__pycache__/config.cpython-312.pyc b/backend/app/core/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000..8a38cd3 Binary files /dev/null and b/backend/app/core/__pycache__/config.cpython-312.pyc differ diff --git a/backend/app/core/__pycache__/database.cpython-310.pyc b/backend/app/core/__pycache__/database.cpython-310.pyc new file mode 100644 index 0000000..30a854a Binary files /dev/null and b/backend/app/core/__pycache__/database.cpython-310.pyc differ diff --git a/backend/app/core/__pycache__/database.cpython-311.pyc b/backend/app/core/__pycache__/database.cpython-311.pyc new file mode 100644 index 0000000..ab5a2f6 Binary files /dev/null and b/backend/app/core/__pycache__/database.cpython-311.pyc differ diff --git a/backend/app/core/__pycache__/database.cpython-312.pyc b/backend/app/core/__pycache__/database.cpython-312.pyc new file mode 100644 index 0000000..0d12647 Binary files /dev/null and b/backend/app/core/__pycache__/database.cpython-312.pyc differ diff --git a/backend/app/core/__pycache__/security.cpython-310.pyc b/backend/app/core/__pycache__/security.cpython-310.pyc new file mode 100644 index 0000000..a207097 Binary files /dev/null and b/backend/app/core/__pycache__/security.cpython-310.pyc differ diff --git a/backend/app/core/__pycache__/security.cpython-311.pyc b/backend/app/core/__pycache__/security.cpython-311.pyc new file mode 100644 index 0000000..b462fc0 Binary files /dev/null and b/backend/app/core/__pycache__/security.cpython-311.pyc differ diff --git a/backend/app/core/__pycache__/security.cpython-312.pyc b/backend/app/core/__pycache__/security.cpython-312.pyc new file mode 100644 index 0000000..e0c3103 Binary files /dev/null and b/backend/app/core/__pycache__/security.cpython-312.pyc differ diff --git a/backend/app/core/config.py b/backend/app/core/config.py new file mode 100644 index 0000000..26069be --- /dev/null +++ b/backend/app/core/config.py @@ -0,0 +1,51 @@ +from pydantic_settings import BaseSettings +from typing import List + +class Settings(BaseSettings): + # 项目信息 + PROJECT_NAME: str = "爱设计 API" + VERSION: str = "1.0.0" + API_PREFIX: str = "/api" + + # 数据库配置 + DATABASE_URL: str = "sqlite:///./test.db" # 使用SQLite进行测试 + # DATABASE_URL: str = "mysql+pymysql://root:password@localhost:3306/aishej" # MySQL配置 + + # JWT 配置 + SECRET_KEY: str = "your-secret-key-change-this" + ALGORITHM: str = "HS256" + ACCESS_TOKEN_EXPIRE_MINUTES: int = 1440 # 24小时 + + # 文件配置 + UPLOAD_DIR: str = "./uploads" + MAX_FILE_SIZE: int = 52428800 # 50MB + + # 水印配置 + WATERMARK_TEXT: str = "爱设计 AiSheji.com" + WATERMARK_OPACITY: int = 128 + + # 缩略图配置 + THUMBNAIL_SIZE: int = 400 + THUMBNAIL_QUALITY: int = 85 + + # CORS 配置(生产环境配置为具体域名) + ALLOWED_ORIGINS: List[str] = [ + "https://backend.huijie168.uk", + "https://app.huijie168.uk", + "https://run.huijie168.uk", + "https://huijie168.uk", + "https://www.huijie168.uk" + ] + + # 易收米支付配置 + YSM_APPID: str = "YSMcd16b45d" + YSM_APPSECRET: str = "899850e778e8d2b53e4c4a4e88695688" + YSM_NOTIFY_URL: str = "https://backend.huijie168.uk/api/payment/notify" # 支付回调地址 + YSM_CALLBACK_URL: str = "https://run.huijie168.uk/payment/success" # 支付成功跳转地址 + YSM_NOPAY_URL: str = "https://run.huijie168.uk/payment/cancel" # 未支付跳转地址 + + class Config: + env_file = ".env" + case_sensitive = True + +settings = Settings() diff --git a/backend/app/core/database.py b/backend/app/core/database.py new file mode 100644 index 0000000..43029a1 --- /dev/null +++ b/backend/app/core/database.py @@ -0,0 +1,26 @@ +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker +from .config import settings + +# 创建数据库引擎 +engine = create_engine( + settings.DATABASE_URL, + pool_pre_ping=True, + pool_recycle=3600, + echo=True # 开发环境打印 SQL +) + +# 创建 Session 工厂 +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +# 创建基类 +Base = declarative_base() + +# 依赖注入:获取数据库会话 +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/backend/app/core/security.py b/backend/app/core/security.py new file mode 100644 index 0000000..c67a119 --- /dev/null +++ b/backend/app/core/security.py @@ -0,0 +1,54 @@ +from datetime import datetime, timedelta +from typing import Optional +from jose import JWTError, jwt +from passlib.context import CryptContext +from .config import settings +import hashlib + +# 密码加密上下文 - 禁用bcrypt的wrap bug检测以避免72字节限制错误 +pwd_context = CryptContext( + schemes=["bcrypt"], + deprecated="auto", + bcrypt__ident="2b", # 使用2b版本,跳过wrap bug检测 +) + +def _truncate_password(password: str) -> str: + """ + 截断密码以适应bcrypt的72字节限制 + 对于超长密码,使用SHA256哈希确保唯一性 + """ + password_bytes = password.encode('utf-8') + if len(password_bytes) <= 72: + return password + # 对超长密码进行hash,确保唯一性且在72字节以内 + return hashlib.sha256(password_bytes).hexdigest() + +def verify_password(plain_password: str, hashed_password: str) -> bool: + """验证密码""" + safe_password = _truncate_password(plain_password) + return pwd_context.verify(safe_password, hashed_password) + +def get_password_hash(password: str) -> str: + """加密密码(bcrypt限制最多72字节)""" + safe_password = _truncate_password(password) + return pwd_context.hash(safe_password) + +def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str: + """创建 JWT Token""" + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) + + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM) + return encoded_jwt + +def decode_access_token(token: str) -> Optional[dict]: + """解析 JWT Token""" + try: + payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]) + return payload + except JWTError: + return None diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..ded5002 --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,65 @@ +from fastapi import FastAPI, Request, Response +from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles +from app.core.config import settings +from app.core.database import Base, engine +from app.api import auth, works, orders, payment, upload + +# 创建数据库表 +Base.metadata.create_all(bind=engine) + +# 创建 FastAPI 应用 +app = FastAPI( + title=settings.PROJECT_NAME, + version=settings.VERSION, + description="图绘 - 设计作品交易平台 API" +) + +# 配置 CORS +app.add_middleware( + CORSMiddleware, + allow_origins=settings.ALLOWED_ORIGINS, + allow_credentials=True, + allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], + allow_headers=["*"], + expose_headers=["*"], + max_age=600, +) + +# 添加全局 CORS 中间件 +@app.middleware("http") +async def add_cors_headers(request: Request, call_next): + response = await call_next(request) + origin = request.headers.get("origin", "") + if origin in settings.ALLOWED_ORIGINS: + response.headers["Access-Control-Allow-Origin"] = origin + response.headers["Access-Control-Allow-Credentials"] = "true" + response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS" + response.headers["Access-Control-Allow-Headers"] = "*" + + if request.method == "OPTIONS": + response.headers["Access-Control-Max-Age"] = "600" + + return response + +# 挂载静态文件目录 +app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads") + +# 注册路由 +app.include_router(auth.router, prefix=settings.API_PREFIX) +app.include_router(works.router, prefix=settings.API_PREFIX) +app.include_router(orders.router, prefix=settings.API_PREFIX) +app.include_router(payment.router, prefix=settings.API_PREFIX) +app.include_router(upload.router, prefix=settings.API_PREFIX) + +@app.get("/") +def root(): + return { + "message": "欢迎使用图绘 API", + "version": settings.VERSION, + "docs": "/docs" + } + +@app.get("/health") +def health_check(): + return {"status": "healthy"} diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py new file mode 100644 index 0000000..4cb1c7f --- /dev/null +++ b/backend/app/models/__init__.py @@ -0,0 +1 @@ +"""数据库模型""" diff --git a/backend/app/models/__pycache__/__init__.cpython-310.pyc b/backend/app/models/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..da855f7 Binary files /dev/null and b/backend/app/models/__pycache__/__init__.cpython-310.pyc differ diff --git a/backend/app/models/__pycache__/__init__.cpython-311.pyc b/backend/app/models/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..03b0260 Binary files /dev/null and b/backend/app/models/__pycache__/__init__.cpython-311.pyc differ diff --git a/backend/app/models/__pycache__/__init__.cpython-312.pyc b/backend/app/models/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..d608062 Binary files /dev/null and b/backend/app/models/__pycache__/__init__.cpython-312.pyc differ diff --git a/backend/app/models/__pycache__/download.cpython-310.pyc b/backend/app/models/__pycache__/download.cpython-310.pyc new file mode 100644 index 0000000..dba2ed1 Binary files /dev/null and b/backend/app/models/__pycache__/download.cpython-310.pyc differ diff --git a/backend/app/models/__pycache__/download.cpython-311.pyc b/backend/app/models/__pycache__/download.cpython-311.pyc new file mode 100644 index 0000000..5ac7b51 Binary files /dev/null and b/backend/app/models/__pycache__/download.cpython-311.pyc differ diff --git a/backend/app/models/__pycache__/download.cpython-312.pyc b/backend/app/models/__pycache__/download.cpython-312.pyc new file mode 100644 index 0000000..0b22ecb Binary files /dev/null and b/backend/app/models/__pycache__/download.cpython-312.pyc differ diff --git a/backend/app/models/__pycache__/order.cpython-310.pyc b/backend/app/models/__pycache__/order.cpython-310.pyc new file mode 100644 index 0000000..a6bcdbb Binary files /dev/null and b/backend/app/models/__pycache__/order.cpython-310.pyc differ diff --git a/backend/app/models/__pycache__/order.cpython-311.pyc b/backend/app/models/__pycache__/order.cpython-311.pyc new file mode 100644 index 0000000..ae6ec3e Binary files /dev/null and b/backend/app/models/__pycache__/order.cpython-311.pyc differ diff --git a/backend/app/models/__pycache__/order.cpython-312.pyc b/backend/app/models/__pycache__/order.cpython-312.pyc new file mode 100644 index 0000000..f1de4c4 Binary files /dev/null and b/backend/app/models/__pycache__/order.cpython-312.pyc differ diff --git a/backend/app/models/__pycache__/user.cpython-310.pyc b/backend/app/models/__pycache__/user.cpython-310.pyc new file mode 100644 index 0000000..d8f049a Binary files /dev/null and b/backend/app/models/__pycache__/user.cpython-310.pyc differ diff --git a/backend/app/models/__pycache__/user.cpython-311.pyc b/backend/app/models/__pycache__/user.cpython-311.pyc new file mode 100644 index 0000000..217ba3d Binary files /dev/null and b/backend/app/models/__pycache__/user.cpython-311.pyc differ diff --git a/backend/app/models/__pycache__/user.cpython-312.pyc b/backend/app/models/__pycache__/user.cpython-312.pyc new file mode 100644 index 0000000..a3f10d6 Binary files /dev/null and b/backend/app/models/__pycache__/user.cpython-312.pyc differ diff --git a/backend/app/models/__pycache__/work.cpython-310.pyc b/backend/app/models/__pycache__/work.cpython-310.pyc new file mode 100644 index 0000000..17ce035 Binary files /dev/null and b/backend/app/models/__pycache__/work.cpython-310.pyc differ diff --git a/backend/app/models/__pycache__/work.cpython-311.pyc b/backend/app/models/__pycache__/work.cpython-311.pyc new file mode 100644 index 0000000..12554cc Binary files /dev/null and b/backend/app/models/__pycache__/work.cpython-311.pyc differ diff --git a/backend/app/models/__pycache__/work.cpython-312.pyc b/backend/app/models/__pycache__/work.cpython-312.pyc new file mode 100644 index 0000000..542c67c Binary files /dev/null and b/backend/app/models/__pycache__/work.cpython-312.pyc differ diff --git a/backend/app/models/download.py b/backend/app/models/download.py new file mode 100644 index 0000000..52c57a8 --- /dev/null +++ b/backend/app/models/download.py @@ -0,0 +1,13 @@ +from sqlalchemy import Column, Integer, DateTime, ForeignKey +from sqlalchemy.sql import func +from app.core.database import Base + +class DownloadRecord(Base): + __tablename__ = "download_records" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True) + work_id = Column(Integer, ForeignKey("works.id"), nullable=False, index=True) + order_id = Column(Integer, ForeignKey("orders.id"), nullable=False, index=True) + + downloaded_at = Column(DateTime(timezone=True), server_default=func.now()) diff --git a/backend/app/models/order.py b/backend/app/models/order.py new file mode 100644 index 0000000..3dc2db2 --- /dev/null +++ b/backend/app/models/order.py @@ -0,0 +1,28 @@ +from sqlalchemy import Column, Integer, String, Float, DateTime, ForeignKey, Enum +from sqlalchemy.sql import func +from app.core.database import Base +import enum + +class OrderStatus(str, enum.Enum): + PENDING = "pending" # 待支付 + PAID = "paid" # 已支付 + CANCELLED = "cancelled" # 已取消 + REFUNDED = "refunded" # 已退款 + +class Order(Base): + __tablename__ = "orders" + + id = Column(Integer, primary_key=True, index=True) + order_no = Column(String(100), unique=True, index=True, nullable=False) # 订单号 + + user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True) + work_id = Column(Integer, ForeignKey("works.id"), nullable=False, index=True) + + amount = Column(Float, nullable=False) # 金额 + payment_method = Column(String(50)) # 支付方式:alipay, wechat, balance + + status = Column(Enum(OrderStatus), default=OrderStatus.PENDING, index=True) + + paid_at = Column(DateTime(timezone=True), nullable=True) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now()) diff --git a/backend/app/models/user.py b/backend/app/models/user.py new file mode 100644 index 0000000..dace2e2 --- /dev/null +++ b/backend/app/models/user.py @@ -0,0 +1,15 @@ +from sqlalchemy import Column, Integer, String, DateTime, Float +from sqlalchemy.sql import func +from app.core.database import Base + +class User(Base): + __tablename__ = "users" + + id = Column(Integer, primary_key=True, index=True) + phone = Column(String(20), unique=True, index=True, nullable=False) # 手机号作为唯一标识 + password_hash = Column(String(255), nullable=False) + nickname = Column(String(100), nullable=True) + avatar = Column(String(500), nullable=True) + balance = Column(Float, default=0.0) # 账户余额 + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now()) diff --git a/backend/app/models/work.py b/backend/app/models/work.py new file mode 100644 index 0000000..1b91a37 --- /dev/null +++ b/backend/app/models/work.py @@ -0,0 +1,33 @@ +from sqlalchemy import Column, Integer, String, Float, DateTime, Text +from sqlalchemy.sql import func +from app.core.database import Base + +class Work(Base): + __tablename__ = "works" + + id = Column(Integer, primary_key=True, index=True) + title = Column(String(255), nullable=False, index=True) + category = Column(String(100), index=True) + designer = Column(String(100)) + level = Column(Integer, default=1) + level_text = Column(String(50)) + + # 文件路径 + original_image = Column(String(500)) # 原图(高清) + watermarked_image = Column(String(500)) # 带水印图(详情页显示) + thumbnail_image = Column(String(500)) # 缩略图(列表显示) + + # 价格 + price = Column(Float, default=0.0) # 下载价格(元) + + # 统计 + views = Column(Integer, default=0) # 浏览量 + downloads = Column(Integer, default=0) # 下载量 + collects = Column(Integer, default=0) # 收藏量 + + # 描述 + description = Column(Text, nullable=True) + tags = Column(String(500), nullable=True) # JSON 格式存储标签 + + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now()) diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py new file mode 100644 index 0000000..bf4b251 --- /dev/null +++ b/backend/app/schemas/__init__.py @@ -0,0 +1 @@ +"""Pydantic Schemas""" diff --git a/backend/app/schemas/__pycache__/__init__.cpython-310.pyc b/backend/app/schemas/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..1ad38a4 Binary files /dev/null and b/backend/app/schemas/__pycache__/__init__.cpython-310.pyc differ diff --git a/backend/app/schemas/__pycache__/__init__.cpython-311.pyc b/backend/app/schemas/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..ce7e88d Binary files /dev/null and b/backend/app/schemas/__pycache__/__init__.cpython-311.pyc differ diff --git a/backend/app/schemas/__pycache__/__init__.cpython-312.pyc b/backend/app/schemas/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..2edd3c5 Binary files /dev/null and b/backend/app/schemas/__pycache__/__init__.cpython-312.pyc differ diff --git a/backend/app/schemas/__pycache__/order.cpython-310.pyc b/backend/app/schemas/__pycache__/order.cpython-310.pyc new file mode 100644 index 0000000..3bef1af Binary files /dev/null and b/backend/app/schemas/__pycache__/order.cpython-310.pyc differ diff --git a/backend/app/schemas/__pycache__/order.cpython-311.pyc b/backend/app/schemas/__pycache__/order.cpython-311.pyc new file mode 100644 index 0000000..659c915 Binary files /dev/null and b/backend/app/schemas/__pycache__/order.cpython-311.pyc differ diff --git a/backend/app/schemas/__pycache__/order.cpython-312.pyc b/backend/app/schemas/__pycache__/order.cpython-312.pyc new file mode 100644 index 0000000..afd1f27 Binary files /dev/null and b/backend/app/schemas/__pycache__/order.cpython-312.pyc differ diff --git a/backend/app/schemas/__pycache__/user.cpython-310.pyc b/backend/app/schemas/__pycache__/user.cpython-310.pyc new file mode 100644 index 0000000..4dd4eff Binary files /dev/null and b/backend/app/schemas/__pycache__/user.cpython-310.pyc differ diff --git a/backend/app/schemas/__pycache__/user.cpython-311.pyc b/backend/app/schemas/__pycache__/user.cpython-311.pyc new file mode 100644 index 0000000..18b219b Binary files /dev/null and b/backend/app/schemas/__pycache__/user.cpython-311.pyc differ diff --git a/backend/app/schemas/__pycache__/user.cpython-312.pyc b/backend/app/schemas/__pycache__/user.cpython-312.pyc new file mode 100644 index 0000000..31d2570 Binary files /dev/null and b/backend/app/schemas/__pycache__/user.cpython-312.pyc differ diff --git a/backend/app/schemas/__pycache__/work.cpython-310.pyc b/backend/app/schemas/__pycache__/work.cpython-310.pyc new file mode 100644 index 0000000..9951f66 Binary files /dev/null and b/backend/app/schemas/__pycache__/work.cpython-310.pyc differ diff --git a/backend/app/schemas/__pycache__/work.cpython-311.pyc b/backend/app/schemas/__pycache__/work.cpython-311.pyc new file mode 100644 index 0000000..6c97211 Binary files /dev/null and b/backend/app/schemas/__pycache__/work.cpython-311.pyc differ diff --git a/backend/app/schemas/__pycache__/work.cpython-312.pyc b/backend/app/schemas/__pycache__/work.cpython-312.pyc new file mode 100644 index 0000000..6aae703 Binary files /dev/null and b/backend/app/schemas/__pycache__/work.cpython-312.pyc differ diff --git a/backend/app/schemas/order.py b/backend/app/schemas/order.py new file mode 100644 index 0000000..c0686cc --- /dev/null +++ b/backend/app/schemas/order.py @@ -0,0 +1,30 @@ +from pydantic import BaseModel +from typing import Optional +from datetime import datetime + +# 创建订单 +class OrderCreate(BaseModel): + work_id: int + payment_method: str = "balance" # balance, alipay, wechat + +# 订单响应 +class OrderResponse(BaseModel): + id: int + order_no: str + user_id: int + work_id: int + amount: float + payment_method: str + status: str + paid_at: Optional[datetime] + created_at: datetime + + class Config: + from_attributes = True + +# 支付响应 +class PaymentResponse(BaseModel): + success: bool + message: str + order_no: str + download_url: Optional[str] = None diff --git a/backend/app/schemas/user.py b/backend/app/schemas/user.py new file mode 100644 index 0000000..08d3acc --- /dev/null +++ b/backend/app/schemas/user.py @@ -0,0 +1,62 @@ +from pydantic import BaseModel, validator +from typing import Optional +from datetime import datetime +import re + +# 用户注册 +class UserRegister(BaseModel): + phone: str # 手机号必填,作为唯一标识 + password: str + nickname: Optional[str] = None + + @validator('phone') + def validate_phone(cls, v): + """验证手机号格式(11位数字)""" + if not v: + raise ValueError('手机号不能为空') + if not re.match(r'^1[3-9]\d{9}$', v): + raise ValueError('手机号格式错误,请输入11位有效手机号') + return v + + @validator('password') + def validate_password(cls, v): + """验证密码""" + if not v: + raise ValueError('密码不能为空') + if len(v) < 6: + raise ValueError('密码长度不能少于6位') + if len(v) > 72: + raise ValueError('密码长度不能超过72位') + return v + +# 用户登录 +class UserLogin(BaseModel): + phone: str # 手机号登录 + password: str + + @validator('phone') + def validate_phone(cls, v): + """验证手机号格式(11位数字)""" + if not v: + raise ValueError('手机号不能为空') + if not re.match(r'^1[3-9]\d{9}$', v): + raise ValueError('手机号格式错误,请输入11位有效手机号') + return v + +# 用户响应 +class UserResponse(BaseModel): + id: int + phone: str + nickname: Optional[str] + avatar: Optional[str] + balance: float + created_at: datetime + + class Config: + from_attributes = True + +# Token 响应 +class Token(BaseModel): + access_token: str + token_type: str = "bearer" + user: UserResponse diff --git a/backend/app/schemas/work.py b/backend/app/schemas/work.py new file mode 100644 index 0000000..c8b2f1d --- /dev/null +++ b/backend/app/schemas/work.py @@ -0,0 +1,50 @@ +from pydantic import BaseModel +from typing import Optional, List +from datetime import datetime + +# 作品创建 +class WorkCreate(BaseModel): + title: str + category: str + designer: str + level: int = 1 + level_text: str = "设计爱好者" + price: float # 下载价格 + description: Optional[str] = None + tags: Optional[str] = None + +# 作品更新 +class WorkUpdate(BaseModel): + title: Optional[str] = None + price: Optional[float] = None + description: Optional[str] = None + +# 作品响应 +class WorkResponse(BaseModel): + id: int + title: str + category: str + designer: str + level: int + level_text: str + + thumbnail_image: Optional[str] + watermarked_image: Optional[str] + + price: float + views: int + downloads: int + collects: int + + description: Optional[str] + tags: Optional[str] + + created_at: datetime + + class Config: + from_attributes = True + +# 作品列表响应 +class WorkListResponse(BaseModel): + total: int + items: List[WorkResponse] diff --git a/backend/app/utils/__init__.py b/backend/app/utils/__init__.py new file mode 100644 index 0000000..f69783d --- /dev/null +++ b/backend/app/utils/__init__.py @@ -0,0 +1 @@ +"""工具函数""" diff --git a/backend/app/utils/image.py b/backend/app/utils/image.py new file mode 100644 index 0000000..da0922a --- /dev/null +++ b/backend/app/utils/image.py @@ -0,0 +1,127 @@ +from PIL import Image, ImageDraw, ImageFont +import os +from pathlib import Path +from app.core.config import settings + +def add_watermark(input_path: str, output_path: str, text: str = None) -> str: + """ + 给图片添加水印 + + Args: + input_path: 输入图片路径 + output_path: 输出图片路径 + text: 水印文字,默认使用配置 + + Returns: + 输出图片路径 + """ + if text is None: + text = settings.WATERMARK_TEXT + + # 打开原图 + image = Image.open(input_path).convert("RGBA") + width, height = image.size + + # 创建水印层 + watermark = Image.new("RGBA", image.size, (0, 0, 0, 0)) + draw = ImageDraw.Draw(watermark) + + # 计算字体大小(根据图片大小自适应) + font_size = int(min(width, height) / 20) + + try: + # 尝试使用系统字体 + font = ImageFont.truetype("arial.ttf", font_size) + except: + # 如果没有找到字体,使用默认字体 + font = ImageFont.load_default() + + # 获取文字大小 + bbox = draw.textbbox((0, 0), text, font=font) + text_width = bbox[2] - bbox[0] + text_height = bbox[3] - bbox[1] + + # 计算水印位置(右下角) + x = width - text_width - 20 + y = height - text_height - 20 + + # 绘制水印(半透明白色文字 + 黑色描边) + # 先绘制黑色描边 + for adj_x in range(-2, 3): + for adj_y in range(-2, 3): + draw.text((x + adj_x, y + adj_y), text, font=font, fill=(0, 0, 0, settings.WATERMARK_OPACITY)) + + # 再绘制白色文字 + draw.text((x, y), text, font=font, fill=(255, 255, 255, settings.WATERMARK_OPACITY)) + + # 合并图层 + watermarked = Image.alpha_composite(image, watermark) + + # 转换为 RGB 并保存 + watermarked_rgb = watermarked.convert("RGB") + + # 确保输出目录存在 + os.makedirs(os.path.dirname(output_path), exist_ok=True) + + watermarked_rgb.save(output_path, "JPEG", quality=95) + + return output_path + +def create_thumbnail(input_path: str, output_path: str, size: int = None) -> str: + """ + 创建缩略图 + + Args: + input_path: 输入图片路径 + output_path: 输出图片路径 + size: 缩略图最大尺寸,默认使用配置 + + Returns: + 输出图片路径 + """ + if size is None: + size = settings.THUMBNAIL_SIZE + + # 打开原图 + image = Image.open(input_path) + + # 计算缩略图尺寸(保持宽高比) + image.thumbnail((size, size), Image.Resampling.LANCZOS) + + # 确保输出目录存在 + os.makedirs(os.path.dirname(output_path), exist_ok=True) + + # 保存缩略图 + image.save(output_path, "JPEG", quality=settings.THUMBNAIL_QUALITY) + + return output_path + +def process_uploaded_image(original_path: str, work_id: int) -> dict: + """ + 处理上传的图片:生成水印图和缩略图 + + Args: + original_path: 原图路径 + work_id: 作品ID + + Returns: + 包含三个图片路径的字典 + """ + # 生成文件名 + filename = f"{work_id}.jpg" + + # 定义输出路径 + watermarked_path = os.path.join(settings.UPLOAD_DIR, "watermarked", filename) + thumbnail_path = os.path.join(settings.UPLOAD_DIR, "thumbnail", filename) + + # 生成带水印的图片 + add_watermark(original_path, watermarked_path) + + # 生成缩略图 + create_thumbnail(original_path, thumbnail_path) + + return { + "original": original_path, + "watermarked": watermarked_path, + "thumbnail": thumbnail_path + } diff --git a/backend/app/ysm_sdk/__init__.py b/backend/app/ysm_sdk/__init__.py new file mode 100644 index 0000000..5949a01 --- /dev/null +++ b/backend/app/ysm_sdk/__init__.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"""易收米支付SDK""" + +from .pay import create_payment +from .query import query_order +from .notify import PaymentNotify + +__all__ = ['create_payment', 'query_order', 'PaymentNotify'] diff --git a/backend/app/ysm_sdk/__pycache__/__init__.cpython-310.pyc b/backend/app/ysm_sdk/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..be23b28 Binary files /dev/null and b/backend/app/ysm_sdk/__pycache__/__init__.cpython-310.pyc differ diff --git a/backend/app/ysm_sdk/__pycache__/__init__.cpython-311.pyc b/backend/app/ysm_sdk/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..2979971 Binary files /dev/null and b/backend/app/ysm_sdk/__pycache__/__init__.cpython-311.pyc differ diff --git a/backend/app/ysm_sdk/__pycache__/__init__.cpython-312.pyc b/backend/app/ysm_sdk/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..09c6927 Binary files /dev/null and b/backend/app/ysm_sdk/__pycache__/__init__.cpython-312.pyc differ diff --git a/backend/app/ysm_sdk/__pycache__/api.cpython-310.pyc b/backend/app/ysm_sdk/__pycache__/api.cpython-310.pyc new file mode 100644 index 0000000..099b8df Binary files /dev/null and b/backend/app/ysm_sdk/__pycache__/api.cpython-310.pyc differ diff --git a/backend/app/ysm_sdk/__pycache__/api.cpython-311.pyc b/backend/app/ysm_sdk/__pycache__/api.cpython-311.pyc new file mode 100644 index 0000000..6b88eb0 Binary files /dev/null and b/backend/app/ysm_sdk/__pycache__/api.cpython-311.pyc differ diff --git a/backend/app/ysm_sdk/__pycache__/api.cpython-312.pyc b/backend/app/ysm_sdk/__pycache__/api.cpython-312.pyc new file mode 100644 index 0000000..ee20e90 Binary files /dev/null and b/backend/app/ysm_sdk/__pycache__/api.cpython-312.pyc differ diff --git a/backend/app/ysm_sdk/__pycache__/notify.cpython-310.pyc b/backend/app/ysm_sdk/__pycache__/notify.cpython-310.pyc new file mode 100644 index 0000000..d9fb222 Binary files /dev/null and b/backend/app/ysm_sdk/__pycache__/notify.cpython-310.pyc differ diff --git a/backend/app/ysm_sdk/__pycache__/notify.cpython-311.pyc b/backend/app/ysm_sdk/__pycache__/notify.cpython-311.pyc new file mode 100644 index 0000000..8d8f9f4 Binary files /dev/null and b/backend/app/ysm_sdk/__pycache__/notify.cpython-311.pyc differ diff --git a/backend/app/ysm_sdk/__pycache__/notify.cpython-312.pyc b/backend/app/ysm_sdk/__pycache__/notify.cpython-312.pyc new file mode 100644 index 0000000..410befc Binary files /dev/null and b/backend/app/ysm_sdk/__pycache__/notify.cpython-312.pyc differ diff --git a/backend/app/ysm_sdk/__pycache__/pay.cpython-310.pyc b/backend/app/ysm_sdk/__pycache__/pay.cpython-310.pyc new file mode 100644 index 0000000..6343400 Binary files /dev/null and b/backend/app/ysm_sdk/__pycache__/pay.cpython-310.pyc differ diff --git a/backend/app/ysm_sdk/__pycache__/pay.cpython-311.pyc b/backend/app/ysm_sdk/__pycache__/pay.cpython-311.pyc new file mode 100644 index 0000000..84b9566 Binary files /dev/null and b/backend/app/ysm_sdk/__pycache__/pay.cpython-311.pyc differ diff --git a/backend/app/ysm_sdk/__pycache__/pay.cpython-312.pyc b/backend/app/ysm_sdk/__pycache__/pay.cpython-312.pyc new file mode 100644 index 0000000..e5cb86d Binary files /dev/null and b/backend/app/ysm_sdk/__pycache__/pay.cpython-312.pyc differ diff --git a/backend/app/ysm_sdk/__pycache__/query.cpython-310.pyc b/backend/app/ysm_sdk/__pycache__/query.cpython-310.pyc new file mode 100644 index 0000000..db0985c Binary files /dev/null and b/backend/app/ysm_sdk/__pycache__/query.cpython-310.pyc differ diff --git a/backend/app/ysm_sdk/__pycache__/query.cpython-311.pyc b/backend/app/ysm_sdk/__pycache__/query.cpython-311.pyc new file mode 100644 index 0000000..16fef20 Binary files /dev/null and b/backend/app/ysm_sdk/__pycache__/query.cpython-311.pyc differ diff --git a/backend/app/ysm_sdk/__pycache__/query.cpython-312.pyc b/backend/app/ysm_sdk/__pycache__/query.cpython-312.pyc new file mode 100644 index 0000000..da5db38 Binary files /dev/null and b/backend/app/ysm_sdk/__pycache__/query.cpython-312.pyc differ diff --git a/backend/app/ysm_sdk/api.py b/backend/app/ysm_sdk/api.py new file mode 100644 index 0000000..e42e1ae --- /dev/null +++ b/backend/app/ysm_sdk/api.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import json +import hashlib +import aiohttp + +class YsmPayApi: + """易收米支付API工具类""" + + @staticmethod + async def http_post(url, data): + """ + 发送异步HTTP POST请求 + + Args: + url: 请求地址 + data: 请求数据(JSON字符串) + + Returns: + str: 响应内容 + """ + headers = { + 'Content-Type': 'application/json; charset=UTF-8', + 'Accept': 'application/json', + 'User-Agent': '*/*', + 'Authorization': 'WECHATPAY2-SHA256-RSA2048 ' + } + + try: + async with aiohttp.ClientSession() as session: + async with session.post(url, data=data, headers=headers, ssl=False, timeout=30) as response: + return await response.text() + except Exception as e: + print(f"Error: {str(e)}") + return None + + @staticmethod + def hash_sign(data, secret): + """ + 生成签名 + + Args: + data: 要签名的数据字典 + secret: 密钥 + + Returns: + str: 签名字符串 + """ + # 按键名升序排序 + sorted_data = dict(sorted(data.items())) + + # 构造签名字符串 + sign_str = "" + for key, value in sorted_data.items(): + # 跳过hash字段和空值 + if key == 'hash' or value is None or value == '': + continue + + # 添加分隔符 + if sign_str: + sign_str += '&' + + # 拼接键值对 + sign_str += f"{key}={value}" + + # 添加密钥并计算SHA256哈希 + sign_str += secret + return hashlib.sha256(sign_str.encode('utf-8')).hexdigest() \ No newline at end of file diff --git a/backend/app/ysm_sdk/notify.py b/backend/app/ysm_sdk/notify.py new file mode 100644 index 0000000..e17dc60 --- /dev/null +++ b/backend/app/ysm_sdk/notify.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +支付成功异步回调处理模块 +当用户支付成功后,支付平台会把订单支付信息异步请求到回调接口 +""" + +import json +import asyncio +from .api import YsmPayApi + +class PaymentNotify: + """支付通知处理类""" + + def __init__(self, appid, appsecret): + """ + 初始化 + + Args: + appid: 支付通道ID + appsecret: 密钥 + """ + self.appid = appid + self.appsecret = appsecret + + async def process(self, request_data): + """ + 处理支付通知(异步) + + Args: + request_data: 请求数据(字符串或字典) + + Returns: + tuple: (是否成功, 消息) + """ + # 解析请求数据 + if isinstance(request_data, str): + try: + data = json.loads(request_data) + except: + return False, "数据格式错误" + else: + data = request_data + + # 验证签名 + calc_hash = YsmPayApi.hash_sign(data, self.appsecret) + if data.get('hash') != calc_hash: + return False, "验签失败,签名错误" + + # 获取商户订单号 + mch_orderid = data.get('mch_orderid') + + # 处理支付结果 + if data.get('state') == 'SUCCESS': + # 在这里处理订单业务逻辑 + # 注意:平台可能会多次调用本接口,请确保订单不会被重复处理 + + # 示例:订单处理逻辑,这里可以使用异步数据库操作 + """ + order = await find_order_by_id(mch_orderid) + if order and order.status != 'paid': + await update_order_status(mch_orderid, 'paid') + # 其他业务逻辑... + """ + + return True, "success" + else: + # 处理未支付的情况 + return False, "订单未支付" + +# FastAPI Web应用示例 +""" +from fastapi import FastAPI, Request, Response +from fastapi.responses import JSONResponse + +app = FastAPI() + +@app.post("/notify") +async def payment_notify(request: Request): + # 配置信息 + appid = '20********' # 支付通道ID + appsecret = 'e605ac7******************4af5164' # AppSecret + + # 创建通知处理器 + notify_handler = PaymentNotify(appid, appsecret) + + # 获取请求数据 + request_data = await request.body() + + # 处理通知 + success, message = await notify_handler.process(request_data) + + if success: + return Response(content=message) + else: + return JSONResponse(content={"error": message}, status_code=400) + +# 启动命令: uvicorn notify:app --reload +""" \ No newline at end of file diff --git a/backend/app/ysm_sdk/pay.py b/backend/app/ysm_sdk/pay.py new file mode 100644 index 0000000..5f730e5 --- /dev/null +++ b/backend/app/ysm_sdk/pay.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +发起支付模块 +""" + +import json +import time +import random +import string +import asyncio +from .api import YsmPayApi + +def generate_nonce_str(length=10): + """生成随机字符串""" + chars = string.ascii_letters + string.digits + return ''.join(random.choice(chars) for _ in range(length)) + +async def create_payment(appid, appsecret, order_id, description, amount, + notify_url, nopay_url, callback_url, pay_type=1): + """ + 创建支付请求(异步) + + Args: + appid: 支付通道ID + appsecret: 密钥 + order_id: 商户订单号 + description: 订单描述 + amount: 支付金额(单位:分) + notify_url: 异步回调地址 + nopay_url: 未支付跳转地址 + callback_url: 支付成功跳转地址 + pay_type: 支付类型,默认为1(微信内) + + Returns: + str: 支付链接 + """ + # 构造支付请求数据 + data = { + 'appid': appid, # 支付通道ID + 'mch_orderid': order_id, # 商户网站订单号 + 'description': description, # 订单标题 + 'total': amount, # 订单金额(分) + 'notify_url': notify_url, # 异步通知地址 + 'nopay_url': nopay_url, # 未支付跳转地址 + 'callback_url': callback_url, # 支付成功跳转地址 + 'time': int(time.time()), # 时间戳 + 'nonce_str': f"abc{int(time.time())}", # 随机字符串 + 'payType': pay_type, # 支付类型 + } + + # 生成签名 + data['sign'] = YsmPayApi.hash_sign(data, appsecret) + + # 发起支付请求 + url = 'https://www.yishoumi.cn/u/payment' # 支付网关 + response = await YsmPayApi.http_post(url, json.dumps(data)) + + # 解析响应 + result = json.loads(response) if response else None + + # 返回支付链接 + if result and 'url' in result: + return result['url'] + else: + # 返回错误信息以便调用者处理 + if result and 'msg' in result: + raise Exception(f"支付创建失败: {result['msg']} (错误代码: {result.get('code', 'unknown')})") + else: + raise Exception("支付创建失败: 无响应或响应格式错误") + +async def main(): + # 示例使用 + appid = 'YSMcd16b45d' # 支付通道ID + appsecret = '899850e778e8d2b53e4c4a4e88695688' # AppSecret + + # 生成订单号 + order_id = f"123321{int(time.time())}" + + # 创建支付 + pay_url = await create_payment( + appid=appid, + appsecret=appsecret, + order_id=order_id, + description="快充数据线", + amount=100, # 1元 + notify_url="http://abc.com/notify.php", + nopay_url="http://abc.com/notify.php", + callback_url="http://abc.com/notify.php", + pay_type=1 # 微信内支付 + ) + + if pay_url: + print(f"支付链接: {pay_url}") + else: + print("创建支付失败") + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/backend/app/ysm_sdk/query.py b/backend/app/ysm_sdk/query.py new file mode 100644 index 0000000..62aa5ca --- /dev/null +++ b/backend/app/ysm_sdk/query.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +查询订单模块 +""" + +import json +import asyncio +from .api import YsmPayApi + +async def query_order(appid, mch_orderid=None, ysm_orderid=None): + """ + 查询订单状态(异步) + + Args: + appid: 支付通道ID + mch_orderid: 商户订单号(与ysm_orderid二选一) + ysm_orderid: 易收米订单号(与mch_orderid二选一) + + Returns: + dict: 订单信息字典,包含状态等信息 + SUCCESS:支付成功 + REFUND:转入退款 + NOTPAY:未支付 + CLOSED:已关闭 + """ + # 检查参数 + if not mch_orderid and not ysm_orderid: + raise ValueError("商户订单号和易收米订单号必须提供一个") + + # 构造请求数据 + data = {'appid': appid} + + if mch_orderid: + data['mch_orderid'] = mch_orderid + elif ysm_orderid: + data['ysm_orderid'] = ysm_orderid + + # 请求URL + url = 'https://www.yishoumi.cn/u/query' # 订单查询网关 + + try: + # 发起请求 + response = await YsmPayApi.http_post(url, json.dumps(data)) + + # 解析响应 + result = json.loads(response) if response else None + + if not result: + raise Exception('服务器错误:' + str(response), 500) + + return result + except Exception as e: + print(f"错误码:{getattr(e, 'code', 0)}, 错误信息:{str(e)}") + return None + +async def main(): + # 示例使用 + appid = '20********' # 支付通道ID + + # 查询订单 + order_info = await query_order( + appid=appid, + mch_orderid='20230510505610', # 商户订单号 + ) + + if order_info: + print(f"订单信息: {json.dumps(order_info, ensure_ascii=False, indent=2)}") + else: + print("查询订单失败") + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/backend/app/ysm_sdk/tmp/README.md b/backend/app/ysm_sdk/tmp/README.md new file mode 100644 index 0000000..7a46d9c --- /dev/null +++ b/backend/app/ysm_sdk/tmp/README.md @@ -0,0 +1,157 @@ +# Ysm-SDK-python + +易收米支付Python SDK,提供支付相关API的Python实现。 + +## 安装依赖 + +```bash +pip install -r requirements.txt +``` + +或者手动安装: + +```bash +pip install aiohttp fastapi uvicorn jinja2 python-multipart +``` + +## 文件说明 + +- `api.py` - 基础API工具类,提供HTTP请求和签名方法 +- `pay.py` - 发起支付模块 +- `query.py` - 订单查询模块 +- `notify.py` - 支付回调处理模块 +- `demo.py` - 命令行演示脚本 +- `web_demo.py` - Web服务演示 +- `requirements.txt` - 依赖包列表 + +## 使用示例 + +### 发起支付 + +```python +from pay import create_payment + +# 配置信息 +appid = '20********' # 支付通道ID +appsecret = 'e605ac7******************4af5164' # AppSecret + +# 创建支付 +pay_url = create_payment( + appid=appid, + appsecret=appsecret, + order_id="123321123321", + description="快充数据线", + amount=100, # 1元 + notify_url="http://example.com/notify", + nopay_url="http://example.com/cancel", + callback_url="http://example.com/success", + pay_type=1 # 微信内支付 +) + +print(f"支付链接: {pay_url}") +``` + +### 查询订单 + +```python +from query import query_order + +# 配置信息 +appid = '20********' # 支付通道ID + +# 查询订单 +order_info = query_order( + appid=appid, + mch_orderid='202305105056', # 商户订单号 +) + +print(f"订单信息: {order_info}") +``` + +### 处理支付回调 + +```python +from flask import Flask, request +from notify import PaymentNotify + +app = Flask(__name__) + +@app.route('/notify', methods=['POST']) +def payment_notify(): + # 配置信息 + appid = '20********' # 支付通道ID + appsecret = 'e605ac7******************4af5164' # AppSecret + + # 创建通知处理器 + notify_handler = PaymentNotify(appid, appsecret) + + # 获取请求数据 + request_data = request.get_data() + + # 处理通知 + success, message = notify_handler.process(request_data) + + if success: + # 处理成功 + return message + else: + # 处理失败 + return {"error": message}, 400 + +if __name__ == '__main__': + app.run(debug=True) +``` + +## 支付类型说明 + +- `pay_type=1`: 微信内支付 +- `pay_type=2`: 微信扫码支付 +- `pay_type=3`: 微信H5支付(非原生) +- `pay_type=11`: 支付宝H5支付 + +## 演示程序 + +### 1. 命令行演示 + +运行完整的SDK功能演示: + +```bash +python demo.py +``` + +这个演示会展示: +- 创建支付订单 +- 查询订单状态 +- 处理支付回调 +- 不同支付类型说明 + +### 2. Web服务演示 + +启动Web演示服务: + +```bash +python web_demo.py +``` + +然后访问 `http://localhost:8000` 查看: +- 支付表单页面 +- 订单查询界面 +- 支付成功/取消页面 +- 支付回调处理接口 + +### 配置说明 + +在使用前请修改以下配置: + +1. 在 `demo.py` 中修改: + ```python + self.appid = 'your_appid' # 替换为你的AppID + self.appsecret = 'your_appsecret' # 替换为你的AppSecret + ``` + +2. 在 `web_demo.py` 中修改: + ```python + APPID = 'your_appid' + APPSECRET = 'your_appsecret' + NOTIFY_URL = "http://your-domain.com/notify" # 替换为你的回调地址 + ``` \ No newline at end of file diff --git a/backend/app/ysm_sdk/tmp/demo.py b/backend/app/ysm_sdk/tmp/demo.py new file mode 100644 index 0000000..0f2ec50 --- /dev/null +++ b/backend/app/ysm_sdk/tmp/demo.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +易收米支付SDK使用演示 + +这个demo展示了如何使用易收米支付SDK的各个功能: +1. 创建支付订单 +2. 查询订单状态 +3. 处理支付回调 + +运行前请确保安装依赖: +pip install aiohttp fastapi uvicorn + +使用方法: +python demo.py +""" + +import json +import time +import asyncio +from pay import create_payment +from query import query_order +from notify import PaymentNotify + +class YsmPayDemo: + """易收米支付演示类""" + + def __init__(self): + # 配置信息 - 请替换为你的真实配置 + self.appid = 'YSMcd16b45d' # 支付通道ID + self.appsecret = '899850e778e8d2b53e4c4a4e88695688' # AppSecret + + # 回调地址配置 + self.notify_url = "http://your-domain.com/notify" + self.nopay_url = "http://your-domain.com/cancel" + self.callback_url = "http://your-domain.com/success" + + async def demo_create_payment(self): + """演示创建支付订单""" + print("\n=== 创建支付订单演示 ===") + + # 生成唯一订单号 + order_id = f"demo_{int(time.time())}" + + try: + # 创建支付 + pay_url = await create_payment( + appid=self.appid, + appsecret=self.appsecret, + order_id=order_id, + description="演示商品 - 快充数据线", + amount=100, # 1元 = 100分 + notify_url=self.notify_url, + nopay_url=self.nopay_url, + callback_url=self.callback_url, + pay_type=1 # 微信内支付 + ) + + if pay_url: + print(f"✅ 支付订单创建成功") + print(f"📋 订单号: {order_id}") + print(f"💰 金额: 1.00元") + print(f"🔗 支付链接: {pay_url}") + return order_id, pay_url + else: + print("❌ 支付订单创建失败") + return None, None + + except Exception as e: + print(f"❌ 创建支付时出错: {str(e)}") + return None, None + + async def demo_query_order(self, order_id): + """演示查询订单状态""" + print(f"\n=== 查询订单状态演示 ===") + print(f"🔍 查询订单: {order_id}") + + try: + # 查询订单 + order_info = await query_order( + appid=self.appid, + mch_orderid=order_id + ) + + if order_info: + print("✅ 订单查询成功") + print(f"📊 订单信息:") + print(json.dumps(order_info, ensure_ascii=False, indent=2)) + + # 解析订单状态 + state = order_info.get('state', 'UNKNOWN') + status_map = { + 'SUCCESS': '✅ 支付成功', + 'REFUND': '🔄 转入退款', + 'NOTPAY': '⏳ 未支付', + 'CLOSED': '❌ 已关闭', + } + + # 检查是否是错误响应 + if 'code' in order_info and order_info.get('code') == 'ORDER_NOT_EXIST': + print(f"💡 订单状态: ⚠️ 订单不存在(演示订单,正常现象)") + else: + print(f"💡 订单状态: {status_map.get(state, f'未知状态({state})')}") + + return order_info + else: + print("❌ 订单查询失败") + return None + + except Exception as e: + print(f"❌ 查询订单时出错: {str(e)}") + return None + + async def demo_payment_notify(self): + """演示支付回调处理""" + print(f"\n=== 支付回调处理演示 ===") + + # 模拟支付成功的回调数据 + mock_notify_data = { + "appid": self.appid, + "mch_orderid": "demo_1234567890", + "ysm_orderid": "YSM20240101123456", + "total": 100, + "state": "SUCCESS", + "time": int(time.time()), + "nonce_str": "abc123456" + } + + # 添加签名 + from api import YsmPayApi + mock_notify_data['hash'] = YsmPayApi.hash_sign(mock_notify_data, self.appsecret) + + print(f"📨 模拟回调数据:") + print(json.dumps(mock_notify_data, ensure_ascii=False, indent=2)) + + # 创建通知处理器 + notify_handler = PaymentNotify(self.appid, self.appsecret) + + # 处理回调 + try: + # 使用异步调用 + success, message = await notify_handler.process(mock_notify_data) + + if success: + print(f"✅ 回调处理成功: {message}") + else: + print(f"❌ 回调处理失败: {message}") + + return success, message + + except Exception as e: + print(f"❌ 处理回调时出错: {str(e)}") + return False, str(e) + + def demo_payment_types(self): + """演示不同支付类型""" + print(f"\n=== 支付类型说明 ===") + + payment_types = { + 1: "微信内支付 - 适用于在微信内打开的页面", + 2: "微信扫码支付 - 生成二维码供用户扫码支付", + 3: "微信H5支付 - 适用于手机浏览器(非微信内)", + 11: "支付宝H5支付 - 适用于手机浏览器跳转支付宝" + } + + for pay_type, description in payment_types.items(): + print(f"💳 pay_type={pay_type}: {description}") + + async def run_full_demo(self): + """运行完整演示""" + print("🚀 易收米支付SDK演示开始") + print("=" * 50) + + # 显示配置信息 + print(f"📋 当前配置:") + print(f" AppID: {self.appid}") + print(f" AppSecret: {self.appsecret[:8]}***{self.appsecret[-8:]}") + + # 1. 演示支付类型 + self.demo_payment_types() + + # 2. 创建支付订单 + order_id, pay_url = await self.demo_create_payment() + + if order_id: + # 3. 查询订单状态 + await self.demo_query_order(order_id) + + # 4. 演示回调处理 + await self.demo_payment_notify() + + print("\n" + "=" * 50) + print("✅ 演示完成") + + # 提示用户 + if pay_url: + print(f"\n💡 提示:") + print(f" 1. 可以访问支付链接进行测试: {pay_url}") + print(f" 2. 支付成功后可以再次查询订单状态") + print(f" 3. 实际使用时请替换为真实的回调地址") + +async def main(): + """主函数""" + demo = YsmPayDemo() + await demo.run_full_demo() + +if __name__ == "__main__": + # 运行演示 + asyncio.run(main()) \ No newline at end of file diff --git a/backend/app/ysm_sdk/tmp/merchant_info_explanation.py b/backend/app/ysm_sdk/tmp/merchant_info_explanation.py new file mode 100644 index 0000000..25d0288 --- /dev/null +++ b/backend/app/ysm_sdk/tmp/merchant_info_explanation.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +易收米支付商户信息说明 + +解释商户全称的显示机制和可控制的参数 +""" + +def explain_merchant_info(): + """解释商户信息显示机制""" + print("=" * 60) + print("易收米支付 - 商户信息显示机制说明") + print("=" * 60) + + print("\n商户信息组成:") + print("1. 商户全称 - 由支付平台后台配置,API无法修改") + print("2. 商品描述 - 可通过API的description参数控制") + print("3. 订单金额 - 可通过API的amount参数控制") + print("4. 订单号 - 可通过API的order_id参数控制") + + print("\n无法通过API隐藏的信息:") + print("- 商户全称(需要在支付平台后台修改)") + print("- 支付平台Logo和名称") + print("- 基本的支付安全信息") + + print("\n可以通过API控制的信息:") + print("- 商品描述(description参数)") + print("- 订单金额(amount参数)") + print("- 订单号格式(order_id参数)") + print("- 回调地址(notify_url等参数)") + + print("\n商户全称修改方法:") + print("1. 登录易收米商户后台") + print("2. 进入商户信息设置") + print("3. 修改商户全称/商户简称") + print("4. 提交审核(可能需要1-3个工作日)") + print("5. 审核通过后生效") + + print("\n当前可优化的设置:") + + # 展示不同的description设置效果 + descriptions = [ + ("支付", "最简洁"), + ("订单支付", "通用描述"), + ("服务费", "服务类"), + ("充值", "充值类"), + ("购买", "购买类"), + ("费用", "费用类"), + ("", "空描述(不推荐)") + ] + + print("\n可选的商品描述设置:") + for desc, note in descriptions: + print(f" description='{desc}' - {note}") + + print("\n注意事项:") + print("- 商户全称是监管要求,必须显示真实商户信息") + print("- 不能完全隐藏商户信息,这违反支付规范") + print("- 只能通过合规方式优化显示内容") + print("- 建议设置简洁明了的商户名称") + +def show_current_merchant_info(): + """显示当前使用的商户信息""" + print("\n" + "=" * 60) + print("当前商户配置信息") + print("=" * 60) + + print("\n当前商户设置:") + print("AppID: YSMcd16b45d") + print("商户全称: 由易收米平台后台配置") + print("支付方式: 微信扫码支付") + + print("\n可控制的参数示例:") + examples = [ + { + "场景": "最简洁支付", + "description": "支付", + "amount": "1分钱", + "显示效果": "商户全称 + 支付 + 0.01元" + }, + { + "场景": "服务支付", + "description": "服务费", + "amount": "1分钱", + "显示效果": "商户全称 + 服务费 + 0.01元" + }, + { + "场景": "充值支付", + "description": "账户充值", + "amount": "1分钱", + "显示效果": "商户全称 + 账户充值 + 0.01元" + } + ] + + for example in examples: + print(f"\n• {example['场景']}:") + print(f" 参数: description='{example['description']}'") + print(f" 效果: {example['显示效果']}") + +def alternative_solutions(): + """提供替代解决方案""" + print("\n" + "=" * 60) + print("替代解决方案") + print("=" * 60) + + print("\n🎯 如果需要更简洁的支付体验:") + print("1. 申请新的商户号,使用简短的商户名称") + print("2. 使用个人收款码(但功能有限)") + print("3. 集成其他支付平台(如官方微信支付API)") + print("4. 使用第三方聚合支付(商户名称可能更简洁)") + + print("\n📱 微信官方支付API对比:") + print("- 微信官方API: 可以设置更详细的商户信息") + print("- 易收米等第三方: 商户信息由平台统一管理") + print("- 个人收款码: 显示个人姓名,但无法API调用") + + print("\n💼 商业建议:") + print("- 如果是正式商业用途,建议使用符合规范的商户全称") + print("- 如果是测试用途,当前配置已经足够简洁") + print("- 关注用户体验的同时要遵守支付规范") + +if __name__ == "__main__": + explain_merchant_info() + show_current_merchant_info() + alternative_solutions() + + print("\n" + "=" * 60) + print("总结") + print("=" * 60) + print("商户全称无法通过API完全隐藏,这是支付行业的规范要求。") + print("但可以通过优化商品描述等方式让支付页面更简洁。") + print("如需修改商户全称,需要在支付平台后台操作。") + print("=" * 60) \ No newline at end of file diff --git a/backend/app/ysm_sdk/tmp/private_payment.py b/backend/app/ysm_sdk/tmp/private_payment.py new file mode 100644 index 0000000..d913fbd --- /dev/null +++ b/backend/app/ysm_sdk/tmp/private_payment.py @@ -0,0 +1,249 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +隐私支付二维码生成器 + +生成微信扫码支付二维码,隐藏商户和商品信息 +""" + +import asyncio +import time +import qrcode +from pay import create_payment + +async def create_private_payment(): + """创建隐私支付二维码""" + print("微信扫码支付 - 隐私模式") + print("=" * 40) + + # 配置信息 + appid = 'YSMcd16b45d' + appsecret = '899850e778e8d2b53e4c4a4e88695688' + + # 生成订单号 + order_id = f"pay_{int(time.time())}" + + print(f"订单号: {order_id}") + print(f"金额: 0.01元") + print() + print("正在生成支付二维码...") + + try: + # 创建扫码支付订单 - 使用简化的描述 + pay_url = await create_payment( + appid=appid, + appsecret=appsecret, + order_id=order_id, + description="支付", # 简化商品描述 + amount=1, # 1分钱 + notify_url="http://localhost/notify", # 简化回调地址 + nopay_url="http://localhost/cancel", + callback_url="http://localhost/success", + pay_type=2 # 微信扫码支付 + ) + + if pay_url: + print("支付二维码生成成功!") + print() + + # 生成二维码 + qr = qrcode.QRCode( + version=1, + error_correction=qrcode.constants.ERROR_CORRECT_L, + box_size=8, # 稍微小一点的二维码 + border=2, + ) + qr.add_data(pay_url) + qr.make(fit=True) + + # 创建二维码图片 + qr_img = qr.make_image(fill_color="black", back_color="white") + + # 保存二维码 + qr_filename = f"private_qr_{order_id}.png" + qr_img.save(qr_filename) + + print(f"二维码已生成: {qr_filename}") + print() + print("使用方法:") + print("1. 用微信扫描生成的二维码") + print("2. 完成0.01元支付") + print("3. 支付页面将显示最小化的商户信息") + print() + print("隐私设置:") + print("- 商品描述: 仅显示'支付'") + print("- 商户信息: 使用平台默认设置") + print("- 订单金额: 0.01元") + + return order_id, pay_url, qr_filename + + else: + print("支付订单创建失败") + return None, None, None + + except Exception as e: + print(f"创建支付时出错: {str(e)}") + return None, None, None + +async def create_custom_payment(): + """创建自定义支付二维码""" + print("\n" + "=" * 40) + print("自定义支付设置") + print("=" * 40) + + # 让用户自定义设置 + description_options = { + "1": "支付", + "2": "订单支付", + "3": "在线支付", + "4": "服务费用", + "5": "充值" + } + + print("选择商品描述:") + for key, value in description_options.items(): + print(f"{key}. {value}") + + # 由于是演示,我们使用默认选项 + choice = "1" # 默认选择"支付" + description = description_options.get(choice, "支付") + + # 配置信息 + appid = 'YSMcd16b45d' + appsecret = '899850e778e8d2b53e4c4a4e88695688' + + # 生成订单号 + order_id = f"custom_{int(time.time())}" + + print(f"\n订单设置:") + print(f"订单号: {order_id}") + print(f"描述: {description}") + print(f"金额: 0.01元") + print("\n正在生成支付二维码...") + + try: + # 创建支付订单 + pay_url = await create_payment( + appid=appid, + appsecret=appsecret, + order_id=order_id, + description=description, + amount=1, + notify_url="http://localhost/notify", + nopay_url="http://localhost/cancel", + callback_url="http://localhost/success", + pay_type=2 + ) + + if pay_url: + # 生成二维码 + qr = qrcode.QRCode(version=1, box_size=8, border=2) + qr.add_data(pay_url) + qr.make(fit=True) + qr_img = qr.make_image(fill_color="black", back_color="white") + + # 保存二维码 + qr_filename = f"custom_qr_{order_id}.png" + qr_img.save(qr_filename) + + print(f"自定义支付二维码已生成: {qr_filename}") + return order_id, pay_url, qr_filename + + else: + print("支付订单创建失败") + return None, None, None + + except Exception as e: + print(f"创建支付时出错: {str(e)}") + return None, None, None + +async def batch_create_payments(): + """批量创建不同类型的支付二维码""" + print("批量生成支付二维码") + print("=" * 40) + + payment_types = [ + ("支付", "simple"), + ("订单", "order"), + ("充值", "recharge"), + ("服务", "service") + ] + + appid = 'YSMcd16b45d' + appsecret = '899850e778e8d2b53e4c4a4e88695688' + + created_qrs = [] + + for desc, type_name in payment_types: + order_id = f"{type_name}_{int(time.time())}" + + try: + pay_url = await create_payment( + appid=appid, + appsecret=appsecret, + order_id=order_id, + description=desc, + amount=1, + notify_url="http://localhost/notify", + nopay_url="http://localhost/cancel", + callback_url="http://localhost/success", + pay_type=2 + ) + + if pay_url: + # 生成二维码 + qr = qrcode.QRCode(version=1, box_size=6, border=2) + qr.add_data(pay_url) + qr.make(fit=True) + qr_img = qr.make_image(fill_color="black", back_color="white") + + qr_filename = f"{type_name}_qr_{order_id}.png" + qr_img.save(qr_filename) + + created_qrs.append({ + 'order_id': order_id, + 'description': desc, + 'filename': qr_filename, + 'url': pay_url + }) + + print(f"✓ {desc} - {qr_filename}") + + except Exception as e: + print(f"✗ 创建{desc}支付失败: {str(e)}") + + print(f"\n成功生成 {len(created_qrs)} 个支付二维码") + return created_qrs + +async def main(): + """主函数""" + print("微信支付二维码生成器 - 隐私版") + print("支持隐藏商户信息和自定义商品描述") + print() + + # 选择模式 + print("选择生成模式:") + print("1. 隐私模式 - 最小化信息显示") + print("2. 自定义模式 - 自定义商品描述") + print("3. 批量模式 - 生成多种类型二维码") + + # 默认使用隐私模式进行演示 + mode = "1" + + if mode == "1": + await create_private_payment() + elif mode == "2": + await create_custom_payment() + elif mode == "3": + await batch_create_payments() + + print("\n" + "=" * 40) + print("提示:") + print("- 所有二维码都已保存到当前目录") + print("- 使用微信扫描任意二维码进行支付测试") + print("- 支付金额均为0.01元") + print("- 商户信息已最小化显示") + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/backend/app/ysm_sdk/tmp/qr_payment.py b/backend/app/ysm_sdk/tmp/qr_payment.py new file mode 100644 index 0000000..d968630 --- /dev/null +++ b/backend/app/ysm_sdk/tmp/qr_payment.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +易收米支付二维码演示 + +生成微信扫码支付二维码,真实支付场景演示 +""" + +import asyncio +import time +import qrcode +from io import BytesIO +from PIL import Image +from pay import create_payment + +async def create_qr_payment(): + """创建二维码支付""" + print("易收米微信扫码支付演示") + print("=" * 50) + + # 配置信息 + appid = 'YSMcd16b45d' + appsecret = '899850e778e8d2b53e4c4a4e88695688' + + # 生成订单号 + order_id = f"qr_{int(time.time())}" + + print(f"订单号: {order_id}") + print(f"商品: 测试商品 - 数据线") + print(f"金额: 0.01元") + print(f"支付方式: 微信扫码支付") + print() + print("正在生成支付二维码...") + + try: + # 创建扫码支付订单 - 使用 pay_type=2 + pay_url = await create_payment( + appid=appid, + appsecret=appsecret, + order_id=order_id, + description="测试商品 - 数据线", + amount=1, # 1分钱 + notify_url="http://your-domain.com/notify", + nopay_url="http://your-domain.com/cancel", + callback_url="http://your-domain.com/success", + pay_type=2 # 微信扫码支付 + ) + + if pay_url: + print("支付订单创建成功!") + print(f"支付链接: {pay_url}") + print() + + # 生成二维码 + qr = qrcode.QRCode( + version=1, + error_correction=qrcode.constants.ERROR_CORRECT_L, + box_size=10, + border=4, + ) + qr.add_data(pay_url) + qr.make(fit=True) + + # 创建二维码图片 + qr_img = qr.make_image(fill_color="black", back_color="white") + + # 保存二维码 + qr_filename = f"payment_qr_{order_id}.png" + qr_img.save(qr_filename) + + print(f"二维码已生成: {qr_filename}") + print() + print("使用方法:") + print("1. 打开微信扫一扫") + print(f"2. 扫描生成的二维码文件: {qr_filename}") + print("3. 完成0.01元支付") + print("4. 支付完成后可以查询订单状态") + print() + print("=" * 50) + print("二维码图片已保存到当前目录") + print("请使用微信扫描二维码文件进行支付") + print("=" * 50) + + return order_id, pay_url, qr_filename + + else: + print("支付订单创建失败") + return None, None, None + + except Exception as e: + print(f"创建支付时出错: {str(e)}") + return None, None, None + +async def check_payment_status(order_id): + """检查支付状态""" + if not order_id: + return + + print(f"\n检查订单状态: {order_id}") + + from query import query_order + + try: + # 查询订单状态 + order_info = await query_order( + appid='YSMcd16b45d', + mch_orderid=order_id + ) + + if order_info: + state = order_info.get('state', 'UNKNOWN') + + if state == 'SUCCESS': + print("支付状态: 支付成功! 🎉") + elif state == 'NOTPAY': + print("支付状态: 等待支付...") + elif 'code' in order_info and order_info.get('code') == 'ORDER_NOT_EXIST': + print("支付状态: 订单查询中...") + else: + print(f"支付状态: {state}") + + print(f"详细信息: {order_info}") + else: + print("查询失败") + + except Exception as e: + print(f"查询订单时出错: {str(e)}") + +async def main(): + """主函数""" + # 生成支付二维码 + order_id, pay_url, qr_file = await create_qr_payment() + + if order_id: + # 等待用户扫码支付 + input("\n按回车键查询支付状态...") + + # 检查支付状态 + await check_payment_status(order_id) + + # 可以持续查询 + while True: + check_again = input("\n是否再次查询订单状态? (y/n): ") + if check_again.lower() == 'y': + await check_payment_status(order_id) + else: + break + +if __name__ == "__main__": + print("请确保已安装依赖: pip install qrcode[pil]") + print() + asyncio.run(main()) \ No newline at end of file diff --git a/backend/app/ysm_sdk/tmp/quick_test.py b/backend/app/ysm_sdk/tmp/quick_test.py new file mode 100644 index 0000000..de5f683 --- /dev/null +++ b/backend/app/ysm_sdk/tmp/quick_test.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +易收米支付SDK快速测试 + +简单的支付功能测试脚本 +""" + +import asyncio +import time +from pay import create_payment + +async def quick_payment_test(): + """快速支付测试""" + print("易收米支付快速测试") + print("=" * 40) + + # 配置信息 - 请替换为你的真实配置 + appid = 'YSMcd16b45d' + appsecret = '899850e778e8d2b53e4c4a4e88695688' + + # 生成测试订单号 + order_id = f"test_{int(time.time())}" + + print(f"创建测试订单: {order_id}") + print(f"金额: 0.01元") + print(f"支付类型: 微信内支付") + + try: + # 创建支付订单 + pay_url = await create_payment( + appid=appid, + appsecret=appsecret, + order_id=order_id, + description="测试商品", + amount=1, # 1分钱 + notify_url="http://your-domain.com/notify", + nopay_url="http://your-domain.com/cancel", + callback_url="http://your-domain.com/success", + pay_type=1 + ) + + if pay_url: + print("支付订单创建成功!") + print(f"支付链接: {pay_url}") + print() + print("使用说明:") + print(" 1. 复制上方链接到微信中打开") + print(" 2. 完成支付测试") + print(" 3. 可以使用query.py查询订单状态") + + else: + print("支付订单创建失败") + + except Exception as e: + print(f"创建支付时出错: {str(e)}") + +if __name__ == "__main__": + asyncio.run(quick_payment_test()) \ No newline at end of file diff --git a/backend/app/ysm_sdk/tmp/requirements.txt b/backend/app/ysm_sdk/tmp/requirements.txt new file mode 100644 index 0000000..743ecfb --- /dev/null +++ b/backend/app/ysm_sdk/tmp/requirements.txt @@ -0,0 +1,5 @@ +aiohttp>=3.8.0 +fastapi>=0.68.0 +uvicorn>=0.15.0 +jinja2>=2.11.0 +python-multipart>=0.0.5 \ No newline at end of file diff --git a/backend/app/ysm_sdk/tmp/web_demo.py b/backend/app/ysm_sdk/tmp/web_demo.py new file mode 100644 index 0000000..51f5b26 --- /dev/null +++ b/backend/app/ysm_sdk/tmp/web_demo.py @@ -0,0 +1,444 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +易收米支付SDK Web服务演示 + +这个演示展示了如何在Web应用中集成易收米支付: +1. 创建支付页面 +2. 处理支付回调 +3. 查询订单状态 + +运行前请安装依赖: +pip install fastapi uvicorn jinja2 python-multipart + +运行方式: +python web_demo.py + +然后访问: http://localhost:8000 +""" + +import json +import time +import asyncio +from fastapi import FastAPI, Request, Form, Response +from fastapi.responses import HTMLResponse, JSONResponse +from fastapi.templating import Jinja2Templates +from pay import create_payment +from query import query_order +from notify import PaymentNotify + +# 创建FastAPI应用 +app = FastAPI(title="易收米支付演示", description="易收米支付SDK Web演示") + +# 配置信息 - 请替换为你的真实配置 +APPID = 'YSMcd16b45d' +APPSECRET = '899850e778e8d2b53e4c4a4e88695688' + +# 回调地址 - 实际使用时应该是你的域名 +NOTIFY_URL = "http://localhost:8000/notify" +NOPAY_URL = "http://localhost:8000/cancel" +CALLBACK_URL = "http://localhost:8000/success" + +# 模板引擎 +templates = Jinja2Templates(directory="templates") + +@app.get("/", response_class=HTMLResponse) +async def index(request: Request): + """首页 - 支付表单""" + + html_content = """ + + + + 易收米支付演示 + + + + +

🚀 易收米支付演示

+ +

创建支付订单

+
+
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ +

查询订单状态

+
+
+ + +
+ +
+ +

📚 接口说明

+ + + + """ + + return HTMLResponse(content=html_content) + +@app.post("/create_payment") +async def create_payment_endpoint( + description: str = Form(...), + amount: float = Form(...), + pay_type: int = Form(...) +): + """创建支付订单""" + + try: + # 生成订单号 + order_id = f"demo_{int(time.time())}" + + # 转换金额为分 + amount_cents = int(amount * 100) + + # 创建支付 + pay_url = await create_payment( + appid=APPID, + appsecret=APPSECRET, + order_id=order_id, + description=description, + amount=amount_cents, + notify_url=NOTIFY_URL, + nopay_url=NOPAY_URL, + callback_url=CALLBACK_URL, + pay_type=pay_type + ) + + if pay_url: + # 保存订单信息(实际应用中应该保存到数据库) + order_info = { + 'order_id': order_id, + 'description': description, + 'amount': amount, + 'pay_type': pay_type, + 'pay_url': pay_url, + 'status': 'created', + 'created_at': time.time() + } + + result_html = f""" + + + + 支付订单创建成功 + + + + +

✅ 支付订单创建成功

+ +
+ 支付订单已成功创建!请点击下方链接进行支付。 +
+ +
+ 订单信息:
+ 订单号: {order_id}
+ 商品: {description}
+ 金额: ¥{amount}
+ 支付类型: {pay_type} +
+ + 🚀 立即支付 + 🏠 返回首页 + 🔍 查询订单 + + + """ + + return HTMLResponse(content=result_html) + else: + return JSONResponse( + content={"error": "创建支付失败"}, + status_code=400 + ) + + except Exception as e: + return JSONResponse( + content={"error": f"创建支付时出错: {str(e)}"}, + status_code=500 + ) + +@app.post("/query_order") +async def query_order_endpoint(order_id: str = Form(...)): + """查询订单状态""" + + try: + # 查询订单 + order_info = await query_order( + appid=APPID, + mch_orderid=order_id + ) + + if order_info: + status_map = { + 'SUCCESS': '✅ 支付成功', + 'REFUND': '🔄 转入退款', + 'NOTPAY': '⏳ 未支付', + 'CLOSED': '❌ 已关闭', + } + + state = order_info.get('state', 'UNKNOWN') + status_text = status_map.get(state, f'未知状态({state})') + + result_html = f""" + + + + 订单查询结果 + + + + +

🔍 订单查询结果

+ +
+ 订单号: {order_id}
+ 状态: {status_text} +
+ +

📋 详细信息:

+
{json.dumps(order_info, ensure_ascii=False, indent=2)}
+ + 🏠 返回首页 + + + """ + + return HTMLResponse(content=result_html) + else: + return JSONResponse( + content={"error": "订单不存在或查询失败"}, + status_code=404 + ) + + except Exception as e: + return JSONResponse( + content={"error": f"查询订单时出错: {str(e)}"}, + status_code=500 + ) + +@app.post("/notify") +async def payment_notify(request: Request): + """支付成功回调处理""" + + try: + # 获取请求数据 + request_data = await request.body() + + # 创建通知处理器 + notify_handler = PaymentNotify(APPID, APPSECRET) + + # 处理通知 + success, message = await notify_handler.process(request_data) + + if success: + # 这里应该更新数据库中的订单状态 + print(f"💰 支付成功回调处理: {message}") + return Response(content=message) + else: + print(f"❌ 支付回调处理失败: {message}") + return JSONResponse(content={"error": message}, status_code=400) + + except Exception as e: + print(f"❌ 处理支付回调时出错: {str(e)}") + return JSONResponse(content={"error": str(e)}, status_code=500) + +@app.get("/success") +async def payment_success(request: Request): + """支付成功页面""" + + html_content = """ + + + + 支付成功 + + + + +
+

🎉 支付成功!

+

您的订单已支付成功,感谢您的购买!

+
+ 🏠 返回首页 + + + """ + + return HTMLResponse(content=html_content) + +@app.get("/cancel") +async def payment_cancel(request: Request): + """支付取消页面""" + + html_content = """ + + + + 支付取消 + + + + +
+

⚠️ 支付已取消

+

您取消了支付,如有需要可以重新发起支付。

+
+ 🏠 返回首页 + + + """ + + return HTMLResponse(content=html_content) + +@app.get("/query_order_page") +async def query_order_page(request: Request, order_id: str = None): + """订单查询页面""" + + if order_id: + # 直接查询订单 + try: + order_info = await query_order(appid=APPID, mch_orderid=order_id) + if order_info: + status_map = { + 'SUCCESS': '✅ 支付成功', + 'REFUND': '🔄 转入退款', + 'NOTPAY': '⏳ 未支付', + 'CLOSED': '❌ 已关闭', + } + + state = order_info.get('state', 'UNKNOWN') + status_text = status_map.get(state, f'未知状态({state})') + + html_content = f""" + + + + 订单详情 + + + + +

📋 订单详情

+
+ 订单号: {order_id}
+ 状态: {status_text} +
+
{json.dumps(order_info, ensure_ascii=False, indent=2)}
+ 🏠 返回首页 + + + """ + return HTMLResponse(content=html_content) + except: + pass + + # 显示查询表单 + html_content = """ + + + + 订单查询 + + + + +

🔍 订单查询

+
+
+ + +
+ +
+ 🏠 返回首页 + + + """ + + return HTMLResponse(content=html_content) + +if __name__ == "__main__": + import uvicorn + + print("🚀 启动易收米支付演示服务...") + print("📱 访问地址: http://localhost:8000") + print("📋 配置信息:") + print(f" AppID: {APPID}") + print(f" AppSecret: {APPSECRET[:8]}***{APPSECRET[-8:]}") + print("=" * 50) + + uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file diff --git a/backend/dispatch_api.py b/backend/dispatch_api.py new file mode 100644 index 0000000..70104a1 --- /dev/null +++ b/backend/dispatch_api.py @@ -0,0 +1,389 @@ +from fastapi import FastAPI, HTTPException, Header, Depends +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel +from typing import Optional, List +from datetime import datetime +import sqlite3 +from pathlib import Path + +# 数据库路径 +DB_PATH = Path("/root/tuhui/backend/dispatch.db") +DESIGNER_DB_PATH = Path("/root/tuhui/backend/designer_status.db") + +# API 密钥(生产环境应该从环境变量读取) +API_KEY = "tuhui_dispatch_key_2026" + +app = FastAPI( + title="图汇派单系统 API", + description="专业的设计师派单管理系统", + version="1.0.0" +) + +# CORS 配置 +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# ========== 数据模型 ========== + +class TaskCreate(BaseModel): + task_name: str + description: str = "" + task_type: str = "design" + priority: int = 1 + deadline: Optional[str] = None + +class TaskAssign(BaseModel): + designer_name: str + notes: Optional[str] = "" + +class TaskComplete(BaseModel): + notes: Optional[str] = "" + +# ========== 认证依赖 ========== + +def verify_api_key(x_api_key: str = Header(..., alias="X-API-Key")): + """验证 API Key""" + if x_api_key != API_KEY: + raise HTTPException(status_code=401, detail="Invalid API Key") + return x_api_key + +# ========== 数据库辅助函数 ========== + +def get_db_connection(db_path: Path): + """获取数据库连接""" + conn = sqlite3.connect(str(db_path)) + conn.row_factory = sqlite3.Row + return conn + +def get_online_designers() -> List[str]: + """获取在线设计师""" + conn = get_db_connection(DESIGNER_DB_PATH) + cursor = conn.cursor() + cursor.execute("SELECT real_name FROM designer_status WHERE status = 'online'") + designers = [row["real_name"] for row in cursor.fetchall()] + conn.close() + return designers + +# ========== API 接口 ========== + +@app.get("/health") +def health_check(): + """健康检查""" + return {"status": "ok", "timestamp": datetime.now().isoformat()} + +@app.get("/online/designers") +def get_online_designers_api(): + """ + 获取在线设计师列表 + + 返回当前所有在线的设计师名单 + """ + designers = get_online_designers() + return { + "online_count": len(designers), + "online_users": designers, + "update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S") + } + +@app.post("/tasks", tags=["任务管理"]) +def create_task(task: TaskCreate, api_key: str = Depends(verify_api_key)): + """ + 创建派单任务 + + - **task_name**: 任务名称 + - **description**: 任务描述 + - **task_type**: 任务类型(design/design_review/other) + - **priority**: 优先级(1-5,5 最高) + - **deadline**: 截止时间(可选) + """ + import uuid + task_id = str(uuid.uuid4())[:8] + + conn = get_db_connection(DB_PATH) + cursor = conn.cursor() + + cursor.execute(''' + INSERT INTO dispatch_tasks (id, task_name, task_description, task_type, priority, status, deadline) + VALUES (?, ?, ?, ?, ?, 'pending', ?) + ''', (task_id, task.task_name, task.description, task.task_type, task.priority, task.deadline)) + + conn.commit() + conn.close() + + return { + "task_id": task_id, + "task_name": task.task_name, + "status": "pending", + "created_at": datetime.now().isoformat() + } + +@app.get("/tasks/pending", tags=["任务管理"]) +def get_pending_tasks(limit: int = 10, api_key: str = Depends(verify_api_key)): + """ + 获取待分配任务列表 + + - **limit**: 返回数量限制(默认 10) + """ + conn = get_db_connection(DB_PATH) + cursor = conn.cursor() + + cursor.execute(''' + SELECT id, task_name, task_description, task_type, priority, created_at, deadline + FROM dispatch_tasks + WHERE status = 'pending' + ORDER BY priority DESC, created_at ASC + LIMIT ? + ''', (limit,)) + + tasks = [] + for row in cursor.fetchall(): + tasks.append({ + "task_id": row["id"], + "task_name": row["task_name"], + "description": row["task_description"], + "type": row["task_type"], + "priority": row["priority"], + "created_at": row["created_at"], + "deadline": row["deadline"] + }) + + conn.close() + + return { + "total": len(tasks), + "tasks": tasks + } + +@app.post("/tasks/{task_id}/assign", tags=["任务管理"]) +def assign_task(task_id: str, assign_data: TaskAssign, api_key: str = Depends(verify_api_key)): + """ + 分配任务给设计师 + + - **task_id**: 任务 ID + - **designer_name**: 设计师姓名 + - **notes**: 备注(可选) + """ + conn = get_db_connection(DB_PATH) + cursor = conn.cursor() + + # 检查任务是否存在 + cursor.execute('SELECT id, status FROM dispatch_tasks WHERE id = ?', (task_id,)) + task = cursor.fetchone() + + if not task: + conn.close() + raise HTTPException(status_code=404, detail="Task not found") + + if task["status"] != "pending": + conn.close() + raise HTTPException(status_code=400, detail=f"Task status is {task['status']}, cannot assign") + + # 更新任务状态 + cursor.execute(''' + UPDATE dispatch_tasks + SET status = 'assigned', assigned_to = ?, assigned_at = ? + WHERE id = ? + ''', (assign_data.designer_name, datetime.now(), task_id)) + + # 记录历史 + cursor.execute(''' + INSERT INTO dispatch_history (task_id, designer_name, action, notes) + VALUES (?, ?, 'assigned', ?) + ''', (task_id, assign_data.designer_name, assign_data.notes)) + + # 更新工作量 + cursor.execute(''' + INSERT OR REPLACE INTO designer_workload (designer_name, pending_tasks, total_tasks, last_updated) + VALUES ( + ?, + COALESCE((SELECT pending_tasks FROM designer_workload WHERE designer_name = ?), 0) + 1, + COALESCE((SELECT total_tasks FROM designer_workload WHERE designer_name = ?), 0) + 1, + ? + ) + ''', (assign_data.designer_name, assign_data.designer_name, assign_data.designer_name, datetime.now())) + + conn.commit() + conn.close() + + return { + "task_id": task_id, + "assigned_to": assign_data.designer_name, + "status": "assigned", + "assigned_at": datetime.now().isoformat() + } + +@app.get("/tasks/{task_id}", tags=["任务管理"]) +def get_task_status(task_id: str, api_key: str = Depends(verify_api_key)): + """ + 查询任务状态 + """ + conn = get_db_connection(DB_PATH) + cursor = conn.cursor() + + cursor.execute(''' + SELECT id, task_name, status, assigned_to, created_at, assigned_at, completed_at, deadline + FROM dispatch_tasks + WHERE id = ? + ''', (task_id,)) + + row = cursor.fetchone() + conn.close() + + if not row: + raise HTTPException(status_code=404, detail="Task not found") + + return { + "task_id": row["id"], + "task_name": row["task_name"], + "status": row["status"], + "assigned_to": row["assigned_to"], + "created_at": row["created_at"], + "assigned_at": row["assigned_at"], + "completed_at": row["completed_at"], + "deadline": row["deadline"] + } + +@app.post("/tasks/{task_id}/complete", tags=["任务管理"]) +def complete_task(task_id: str, complete_data: TaskComplete, api_key: str = Depends(verify_api_key)): + """ + 完成任务 + + - **task_id**: 任务 ID + - **notes**: 完成备注(可选) + """ + conn = get_db_connection(DB_PATH) + cursor = conn.cursor() + + # 获取任务信息 + cursor.execute('SELECT assigned_to, status FROM dispatch_tasks WHERE id = ?', (task_id,)) + row = cursor.fetchone() + + if not row: + conn.close() + raise HTTPException(status_code=404, detail="Task not found") + + if row["status"] != "assigned" and row["status"] != "in_progress": + conn.close() + raise HTTPException(status_code=400, detail=f"Task status is {row['status']}, cannot complete") + + designer_name = row["assigned_to"] + + # 更新任务状态 + cursor.execute(''' + UPDATE dispatch_tasks + SET status = 'completed', completed_at = ? + WHERE id = ? + ''', (datetime.now(), task_id)) + + # 记录历史 + cursor.execute(''' + INSERT INTO dispatch_history (task_id, designer_name, action, notes) + VALUES (?, ?, 'completed', ?) + ''', (task_id, designer_name, complete_data.notes)) + + # 更新工作量 + cursor.execute(''' + UPDATE designer_workload + SET pending_tasks = pending_tasks - 1, + completed_tasks = completed_tasks + 1, + last_updated = ? + WHERE designer_name = ? + ''', (datetime.now(), designer_name)) + + conn.commit() + conn.close() + + return { + "task_id": task_id, + "status": "completed", + "completed_at": datetime.now().isoformat() + } + +@app.get("/designers/workload", tags=["设计师管理"]) +def get_designer_workload(api_key: str = Depends(verify_api_key)): + """ + 获取设计师工作量统计 + """ + conn = get_db_connection(DB_PATH) + cursor = conn.cursor() + + cursor.execute(''' + SELECT designer_name, total_tasks, completed_tasks, pending_tasks, last_updated + FROM designer_workload + ORDER BY total_tasks DESC + ''') + + workload = [] + for row in cursor.fetchall(): + workload.append({ + "designer": row["designer_name"], + "total_tasks": row["total_tasks"], + "completed_tasks": row["completed_tasks"], + "pending_tasks": row["pending_tasks"], + "last_updated": row["last_updated"] + }) + + conn.close() + + return { + "total_designers": len(workload), + "designers": workload + } + +@app.get("/dispatch/queue", tags=["派单队列"]) +def get_dispatch_queue(api_key: str = Depends(verify_api_key)): + """ + 获取派单队列(待分配任务 + 在线设计师) + + 用于轮询派单的主接口 + """ + # 获取待分配任务 + conn = get_db_connection(DB_PATH) + cursor = conn.cursor() + cursor.execute(''' + SELECT id, task_name, task_description, task_type, priority, created_at, deadline + FROM dispatch_tasks + WHERE status = 'pending' + ORDER BY priority DESC, created_at ASC + LIMIT 20 + ''') + + pending_tasks = [] + for row in cursor.fetchall(): + pending_tasks.append({ + "task_id": row["id"], + "task_name": row["task_name"], + "description": row["task_description"], + "type": row["task_type"], + "priority": row["priority"], + "created_at": row["created_at"], + "deadline": row["deadline"] + }) + conn.close() + + # 获取在线设计师 + online_designers = get_online_designers() + + return { + "timestamp": datetime.now().isoformat(), + "pending_tasks": { + "count": len(pending_tasks), + "tasks": pending_tasks + }, + "online_designers": { + "count": len(online_designers), + "designers": online_designers + }, + "suggestion": online_designers[0] if online_designers and pending_tasks else None + } + +# ========== 主程序 ========== + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8001) diff --git a/backend/dispatch_api_backup.py b/backend/dispatch_api_backup.py new file mode 100644 index 0000000..70104a1 --- /dev/null +++ b/backend/dispatch_api_backup.py @@ -0,0 +1,389 @@ +from fastapi import FastAPI, HTTPException, Header, Depends +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel +from typing import Optional, List +from datetime import datetime +import sqlite3 +from pathlib import Path + +# 数据库路径 +DB_PATH = Path("/root/tuhui/backend/dispatch.db") +DESIGNER_DB_PATH = Path("/root/tuhui/backend/designer_status.db") + +# API 密钥(生产环境应该从环境变量读取) +API_KEY = "tuhui_dispatch_key_2026" + +app = FastAPI( + title="图汇派单系统 API", + description="专业的设计师派单管理系统", + version="1.0.0" +) + +# CORS 配置 +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# ========== 数据模型 ========== + +class TaskCreate(BaseModel): + task_name: str + description: str = "" + task_type: str = "design" + priority: int = 1 + deadline: Optional[str] = None + +class TaskAssign(BaseModel): + designer_name: str + notes: Optional[str] = "" + +class TaskComplete(BaseModel): + notes: Optional[str] = "" + +# ========== 认证依赖 ========== + +def verify_api_key(x_api_key: str = Header(..., alias="X-API-Key")): + """验证 API Key""" + if x_api_key != API_KEY: + raise HTTPException(status_code=401, detail="Invalid API Key") + return x_api_key + +# ========== 数据库辅助函数 ========== + +def get_db_connection(db_path: Path): + """获取数据库连接""" + conn = sqlite3.connect(str(db_path)) + conn.row_factory = sqlite3.Row + return conn + +def get_online_designers() -> List[str]: + """获取在线设计师""" + conn = get_db_connection(DESIGNER_DB_PATH) + cursor = conn.cursor() + cursor.execute("SELECT real_name FROM designer_status WHERE status = 'online'") + designers = [row["real_name"] for row in cursor.fetchall()] + conn.close() + return designers + +# ========== API 接口 ========== + +@app.get("/health") +def health_check(): + """健康检查""" + return {"status": "ok", "timestamp": datetime.now().isoformat()} + +@app.get("/online/designers") +def get_online_designers_api(): + """ + 获取在线设计师列表 + + 返回当前所有在线的设计师名单 + """ + designers = get_online_designers() + return { + "online_count": len(designers), + "online_users": designers, + "update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S") + } + +@app.post("/tasks", tags=["任务管理"]) +def create_task(task: TaskCreate, api_key: str = Depends(verify_api_key)): + """ + 创建派单任务 + + - **task_name**: 任务名称 + - **description**: 任务描述 + - **task_type**: 任务类型(design/design_review/other) + - **priority**: 优先级(1-5,5 最高) + - **deadline**: 截止时间(可选) + """ + import uuid + task_id = str(uuid.uuid4())[:8] + + conn = get_db_connection(DB_PATH) + cursor = conn.cursor() + + cursor.execute(''' + INSERT INTO dispatch_tasks (id, task_name, task_description, task_type, priority, status, deadline) + VALUES (?, ?, ?, ?, ?, 'pending', ?) + ''', (task_id, task.task_name, task.description, task.task_type, task.priority, task.deadline)) + + conn.commit() + conn.close() + + return { + "task_id": task_id, + "task_name": task.task_name, + "status": "pending", + "created_at": datetime.now().isoformat() + } + +@app.get("/tasks/pending", tags=["任务管理"]) +def get_pending_tasks(limit: int = 10, api_key: str = Depends(verify_api_key)): + """ + 获取待分配任务列表 + + - **limit**: 返回数量限制(默认 10) + """ + conn = get_db_connection(DB_PATH) + cursor = conn.cursor() + + cursor.execute(''' + SELECT id, task_name, task_description, task_type, priority, created_at, deadline + FROM dispatch_tasks + WHERE status = 'pending' + ORDER BY priority DESC, created_at ASC + LIMIT ? + ''', (limit,)) + + tasks = [] + for row in cursor.fetchall(): + tasks.append({ + "task_id": row["id"], + "task_name": row["task_name"], + "description": row["task_description"], + "type": row["task_type"], + "priority": row["priority"], + "created_at": row["created_at"], + "deadline": row["deadline"] + }) + + conn.close() + + return { + "total": len(tasks), + "tasks": tasks + } + +@app.post("/tasks/{task_id}/assign", tags=["任务管理"]) +def assign_task(task_id: str, assign_data: TaskAssign, api_key: str = Depends(verify_api_key)): + """ + 分配任务给设计师 + + - **task_id**: 任务 ID + - **designer_name**: 设计师姓名 + - **notes**: 备注(可选) + """ + conn = get_db_connection(DB_PATH) + cursor = conn.cursor() + + # 检查任务是否存在 + cursor.execute('SELECT id, status FROM dispatch_tasks WHERE id = ?', (task_id,)) + task = cursor.fetchone() + + if not task: + conn.close() + raise HTTPException(status_code=404, detail="Task not found") + + if task["status"] != "pending": + conn.close() + raise HTTPException(status_code=400, detail=f"Task status is {task['status']}, cannot assign") + + # 更新任务状态 + cursor.execute(''' + UPDATE dispatch_tasks + SET status = 'assigned', assigned_to = ?, assigned_at = ? + WHERE id = ? + ''', (assign_data.designer_name, datetime.now(), task_id)) + + # 记录历史 + cursor.execute(''' + INSERT INTO dispatch_history (task_id, designer_name, action, notes) + VALUES (?, ?, 'assigned', ?) + ''', (task_id, assign_data.designer_name, assign_data.notes)) + + # 更新工作量 + cursor.execute(''' + INSERT OR REPLACE INTO designer_workload (designer_name, pending_tasks, total_tasks, last_updated) + VALUES ( + ?, + COALESCE((SELECT pending_tasks FROM designer_workload WHERE designer_name = ?), 0) + 1, + COALESCE((SELECT total_tasks FROM designer_workload WHERE designer_name = ?), 0) + 1, + ? + ) + ''', (assign_data.designer_name, assign_data.designer_name, assign_data.designer_name, datetime.now())) + + conn.commit() + conn.close() + + return { + "task_id": task_id, + "assigned_to": assign_data.designer_name, + "status": "assigned", + "assigned_at": datetime.now().isoformat() + } + +@app.get("/tasks/{task_id}", tags=["任务管理"]) +def get_task_status(task_id: str, api_key: str = Depends(verify_api_key)): + """ + 查询任务状态 + """ + conn = get_db_connection(DB_PATH) + cursor = conn.cursor() + + cursor.execute(''' + SELECT id, task_name, status, assigned_to, created_at, assigned_at, completed_at, deadline + FROM dispatch_tasks + WHERE id = ? + ''', (task_id,)) + + row = cursor.fetchone() + conn.close() + + if not row: + raise HTTPException(status_code=404, detail="Task not found") + + return { + "task_id": row["id"], + "task_name": row["task_name"], + "status": row["status"], + "assigned_to": row["assigned_to"], + "created_at": row["created_at"], + "assigned_at": row["assigned_at"], + "completed_at": row["completed_at"], + "deadline": row["deadline"] + } + +@app.post("/tasks/{task_id}/complete", tags=["任务管理"]) +def complete_task(task_id: str, complete_data: TaskComplete, api_key: str = Depends(verify_api_key)): + """ + 完成任务 + + - **task_id**: 任务 ID + - **notes**: 完成备注(可选) + """ + conn = get_db_connection(DB_PATH) + cursor = conn.cursor() + + # 获取任务信息 + cursor.execute('SELECT assigned_to, status FROM dispatch_tasks WHERE id = ?', (task_id,)) + row = cursor.fetchone() + + if not row: + conn.close() + raise HTTPException(status_code=404, detail="Task not found") + + if row["status"] != "assigned" and row["status"] != "in_progress": + conn.close() + raise HTTPException(status_code=400, detail=f"Task status is {row['status']}, cannot complete") + + designer_name = row["assigned_to"] + + # 更新任务状态 + cursor.execute(''' + UPDATE dispatch_tasks + SET status = 'completed', completed_at = ? + WHERE id = ? + ''', (datetime.now(), task_id)) + + # 记录历史 + cursor.execute(''' + INSERT INTO dispatch_history (task_id, designer_name, action, notes) + VALUES (?, ?, 'completed', ?) + ''', (task_id, designer_name, complete_data.notes)) + + # 更新工作量 + cursor.execute(''' + UPDATE designer_workload + SET pending_tasks = pending_tasks - 1, + completed_tasks = completed_tasks + 1, + last_updated = ? + WHERE designer_name = ? + ''', (datetime.now(), designer_name)) + + conn.commit() + conn.close() + + return { + "task_id": task_id, + "status": "completed", + "completed_at": datetime.now().isoformat() + } + +@app.get("/designers/workload", tags=["设计师管理"]) +def get_designer_workload(api_key: str = Depends(verify_api_key)): + """ + 获取设计师工作量统计 + """ + conn = get_db_connection(DB_PATH) + cursor = conn.cursor() + + cursor.execute(''' + SELECT designer_name, total_tasks, completed_tasks, pending_tasks, last_updated + FROM designer_workload + ORDER BY total_tasks DESC + ''') + + workload = [] + for row in cursor.fetchall(): + workload.append({ + "designer": row["designer_name"], + "total_tasks": row["total_tasks"], + "completed_tasks": row["completed_tasks"], + "pending_tasks": row["pending_tasks"], + "last_updated": row["last_updated"] + }) + + conn.close() + + return { + "total_designers": len(workload), + "designers": workload + } + +@app.get("/dispatch/queue", tags=["派单队列"]) +def get_dispatch_queue(api_key: str = Depends(verify_api_key)): + """ + 获取派单队列(待分配任务 + 在线设计师) + + 用于轮询派单的主接口 + """ + # 获取待分配任务 + conn = get_db_connection(DB_PATH) + cursor = conn.cursor() + cursor.execute(''' + SELECT id, task_name, task_description, task_type, priority, created_at, deadline + FROM dispatch_tasks + WHERE status = 'pending' + ORDER BY priority DESC, created_at ASC + LIMIT 20 + ''') + + pending_tasks = [] + for row in cursor.fetchall(): + pending_tasks.append({ + "task_id": row["id"], + "task_name": row["task_name"], + "description": row["task_description"], + "type": row["task_type"], + "priority": row["priority"], + "created_at": row["created_at"], + "deadline": row["deadline"] + }) + conn.close() + + # 获取在线设计师 + online_designers = get_online_designers() + + return { + "timestamp": datetime.now().isoformat(), + "pending_tasks": { + "count": len(pending_tasks), + "tasks": pending_tasks + }, + "online_designers": { + "count": len(online_designers), + "designers": online_designers + }, + "suggestion": online_designers[0] if online_designers and pending_tasks else None + } + +# ========== 主程序 ========== + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8001) diff --git a/backend/dispatch_api_backup2.py b/backend/dispatch_api_backup2.py new file mode 100644 index 0000000..b241f21 --- /dev/null +++ b/backend/dispatch_api_backup2.py @@ -0,0 +1,346 @@ +from fastapi import FastAPI, HTTPException, Header, Depends +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel +from typing import Optional, List +from datetime import datetime +import sqlite3 +from pathlib import Path +import requests + +# 数据库路径 +DB_PATH = Path("/root/tuhui/backend/dispatch.db") +DESIGNER_DB_PATH = Path("/root/tuhui/backend/designer_status.db") + +# API 密钥 +API_KEY = "tuhui_dispatch_key_2026" + +# 企业微信 Webhook +WECHAT_WEBHOOK = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=cc88bdef-a13f-4d7e-bdb6-ee51b68b8205" + +app = FastAPI( + title="图汇派单系统 API", + description="专业的设计师派单管理系统", + version="1.0.0" +) + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +class TaskCreate(BaseModel): + task_name: str + description: str = "" + task_type: str = "design" + priority: int = 1 + deadline: Optional[str] = None + +class TaskAssign(BaseModel): + designer_name: str + notes: Optional[str] = "" + +class TaskComplete(BaseModel): + notes: Optional[str] = "" + +def verify_api_key(x_api_key: str = Header(..., alias="X-API-Key")): + if x_api_key != API_KEY: + raise HTTPException(status_code=401, detail="Invalid API Key") + return x_api_key + +def get_db_connection(db_path: Path): + conn = sqlite3.connect(str(db_path)) + conn.row_factory = sqlite3.Row + return conn + +def get_online_designers() -> List[str]: + conn = get_db_connection(DESIGNER_DB_PATH) + cursor = conn.cursor() + cursor.execute("SELECT real_name FROM designer_status WHERE status = 'online'") + designers = [row["real_name"] for row in cursor.fetchall()] + conn.close() + return designers + +def send_wechat_notification(message: str): + """发送企业微信通知""" + try: + requests.post(WECHAT_WEBHOOK, json={ + "msgtype": "text", + "text": {"content": message} + }, timeout=5) + print(f"✅ 通知已发送") + except Exception as e: + print(f"❌ 发送通知失败:{e}") + +@app.get("/health") +def health_check(): + return {"status": "ok", "timestamp": datetime.now().isoformat()} + +@app.get("/online/designers") +def get_online_designers_api(): + designers = get_online_designers() + return { + "online_count": len(designers), + "online_users": designers, + "update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S") + } + +@app.post("/tasks", tags=["任务管理"]) +def create_task(task: TaskCreate, api_key: str = Depends(verify_api_key)): + import uuid + task_id = str(uuid.uuid4())[:8] + + conn = get_db_connection(DB_PATH) + cursor = conn.cursor() + + cursor.execute(''' + INSERT INTO dispatch_tasks (id, task_name, task_description, task_type, priority, status, deadline) + VALUES (?, ?, ?, ?, ?, 'pending', ?) + ''', (task_id, task.task_name, task.description, task.task_type, task.priority, task.deadline)) + + conn.commit() + conn.close() + + return { + "task_id": task_id, + "task_name": task.task_name, + "status": "pending", + "created_at": datetime.now().isoformat() + } + +@app.get("/tasks/pending", tags=["任务管理"]) +def get_pending_tasks(limit: int = 10, api_key: str = Depends(verify_api_key)): + conn = get_db_connection(DB_PATH) + cursor = conn.cursor() + + cursor.execute(''' + SELECT id, task_name, task_description, task_type, priority, created_at, deadline + FROM dispatch_tasks + WHERE status = 'pending' + ORDER BY priority DESC, created_at ASC + LIMIT ? + ''', (limit,)) + + tasks = [] + for row in cursor.fetchall(): + tasks.append({ + "task_id": row["id"], + "task_name": row["task_name"], + "description": row["task_description"], + "type": row["task_type"], + "priority": row["priority"], + "created_at": row["created_at"], + "deadline": row["deadline"] + }) + + conn.close() + + return {"total": len(tasks), "tasks": tasks} + +@app.post("/tasks/{task_id}/assign", tags=["任务管理"]) +def assign_task(task_id: str, assign_data: TaskAssign, api_key: str = Depends(verify_api_key)): + conn = get_db_connection(DB_PATH) + cursor = conn.cursor() + + cursor.execute('SELECT id, task_name, status FROM dispatch_tasks WHERE id = ?', (task_id,)) + task = cursor.fetchone() + + if not task: + conn.close() + raise HTTPException(status_code=404, detail="Task not found") + + if task["status"] != "pending": + conn.close() + raise HTTPException(status_code=400, detail=f"Task status is {task['status']}, cannot assign") + + cursor.execute(''' + UPDATE dispatch_tasks + SET status = 'assigned', assigned_to = ?, assigned_at = ? + WHERE id = ? + ''', (assign_data.designer_name, datetime.now(), task_id)) + + cursor.execute(''' + INSERT INTO dispatch_history (task_id, designer_name, action, notes) + VALUES (?, ?, 'assigned', ?) + ''', (task_id, assign_data.designer_name, assign_data.notes)) + + cursor.execute(''' + INSERT OR REPLACE INTO designer_workload (designer_name, pending_tasks, total_tasks, last_updated) + VALUES ( + ?, + COALESCE((SELECT pending_tasks FROM designer_workload WHERE designer_name = ?), 0) + 1, + COALESCE((SELECT total_tasks FROM designer_workload WHERE designer_name = ?), 0) + 1, + ? + ) + ''', (assign_data.designer_name, assign_data.designer_name, assign_data.designer_name, datetime.now())) + + conn.commit() + conn.close() + + # 发送群通知 + notification = f"""📢【新任务派发】 + +🎯 任务:{task['task_name']} +👤 派给:{assign_data.designer_name} +📝 备注:{assign_data.notes or '无'} + +💪 加油,看好你哦!""" + + send_wechat_notification(notification) + + return { + "task_id": task_id, + "assigned_to": assign_data.designer_name, + "status": "assigned", + "assigned_at": datetime.now().isoformat() + } + +@app.get("/tasks/{task_id}", tags=["任务管理"]) +def get_task_status(task_id: str, api_key: str = Depends(verify_api_key)): + conn = get_db_connection(DB_PATH) + cursor = conn.cursor() + + cursor.execute(''' + SELECT id, task_name, status, assigned_to, created_at, assigned_at, completed_at, deadline + FROM dispatch_tasks + WHERE id = ? + ''', (task_id,)) + + row = cursor.fetchone() + conn.close() + + if not row: + raise HTTPException(status_code=404, detail="Task not found") + + return { + "task_id": row["id"], + "task_name": row["task_name"], + "status": row["status"], + "assigned_to": row["assigned_to"], + "created_at": row["created_at"], + "assigned_at": row["assigned_at"], + "completed_at": row["completed_at"], + "deadline": row["deadline"] + } + +@app.post("/tasks/{task_id}/complete", tags=["任务管理"]) +def complete_task(task_id: str, complete_data: TaskComplete, api_key: str = Depends(verify_api_key)): + conn = get_db_connection(DB_PATH) + cursor = conn.cursor() + + cursor.execute('SELECT assigned_to, status FROM dispatch_tasks WHERE id = ?', (task_id,)) + row = cursor.fetchone() + + if not row: + conn.close() + raise HTTPException(status_code=404, detail="Task not found") + + if row["status"] != "assigned" and row["status"] != "in_progress": + conn.close() + raise HTTPException(status_code=400, detail=f"Task status is {row['status']}, cannot complete") + + designer_name = row["assigned_to"] + + cursor.execute(''' + UPDATE dispatch_tasks + SET status = 'completed', completed_at = ? + WHERE id = ? + ''', (datetime.now(), task_id)) + + cursor.execute(''' + INSERT INTO dispatch_history (task_id, designer_name, action, notes) + VALUES (?, ?, 'completed', ?) + ''', (task_id, designer_name, complete_data.notes)) + + cursor.execute(''' + UPDATE designer_workload + SET pending_tasks = pending_tasks - 1, + completed_tasks = completed_tasks + 1, + last_updated = ? + WHERE designer_name = ? + ''', (datetime.now(), designer_name)) + + conn.commit() + conn.close() + + # 发送完成通知 + notification = f"""✅【任务完成】 + +🎯 任务:{row['task_name']} +👤 设计师:{designer_name} +📝 备注:{complete_data.notes or '无'} + +🎉 辛苦啦,棒棒哒!""" + + send_wechat_notification(notification) + + return { + "task_id": task_id, + "status": "completed", + "completed_at": datetime.now().isoformat() + } + +@app.get("/designers/workload", tags=["设计师管理"]) +def get_designer_workload(api_key: str = Depends(verify_api_key)): + conn = get_db_connection(DB_PATH) + cursor = conn.cursor() + + cursor.execute(''' + SELECT designer_name, total_tasks, completed_tasks, pending_tasks, last_updated + FROM designer_workload + ORDER BY total_tasks DESC + ''') + + workload = [] + for row in cursor.fetchall(): + workload.append({ + "designer": row["designer_name"], + "total_tasks": row["total_tasks"], + "completed_tasks": row["completed_tasks"], + "pending_tasks": row["pending_tasks"], + "last_updated": row["last_updated"] + }) + + conn.close() + + return {"total_designers": len(workload), "designers": workload} + +@app.get("/dispatch/queue", tags=["派单队列"]) +def get_dispatch_queue(api_key: str = Depends(verify_api_key)): + conn = get_db_connection(DB_PATH) + cursor = conn.cursor() + cursor.execute(''' + SELECT id, task_name, task_description, task_type, priority, created_at, deadline + FROM dispatch_tasks + WHERE status = 'pending' + ORDER BY priority DESC, created_at ASC + LIMIT 20 + ''') + + pending_tasks = [] + for row in cursor.fetchall(): + pending_tasks.append({ + "task_id": row["id"], + "task_name": row["task_name"], + "description": row["task_description"], + "type": row["task_type"], + "priority": row["priority"], + "created_at": row["created_at"], + "deadline": row["deadline"] + }) + conn.close() + + online_designers = get_online_designers() + + return { + "timestamp": datetime.now().isoformat(), + "pending_tasks": {"count": len(pending_tasks), "tasks": pending_tasks}, + "online_designers": {"count": len(online_designers), "designers": online_designers}, + "suggestion": online_designers[0] if online_designers and pending_tasks else None + } + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8005) diff --git a/backend/dispatch_system.py b/backend/dispatch_system.py new file mode 100644 index 0000000..5802737 --- /dev/null +++ b/backend/dispatch_system.py @@ -0,0 +1,264 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +设计师派单系统数据库 +存储派单记录和任务队列 +""" + +import sqlite3 +from datetime import datetime +from pathlib import Path +import uuid + +# 数据库路径 +DB_PATH = Path("/root/tuhui/backend/dispatch.db") + +def init_db(): + """初始化数据库""" + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + # 创建派单任务表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS dispatch_tasks ( + id TEXT PRIMARY KEY, + task_name TEXT NOT NULL, + task_description TEXT, + task_type TEXT DEFAULT 'design', + priority INTEGER DEFAULT 1, + status TEXT NOT NULL CHECK (status IN ('pending', 'assigned', 'in_progress', 'completed', 'cancelled')), + assigned_to TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + assigned_at DATETIME, + started_at DATETIME, + completed_at DATETIME, + deadline DATETIME, + metadata TEXT + ) + ''') + + # 创建派单历史记录表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS dispatch_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + task_id TEXT NOT NULL, + designer_name TEXT NOT NULL, + action TEXT NOT NULL, + action_time DATETIME DEFAULT CURRENT_TIMESTAMP, + notes TEXT + ) + ''') + + # 创建设计师工作量统计表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS designer_workload ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + designer_name TEXT UNIQUE NOT NULL, + total_tasks INTEGER DEFAULT 0, + completed_tasks INTEGER DEFAULT 0, + pending_tasks INTEGER DEFAULT 0, + last_updated DATETIME DEFAULT CURRENT_TIMESTAMP + ) + '''); + + conn.commit() + conn.close() + print("✅ 派单数据库初始化完成") + +def create_task(task_name: str, description: str = "", task_type: str = "design", priority: int = 1, deadline: str = None) -> str: + """创建派单任务""" + task_id = str(uuid.uuid4())[:8] + + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + cursor.execute(''' + INSERT INTO dispatch_tasks (id, task_name, task_description, task_type, priority, status, deadline) + VALUES (?, ?, ?, ?, ?, 'pending', ?) + ''', (task_id, task_name, description, task_type, priority, deadline)) + + conn.commit() + conn.close() + + print(f"✅ 已创建任务:{task_id} - {task_name}") + return task_id + +def assign_task(task_id: str, designer_name: str): + """分配任务给设计师""" + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + # 更新任务状态 + cursor.execute(''' + UPDATE dispatch_tasks + SET status = 'assigned', assigned_to = ?, assigned_at = ? + WHERE id = ? + ''', (designer_name, datetime.now(), task_id)) + + # 记录历史 + cursor.execute(''' + INSERT INTO dispatch_history (task_id, designer_name, action, notes) + VALUES (?, ?, 'assigned', ?) + ''', (task_id, designer_name, f"任务分配给 {designer_name}")) + + # 更新工作量 + cursor.execute(''' + INSERT OR REPLACE INTO designer_workload (designer_name, pending_tasks, total_tasks, last_updated) + VALUES ( + ?, + COALESCE((SELECT pending_tasks FROM designer_workload WHERE designer_name = ?), 0) + 1, + COALESCE((SELECT total_tasks FROM designer_workload WHERE designer_name = ?), 0) + 1, + ? + ) + ''', (designer_name, designer_name, designer_name, datetime.now())) + + conn.commit() + conn.close() + + print(f"✅ 任务 {task_id} 已分配给 {designer_name}") + +def get_pending_tasks(limit: int = 10) -> list: + """获取待分配任务""" + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + cursor.execute(''' + SELECT id, task_name, task_description, task_type, priority, created_at, deadline + FROM dispatch_tasks + WHERE status = 'pending' + ORDER BY priority DESC, created_at ASC + LIMIT ? + ''', (limit,)) + + tasks = cursor.fetchall() + conn.close() + + return [ + { + "id": row[0], + "task_name": row[1], + "description": row[2], + "type": row[3], + "priority": row[4], + "created_at": row[5], + "deadline": row[6] + } + for row in tasks + ] + +def get_task_status(task_id: str) -> dict: + """查询任务状态""" + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + cursor.execute(''' + SELECT id, task_name, status, assigned_to, created_at, assigned_at, completed_at + FROM dispatch_tasks + WHERE id = ? + ''', (task_id,)) + + row = cursor.fetchone() + conn.close() + + if row: + return { + "task_id": row[0], + "task_name": row[1], + "status": row[2], + "assigned_to": row[3], + "created_at": row[4], + "assigned_at": row[5], + "completed_at": row[6] + } + return None + +def complete_task(task_id: str, notes: str = ""): + """完成任务""" + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + # 获取任务信息 + cursor.execute('SELECT assigned_to FROM dispatch_tasks WHERE id = ?', (task_id,)) + row = cursor.fetchone() + + if row and row[0]: + designer_name = row[0] + + # 更新任务状态 + cursor.execute(''' + UPDATE dispatch_tasks + SET status = 'completed', completed_at = ? + WHERE id = ? + ''', (datetime.now(), task_id)) + + # 记录历史 + cursor.execute(''' + INSERT INTO dispatch_history (task_id, designer_name, action, notes) + VALUES (?, ?, 'completed', ?) + ''', (task_id, designer_name, notes)) + + # 更新工作量 + cursor.execute(''' + UPDATE designer_workload + SET pending_tasks = pending_tasks - 1, + completed_tasks = completed_tasks + 1, + last_updated = ? + WHERE designer_name = ? + ''', (datetime.now(), designer_name)) + + conn.commit() + conn.close() + print(f"✅ 任务 {task_id} 已完成") + +def get_designer_workload() -> list: + """获取设计师工作量统计""" + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + cursor.execute(''' + SELECT designer_name, total_tasks, completed_tasks, pending_tasks, last_updated + FROM designer_workload + ORDER BY total_tasks DESC + ''') + + workload = cursor.fetchall() + conn.close() + + return [ + { + "designer": row[0], + "total": row[1], + "completed": row[2], + "pending": row[3], + "last_updated": row[4] + } + for row in workload + ] + +if __name__ == "__main__": + # 初始化数据库 + init_db() + + # 创建测试任务 + task1 = create_task("海报设计", "设计一个产品宣传海报", "design", priority=2) + task2 = create_task("Logo 设计", "设计公司 Logo", "design", priority=3) + task3 = create_task("详情页设计", "电商产品详情页", "design", priority=1) + + # 分配任务 + assign_task(task1, "橘子") + assign_task(task2, "婷婷") + + # 查询待分配任务 + print("\n📋 待分配任务:") + for task in get_pending_tasks(): + print(f" {task['id']} - {task['task_name']} (优先级:{task['priority']})") + + # 查询任务状态 + print("\n📊 任务状态:") + status = get_task_status(task1) + print(f" {status}") + + # 查询工作量 + print("\n💼 设计师工作量:") + for w in get_designer_workload(): + print(f" {w['designer']}: 总{w['total']} 完成{w['completed']} 待处理{w['pending']}") diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml new file mode 100644 index 0000000..b98a992 --- /dev/null +++ b/backend/docker-compose.yml @@ -0,0 +1,54 @@ +version: '3.8' + +networks: + aishej_dev_network: + driver: bridge + +services: + mysql: + image: mysql:8.0 + container_name: aishej_mysql_dev + restart: always + networks: + - aishej_dev_network + environment: + MYSQL_ROOT_PASSWORD: rootpassword123 + MYSQL_DATABASE: aishej_dev + MYSQL_USER: aishej_dev + MYSQL_PASSWORD: aishej_dev_123 + ports: + - "3307:3306" # 使用3307端口避免冲突 + volumes: + - mysql_data:/var/lib/mysql + - ./init_mysql.sql:/docker-entrypoint-initdb.d/init.sql + command: --default-authentication-plugin=mysql_native_password + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + timeout: 5s + retries: 10 + + api: + build: . + container_name: aishej_api_dev + restart: always + networks: + - aishej_dev_network + ports: + - "8001:8001" # 使用8001端口避免冲突 + volumes: + - ./app:/app/app # 挂载整个app目录 + - ./uploads:/app/uploads + - ./seed_data.py:/app/seed_data.py + - ./download_test_images.py:/app/download_test_images.py + environment: + DATABASE_URL: mysql+pymysql://aishej_dev:aishej_dev_123@mysql:3306/aishej_dev + API_PORT: "8001" + PYTHONUNBUFFERED: "1" # Python不缓冲输出 + depends_on: + mysql: + condition: service_healthy + command: sh -c "sleep 10 && uvicorn app.main:app --host 0.0.0.0 --port 8001 --reload" + +volumes: + mysql_data: + name: aishej_dev_mysql_data \ No newline at end of file diff --git a/backend/docker-start.bat b/backend/docker-start.bat new file mode 100755 index 0000000..2b27c44 --- /dev/null +++ b/backend/docker-start.bat @@ -0,0 +1,47 @@ +@echo off +chcp 65001 >nul +echo ======================================== +echo 爱设计 Docker 环境启动 +echo ======================================== +echo. +echo [*] 使用端口: +echo - MySQL: 3307 +echo - API: 8001 +echo. +echo [1] 停止并清理旧容器... +docker-compose down + +echo. +echo [2] 构建并启动服务... +docker-compose up -d --build + +echo. +echo [3] 等待MySQL启动... +timeout /t 15 /nobreak >nul + +echo. +echo [4] 初始化数据库(创建表)... +docker exec aishej_api_dev python -c "from app.core.database import Base, engine; Base.metadata.create_all(bind=engine); print('Tables created!')" + +echo. +echo [5] 添加测试数据... +docker exec aishej_api_dev python seed_data.py + +echo. +echo [6] 下载测试图片... +docker exec aishej_api_dev python download_test_images.py + +echo. +echo ======================================== +echo 启动完成! +echo ======================================== +echo. +echo 服务地址: +echo - API: http://localhost:8001 +echo - API文档: http://localhost:8001/docs +echo - MySQL: localhost:3307 +echo. +echo 查看日志: docker-compose logs -f api +echo 停止服务: docker-compose down +echo. +pause diff --git a/backend/docker-start.sh b/backend/docker-start.sh new file mode 100644 index 0000000..3951760 --- /dev/null +++ b/backend/docker-start.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +echo "========================================" +echo " 爱设计 Docker 环境启动" +echo "========================================" +echo "" +echo "[*] 使用端口:" +echo " - MySQL: 3307" +echo " - API: 8001" +echo "" + +echo "[1] 停止并清理旧容器..." +docker-compose down + +echo "" +echo "[2] 构建并启动服务..." +docker-compose up -d --build + +echo "" +echo "[3] 等待MySQL启动..." +sleep 15 + +echo "" +echo "[4] 初始化数据库(创建表)..." +docker exec aishej_api_dev python -c "from app.core.database import Base, engine; Base.metadata.create_all(bind=engine); print('Tables created!')" + +echo "" +echo "[5] 添加测试数据..." +docker exec aishej_api_dev python seed_data.py + +echo "" +echo "[6] 下载测试图片..." +docker exec aishej_api_dev python download_test_images.py + +echo "" +echo "========================================" +echo " 启动完成!" +echo "========================================" +echo "" +echo "服务地址:" +echo " - API: http://localhost:8001" +echo " - API文档: http://localhost:8001/docs" +echo " - MySQL: localhost:3307" +echo "" +echo "查看日志: docker-compose logs -f api" +echo "停止服务: docker-compose down" +echo "" diff --git a/backend/download_test_images.py b/backend/download_test_images.py new file mode 100644 index 0000000..cc88de1 --- /dev/null +++ b/backend/download_test_images.py @@ -0,0 +1,106 @@ +"""下载测试图片到 uploads 目录""" + +import os +import requests +from pathlib import Path + +def download_test_images(): + """从 Picsum 下载测试图片""" + + # 确保目录存在 + base_dir = Path("uploads") + dirs = ["thumbnail", "watermarked", "original"] + + for dir_name in dirs: + (base_dir / dir_name).mkdir(parents=True, exist_ok=True) + + # 下载图片(与 seed_data.py 中的作品ID对应) + # 使用不同的 seed 值获得不同的图片 + images = [ + {"id": 1, "seed": 134, "title": "活动海报"}, + {"id": 2, "seed": 573, "title": "马年招聘海报"}, + {"id": 3, "seed": 60, "title": "新中式主视觉"}, + {"id": 4, "seed": 237, "title": "品牌设计"}, + {"id": 5, "seed": 456, "title": "电商Banner"}, + {"id": 6, "seed": 789, "title": "节日海报"}, + {"id": 7, "seed": 321, "title": "UI设计"}, + {"id": 8, "seed": 654, "title": "插画作品"}, + {"id": 9, "seed": 987, "title": "摄影作品"}, + {"id": 10, "seed": 147, "title": "Logo设计"}, + {"id": 11, "seed": 258, "title": "包装设计"}, + {"id": 12, "seed": 369, "title": "名片设计"}, + {"id": 13, "seed": 741, "title": "宣传单设计"}, + {"id": 14, "seed": 852, "title": "画册设计"}, + {"id": 15, "seed": 963, "title": "展板设计"}, + ] + + print("[*] 开始下载测试图片...\n") + + for img in images: + img_id = img["id"] + seed = img["seed"] + title = img["title"] + + print(f"[+] 下载 [{title}] ...") + + # 下载缩略图 (400x300) + thumbnail_url = f"https://picsum.photos/seed/{seed}/400/300" + download_image(thumbnail_url, base_dir / "thumbnail" / f"{img_id}.jpg") + + # 下载水印图 (800x600) + watermarked_url = f"https://picsum.photos/seed/{seed}/800/600" + download_image(watermarked_url, base_dir / "watermarked" / f"{img_id}.jpg") + + # 下载原图 (1920x1080) + original_url = f"https://picsum.photos/seed/{seed}/1920/1080" + download_image(original_url, base_dir / "original" / f"{img_id}.jpg") + + print(f" [OK] {title} 下载完成\n") + + print("[*] 所有测试图片下载完成!") + print(f"\n图片保存路径: {base_dir.absolute()}") + +def download_image(url, save_path): + """下载单个图片""" + try: + # 设置超时时间,避免卡住 + response = requests.get(url, timeout=30, allow_redirects=True) + response.raise_for_status() + + # 保存图片 + with open(save_path, 'wb') as f: + f.write(response.content) + + # 显示文件大小 + size_kb = len(response.content) / 1024 + print(f" |- {save_path.name}: {size_kb:.1f} KB") + + except Exception as e: + print(f" [ERROR] 下载失败: {e}") + +def clear_images(): + """清空所有测试图片""" + base_dir = Path("uploads") + dirs = ["thumbnail", "watermarked", "original"] + + print("[*] 清空测试图片...\n") + + count = 0 + for dir_name in dirs: + dir_path = base_dir / dir_name + if dir_path.exists(): + for file in dir_path.glob("*.jpg"): + file.unlink() + count += 1 + print(f" 删除: {file}") + + print(f"\n[OK] 已删除 {count} 个图片文件") + +if __name__ == "__main__": + import sys + + # 检查命令行参数 + if len(sys.argv) > 1 and sys.argv[1] == "clear": + clear_images() + else: + download_test_images() diff --git a/backend/env.example b/backend/env.example new file mode 100644 index 0000000..fe878a8 --- /dev/null +++ b/backend/env.example @@ -0,0 +1,22 @@ +# 数据库配置 +DATABASE_URL=mysql+pymysql://aishej:aishej123@mysql:3306/aishej + +# JWT 配置 +SECRET_KEY=change-this-to-a-random-secret-key-in-production +ALGORITHM=HS256 +ACCESS_TOKEN_EXPIRE_MINUTES=1440 + +# 文件上传配置 +UPLOAD_DIR=./uploads +MAX_FILE_SIZE=52428800 + +# 水印配置 +WATERMARK_TEXT=爱设计 AiSheji.com +WATERMARK_OPACITY=128 + +# 缩略图配置 +THUMBNAIL_SIZE=400 +THUMBNAIL_QUALITY=85 + +# CORS 配置 +ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000 diff --git a/backend/init_mysql.sql b/backend/init_mysql.sql new file mode 100644 index 0000000..807485a --- /dev/null +++ b/backend/init_mysql.sql @@ -0,0 +1,11 @@ +-- 初始化MySQL数据库 +-- 设置字符集 +SET NAMES utf8mb4; +SET CHARACTER SET utf8mb4; + +-- 使用数据库 +USE aishej_dev; + +-- 数据库表会由SQLAlchemy自动创建 +-- 这里只是设置字符集 +ALTER DATABASE aishej_dev CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci; diff --git a/backend/instant_dispatch.py b/backend/instant_dispatch.py new file mode 100644 index 0000000..033ba6d --- /dev/null +++ b/backend/instant_dispatch.py @@ -0,0 +1,138 @@ +from fastapi import FastAPI, HTTPException, Header +import sqlite3 +from pathlib import Path +from datetime import datetime +import requests +import time + +app = FastAPI(title="即时派单 API") + +API_KEY = "tuhui_dispatch_key_2026" +DESIGNER_DB_PATH = Path("/root/tuhui/backend/designer_status.db") +DISPATCH_DB_PATH = Path("/root/tuhui/backend/dispatch.db") +WECHAT_WEBHOOK = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=cc88bdef-a13f-4d7e-bdb6-ee51b68b8205" + +@app.get("/health") +def health(): + return {"status": "ok", "timestamp": datetime.now().isoformat()} + +@app.get("/online") +def get_online_designers(x_api_key: str = Header(None)): + """查询当前在线设计师""" + if x_api_key != API_KEY: + raise HTTPException(status_code=401, detail="Invalid API Key") + + conn = sqlite3.connect(str(DESIGNER_DB_PATH), timeout=30) + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + + cursor.execute("SELECT real_name, last_seen FROM designer_status WHERE status='online' ORDER BY last_seen DESC") + designers = [row["real_name"] for row in cursor.fetchall()] + + conn.close() + + return { + "count": len(designers), + "online": designers, + "timestamp": datetime.now().isoformat() + } + +@app.get("/assign") +def assign_task(x_api_key: str = Header(None)): + """ + 即时派单接口 - 轮询均匀分配版 + 1. 查询在线设计师 + 2. 统计每个设计师的已分配任务数 + 3. 选择任务最少的(轮询) + 4. 发送企业微信通知 + 5. 返回派单结果 + """ + if x_api_key != API_KEY: + raise HTTPException(status_code=401, detail="Invalid API Key") + + try: + # 1. 查询在线设计师 + conn = sqlite3.connect(str(DESIGNER_DB_PATH), timeout=30) + dispatch_conn = sqlite3.connect(str(DISPATCH_DB_PATH), timeout=30) + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + dispatch_cursor = dispatch_conn.cursor() + + cursor.execute(""" + SELECT real_name FROM designer_status + WHERE status = 'online' + ORDER BY last_seen DESC + """) + online_designers = [row["real_name"] for row in cursor.fetchall()] + + if not online_designers: + conn.close() + dispatch_conn.close() + raise HTTPException(status_code=400, detail="暂无在线设计师") + + # 2. 统计每个在线设计师的已分配任务数(今天) + designer_workload = {} + for designer in online_designers: + dispatch_cursor.execute(''' + SELECT COUNT(*) as count FROM dispatch_tasks + WHERE assigned_to = ? + AND date(assigned_at) = date('now') + ''', (designer,)) + count = dispatch_cursor.fetchone()[0] + designer_workload[designer] = count + + # 3. 选择任务最少的(轮询均匀分配) + selected_designer = min(designer_workload, key=designer_workload.get) + min_tasks = designer_workload[selected_designer] + + # 4. 创建任务 + task_id = f"task_{int(datetime.now().timestamp())}" + + dispatch_cursor.execute(''' + INSERT INTO dispatch_tasks (id, task_name, task_description, task_type, priority, status, assigned_to, assigned_at) + VALUES (?, ?, ?, ?, ?, 'assigned', ?, ?) + ''', (task_id, "临时任务", "即时分配", "design", 1, selected_designer, datetime.now())) + + dispatch_conn.commit() + + # 5. 发送企业微信通知 + message = f"""📋 新任务分配 + +👤 设计师:{selected_designer} +📊 今日已分配:{min_tasks + 1} 个任务 +📝 任务:临时任务 +⏰ 时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} + +请及时处理!""" + + try: + requests.post(WECHAT_WEBHOOK, json={ + "msgtype": "markdown", + "markdown": {"content": message} + }, timeout=10) + notification_sent = True + except: + notification_sent = False + + conn.close() + dispatch_conn.close() + + return { + "success": True, + "task_id": task_id, + "assigned_to": selected_designer, + "workload": designer_workload, + "online_count": len(online_designers), + "notification_sent": notification_sent, + "timestamp": datetime.now().isoformat() + } + + except sqlite3.OperationalError as e: + if "locked" in str(e): + time.sleep(0.5) # 等待 0.5 秒重试 + return assign_task(x_api_key) + raise + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8006) diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..639f85c --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,14 @@ +fastapi==0.109.0 +uvicorn[standard]==0.27.0 +sqlalchemy==2.0.25 +pymysql==1.1.0 +python-jose[cryptography]==3.3.0 +passlib[bcrypt]==1.7.4 +bcrypt==4.1.2 +python-multipart==0.0.6 +pillow==10.2.0 +python-dotenv==1.0.0 +pydantic==2.5.3 +pydantic-settings==2.1.0 +alembic==1.13.1 +aiohttp==3.9.1 diff --git a/backend/seed_data.py b/backend/seed_data.py new file mode 100644 index 0000000..95b595b --- /dev/null +++ b/backend/seed_data.py @@ -0,0 +1,239 @@ +"""添加测试数据""" + +from sqlalchemy.orm import Session +from app.core.database import SessionLocal +from app.core.security import get_password_hash +from app.models.user import User +from app.models.work import Work + +def seed_data(): + """添加测试数据""" + db = SessionLocal() + + try: + # 创建测试用户(使用手机号) + test_user = User( + phone="13800138000", + password_hash=get_password_hash("123456"), + nickname="测试用户", + balance=100.0 # 给测试用户100元余额 + ) + db.add(test_user) + + # 创建测试作品 + test_works = [ + Work( + title="活动海报设计", + category="活动", + designer="呵呵", + level=3, + level_text="设计师", + price=9.9, + thumbnail_image="/uploads/thumbnail/1.jpg", + watermarked_image="/uploads/watermarked/1.jpg", + original_image="/uploads/original/1.jpg", + description="高品质活动海报设计,适合各类活动宣传", + tags='["海报", "活动", "设计"]' + ), + Work( + title="马年招聘海报", + category="活动", + designer="六十六号屯", + level=4, + level_text="资深设计师", + price=15.9, + thumbnail_image="/uploads/thumbnail/2.jpg", + watermarked_image="/uploads/watermarked/2.jpg", + original_image="/uploads/original/2.jpg", + description="马年主题招聘海报,创意十足", + tags='["招聘", "马年", "海报"]' + ), + Work( + title="新中式主视觉", + category="中式", + designer="小鱼是设计", + level=4, + level_text="资深设计师", + price=19.9, + thumbnail_image="/uploads/thumbnail/3.jpg", + watermarked_image="/uploads/watermarked/3.jpg", + original_image="/uploads/original/3.jpg", + description="新中式风格主视觉设计,高端大气", + tags='["中式", "主视觉", "设计"]' + ), + Work( + title="品牌VI设计", + category="直播", + designer="设计狮", + level=5, + level_text="首席设计师", + price=29.9, + thumbnail_image="/uploads/thumbnail/4.jpg", + watermarked_image="/uploads/watermarked/4.jpg", + original_image="/uploads/original/4.jpg", + description="完整品牌VI视觉识别系统设计", + tags='["品牌", "VI", "Logo"]' + ), + Work( + title="电商促销Banner", + category="活动", + designer="小美设计", + level=3, + level_text="设计师", + price=12.9, + thumbnail_image="/uploads/thumbnail/5.jpg", + watermarked_image="/uploads/watermarked/5.jpg", + original_image="/uploads/original/5.jpg", + description="电商平台促销活动Banner设计", + tags='["电商", "Banner", "促销"]' + ), + Work( + title="春节主题海报", + category="周年庆", + designer="创意工作室", + level=4, + level_text="资深设计师", + price=18.9, + thumbnail_image="/uploads/thumbnail/6.jpg", + watermarked_image="/uploads/watermarked/6.jpg", + original_image="/uploads/original/6.jpg", + description="春节节日氛围海报设计", + tags='["春节", "节日", "海报"]' + ), + Work( + title="移动端UI界面", + category="直播", + designer="UI设计师", + level=4, + level_text="资深设计师", + price=25.9, + thumbnail_image="/uploads/thumbnail/7.jpg", + watermarked_image="/uploads/watermarked/7.jpg", + original_image="/uploads/original/7.jpg", + description="简洁大气的移动端应用界面设计", + tags='["UI", "移动端", "界面"]' + ), + Work( + title="扁平风格插画", + category="活动", + designer="插画师小陈", + level=5, + level_text="首席设计师", + price=22.9, + thumbnail_image="/uploads/thumbnail/8.jpg", + watermarked_image="/uploads/watermarked/8.jpg", + original_image="/uploads/original/8.jpg", + description="扁平风格商业插画设计", + tags='["插画", "扁平", "商业"]' + ), + Work( + title="风光摄影作品", + category="中式", + designer="摄影师老王", + level=6, + level_text="大师", + price=39.9, + thumbnail_image="/uploads/thumbnail/9.jpg", + watermarked_image="/uploads/watermarked/9.jpg", + original_image="/uploads/original/9.jpg", + description="自然风光摄影精品", + tags='["摄影", "风光", "自然"]' + ), + Work( + title="简约Logo设计", + category="直播", + designer="品牌设计师", + level=5, + level_text="首席设计师", + price=35.9, + thumbnail_image="/uploads/thumbnail/10.jpg", + watermarked_image="/uploads/watermarked/10.jpg", + original_image="/uploads/original/10.jpg", + description="简约现代风格Logo设计", + tags='["Logo", "简约", "品牌"]' + ), + Work( + title="产品包装设计", + category="活动", + designer="包装设计工作室", + level=4, + level_text="资深设计师", + price=28.9, + thumbnail_image="/uploads/thumbnail/11.jpg", + watermarked_image="/uploads/watermarked/11.jpg", + original_image="/uploads/original/11.jpg", + description="创意产品包装盒设计", + tags='["包装", "产品", "创意"]' + ), + Work( + title="商务名片设计", + category="直播", + designer="名片专家", + level=3, + level_text="设计师", + price=8.9, + thumbnail_image="/uploads/thumbnail/12.jpg", + watermarked_image="/uploads/watermarked/12.jpg", + original_image="/uploads/original/12.jpg", + description="高端商务名片设计", + tags='["名片", "商务", "印刷"]' + ), + Work( + title="宣传单页设计", + category="活动", + designer="平面设计师", + level=3, + level_text="设计师", + price=11.9, + thumbnail_image="/uploads/thumbnail/13.jpg", + watermarked_image="/uploads/watermarked/13.jpg", + original_image="/uploads/original/13.jpg", + description="活动宣传单页设计", + tags='["宣传单", "活动", "印刷"]' + ), + Work( + title="企业画册设计", + category="中式", + designer="画册设计团队", + level=5, + level_text="首席设计师", + price=45.9, + thumbnail_image="/uploads/thumbnail/14.jpg", + watermarked_image="/uploads/watermarked/14.jpg", + original_image="/uploads/original/14.jpg", + description="企业宣传画册整体设计", + tags='["画册", "企业", "宣传"]' + ), + Work( + title="展板海报设计", + category="周年庆", + designer="展览设计师", + level=4, + level_text="资深设计师", + price=24.9, + thumbnail_image="/uploads/thumbnail/15.jpg", + watermarked_image="/uploads/watermarked/15.jpg", + original_image="/uploads/original/15.jpg", + description="展览展示展板设计", + tags='["展板", "展览", "海报"]' + ), + ] + + for work in test_works: + db.add(work) + + db.commit() + print("✅ 测试数据添加成功!") + print("\n测试账号:") + print("手机号: 13800138000") + print("密码: 123456") + print("余额: 100.0 元") + + except Exception as e: + print(f"❌ 错误: {e}") + db.rollback() + finally: + db.close() + +if __name__ == "__main__": + seed_data() diff --git a/backend/simple_dispatch_poller.py b/backend/simple_dispatch_poller.py new file mode 100755 index 0000000..29a9ceb --- /dev/null +++ b/backend/simple_dispatch_poller.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +简单轮询派单系统(数据库版) +- 自动轮询派单队列 +- 轮流分配设计师(数据库持久化) +- 自动重试机制 +""" + +import requests +import sqlite3 +import time +from datetime import datetime +from pathlib import Path + +# 配置 +API_BASE = "http://1.12.50.92:8005" +API_KEY = "tuhui_dispatch_key_2026" +HEADERS = {"X-API-Key": API_KEY} +DB_PATH = Path("/root/tuhui/backend/dispatch.db") + +# 轮询间隔(秒) +POLL_INTERVAL = 10 +MAX_RETRIES = 3 + +def log(message: str): + """打印日志""" + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + print(f"[{timestamp}] {message}") + +def init_db(): + """初始化数据库表""" + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + # 创建轮询历史表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS dispatch_rotation ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + designer_name TEXT NOT NULL, + task_id TEXT NOT NULL, + assigned_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + ''') + + conn.commit() + conn.close() + +def get_recent_assignments(limit: int = 20) -> list: + """获取最近分配的设计师(数据库)""" + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + cursor.execute(''' + SELECT designer_name FROM dispatch_rotation + ORDER BY assigned_at DESC + LIMIT ? + ''', (limit,)) + + designers = [row[0] for row in cursor.fetchall()] + conn.close() + + return designers + +def record_assignment(designer_name: str, task_id: str): + """记录分配历史(数据库)""" + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + cursor.execute(''' + INSERT INTO dispatch_rotation (designer_name, task_id) + VALUES (?, ?) + ''', (designer_name, task_id)) + + conn.commit() + conn.close() + +def get_dispatch_queue() -> dict: + """获取派单队列""" + try: + response = requests.get(f"{API_BASE}/dispatch/queue", headers=HEADERS, timeout=5) + if response.status_code == 200: + return response.json() + except Exception as e: + log(f"❌ 获取队列失败:{e}") + return None + +def select_designer(online_designers: list) -> str: + """ + 轮流选择设计师 + 找最近没被分配过的设计师 + """ + if not online_designers: + return None + + if len(online_designers) == 1: + return online_designers[0] + + # 获取最近分配记录 + recent = get_recent_assignments(limit=20) + + # 找最近没被分配的 + for designer in online_designers: + if designer not in recent: + return designer + + # 如果都被分配过了,选第一个 + return online_designers[0] + +def assign_task(task_id: str, designer: str) -> bool: + """分配任务""" + for attempt in range(MAX_RETRIES): + try: + response = requests.post( + f"{API_BASE}/tasks/{task_id}/assign", + headers=HEADERS, + json={ + "designer_name": designer, + "notes": "系统自动派单" + }, + timeout=5 + ) + + if response.status_code == 200: + # 记录到数据库 + record_assignment(designer, task_id) + return True + else: + log(f"❌ 派单失败:{response.text}") + + except Exception as e: + log(f"❌ 网络错误:{e}") + + if attempt < MAX_RETRIES - 1: + time.sleep(2) + + return False + +def poll_and_dispatch(): + """主轮询函数""" + # 初始化数据库 + init_db() + + log("🚀 轮询派单系统启动(数据库版)") + log(f"轮询间隔:{POLL_INTERVAL}秒") + log(f"数据库:{DB_PATH}") + + while True: + try: + # 获取派单队列 + queue = get_dispatch_queue() + + if not queue: + time.sleep(POLL_INTERVAL) + continue + + pending_count = queue["pending_tasks"]["count"] + online_count = queue["online_designers"]["count"] + tasks = queue["pending_tasks"]["tasks"] + designers = queue["online_designers"]["designers"] + + # 没任务 + if pending_count == 0: + log("📭 暂无待分配任务") + time.sleep(POLL_INTERVAL) + continue + + # 没人在线 + if online_count == 0: + log("😴 暂无设计师在线") + time.sleep(POLL_INTERVAL) + continue + + log(f"📋 待分配:{pending_count} 个 | 👥 在线:{online_count} 人 - {', '.join(designers)}") + + # 分配任务 + assigned = 0 + for task in tasks: + designer = select_designer(designers) + + if designer: + success = assign_task(task["task_id"], designer) + if success: + log(f"✅ {task['task_name']} → {designer}") + assigned += 1 + time.sleep(0.5) + else: + break + + log(f"📊 本轮分配:{assigned}/{pending_count} 个") + + except Exception as e: + log(f"❌ 异常:{e}") + + time.sleep(POLL_INTERVAL) + +if __name__ == "__main__": + try: + poll_and_dispatch() + except KeyboardInterrupt: + log("👋 已停止") diff --git a/backend/smart_dispatch_poller.py b/backend/smart_dispatch_poller.py new file mode 100755 index 0000000..5d6b15b --- /dev/null +++ b/backend/smart_dispatch_poller.py @@ -0,0 +1,214 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +智能轮询派单系统 +- 自动轮询派单队列 +- 负载均衡:轮流分配 + 工作量感知 +- 自动重试机制 +- 详细日志记录 +""" + +import requests +import time +import json +from datetime import datetime +from collections import deque + +# 配置 +API_BASE = "http://1.12.50.92:8005" +API_KEY = "tuhui_dispatch_key_2026" +HEADERS = {"X-API-Key": API_KEY} + +# 轮询间隔(秒) +POLL_INTERVAL = 10 +MAX_RETRIES = 3 +RETRY_DELAY = 3 + +# 负载均衡:记录最近分配的设计师(轮询分配) +recent_assignments = deque(maxlen=10) + +def log(message: str, level: str = "INFO"): + """打印日志""" + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + print(f"[{timestamp}] [{level}] {message}") + +def get_online_designers() -> list: + """获取在线设计师""" + try: + response = requests.get(f"{API_BASE}/online/designers", headers=HEADERS, timeout=5) + if response.status_code == 200: + data = response.json() + return data["online_users"] + except Exception as e: + log(f"获取在线设计师失败:{e}", "ERROR") + return [] + +def get_pending_tasks(limit: int = 10) -> list: + """获取待分配任务""" + try: + response = requests.get(f"{API_BASE}/tasks/pending", headers=HEADERS, timeout=5, params={"limit": limit}) + if response.status_code == 200: + data = response.json() + return data["tasks"] + except Exception as e: + log(f"获取待分配任务失败:{e}", "ERROR") + return [] + +def get_designer_workload() -> dict: + """获取设计师工作量""" + try: + response = requests.get(f"{API_BASE}/designers/workload", headers=HEADERS, timeout=5) + if response.status_code == 200: + data = response.json() + # 转换为字典:设计师 -> 待处理任务数 + return {d["designer"]: d["pending_tasks"] for d in data["designers"]} + except Exception as e: + log(f"获取工作量失败:{e}", "ERROR") + return {} + +def select_designer_smart(online_designers: list) -> str: + """ + 智能选择设计师(负载均衡) + + 策略: + 1. 优先选最近没被分配的设计师(轮询) + 2. 如果都有任务,选工作量最少的 + 3. 如果都没数据,随机选 + """ + if not online_designers: + return None + + if len(online_designers) == 1: + return online_designers[0] + + # 策略 1:找最近没被分配的 + for designer in online_designers: + if designer not in recent_assignments: + log(f"选择设计师(轮询):{designer}") + return designer + + # 策略 2:找工作量最少的 + workload = get_designer_workload() + min_workload = float('inf') + selected = None + + for designer in online_designers: + pending = workload.get(designer, 0) + if pending < min_workload: + min_workload = pending + selected = designer + + if selected: + log(f"选择设计师(工作量最少:{min_workload}):{selected}") + return selected + + # 策略 3:随机选第一个 + log(f"选择设计师(默认):{online_designers[0]}") + return online_designers[0] + +def assign_task_with_retry(task_id: str, designer: str, max_retries: int = MAX_RETRIES) -> bool: + """带重试的分配任务""" + for attempt in range(max_retries): + try: + response = requests.post( + f"{API_BASE}/tasks/{task_id}/assign", + headers=HEADERS, + json={ + "designer_name": designer, + "notes": "系统自动派单" + }, + timeout=5 + ) + + if response.status_code == 200: + log(f"✅ 任务 {task_id} 已分配给 {designer}", "SUCCESS") + # 记录分配历史 + recent_assignments.append(designer) + return True + else: + log(f"派单失败 (尝试 {attempt+1}/{max_retries}): {response.text}", "ERROR") + + except Exception as e: + log(f"网络错误 (尝试 {attempt+1}/{max_retries}): {e}", "ERROR") + + if attempt < max_retries - 1: + time.sleep(RETRY_DELAY) + + return False + +def poll_and_dispatch(): + """主轮询函数""" + log("🚀 智能轮询派单系统启动", "START") + log(f"轮询间隔:{POLL_INTERVAL}秒") + log(f"API 地址:{API_BASE}") + + consecutive_errors = 0 + max_consecutive_errors = 10 + + while True: + try: + # 1. 获取待分配任务 + tasks = get_pending_tasks(limit=20) + + if not tasks: + log("📭 暂无待分配任务") + consecutive_errors = 0 + time.sleep(POLL_INTERVAL) + continue + + log(f"📋 待分配任务:{len(tasks)} 个") + + # 2. 获取在线设计师 + online_designers = get_online_designers() + + if not online_designers: + log("😴 暂无设计师在线") + consecutive_errors = 0 + time.sleep(POLL_INTERVAL) + continue + + log(f"👥 在线设计师:{len(online_designers)} 人 - {', '.join(online_designers)}") + + # 3. 按优先级排序任务 + tasks.sort(key=lambda x: x["priority"], reverse=True) + + # 4. 智能分配 + assigned_count = 0 + for task in tasks: + designer = select_designer_smart(online_designers) + + if designer: + success = assign_task_with_retry(task["task_id"], designer) + if success: + assigned_count += 1 + # 短暂等待,避免并发冲突 + time.sleep(0.5) + else: + log(f"❌ 任务 {task['task_id']} 分配失败", "ERROR") + break # 停止分配,下次轮询再试 + else: + log("⚠️ 无法选择设计师", "ERROR") + break + + log(f"📊 本轮分配:{assigned_count}/{len(tasks)} 个任务", "SUCCESS") + consecutive_errors = 0 + + except Exception as e: + log(f"❌ 轮询异常:{e}", "ERROR") + consecutive_errors += 1 + + if consecutive_errors >= max_consecutive_errors: + log(f"⚠️ 连续 {consecutive_errors} 次错误,等待 30 秒...", "WARNING") + time.sleep(30) + consecutive_errors = 0 + else: + time.sleep(POLL_INTERVAL) + + # 正常轮询间隔 + time.sleep(POLL_INTERVAL) + +if __name__ == "__main__": + try: + poll_and_dispatch() + except KeyboardInterrupt: + log("👋 轮询派单系统已停止", "INFO") diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..2e19869 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,17 @@ +version: '3.8' + +services: + api: + build: . + container_name: tuhui_backend + restart: always + ports: + - "8002:8002" + volumes: + - ./backend/app:/app/app + - /var/www/tuhui_uploads:/app/uploads + environment: + DATABASE_URL: mysql+pymysql://tuhui_user:tuhui_123456@host.docker.internal:3306/tuhui + API_PORT: "8002" + PYTHONUNBUFFERED: "1" + command: uvicorn app.main:app --host 0.0.0.0 --port 8002 --reload diff --git a/frontend/cypress.config.js b/frontend/cypress.config.js new file mode 100644 index 0000000..8144f26 --- /dev/null +++ b/frontend/cypress.config.js @@ -0,0 +1,17 @@ +import { defineConfig } from 'cypress' + +export default defineConfig({ + e2e: { + baseUrl: 'http://localhost:5173', + setupNodeEvents(on, config) { + // implement node event listeners here + }, + viewportWidth: 1280, + viewportHeight: 720, + video: true, + screenshotOnRunFailure: true, + defaultCommandTimeout: 10000, + requestTimeout: 10000, + responseTimeout: 10000, + }, +}) diff --git a/frontend/cypress/e2e/auth.cy.js b/frontend/cypress/e2e/auth.cy.js new file mode 100644 index 0000000..0b6eff1 --- /dev/null +++ b/frontend/cypress/e2e/auth.cy.js @@ -0,0 +1,107 @@ +// 生成随机手机号 +const generatePhone = () => { + const timestamp = Date.now().toString().slice(-8); + return `138${timestamp}`; +}; + +describe('用户认证测试', () => { + let testPhone; + let testPassword = 'Test123456'; + + beforeEach(() => { + testPhone = generatePhone(); + cy.visit('/'); + cy.wait(1000); // 等待页面加载 + }); + + it('应该能够成功注册新用户', () => { + // 点击注册按钮 - 使用class选择器 + cy.get('.btn-register').click(); + + // 等待注册模态框出现 + cy.get('.auth-modal', { timeout: 10000 }).should('be.visible'); + + // 填写注册表单 + cy.get('input[placeholder*="手机号"]').type(testPhone); + cy.get('input[placeholder*="昵称"]').type('E2E测试用户'); + cy.get('input[placeholder*="设置密码"]').type(testPassword); + cy.get('input[placeholder*="确认密码"]').type(testPassword); + + // 点击提交注册 + cy.get('.auth-modal .auth-submit-btn').click(); + + // 验证注册成功 - 等待页面刷新并检查登录状态 + // 注册成功后会自动刷新页面,所以等待头像出现即可 + cy.get('.ant-avatar', { timeout: 15000 }).should('exist'); + + cy.log(`✅ 注册测试通过 - 手机号: ${testPhone}`); + }); + + it('应该能够使用已注册账号登录', () => { + // 先注册 + cy.get('.btn-register').click(); + cy.get('.auth-modal').should('be.visible'); + cy.get('input[placeholder*="手机号"]').type(testPhone); + cy.get('input[placeholder*="设置密码"]').type(testPassword); + cy.get('input[placeholder*="确认密码"]').type(testPassword); + cy.get('.auth-modal .auth-submit-btn').click(); + + // 等待注册成功和页面刷新 - 等待头像出现 + cy.get('.ant-avatar', { timeout: 15000 }).should('exist'); + + // 退出登录 + cy.get('.ant-avatar').click(); + cy.contains('退出登录').click(); + cy.wait(2000); + + // 重新登录 + cy.visit('/'); + cy.wait(1000); + cy.get('.btn-login').click(); + cy.get('.auth-modal').should('be.visible'); + cy.get('input[placeholder*="手机号"]').type(testPhone); + cy.get('input[placeholder*="密码"]').first().type(testPassword); + cy.get('.auth-modal .auth-submit-btn').click(); + + // 验证登录成功 - 等待页面刷新并检查登录状态 + cy.get('.ant-avatar', { timeout: 15000 }).should('exist'); + + cy.log(`✅ 登录测试通过 - 手机号: ${testPhone}`); + }); + + it('应该验证手机号格式', () => { + cy.get('.btn-register').click(); + cy.get('.auth-modal').should('be.visible'); + + // 输入无效手机号 + cy.get('input[placeholder*="手机号"]').type('123456'); + cy.get('input[placeholder*="设置密码"]').type(testPassword); + cy.get('input[placeholder*="确认密码"]').type(testPassword); + + // 尝试提交 + cy.get('.auth-modal .auth-submit-btn').click(); + + // 应该显示错误提示 + cy.contains('请输入正确的11位手机号').should('be.visible'); + + cy.log('✅ 手机号验证测试通过'); + }); + + it('应该验证密码确认', () => { + cy.get('.btn-register').click(); + cy.get('.auth-modal').should('be.visible'); + + // 输入不匹配的密码 + cy.get('input[placeholder*="手机号"]').type(testPhone); + cy.get('input[placeholder*="设置密码"]').type(testPassword); + cy.get('input[placeholder*="确认密码"]').type('DifferentPassword123'); + + // 尝试提交 + cy.get('.auth-modal .auth-submit-btn').click(); + + // 应该显示密码不一致错误 + cy.contains('两次输入的密码不一致').should('be.visible'); + + cy.log('✅ 密码确认测试通过'); + }); +}); diff --git a/frontend/cypress/e2e/purchase.cy.js b/frontend/cypress/e2e/purchase.cy.js new file mode 100644 index 0000000..257a350 --- /dev/null +++ b/frontend/cypress/e2e/purchase.cy.js @@ -0,0 +1,124 @@ +// 生成随机手机号 +const generatePhone = () => { + const timestamp = Date.now().toString().slice(-8); + return `138${timestamp}`; +}; + +describe('购买流程测试', () => { + let testPhone; + let testPassword = 'Test123456'; + + beforeEach(() => { + testPhone = generatePhone(); + + // 注册并登录 + cy.visit('/'); + cy.wait(1000); + cy.get('.btn-register').click(); + cy.get('.auth-modal').should('be.visible'); + cy.get('input[placeholder*="手机号"]').type(testPhone); + cy.get('input[placeholder*="设置密码"]').type(testPassword); + cy.get('input[placeholder*="确认密码"]').type(testPassword); + cy.get('.auth-modal .auth-submit-btn').click(); + cy.wait(3000); // 等待页面刷新 + }); + + it('未登录用户点击下载应该提示登录', () => { + // 退出登录 + cy.get('.ant-avatar').click(); + cy.contains('退出登录').click(); + cy.wait(1000); + + // 进入作品详情页 + cy.visit('/'); + cy.wait(1000); + cy.get('.work-card', { timeout: 15000 }).first().click(); + cy.wait(2000); + + // 点击下载按钮 + cy.get('.download-btn').click(); + + // 应该显示登录提示 + cy.contains('请先登录', { timeout: 5000 }).should('exist'); + + cy.log('✅ 未登录下载提示测试通过'); + }); + + it('已登录用户应该能看到购买弹窗', () => { + // 进入作品详情页 + cy.get('.work-card', { timeout: 15000 }).first().click(); + cy.wait(2000); + + // 点击下载按钮 + cy.get('.download-btn').click(); + cy.wait(2000); + + // 检查是否显示购买弹窗或直接下载 + cy.get('body').then($body => { + if ($body.find('.ant-modal:visible').length > 0) { + cy.get('.ant-modal').should('be.visible'); + cy.contains('购买作品').should('exist'); + cy.log('✅ 购买弹窗测试通过'); + } else { + cy.log('✅ 已购买,直接下载测试通过'); + } + }); + }); + + it('点击确定购买应该创建订单', () => { + // 进入作品详情页 + cy.get('.work-card', { timeout: 15000 }).first().click(); + cy.wait(2000); + + // 点击下载按钮 + cy.contains('button', '我要下载').click(); + cy.wait(1000); + + // 如果显示购买弹窗 + cy.get('body').then($body => { + if ($body.find('.ant-modal:contains("购买")').length > 0) { + // 拦截网络请求 + cy.intercept('POST', '**/orders/create').as('createOrder'); + cy.intercept('POST', '**/payment/create').as('createPayment'); + + // 点击确定购买 + cy.get('.ant-modal').contains('button', '确定购买').click(); + + // 等待请求完成 + cy.wait('@createOrder', { timeout: 10000 }).its('response.statusCode').should('be.oneOf', [200, 201]); + + cy.log('✅ 购买流程测试通过'); + } else { + cy.log('✅ 已购买作品,跳过购买测试'); + } + }); + }); + + it('应该验证收藏功能', () => { + // 进入作品详情页 + cy.get('.work-card', { timeout: 15000 }).first().click(); + cy.wait(2000); + + // 点击收藏按钮 + cy.get('.collect-btn').click(); + cy.wait(500); + + // 应该显示消息 + cy.contains('收藏成功', { timeout: 5000 }).should('exist'); + cy.log('✅ 收藏功能测试通过'); + }); + + it('应该验证分享功能', () => { + // 进入作品详情页 + cy.get('.work-card', { timeout: 15000 }).first().click(); + cy.wait(2000); + + // 点击分享按钮 + cy.get('.share-btn').click(); + cy.wait(500); + + // 应该显示成功消息 + cy.contains('链接已复制', { timeout: 5000 }).should('exist'); + cy.log('✅ 分享功能测试通过'); + }); +}); diff --git a/frontend/cypress/e2e/works.cy.js b/frontend/cypress/e2e/works.cy.js new file mode 100644 index 0000000..cc01886 --- /dev/null +++ b/frontend/cypress/e2e/works.cy.js @@ -0,0 +1,70 @@ +describe('作品浏览测试', () => { + beforeEach(() => { + cy.visit('/'); + }); + + it('应该显示作品列表', () => { + // 等待作品列表加载 + cy.get('.work-card', { timeout: 15000 }).should('have.length.at.least', 1); + + // 获取作品数量 + cy.get('.work-card').then($cards => { + cy.log(`✅ 作品列表测试通过 - 共 ${$cards.length} 个作品`); + }); + }); + + it('应该能够查看作品详情', () => { + // 等待作品列表加载 + cy.get('.work-card', { timeout: 15000 }).should('exist'); + + // 点击第一个作品 + cy.get('.work-card').first().click(); + + // 等待详情页加载 + cy.url().should('include', '/detail/'); + cy.get('.work-detail-page', { timeout: 10000 }).should('be.visible'); + + // 验证标题存在 + cy.get('h1, h2').should('exist'); + + // 获取作品标题 + cy.get('h1, h2').first().invoke('text').then(title => { + cy.log(`✅ 作品详情测试通过 - 作品: ${title.trim()}`); + }); + }); + + it('应该显示作品信息', () => { + // 进入作品详情页 + cy.get('.work-card', { timeout: 15000 }).first().click(); + cy.wait(2000); + + // 验证作品信息显示 + cy.get('.work-detail-page').should('be.visible'); + + // 验证关键元素 + cy.get('.work-title').should('exist'); + cy.get('.download-btn').should('exist'); + + cy.log('✅ 作品信息显示测试通过'); + }); + + it('应该显示相关作品', () => { + // 进入作品详情页 + cy.get('.work-card', { timeout: 15000 }).first().click(); + cy.wait(2000); + + // 向下滚动查看相关作品 + cy.scrollTo('bottom', { duration: 1000 }); + cy.wait(1000); + + // 验证相关作品存在(如果有的话) + cy.get('body').then($body => { + if ($body.find('.related-works, .similar-works').length > 0) { + cy.get('.related-works, .similar-works').should('be.visible'); + cy.log('✅ 相关作品显示测试通过'); + } else { + cy.log('✅ 相关作品功能未实现(跳过)'); + } + }); + }); +}); diff --git a/frontend/cypress/screenshots/auth.cy.js/用户认证测试 -- 应该能够使用已注册账号登录 (failed).png b/frontend/cypress/screenshots/auth.cy.js/用户认证测试 -- 应该能够使用已注册账号登录 (failed).png new file mode 100644 index 0000000..27abd59 Binary files /dev/null and b/frontend/cypress/screenshots/auth.cy.js/用户认证测试 -- 应该能够使用已注册账号登录 (failed).png differ diff --git a/frontend/cypress/screenshots/auth.cy.js/用户认证测试 -- 应该能够成功注册新用户 (failed).png b/frontend/cypress/screenshots/auth.cy.js/用户认证测试 -- 应该能够成功注册新用户 (failed).png new file mode 100644 index 0000000..4eb2455 Binary files /dev/null and b/frontend/cypress/screenshots/auth.cy.js/用户认证测试 -- 应该能够成功注册新用户 (failed).png differ diff --git a/frontend/cypress/screenshots/purchase.cy.js/购买流程测试 -- 已登录用户应该能看到购买弹窗 (failed).png b/frontend/cypress/screenshots/purchase.cy.js/购买流程测试 -- 已登录用户应该能看到购买弹窗 (failed).png new file mode 100644 index 0000000..fb82b88 Binary files /dev/null and b/frontend/cypress/screenshots/purchase.cy.js/购买流程测试 -- 已登录用户应该能看到购买弹窗 (failed).png differ diff --git a/frontend/cypress/screenshots/purchase.cy.js/购买流程测试 -- 应该验证分享功能 (failed).png b/frontend/cypress/screenshots/purchase.cy.js/购买流程测试 -- 应该验证分享功能 (failed).png new file mode 100644 index 0000000..8496a68 Binary files /dev/null and b/frontend/cypress/screenshots/purchase.cy.js/购买流程测试 -- 应该验证分享功能 (failed).png differ diff --git a/frontend/cypress/screenshots/purchase.cy.js/购买流程测试 -- 应该验证收藏功能 (failed).png b/frontend/cypress/screenshots/purchase.cy.js/购买流程测试 -- 应该验证收藏功能 (failed).png new file mode 100644 index 0000000..41c5f32 Binary files /dev/null and b/frontend/cypress/screenshots/purchase.cy.js/购买流程测试 -- 应该验证收藏功能 (failed).png differ diff --git a/frontend/cypress/screenshots/purchase.cy.js/购买流程测试 -- 未登录用户点击下载应该提示登录 (failed).png b/frontend/cypress/screenshots/purchase.cy.js/购买流程测试 -- 未登录用户点击下载应该提示登录 (failed).png new file mode 100644 index 0000000..989881d Binary files /dev/null and b/frontend/cypress/screenshots/purchase.cy.js/购买流程测试 -- 未登录用户点击下载应该提示登录 (failed).png differ diff --git a/frontend/cypress/screenshots/purchase.cy.js/购买流程测试 -- 点击确定购买应该创建订单 (failed).png b/frontend/cypress/screenshots/purchase.cy.js/购买流程测试 -- 点击确定购买应该创建订单 (failed).png new file mode 100644 index 0000000..4111cf5 Binary files /dev/null and b/frontend/cypress/screenshots/purchase.cy.js/购买流程测试 -- 点击确定购买应该创建订单 (failed).png differ diff --git a/frontend/cypress/support/commands.js b/frontend/cypress/support/commands.js new file mode 100644 index 0000000..6b46f63 --- /dev/null +++ b/frontend/cypress/support/commands.js @@ -0,0 +1,48 @@ +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** + +// -- This is a custom command for login -- +Cypress.Commands.add('login', (phone, password) => { + cy.visit('/'); + cy.wait(1000); + cy.get('.btn-login').click(); + cy.get('.auth-modal').should('be.visible'); + cy.get('input[placeholder*="手机号"]').type(phone); + cy.get('input[placeholder*="密码"]').first().type(password); + cy.get('.auth-modal .auth-submit-btn').click(); + cy.wait(3000); +}); + +Cypress.Commands.add('register', (phone, password, nickname = '测试用户') => { + cy.visit('/'); + cy.wait(1000); + cy.get('.btn-register').click(); + cy.get('.auth-modal').should('be.visible'); + cy.get('input[placeholder*="手机号"]').type(phone); + if (nickname) { + cy.get('input[placeholder*="昵称"]').type(nickname); + } + cy.get('input[placeholder*="设置密码"]').type(password); + cy.get('input[placeholder*="确认密码"]').type(password); + cy.get('.auth-modal .auth-submit-btn').click(); + cy.wait(3000); +}); + +// -- This is a parent command -- +// Cypress.Commands.add('login', (email, password) => { ... }) +// +// -- This is a child command -- +// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) +// +// -- This is a dual command -- +// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// -- This will overwrite an existing command -- +// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) diff --git a/frontend/cypress/support/e2e.js b/frontend/cypress/support/e2e.js new file mode 100644 index 0000000..d1dd135 --- /dev/null +++ b/frontend/cypress/support/e2e.js @@ -0,0 +1,20 @@ +// *********************************************************** +// This example support/e2e.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands' + +// Alternatively you can use CommonJS syntax: +// require('./commands') diff --git a/frontend/cypress/videos/auth.cy.js.mp4 b/frontend/cypress/videos/auth.cy.js.mp4 new file mode 100644 index 0000000..91f2a1e Binary files /dev/null and b/frontend/cypress/videos/auth.cy.js.mp4 differ diff --git a/frontend/cypress/videos/purchase.cy.js.mp4 b/frontend/cypress/videos/purchase.cy.js.mp4 new file mode 100644 index 0000000..ff584fa Binary files /dev/null and b/frontend/cypress/videos/purchase.cy.js.mp4 differ diff --git a/frontend/cypress/videos/works.cy.js.mp4 b/frontend/cypress/videos/works.cy.js.mp4 new file mode 100644 index 0000000..bf636e6 Binary files /dev/null and b/frontend/cypress/videos/works.cy.js.mp4 differ diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..4fa125d --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,29 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{js,jsx}'], + extends: [ + js.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + ecmaVersion: 'latest', + ecmaFeatures: { jsx: true }, + sourceType: 'module', + }, + }, + rules: { + 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], + }, + }, +]) diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..8cd2540 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,14 @@ + + + + + + + 图汇 - 设计作品下载平台 + + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..5b1b92d --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,5976 @@ +{ + "name": "nitu", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "nitu", + "version": "0.0.0", + "dependencies": { + "@ant-design/icons": "^6.1.0", + "antd": "^6.1.4", + "axios": "^1.13.2", + "qrcode.react": "^4.2.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-router-dom": "^7.12.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "cypress": "^15.8.2", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "vite": "^7.2.4" + } + }, + "node_modules/@ant-design/colors": { + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/@ant-design/colors/-/colors-8.0.1.tgz", + "integrity": "sha512-foPVl0+SWIslGUtD/xBr1p9U4AKzPhNYEseXYRRo5QSzGACYZrQbe11AYJbYfAWnWSpGBx6JjBmSeugUsD9vqQ==", + "license": "MIT", + "dependencies": { + "@ant-design/fast-color": "^3.0.0" + } + }, + "node_modules/@ant-design/cssinjs": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/@ant-design/cssinjs/-/cssinjs-2.0.2.tgz", + "integrity": "sha512-7KDVIigtqlamOLtJ0hbjECX/sDGDaJXsM/KHala8I/1E4lpl9RAO585kbVvh/k1rIrFAV6JeGkXmdWyYj9XvuA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "@emotion/hash": "^0.8.0", + "@emotion/unitless": "^0.7.5", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "stylis": "^4.3.4" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/cssinjs-utils": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/@ant-design/cssinjs-utils/-/cssinjs-utils-2.0.2.tgz", + "integrity": "sha512-Mq3Hm6fJuQeFNKSp3+yT4bjuhVbdrsyXE2RyfpJFL0xiYNZdaJ6oFaE3zFrzmHbmvTd2Wp3HCbRtkD4fU+v2ZA==", + "license": "MIT", + "dependencies": { + "@ant-design/cssinjs": "^2.0.1", + "@babel/runtime": "^7.23.2", + "@rc-component/util": "^1.4.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/@ant-design/fast-color": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/@ant-design/fast-color/-/fast-color-3.0.0.tgz", + "integrity": "sha512-eqvpP7xEDm2S7dUzl5srEQCBTXZMmY3ekf97zI+M2DHOYyKdJGH0qua0JACHTqbkRnD/KHFQP9J1uMJ/XWVzzA==", + "license": "MIT", + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@ant-design/icons": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/@ant-design/icons/-/icons-6.1.0.tgz", + "integrity": "sha512-KrWMu1fIg3w/1F2zfn+JlfNDU8dDqILfA5Tg85iqs1lf8ooyGlbkA+TkwfOKKgqpUmAiRY1PTFpuOU2DAIgSUg==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^8.0.0", + "@ant-design/icons-svg": "^4.4.0", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/icons-svg": { + "version": "4.4.2", + "resolved": "https://registry.npmmirror.com/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz", + "integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==", + "license": "MIT" + }, + "node_modules/@ant-design/react-slick": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/@ant-design/react-slick/-/react-slick-2.0.0.tgz", + "integrity": "sha512-HMS9sRoEmZey8LsE/Yo6+klhlzU12PisjrVcydW3So7RdklyEd2qehyU6a7Yp+OYN72mgsYs3NFCyP2lCPFVqg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "clsx": "^2.1.1", + "json2mq": "^0.2.0", + "throttle-debounce": "^5.0.0" + }, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cypress/request": { + "version": "3.0.10", + "resolved": "https://registry.npmmirror.com/@cypress/request/-/request-3.0.10.tgz", + "integrity": "sha512-hauBrOdvu08vOsagkZ/Aju5XuiZx6ldsLfByg1htFeldhex+PeMrYauANzFsMJeAA0+dyPLbDoX2OYuvVoLDkQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~4.0.4", + "http-signature": "~1.4.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "performance-now": "^2.1.0", + "qs": "~6.14.1", + "safe-buffer": "^5.1.2", + "tough-cookie": "^5.0.0", + "tunnel-agent": "^0.6.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@cypress/xvfb": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/@cypress/xvfb/-/xvfb-1.2.4.tgz", + "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.1.0", + "lodash.once": "^4.1.1" + } + }, + "node_modules/@cypress/xvfb/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmmirror.com/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmmirror.com/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmmirror.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmmirror.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmmirror.com/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmmirror.com/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmmirror.com/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmmirror.com/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmmirror.com/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.2", + "resolved": "https://registry.npmmirror.com/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmmirror.com/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmmirror.com/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmmirror.com/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rc-component/async-validator": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/@rc-component/async-validator/-/async-validator-5.1.0.tgz", + "integrity": "sha512-n4HcR5siNUXRX23nDizbZBQPO0ZM/5oTtmKZ6/eqL0L2bo747cklFdZGRN2f+c9qWGICwDzrhW0H7tE9PptdcA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.4" + }, + "engines": { + "node": ">=14.x" + } + }, + "node_modules/@rc-component/cascader": { + "version": "1.11.0", + "resolved": "https://registry.npmmirror.com/@rc-component/cascader/-/cascader-1.11.0.tgz", + "integrity": "sha512-VDiEsskThWi8l0/1Nquc9I4ytcMKQYAb9Jkm6wiX5O5fpcMRsm+b8OulBMbr/b4rFTl/2y2y4GdKqQ+2whD+XQ==", + "license": "MIT", + "dependencies": { + "@rc-component/select": "~1.5.0", + "@rc-component/tree": "~1.1.0", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/checkbox": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@rc-component/checkbox/-/checkbox-1.0.1.tgz", + "integrity": "sha512-08yTH8m+bSm8TOqbybbJ9KiAuIATti6bDs2mVeSfu4QfEnyeF6X0enHVvD1NEAyuBWEAo56QtLe++MYs2D9XiQ==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/collapse": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@rc-component/collapse/-/collapse-1.1.2.tgz", + "integrity": "sha512-ilBYk1dLLJHu5Q74dF28vwtKUYQ42ZXIIDmqTuVy4rD8JQVvkXOs+KixVNbweyuIEtJYJ7+t+9GVD9dPc6N02w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/motion": "^1.1.4", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/color-picker": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/@rc-component/color-picker/-/color-picker-3.0.3.tgz", + "integrity": "sha512-V7gFF9O7o5XwIWafdbOtqI4BUUkEUkgdBwp6favy3xajMX/2dDqytFaiXlcwrpq6aRyPLp5dKLAG5RFKLXMeGA==", + "license": "MIT", + "dependencies": { + "@ant-design/fast-color": "^3.0.0", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/context": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/@rc-component/context/-/context-2.0.1.tgz", + "integrity": "sha512-HyZbYm47s/YqtP6pKXNMjPEMaukyg7P0qVfgMLzr7YiFNMHbK2fKTAGzms9ykfGHSfyf75nBbgWw+hHkp+VImw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/dialog": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/@rc-component/dialog/-/dialog-1.5.1.tgz", + "integrity": "sha512-by4Sf/a3azcb89WayWuwG19/Y312xtu8N81HoVQQtnsBDylfs+dog98fTAvLinnpeoWG52m/M7QLRW6fXR3l1g==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.1.3", + "@rc-component/portal": "^2.0.0", + "@rc-component/util": "^1.0.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/drawer": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/@rc-component/drawer/-/drawer-1.3.0.tgz", + "integrity": "sha512-rE+sdXEmv2W25VBQ9daGbnb4J4hBIEKmdbj0b3xpY+K7TUmLXDIlSnoXraIbFZdGyek9WxxGKK887uRnFgI+pQ==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.1.4", + "@rc-component/portal": "^2.0.0", + "@rc-component/util": "^1.2.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/dropdown": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/@rc-component/dropdown/-/dropdown-1.0.2.tgz", + "integrity": "sha512-6PY2ecUSYhDPhkNHHb4wfeAya04WhpmUSKzdR60G+kMNVUCX2vjT/AgTS0Lz0I/K6xrPMJ3enQbwVpeN3sHCgg==", + "license": "MIT", + "dependencies": { + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.2.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.11.0", + "react-dom": ">=16.11.0" + } + }, + "node_modules/@rc-component/form": { + "version": "1.6.2", + "resolved": "https://registry.npmmirror.com/@rc-component/form/-/form-1.6.2.tgz", + "integrity": "sha512-OgIn2RAoaSBqaIgzJf/X6iflIa9LpTozci1lagLBdURDFhGA370v0+T0tXxOi8YShMjTha531sFhwtnrv+EJaQ==", + "license": "MIT", + "dependencies": { + "@rc-component/async-validator": "^5.1.0", + "@rc-component/util": "^1.6.2", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/image": { + "version": "1.5.3", + "resolved": "https://registry.npmmirror.com/@rc-component/image/-/image-1.5.3.tgz", + "integrity": "sha512-/NR7QW9uCN8Ugar+xsHZOPvzPySfEhcW2/vLcr7VPRM+THZMrllMRv7LAUgW7ikR+Z67Ab67cgPp5K5YftpJsQ==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.0.0", + "@rc-component/portal": "^2.0.0", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/input": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@rc-component/input/-/input-1.1.2.tgz", + "integrity": "sha512-Q61IMR47piUBudgixJ30CciKIy9b1H95qe7GgEKOmSJVJXvFRWJllJfQry9tif+MX2cWFXWJf/RXz4kaCeq/Fg==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@rc-component/input-number": { + "version": "1.6.2", + "resolved": "https://registry.npmmirror.com/@rc-component/input-number/-/input-number-1.6.2.tgz", + "integrity": "sha512-Gjcq7meZlCOiWN1t1xCC+7/s85humHVokTBI7PJgTfoyw5OWF74y3e6P8PHX104g9+b54jsodFIzyaj6p8LI9w==", + "license": "MIT", + "dependencies": { + "@rc-component/mini-decimal": "^1.0.1", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/mentions": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/@rc-component/mentions/-/mentions-1.6.0.tgz", + "integrity": "sha512-KIkQNP6habNuTsLhUv0UGEOwG67tlmE7KNIJoQZZNggEZl5lQJTytFDb69sl5CK3TDdISCTjKP3nGEBKgT61CQ==", + "license": "MIT", + "dependencies": { + "@rc-component/input": "~1.1.0", + "@rc-component/menu": "~1.2.0", + "@rc-component/textarea": "~1.1.0", + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/menu": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/@rc-component/menu/-/menu-1.2.0.tgz", + "integrity": "sha512-VWwDuhvYHSnTGj4n6bV3ISrLACcPAzdPOq3d0BzkeiM5cve8BEYfvkEhNoM0PLzv51jpcejeyrLXeMVIJ+QJlg==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.1.4", + "@rc-component/overflow": "^1.0.0", + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/mini-decimal": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@rc-component/mini-decimal/-/mini-decimal-1.1.0.tgz", + "integrity": "sha512-jS4E7T9Li2GuYwI6PyiVXmxTiM6b07rlD9Ge8uGZSCz3WlzcG5ZK7g5bbuKNeZ9pgUuPK/5guV781ujdVpm4HQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0" + }, + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@rc-component/motion": { + "version": "1.1.6", + "resolved": "https://registry.npmmirror.com/@rc-component/motion/-/motion-1.1.6.tgz", + "integrity": "sha512-aEQobs/YA0kqRvHIPjQvOytdtdRVyhf/uXAal4chBjxDu6odHckExJzjn2D+Ju1aKK6hx3pAs6BXdV9+86xkgQ==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/mutate-observer": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/@rc-component/mutate-observer/-/mutate-observer-2.0.1.tgz", + "integrity": "sha512-AyarjoLU5YlxuValRi+w8JRH2Z84TBbFO2RoGWz9d8bSu0FqT8DtugH3xC3BV7mUwlmROFauyWuXFuq4IFbH+w==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/notification": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/@rc-component/notification/-/notification-1.2.0.tgz", + "integrity": "sha512-OX3J+zVU7rvoJCikjrfW7qOUp7zlDeFBK2eA3SFbGSkDqo63Sl4Ss8A04kFP+fxHSxMDIS9jYVEZtU1FNCFuBA==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.1.4", + "@rc-component/util": "^1.2.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/overflow": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/@rc-component/overflow/-/overflow-1.0.0.tgz", + "integrity": "sha512-GSlBeoE0XTBi5cf3zl8Qh7Uqhn7v8RrlJ8ajeVpEkNe94HWy5l5BQ0Mwn2TVUq9gdgbfEMUmTX7tJFAg7mz0Rw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "@rc-component/resize-observer": "^1.0.1", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/pagination": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/@rc-component/pagination/-/pagination-1.2.0.tgz", + "integrity": "sha512-YcpUFE8dMLfSo6OARJlK6DbHHvrxz7pMGPGmC/caZSJJz6HRKHC1RPP001PRHCvG9Z/veD039uOQmazVuLJzlw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/picker": { + "version": "1.9.0", + "resolved": "https://registry.npmmirror.com/@rc-component/picker/-/picker-1.9.0.tgz", + "integrity": "sha512-OLisdk8AWVCG9goBU1dWzuH5QlBQk8jktmQ6p0/IyBFwdKGwyIZOSjnBYo8hooHiTdl0lU+wGf/OfMtVBw02KQ==", + "license": "MIT", + "dependencies": { + "@rc-component/overflow": "^1.0.0", + "@rc-component/resize-observer": "^1.0.0", + "@rc-component/trigger": "^3.6.15", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=12.x" + }, + "peerDependencies": { + "date-fns": ">= 2.x", + "dayjs": ">= 1.x", + "luxon": ">= 3.x", + "moment": ">= 2.x", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + }, + "peerDependenciesMeta": { + "date-fns": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + } + } + }, + "node_modules/@rc-component/portal": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/@rc-component/portal/-/portal-2.2.0.tgz", + "integrity": "sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=12.x" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/progress": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/@rc-component/progress/-/progress-1.0.2.tgz", + "integrity": "sha512-WZUnH9eGxH1+xodZKqdrHke59uyGZSWgj5HBM5Kwk5BrTMuAORO7VJ2IP5Qbm9aH3n9x3IcesqHHR0NWPBC7fQ==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/qrcode": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@rc-component/qrcode/-/qrcode-1.1.1.tgz", + "integrity": "sha512-LfLGNymzKdUPjXUbRP+xOhIWY4jQ+YMj5MmWAcgcAq1Ij8XP7tRmAXqyuv96XvLUBE/5cA8hLFl9eO1JQMujrA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/rate": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@rc-component/rate/-/rate-1.0.1.tgz", + "integrity": "sha512-bkXxeBqDpl5IOC7yL7GcSYjQx9G8H+6kLYQnNZWeBYq2OYIv1MONd6mqKTjnnJYpV0cQIU2z3atdW0j1kttpTw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/resize-observer": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@rc-component/resize-observer/-/resize-observer-1.0.1.tgz", + "integrity": "sha512-r+w+Mz1EiueGk1IgjB3ptNXLYSLZ5vnEfKHH+gfgj7JMupftyzvUUl3fRcMZe5uMM04x0n8+G2o/c6nlO2+Wag==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/segmented": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/@rc-component/segmented/-/segmented-1.3.0.tgz", + "integrity": "sha512-5J/bJ01mbDnoA6P/FW8SxUvKn+OgUSTZJPzCNnTBntG50tzoP7DydGhqxp7ggZXZls7me3mc2EQDXakU3iTVFg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "@rc-component/motion": "^1.1.4", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@rc-component/select": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/@rc-component/select/-/select-1.5.0.tgz", + "integrity": "sha512-Zz0hpToAfOdWo/1jj3dW5iooBNU8F6fVgVaYN4Jy1SL3Xcx2OO+IqiQnxqk/PjY6hg1HVt7LjkkrYvpJQyZxoQ==", + "license": "MIT", + "dependencies": { + "@rc-component/overflow": "^1.0.0", + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.3.0", + "@rc-component/virtual-list": "^1.0.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/@rc-component/slider": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@rc-component/slider/-/slider-1.0.1.tgz", + "integrity": "sha512-uDhEPU1z3WDfCJhaL9jfd2ha/Eqpdfxsn0Zb0Xcq1NGQAman0TWaR37OWp2vVXEOdV2y0njSILTMpTfPV1454g==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/steps": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/@rc-component/steps/-/steps-1.2.2.tgz", + "integrity": "sha512-/yVIZ00gDYYPHSY0JP+M+s3ZvuXLu2f9rEjQqiUDs7EcYsUYrpJ/1bLj9aI9R7MBR3fu/NGh6RM9u2qGfqp+Nw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/switch": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@rc-component/switch/-/switch-1.0.3.tgz", + "integrity": "sha512-Jgi+EbOBquje/XNdofr7xbJQZPYJP+BlPfR0h+WN4zFkdtB2EWqEfvkXJWeipflwjWip0/17rNbxEAqs8hVHfw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/table": { + "version": "1.9.1", + "resolved": "https://registry.npmmirror.com/@rc-component/table/-/table-1.9.1.tgz", + "integrity": "sha512-FVI5ZS/GdB3BcgexfCYKi3iHhZS3Fr59EtsxORszYGrfpH1eWr33eDNSYkVfLI6tfJ7vftJDd9D5apfFWqkdJg==", + "license": "MIT", + "dependencies": { + "@rc-component/context": "^2.0.1", + "@rc-component/resize-observer": "^1.0.0", + "@rc-component/util": "^1.1.0", + "@rc-component/virtual-list": "^1.0.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/tabs": { + "version": "1.7.0", + "resolved": "https://registry.npmmirror.com/@rc-component/tabs/-/tabs-1.7.0.tgz", + "integrity": "sha512-J48cs2iBi7Ho3nptBxxIqizEliUC+ExE23faspUQKGQ550vaBlv3aGF8Epv/UB1vFWeoJDTW/dNzgIU0Qj5i/w==", + "license": "MIT", + "dependencies": { + "@rc-component/dropdown": "~1.0.0", + "@rc-component/menu": "~1.2.0", + "@rc-component/motion": "^1.1.3", + "@rc-component/resize-observer": "^1.0.0", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/textarea": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@rc-component/textarea/-/textarea-1.1.2.tgz", + "integrity": "sha512-9rMUEODWZDMovfScIEHXWlVZuPljZ2pd1LKNjslJVitn4SldEzq5vO1CL3yy3Dnib6zZal2r2DPtjy84VVpF6A==", + "license": "MIT", + "dependencies": { + "@rc-component/input": "~1.1.0", + "@rc-component/resize-observer": "^1.0.0", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/tooltip": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/@rc-component/tooltip/-/tooltip-1.4.0.tgz", + "integrity": "sha512-8Rx5DCctIlLI4raR0I0xHjVTf1aF48+gKCNeAAo5bmF5VoR5YED+A/XEqzXv9KKqrJDRcd3Wndpxh2hyzrTtSg==", + "license": "MIT", + "dependencies": { + "@rc-component/trigger": "^3.7.1", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/tour": { + "version": "2.2.1", + "resolved": "https://registry.npmmirror.com/@rc-component/tour/-/tour-2.2.1.tgz", + "integrity": "sha512-BUCrVikGJsXli38qlJ+h2WyDD6dYxzDA9dV3o0ij6gYhAq6ooT08SUMWOikva9v4KZ2BEuluGl5bPcsjrSoBgQ==", + "license": "MIT", + "dependencies": { + "@rc-component/portal": "^2.0.0", + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/tree": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@rc-component/tree/-/tree-1.1.0.tgz", + "integrity": "sha512-HZs3aOlvFgQdgrmURRc/f4IujiNBf4DdEeXUlkS0lPoLlx9RoqsZcF0caXIAMVb+NaWqKtGQDnrH8hqLCN5zlA==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.0.0", + "@rc-component/util": "^1.2.1", + "@rc-component/virtual-list": "^1.0.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=10.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/@rc-component/tree-select": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/@rc-component/tree-select/-/tree-select-1.6.0.tgz", + "integrity": "sha512-UvEGmZT+gcVvRwImAZg3/sXw9nUdn4FmCs1rSIMWjEXEIAo0dTGmIyWuLCvs+1rGe9AZ7CHMPiQUEbdadwV0fw==", + "license": "MIT", + "dependencies": { + "@rc-component/select": "~1.5.0", + "@rc-component/tree": "~1.1.0", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/@rc-component/trigger": { + "version": "3.8.1", + "resolved": "https://registry.npmmirror.com/@rc-component/trigger/-/trigger-3.8.1.tgz", + "integrity": "sha512-walnDJnKq+OcPQFHBMN+YZmdHV8+6z75+Rgpc0dW1c+Dmy6O7tRueDs4LdbwjlryQfTdsw84PIkNPzcx5yQ7qQ==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.1.4", + "@rc-component/portal": "^2.2.0", + "@rc-component/resize-observer": "^1.0.0", + "@rc-component/util": "^1.2.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/upload": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@rc-component/upload/-/upload-1.1.0.tgz", + "integrity": "sha512-LIBV90mAnUE6VK5N4QvForoxZc4XqEYZimcp7fk+lkE4XwHHyJWxpIXQQwMU8hJM+YwBbsoZkGksL1sISWHQxw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/util": { + "version": "1.7.0", + "resolved": "https://registry.npmmirror.com/@rc-component/util/-/util-1.7.0.tgz", + "integrity": "sha512-tIvIGj4Vl6fsZFvWSkYw9sAfiCKUXMyhVz6kpKyZbwyZyRPqv2vxYZROdaO1VB4gqTNvUZFXh6i3APUiterw5g==", + "license": "MIT", + "dependencies": { + "is-mobile": "^5.0.0", + "react-is": "^18.2.0" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/virtual-list": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/@rc-component/virtual-list/-/virtual-list-1.0.2.tgz", + "integrity": "sha512-uvTol/mH74FYsn5loDGJxo+7kjkO4i+y4j87Re1pxJBs0FaeuMuLRzQRGaXwnMcV1CxpZLi2Z56Rerj2M00fjQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.0", + "@rc-component/resize-observer": "^1.0.1", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", + "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", + "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", + "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", + "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", + "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", + "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", + "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", + "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", + "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", + "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", + "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", + "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", + "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", + "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", + "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", + "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", + "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", + "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", + "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", + "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", + "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", + "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", + "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", + "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", + "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", + "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmmirror.com/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmmirror.com/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmmirror.com/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmmirror.com/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.0.5", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-25.0.5.tgz", + "integrity": "sha512-FuLxeLuSVOqHPxSN1fkcD8DLU21gAP7nCKqGRJ/FglbCUBs0NYN6TpHcdmyLeh8C0KwGIaZQJSv+OYG+KZz+Gw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.7", + "resolved": "https://registry.npmmirror.com/@types/react/-/react-19.2.7.tgz", + "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmmirror.com/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.1", + "resolved": "https://registry.npmmirror.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz", + "integrity": "sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/sizzle": { + "version": "2.3.10", + "resolved": "https://registry.npmmirror.com/@types/sizzle/-/sizzle-2.3.10.tgz", + "integrity": "sha512-TC0dmN0K8YcWEAEfiPi5gJP14eJe30TTGjkvek3iM/1NdHHsdCA/Td6GvNndMOo/iSnIsZ4HuuhrYPDAmbxzww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/tmp": { + "version": "0.2.6", + "resolved": "https://registry.npmmirror.com/@types/tmp/-/tmp-0.2.6.tgz", + "integrity": "sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmmirror.com/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", + "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.53", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmmirror.com/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmmirror.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/antd": { + "version": "6.1.4", + "resolved": "https://registry.npmmirror.com/antd/-/antd-6.1.4.tgz", + "integrity": "sha512-ZSafdq6pZ94GvaCjNx2yS+zeTbL1DRukc6uuarMu1K7ptx6MSZbjyFUO4rHIRNhi5a8Zp2frxFBIxYViiTgecQ==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^8.0.0", + "@ant-design/cssinjs": "^2.0.1", + "@ant-design/cssinjs-utils": "^2.0.2", + "@ant-design/fast-color": "^3.0.0", + "@ant-design/icons": "^6.1.0", + "@ant-design/react-slick": "~2.0.0", + "@babel/runtime": "^7.28.4", + "@rc-component/cascader": "~1.11.0", + "@rc-component/checkbox": "~1.0.1", + "@rc-component/collapse": "~1.1.2", + "@rc-component/color-picker": "~3.0.3", + "@rc-component/dialog": "~1.5.1", + "@rc-component/drawer": "~1.3.0", + "@rc-component/dropdown": "~1.0.2", + "@rc-component/form": "~1.6.0", + "@rc-component/image": "~1.5.3", + "@rc-component/input": "~1.1.2", + "@rc-component/input-number": "~1.6.2", + "@rc-component/mentions": "~1.6.0", + "@rc-component/menu": "~1.2.0", + "@rc-component/motion": "~1.1.6", + "@rc-component/mutate-observer": "^2.0.1", + "@rc-component/notification": "~1.2.0", + "@rc-component/pagination": "~1.2.0", + "@rc-component/picker": "~1.9.0", + "@rc-component/progress": "~1.0.2", + "@rc-component/qrcode": "~1.1.1", + "@rc-component/rate": "~1.0.1", + "@rc-component/resize-observer": "^1.0.1", + "@rc-component/segmented": "~1.3.0", + "@rc-component/select": "~1.5.0", + "@rc-component/slider": "~1.0.1", + "@rc-component/steps": "~1.2.2", + "@rc-component/switch": "~1.0.3", + "@rc-component/table": "~1.9.1", + "@rc-component/tabs": "~1.7.0", + "@rc-component/textarea": "~1.1.2", + "@rc-component/tooltip": "~1.4.0", + "@rc-component/tour": "~2.2.1", + "@rc-component/tree": "~1.1.0", + "@rc-component/tree-select": "~1.6.0", + "@rc-component/trigger": "^3.8.1", + "@rc-component/upload": "~1.1.0", + "@rc-component/util": "^1.7.0", + "clsx": "^2.1.1", + "dayjs": "^1.11.11", + "scroll-into-view-if-needed": "^3.1.0", + "throttle-debounce": "^5.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ant-design" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/arch": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmmirror.com/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmmirror.com/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmmirror.com/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "dev": true, + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmmirror.com/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/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.13", + "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.13.tgz", + "integrity": "sha512-WhtvB2NG2wjr04+h77sg3klAIwrgOqnjS49GGudnUPGFFgg7G17y7Qecqp+2Dr5kUDxNRBca0SK7cG8JwzkWDQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/blob-util": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/blob-util/-/blob-util-2.0.2.tgz", + "integrity": "sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmmirror.com/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmmirror.com/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmmirror.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/cachedir": { + "version": "2.4.0", + "resolved": "https://registry.npmmirror.com/cachedir/-/cachedir-2.4.0.tgz", + "integrity": "sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/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/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001763", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001763.tgz", + "integrity": "sha512-mh/dGtq56uN98LlNX9qdbKnzINhX0QzhiWBFEkFfsFO4QyCvL8YegrJAazCwXIeqkIob8BlZPGM3xdnY+sgmvQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmmirror.com/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmmirror.com/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-table3": { + "version": "0.6.1", + "resolved": "https://registry.npmmirror.com/cli-table3/-/cli-table3-0.6.1.tgz", + "integrity": "sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "colors": "1.4.0" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmmirror.com/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/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/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmmirror.com/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/compute-scroll-into-view": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz", + "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==", + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/cypress": { + "version": "15.8.2", + "resolved": "https://registry.npmmirror.com/cypress/-/cypress-15.8.2.tgz", + "integrity": "sha512-KGpuiE8o9l9eyVLPkig574t8zOXkEDKzI0a+RQwy4cfXzpaLipvTOv0t+QEEkLiw/8HbgMNZ0fbPKakJAB3USA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@cypress/request": "^3.0.10", + "@cypress/xvfb": "^1.2.4", + "@types/sinonjs__fake-timers": "8.1.1", + "@types/sizzle": "^2.3.2", + "@types/tmp": "^0.2.3", + "arch": "^2.2.0", + "blob-util": "^2.0.2", + "bluebird": "^3.7.2", + "buffer": "^5.7.1", + "cachedir": "^2.3.0", + "chalk": "^4.1.0", + "ci-info": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-table3": "0.6.1", + "commander": "^6.2.1", + "common-tags": "^1.8.0", + "dayjs": "^1.10.4", + "debug": "^4.3.4", + "enquirer": "^2.3.6", + "eventemitter2": "6.4.7", + "execa": "4.1.0", + "executable": "^4.1.1", + "extract-zip": "2.0.1", + "figures": "^3.2.0", + "fs-extra": "^9.1.0", + "hasha": "5.2.2", + "is-installed-globally": "~0.4.0", + "listr2": "^3.8.3", + "lodash": "^4.17.21", + "log-symbols": "^4.0.0", + "minimist": "^1.2.8", + "ospath": "^1.2.2", + "pretty-bytes": "^5.6.0", + "process": "^0.11.10", + "proxy-from-env": "1.0.0", + "request-progress": "^3.0.0", + "supports-color": "^8.1.1", + "systeminformation": "^5.27.14", + "tmp": "~0.2.4", + "tree-kill": "1.2.2", + "untildify": "^4.0.0", + "yauzl": "^2.10.0" + }, + "bin": { + "cypress": "bin/cypress" + }, + "engines": { + "node": "^20.1.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/cypress/node_modules/proxy-from-env": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz", + "integrity": "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cypress/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmmirror.com/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmmirror.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmmirror.com/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmmirror.com/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.27.2", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.2", + "resolved": "https://registry.npmmirror.com/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmmirror.com/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmmirror.com/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmmirror.com/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter2": { + "version": "6.4.7", + "resolved": "https://registry.npmmirror.com/eventemitter2/-/eventemitter2-6.4.7.tgz", + "integrity": "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/execa": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/execa/-/execa-4.1.0.tgz", + "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/executable": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/executable/-/executable-4.1.1.tgz", + "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/figures/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmmirror.com/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmmirror.com/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/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmmirror.com/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmmirror.com/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/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmmirror.com/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/global-dirs": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/global-dirs/-/global-dirs-3.0.1.tgz", + "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmmirror.com/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/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/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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/hasha": { + "version": "5.2.2", + "resolved": "https://registry.npmmirror.com/hasha/-/hasha-5.2.2.tgz", + "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmmirror.com/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmmirror.com/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/http-signature": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/http-signature/-/http-signature-1.4.0.tgz", + "integrity": "sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^2.0.2", + "sshpk": "^1.18.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8.12.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-mobile": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/is-mobile/-/is-mobile-5.0.0.tgz", + "integrity": "sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ==", + "license": "MIT" + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmmirror.com/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmmirror.com/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true, + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC" + }, + "node_modules/json2mq": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/json2mq/-/json2mq-0.2.0.tgz", + "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==", + "license": "MIT", + "dependencies": { + "string-convert": "^0.2.0" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsprim": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/jsprim/-/jsprim-2.0.2.tgz", + "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/listr2": { + "version": "3.14.0", + "resolved": "https://registry.npmmirror.com/listr2/-/listr2-3.14.0.tgz", + "integrity": "sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^2.1.0", + "colorette": "^2.0.16", + "log-update": "^4.0.0", + "p-map": "^4.0.0", + "rfdc": "^1.3.0", + "rxjs": "^7.5.1", + "through": "^2.3.8", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "enquirer": ">= 2.3.0 < 3" + }, + "peerDependenciesMeta": { + "enquirer": { + "optional": true + } + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "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/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ospath": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/ospath/-/ospath-1.2.2.tgz", + "integrity": "sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "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/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmmirror.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmmirror.com/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qrcode.react": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/qrcode.react/-/qrcode.react-4.2.0.tgz", + "integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmmirror.com/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/react": { + "version": "19.2.3", + "resolved": "https://registry.npmmirror.com/react/-/react-19.2.3.tgz", + "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.3" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.12.0", + "resolved": "https://registry.npmmirror.com/react-router/-/react-router-7.12.0.tgz", + "integrity": "sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.12.0", + "resolved": "https://registry.npmmirror.com/react-router-dom/-/react-router-dom-7.12.0.tgz", + "integrity": "sha512-pfO9fiBcpEfX4Tx+iTYKDtPbrSLLCbwJ5EqP+SPYQu1VYCXdy79GSj0wttR0U4cikVdlImZuEZ/9ZNCgoaxwBA==", + "license": "MIT", + "dependencies": { + "react-router": "7.12.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/request-progress": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/request-progress/-/request-progress-3.0.0.tgz", + "integrity": "sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "throttleit": "^1.0.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.55.1", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.55.1.tgz", + "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", + "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.55.1", + "@rollup/rollup-android-arm64": "4.55.1", + "@rollup/rollup-darwin-arm64": "4.55.1", + "@rollup/rollup-darwin-x64": "4.55.1", + "@rollup/rollup-freebsd-arm64": "4.55.1", + "@rollup/rollup-freebsd-x64": "4.55.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", + "@rollup/rollup-linux-arm-musleabihf": "4.55.1", + "@rollup/rollup-linux-arm64-gnu": "4.55.1", + "@rollup/rollup-linux-arm64-musl": "4.55.1", + "@rollup/rollup-linux-loong64-gnu": "4.55.1", + "@rollup/rollup-linux-loong64-musl": "4.55.1", + "@rollup/rollup-linux-ppc64-gnu": "4.55.1", + "@rollup/rollup-linux-ppc64-musl": "4.55.1", + "@rollup/rollup-linux-riscv64-gnu": "4.55.1", + "@rollup/rollup-linux-riscv64-musl": "4.55.1", + "@rollup/rollup-linux-s390x-gnu": "4.55.1", + "@rollup/rollup-linux-x64-gnu": "4.55.1", + "@rollup/rollup-linux-x64-musl": "4.55.1", + "@rollup/rollup-openbsd-x64": "4.55.1", + "@rollup/rollup-openharmony-arm64": "4.55.1", + "@rollup/rollup-win32-arm64-msvc": "4.55.1", + "@rollup/rollup-win32-ia32-msvc": "4.55.1", + "@rollup/rollup-win32-x64-gnu": "4.55.1", + "@rollup/rollup-win32-x64-msvc": "4.55.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmmirror.com/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/scroll-into-view-if-needed": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", + "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==", + "license": "MIT", + "dependencies": { + "compute-scroll-into-view": "^3.0.2" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmmirror.com/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmmirror.com/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-convert": { + "version": "0.2.1", + "resolved": "https://registry.npmmirror.com/string-convert/-/string-convert-0.2.1.tgz", + "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==", + "license": "MIT" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stylis": { + "version": "4.3.6", + "resolved": "https://registry.npmmirror.com/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/systeminformation": { + "version": "5.30.2", + "resolved": "https://registry.npmmirror.com/systeminformation/-/systeminformation-5.30.2.tgz", + "integrity": "sha512-Rrt5oFTWluUVuPlbtn3o9ja+nvjdF3Um4DG0KxqfYvpzcx7Q9plZBTjJiJy9mAouua4+OI7IUGBaG9Zyt9NgxA==", + "dev": true, + "license": "MIT", + "os": [ + "darwin", + "linux", + "win32", + "freebsd", + "openbsd", + "netbsd", + "sunos", + "android" + ], + "bin": { + "systeminformation": "lib/cli.js" + }, + "engines": { + "node": ">=8.0.0" + }, + "funding": { + "type": "Buy me a coffee", + "url": "https://www.buymeacoffee.com/systeminfo" + } + }, + "node_modules/throttle-debounce": { + "version": "5.0.2", + "resolved": "https://registry.npmmirror.com/throttle-debounce/-/throttle-debounce-5.0.2.tgz", + "integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==", + "license": "MIT", + "engines": { + "node": ">=12.22" + } + }, + "node_modules/throttleit": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/throttleit/-/throttleit-1.0.1.tgz", + "integrity": "sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmmirror.com/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmmirror.com/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmmirror.com/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmmirror.com/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmmirror.com/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmmirror.com/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmmirror.com/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmmirror.com/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.5", + "resolved": "https://registry.npmmirror.com/zod/-/zod-4.3.5.tgz", + "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..0db586d --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,37 @@ +{ + "name": "nitu", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview", + "cypress": "cypress open", + "cypress:headless": "cypress run", + "test:e2e": "cypress run", + "test:e2e:ui": "cypress open" + }, + "dependencies": { + "@ant-design/icons": "^6.1.0", + "antd": "^6.1.4", + "axios": "^1.13.2", + "qrcode.react": "^4.2.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-router-dom": "^7.12.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "cypress": "^15.8.2", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "vite": "^7.2.4" + } +} diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 0000000..62522e7 --- /dev/null +++ b/frontend/src/App.css @@ -0,0 +1,56 @@ +@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&display=swap'); + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Noto Sans SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: #f5f5f5; + color: #333; +} + +.app { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +.main-content { + max-width: 1200px; + margin: 0 auto; + padding: 0 20px; + width: 100%; +} + +/* Scrollbar */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: #f1f1f1; +} + +::-webkit-scrollbar-thumb { + background: #c1c1c1; + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: #a8a8a8; +} + +/* Ant Design Overrides */ +.ant-btn-primary { + background: #ff5a5a; + border-color: #ff5a5a; +} + +.ant-btn-primary:hover { + background: #ff7070 !important; + border-color: #ff7070 !important; +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000..30b0bfa --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { Routes, Route } from 'react-router-dom'; +import Home from './pages/Home'; +import WorkDetail from './pages/WorkDetail'; +import CategoryDetail from './pages/CategoryDetail'; +import Protocol from './pages/Protocol'; +import PayProtocol from './pages/PayProtocol'; +import Copyright from './pages/Copyright'; +import Help from './pages/Help'; +import UploadGuide from './pages/UploadGuide'; +import './App.css'; + +function App() { + return ( +
+ + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + +
+ ); +} + +export default App; diff --git a/frontend/src/api/auth.js b/frontend/src/api/auth.js new file mode 100644 index 0000000..d7a51c9 --- /dev/null +++ b/frontend/src/api/auth.js @@ -0,0 +1,83 @@ +import request from '../utils/request'; + +/** + * 用户注册(使用手机号,无需验证码) + * @param {string} phone - 手机号(11位) + * @param {string} password - 密码 + * @param {string} nickname - 昵称(可选) + */ +export const register = async (phone, password, nickname = '') => { + try { + const response = await request.post('/auth/register', { + phone, + password, + nickname + }); + + // 保存 Token 和用户信息 + localStorage.setItem('access_token', response.access_token); + localStorage.setItem('user_info', JSON.stringify(response.user)); + + return { + success: true, + data: response + }; + } catch (error) { + return { + success: false, + message: error.message + }; + } +}; + +/** + * 用户登录(使用手机号) + * @param {string} phone - 手机号 + * @param {string} password - 密码 + */ +export const login = async (phone, password) => { + try { + const response = await request.post('/auth/login', { + phone, + password + }); + + // 保存 Token 和用户信息 + localStorage.setItem('access_token', response.access_token); + localStorage.setItem('user_info', JSON.stringify(response.user)); + + return { + success: true, + data: response + }; + } catch (error) { + return { + success: false, + message: error.message + }; + } +}; + +/** + * 退出登录 + */ +export const logout = () => { + localStorage.removeItem('access_token'); + localStorage.removeItem('user_info'); + window.location.reload(); +}; + +/** + * 获取当前用户信息 + */ +export const getCurrentUser = () => { + const userInfo = localStorage.getItem('user_info'); + return userInfo ? JSON.parse(userInfo) : null; +}; + +/** + * 检查是否已登录 + */ +export const isLoggedIn = () => { + return !!localStorage.getItem('access_token'); +}; diff --git a/frontend/src/api/download.js b/frontend/src/api/download.js new file mode 100644 index 0000000..26337e5 --- /dev/null +++ b/frontend/src/api/download.js @@ -0,0 +1,72 @@ +import axios from 'axios'; +import { message } from 'antd'; +import { API_CONFIG } from '../utils/config'; + +/** + * 下载作品原图 + * @param {number} workId - 作品ID + * @param {string} fileName - 保存的文件名 + */ +export const downloadWork = async (workId, fileName = null) => { + try { + const token = localStorage.getItem('access_token'); + + if (!token) { + message.error('请先登录'); + return { + success: false, + message: '请先登录' + }; + } + + // 使用 axios 下载文件 + const response = await axios({ + method: 'GET', + url: `${API_CONFIG.baseURL}${API_CONFIG.apiPrefix}/works/${workId}/download`, + headers: { + 'Authorization': `Bearer ${token}` + }, + responseType: 'blob' // 重要:以 blob 形式接收 + }); + + // 创建下载链接 + const blob = new Blob([response.data]); + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = fileName || `work_${workId}.jpg`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + + message.success('下载成功'); + return { + success: true, + message: '下载成功' + }; + } catch (error) { + if (error.response && error.response.status === 403) { + const errorMessage = '您还未购买此作品,请先完成支付'; + message.error(errorMessage); + return { + success: false, + message: errorMessage, + needPurchase: true + }; + } + if (error.response && error.response.status === 401) { + message.error('请先登录'); + return { + success: false, + message: '请先登录' + }; + } + const errorMessage = error.message || '下载失败'; + message.error(errorMessage); + return { + success: false, + message: errorMessage + }; + } +}; diff --git a/frontend/src/api/orders.js b/frontend/src/api/orders.js new file mode 100644 index 0000000..a7ef172 --- /dev/null +++ b/frontend/src/api/orders.js @@ -0,0 +1,59 @@ +import request from '../utils/request'; + +/** + * 创建订单 + * @param {number} workId - 作品ID + */ +export const createOrder = async (workId) => { + try { + const response = await request.post('/orders/create', { + work_id: workId + }); + return { + success: true, + data: response + }; + } catch (error) { + return { + success: false, + message: error.message + }; + } +}; + +/** + * 获取我的订单列表 + */ +export const getMyOrders = async () => { + try { + const response = await request.get('/orders/my'); + return { + success: true, + data: response + }; + } catch (error) { + return { + success: false, + message: error.message + }; + } +}; + +/** + * 模拟支付(仅用于测试) + * @param {number} orderId - 订单ID + */ +export const mockPay = async (orderId) => { + try { + const response = await request.post(`/orders/pay/${orderId}`); + return { + success: true, + data: response + }; + } catch (error) { + return { + success: false, + message: error.message + }; + } +}; diff --git a/frontend/src/api/payment.js b/frontend/src/api/payment.js new file mode 100644 index 0000000..b919a42 --- /dev/null +++ b/frontend/src/api/payment.js @@ -0,0 +1,58 @@ +import request from '../utils/request'; + +/** + * 创建支付链接(易收米) + * @param {number} orderId - 订单ID + */ +export const createPayment = async (orderId) => { + try { + const response = await request.post(`/payment/create/${orderId}`); + return { + success: true, + data: response + }; + } catch (error) { + return { + success: false, + message: error.message + }; + } +}; + +/** + * 查询订单支付状态 + * @param {string} orderNumber - 订单号 + */ +export const queryPaymentStatus = async (orderNumber) => { + try { + const response = await request.get(`/payment/query/${orderNumber}`); + return { + success: true, + data: response + }; + } catch (error) { + return { + success: false, + message: error.message + }; + } +}; + +/** + * 取消订单 + * @param {number} orderId - 订单ID + */ +export const cancelOrder = async (orderId) => { + try { + const response = await request.post(`/payment/cancel/${orderId}`); + return { + success: true, + data: response + }; + } catch (error) { + return { + success: false, + message: error.message + }; + } +}; diff --git a/frontend/src/api/works.js b/frontend/src/api/works.js new file mode 100644 index 0000000..eaf18d3 --- /dev/null +++ b/frontend/src/api/works.js @@ -0,0 +1,46 @@ +import request from '../utils/request'; + +/** + * 获取作品列表 + * @param {number} page - 页码(从1开始) + * @param {number} pageSize - 每页数量 + * @param {string} category - 分类筛选(可选) + * @param {string} keyword - 关键词搜索(可选) + */ +export const getWorksList = async (page = 1, pageSize = 20, category = '', keyword = '') => { + try { + const params = { page, page_size: pageSize }; + if (category) params.category = category; + if (keyword) params.keyword = keyword; + + const response = await request.get('/works', { params }); + return { + success: true, + data: response + }; + } catch (error) { + return { + success: false, + message: error.message + }; + } +}; + +/** + * 获取作品详情 + * @param {number} workId - 作品ID + */ +export const getWorkDetail = async (workId) => { + try { + const response = await request.get(`/works/${workId}`); + return { + success: true, + data: response + }; + } catch (error) { + return { + success: false, + message: error.message + }; + } +}; diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/frontend/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/components/AuthModal.css b/frontend/src/components/AuthModal.css new file mode 100644 index 0000000..8b0936a --- /dev/null +++ b/frontend/src/components/AuthModal.css @@ -0,0 +1,275 @@ +.auth-modal .ant-modal-content { + border-radius: 16px; + overflow: hidden; + padding: 0; +} + +.auth-modal .ant-modal-body { + padding: 0; +} + +.auth-modal .ant-modal-close { + top: 16px; + right: 16px; +} + +.close-icon { + font-size: 24px; + color: #999; + line-height: 1; + transition: color 0.2s; +} + +.close-icon:hover { + color: #ff5a5a; +} + +.auth-modal-content { + padding: 40px 40px; +} + +.auth-header { + text-align: center; + margin-bottom: 32px; +} + +.auth-title { + font-size: 24px; + font-weight: 600; + color: #333; + margin: 0 0 8px; +} + +.auth-subtitle { + font-size: 14px; + color: #999; + margin: 0; +} + +.auth-form { + margin-top: 24px; +} + +.auth-input { + border-radius: 8px; + height: 48px; +} + +.auth-input .ant-input { + font-size: 14px; +} + +.auth-input:hover, +.auth-input:focus, +.auth-input.ant-input-affix-wrapper-focused { + border-color: #ff5a5a; + box-shadow: 0 0 0 2px rgba(255, 90, 90, 0.1); +} + +.input-icon { + color: #bbb; + font-size: 16px; +} + +.form-options { + display: flex; + justify-content: space-between; + align-items: center; +} + +.form-options .ant-checkbox-wrapper { + color: #666; +} + +.form-options .ant-checkbox-checked .ant-checkbox-inner { + background-color: #ff5a5a; + border-color: #ff5a5a; +} + +.forgot-link { + color: #ff5a5a; + font-size: 14px; +} + +.forgot-link:hover { + color: #ff7070; +} + +.auth-submit-btn { + height: 48px; + border-radius: 24px; + font-size: 16px; + font-weight: 500; + background: linear-gradient(135deg, #ff5a5a 0%, #ff8080 100%); + border: none; + box-shadow: 0 4px 12px rgba(255, 90, 90, 0.3); + transition: all 0.3s; +} + +.auth-submit-btn:hover { + background: linear-gradient(135deg, #ff7070 0%, #ff9090 100%) !important; + transform: translateY(-2px); + box-shadow: 0 6px 16px rgba(255, 90, 90, 0.4); +} + +.auth-divider { + margin: 24px 0; +} + +.auth-divider .ant-divider-inner-text { + color: #999; + font-size: 12px; +} + +.social-login { + display: flex; + justify-content: center; + gap: 24px; + margin-bottom: 24px; +} + +.social-btn { + width: 48px; + height: 48px; + border: 1px solid #e8e8e8; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s; +} + +.social-btn .anticon { + font-size: 22px; +} + +.social-btn.wechat { + color: #07c160; +} + +.social-btn.wechat:hover { + background: #07c160; + border-color: #07c160; + color: white; +} + +.social-btn.qq { + color: #12b7f5; +} + +.social-btn.qq:hover { + background: #12b7f5; + border-color: #12b7f5; + color: white; +} + +.social-btn.weibo { + color: #e6162d; +} + +.social-btn.weibo:hover { + background: #e6162d; + border-color: #e6162d; + color: white; +} + +.auth-footer { + text-align: center; + font-size: 14px; + color: #999; +} + +.switch-link { + color: #ff5a5a; + font-weight: 500; + margin-left: 4px; + cursor: pointer; +} + +.switch-link:hover { + color: #ff7070; +} + +/* Verify Code */ +.verify-code-wrapper { + display: flex; + gap: 12px; +} + +.verify-input { + flex: 1; +} + +.verify-btn { + height: 48px; + border-radius: 8px; + font-size: 14px; + white-space: nowrap; + padding: 0 16px; + border-color: #ff5a5a; + color: #ff5a5a; +} + +.verify-btn:hover:not(:disabled) { + border-color: #ff7070 !important; + color: #ff7070 !important; +} + +.verify-btn:disabled { + color: #999; + border-color: #d9d9d9; +} + +/* Agreement */ +.agreement-checkbox { + font-size: 12px; + color: #999; +} + +.agreement-checkbox a { + color: #ff5a5a; +} + +.agreement-checkbox a:hover { + color: #ff7070; +} + +.agreement-checkbox .ant-checkbox-checked .ant-checkbox-inner { + background-color: #ff5a5a; + border-color: #ff5a5a; +} + +/* Animation */ +.auth-modal .ant-modal { + animation: modalFadeIn 0.3s ease; +} + +@keyframes modalFadeIn { + from { + opacity: 0; + transform: scale(0.9); + } + to { + opacity: 1; + transform: scale(1); + } +} + +/* Responsive */ +@media (max-width: 480px) { + .auth-modal-content { + padding: 32px 24px; + } + + .auth-title { + font-size: 20px; + } + + .social-login { + gap: 16px; + } + + .social-btn { + width: 44px; + height: 44px; + } +} diff --git a/frontend/src/components/Categories.css b/frontend/src/components/Categories.css new file mode 100644 index 0000000..c06a5b0 --- /dev/null +++ b/frontend/src/components/Categories.css @@ -0,0 +1,128 @@ +.categories { + padding: 30px 20px; + max-width: 1200px; + margin: 0 auto; +} + +.categories-container { + position: relative; +} + +.categories-scroll { + display: flex; + gap: 16px; + overflow-x: auto; + padding: 10px 0; + scrollbar-width: none; + -ms-overflow-style: none; +} + +.categories-scroll::-webkit-scrollbar { + display: none; +} + +.category-card { + flex-shrink: 0; + width: 170px; + border-radius: 12px; + overflow: hidden; + background: white; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + cursor: pointer; + transition: all 0.3s ease; + animation: fadeInUp 0.6s ease forwards; + animation-delay: var(--delay); + opacity: 0; +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.category-card:hover { + transform: translateY(-8px); + box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15); +} + +.category-image { + height: 170px; + position: relative; + display: flex; + align-items: center; + justify-content: center; +} + +.category-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.1); + display: flex; + align-items: center; + justify-content: center; +} + +.category-icon { + font-size: 80px; + color: white; + font-weight: 700; + text-shadow: 2px 2px 10px rgba(0, 0, 0, 0.3); +} + +.category-info { + padding: 12px 16px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.category-name { + font-size: 16px; + font-weight: 600; + color: #333; +} + +.category-count { + font-size: 12px; + color: #999; +} + +.scroll-btn { + position: absolute; + top: 50%; + transform: translateY(-50%); + z-index: 10; + background: white; + border: none; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + width: 40px; + height: 40px; +} + +.scroll-btn:hover { + background: #ff5a5a !important; + color: white !important; +} + +.scroll-btn-right { + right: -10px; +} + +@media (max-width: 768px) { + .category-card { + width: 160px; + } + + .category-image { + height: 100px; + } +} diff --git a/frontend/src/components/Categories.jsx b/frontend/src/components/Categories.jsx new file mode 100644 index 0000000..b65719d --- /dev/null +++ b/frontend/src/components/Categories.jsx @@ -0,0 +1,67 @@ +import { useRef } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Card, Button } from 'antd'; +import { RightOutlined } from '@ant-design/icons'; +import './Categories.css'; + +const Categories = () => { + const scrollRef = useRef(null); + const navigate = useNavigate(); + + const categories = [ + { name: '活动', count: '408131', gradient: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)' }, + { name: '中式', count: '105545', gradient: 'linear-gradient(135deg, #c94b4b 0%, #4b134f 100%)' }, + { name: '直播', count: '35429', gradient: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)' }, + { name: '旅游', count: '97826', gradient: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)' }, + { name: '周年庆', count: '15868', gradient: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)' }, + { name: '长图', count: '130856', gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' }, + { name: '价值点', count: '214448', gradient: 'linear-gradient(135deg, #0c3483 0%, #a2b6df 100%)' }, + { name: '酒吧', count: '36397', gradient: 'linear-gradient(135deg, #ff6b35 0%, #ff8c42 100%)' }, + ]; + + const scroll = (direction) => { + if (scrollRef.current) { + const scrollAmount = direction === 'left' ? -300 : 300; + scrollRef.current.scrollBy({ left: scrollAmount, behavior: 'smooth' }); + } + }; + + const handleCategoryClick = (categoryName) => { + navigate(`/category/${categoryName}`); + }; + + return ( +
+
+
+ {categories.map((cat, index) => ( +
handleCategoryClick(cat.name)} + > +
+
+ {cat.name.charAt(0)} +
+
+
+ {cat.name} + {cat.count} 张 +
+
+ ))} +
+
+
+ ); +}; + +export default Categories; diff --git a/frontend/src/components/Designers.css b/frontend/src/components/Designers.css new file mode 100644 index 0000000..9ea7c9a --- /dev/null +++ b/frontend/src/components/Designers.css @@ -0,0 +1,144 @@ +.designers-section { + padding: 30px 20px 50px; + max-width: 1200px; + margin: 0 auto; +} + +.designers-section .section-header { + margin-bottom: 20px; +} + +.designers-section .section-title { + font-size: 20px; + font-weight: 600; + color: #333; + margin: 0; + position: relative; + padding-left: 12px; +} + +.designers-section .section-title::before { + content: ''; + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + width: 4px; + height: 20px; + background: #ff5a5a; + border-radius: 2px; +} + +.designers-grid { + margin: 0 !important; +} + +.designer-card { + text-align: center; + border-radius: 12px; + padding: 20px 12px; + cursor: pointer; + transition: all 0.3s ease; + animation: fadeInUp 0.5s ease forwards; + animation-delay: var(--delay); + opacity: 0; +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.designer-card:hover { + transform: translateY(-8px); + box-shadow: 0 16px 32px rgba(0, 0, 0, 0.1); +} + +.designer-card .ant-card-body { + padding: 0; +} + +.designer-avatar { + width: 80px; + height: 80px; + border-radius: 50%; + margin: 0 auto 12px; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +.avatar-emoji { + font-size: 36px; +} + +.designer-name { + font-size: 14px; + font-weight: 600; + color: #333; + margin: 0 0 4px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.designer-desc { + font-size: 12px; + color: #999; + margin: 0 0 8px; +} + +.designer-works { + font-size: 24px; + font-weight: 700; + color: #ff5a5a; + line-height: 1.2; +} + +.designer-fans { + font-size: 12px; + color: #999; + margin: 8px 0 12px; +} + +.fans-count { + color: #333; + font-weight: 500; +} + +.follow-btn { + border-radius: 20px; + border-color: #e8e8e8; + color: #666; + font-size: 12px; + height: 28px; + padding: 0 16px; +} + +.follow-btn:hover { + color: #ff5a5a !important; + border-color: #ff5a5a !important; + background: #fff5f5 !important; +} + +@media (max-width: 768px) { + .designer-avatar { + width: 60px; + height: 60px; + } + + .avatar-emoji { + font-size: 28px; + } + + .designer-works { + font-size: 20px; + } +} diff --git a/frontend/src/components/Designers.jsx b/frontend/src/components/Designers.jsx new file mode 100644 index 0000000..5dff98b --- /dev/null +++ b/frontend/src/components/Designers.jsx @@ -0,0 +1,72 @@ +import { Card, Button, Avatar, Row, Col } from 'antd'; +import { PlusOutlined, UserOutlined } from '@ant-design/icons'; +import './Designers.css'; + +const Designers = () => { + const designers = [ + { id: 1, name: '顾九思', works: 223, fans: 546, avatar: '🎨' }, + { id: 2, name: '下辈子别做设计', works: 380, fans: 1099, avatar: '🖼️' }, + { id: 3, name: 'h突然的', works: 537, fans: 574, avatar: '✨' }, + { id: 4, name: '赤木流歌', works: 300, fans: 735, avatar: '🎭' }, + { id: 5, name: '秃头选手', works: 311, fans: 538, avatar: '🎯' }, + { id: 6, name: 'M.A', works: 553, fans: 567, avatar: '🌟' }, + { id: 7, name: 'NIMINMIN', works: 305, fans: 1208, avatar: '💫' }, + { id: 8, name: '星玥设计', works: 402, fans: 576, avatar: '⭐' }, + { id: 9, name: '一十九', works: 1598, fans: 2457, avatar: '🔥' }, + { id: 10, name: 'Jance', works: 341, fans: 3424, avatar: '💎' }, + ]; + + const getGradient = (index) => { + const gradients = [ + 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', + 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)', + 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)', + 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)', + 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)', + 'linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)', + 'linear-gradient(135deg, #d299c2 0%, #fef9d7 100%)', + 'linear-gradient(135deg, #89f7fe 0%, #66a6ff 100%)', + 'linear-gradient(135deg, #fddb92 0%, #d1fdff 100%)', + 'linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%)', + ]; + return gradients[index % gradients.length]; + }; + + return ( +
+
+

设计师

+
+ + + {designers.map((designer, index) => ( + + +
+ {designer.avatar} +
+

{designer.name}

+

他/她已上传作品

+
{designer.works}
+
+ {designer.fans} + 粉丝 +
+ +
+ + ))} +
+
+ ); +}; + +export default Designers; diff --git a/frontend/src/components/Festival.css b/frontend/src/components/Festival.css new file mode 100644 index 0000000..a21e478 --- /dev/null +++ b/frontend/src/components/Festival.css @@ -0,0 +1,122 @@ +.festival { + padding: 20px 20px 30px; + max-width: 1200px; + margin: 0 auto; +} + +.festival-container { + position: relative; +} + +.festival-scroll { + display: flex; + gap: 12px; + overflow-x: auto; + padding: 10px 0; + scrollbar-width: none; + -ms-overflow-style: none; +} + +.festival-scroll::-webkit-scrollbar { + display: none; +} + +.festival-card { + flex-shrink: 0; + width: 150px; + background: white; + border-radius: 12px; + padding: 16px; + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); + cursor: pointer; + transition: all 0.3s ease; + animation: slideIn 0.5s ease forwards; + animation-delay: var(--delay); + opacity: 0; + transform: translateX(20px); +} + +@keyframes slideIn { + to { + opacity: 1; + transform: translateX(0); + } +} + +.festival-card:hover { + transform: translateY(-4px); + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12); +} + +.festival-emoji { + font-size: 32px; + line-height: 1; +} + +.festival-content { + text-align: center; +} + +.festival-name { + font-size: 16px; + font-weight: 600; + color: #333; + margin: 0; +} + +.festival-date { + font-size: 12px; + color: #999; + margin: 4px 0 0; +} + +.festival-days { + display: flex; + align-items: baseline; + gap: 2px; + margin-top: 4px; +} + +.days-number { + font-size: 20px; + font-weight: 700; + color: #ff5a5a; +} + +.days-text { + font-size: 12px; + color: #999; +} + +.festival-scroll-btn { + position: absolute; + right: -10px; + top: 50%; + transform: translateY(-50%); + z-index: 10; + background: white; + border: none; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + width: 36px; + height: 36px; +} + +.festival-scroll-btn:hover { + background: #ff5a5a !important; + color: white !important; +} + +@media (max-width: 768px) { + .festival-card { + width: 130px; + padding: 12px; + } + + .festival-emoji { + font-size: 28px; + } +} diff --git a/frontend/src/components/Festival.jsx b/frontend/src/components/Festival.jsx new file mode 100644 index 0000000..8a649fb --- /dev/null +++ b/frontend/src/components/Festival.jsx @@ -0,0 +1,81 @@ +import { useRef } from 'react'; +import { Button } from 'antd'; +import { RightOutlined } from '@ant-design/icons'; +import './Festival.css'; + +const Festival = () => { + const scrollRef = useRef(null); + + const festivals = [ + { name: '惊蛰', date: '2026-03-05', weekday: '周四', daysLeft: 6, emoji: '⚡' }, + { name: '植树节', date: '2026-03-12', weekday: '周四', daysLeft: 13, emoji: '🌳' }, + { name: '春分', date: '2026-03-20', weekday: '周五', daysLeft: 21, emoji: '🌸' }, + { name: '愚人节', date: '2026-04-01', weekday: '周三', daysLeft: 33, emoji: '🤡' }, + { name: '清明节', date: '2026-04-05', weekday: '周日', daysLeft: 37, emoji: '🌿' }, + { name: '谷雨', date: '2026-04-20', weekday: '周一', daysLeft: 52, emoji: '🌾' }, + { name: '劳动节', date: '2026-05-01', weekday: '周五', daysLeft: 63, emoji: '🎉' }, + { name: '青年节', date: '2026-05-04', weekday: '周一', daysLeft: 66, emoji: '🔥' }, + { name: '立夏', date: '2026-05-05', weekday: '周二', daysLeft: 67, emoji: '☀️' }, + { name: '母亲节', date: '2026-05-10', weekday: '周日', daysLeft: 72, emoji: '💐' }, + { name: '小满', date: '2026-05-21', weekday: '周四', daysLeft: 83, emoji: '🌾' }, + { name: '端午节', date: '2026-06-19', weekday: '周五', daysLeft: 112, emoji: '🛶' }, + { name: '夏至', date: '2026-06-21', weekday: '周日', daysLeft: 114, emoji: '🌻' }, + { name: '建党节', date: '2026-07-01', weekday: '周三', daysLeft: 124, emoji: '🚩' }, + { name: '小暑', date: '2026-07-07', weekday: '周二', daysLeft: 130, emoji: '🔥' }, + { name: '建军节', date: '2026-08-01', weekday: '周六', daysLeft: 155, emoji: '🎖️' }, + { name: '立秋', date: '2026-08-07', weekday: '周五', daysLeft: 161, emoji: '🍂' }, + { name: '七夕节', date: '2026-08-25', weekday: '周二', daysLeft: 179, emoji: '💕' }, + { name: '白露', date: '2026-09-07', weekday: '周一', daysLeft: 192, emoji: '💧' }, + { name: '教师节', date: '2026-09-10', weekday: '周四', daysLeft: 195, emoji: '📚' }, + { name: '秋分', date: '2026-09-23', weekday: '周三', daysLeft: 208, emoji: '🍁' }, + { name: '国庆节', date: '2026-10-01', weekday: '周四', daysLeft: 216, emoji: '🇨🇳' }, + { name: '中秋节', date: '2026-10-03', weekday: '周六', daysLeft: 218, emoji: '🥮' }, + { name: '重阳节', date: '2026-10-21', weekday: '周三', daysLeft: 236, emoji: '👴' }, + { name: '立冬', date: '2026-11-07', weekday: '周六', daysLeft: 253, emoji: '❄️' }, + { name: '感恩节', date: '2026-11-26', weekday: '周四', daysLeft: 272, emoji: '🦃' }, + { name: '大雪', date: '2026-12-07', weekday: '周一', daysLeft: 283, emoji: '🌨️' }, + { name: '冬至', date: '2026-12-22', weekday: '周二', daysLeft: 298, emoji: '🥟' }, + { name: '圣诞节', date: '2026-12-25', weekday: '周五', daysLeft: 301, emoji: '🎄' }, + ]; + + const scroll = (direction) => { + if (scrollRef.current) { + const scrollAmount = direction === 'left' ? -300 : 300; + scrollRef.current.scrollBy({ left: scrollAmount, behavior: 'smooth' }); + } + }; + + return ( +
+
+
+ {festivals.map((item, index) => ( +
+
{item.emoji}
+
+

{item.name}

+

{item.date} {item.weekday}

+
+
+ {item.daysLeft} + 天后 +
+
+ ))} +
+
+
+ ); +}; + +export default Festival; diff --git a/frontend/src/components/Footer.css b/frontend/src/components/Footer.css new file mode 100644 index 0000000..5d09aa5 --- /dev/null +++ b/frontend/src/components/Footer.css @@ -0,0 +1,159 @@ +.footer { + margin-top: auto; +} + +.footer-main { + background: #f5f5f5; + padding: 40px 0; +} + +.footer-container { + max-width: 1200px; + margin: 0 auto; + padding: 0 20px; +} + +.footer-links-group { + display: flex; + gap: 24px; + margin-bottom: 12px; +} + +.footer-link { + color: #666; + font-size: 14px; + transition: color 0.2s; +} + +.footer-link:hover { + color: #ff5a5a; +} + +.footer-contact { + margin-bottom: 20px; +} + +.contact-title { + font-size: 14px; + color: #333; + margin: 0 0 12px; + font-weight: 500; +} + +.email-input { + background: white; + border-radius: 8px; + max-width: 360px; +} + +.email-input .ant-input { + color: #666; +} + +.footer-qrcodes { + display: flex; + gap: 24px; +} + +.qrcode-item { + text-align: center; +} + +.qrcode-placeholder { + width: 100px; + height: 100px; + background: white; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 8px; + border: 1px solid #e8e8e8; +} + +.qrcode-icon { + font-size: 48px; + color: #ccc; +} + +.qrcode-text { + font-size: 12px; + color: #999; +} + +.footer-partners { + margin-top: 30px; + padding-top: 20px; + border-top: 1px solid #e8e8e8; + display: flex; + flex-wrap: wrap; + gap: 12px 20px; +} + +.partner-link { + font-size: 12px; + color: #999; + transition: color 0.2s; +} + +.partner-link:hover { + color: #ff5a5a; +} + +.footer-bottom { + background: #3d3d3d; + padding: 20px 0; +} + +.copyright-text { + color: #999; + font-size: 12px; + line-height: 1.8; + margin: 0 0 8px; +} + +.email-link { + color: #ff5a5a; +} + +.email-link:hover { + text-decoration: underline; +} + +.copyright-links { + color: #999; + font-size: 12px; + margin: 0; +} + +.copyright-links a { + color: #999; + transition: color 0.2s; +} + +.copyright-links a:hover { + color: #ff5a5a; +} + +@media (max-width: 768px) { + .footer-main { + padding: 30px 0; + } + + .footer-links-group { + justify-content: center; + } + + .footer-qrcodes { + justify-content: center; + } + + .footer-partners { + justify-content: center; + } + + .copyright-text, + .copyright-links { + text-align: center; + } +} diff --git a/frontend/src/components/Footer.jsx b/frontend/src/components/Footer.jsx new file mode 100644 index 0000000..d3e03f5 --- /dev/null +++ b/frontend/src/components/Footer.jsx @@ -0,0 +1,107 @@ +import { Row, Col, Input, Button, Divider } from 'antd'; +import { MailOutlined, QrcodeOutlined } from '@ant-design/icons'; +import { useNavigate } from 'react-router-dom'; +import './Footer.css'; + +const Footer = () => { + const navigate = useNavigate(); + + const navLinks = [ + { title: '网站协议', url: '/protocol' }, + { title: '支付协议', url: '/pay-protocol' }, + { title: '版权声明', url: '/copyright' }, + ]; + + const navLinks2 = [ + { title: '帮助中心', url: '/help' }, + { title: '供稿必读', url: '/upload-guide' }, + ]; + + const handleLinkClick = (url) => { + navigate(url); + }; + + return ( + + ); +}; + +export default Footer; diff --git a/frontend/src/components/Header.css b/frontend/src/components/Header.css new file mode 100644 index 0000000..aa82781 --- /dev/null +++ b/frontend/src/components/Header.css @@ -0,0 +1,175 @@ +.header { + background: #fff; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 1000; +} + +.header-container { + max-width: 1400px; + margin: 0 auto; + padding: 0 24px; + height: 64px; + display: flex; + align-items: center; + gap: 32px; +} + +.header-left { + flex-shrink: 0; +} + +.logo { + display: flex; + align-items: center; + gap: 8px; + text-decoration: none; +} + +.logo-icon { + width: 40px; + height: 24px; +} + +.logo-text { + font-size: 20px; + font-weight: 700; + color: #ff5a5a; +} + +.logo-domain { + font-size: 10px; + color: #999; + margin-left: 4px; +} + +.header-nav { + display: flex; + align-items: center; + gap: 24px; +} + +.nav-item { + color: #333; + font-size: 14px; + cursor: pointer; + transition: color 0.2s; + display: flex; + align-items: center; + gap: 4px; + position: relative; +} + +.nav-item:hover { + color: #ff5a5a; +} + +.nav-new { + position: relative; +} + +.new-dot { + position: absolute; + top: -2px; + right: -8px; + width: 6px; + height: 6px; + background: #ff5a5a; + border-radius: 50%; +} + +.header-search { + flex: 1; + max-width: 320px; +} + +.header-search .ant-input-affix-wrapper { + border-radius: 20px; + border-color: #e8e8e8; + padding: 6px 16px; +} + +.header-search .ant-input-affix-wrapper:hover, +.header-search .ant-input-affix-wrapper:focus, +.header-search .ant-input-affix-wrapper-focused { + border-color: #ff5a5a; + box-shadow: none; +} + +.header-right { + display: flex; + align-items: center; + gap: 12px; + flex-shrink: 0; +} + +.btn-invite { + background: #ff5a5a; + border-color: #ff5a5a; + border-radius: 20px; + padding: 4px 20px; + height: 36px; +} + +.btn-invite:hover { + background: #ff7070 !important; + border-color: #ff7070 !important; +} + +.btn-register { + border-radius: 20px; + padding: 4px 20px; + height: 36px; + border-color: #333; + color: #333; +} + +.btn-register:hover { + border-color: #ff5a5a !important; + color: #ff5a5a !important; +} + +.btn-login { + border-radius: 20px; + padding: 4px 20px; + height: 36px; + border-color: #333; + color: #333; +} + +.btn-login:hover { + border-color: #ff5a5a !important; + color: #ff5a5a !important; +} + +@media (max-width: 1024px) { + .header-nav { + display: none; + } + + .header-search { + max-width: 200px; + } +} + +@media (max-width: 768px) { + .header-container { + padding: 0 16px; + gap: 16px; + } + + .logo-domain { + display: none; + } + + .header-search { + display: none; + } + + .btn-invite { + display: none; + } +} diff --git a/frontend/src/components/Header.jsx b/frontend/src/components/Header.jsx new file mode 100644 index 0000000..763a510 --- /dev/null +++ b/frontend/src/components/Header.jsx @@ -0,0 +1,150 @@ +import { useState, useEffect } from 'react'; +import { Input, Button, Space, Dropdown, Avatar } from 'antd'; +import { SearchOutlined, CameraOutlined, MenuOutlined, UserOutlined, LogoutOutlined, ShoppingOutlined } from '@ant-design/icons'; +import { getCurrentUser, isLoggedIn, logout } from '../api/auth'; +import LoginModal from './LoginModal'; +import RegisterModal from './RegisterModal'; +import './Header.css'; + +const Header = () => { + const [searchValue, setSearchValue] = useState(''); + const [loginOpen, setLoginOpen] = useState(false); + const [registerOpen, setRegisterOpen] = useState(false); + const [user, setUser] = useState(null); + + useEffect(() => { + if (isLoggedIn()) { + setUser(getCurrentUser()); + } + }, []); + + const menuItems = [ + { key: 'type', label: '类型', children: [ + { key: 'poster', label: '海报' }, + { key: 'banner', label: 'Banner' }, + { key: 'ecommerce', label: '电商' }, + { key: 'illustration', label: '插画' }, + ]}, + { key: 'theme', label: '主题', children: [ + { key: 'realestate', label: '地产' }, + { key: 'medical', label: '医美' }, + { key: 'travel', label: '旅游' }, + { key: 'tech', label: '科技' }, + ]}, + { key: 'style', label: '风格', children: [ + { key: 'chinese', label: '中式' }, + { key: 'premium', label: '高端' }, + { key: 'creative', label: '创意' }, + { key: 'minimalist', label: '清新' }, + ]}, + ]; + + const handleOpenLogin = () => { + setRegisterOpen(false); + setLoginOpen(true); + }; + + const handleOpenRegister = () => { + setLoginOpen(false); + setRegisterOpen(true); + }; + + const handleCloseLogin = () => { + setLoginOpen(false); + }; + + const handleCloseRegister = () => { + setRegisterOpen(false); + }; + + const handleLogout = () => { + logout(); + }; + + const userMenuItems = [ + { + key: 'orders', + label: '我的订单', + icon: + }, + { + key: 'logout', + label: '退出登录', + icon: , + onClick: handleLogout + } + ]; + + return ( + <> +
+
+ + + + +
+ setSearchValue(e.target.value)} + prefix={} + suffix={} + /> +
+ +
+ {user ? ( + +
+ } style={{ marginRight: 8 }} className="ant-avatar" /> + {user.nickname || user.phone} +
+
+ ) : ( + <> + + + + )} +
+
+
+ + + + + ); +}; + +export default Header; diff --git a/frontend/src/components/Hero.css b/frontend/src/components/Hero.css new file mode 100644 index 0000000..56b2984 --- /dev/null +++ b/frontend/src/components/Hero.css @@ -0,0 +1,177 @@ +/* ========== Hero 区块整体样式 ========== */ +.hero-section { + position: relative; + height: 600px; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + animation: fadeIn 1s ease-out; +} + +/* ========== 动态背景 ========== */ +.hero-background { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + overflow: hidden; +} + +.hero-particle { + position: absolute; + width: 4px; + height: 4px; + background: rgba(255, 255, 255, 0.3); + border-radius: 50%; + animation: float 6s ease-in-out infinite; +} + +.hero-particle:nth-child(1) { left: 10%; animation-delay: 0s; } +.hero-particle:nth-child(2) { left: 20%; animation-delay: 1s; } +.hero-particle:nth-child(3) { left: 30%; animation-delay: 2s; } +.hero-particle:nth-child(4) { left: 40%; animation-delay: 3s; } +.hero-particle:nth-child(5) { left: 50%; animation-delay: 4s; } +.hero-particle:nth-child(6) { left: 60%; animation-delay: 5s; } +.hero-particle:nth-child(7) { left: 70%; animation-delay: 6s; } +.hero-particle:nth-child(8) { left: 80%; animation-delay: 7s; } +.hero-particle:nth-child(9) { left: 90%; animation-delay: 8s; } + +/* ========== Hero 内容 ========== */ +.hero-content { + position: relative; + z-index: 2; + text-align: center; + color: white; + max-width: 800px; + padding: 0 20px; + animation: slideIn 1s ease-out; +} + +.hero-title { + font-size: 56px; + font-weight: 800; + margin-bottom: 20px; + line-height: 1.2; + text-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); +} + +.hero-subtitle { + font-size: 20px; + margin-bottom: 40px; + opacity: 0.95; + line-height: 1.6; +} + +/* ========== Hero 按钮 ========== */ +.hero-buttons { + display: flex; + gap: 20px; + justify-content: center; + flex-wrap: wrap; +} + +.btn-hero-primary { + background: white; + color: #667eea; + padding: 16px 40px; + border-radius: 30px; + font-size: 18px; + font-weight: 700; + border: none; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); +} + +.btn-hero-primary:hover { + transform: translateY(-3px); + box-shadow: 0 15px 40px rgba(0, 0, 0, 0.3); +} + +.btn-hero-secondary { + background: transparent; + color: white; + padding: 16px 40px; + border-radius: 30px; + font-size: 18px; + font-weight: 700; + border: 2px solid white; + cursor: pointer; + transition: all 0.3s ease; +} + +.btn-hero-secondary:hover { + background: rgba(255, 255, 255, 0.1); + transform: translateY(-3px); +} + +/* ========== 装饰元素 ========== */ +.hero-decoration { + position: absolute; + width: 400px; + height: 400px; + border: 2px solid rgba(255, 255, 255, 0.1); + border-radius: 50%; + animation: float 8s ease-in-out infinite; +} + +.hero-decoration-1 { + top: -100px; + right: -100px; +} + +.hero-decoration-2 { + bottom: -150px; + left: -150px; + width: 300px; + height: 300px; + animation-delay: 2s; +} + +/* ========== 渐变遮罩 ========== */ +.hero-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: radial-gradient(circle at center, transparent 0%, rgba(102, 126, 234, 0.3) 100%); +} + +/* ========== 响应式设计 ========== */ +@media (max-width: 768px) { + .hero-section { + height: 500px; + } + + .hero-title { + font-size: 36px; + } + + .hero-subtitle { + font-size: 16px; + } + + .hero-buttons { + flex-direction: column; + gap: 15px; + } + + .btn-hero-primary, + .btn-hero-secondary { + padding: 14px 30px; + font-size: 16px; + width: 100%; + max-width: 300px; + } +} + +/* ========== 暗黑模式 ========== */ +@media (prefers-color-scheme: dark) { + .hero-section { + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); + } +} diff --git a/frontend/src/components/Hero.jsx b/frontend/src/components/Hero.jsx new file mode 100644 index 0000000..830fbbe --- /dev/null +++ b/frontend/src/components/Hero.jsx @@ -0,0 +1,76 @@ +import { Input, Button, Tag } from 'antd'; +import { SearchOutlined, CameraOutlined } from '@ant-design/icons'; +import './Hero.css'; + +const Hero = () => { + const recommendTags = ['地产', '医美', '旅游', '汽车', '价值点', '美陈', '电商', '画册', '大寒']; + + return ( +
+
+ {/* Winter Scene Illustration */} +
+
+ {[...Array(20)].map((_, i) => ( +
+ ))} +
+ + {/* Left Side - Title */} +
+

这个冬天

+

相约雪山

+

APPOINTMENT AT SNOWMOUNTAIN

+
+ + {/* Decorative Elements */} +
+
+
+
🌲
+
🌲
+
🎄
+
+
🎿
+
🛷
+
+
+
+ +
+

有所想,不如有所享!

+ +
+ + +
+ } + /> +
+ +
+ 推荐搜索: + {recommendTags.map(tag => ( + {tag} + ))} +
+ +
+ 设计师:M.A +
+ +
+ ); +}; + +export default Hero; diff --git a/frontend/src/components/LoginModal.jsx b/frontend/src/components/LoginModal.jsx new file mode 100644 index 0000000..f17bd9d --- /dev/null +++ b/frontend/src/components/LoginModal.jsx @@ -0,0 +1,143 @@ +import { useState } from 'react'; +import { Modal, Form, Input, Button, Checkbox, Divider, message } from 'antd'; +import { MobileOutlined, LockOutlined, WechatOutlined, QqOutlined, WeiboOutlined } from '@ant-design/icons'; +import { login } from '../api/auth'; +import './AuthModal.css'; + +const LoginModal = ({ open, onClose, onSwitchToRegister }) => { + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false); + + const handleLogin = async (values) => { + setLoading(true); + const result = await login(values.phone, values.password); + setLoading(false); + + if (result.success) { + message.success('登录成功!'); + onClose(); + form.resetFields(); + // 刷新页面以更新用户信息 + window.location.reload(); + } else { + message.error(result.message || '登录失败'); + } + }; + + const handleClose = () => { + form.resetFields(); + onClose(); + }; + + return ( + ×} + data-testid="login-modal" + > +
+
+

登录图汇

+

欢迎回来,开始您的设计之旅

+
+ +
+ + } + placeholder="请输入手机号" + maxLength={11} + size="large" + className="auth-input" + data-testid="login-phone-input" + /> + + + + } + placeholder="密码" + size="large" + className="auth-input" + data-testid="login-password-input" + /> + + + +
+ 记住我 + 忘记密码? +
+
+ + + + +
+ + + 其他登录方式 + + +
+
+ +
+ 还没有账号? + 立即注册 +
+
+
+ ); +}; + +export default LoginModal; diff --git a/frontend/src/components/RegisterModal.jsx b/frontend/src/components/RegisterModal.jsx new file mode 100644 index 0000000..1809397 --- /dev/null +++ b/frontend/src/components/RegisterModal.jsx @@ -0,0 +1,175 @@ +import { useState } from 'react'; +import { Modal, Form, Input, Button, Checkbox, Divider, message } from 'antd'; +import { UserOutlined, LockOutlined, MobileOutlined, WechatOutlined, QqOutlined, WeiboOutlined } from '@ant-design/icons'; +import { register } from '../api/auth'; +import './AuthModal.css'; + +const RegisterModal = ({ open, onClose, onSwitchToLogin }) => { + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false); + + const handleRegister = async (values) => { + setLoading(true); + const result = await register(values.phone, values.password, values.nickname); + setLoading(false); + + if (result.success) { + message.success('注册成功!'); + onClose(); + form.resetFields(); + // 刷新页面以更新用户信息 + window.location.reload(); + } else { + message.error(result.message || '注册失败'); + } + }; + + const handleClose = () => { + form.resetFields(); + onClose(); + }; + + return ( + ×} + data-testid="register-modal" + > +
+
+

注册图汇

+

加入我们,发现无限创意

+
+ +
+ + } + placeholder="请输入手机号" + maxLength={11} + size="large" + className="auth-input" + data-testid="register-phone-input" + /> + + + + } + placeholder="昵称(选填,不填则自动生成)" + size="large" + className="auth-input" + data-testid="register-nickname-input" + /> + + + + } + placeholder="设置密码(至少6位)" + size="large" + className="auth-input" + data-testid="register-password-input" + /> + + + ({ + validator(_, value) { + if (!value || getFieldValue('password') === value) { + return Promise.resolve(); + } + return Promise.reject(new Error('两次输入的密码不一致')); + }, + }), + ]} + > + } + placeholder="确认密码" + size="large" + className="auth-input" + data-testid="register-confirm-password-input" + /> + + + + + +
+ + + 其他注册方式 + + +
+
+ +
+ 已有账号? + 立即登录 +
+
+
+ ); +}; + +export default RegisterModal; diff --git a/frontend/src/components/Works.css b/frontend/src/components/Works.css new file mode 100644 index 0000000..5bb4afe --- /dev/null +++ b/frontend/src/components/Works.css @@ -0,0 +1,275 @@ +/* ========== 作品区块整体样式 ========== */ +.works-section { + padding: 60px 20px; + max-width: 1400px; + margin: 0 auto; + animation: fadeIn 0.8s ease-out; +} + +/* ========== 区块标题 ========== */ +.section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 40px; + padding: 0 10px; +} + +.section-title { + font-size: 32px; + font-weight: 700; + color: #2d3436; + position: relative; + padding-left: 20px; + animation: slideIn 0.6s ease-out; +} + +.section-title::before { + content: ''; + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + width: 6px; + height: 32px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 3px; +} + +.view-more { + color: #667eea; + font-size: 16px; + font-weight: 600; + text-decoration: none; + transition: all 0.3s ease; + display: flex; + align-items: center; + gap: 6px; + padding: 8px 16px; + border-radius: 20px; + background: rgba(102, 126, 234, 0.1); +} + +.view-more:hover { + background: rgba(102, 126, 234, 0.2); + transform: translateX(5px); +} + +/* ========== 作品网格布局 ========== */ +.works-grid { + animation: scaleIn 0.8s ease-out; +} + +/* ========== 作品卡片 ========== */ +.work-card { + border-radius: 16px; + overflow: hidden; + border: none; + background: white; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); + transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); + cursor: pointer; + animation: fadeIn 0.6s ease-out; + animation-fill-mode: both; +} + +.work-card:hover { + transform: translateY(-10px) scale(1.02); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15); +} + +/* ========== 作品图片容器 ========== */ +.work-image { + position: relative; + width: 100%; + height: 280px; + overflow: hidden; + background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); +} + +.work-image img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.5s ease; +} + +.work-card:hover .work-image img { + transform: scale(1.1); +} + +/* ========== 预览遮罩层 ========== */ +.work-preview { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(102, 126, 234, 0.8); + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transition: all 0.3s ease; +} + +.work-card:hover .work-preview { + opacity: 1; +} + +.preview-icon { + font-size: 48px; + transform: scale(0.5); + transition: transform 0.3s ease; +} + +.work-card:hover .preview-icon { + transform: scale(1); +} + +/* ========== 作品信息 ========== */ +.work-info { + padding: 20px; + background: white; +} + +.work-title { + font-size: 16px; + font-weight: 600; + color: #2d3436; + margin-bottom: 12px; + line-height: 1.5; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + transition: color 0.3s ease; +} + +.work-card:hover .work-title { + color: #667eea; +} + +/* ========== 作品元数据 ========== */ +.work-meta { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 12px; + flex-wrap: wrap; +} + +.work-designer { + font-size: 13px; + color: #636e72; + font-weight: 500; +} + +.work-level { + font-size: 12px; + font-weight: 600; + padding: 3px 10px; + border-radius: 12px; + border: 1px solid; +} + +.work-level-text { + font-size: 12px; + color: #b2bec3; +} + +/* ========== 作品价格 ========== */ +.work-price { + font-size: 20px; + font-weight: 700; + color: #ff5a5a; + display: flex; + align-items: center; + gap: 4px; +} + +.work-price::before { + content: '¥'; + font-size: 14px; +} + +/* ========== 卡片延迟动画 ========== */ +.work-card:nth-child(1) { animation-delay: 0.05s; } +.work-card:nth-child(2) { animation-delay: 0.1s; } +.work-card:nth-child(3) { animation-delay: 0.15s; } +.work-card:nth-child(4) { animation-delay: 0.2s; } +.work-card:nth-child(5) { animation-delay: 0.25s; } +.work-card:nth-child(6) { animation-delay: 0.3s; } +.work-card:nth-child(7) { animation-delay: 0.35s; } +.work-card:nth-child(8) { animation-delay: 0.4s; } +.work-card:nth-child(9) { animation-delay: 0.45s; } + +/* ========== 加载状态 ========== */ +.works-section .ant-spin { + color: #667eea; +} + +.works-section .ant-spin-text { + color: #636e72; + font-size: 14px; + margin-top: 10px; +} + +/* ========== 空状态 ========== */ +.works-empty { + text-align: center; + padding: 60px 20px; + color: #b2bec3; +} + +.works-empty-icon { + font-size: 64px; + margin-bottom: 20px; + opacity: 0.5; +} + +.works-empty-text { + font-size: 16px; +} + +/* ========== 响应式设计 ========== */ +@media (max-width: 768px) { + .works-section { + padding: 40px 15px; + } + + .section-title { + font-size: 24px; + } + + .work-image { + height: 200px; + } + + .work-title { + font-size: 14px; + } + + .work-price { + font-size: 18px; + } +} + +/* ========== 暗黑模式 ========== */ +@media (prefers-color-scheme: dark) { + .work-card { + background: #2d2d44; + } + + .work-title { + color: #e0e0e0; + } + + .work-designer { + color: #a0a0a0; + } + + .work-card:hover .work-title { + color: #667eea; + } +} diff --git a/frontend/src/components/Works.jsx b/frontend/src/components/Works.jsx new file mode 100644 index 0000000..2c34dc9 --- /dev/null +++ b/frontend/src/components/Works.jsx @@ -0,0 +1,120 @@ +import { useState, useEffect } from 'react'; +import { Card, Tag, Row, Col, Spin } from 'antd'; +import { RightOutlined } from '@ant-design/icons'; +import { useNavigate } from 'react-router-dom'; +import { getWorksList } from '../api/works'; +import { API_CONFIG } from '../utils/config'; +import './Works.css'; + +const Works = ({ title, type, categoryFilter }) => { + const navigate = useNavigate(); + const [works, setWorks] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + loadWorks(); + }, [type, categoryFilter]); + + const loadWorks = async () => { + setLoading(true); + const result = await getWorksList(1, 9, categoryFilter || ''); + setLoading(false); + + if (result.success) { + setWorks(result.data.items || []); + } + }; + + // 使用 Picsum 随机图片(更稳定) + const getImageUrl = (id, width = 400, height = 300) => { + return `https://picsum.photos/seed/${id}/${width}/${height}`; + }; + + const getLevelColor = (level) => { + const colors = { + 1: '#999', + 2: '#74b9ff', + 3: '#00b894', + 4: '#6c5ce7', + 5: '#e17055', + 6: '#ff5a5a', + }; + return colors[level] || '#999'; + }; + + // 点击卡片,跳转到详情页 + const handleCardClick = (work) => { + navigate(`/detail/${work.id}`); + }; + + if (loading) { + return ( +
+
+ +
+
+ ); + } + + return ( +
+
+

{title}

+ + 查看更多 + +
+ + + {works.map((work, index) => ( + + handleCardClick(work)} + cover={ +
+ {work.title} { + e.target.src = getImageUrl(work.id); + }} + /> +
+ 🔍 +
+
+ } + > +
+

{work.title}

+
+ {work.designer} + + Lv.{work.level} + + {work.level_text} +
+
+ ¥{work.price} +
+
+
+ + ))} +
+
+ ); +}; + +export default Works; diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..e948621 --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,246 @@ +/* ========== 全局样式重置 ========== */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', Arial, sans-serif; + font-size: 16px; + line-height: 1.6; + color: #2d3436; + background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); + min-height: 100vh; +} + +/* ========== 滚动条美化 ========== */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 4px; +} + +::-webkit-scrollbar-thumb { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: linear-gradient(135deg, #764ba2 0%, #667eea 100%); +} + +/* ========== 通用动画 ========== */ +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateX(-30px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes scaleIn { + from { + opacity: 0; + transform: scale(0.9); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes float { + 0%, 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-10px); + } +} + +@keyframes shimmer { + 0% { + background-position: -1000px 0; + } + 100% { + background-position: 1000px 0; + } +} + +@keyframes pulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } +} + +/* ========== 通用类 ========== */ +.fade-in { + animation: fadeIn 0.6s ease-out; +} + +.slide-in { + animation: slideIn 0.6s ease-out; +} + +.scale-in { + animation: scaleIn 0.6s ease-out; +} + +.float { + animation: float 3s ease-in-out infinite; +} + +/* ========== 按钮样式 ========== */ +.btn-primary { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; + padding: 12px 30px; + border-radius: 25px; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4); +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6); +} + +.btn-primary:active { + transform: translateY(0); +} + +/* ========== 卡片阴影 ========== */ +.card-shadow { + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); + transition: all 0.3s ease; +} + +.card-shadow:hover { + box-shadow: 0 15px 40px rgba(0, 0, 0, 0.15); + transform: translateY(-5px); +} + +/* ========== 渐变背景 ========== */ +.gradient-bg { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); +} + +.gradient-text { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* ========== 玻璃拟态 ========== */ +.glass { + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.2); +} + +/* ========== 加载动画 ========== */ +.loading-shimmer { + background: linear-gradient( + 90deg, + #f0f0f0 0%, + #e0e0e0 20%, + #f0f0f0 40%, + #f0f0f0 100% + ); + background-size: 1000px 100%; + animation: shimmer 2s infinite; +} + +/* ========== 标签样式 ========== */ +.tag-gradient { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; + padding: 4px 12px; + border-radius: 12px; + font-size: 12px; + font-weight: 600; +} + +/* ========== 输入框美化 ========== */ +.input-modern { + width: 100%; + padding: 12px 20px; + border: 2px solid #e0e0e0; + border-radius: 12px; + font-size: 16px; + transition: all 0.3s ease; + background: white; +} + +.input-modern:focus { + outline: none; + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); +} + +/* ========== 分割线 ========== */ +.divider-gradient { + height: 2px; + border: none; + background: linear-gradient(90deg, #667eea 0%, #764ba2 50%, #667eea 100%); + background-size: 200% 100%; + animation: shimmer 3s infinite; +} + +/* ========== 响应式断点 ========== */ +@media (max-width: 768px) { + body { + font-size: 14px; + } + + .btn-primary { + padding: 10px 20px; + font-size: 14px; + } +} + +/* ========== 暗黑模式支持 ========== */ +@media (prefers-color-scheme: dark) { + body { + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); + color: #e0e0e0; + } + + .input-modern { + background: #2d2d44; + border-color: #404060; + color: #e0e0e0; + } + + .input-modern:focus { + border-color: #667eea; + } +} diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx new file mode 100644 index 0000000..b378cc1 --- /dev/null +++ b/frontend/src/main.jsx @@ -0,0 +1,13 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; +import App from './App.jsx'; +import './index.css'; + +ReactDOM.createRoot(document.getElementById('root')).render( + + + + + , +); diff --git a/frontend/src/pages/CategoryDetail.css b/frontend/src/pages/CategoryDetail.css new file mode 100644 index 0000000..0ead4cb --- /dev/null +++ b/frontend/src/pages/CategoryDetail.css @@ -0,0 +1,121 @@ +.category-detail-page { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +.category-detail-hero { + height: 350px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + color: white; +} + +.category-detail-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.3); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 20px; +} + +.back-btn { + position: absolute; + top: 90px; + left: 40px; + background: rgba(255, 255, 255, 0.9); + border: none; + color: #333; + font-weight: 500; + height: 40px; + padding: 0 20px; + transition: all 0.3s ease; +} + +.back-btn:hover { + background: white !important; + color: #ff5a5a !important; + transform: translateX(-5px); +} + +.category-detail-content { + text-align: center; + animation: fadeInUp 0.8s ease; +} + +.category-detail-title { + font-size: 56px; + font-weight: 700; + margin: 0; + text-shadow: 2px 2px 20px rgba(0, 0, 0, 0.3); + animation: scaleIn 0.6s ease; +} + +.category-detail-count { + font-size: 20px; + margin-top: 16px; + opacity: 0.95; + text-shadow: 1px 1px 10px rgba(0, 0, 0, 0.2); +} + +.category-detail-works { + flex: 1; + padding: 40px 0; + background: #f8f9fa; +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes scaleIn { + from { + opacity: 0; + transform: scale(0.8); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@media (max-width: 768px) { + .category-detail-hero { + height: 250px; + } + + .back-btn { + top: 70px; + left: 20px; + height: 36px; + padding: 0 16px; + font-size: 14px; + } + + .category-detail-title { + font-size: 36px; + } + + .category-detail-count { + font-size: 16px; + } + + .category-detail-works { + padding: 30px 0; + } +} diff --git a/frontend/src/pages/CategoryDetail.jsx b/frontend/src/pages/CategoryDetail.jsx new file mode 100644 index 0000000..03cf0f7 --- /dev/null +++ b/frontend/src/pages/CategoryDetail.jsx @@ -0,0 +1,56 @@ +import { useParams, useNavigate } from 'react-router-dom'; +import { Button } from 'antd'; +import { LeftOutlined } from '@ant-design/icons'; +import Header from '../components/Header'; +import Footer from '../components/Footer'; +import Works from '../components/Works'; +import './CategoryDetail.css'; + +const CategoryDetail = () => { + const { name } = useParams(); + const navigate = useNavigate(); + + // 分类数据映射 + const categoryData = { + '文化墙': { count: '6572', gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' }, + '周年庆': { count: '15868', gradient: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)' }, + '直播': { count: '35429', gradient: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)' }, + '包装': { count: '9533', gradient: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)' }, + '科技': { count: '44921', gradient: 'linear-gradient(135deg, #0c3483 0%, #a2b6df 100%)' }, + '活动': { count: '408131', gradient: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)' }, + '医美': { count: '223068', gradient: 'linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)' }, + '邀请函': { count: '15361', gradient: 'linear-gradient(135deg, #d299c2 0%, #fef9d7 100%)' }, + }; + + const category = categoryData[name]; + + return ( +
+
+ +
+
+ +
+

{name}

+

共 {category?.count} 张作品

+
+
+
+ +
+ +
+ +
+
+ ); +}; + +export default CategoryDetail; diff --git a/frontend/src/pages/Copyright.jsx b/frontend/src/pages/Copyright.jsx new file mode 100644 index 0000000..dea00cb --- /dev/null +++ b/frontend/src/pages/Copyright.jsx @@ -0,0 +1,76 @@ +import { useNavigate } from 'react-router-dom'; +import Header from '../components/Header'; +import Footer from '../components/Footer'; +import './Protocol.css'; + +const Copyright = () => { + const navigate = useNavigate(); + + return ( +
+
+ +
+ + +
+

版权声明

+ +

CC共享版权授权协议

+

"知识共享"(Creative Commons,简称CC)是一种版权授权协议的统称。

+ +
+

CC-BY-NC 授权协议

+

署名-非商业性使用(BY-NC):使用时需署名,可转载、节选、混编、二次创作,但不得用于商业目的。

+
+ +

版权保护声明

+ +

图汇作为网络服务平台,十分重视版权及知识产权保护,向用户传达版权保护理念:

+ +

1. 供稿设计师责任

+

须进行实名登记,上传身份证和手机号码验证,对身份真实性和作品版权负责。

+ +

2. 平台审查机制

+

图汇对作品进行严格审查,提示设计师遵守法律法规,不得上传侵权作品。

+ +

3. 违规处理

+

对上传侵权作品的设计师,平台有权进行处罚。侵权行为造成的损失由设计师承担。

+ +

4. 侵权处置

+

对明显侵权、违法内容,图汇有权不事先通知直接删除。

+ +

5. 使用范围

+

用户下载作品应在学习、交流、分享范围内使用,不得用于商业目的。

+ +

6. 严禁二次销售

+
+

⚠️ 严格保护平台设计师作品,未经授权,禁止转载作品到第三方网站进行二次销售。一经发现,立即封号,依法追究法律责任。

+
+ +

侵权投诉

+

若您的权益被侵害,请发送邮件联系我们:

+

service@aishej.com

+

工作人员会尽快处理,请您耐心等待。

+ +
+

本声明最后更新日期:2026年1月10日

+
+
+
+ +
+
+ ); +}; + +export default Copyright; diff --git a/frontend/src/pages/Help.css b/frontend/src/pages/Help.css new file mode 100644 index 0000000..0d647f0 --- /dev/null +++ b/frontend/src/pages/Help.css @@ -0,0 +1,147 @@ +.help-content { + min-height: 600px; +} + +.help-content h1 { + display: flex; + align-items: center; +} + +.help-collapse { + margin-top: 24px; + background: transparent; + border: none; +} + +.help-collapse .ant-collapse-item { + background: white; + border: 1px solid #f0f0f0; + border-radius: 8px; + margin-bottom: 12px; + overflow: hidden; +} + +.help-collapse .ant-collapse-header { + padding: 16px 20px; + font-size: 16px; + font-weight: 600; + color: #333; +} + +.help-collapse .ant-collapse-content { + border-top: 1px solid #f0f0f0; +} + +.help-qa { + padding: 12px 0; +} + +.qa-item { + padding: 16px 20px; + border-bottom: 1px dashed #f0f0f0; +} + +.qa-item:last-child { + border-bottom: none; +} + +.qa-item h4 { + font-size: 14px; + font-weight: 600; + color: #333; + margin-bottom: 8px; +} + +.qa-item h4::before { + content: ''; + display: inline-block; + width: 4px; + height: 14px; + background: #ff5a5a; + margin-right: 8px; + vertical-align: middle; +} + +.qa-item p { + font-size: 14px; + line-height: 1.8; + color: #666; + margin: 0; + padding-left: 12px; +} + +.help-contact { + margin-top: 40px; + padding: 32px; + background: linear-gradient(135deg, #fff5f5 0%, #ffe5e5 100%); + border-radius: 12px; + text-align: center; +} + +.help-contact h3 { + font-size: 20px; + font-weight: 600; + color: #333; + margin-bottom: 8px; +} + +.help-contact > p { + color: #666; + font-size: 14px; + margin-bottom: 24px; +} + +.contact-info { + display: flex; + justify-content: center; + gap: 40px; + flex-wrap: wrap; +} + +.contact-info p { + font-size: 14px; + color: #666; + margin: 0; +} + +.highlight-box { + background: #fff5f5; + border-left: 4px solid #ff5a5a; + padding: 20px; + margin: 24px 0; + border-radius: 8px; +} + +.highlight-box h3 { + font-size: 16px; + font-weight: 600; + color: #ff5a5a; + margin-bottom: 12px; +} + +.highlight-box p { + margin: 0; + line-height: 1.8; +} + +.warning-box { + background: #fff9e6; + border: 2px solid #ffcc00; + padding: 20px; + margin: 24px 0; + border-radius: 8px; +} + +.warning-box p { + margin: 0; + color: #cc8800; + font-weight: 500; + line-height: 1.8; +} + +@media (max-width: 768px) { + .contact-info { + flex-direction: column; + gap: 12px; + } +} diff --git a/frontend/src/pages/Help.jsx b/frontend/src/pages/Help.jsx new file mode 100644 index 0000000..a309448 --- /dev/null +++ b/frontend/src/pages/Help.jsx @@ -0,0 +1,136 @@ +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Collapse } from 'antd'; +import { QuestionCircleOutlined } from '@ant-design/icons'; +import Header from '../components/Header'; +import Footer from '../components/Footer'; +import './Protocol.css'; +import './Help.css'; + +const Help = () => { + const navigate = useNavigate(); + + const helpItems = [ + { + key: '1', + label: '账号相关', + children: ( +
+
+

Q: 账号使用为什么需要手机验证?

+

A: 为保护您的账号安全,当出现异常登录、异地下载等安全风险时,系统会要求手机验证,确保是本人操作。

+
+
+

Q: 账号能绑定手机号码登录吗?

+

A: 可以,在个人中心进行手机号绑定后,即可使用手机号登录。

+
+
+

Q: 为什么需要频繁登录?

+

A: 为了账号安全,系统会在一定时间后自动退出登录,请重新登录即可。

+
+
+ ) + }, + { + key: '2', + label: '作品相关', + children: ( +
+
+

Q: 如何使用快速收藏功能?

+

A: 浏览作品时,点击作品卡片上的"收藏"按钮即可快速收藏,在"我的收藏"中查看。

+
+
+

Q: 如何使用以图搜图功能?

+

A: 在首页搜索框旁点击相机图标,上传图片后即可搜索相似作品。

+
+
+

Q: 作品下载失败怎么办?

+

A: 请检查网络连接,清除浏览器缓存后重试。如仍有问题,请联系客服。

+
+
+

Q: 作品错误如何投诉举报?

+

A: 在作品详情页点击"举报"按钮,选择举报原因并提交,我们会及时处理。

+
+
+ ) + }, + { + key: '3', + label: '资金相关', + children: ( +
+
+

Q: 作品收益提现多久到账?

+

A: 提现申请审核通过后,一般3-5个工作日到账。

+
+
+

Q: 上传作品有哪几种收益方式?

+

A: 设计师上传的作品被用户下载后,可获得下载分成收益。收益会显示在个人中心。

+
+
+ ) + }, + { + key: '4', + label: '其它问题', + children: ( +
+
+

Q: 如何申请开具发票?

+

A: 在个人中心-我的订单中,选择需要开票的订单,填写发票信息后提交申请。

+
+
+

Q: VIP会员有什么权益?

+

A: VIP会员可享受每天10次免费下载、无广告浏览等专属权益。

+
+
+ ) + } + ]; + + return ( +
+
+ +
+ + +
+

+ + 帮助中心 +

+ + + +
+

没有找到您要的问题?

+

您也可以联系我们的客服团队

+
+

📧 客服邮箱:service@aishej.com

+

⏰ 工作时间:工作日 9:00-18:00

+
+
+
+
+ +
+
+ ); +}; + +export default Help; diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx new file mode 100644 index 0000000..fb8237c --- /dev/null +++ b/frontend/src/pages/Home.jsx @@ -0,0 +1,24 @@ +import Header from '../components/Header'; +import Hero from '../components/Hero'; +import Categories from '../components/Categories'; +import Festival from '../components/Festival'; +import Works from '../components/Works'; +import Designers from '../components/Designers'; +import Footer from '../components/Footer'; + +function Home() { + return ( +
+
+ + + + + + +
+
+ ); +} + +export default Home; diff --git a/frontend/src/pages/PayProtocol.jsx b/frontend/src/pages/PayProtocol.jsx new file mode 100644 index 0000000..b4fbe8e --- /dev/null +++ b/frontend/src/pages/PayProtocol.jsx @@ -0,0 +1,79 @@ +import { useNavigate } from 'react-router-dom'; +import Header from '../components/Header'; +import Footer from '../components/Footer'; +import './Protocol.css'; + +const PayProtocol = () => { + const navigate = useNavigate(); + + return ( +
+
+ +
+ + +
+

支付协议

+ +

欢迎使用图汇网站支付服务,为了保障您的权益,请认真阅读本协议。

+ +

一、使用规范

+ +

1. 会员服务

+

VIP会员每天最多可以下载10个作品。当日未使用的下载次数将在24点清零,不可累计。

+ +

2. 服务周期

+

VIP会员服务有效期根据您选择购买的时长为准,自支付成功之日起计算。

+ +

3. 重复下载规则

+

同一作品当天再次下载不会重复扣除次数;次日起重新计入下载次数。

+ +

4. 账号独立性

+

不同登录方式(QQ、微信、手机号)将被识别为独立账号,权限无法转移。

+ +

5. 退款政策

+

购买后已使用下载服务,不接受退款。未使用且未超过7日可申请退款。

+ +

6. 使用规范

+

VIP会员仅限本人使用,禁止转让、出借或用于商业牟利。禁止恶意批量下载行为。

+ +

二、作品投诉说明

+ +

以下情况不在投诉受理范围:

+
    +
  • 文字未转曲(不影响创意和分层素材使用)
  • +
  • 立体字、标题字无法直接编辑
  • +
  • 色差差异问题(不同设备显示差异)
  • +
  • 误下载或不会使用软件
  • +
  • 软件版本问题无法打开
  • +
+ +

三、其他约定

+ +

1. 使用支付服务即视为已阅读并同意本协议。

+

2. 本协议适用中华人民共和国法律。

+

3. 如有问题,请联系客服处理。

+ +
+

客服邮箱:service@aishej.com(工作日 9:00-18:00)

+

本协议最后更新日期:2026年1月10日

+
+
+
+ +
+
+ ); +}; + +export default PayProtocol; diff --git a/frontend/src/pages/Protocol.css b/frontend/src/pages/Protocol.css new file mode 100644 index 0000000..0398c2b --- /dev/null +++ b/frontend/src/pages/Protocol.css @@ -0,0 +1,164 @@ +.protocol-page { + min-height: 100vh; + background: #f5f5f5; +} + +.protocol-container { + max-width: 1200px; + margin: 0 auto; + padding: 40px 20px; + display: flex; + gap: 30px; +} + +.protocol-sidebar { + width: 200px; + background: white; + border-radius: 12px; + padding: 24px; + height: fit-content; + position: sticky; + top: 90px; +} + +.protocol-sidebar h3 { + font-size: 18px; + font-weight: 600; + margin-bottom: 16px; + color: #333; +} + +.protocol-sidebar ul { + list-style: none; + padding: 0; + margin: 0; +} + +.protocol-sidebar li { + margin-bottom: 12px; +} + +.protocol-sidebar a { + color: #666; + text-decoration: none; + font-size: 14px; + cursor: pointer; + transition: color 0.3s; + display: block; + padding: 8px 12px; + border-radius: 6px; +} + +.protocol-sidebar a:hover { + color: #ff5a5a; + background: #fff5f5; +} + +.protocol-sidebar a.active { + color: #ff5a5a; + background: #fff5f5; + font-weight: 500; +} + +.protocol-content { + flex: 1; + background: white; + border-radius: 12px; + padding: 40px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); +} + +.protocol-content h1 { + font-size: 28px; + font-weight: 600; + color: #333; + margin-bottom: 24px; + padding-bottom: 16px; + border-bottom: 2px solid #ff5a5a; +} + +.protocol-content h2 { + font-size: 20px; + font-weight: 600; + color: #333; + margin-top: 32px; + margin-bottom: 16px; +} + +.protocol-content p { + line-height: 1.8; + color: #666; + margin-bottom: 12px; + font-size: 14px; +} + +.protocol-content ul { + padding-left: 24px; + margin-bottom: 16px; +} + +.protocol-content li { + line-height: 1.8; + color: #666; + margin-bottom: 8px; + font-size: 14px; +} + +.protocol-footer { + margin-top: 40px; + padding-top: 24px; + border-top: 1px solid #f0f0f0; + text-align: center; + color: #999; + font-size: 13px; +} + +.protocol-content code { + background: #f5f5f5; + padding: 2px 8px; + border-radius: 4px; + font-family: 'Courier New', monospace; + font-size: 13px; + color: #ff5a5a; +} + +.level-table { + width: 100%; + border-collapse: collapse; + margin: 20px 0; +} + +.level-table td { + padding: 12px 16px; + border: 1px solid #f0f0f0; + font-size: 14px; + color: #666; +} + +.level-table tr:nth-child(even) { + background: #fafafa; +} + +.level-table td:first-child { + font-weight: 500; + color: #333; +} + +@media (max-width: 768px) { + .protocol-container { + flex-direction: column; + } + + .protocol-sidebar { + width: 100%; + position: static; + } + + .protocol-content { + padding: 24px; + } + + .protocol-content h1 { + font-size: 24px; + } +} diff --git a/frontend/src/pages/Protocol.jsx b/frontend/src/pages/Protocol.jsx new file mode 100644 index 0000000..409e0fd --- /dev/null +++ b/frontend/src/pages/Protocol.jsx @@ -0,0 +1,70 @@ +import { useNavigate } from 'react-router-dom'; +import Header from '../components/Header'; +import Footer from '../components/Footer'; +import './Protocol.css'; + +const Protocol = () => { + const navigate = useNavigate(); + + return ( +
+
+ +
+ + +
+

网站协议

+ +

为维护您自身权益,您在注册流程中确认同意本协议之前,应当认真阅读本协议各条款。

+ +

1. 协议范围

+

1.1 本协议由您与图汇共同缔结,具有合同效力。

+

1.2 当您完成注册程序后,即表示您已充分阅读、理解并接受本协议的全部内容。

+

1.3 如果您不同意本协议,应立即停止注册程序。

+ +

2. 用户注册与账号使用

+

2.1 用户需提供真实、准确的个人信息进行注册。

+

2.2 符合以下条件方可注册:

+
    +
  • 年满十八岁,具有完全民事行为能力的自然人
  • +
  • 提供真实的姓名、身份证和联系方式
  • +
+

2.3 用户应妥善保管账号与密码,不得转让或出借给他人使用。

+

2.4 用户在使用服务过程中,必须遵守相关法律法规。

+ +

3. 图汇的权利和义务

+

3.1 图汇提供优质的网络服务支持,对作品进行审核。

+

3.2 图汇有权在网站投放广告。

+

3.3 图汇有权对违规内容进行下架处理。

+

3.4 图汇严格保护设计师作品版权,禁止未经授权的转载和二次销售。

+ +

4. 免责声明

+

4.1 用户使用服务的风险由其自己承担。

+

4.2 图汇不保证服务不会中断,对及时性、安全性不作担保。

+

4.3 对于不可抗力造成的服务中断,图汇不承担责任。

+ +

5. 违约赔偿

+

用户如有侵权、违规行为,需承担相应的法律责任和赔偿责任。

+ +
+

本协议最后更新日期:2026年1月10日

+
+
+
+ +
+
+ ); +}; + +export default Protocol; diff --git a/frontend/src/pages/UploadGuide.jsx b/frontend/src/pages/UploadGuide.jsx new file mode 100644 index 0000000..7da946d --- /dev/null +++ b/frontend/src/pages/UploadGuide.jsx @@ -0,0 +1,132 @@ +import { useNavigate } from 'react-router-dom'; +import { CheckCircleOutlined } from '@ant-design/icons'; +import Header from '../components/Header'; +import Footer from '../components/Footer'; +import './Protocol.css'; +import './Help.css'; + +const UploadGuide = () => { + const navigate = useNavigate(); + + return ( +
+
+ +
+ + +
+

供稿必读

+ +

为了保障您的权益且规范使用供稿服务,请认真阅读本协议。

+ +

1. 作品供稿流程

+

供稿需经过4个阶段:

+
    +
  • 上传作品文件
  • +
  • 编辑作品内容和类别
  • +
  • 作品审核
  • +
  • 通过上架
  • +
+ +

2. 作品要求

+

必须包含设计分层源文件,不够分层或无价值的作品将不予通过。

+

文件格式:需将源文件和预览图压缩成 RARZIP7Z 格式。

+

建议上传低版本源文件,方便更多用户使用。

+ +

3. 禁止内容

+
+

⚠️ 禁止上传第三方素材网站模板

+

⚠️ 禁止包含第三方广告信息

+

⚠️ 禁止上传未分层的作品

+
+ +

4. 关键词填写规范

+

关键词用空格分开,包含以下要素:

+
    +
  • 作品类型(如:海报、背景板、电商详情页)
  • +
  • 作品行业或节日(如:房地产、圣诞节、旅游)
  • +
  • 作品主题(如:发布会、招聘、活动)
  • +
  • 作品风格(如:中式、小清新、科技)
  • +
  • 突出元素(如:人物、山水、礼物)
  • +
+ +
+

示例

+

✅ 海报 节日 圣诞节 红金 插画 圣诞老人 礼物

+

✅ 背景板 房地产 发布会 科技 炫酷 城市

+

✅ 电商详情页 旅游 云南 小清新 风景

+
+ +

5. 设计师等级

+

职称晋升根据上架作品数量分为六个等级:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Lv.1 设计爱好者作品数 10个以下
Lv.2 设计助理作品数 10-49个
Lv.3 设计师作品数 50-199个
Lv.4 资深设计师作品数 200-499个
Lv.5 设计经理作品数 500-999个
Lv.6 设计总监作品数 1000个以上
+ +

6. 实名认证

+

供稿设计师须进行实名登记,上传身份证和手机号码验证,对作品版权负责。

+ +

7. 收益结算

+

收益按劳务报酬结算,实名信息(姓名、身份证、结算账户)必须一致。

+

禁止设计师与用户直接进行交易。

+ +

8. AIGC内容标识

+

若作品包含AI生成的素材或元素,上传时应选择"包含AIGC元素"标识。

+ +

9. 禁止上传的内容

+

不得制作、上传含有以下内容的作品:

+
    +
  • 反对宪法基本原则的
  • +
  • 危害国家安全的
  • +
  • 散布淫秽、色情、暴力的
  • +
  • 侵犯他人合法权益的
  • +
  • 其他违法违规内容
  • +
+ +
+

本指南最后更新日期:2026年1月10日

+
+
+
+ +
+
+ ); +}; + +export default UploadGuide; diff --git a/frontend/src/pages/WorkDetail.css b/frontend/src/pages/WorkDetail.css new file mode 100644 index 0000000..9ddcc7f --- /dev/null +++ b/frontend/src/pages/WorkDetail.css @@ -0,0 +1,474 @@ +.work-detail-page { + min-height: 100vh; + background: #f5f5f5; + padding-top: 70px; +} + +.work-detail-container { + max-width: 1400px; + margin: 0 auto; + padding: 20px; +} + +/* 面包屑 */ +.breadcrumb { + padding: 16px 0; + font-size: 14px; + color: #999; +} + +.breadcrumb a { + color: #666; + cursor: pointer; + transition: color 0.3s; +} + +.breadcrumb a:hover { + color: #ff5a5a; +} + +.breadcrumb .current { + color: #333; +} + +/* 主要内容区 */ +.work-detail-content { + display: flex; + gap: 24px; + align-items: flex-start; +} + +/* 左侧作品展示 */ +.work-main { + flex: 1; + background: white; + border-radius: 12px; + overflow: hidden; +} + +.work-image-wrapper { + padding: 24px; +} + +.work-image { + width: 100%; + height: auto; + display: block; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} + +/* 相关标签 */ +.related-tags { + margin-top: 32px; + padding-top: 24px; + border-top: 1px solid #f0f0f0; +} + +.related-tags h4 { + font-size: 16px; + font-weight: 600; + margin-bottom: 16px; + color: #333; +} + +.tags-list { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.work-tag { + padding: 6px 16px; + background: #f5f5f5; + border: 1px solid #e8e8e8; + border-radius: 20px; + cursor: pointer; + transition: all 0.3s; +} + +.work-tag:hover { + background: #fff0f0; + border-color: #ff5a5a; + color: #ff5a5a; +} + +/* 猜你喜欢 */ +.related-works { + margin-top: 32px; + padding-top: 24px; + border-top: 1px solid #f0f0f0; +} + +.related-works h3 { + font-size: 18px; + font-weight: 600; + margin-bottom: 20px; + color: #333; +} + +.related-works-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 16px; +} + +.related-work-card { + cursor: pointer; + border-radius: 8px; + overflow: hidden; + transition: transform 0.3s; + background: #fafafa; +} + +.related-work-card:hover { + transform: translateY(-4px); +} + +.related-work-image { + position: relative; + width: 100%; + padding-top: 150%; + overflow: hidden; + background: #f0f0f0; +} + +.related-work-image img { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + object-fit: cover; +} + +.related-work-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.6); + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transition: opacity 0.3s; +} + +.related-work-card:hover .related-work-overlay { + opacity: 1; +} + +.related-work-info { + padding: 12px; +} + +.related-work-info h4 { + font-size: 14px; + font-weight: 500; + margin-bottom: 8px; + color: #333; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.related-work-designer { + font-size: 12px; + color: #999; +} + +.designer-level { + color: #ff5a5a; + margin-right: 4px; +} + +.designer-level-name { + margin-right: 8px; +} + +.designer-name { + margin-top: 4px; + color: #666; +} + +/* 右侧信息栏 */ +.work-sidebar { + width: 380px; + background: white; + border-radius: 12px; + padding: 24px; + position: sticky; + top: 80px; +} + +/* 作品编号 */ +.work-id-section { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px; + background: #f5f5f5; + border-radius: 8px; + margin-bottom: 16px; +} + +.work-id { + font-size: 14px; + color: #666; + font-family: monospace; +} + +/* 作品标题 */ +.work-title { + font-size: 24px; + font-weight: 600; + color: #333; + margin-bottom: 20px; + line-height: 1.4; +} + +/* 作品信息列表 */ +.work-info-list { + border-top: 1px solid #f0f0f0; + border-bottom: 1px solid #f0f0f0; + padding: 16px 0; + margin-bottom: 20px; +} + +.work-info-item { + display: flex; + justify-content: space-between; + padding: 8px 0; + font-size: 14px; +} + +.info-label { + color: #999; +} + +.info-value { + color: #333; + font-weight: 500; +} + +/* 操作按钮 */ +.work-actions { + display: grid; + grid-template-columns: 1fr auto auto; + gap: 12px; + margin-bottom: 24px; +} + +.download-btn { + background: linear-gradient(135deg, #ff5a5a 0%, #ff8080 100%); + border: none; + height: 44px; + font-size: 16px; + font-weight: 500; + box-shadow: 0 4px 12px rgba(255, 90, 90, 0.3); +} + +.download-btn:hover { + background: linear-gradient(135deg, #ff7070 0%, #ff9090 100%) !important; +} + +.collect-btn, +.share-btn { + height: 44px; + width: 44px; + padding: 0; + border-color: #e8e8e8; + color: #666; +} + +.collect-btn.collected { + color: #ff5a5a; + border-color: #ff5a5a; +} + +.collect-btn:hover, +.share-btn:hover { + color: #ff5a5a; + border-color: #ff5a5a; +} + +/* 设计师卡片 */ +.designer-card { + padding: 20px; + background: #fafafa; + border-radius: 12px; + margin-bottom: 24px; +} + +.designer-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 16px; +} + +.designer-avatar { + width: 56px; + height: 56px; + border-radius: 50%; + overflow: hidden; + border: 2px solid #fff; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.designer-avatar img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.designer-info { + flex: 1; +} + +.designer-name-level { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 4px; +} + +.designer-level-badge { + background: #ff5a5a; + color: white; + padding: 2px 8px; + border-radius: 12px; + font-size: 12px; +} + +.designer-level-text { + font-size: 12px; + color: #999; +} + +.designer-card .designer-name { + font-size: 16px; + font-weight: 600; + color: #333; + margin: 0; +} + +.designer-stats { + display: flex; + justify-content: space-around; + padding: 16px 0; + border-top: 1px solid #e8e8e8; + border-bottom: 1px solid #e8e8e8; + margin-bottom: 16px; +} + +.designer-stats .stat-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; +} + +.stat-value { + font-size: 20px; + font-weight: 600; + color: #333; +} + +.stat-label { + font-size: 12px; + color: #999; +} + +.follow-btn { + height: 40px; + background: #ff5a5a; + border: none; +} + +.follow-btn:hover { + background: #ff7070 !important; +} + +.follow-btn.followed { + background: #e8e8e8; + color: #666; +} + +.follow-btn.followed:hover { + background: #d0d0d0 !important; +} + +/* 搜索画板 */ +.board-search { + margin-bottom: 24px; +} + +.board-search h4 { + font-size: 14px; + font-weight: 600; + margin-bottom: 12px; + color: #333; +} + +.board-search-input { + height: 40px; + border-radius: 20px; +} + +/* 浏览统计 */ +.work-stats { + display: flex; + justify-content: space-around; + padding-top: 16px; + border-top: 1px solid #f0f0f0; +} + +.work-stats .stat-item { + display: flex; + align-items: center; + gap: 6px; + font-size: 14px; + color: #999; +} + +.work-stats .stat-item .anticon { + font-size: 16px; +} + +/* 响应式 */ +@media (max-width: 1200px) { + .work-detail-content { + flex-direction: column; + } + + .work-sidebar { + width: 100%; + position: static; + } + + .related-works-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 768px) { + .work-detail-container { + padding: 12px; + } + + .related-works-grid { + grid-template-columns: 1fr; + } + + .work-actions { + grid-template-columns: 1fr; + } + + .collect-btn, + .share-btn { + width: 100%; + } +} diff --git a/frontend/src/pages/WorkDetail.jsx b/frontend/src/pages/WorkDetail.jsx new file mode 100644 index 0000000..be15b14 --- /dev/null +++ b/frontend/src/pages/WorkDetail.jsx @@ -0,0 +1,463 @@ +import { useState, useEffect } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { Button, Tag, message, Input, Modal, Spin } from 'antd'; +import { HeartOutlined, HeartFilled, DownloadOutlined, ShareAltOutlined, EyeOutlined, PlusOutlined, SearchOutlined } from '@ant-design/icons'; +import { QRCodeSVG } from 'qrcode.react'; +import { getWorkDetail } from '../api/works'; +import { createOrder } from '../api/orders'; +import { createPayment, queryPaymentStatus } from '../api/payment'; +import { downloadWork } from '../api/download'; +import { isLoggedIn } from '../api/auth'; +import { API_CONFIG } from '../utils/config'; +import Header from '../components/Header'; +import Footer from '../components/Footer'; +import './WorkDetail.css'; + +const WorkDetail = () => { + const { id } = useParams(); + const navigate = useNavigate(); + const [collected, setCollected] = useState(false); + const [followed, setFollowed] = useState(false); + const [downloadModalOpen, setDownloadModalOpen] = useState(false); + const [workData, setWorkData] = useState(null); + const [loading, setLoading] = useState(true); + const [purchasing, setPurchasing] = useState(false); + const [paymentUrl, setPaymentUrl] = useState(''); + const [orderNumber, setOrderNumber] = useState(''); + const [checkingPayment, setCheckingPayment] = useState(false); + + // 加载作品详情 + useEffect(() => { + loadWorkDetail(); + }, [id]); + + const loadWorkDetail = async () => { + setLoading(true); + const result = await getWorkDetail(id); + setLoading(false); + + if (result.success) { + setWorkData(result.data); + } else { + message.error('加载作品详情失败'); + } + }; + + // 使用 Picsum 图片作为后备 + const getImageUrl = (workId) => { + return `https://picsum.photos/seed/${workId}/800/1200`; + }; + + const getRelatedImageUrl = (workId) => { + return `https://picsum.photos/seed/related${workId}/400/600`; + }; + + const relatedWorks = [ + { + id: '20', + title: '相关设计作品1', + image: getRelatedImageUrl(20), + designer: 'cestbon', + level: 1, + levelName: '设计爱好者' + }, + { + id: '21', + title: '相关设计作品2', + image: getRelatedImageUrl(21), + designer: '六十六号屯', + level: 4, + levelName: '资深设计师' + }, + { + id: '22', + title: '相关设计作品3', + image: getRelatedImageUrl(22), + designer: '扶摇', + level: 3, + levelName: '设计师' + } + ]; + + const handleCollect = () => { + setCollected(!collected); + message.success(collected ? '已取消收藏' : '收藏成功'); + }; + + const handleFollow = () => { + setFollowed(!followed); + message.success(followed ? '已取消关注' : '关注成功'); + }; + + const handleDownload = async () => { + // 检查是否登录 + if (!isLoggedIn()) { + message.warning('请先登录'); + return; + } + + // 尝试直接下载 + const result = await downloadWork(id, `${workData.title}.jpg`); + + // 如果需要购买,显示购买确认弹窗 + if (!result.success && result.needPurchase) { + setDownloadModalOpen(true); + } + }; + + const confirmDownload = async () => { + setPurchasing(true); + + // 1. 创建订单 + const orderResult = await createOrder(id); + if (!orderResult.success) { + setPurchasing(false); + message.error(orderResult.message); + return; + } + + const order = orderResult.data; + setOrderNumber(order.order_no); + + // 2. 创建支付链接 + const paymentResult = await createPayment(order.id); + setPurchasing(false); + + if (paymentResult.success) { + setPaymentUrl(paymentResult.data.pay_url); + message.success('请扫描二维码完成支付'); + // 开始轮询支付状态 + startPaymentStatusCheck(order.order_no); + } else { + message.error(paymentResult.message); + setDownloadModalOpen(false); + } + }; + + // 轮询检查支付状态 + const startPaymentStatusCheck = (orderNum) => { + setCheckingPayment(true); + const checkInterval = setInterval(async () => { + const result = await queryPaymentStatus(orderNum); + if (result.success) { + // 检查本地订单状态是否为已支付 + if (result.data.local_status === 'paid' || result.data.remote_status === 'SUCCESS') { + clearInterval(checkInterval); + setCheckingPayment(false); + setDownloadModalOpen(false); + setPaymentUrl(''); + message.success('支付成功!开始下载...'); + // 重新尝试下载 + await downloadWork(id, `${workData.title}.jpg`); + } + } + }, 3000); // 每3秒检查一次 + + // 5分钟后停止检查 + setTimeout(() => { + clearInterval(checkInterval); + setCheckingPayment(false); + }, 300000); + }; + + const handleModalClose = () => { + setDownloadModalOpen(false); + setPaymentUrl(''); + setOrderNumber(''); + setCheckingPayment(false); + }; + + // 生成作品编号:半年前日期 + 当前时分秒 + 作品ID + const generateWorkNumber = (workId) => { + const now = new Date(); + const sixMonthsAgo = new Date(now.getTime() - 180 * 24 * 60 * 60 * 1000); // 半年前 + const datePart = sixMonthsAgo.toISOString().slice(0, 10).replace(/-/g, ''); // YYYYMMDD + const timePart = now.toTimeString().slice(0, 8).replace(/:/g, ''); // HHMMSS + return `${datePart}${timePart}${workId}`; + }; + + const handleShare = () => { + navigator.clipboard.writeText(window.location.href); + message.success('链接已复制到剪贴板'); + }; + + const copyWorkId = () => { + const fullWorkNumber = generateWorkNumber(workData?.id || 0); + navigator.clipboard.writeText(fullWorkNumber); + message.success('编号已复制'); + }; + + if (loading) { + return ( +
+
+
+ +
+
+
+ ); + } + + if (!workData) { + return ( +
+
+
+

作品不存在

+
+
+
+ ); + } + + // 解析标签 + const tags = workData.tags ? (typeof workData.tags === 'string' ? JSON.parse(workData.tags) : workData.tags) : []; + + return ( +
+
+ +
+ {/* 面包屑导航 */} +
+ navigate('/')}>首页 + / + {workData.title} +
+ +
+ {/* 左侧:作品展示 */} +
+
+ {workData.title} + + {/* 相关搜索标签 */} +
+

相关搜索

+
+ {tags.map((tag, index) => ( + {tag} + ))} +
+
+ + {/* 猜你喜欢 */} +
+

猜你喜欢

+
+ {relatedWorks.map(work => ( +
navigate(`/detail/${work.id}`)}> +
+ {work.title} +
+ +
+
+
+

{work.title}

+
+ Lv.{work.level} + {work.levelName} +

{work.designer}

+
+
+
+ ))} +
+
+
+
+ + {/* 右侧:作品信息 */} +
+ {/* 编号和复制 */} +
+ {generateWorkNumber(workData.id)} + +
+ + {/* 作品标题 */} +

{workData.title}

+ + {/* 作品信息 */} +
+
+ 分类 + {workData.category} +
+
+ 设计师 + {workData.designer} +
+
+ 等级 + Lv.{workData.level} {workData.level_text} +
+
+ 价格 + ¥{workData.price} +
+
+ + {/* 操作按钮 */} +
+ +
+ + {/* 设计师信息 */} +
+
+
+ {workData.designer} +
+
+
+ Lv.{workData.level} + {workData.level_text} +
+

{workData.designer}

+
+
+ +
+
+ - + 作品 +
+
+ - + 粉丝 +
+
+ + +
+ + {/* 搜索画板 */} +
+

搜索画板

+ } + className="board-search-input" + /> +
+ + {/* 浏览统计 */} +
+
+ + {workData.views} 浏览 +
+
+ + {workData.collects} 收藏 +
+
+
+
+
+ + {/* 购买确认弹窗 */} + + 关闭 + + ] : undefined} + width={paymentUrl ? 450 : 416} + > + {!paymentUrl ? ( +
+

作品:{workData.title}

+

价格:¥{workData.price}

+

点击确定后将生成支付二维码

+
+ ) : ( +
+
+ +
+

+ 订单金额:¥{workData.price} +

+

+ 请使用微信或支付宝扫码支付 +

+ {checkingPayment && ( +
+ + 等待支付中... +
+ )} +

+ 订单号:{orderNumber} +

+

+ 支付完成后将自动开始下载 +

+
+ )} +
+ +
+ ); +}; + +export default WorkDetail; diff --git a/frontend/src/utils/config.js b/frontend/src/utils/config.js new file mode 100644 index 0000000..39c5d6f --- /dev/null +++ b/frontend/src/utils/config.js @@ -0,0 +1,8 @@ +// API 配置 +export const API_CONFIG = { + baseURL: "", + apiPrefix: "/api", + timeout: 30000 +}; + +export const API_URL = `${API_CONFIG.baseURL}${API_CONFIG.apiPrefix}`; diff --git a/frontend/src/utils/request.js b/frontend/src/utils/request.js new file mode 100644 index 0000000..b703905 --- /dev/null +++ b/frontend/src/utils/request.js @@ -0,0 +1,69 @@ +import axios from 'axios'; +import { message } from 'antd'; +import { API_CONFIG } from './config'; + +// 创建 axios 实例 +const request = axios.create({ + baseURL: `${API_CONFIG.baseURL}${API_CONFIG.apiPrefix}`, + timeout: API_CONFIG.timeout, + headers: { + 'Content-Type': 'application/json' + } +}); + +// 请求拦截器 - 添加 Token +request.interceptors.request.use( + config => { + const token = localStorage.getItem('access_token'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + error => { + return Promise.reject(error); + } +); + +// 响应拦截器 - 统一错误处理 +request.interceptors.response.use( + response => { + return response.data; + }, + error => { + if (error.response) { + const { status, data } = error.response; + + // Token 过期或无效 + if (status === 401) { + localStorage.removeItem('access_token'); + localStorage.removeItem('user_info'); + message.error('登录已过期,请重新登录'); + window.location.href = '/'; + return Promise.reject(new Error('登录已过期,请重新登录')); + } + + // 权限不足 + if (status === 403) { + const errorMessage = data.detail || '没有权限访问'; + message.error(errorMessage); + return Promise.reject(new Error(errorMessage)); + } + + // 其他错误 + const errorMessage = data.detail || '请求失败'; + message.error(errorMessage); + return Promise.reject(new Error(errorMessage)); + } + + // 网络错误 + if (error.request) { + message.error('网络连接失败,请检查网络'); + return Promise.reject(new Error('网络连接失败')); + } + + return Promise.reject(error); + } +); + +export default request; diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..8b0f57b --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +})