From 330ac27b1e6fe6521726d7cced4931cd34df2bd3 Mon Sep 17 00:00:00 2001 From: wwwwwwwww <526125649@qq.com> Date: Fri, 10 Jul 2026 19:27:17 +0800 Subject: [PATCH 01/12] 1 --- video-gen-app/src/pages/CreditRecordsPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/video-gen-app/src/pages/CreditRecordsPage.tsx b/video-gen-app/src/pages/CreditRecordsPage.tsx index a072a8e8..f7c492cc 100644 --- a/video-gen-app/src/pages/CreditRecordsPage.tsx +++ b/video-gen-app/src/pages/CreditRecordsPage.tsx @@ -34,7 +34,7 @@ const CreditRecordsPage: React.FC = () => { const columns = [ { - title: '变动类型', + title: '变动类型1', dataIndex: 'type', key: 'type', width: 120, From 8641d507e693da468f06ac69cefa240aa6c8692c Mon Sep 17 00:00:00 2001 From: wwwwwwwww <526125649@qq.com> Date: Fri, 10 Jul 2026 19:28:39 +0800 Subject: [PATCH 02/12] 1 --- video-gen-app/src/pages/CreditRecordsPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/video-gen-app/src/pages/CreditRecordsPage.tsx b/video-gen-app/src/pages/CreditRecordsPage.tsx index f7c492cc..a072a8e8 100644 --- a/video-gen-app/src/pages/CreditRecordsPage.tsx +++ b/video-gen-app/src/pages/CreditRecordsPage.tsx @@ -34,7 +34,7 @@ const CreditRecordsPage: React.FC = () => { const columns = [ { - title: '变动类型1', + title: '变动类型', dataIndex: 'type', key: 'type', width: 120, From 111c74c7c6f3f89d841e8503a54fddc729da278b Mon Sep 17 00:00:00 2001 From: wwwwwwwww <526125649@qq.com> Date: Sat, 11 Jul 2026 09:17:07 +0800 Subject: [PATCH 03/12] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=9E=B6=E6=9E=84md?= =?UTF-8?q?=E8=B7=9F=E4=BD=BF=E7=94=A8md=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CODEBASE.md | 802 ++++++++++++++++++++++++++++++++++++++++++++++++++ DEPLOYMENT.md | 315 +++++++++++++++----- 2 files changed, 1047 insertions(+), 70 deletions(-) create mode 100644 CODEBASE.md diff --git a/CODEBASE.md b/CODEBASE.md new file mode 100644 index 00000000..9cc25c3f --- /dev/null +++ b/CODEBASE.md @@ -0,0 +1,802 @@ +# VideoGen 代码架构指南 + +> 本文档面向开发者或 AI 模型,帮助快速理解代码风格、架构约定和修改规则。 + +--- + +## 一、整体架构 + +``` +video_item/ +├── video-gen-api/ # 后端 (Python FastAPI) +├── video-gen-app/ # 前台/用户端 (React + Vite) ──┤ +└── video-gen-admin/ # 后台管理端 (React + Vite) ──┘ 两者共用同一个后端 API +``` + +**核心原则:** 三个项目完全分离,前台和后台是两个独立的 SPA,部署在不同的子域名上,但调用同一套后端接口。 + +--- + +## 二、后端架构 (video-gen-api) + +### 2.1 目录结构 + +``` +app/ +├── api/ # API 路由层(薄层,仅做参数解析和调用 service) +│ ├── v1/ # 前台 + 部分管理接口(FastAPI 意义上的 v1 版本) +│ │ ├── __init__.py # 聚合所有 v1 router +│ │ ├── auth.py +│ │ ├── projects.py +│ │ ├── generation.py +│ │ ├── admin.py # prefix="/admin"(管理接口混入 v1) +│ │ └── ... +│ └── admin/ # 额外的管理端细分路由 +│ ├── __init__.py +│ ├── team.py +│ ├── home_material.py +│ └── ... +├── schemas/ # Pydantic 模型(请求入参 + 响应序列化) +│ ├── common.py # 公共基类:NaiveDatetime, PaginatedResponse +│ ├── generation.py +│ ├── auth.py +│ └── ... +├── models/ # SQLAlchemy ORM 模型(41 个) +│ ├── base.py # Base, TimestampMixin, SoftDeleteMixin +│ ├── user.py +│ └── ... +├── services/ # 业务逻辑层(核心业务写在这里) +│ ├── auth.py # JWT 认证、密码哈希 +│ ├── credits.py # 积分扣减 +│ ├── operation_log.py # 操作日志 +│ └── ... +├── enums/ # 枚举定义(全部用 str, Enum) +│ ├── generation_status.py +│ ├── celery_queue.py +│ └── ... +├── tasks/ # Celery 异步任务 +│ ├── celery_app.py # Celery 实例 + 配置 +│ ├── cleanup.py +│ └── ... +├── middleware/ # FastAPI 中间件 +│ ├── logging.py +│ ├── rate_limit.py +│ ├── request_encrypt.py +│ ├── anti_crawler.py +│ └── ... +├── utils/ # 通用工具 +│ ├── id_gen.py # generate_id(), generate_order_no() +│ ├── exceptions.py # 自定义 HTTPException +│ └── security.py # AES 加密、HMAC +├── dependencies.py # FastAPI 依赖注入(get_db, get_current_user 等) +├── config.py # Pydantic Settings,读取 .env +└── main.py # FastAPI 应用入口、lifespan、种子数据 +``` + +### 2.2 分层约定(重要) + +请求处理严格遵循 **路由 → 服务 → 模型** 三层: + +``` +Route (api/) → Service (services/) → Model (models/) + 解析参数 业务逻辑 数据库操作 + 调用 service 编排调用 纯 CRUD + 返回序列化结果 不直接操作 ORM 无业务规则 +``` + +**禁止:** +- 路由中直接写复杂 SQL/ORM 操作 → 应抽到 service +- Model 文件中写业务逻辑 → model 只定义字段和关系 +- Service 中混入 HTTP 相关代码 → 不 import FastAPI 对象 + +### 2.3 SQLAlchemy 模型约定 + +使用 SQLAlchemy 2.0 风格的 `mapped_column`: + +```python +# app/models/project.py — 标准模板 +from sqlalchemy import ForeignKey, String +from sqlalchemy.orm import Mapped, mapped_column +from app.models.base import Base, TimestampMixin, SoftDeleteMixin + +class Project(Base, TimestampMixin, SoftDeleteMixin): + __tablename__ = "projects" + + id: Mapped[str] = mapped_column(String(32), primary_key=True) + user_id: Mapped[str] = mapped_column(String(32), ForeignKey("users.id", ondelete="CASCADE"), index=True) + name: Mapped[str] = mapped_column(String(128)) + industry: Mapped[str] = mapped_column(String(32)) +``` + +**约定:** +- 主键统一用 `String(32)`,由 `generate_id()` 生成(13 位时间戳 hex + 6 位随机 hex) +- 继承 `TimestampMixin` 自动获得 `created_at` / `updated_at` +- 需要软删除的继承 `SoftDeleteMixin` 获得 `deleted_at` +- 外键统一加 `index=True`,`ondelete="CASCADE"` 按需 +- 所有 `str` 类型指定长度:`String(32)` / `String(128)` / `String(255)` 等 +- 时间统一用 `DateTime(timezone=True)` +- **新增 model 后**必须在 `app/models/__init__.py` 中 import + +```python +# app/models/base.py — 公共 Mixin +class TimestampMixin: + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) + updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + +class SoftDeleteMixin: + deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True) +``` + +### 2.4 Pydantic Schema 约定 + +```python +# app/schemas/project.py +from pydantic import BaseModel, Field +from app.schemas.common import NaiveDatetime + +class ProjectCreate(BaseModel): # 请求入参(客户端 → 服务端) + name: str = Field(..., max_length=128) + industry: str = Field(..., max_length=64) + +class ProjectOut(BaseModel): # 响应序列化(服务端 → 客户端) + id: str + name: str + industry: str + created_at: NaiveDatetime + updated_at: NaiveDatetime + + model_config = {"from_attributes": True} # ← 必加!允许从 ORM 对象直接构造 +``` + +**命名约定:** +- `XxxCreate` — POST 创建请求 +- `XxxUpdate` — PUT 更新请求 +- `XxxOut` / `XxxResponse` — 响应 +- `XxxListOut` — 列表响应(含 items + total) +- `XxxRequest` — 通用请求 + +**公共工具:** +- `NaiveDatetime` / `NaiveDatetimeOptional` — 自动将带时区的 datetime 转为北京时间 naive datetime +- `PaginatedResponse` — 分页响应基类 + +### 2.5 API 路由约定 + +```python +# app/api/v1/projects.py — 标准模板 +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.dependencies import get_db, get_current_user +from app.models.user import User +from app.models.project import Project +from app.schemas.project import ProjectCreate, ProjectOut +from app.utils.id_gen import generate_id + +router = APIRouter(prefix="/projects", tags=["projects"]) # prefix 不含 /api + +@router.get("", response_model=list[ProjectOut]) # GET 列表用 list[XxxOut] +async def list_projects( + current_user: User = Depends(get_current_user), # 鉴权依赖 + db: AsyncSession = Depends(get_db), # 数据库 session +): + result = await db.execute( + select(Project).where( + Project.user_id == current_user.id, + Project.deleted_at.is_(None), # 软删除过滤 + ).order_by(Project.created_at.desc()) + ) + return result.scalars().all() # 直接返回 ORM 对象,Pydantic 自动序列化 + +@router.post("", response_model=ProjectOut) +async def create_project( + req: ProjectCreate, # 请求体自动校验 + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + project = Project( + id=generate_id(), # 主键手工生成 + user_id=current_user.id, + name=req.name, + industry=req.industry, + ) + db.add(project) + await db.flush() # flush 获取自增字段但不出事务 + return project +``` + +**关键约定:** +- `prefix` 不含 `/api`(`/api` 在 `main.py` 的 `include_router` 统一加) +- 受保护接口用 `Depends(get_current_user)` 获取当前用户 +- 管理员接口用 `Depends(get_admin_user)` +- 可选登录用 `Depends(get_optional_current_user)` +- 查询过滤软删除: `.where(Model.deleted_at.is_(None))` +- 返回 ORM 对象时 Schema 必须有 `model_config = {"from_attributes": True}` +- 错误抛 `HTTPException` 或用自定义异常(见 `app/utils/exceptions.py`) + +### 2.6 依赖注入约定 + +```python +# app/dependencies.py +get_db # 生成 AsyncSession,自动 commit/rollback +get_current_user # 验证 JWT → 返回 User,要求密码已设置 +get_current_user_allow_password_pending # 验证 JWT → 返回 User(允许未设置密码) +get_optional_current_user # 验证 JWT → User | None(不强制登录) +get_admin_user # 验证 JWT + is_admin + user_type=="admin" +get_backend_user # 验证 JWT + user_type=="admin"(不要求 is_admin) +``` + +### 2.7 服务层约定 + +```python +# app/services/team_service.py — 标准模板 +from __future__ import annotations # 前向引用必需的 import +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from app.models.team import Team +from app.schemas.team import TeamCreate, TeamUpdate +from app.utils.id_gen import generate_id + +async def create_team(db: AsyncSession, req: TeamCreate) -> Team: + team = Team( + id=generate_id(), + name=req.name, + code=req.code, + description=req.description, + ) + db.add(team) + await db.flush() + return team +``` + +**约定:** +- 函数签名统一 `(db: AsyncSession, ...) → Model | list[Model] | dict` +- 不处理 HTTP 异常,HTTP 相关处理留给 route 层 +- 用 `from __future__ import annotations` 支持前向引用 +- 纯查询类服务可以接收 `db` 作为第一个参数 + +### 2.8 枚举约定 + +```python +# app/enums/generation_status.py +from enum import Enum + +class GenerationStatus(str, Enum): # 继承 str 使值可直接序列化 + prompt_optimized = "prompt_optimized" + generating = "generating" + completed = "completed" + failed = "failed" + +class GenerationType(str, Enum): + video = "video" + image = "image" +``` + +**约定:** +- 所有枚举继承 `str, Enum`(可序列化为 JSON) +- 枚举值用 snake_case +- 枚举文件统一放 `app/enums/` +- 在 schema 中引用枚举做校验: `gen_type: GenerationType = Field(...)` + +### 2.9 异常处理约定 + +```python +# app/utils/exceptions.py — 预定义异常 +InsufficientCreditsError # 402 积分不足 +CaptchaFailedError # 400 验证码失败 +RecordNotFoundError # 404 记录不存在 +ProjectNotFoundError # 404 项目不存在 +InvalidStatusError # 400 状态不允许操作 + +# 在 service 或 route 中抛出 +raise InsufficientCreditsError() +``` + +### 2.10 认证约定 + +```python +# Token 创建 +create_access_token(user_id, remember_me=False) → str # JWT 令牌 + +# Token 解码 +decode_access_token(token) → str | None # 返回 user_id 或 None + +# 密码 +hash_password(plain) → str # bcrypt 哈希 +verify_password(plain, hashed) → bool # bcrypt 校验 +``` + +**鉴权流程:** +1. 前端 Authorization header: `Bearer ` +2. `HTTPBearer` 提取 token → `decode_access_token` 获取 `user_id` +3. 查 User 表验证 `is_active` +4. 检查 `user_must_set_password`(短信注册用户首次需设置密码) + +### 2.11 数据库 Session 模式 + +```python +# dependencies.py 中的 get_db — 自动管理事务 +async def get_db(): + async with async_session() as session: + try: + yield session + await session.commit() # 成功自动提交 + except Exception: + await session.rollback() # 异常自动回滚 + raise + finally: + await session.close() +``` + +**注意:** 在 route 中如果只读查询不需要手动 commit(get_db 自动处理)。在 service 中做写入时需要调用者负责 commit,或由 get_db 处理。 + +### 2.12 中间件约定 + +中间件按**注册顺序**从外到内执行(`main.py`): + +```python +application.add_middleware(RequestLoggingMiddleware) # 1. 请求日志 +application.add_middleware(AntiCrawlerMiddleware) # 2. 反爬虫 +application.add_middleware(RateLimitMiddleware) # 3. 限流 (Redis) +application.add_middleware(RequestEncryptMiddleware) # 4. 加密/解密 +application.add_middleware(CORSMiddleware, ...) # 5. CORS +``` + +**加密中间件行为:** +- 请求有 `X-Encrypted: true` header → 解密请求体 +- GET 请求无 body 但**响应仍加密** +- 支付回调路径白名单跳过加密 (`/payments/alipay/callback`, `/payments/wechat/callback`) + +### 2.13 Celery 任务约定 + +```python +# app/tasks/cleanup.py — 标准模板 +from app.tasks.celery_app import celery_app + +@celery_app.task # 装饰器注册任务 +def cleanup_expired_video_urls(): + """Run hourly. Clear expired video URL tokens.""" + asyncio.run(_cleanup_urls()) # 同步任务内跑异步代码 + +async def _cleanup_urls(): # 实际逻辑写在 async 函数里 + from app.models.base import async_session # 延迟导入避免循环 + async with async_session() as db: + ... + await db.commit() +``` + +**约定:** +- 任务装饰器: `@celery_app.task` +- 同步入口 → `asyncio.run()` 包异步逻辑 +- 任务名自动生成: `文件名.函数名`(如 `app.tasks.cleanup.cleanup_expired_video_urls`) +- 队列路由在 `celery_app.py` 的 `task_routes` 配置 +- 必需参数通过 `apply_async(args=[...], queue="xxx", priority=0)` 传递 + +### 2.14 配置约定 + +```python +# app/config.py — 基于 pydantic-settings +from pydantic_settings import BaseSettings + +class Settings(BaseSettings): + model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8") + DATABASE_URL: str = "sqlite+aiosqlite:///./videogen.db" + SECRET_KEY: str = "change-me" + # ... 所有配置项有默认值,生产通过 .env 覆盖 + +settings = Settings() # 全局单例 +``` + +**约定:** +- 所有环境变量在 `config.py` 中有类型注解和默认值 +- 通过 `settings.XXX` 全局访问 +- 布尔值用 `bool` 类型(pydantic 自动解析 "true"/"false" 字符串) +- 列表值用 `list[str]`(pydantic 自动解析 JSON 数组字符串) + +### 2.15 操作日志约定 + +```python +from app.services.operation_log import log_operation + +# 在路由中调用(通常在变更操作后) +await log_operation( + db, + admin.id, + admin.username, + f"创建团队 {team.name}", # action 描述 + "POST", # HTTP method + "/admin/teams", # path + detail=None, # 可选详情 + ip=None, # 可选 IP +) +``` + +--- + +## 三、前端架构 (video-gen-app & video-gen-admin) + +### 3.1 相同点 + +两个前端项目遵循完全相同的架构约定: +- React 19 + TypeScript + Vite 8 + Ant Design 6 + Tailwind CSS 3 +- Zustand 状态管理 +- 统一的 API 调用层(crypto/client/index 三层) +- 路由结构 SPA(react-router-dom v7) + +### 3.2 目录结构(以 video-gen-app 为例) + +``` +src/ +├── api/ # API 调用层 +│ ├── client.ts # 核心:apiRequest(加密/解密、错误处理) +│ ├── crypto.ts # AES-GCM 加密/解密(Web Crypto API) +│ ├── index.ts # 所有业务 API 函数(按业务域分组) +│ └── mock.ts # Mock 数据(VITE_USE_MOCK=true 时使用) +├── store/ # Zustand 状态 +│ ├── useAuthStore.ts # 认证状态(登录/登出/当前用户) +│ └── useAppStore.ts # 应用状态 +├── types/ # TypeScript 类型定义 +│ └── index.ts # 所有 interface/type +├── pages/ # 页面组件(路由对应) +│ ├── LoginPage.tsx +│ ├── ProjectsPage.tsx +│ └── ... +├── components/ # 可复用组件 +│ ├── Layout/ +│ │ ├── AppLayout.tsx # 主布局(Header + Sider + Content) +│ │ └── AppLayout.css +│ ├── privatePortrait/ +│ └── ... +├── utils/ # 工具函数 +│ ├── formatDate.ts +│ └── ... +├── App.tsx # 路由定义 + 全局配置 +└── main.tsx # 入口 +``` + +### 3.3 API 调用约定(核心) + +```typescript +// ── client.ts — 核心请求函数 ── +// 所有请求统一走 apiRequest,自动处理加密和响应解析 +async function apiRequest(path: string, options: RequestOptions): Promise + +// 调用参数: +interface RequestOptions { + method?: string // GET/POST/PUT/DELETE,默认 GET + body?: unknown // 请求体(自动 JSON 序列化 + 加密) + auth?: boolean // 是否带 Authorization header,默认 true + encryptBody?: boolean // 是否加密,默认跟随全局开关 + signal?: AbortSignal // 取消请求 + skipAuthRedirect?: boolean // 401 时不跳转登录页 +} +``` + +```typescript +// ── index.ts — 业务 API 函数 ── +// 命名约定:动词 + 业务名,camelCase +export async function login(username: string, password: string): Promise { + if (USE_MOCK) return mock.mockLogin({ username, password }); + const res = await api.post<{ accessToken: string; user: User }>( + '/auth/login', + { username, password }, + false, // auth: false(登录不需要 token) + true // encryptBody: true(登录凭证需要加密) + ); + setToken(res.accessToken); + return res.user; +} + +// GET 列表 +export async function getProjects(): Promise { + if (USE_MOCK) return mock.mockGetProjects(); + return api.get('/projects'); +} + +// POST 创建 +export async function createProject(name: string, industry: Industry): Promise { + if (USE_MOCK) return mock.mockCreateProject(name, industry); + return api.post('/projects', { name, industry }); +} + +// 带查询参数的 GET +export async function getRecordsPage(params): Promise { + const query = new URLSearchParams(); + if (params.projectId) query.set('project_id', params.projectId); + query.set('page', String(page)); + return api.get(`/generation-records?${query.toString()}`); +} +``` + +**关键约定:** +- `api.get/post/put/delete(path, body?, auth?, encryptBody?)` — 返回**已解析的 T 类型数据**(不含响应包裹层) +- 后端返回 `{ data: ... }` 的结构,`apiRequest` 会自动解包 +- 所有 id 字段前端用 `string`(不转 number) +- 后端 `snake_case` 字段在 `client.ts` 自动转 `camelCase`(user_id → userId) +- 文件上传不走 `apiRequest`,直接用 `fetch` + `FormData`(见 `uploadAudio/uploadImage` 等) +- 上传相关 API 直接读 `import.meta.env.VITE_API_BASE` 拼 URL + +### 3.4 状态管理约定(Zustand) + +```typescript +// store/useAuthStore.ts — 标准模板 +import { create } from 'zustand'; +import type { User } from '../types'; +import * as api from '../api'; + +interface AuthState { + user: User | null; + loading: boolean; + login: (username: string, password: string) => Promise; + logout: () => Promise; + checkAuth: () => Promise; // 启动时验证 token 有效性 +} + +export const useAuthStore = create((set) => ({ + user: null, + loading: true, + + login: async (username, password) => { + const user = await api.login(username, password); + set({ user }); + }, + // ... +})); +``` + +**约定:** +- 全局状态用 Zustand `create`,不用 Redux +- API 调用写在 store actions 或页面中,**不要**写在 components 里 +- 状态更新用 `set()`,异步操作加 `async/await` +- 组件中消费: `const { user, login } = useAuthStore()` + +### 3.5 页面组件约定 + +```typescript +// pages/ProjectsPage.tsx — 标准模板 +import React, { useEffect, useState } from 'react'; +import { Button, Empty, Form, Input, message, Modal, Typography } from 'antd'; +import { PlusOutlined } from '@ant-design/icons'; +import { useNavigate } from 'react-router-dom'; +import { useAppStore } from '../store/useAppStore'; +import { getIndustries } from '../api'; +import type { IndustryConfig } from '../types'; + +const ProjectsPage: React.FC = () => { + const navigate = useNavigate(); + const [projects, setProjects] = useState([]); + const [loading, setLoading] = useState(false); + + const loadProjects = async () => { + setLoading(true); + try { + const data = await getProjects(); // 调 API + setProjects(data); + } catch (e: any) { + message.error(e?.message || '加载失败'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { loadProjects(); }, []); + + return ( +
+ 我的项目 + {/* ... */} +
+ ); +}; + +export default ProjectsPage; +``` + +**约定:** +- 页面组件是 `React.FC`,默认导出 +- 业务数据类型从 `../types` 导入(`import type`) +- API 函数从 `../api` 导入 +- 不用 `axios`,所有请求走 `api/index.ts` +- 用 `message.success/error/warning` 显示操作反馈 +- 用 `useNavigate()` 编程式导航 +- 列表加载用 `loading` 状态 + `Spin` 组件 +- 错误统一 `catch (e: any)` + `message.error(e?.message || '默认消息')` + +### 3.6 路由约定 + +```typescript +// App.tsx — 路由结构 +// 公开路由 +} /> +} /> + +// 受保护路由(需要登录) +}> + } /> + } /> + } /> + } /> + +``` + +**约定:** +- 路由定义集中在 `App.tsx` +- 登录保护通过 `ProtectedRoute` 包装器实现(检查 `useAuthStore.user`) +- 未登录用户访问受保护路由 → 跳 `/login` +- 动态路由参数用 `:paramName` +- 嵌套路由用 `` 渲染子页面 + +### 3.7 TypeScript 类型约定 + +```typescript +// types/index.ts — 全局类型定义 +export interface User { + id: string; // 所有 ID 都是 string + username: string; + credits: number; + // ... +} +``` + +**约定:** +- 全局共享类型在 `types/index.ts` 定义 +- 类型/接口名 PascalCase + 业务含义 +- 所有 ID、外键字段类型为 `string` +- 可选字段用 `?:`,不可选但不一定传的用联合类型 `string | null` +- 从其他文件导入类型: `import type { ... } from '../types'`(用 `import type` 优化打包) + +### 3.8 加密约定 + +```typescript +// crypto.ts +const USE_ENCRYPTION = !!import.meta.env.VITE_ENCRYPTION_KEY && isCryptoAvailable(); + +// 前端加密流程: +// 1. apiRequest 中如果 encryptBody=true: +// body → JSON.stringify → encrypt() → { data: "<密文>" } +// headers['X-Encrypted'] = 'true' +// 2. 收到响应如果 encryptBody=true 且 parsed.data 是字符串: +// decrypt(parsed.data) → JSON.parse → 返回 +// +// 自动判断: USE_ENCRYPTION 全局开关控制是否启用加密 +// 加密算法: AES-256-GCM (12 字节 IV + 128 位认证标签) +// 密钥来源: VITE_ENCRYPTION_KEY (32 字节 base64) +``` + +### 3.9 Mock 模式约定 + +当 `VITE_USE_MOCK=true` 时,前端走本地假数据,无需后端。 + +```typescript +// api/index.ts 中每个 API 函数都检查 USE_MOCK +const USE_MOCK = import.meta.env.VITE_USE_MOCK === 'true'; + +export async function getProjects(): Promise { + if (USE_MOCK) return mock.mockGetProjects(); // ← 先看 mock + return api.get('/projects'); // ← 后走真实请求 +} +``` + +**新增 API 函数时**必须同时提供 mock 数据函数(在 `mock.ts` 中)。 + +--- + +## 四、跨项目约定(前后端协作) + +### 4.1 字段名转换 + +| 后端 (Python) | 前端 (TypeScript) | 转换 | +|--------------|-------------------|------| +| `snake_case` | `camelCase` | client.ts 自动转 | +| `created_at` | `createdAt` | 自动 | +| `user_id` | `userId` | 自动 | + +**新增 API 时**: +- 后端 request schema 字段用 `snake_case` +- 前端 types/interface 字段用 `camelCase` +- **不需要**手动转,`client.ts` 的 `keysToCamel()` 递归转换所有 key + +### 4.2 API 路径约定 + +``` +/api/auth/login → 登录(公开) +/api/projects → 项目管理(需登录) +/api/admin/users → 用户管理(需管理员) +/admin/teams → 团队管理(需管理员,二级路由) +``` + +- `/api/` 前缀统一 +- 管理接口路径含 `/admin/` +- 路径用小写 + 短横线(kebab-case) + +### 4.3 分页约定 + +后端返回: +```python +@router.get("", response_model=GenerationRecordPageListOut) +# { items: [...], total: 100, page: 1, page_size: 20 } +``` + +前端类型: +```typescript +export interface GenerationRecordPageListOut { + page: number; + pageSize: number; + total: number; + items: GenerationRecord[]; +} +``` + +### 4.4 错误处理约定 + +后端抛 `HTTPException(status_code, detail)` → 前端 `apiRequest` 捕获后抛 `Error` → 页面 `catch` + `message.error(e.message)`。 + +常见错误码: +- `401` — token 无效/过期 → 自动跳 `/login` +- `403` — 权限不足 +- `404` — 资源不存在 +- `402` — 积分不足(`InsufficientCreditsError`) +- `400` — 参数错误 + +--- + +## 五、修改检查清单 + +### 新增后端接口时 ✅ + +- [ ] 在 `app/schemas/` 中定义 Request/Response Schema(含 `from_attributes`) +- [ ] 在 `app/services/` 中实现业务逻辑函数 +- [ ] 在 `app/api/v1/` (或 `app/api/admin/`) 中定义路由(`APIRouter` + `Depends`) +- [ ] 确认 `__init__.py` 中注册了新 router +- [ ] 复杂操作记录 `log_operation()` +- [ ] 写入操作后正确 `flush()` + `commit()` + +### 新增后端 Model 时 ✅ + +- [ ] 在 `app/models/` 中新建或编辑 model 文件 +- [ ] 继承 `Base` + 按需加 `TimestampMixin` / `SoftDeleteMixin` +- [ ] 主键用 `generate_id()`,外键加 `index=True` +- [ ] 在 `app/models/__init__.py` 中 import +- [ ] 执行 `alembic revision --autogenerate -m "描述"` 生成迁移 +- [ ] 执行 `alembic upgrade head` 应用迁移 + +### 新增前端页面/功能时 ✅ + +- [ ] 在 `types/index.ts` 中定义/补充类型 +- [ ] 在 `api/index.ts` 中添加 API 函数(含 mock 分支) +- [ ] 在 `pages/` 中新建页面组件(`React.FC` + 默认导出) +- [ ] 在 `App.tsx` 中注册路由 +- [ ] 受保护路由包裹 `` +- [ ] 在 `mock.ts` 中添加对应 mock 数据函数 + +--- + +## 六、常见反模式(应避免) + +| ❌ 反模式 | ✅ 正确做法 | +|----------|-----------| +| Route 中写复杂 ORM 查询 | 抽到 `services/` 函数 | +| Model 中有业务逻辑 | Model 只定义字段和关系 | +| 前端直接用 `fetch` 调后端 API | 走 `api/index.ts` + `apiRequest` | +| 新增 Model 不加到 `__init__.py` | 立即 import | +| 主键用数据库自增 int | 用 `generate_id()` 生成 string | +| Schema 不加 `from_attributes` | 响应 Schema 必须加 | +| 前端用 `any` 不定义类型 | 在 `types/index.ts` 定义 | +| 密码明文存储 | bcrypt 哈希(通过 `hash_password`) | + +--- + +## 七、快速定位表 + +| 想修改什么 | 去哪里 | +|-----------|--------| +| 新增/改表字段 | `app/models/xxx.py` + `alembic revision` | +| 新增接口 | `app/api/v1/xxx.py` + `app/schemas/xxx.py` + `app/services/xxx.py` | +| 改业务规则 | `app/services/xxx.py` | +| 改枚举值 | `app/enums/xxx.py` | +| 加新页面 | `src/pages/XxxPage.tsx` + `App.tsx` 路由 + `src/types/index.ts` | +| 加新 API 调用 | `src/api/index.ts` + `src/api/mock.ts` | +| 改全局状态 | `src/store/useAuthStore.ts` / `useAppStore.ts` | +| 改配置项 | `app/config.py` + `.env.example` | +| 改中间件行为 | `app/middleware/xxx.py` | +| 加异步任务 | `app/tasks/xxx.py` + `celery_app.py` 的 `task_routes` | \ No newline at end of file diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 05376543..9acde179 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -5,11 +5,13 @@ ``` video_item/ ├── video-gen-api/ # 后端 (Python FastAPI + SQLAlchemy + Alembic) -├── video-gen-app/ # 前台 (React 19 + Vite 8 + Ant Design 6 + Tailwind) -├── video-gen-admin/ # 后台管理 (React 19 + Vite 8 + Ant Design 6) +├── video-gen-app/ # 前台/用户端 (React 19 + Vite 8 + Ant Design 6) +├── video-gen-admin/ # 后台管理端 (React 19 + Vite 8 + Ant Design 6) └── DEPLOYMENT.md # 本文档 ``` +**三个前端/后端的关系:** `video-gen-app` (用户前台) 和 `video-gen-admin` (管理后台) 都连接同一个 `video-gen-api` 后端。 + --- ## 一、环境要求 @@ -18,14 +20,13 @@ video_item/ |------|---------|------| | Python | >= 3.10 | 推荐 3.12 | | Node.js | >= 18 | 推荐 20+ | -| PostgreSQL | >= 14 | 推荐 16 | -| Redis | >= 6 | 可选,推荐用于限流/验证码/Celery | -| FFmpeg | 任意 | 可选,用于视频封面截帧 | -| alipay-sdk-python | >=3.7.1160 | 可选,用于支付 | -| wechatpayv3 | >=2.0.2 | 可选,用于支付 | -| volcengine-python-sdk | >=1.1.0 | 可选,用于视频生成 | +| PostgreSQL | >= 14 | 推荐 16;**必须** | +| Redis | >= 6 | 推荐用于: 限流/验证码/Celery/任务状态 | +| FFmpeg | 任意 | 用于视频封面截帧,留空时从 PATH 自动查找 | | ca-certificates | 任意 | **必须**,HTTPS 请求需要(新服务器/容器常缺) | - +| alipay-sdk-python | >=3.7.1160 | 可选,支付宝支付 | +| wechatpayv3 | >=2.0.2 | 可选,微信支付 | +| volcengine-python-sdk | >=1.1.0 | 可选,视频生成/短信 | --- @@ -34,7 +35,7 @@ video_item/ ### 1. 安装依赖 ```bash -# ⚠️ 新服务器/容器必须先装 CA 证书,否则 HTTPS 请求(支付宝/火山等)全部失败 +# ⚠️ 新服务器/容器必须先装 CA 证书,否则 HTTPS(支付宝/火山等)全部失败 # CentOS/RHEL sudo yum install -y ca-certificates # Ubuntu/Debian @@ -49,7 +50,10 @@ python -m venv .venv # Linux/Mac source .venv/bin/activate -# 安装基础依赖 + PostgreSQL 驱动 +# 安装基础依赖 +pip install -e . + +# 安装 PostgreSQL 驱动(生产环境必须) pip install -e ".[pg]" # 如需 Redis 支持(限流、验证码、Celery) @@ -58,21 +62,19 @@ pip install -e ".[pg,redis]" # 如需 Celery 异步任务(ChatAPI 生成流水线) pip install -e ".[pg,redis,celery]" -#安装阿里支付sdk +# 如需支付宝 pip install -e ".[pg,redis,celery,alipay]" -# 安装微信支付sdk -pip install -e ".[pg,redis,celery,alipay,wechatpayv3]" - -#安装火山sdk -pip install -e ".[pg,redis,celery,alipay,wechatpayv3,volc]" - +# 如需微信支付 +pip install -e ".[pg,redis,celery,alipay,wxpay]" +# 如需火山引擎 SDK(短信等) +pip install -e ".[pg,redis,celery,alipay,wxpay,volc]" ``` ### 2. 配置环境变量 -复制 `.env.example` 为 `.env`,修改以下关键配置: +复制 `.env.example` 为 `.env`,修改关键配置: ```bash cp .env.example .env @@ -81,13 +83,14 @@ cp .env.example .env ```ini # ── 基础配置 ── APP_NAME=VideoGen API +APP_VERSION=1.0.0 DEBUG=false -SECRET_KEY=改成一个随机的长字符串 +SECRET_KEY=改成一个随机的长字符串(JWT 签名密钥) -# ── 数据库 ── +# ── 数据库(必须) ── DATABASE_URL=postgresql+asyncpg://用户名:密码@localhost:5432/videogen -# ── Redis(可选,留空则禁用限流和验证码) ── +# ── Redis(可选,留空则禁用限流/验证码/Celery) ── REDIS_URL=redis://localhost:6379/0 # ── JWT ── @@ -101,24 +104,30 @@ SEEDANCE_API_BASE=https://ark.cn-beijing.volces.com/api/v3 SEEDANCE_CALLBACK_URL=https://你的域名/api/generation-records/callback # ── LLM 提示词优化 ── -LLM_MOCK=false +LLM_API_BASE=https://api.openai.com/v1 +LLM_API_KEY= +LLM_MODEL=gpt-4o +LLM_MOCK=true # true=使用 mock 响应,不调用真实 LLM # ── 前后端通信加密(32字节 base64,留空则禁用) ── ENCRYPTION_KEY=你的32字节base64密钥 -# ── 存储路径 ── +# ── 存储路径(本地存储) ── STORAGE_TYPE=local STORAGE_LOCAL_PATH=./storage/generate/videos STORAGE_IMAGE_LOCAL_PATH=./storage/generate/images STORAGE_VIDEO_COVER_LOCAL_PATH=./storage/generate/covers UPLOAD_LOCAL_PATH=./storage/uploads -# ── 跨域(生产环境务必限制域名) ── +# ── 跨域(生产环境务必限制域名,默认 ["*"] 允许所有) ── CORS_ORIGINS=["https://你的前台域名.com", "https://你的后台域名.com"] # ── 回调基础地址 ── BASE_URL=https://你的域名 +# ── 验证码 ── +CAPTCHA_ENABLED=true + # ── 短信(火山引擎 SDK,SMS_MOCK=false 时生效) ── SMS_MOCK=true VOLC_SMS_ACCESS_KEY_ID= @@ -129,6 +138,12 @@ VOLC_SMS_SIGN=短信签名 # ── 支付(PAYMENT_MOCK=false 时生效) ── PAYMENT_MOCK=true +WECHAT_MCH_ID= +WECHAT_API_KEY= +ALIPAY_APP_ID= +ALIPAY_PRIVATE_KEY= +ALIPAY_PUBLIC_KEY= +ALIPAY_NOTIFY_URL= # ── Celery(可选,留空则禁用 ChatAPI 异步流水线) ── CELERY_BROKER_URL=redis://localhost:6379/5 @@ -138,27 +153,30 @@ CELERY_RESULT_BACKEND=redis://localhost:6379/6 FFMPEG_BIN=/usr/bin/ffmpeg ``` +> **关于 ENCRYPTION_KEY 的生成:** 需要 32 字节(256 位)的 base64 编码字符串。生成方式:`openssl rand -base64 32`。前后端必须使用**完全相同**的密钥。 + ### 3. 初始化数据库 ```bash # 创建 PostgreSQL 数据库 psql -U postgres -c "CREATE DATABASE videogen OWNER videogen;" -# 启动后端(首次启动自动建表 + 填充种子数据) +# 启动后端(首次启动自动建表) python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 ``` -首次启动会自动完成: -- 创建所有数据表(23 个 model) -- 创建管理员账号:`admin` / `123456` -- 创建演示用户:`demo` / `123456`(手机号 `13888888888`) -- 填充系统配置、视频引擎(Seedance 2.0 / 2.0 fast)、图片引擎(Seedream 5.0)、模型配置、积分比例、菜单配置、充值套餐(4 档)、行业配置 +首次启动时会**自动创建所有数据表**(41 个 model)。 -**首次部署后务必修改默认密码。** +> ⚠️ **关于种子数据:** 代码中包含 `_seed_data()` 函数(创建管理员/演示用户、系统配置、引擎配置等),但当前在 `main.py` 中被**注释掉了**(`# await _seed_data()`)。因此首次启动**不会**自动创建管理员账号。 +> +> **如果你需要种子数据,**有以下选择: +> 1. 在 `main.py` 中取消注释 `# await _seed_data()` 后重启 +> 2. 手动通过 API 或数据库脚本创建管理员账号 +> 3. 自行编写独立的种子脚本调用 `_seed_data()` ### 4. 数据库迁移 (Alembic) -项目使用 Alembic 管理数据库结构变更,`env.py` 已导入全部 20 个 model。 +项目使用 Alembic 管理数据库结构变更。 ```bash cd video-gen-api @@ -179,17 +197,13 @@ python -m alembic history python -m alembic downgrade -1 ``` -**部署流程:** 拉取代码后先执行 `alembic upgrade head`,再重启后端服务。 - -**新增 model 时:** 需要在 `alembic/env.py` 中添加对应的 import。 +**部署流程:** 拉取代码后先执行 `alembic upgrade head`,再重启后端服务。 ### 5. 生产运行 ```bash -# 方式一:直接运行(推荐 4 workers) +# 直接运行(推荐 4 workers) python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4 - -# 方式二:使用 systemd (Linux) ``` **systemd 服务文件** `/etc/systemd/system/videogen-api.service`: @@ -220,11 +234,22 @@ sudo systemctl start videogen-api ### 6. Celery Worker(可选) -ChatAPI 异步生成流水线需要 Celery Worker。需要 Redis 作为 Broker。 +ChatAPI 异步生成流水线需要 Celery Worker,依赖 Redis 作为 Broker。 + +Celery 使用 **6 个队列**,按功能分离: + +| 队列 | 用途 | +|------|------| +| `gen_chatapi_create` | ChatAPI 生成任务创建(含爆款开头/拆镜复刻的提词步骤) | +| `gen_provider_poll` | 轮询火山引擎生成状态 | +| `gen_result_download` | 下载生成的视频/图片结果 | +| `gen_recovery` | 容灾恢复任务(统一队列,避免占用业务 worker) | +| `gen_private_portrait` | 真人素材认证与同步 | +| `default` | 默认队列(用户 OAuth、清理任务等) | ```bash -# 启动 Worker(处理 3 个队列:gen_chatapi_create, gen_provider_poll, gen_result_download) -celery -A app.tasks.celery_app worker -l info -Q gen_chatapi_create,gen_provider_poll,gen_result_download,default +# 启动 Worker(消费所有队列) +celery -A app.tasks.celery_app worker -l info -Q gen_chatapi_create,gen_provider_poll,gen_result_download,gen_recovery,gen_private_portrait,default ``` **systemd 服务文件** `/etc/systemd/system/videogen-worker.service`: @@ -239,7 +264,7 @@ Type=simple User=www-data WorkingDirectory=/opt/video-gen-api Environment=PATH=/opt/video-gen-api/.venv/bin -ExecStart=/opt/video-gen-api/.venv/bin/celery -A app.tasks.celery_app worker -l info -Q gen_chatapi_create,gen_provider_poll,gen_result_download,default +ExecStart=/opt/video-gen-api/.venv/bin/celery -A app.tasks.celery_app worker -l info -Q gen_chatapi_create,gen_provider_poll,gen_result_download,gen_recovery,gen_private_portrait,default Restart=always RestartSec=5 @@ -249,23 +274,25 @@ WantedBy=multi-user.target ### 7. Docker 部署(可选) -项目提供 `Dockerfile` 和 `docker-compose.yml`,一键启动完整环境: +项目提供 `Dockerfile` 和 `docker-compose.yml`。 + +> ⚠️ **注意:** 默认 `Dockerfile` 只安装基础依赖(`pip install .`),生产使用需改为 `".[pg,redis,celery]"`。 ```bash cd video-gen-api -# 注意:Dockerfile 默认只装基础依赖,需修改为安装 pg+redis+celery -# 将 Dockerfile 中的 RUN pip install --no-cache-dir . 改为: +# 使用前需修改 Dockerfile 第 6 行为: # RUN pip install --no-cache-dir ".[pg,redis,celery]" docker compose up -d ``` 启动的服务: + | 服务 | 端口 | 说明 | |------|------|------| -| api | 8000 | FastAPI 应用(带 `--reload`,开发模式) | -| worker | - | Celery Worker | +| api | 8000 | FastAPI 应用(`--reload` 开发模式) | +| worker | - | Celery Worker(消费所有队列) | | postgres | 5432 | PostgreSQL 16 | | redis | 6379 | Redis 7 | @@ -276,7 +303,7 @@ server { listen 80; server_name api.yourdomain.com; - # 上传文件大小限制 + # 上传文件大小限制(视频上传需要较大值) client_max_body_size 100M; location / { @@ -300,6 +327,8 @@ server { ## 三、前台部署 (video-gen-app) +用户前台,面向最终用户。 + ### 1. 安装依赖 & 构建 ```bash @@ -310,8 +339,8 @@ npm install # 配置 API 地址(创建 .env.production) echo "VITE_API_BASE=https://api.yourdomain.com" > .env.production -# 如需前后端加密通信 -echo "VITE_ENCRYPTION_KEY=与后端ENCRYPTION_KEY相同" >> .env.production +# 如需前后端加密通信(与后端 ENCRYPTION_KEY 相同) +echo "VITE_ENCRYPTION_KEY=密钥" >> .env.production # 构建 npm run build @@ -319,7 +348,15 @@ npm run build 构建产物在 `dist/` 目录。 -### 2. Nginx 配置 +### 2. 前端环境变量 + +| 变量 | 说明 | 默认值 | +|------|------|--------| +| `VITE_API_BASE` | 后端 API 地址 | `http://localhost:8000` | +| `VITE_USE_MOCK` | 是否使用 mock 数据(无需后端) | `false` | +| `VITE_ENCRYPTION_KEY` | 前后端通信加密密钥(需与后端一致) | 空(不加密) | + +### 3. Nginx 配置 ```nginx server { @@ -328,12 +365,12 @@ server { root /opt/video-gen-app/dist; index index.html; - # SPA 路由 + # SPA 路由:所有页面请求回退到 index.html location / { try_files $uri $uri/ /index.html; } - # 静态资源缓存 + # 静态资源缓存(带 hash 的文件名可长期缓存) location /assets/ { expires 1y; add_header Cache-Control "public, immutable"; @@ -345,6 +382,8 @@ server { ## 四、后台管理部署 (video-gen-admin) +管理员后台,面向运营/管理人员。 + ### 1. 安装依赖 & 构建 ```bash @@ -356,13 +395,21 @@ npm install echo "VITE_API_BASE=https://api.yourdomain.com" > .env.production # 如需前后端加密通信 -echo "VITE_ENCRYPTION_KEY=与后端ENCRYPTION_KEY相同" >> .env.production +echo "VITE_ENCRYPTION_KEY=密钥" >> .env.production # 构建 npm run build ``` -### 2. Nginx 配置 +### 2. 后台环境变量 + +| 变量 | 说明 | 默认值 | +|------|------|--------| +| `VITE_API_BASE` | 后端 API 地址 | `http://localhost:8000` | +| `VITE_USE_MOCK` | 是否使用 mock 数据 | `false` | +| `VITE_ENCRYPTION_KEY` | 前后端通信加密密钥 | 空(不加密) | + +### 3. Nginx 配置 ```nginx server { @@ -456,6 +503,8 @@ sudo certbot --nginx -d yourdomain.com -d admin.yourdomain.com -d api.yourdomain ## 七、默认账号 +> ⚠️ 默认账号仅在种子数据被执行后存在(见第三节第 3 点说明)。 + | 角色 | 用户名 | 手机号 | 密码 | 积分 | |------|--------|--------|------|------| | 管理员 | admin | 13800000000 | 123456 | 10000 | @@ -467,32 +516,84 @@ sudo certbot --nginx -d yourdomain.com -d admin.yourdomain.com -d api.yourdomain ## 八、架构说明 -### 异步任务处理 +### 后端启动时的后台任务 -系统有两套异步任务机制: +后端启动时(`lifespan`)会自动启动以下**进程内**后台任务: -| 机制 | 说明 | 依赖 | -|------|------|------| -| 内置 TaskQueue | asyncio 实现,运行在 uvicorn 进程内,轮询视频/图片生成状态 | 无额外依赖 | -| Celery Worker | 处理 ChatAPI 异步生成流水线,3 个队列分优先级 | Redis 作为 Broker | +| 任务 | 说明 | +|------|------| +| `task_queue` (video_queue) | 内置 asyncio 任务队列,轮询视频/图片生成状态 | +| `upload_queue` | 上传任务队列 | +| `material_consumption_queue` | 素材消耗队列 | +| `token_refresh_scheduler` | 每 5 分钟检查并刷新即将过期的 token | +| `poll_pre_test_results` | 每分钟轮询前测结果 | +| `schedule_daily_sync` | 每天 9 点自动同步素材消耗 | +| `_order_expiry_loop` | 每分钟同步待支付订单状态 + 自动过期订单 | -### 中间件栈(从外到内) +加上 **Celery Worker**(可选,处理 ChatAPI 异步流水线)。 + +### 中间件栈(从外到内,即请求到达的顺序) 1. `RequestLoggingMiddleware` — 请求/响应日志记录 2. `AntiCrawlerMiddleware` — 反爬虫(拦截空 UA 和常见 bot) 3. `RateLimitMiddleware` — 滑动窗口限流(Redis 支撑) 4. `RequestEncryptMiddleware` — AES-256-GCM 请求/响应加密 -5. `CORSMiddleware` — 跨域 +5. `CORSMiddleware` — 跨域(expose `X-Encrypted` 响应头) -### 日志系统 +### 前后端通信加密 -| 日志类型 | 目录 | 控制方式 | +**机制:** AES-256-GCM 对称加密,前后端共享同一个 `ENCRYPTION_KEY`。使用 Web Crypto API(前端)和 `cryptography` 库(后端)。 + +**生效条件:** 同时满足以下两个条件才启用加密: +- 后端 `.env` 中 `ENCRYPTION_KEY` 非空 +- 前端 `.env` 中 `VITE_ENCRYPTION_KEY` 非空 +- 运行在安全上下文(HTTPS 或 localhost) + +**加密范围:** + +| 请求类型 | 请求体 | 响应体 | +|---------|--------|--------| +| GET(无 body) | 不加密(无内容) | **加密** | +| POST/PUT/DELETE(有 body) | **加密** | **加密** | + +**工作流:** +1. 前端发送 POST 请求时,将 JSON 请求体加密为 `{ data: "<密文>" }`,并加 `X-Encrypted: true` 请求头 +2. 后端中间件检测到 `X-Encrypted: true` 时解密请求体,处理完后加密响应体 +3. 支付回调接口(`/payments/alipay/callback`, `/payments/wechat/callback`)白名单跳过加密 + +> **GET 请求注意:** GET 没有请求体,但响应仍会被加密。前端会自动检测并解密。 + +### 文件日志加密(独立机制) + +与通信加密不同,**文件日志存储**使用另一套独立的 AES-CBC-256 加密: + +| 日志类型 | 目录 | 加密方式 | |---------|------|---------| -| 请求/响应日志 | `log/RequestResponse/{日期}.log` | 始终开启 | -| AI 模型日志 | `log/AiModel/{日期}.log` | `AI_LOG_ENABLED` 环境变量 | -| Python 控制台日志 | stderr | `DEBUG=true` 时输出 INFO,否则 WARNING | +| 请求/响应日志 | `log/RequestResponse/{日期}.log` | AES-CBC-256,密钥硬编码 | +| AI 模型日志 | `log/AiModel/{日期}.log` | AES-CBC-256,密钥硬编码 | -文件日志使用 AES-CBC 加密存储,解密工具:`/internal/decrypt-data` 端点。 +日志加密密钥与 `ENCRYPTION_KEY` 无关,是代码中硬编码的值。解密工具:`/internal/decrypt-data` 页面。 + +### 内部管理端点 + +| 端点 | 用途 | +|------|------| +| `/internal/` | 后端入口导航页 | +| `/internal/health` | 健康检查 (`{"status": "ok"}`) | +| `/internal/status` | 服务状态监控页 (Celery/Redis 连接状态) | +| `/internal/decrypt-data` | 日志数据解密工具(AES-CBC) | +| `/internal/api-docs` | Swagger API 文档 | +| `/internal/api-redoc` | ReDoc API 文档 | +| `/uploads/` | 用户上传的静态文件(挂载为静态目录) | +| `/api/decrypt` | 解密接口(POST,供解密工具调用) | + +### 前端 Mock 模式 + +前端支持 Mock 数据模式,无需后端即可开发: + +- 设置 `VITE_USE_MOCK=true` 时,所有 API 调用返回本地假数据 +- 适用场景:纯前端开发、演示、无后端环境 +- 注意:mock 模式下仍会调用部分真实 API(如站点信息、验证码) ### 存储路径 @@ -522,6 +623,15 @@ sudo certbot --nginx -d yourdomain.com -d admin.yourdomain.com -d api.yourdomain │ ├── .env # 环境变量 │ ├── .venv/ # Python 虚拟环境 │ ├── app/ # 应用代码 +│ │ ├── api/ # API 路由 +│ │ │ ├── v1/ # v1 版本接口(前端用户端 + 部分管理接口) +│ │ │ └── admin/ # 管理端接口 +│ │ ├── middleware/ # 中间件 +│ │ ├── models/ # 数据模型 (41 个) +│ │ ├── services/ # 业务服务 +│ │ ├── tasks/ # Celery 任务 +│ │ ├── enums/ # 枚举定义 +│ │ └── utils/ # 工具函数 │ ├── alembic/ # 数据库迁移文件 │ ├── storage/ │ │ ├── generate/ @@ -565,4 +675,69 @@ cd /opt/video-gen-api && python -m alembic upgrade head # 生成迁移文件 cd /opt/video-gen-api && python -m alembic revision --autogenerate -m "描述" + +# 查看 Celery 活动任务 +celery -A app.tasks.celery_app inspect active ``` + +--- + +## 十一、环境变量完整参考 + +### 后端 (video-gen-api/.env) + +| 变量 | 必填 | 默认值 | 说明 | +|------|------|--------|------| +| `SECRET_KEY` | **是** | `change-me` | JWT 签名密钥 | +| `DATABASE_URL` | **是** | sqlite | PostgreSQL 连接串 | +| `REDIS_URL` | 否 | 空 | Redis 连接串,留空禁用限流/验证码 | +| `JWT_EXPIRE_MINUTES` | 否 | 1440 | Token 有效期(分钟) | +| `JWT_EXPIRE_REMEMBER_MINUTES` | 否 | 10080 | 记住登录 Token 有效期 | +| `SEEDANCE_API_KEY` | **是** | 空 | 火山引擎 API Key | +| `SEEDANCE_API_BASE` | 否 | 火山地址 | API 基础 URL | +| `SEEDANCE_CALLBACK_URL` | 否 | 空 | 生成结果回调 URL | +| `LLM_API_BASE` | 否 | OpenAI | LLM API 地址 | +| `LLM_API_KEY` | 否 | 空 | LLM API Key | +| `LLM_MODEL` | 否 | gpt-4o | LLM 模型名称 | +| `LLM_MOCK` | 否 | true | 是否 mock LLM 响应 | +| `ENCRYPTION_KEY` | 否 | 占位符 | 前后端通信加密密钥 | +| `SMS_MOCK` | 否 | true | 是否 mock 短信 | +| `PAYMENT_MOCK` | 否 | false | 是否 mock 支付 | +| `CORS_ORIGINS` | 否 | `["*"]` | 允许的跨域来源 | +| `BASE_URL` | 否 | 测试地址 | 站点基础 URL,用于回调拼接 | +| `STORAGE_TYPE` | 否 | local | 存储类型 | +| `UPLOAD_LOCAL_PATH` | 否 | `./storage/uploads` | 上传文件存储路径 | +| `FFMPEG_BIN` | 否 | 空 | FFmpeg 路径(留空自动查找) | +| `CAPTCHA_ENABLED` | 否 | true | 是否启用验证码 | +| `CELERY_BROKER_URL` | 否 | 空 | Celery Broker(留空禁用 Celery) | +| `CELERY_RESULT_BACKEND` | 否 | 空 | Celery 结果后端 | +| `RATE_LIMIT_ENABLED` | 否 | true | 是否启用限流 | + +### 前台 (video-gen-app/.env.production) + +| 变量 | 必填 | 默认值 | 说明 | +|------|------|--------|------| +| `VITE_API_BASE` | **是** | localhost:8000 | 后端 API 地址 | +| `VITE_USE_MOCK` | 否 | false | 是否使用 mock 数据 | +| `VITE_ENCRYPTION_KEY` | 否 | 空 | 通信加密密钥(与后端一致) | + +### 后台管理 (video-gen-admin/.env.production) + +同前台。 + +--- + +## 十二、故障排查 + +| 现象 | 可能原因 | 解决方案 | +|------|---------|---------| +| 前端登录后 401 | Token 过期或 SECRET_KEY 不一致 | 检查后端 `SECRET_KEY` 是否变更 | +| 上传文件失败 413 | Nginx 上传大小限制 | 增大 `client_max_body_size` | +| 加密请求报错"解密失败" | 前后端密钥不一致 | 确保 `VITE_ENCRYPTION_KEY` = 后端 `ENCRYPTION_KEY` | +| 非 HTTPS 环境加密无效 | Web Crypto API 需要安全上下文 | 本地开发用 localhost,生产用 HTTPS | +| Celery 任务不执行 | Redis 未启动或地址错误 | 检查 `CELERY_BROKER_URL` 和 Redis | +| 限流不生效 | Redis 未配置 | 检查 `REDIS_URL` | +| 短信发送失败 | 火山配置缺失或 `SMS_MOCK=true` | 填入 `VOLC_SMS_*` 变量并设 `SMS_MOCK=false` | +| 视频封面无法生成 | FFmpeg 未安装 | 安装 FFmpeg 或设置 `FFMPEG_BIN` | +| HTTPS 请求支付宝/火山失败 | 缺少 CA 证书 | 安装 `ca-certificates` | +| 数据库迁移失败 | Model 定义与迁移不一致 | 重新生成迁移文件后执行 | \ No newline at end of file From d2a30c58ca828649f21e98fccacdbbd721d51224 Mon Sep 17 00:00:00 2001 From: wwwwwwwww <526125649@qq.com> Date: Sat, 11 Jul 2026 10:58:48 +0800 Subject: [PATCH 04/12] =?UTF-8?q?1=E3=80=81=E5=90=8E=E5=8F=B0=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E8=AE=BE=E7=BD=AEsettings=E9=A1=B5=E9=9D=A2=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E4=B8=80=E4=B8=AA=E5=BC=80=E5=90=AF=E5=85=B3=E9=97=AD?= =?UTF-8?q?=E7=BD=91=E7=AB=99=E7=9A=84=E6=8C=89=E9=92=AE=202=E3=80=81?= =?UTF-8?q?=E5=A6=82=E6=9E=9C=E7=BD=91=E7=AB=99=E5=85=B3=E9=97=AD=EF=BC=8C?= =?UTF-8?q?=E5=89=8D=E5=8F=B0=E9=A1=B5=E9=9D=A2=E6=89=80=E6=9C=89=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E6=9A=82=E6=97=B6=E8=B7=B3=E8=BD=AC=E5=8D=95=E7=8B=AC?= =?UTF-8?q?=E7=9A=84=E9=A1=B5=E5=A6=82=E6=9E=9C=E7=BD=91=E7=AB=99=E5=85=B3?= =?UTF-8?q?=E9=97=AD=EF=BC=8C=E5=89=8D=E5=8F=B0=E9=A1=B5=E9=9D=A2=E6=89=80?= =?UTF-8?q?=E6=9C=89=E8=AF=B7=E6=B1=82=E6=9A=82=E6=97=B6=E8=B7=B3=E8=BD=AC?= =?UTF-8?q?=E5=8D=95=E7=8B=AC=E7=9A=84=E9=A1=B5=E9=9D=A2=EF=BC=8C=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E5=86=85=E5=AE=B9=20=E7=B3=BB=E7=BB=9F=E6=AD=A3?= =?UTF-8?q?=E5=9C=A8=E5=8D=87=E7=BA=A7=E7=9B=B8=E5=85=B3=E4=BF=A1=E6=81=AF?= =?UTF-8?q?=203=E3=80=81=E8=A6=81=E6=8F=90=E4=BE=9B=E4=B8=80=E4=B8=AA?= =?UTF-8?q?=E5=BC=80=E5=8F=91=E4=BA=BA=E5=91=98=E5=8F=AF=E4=BB=A5=E6=9F=A5?= =?UTF-8?q?=E7=9C=8B=E7=9C=9F=E5=AE=9E=E7=BD=91=E7=AB=99=E5=86=85=E5=AE=B9?= =?UTF-8?q?=E7=9A=84=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CODEBASE.md | 11 ++ video-gen-admin/src/api/index.ts | 4 + video-gen-admin/src/pages/AdminSettings.tsx | 87 +++++++++++++- video-gen-api/app/api/v1/admin.py | 35 ++++++ video-gen-api/app/api/v1/auth.py | 7 ++ video-gen-api/app/main.py | 39 +++++- video-gen-api/app/middleware/site_status.py | 125 ++++++++++++++++++++ video-gen-app/src/App.tsx | 40 ++++++- video-gen-app/src/api/client.ts | 13 ++ video-gen-app/src/api/index.ts | 2 +- video-gen-app/src/pages/MaintenancePage.tsx | 103 ++++++++++++++++ 11 files changed, 461 insertions(+), 5 deletions(-) create mode 100644 video-gen-api/app/middleware/site_status.py create mode 100644 video-gen-app/src/pages/MaintenancePage.tsx diff --git a/CODEBASE.md b/CODEBASE.md index 9cc25c3f..e0320b0b 100644 --- a/CODEBASE.md +++ b/CODEBASE.md @@ -1,7 +1,18 @@ # VideoGen 代码架构指南 > 本文档面向开发者或 AI 模型,帮助快速理解代码风格、架构约定和修改规则。 +# AI 工作规则(最高优先级) +当你执行开发任务时: + +1. 不要扫描整个 Repository。 +2. 不要执行全量目录探索。 +3. 仅阅读本任务涉及的目录。 +4. 优先按照本文档中的"快速定位表"寻找文件。 +5. 如果无法定位文件,再进行有限范围搜索。 +6. 单次最多搜索一级目录。 +7. 不允许重复搜索已经访问过的目录。 +8. 阅读完目标文件后立即开始修改代码。 --- ## 一、整体架构 diff --git a/video-gen-admin/src/api/index.ts b/video-gen-admin/src/api/index.ts index ec4c4191..7576e2e5 100644 --- a/video-gen-admin/src/api/index.ts +++ b/video-gen-admin/src/api/index.ts @@ -224,6 +224,10 @@ export async function updateSystemConfig(id: string, value: string): Promise { + return api.post('/admin/system-configs/regenerate-dev-token'); +} + export async function getGlobalResourceCapacity(): Promise { return api.get('/admin/resource-capacity/global'); } diff --git a/video-gen-admin/src/pages/AdminSettings.tsx b/video-gen-admin/src/pages/AdminSettings.tsx index 3afe6226..cdcb32d9 100644 --- a/video-gen-admin/src/pages/AdminSettings.tsx +++ b/video-gen-admin/src/pages/AdminSettings.tsx @@ -1,9 +1,10 @@ import React, { useEffect, useState } from 'react'; import { - Button, Card, Form, Input, InputNumber, message, Select, Space, Switch, Typography, Upload, + Button, Card, Form, Input, InputNumber, message, Popconfirm, Select, Space, Switch, Typography, Upload, } from 'antd'; import { SettingOutlined, SaveOutlined, UploadOutlined, FilePdfOutlined, EyeOutlined, DatabaseOutlined, + PoweroffOutlined, ReloadOutlined, CopyOutlined, } from '@ant-design/icons'; import { getGlobalResourceCapacity, @@ -12,6 +13,7 @@ import { updateSystemConfig, uploadLogo, uploadPdf, + regenerateDevToken, } from '../api'; import type { ResourceCapacityUnit, SystemConfig } from '../types'; @@ -42,6 +44,8 @@ const AdminSettings: React.FC = () => { setConfigs(data); const formValues: Record = {}; data.forEach(c => { formValues[c.key] = c.value; }); + // 网站开关布尔值转换 + formValues.site_enabled = siteEnabledConfig?.value !== 'false'; formValues.resource_capacity_enabled = capacity.enabled; formValues.resource_capacity_limit_value = capacity.limitValue || '1.000'; formValues.resource_capacity_limit_unit = capacity.limitUnit || 'GB'; @@ -63,6 +67,13 @@ const AdminSettings: React.FC = () => { await updateSystemConfig(config.id, String(newVal ?? '')); } } + // 保存网站开关(布尔值转为字符串) + if (siteEnabledConfig) { + const newEnabled = values.site_enabled ? 'true' : 'false'; + if (newEnabled !== siteEnabledConfig.value) { + await updateSystemConfig(siteEnabledConfig.id, newEnabled); + } + } await saveGlobalResourceCapacity({ enabled: !!values.resource_capacity_enabled, limitValue: String(values.resource_capacity_limit_value ?? '1.000'), @@ -77,6 +88,20 @@ const AdminSettings: React.FC = () => { } }; + const handleRegenerateDevToken = async () => { + try { + const result = await regenerateDevToken(); + message.success('开发者令牌已重新生成'); + await load(); + // 更新显示的令牌 + if (result?.token) { + form.setFieldsValue({ site_dev_access_token: result.token }); + } + } catch (e: any) { + message.error(e?.message || '重新生成失败'); + } + }; + const handleUpload = async (file: File, configKey: string) => { setUploading(configKey); try { @@ -118,8 +143,11 @@ const AdminSettings: React.FC = () => { return false; }; + const siteEnabledConfig = configs.find(c => c.key === 'site_enabled'); + const devTokenConfig = configs.find(c => c.key === 'site_dev_access_token'); + const groupedConfigs: Record = { - '站点信息': configs.filter(c => c.key.startsWith('site_')), + '站点信息': configs.filter(c => c.key.startsWith('site_') && c.key !== 'site_enabled' && c.key !== 'site_dev_access_token'), '协议配置': configs.filter(c => c.key === 'user_agreement_privacy_url'), 'SEO 设置': configs.filter(c => c.key.startsWith('seo_')), '用户积分配置': configs.filter(c => c.key.startsWith('user_') && c.key.includes('credits')), @@ -297,6 +325,61 @@ const AdminSettings: React.FC = () => {
+ {/* 网站访问控制 */} +
+ + 访问控制 + + {siteEnabledConfig ? ( + + + + ) : ( + + 系统配置 site_enabled 未初始化,请通过数据库手动添加。 + + )} + + {devTokenConfig && ( + + + + + + + + + + )} +
+ {Object.entries(groupedConfigs).map(([group, items]) => (
diff --git a/video-gen-api/app/api/v1/admin.py b/video-gen-api/app/api/v1/admin.py index f37562c2..861a0c5d 100644 --- a/video-gen-api/app/api/v1/admin.py +++ b/video-gen-api/app/api/v1/admin.py @@ -1622,6 +1622,41 @@ async def update_system_config( return config +@router.post("/system-configs/regenerate-dev-token") +async def regenerate_dev_token( + admin: User = Depends(get_admin_user), + db: AsyncSession = Depends(get_db), +): + """重新生成开发者访问令牌。""" + import secrets + result = await db.execute( + select(SystemConfig).where(SystemConfig.key == "site_dev_access_token").limit(1) + ) + config = result.scalar_one_or_none() + new_token = secrets.token_urlsafe(32) + if config: + config.value = new_token + else: + config = SystemConfig( + id=generate_id(), + key="site_dev_access_token", + value=new_token, + description="开发者访问令牌(网站关闭时用于绕过限制)", + ) + db.add(config) + await db.flush() + await log_operation( + db, + admin.id, + admin.username, + "重新生成开发者访问令牌", + "POST", + "/admin/system-configs/regenerate-dev-token", + ) + await db.commit() + return {"token": new_token} + + # ── Operation Logs ────────────────────────────────────── @router.get("/operation-logs") diff --git a/video-gen-api/app/api/v1/auth.py b/video-gen-api/app/api/v1/auth.py index 0a16d8cb..09d8eb97 100644 --- a/video-gen-api/app/api/v1/auth.py +++ b/video-gen-api/app/api/v1/auth.py @@ -345,12 +345,19 @@ async def get_site_info(db: AsyncSession = Depends(get_db)): base_url = settings.BASE_URL.rstrip("/") return f"{base_url}{path}" + # 读取网站开关状态(不存在时默认开启) + site_enabled_result = await db.execute( + select(SystemConfig.value).where(SystemConfig.key == "site_enabled").limit(1) + ) + site_enabled_value = site_enabled_result.scalar_one_or_none() + return { "site_name": info.get("site_name", "VideoGen.AI"), "site_logo": to_full_url(info.get("site_logo")), "user_agreement_privacy_url": to_full_url(info.get("user_agreement_privacy_url")), "site_copyright": info.get("site_copyright", "© 2024 民众智创 版权所有"), "operation_manual": info.get("operation_manual", ""), + "site_enabled": site_enabled_value != "false", } diff --git a/video-gen-api/app/main.py b/video-gen-api/app/main.py index 6fe1afae..7782bed9 100644 --- a/video-gen-api/app/main.py +++ b/video-gen-api/app/main.py @@ -16,6 +16,7 @@ from app.middleware.logging import RequestLoggingMiddleware from app.middleware.anti_crawler import AntiCrawlerMiddleware from app.middleware.rate_limit import RateLimitMiddleware from app.middleware.request_encrypt import RequestEncryptMiddleware +from app.middleware.site_status import SiteStatusMiddleware from app.services.log_config import decrypt_data logging.basicConfig(level=logging.INFO if settings.DEBUG else logging.WARNING) @@ -29,6 +30,7 @@ async def lifespan(app: FastAPI): os.makedirs(settings.UPLOAD_LOCAL_PATH, exist_ok=True) await init_database() await init_redis() + await _ensure_site_configs() # await _seed_data() # Start task queue (handles both video and image generation) @@ -513,6 +515,40 @@ async def _seed_data(): await db.commit() +async def _ensure_site_configs() -> None: + """确保网站开关相关配置项存在(不存在则自动创建)。""" + import secrets + from app.models.base import async_session + from app.models.system_config import SystemConfig + from app.utils.id_gen import generate_id + from sqlalchemy import select + + async with async_session() as db: + # site_enabled + existing = await db.execute( + select(SystemConfig).where(SystemConfig.key == "site_enabled").limit(1) + ) + if not existing.scalar_one_or_none(): + db.add(SystemConfig( + id=generate_id(), + key="site_enabled", + value="true", + description="网站访问开关(true=开启,false=关闭)", + )) + # site_dev_access_token + existing_token = await db.execute( + select(SystemConfig).where(SystemConfig.key == "site_dev_access_token").limit(1) + ) + if not existing_token.scalar_one_or_none(): + db.add(SystemConfig( + id=generate_id(), + key="site_dev_access_token", + value=secrets.token_urlsafe(32), + description="开发者访问令牌(网站关闭时用于绕过限制)", + )) + await db.commit() + + def create_app() -> FastAPI: application = FastAPI( title=settings.APP_NAME, @@ -522,7 +558,8 @@ def create_app() -> FastAPI: redoc_url="/internal/api-redoc", ) - # Middleware (outermost first) + # Middleware (outermost first) — SiteStatus 放最外层,早于其他中间件拦截 + application.add_middleware(SiteStatusMiddleware) application.add_middleware(RequestLoggingMiddleware) application.add_middleware(AntiCrawlerMiddleware) application.add_middleware(RateLimitMiddleware) diff --git a/video-gen-api/app/middleware/site_status.py b/video-gen-api/app/middleware/site_status.py new file mode 100644 index 00000000..98800baa --- /dev/null +++ b/video-gen-api/app/middleware/site_status.py @@ -0,0 +1,125 @@ +import logging +import time + +from fastapi import Request, Response +from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint +from starlette.responses import JSONResponse + +logger = logging.getLogger("videogen") + +# 网站关闭时仍然放行的路径前缀 +_WHITELIST_PREFIXES = ( + "/api/auth/", # 登录/注册/短信/验证码 + "/api/admin/", # 后台管理(确保管理员能登录并重新开启网站) + "/api/captcha/", # 图形验证码 + "/internal/", # 健康检查/文档 + "/uploads/", # 静态文件 + "/api/generation-records/callback", # 火山引擎回调 + "/api/payments/wechat/callback", # 微信回调 + "/api/payments/alipay/callback", # 支付宝回调 +) + +# 完全匹配的白名单路径 +_WHITELIST_EXACT = ( + "/api/auth/site-info", + "/internal/health", +) + +# site_enabled 缓存有效期(秒) +_CACHE_TTL = 5.0 + + +class SiteStatusMiddleware(BaseHTTPMiddleware): + """网站访问开关中间件。 + + 检查 SystemConfig 中 site_enabled 的值: + - 配置不存在或值不为 "false" → 放行 + - 值为 "false" 且不在白名单 → 检查开发者令牌,不匹配则返回 503 { code: "SITE_CLOSED" } + """ + + def __init__(self, app): + super().__init__(app) + self._cache_enabled = True # 缓存的站点状态(True=开启) + self._cache_at: float = 0.0 # 缓存时间戳 + + async def _is_site_enabled(self) -> bool: + """查询 site_enabled 配置,带短缓存避免每个请求查 DB。""" + now = time.time() + if now - self._cache_at < _CACHE_TTL: + return self._cache_enabled + + try: + from app.models.base import async_session + from app.models.system_config import SystemConfig + from sqlalchemy import select + + async with async_session() as db: + result = await db.execute( + select(SystemConfig.value).where(SystemConfig.key == "site_enabled").limit(1) + ) + value = result.scalar_one_or_none() + self._cache_enabled = value != "false" + self._cache_at = now + except Exception: + # 查询异常时默认放行,避免数据库故障导致全站不可用 + logger.exception("Failed to read site_enabled config, defaulting to enabled") + self._cache_enabled = True + self._cache_at = now + + return self._cache_enabled + + @staticmethod + def _is_whitelisted(path: str) -> bool: + """检查路径是否在白名单中。""" + if path in _WHITELIST_EXACT: + return True + return any(path.startswith(prefix) for prefix in _WHITELIST_PREFIXES) + + @staticmethod + async def _check_dev_token(token: str) -> bool: + """校验开发者访问令牌是否匹配配置。""" + try: + from app.models.base import async_session + from app.models.system_config import SystemConfig + from sqlalchemy import select + + async with async_session() as db: + result = await db.execute( + select(SystemConfig.value) + .where(SystemConfig.key == "site_dev_access_token") + .limit(1) + ) + expected = result.scalar_one_or_none() + return expected is not None and token == expected + except Exception: + logger.exception("Failed to verify dev access token") + return False + + async def dispatch( + self, request: Request, call_next: RequestResponseEndpoint + ) -> Response: + path = request.url.path + + # 白名单直接放行 + if self._is_whitelisted(path): + return await call_next(request) + + # 网站开启时放行 + if await self._is_site_enabled(): + return await call_next(request) + + # 网站已关闭 — 校验开发者令牌 + token = request.query_params.get("dev_access") or request.headers.get("x-dev-access") + if token and await self._check_dev_token(token): + return await call_next(request) + + # 拦截请求,返回维护状态 + return JSONResponse( + status_code=503, + content={ + "detail": { + "code": "SITE_CLOSED", + "message": "系统正在升级维护,请稍后再试", + } + }, + ) diff --git a/video-gen-app/src/App.tsx b/video-gen-app/src/App.tsx index d166aefc..f1abaee7 100644 --- a/video-gen-app/src/App.tsx +++ b/video-gen-app/src/App.tsx @@ -1,10 +1,11 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import { ConfigProvider, App as AntApp, Spin } from 'antd'; import zhCN from 'antd/locale/zh_CN'; import dayjs from 'dayjs'; import 'dayjs/locale/zh-cn'; dayjs.locale('zh-cn'); +import MaintenancePage from './pages/MaintenancePage'; import AppLayout from './components/Layout/AppLayout'; import LoginPage from './pages/LoginPage'; import ProjectsPage from './pages/ProjectsPage'; @@ -61,11 +62,47 @@ const ProtectedRoute = ({ children }: { children: React.ReactNode }) => { const App = () => { const { checkAuth } = useAuthStore(); + const [siteReady, setSiteReady] = useState(false); useEffect(() => { + // 处理 URL 中的开发者访问令牌 + const params = new URLSearchParams(window.location.search); + const devToken = params.get('dev_access'); + if (devToken) { + localStorage.setItem('dev_access_token', devToken); + // 清除 URL 中的令牌参数 + params.delete('dev_access'); + const newSearch = params.toString(); + window.history.replaceState({}, '', window.location.pathname + (newSearch ? `?${newSearch}` : '')); + } + + // 检查网站状态(仅当用户没有有效 dev token 时) checkAuth(); + if (!localStorage.getItem('dev_access_token')) { + import('./api').then(({ getSiteInfo }) => { + getSiteInfo() + .then((info) => { + if (!info.siteEnabled && !window.location.pathname.startsWith('/maintenance')) { + window.location.href = '/maintenance'; + } + }) + .catch(() => { /* 请求失败时默认放行 */ }) + .finally(() => setSiteReady(true)); + }); + } else { + setSiteReady(true); + } }, []); + // 网站状态检查中显示 loading + if (!siteReady && !localStorage.getItem('dev_access_token')) { + return ( +
+ +
+ ); + } + return ( { } /> + } /> } /> } /> (path: string, options: RequestOptions = {}): if (token) headers['Authorization'] = `Bearer ${token}`; } + // 开发者访问令牌(网站关闭时用于绕过限制) + const devAccessToken = localStorage.getItem('dev_access_token'); + if (devAccessToken) { + headers['X-Dev-Access'] = devAccessToken; + } + let bodyStr: string | undefined; if (body !== undefined) { const json = JSON.stringify(body); @@ -110,6 +116,13 @@ export async function apiRequest(path: string, options: RequestOptions = {}): // Handle error responses if (!res.ok) { + // 网站关闭 → 跳转到维护页面 + if (res.status === 503 && parsed?.detail?.code === 'SITE_CLOSED') { + if (!window.location.pathname.startsWith('/maintenance')) { + window.location.href = '/maintenance'; + } + throw new Error(parsed?.detail?.message || '系统正在升级维护'); + } let msg = parsed?.detail?.message || parsed?.message || `请求失败 (${res.status})`; if (typeof parsed?.detail === 'string') { msg = parsed.detail; diff --git a/video-gen-app/src/api/index.ts b/video-gen-app/src/api/index.ts index 8e11da08..6c591651 100644 --- a/video-gen-app/src/api/index.ts +++ b/video-gen-app/src/api/index.ts @@ -290,7 +290,7 @@ export async function verifyCaptcha(captchaId: string, x: number): Promise { +export async function getSiteInfo(): Promise<{ siteName: string; siteLogo: string; userAgreementPrivacyUrl: string; siteCopyright: string; operationManual: string; siteEnabled: boolean }> { if (USE_MOCK) return { siteName: 'VideoGen.AI', siteLogo: '', userAgreementPrivacyUrl: '', siteCopyright: '© 2024 民众智创 版权所有', operationManual: '' }; return api.get('/auth/site-info', false); } diff --git a/video-gen-app/src/pages/MaintenancePage.tsx b/video-gen-app/src/pages/MaintenancePage.tsx new file mode 100644 index 00000000..c0d8eef1 --- /dev/null +++ b/video-gen-app/src/pages/MaintenancePage.tsx @@ -0,0 +1,103 @@ +import React, { useEffect, useState } from 'react'; +import { Button, Input, Space, Typography, message } from 'antd'; +import { ToolOutlined, SafetyOutlined } from '@ant-design/icons'; +import { getSiteInfo } from '../api'; + +const MaintenancePage: React.FC = () => { + const [devToken, setDevToken] = useState(''); + const [showDevInput, setShowDevInput] = useState(false); + const [siteName, setSiteName] = useState(''); + + useEffect(() => { + // site-info 在网站关闭时仍可访问(白名单) + getSiteInfo().then((info) => { + if (info.siteName) setSiteName(info.siteName); + }).catch(() => {}); + }, []); + + const handleDevAccess = () => { + const token = devToken.trim(); + if (!token) { + message.warning('请输入开发者访问令牌'); + return; + } + localStorage.setItem('dev_access_token', token); + message.success('开发者访问已启用,正在刷新...'); + window.location.href = '/'; + }; + + return ( +
+ {/* 开发者入口 — 右下角 */} +
+ {!showDevInput ? ( + setShowDevInput(true)} + style={{ fontSize: 12, color: '#94a3b8' }} + > + 开发者入口 + + ) : ( + + setDevToken(e.target.value)} + onPressEnter={handleDevAccess} + style={{ width: 200 }} + /> + + + )} +
+ + {/* 主内容 */} +
+ +
+ + {siteName && ( + + {siteName} + + )} + + + 系统正在升级维护 + + + + 为了提供更好的服务,系统正在进行升级维护。 +
+ 请稍后再来,感谢您的理解与支持! +
+
+ ); +}; + +export default MaintenancePage; From f5f4be2986a470185829a05d980e0b79828eafd1 Mon Sep 17 00:00:00 2001 From: wwwwwwwww <526125649@qq.com> Date: Sat, 11 Jul 2026 11:05:19 +0800 Subject: [PATCH 05/12] =?UTF-8?q?=E5=8F=96=E6=B6=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- video-gen-admin/src/api/index.ts | 4 - video-gen-admin/src/pages/AdminSettings.tsx | 87 +------------- video-gen-api/app/api/v1/admin.py | 35 ------ video-gen-api/app/api/v1/auth.py | 7 -- video-gen-api/app/main.py | 39 +----- video-gen-api/app/middleware/site_status.py | 125 -------------------- video-gen-app/src/App.tsx | 40 +------ video-gen-app/src/api/client.ts | 13 -- video-gen-app/src/api/index.ts | 2 +- video-gen-app/src/pages/MaintenancePage.tsx | 103 ---------------- 10 files changed, 5 insertions(+), 450 deletions(-) delete mode 100644 video-gen-api/app/middleware/site_status.py delete mode 100644 video-gen-app/src/pages/MaintenancePage.tsx diff --git a/video-gen-admin/src/api/index.ts b/video-gen-admin/src/api/index.ts index 7576e2e5..ec4c4191 100644 --- a/video-gen-admin/src/api/index.ts +++ b/video-gen-admin/src/api/index.ts @@ -224,10 +224,6 @@ export async function updateSystemConfig(id: string, value: string): Promise { - return api.post('/admin/system-configs/regenerate-dev-token'); -} - export async function getGlobalResourceCapacity(): Promise { return api.get('/admin/resource-capacity/global'); } diff --git a/video-gen-admin/src/pages/AdminSettings.tsx b/video-gen-admin/src/pages/AdminSettings.tsx index cdcb32d9..3afe6226 100644 --- a/video-gen-admin/src/pages/AdminSettings.tsx +++ b/video-gen-admin/src/pages/AdminSettings.tsx @@ -1,10 +1,9 @@ import React, { useEffect, useState } from 'react'; import { - Button, Card, Form, Input, InputNumber, message, Popconfirm, Select, Space, Switch, Typography, Upload, + Button, Card, Form, Input, InputNumber, message, Select, Space, Switch, Typography, Upload, } from 'antd'; import { SettingOutlined, SaveOutlined, UploadOutlined, FilePdfOutlined, EyeOutlined, DatabaseOutlined, - PoweroffOutlined, ReloadOutlined, CopyOutlined, } from '@ant-design/icons'; import { getGlobalResourceCapacity, @@ -13,7 +12,6 @@ import { updateSystemConfig, uploadLogo, uploadPdf, - regenerateDevToken, } from '../api'; import type { ResourceCapacityUnit, SystemConfig } from '../types'; @@ -44,8 +42,6 @@ const AdminSettings: React.FC = () => { setConfigs(data); const formValues: Record = {}; data.forEach(c => { formValues[c.key] = c.value; }); - // 网站开关布尔值转换 - formValues.site_enabled = siteEnabledConfig?.value !== 'false'; formValues.resource_capacity_enabled = capacity.enabled; formValues.resource_capacity_limit_value = capacity.limitValue || '1.000'; formValues.resource_capacity_limit_unit = capacity.limitUnit || 'GB'; @@ -67,13 +63,6 @@ const AdminSettings: React.FC = () => { await updateSystemConfig(config.id, String(newVal ?? '')); } } - // 保存网站开关(布尔值转为字符串) - if (siteEnabledConfig) { - const newEnabled = values.site_enabled ? 'true' : 'false'; - if (newEnabled !== siteEnabledConfig.value) { - await updateSystemConfig(siteEnabledConfig.id, newEnabled); - } - } await saveGlobalResourceCapacity({ enabled: !!values.resource_capacity_enabled, limitValue: String(values.resource_capacity_limit_value ?? '1.000'), @@ -88,20 +77,6 @@ const AdminSettings: React.FC = () => { } }; - const handleRegenerateDevToken = async () => { - try { - const result = await regenerateDevToken(); - message.success('开发者令牌已重新生成'); - await load(); - // 更新显示的令牌 - if (result?.token) { - form.setFieldsValue({ site_dev_access_token: result.token }); - } - } catch (e: any) { - message.error(e?.message || '重新生成失败'); - } - }; - const handleUpload = async (file: File, configKey: string) => { setUploading(configKey); try { @@ -143,11 +118,8 @@ const AdminSettings: React.FC = () => { return false; }; - const siteEnabledConfig = configs.find(c => c.key === 'site_enabled'); - const devTokenConfig = configs.find(c => c.key === 'site_dev_access_token'); - const groupedConfigs: Record = { - '站点信息': configs.filter(c => c.key.startsWith('site_') && c.key !== 'site_enabled' && c.key !== 'site_dev_access_token'), + '站点信息': configs.filter(c => c.key.startsWith('site_')), '协议配置': configs.filter(c => c.key === 'user_agreement_privacy_url'), 'SEO 设置': configs.filter(c => c.key.startsWith('seo_')), '用户积分配置': configs.filter(c => c.key.startsWith('user_') && c.key.includes('credits')), @@ -325,61 +297,6 @@ const AdminSettings: React.FC = () => {
- {/* 网站访问控制 */} -
- - 访问控制 - - {siteEnabledConfig ? ( - - - - ) : ( - - 系统配置 site_enabled 未初始化,请通过数据库手动添加。 - - )} - - {devTokenConfig && ( - - - - - - - - - - )} -
- {Object.entries(groupedConfigs).map(([group, items]) => (
diff --git a/video-gen-api/app/api/v1/admin.py b/video-gen-api/app/api/v1/admin.py index 861a0c5d..f37562c2 100644 --- a/video-gen-api/app/api/v1/admin.py +++ b/video-gen-api/app/api/v1/admin.py @@ -1622,41 +1622,6 @@ async def update_system_config( return config -@router.post("/system-configs/regenerate-dev-token") -async def regenerate_dev_token( - admin: User = Depends(get_admin_user), - db: AsyncSession = Depends(get_db), -): - """重新生成开发者访问令牌。""" - import secrets - result = await db.execute( - select(SystemConfig).where(SystemConfig.key == "site_dev_access_token").limit(1) - ) - config = result.scalar_one_or_none() - new_token = secrets.token_urlsafe(32) - if config: - config.value = new_token - else: - config = SystemConfig( - id=generate_id(), - key="site_dev_access_token", - value=new_token, - description="开发者访问令牌(网站关闭时用于绕过限制)", - ) - db.add(config) - await db.flush() - await log_operation( - db, - admin.id, - admin.username, - "重新生成开发者访问令牌", - "POST", - "/admin/system-configs/regenerate-dev-token", - ) - await db.commit() - return {"token": new_token} - - # ── Operation Logs ────────────────────────────────────── @router.get("/operation-logs") diff --git a/video-gen-api/app/api/v1/auth.py b/video-gen-api/app/api/v1/auth.py index 09d8eb97..0a16d8cb 100644 --- a/video-gen-api/app/api/v1/auth.py +++ b/video-gen-api/app/api/v1/auth.py @@ -345,19 +345,12 @@ async def get_site_info(db: AsyncSession = Depends(get_db)): base_url = settings.BASE_URL.rstrip("/") return f"{base_url}{path}" - # 读取网站开关状态(不存在时默认开启) - site_enabled_result = await db.execute( - select(SystemConfig.value).where(SystemConfig.key == "site_enabled").limit(1) - ) - site_enabled_value = site_enabled_result.scalar_one_or_none() - return { "site_name": info.get("site_name", "VideoGen.AI"), "site_logo": to_full_url(info.get("site_logo")), "user_agreement_privacy_url": to_full_url(info.get("user_agreement_privacy_url")), "site_copyright": info.get("site_copyright", "© 2024 民众智创 版权所有"), "operation_manual": info.get("operation_manual", ""), - "site_enabled": site_enabled_value != "false", } diff --git a/video-gen-api/app/main.py b/video-gen-api/app/main.py index 7782bed9..6fe1afae 100644 --- a/video-gen-api/app/main.py +++ b/video-gen-api/app/main.py @@ -16,7 +16,6 @@ from app.middleware.logging import RequestLoggingMiddleware from app.middleware.anti_crawler import AntiCrawlerMiddleware from app.middleware.rate_limit import RateLimitMiddleware from app.middleware.request_encrypt import RequestEncryptMiddleware -from app.middleware.site_status import SiteStatusMiddleware from app.services.log_config import decrypt_data logging.basicConfig(level=logging.INFO if settings.DEBUG else logging.WARNING) @@ -30,7 +29,6 @@ async def lifespan(app: FastAPI): os.makedirs(settings.UPLOAD_LOCAL_PATH, exist_ok=True) await init_database() await init_redis() - await _ensure_site_configs() # await _seed_data() # Start task queue (handles both video and image generation) @@ -515,40 +513,6 @@ async def _seed_data(): await db.commit() -async def _ensure_site_configs() -> None: - """确保网站开关相关配置项存在(不存在则自动创建)。""" - import secrets - from app.models.base import async_session - from app.models.system_config import SystemConfig - from app.utils.id_gen import generate_id - from sqlalchemy import select - - async with async_session() as db: - # site_enabled - existing = await db.execute( - select(SystemConfig).where(SystemConfig.key == "site_enabled").limit(1) - ) - if not existing.scalar_one_or_none(): - db.add(SystemConfig( - id=generate_id(), - key="site_enabled", - value="true", - description="网站访问开关(true=开启,false=关闭)", - )) - # site_dev_access_token - existing_token = await db.execute( - select(SystemConfig).where(SystemConfig.key == "site_dev_access_token").limit(1) - ) - if not existing_token.scalar_one_or_none(): - db.add(SystemConfig( - id=generate_id(), - key="site_dev_access_token", - value=secrets.token_urlsafe(32), - description="开发者访问令牌(网站关闭时用于绕过限制)", - )) - await db.commit() - - def create_app() -> FastAPI: application = FastAPI( title=settings.APP_NAME, @@ -558,8 +522,7 @@ def create_app() -> FastAPI: redoc_url="/internal/api-redoc", ) - # Middleware (outermost first) — SiteStatus 放最外层,早于其他中间件拦截 - application.add_middleware(SiteStatusMiddleware) + # Middleware (outermost first) application.add_middleware(RequestLoggingMiddleware) application.add_middleware(AntiCrawlerMiddleware) application.add_middleware(RateLimitMiddleware) diff --git a/video-gen-api/app/middleware/site_status.py b/video-gen-api/app/middleware/site_status.py deleted file mode 100644 index 98800baa..00000000 --- a/video-gen-api/app/middleware/site_status.py +++ /dev/null @@ -1,125 +0,0 @@ -import logging -import time - -from fastapi import Request, Response -from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint -from starlette.responses import JSONResponse - -logger = logging.getLogger("videogen") - -# 网站关闭时仍然放行的路径前缀 -_WHITELIST_PREFIXES = ( - "/api/auth/", # 登录/注册/短信/验证码 - "/api/admin/", # 后台管理(确保管理员能登录并重新开启网站) - "/api/captcha/", # 图形验证码 - "/internal/", # 健康检查/文档 - "/uploads/", # 静态文件 - "/api/generation-records/callback", # 火山引擎回调 - "/api/payments/wechat/callback", # 微信回调 - "/api/payments/alipay/callback", # 支付宝回调 -) - -# 完全匹配的白名单路径 -_WHITELIST_EXACT = ( - "/api/auth/site-info", - "/internal/health", -) - -# site_enabled 缓存有效期(秒) -_CACHE_TTL = 5.0 - - -class SiteStatusMiddleware(BaseHTTPMiddleware): - """网站访问开关中间件。 - - 检查 SystemConfig 中 site_enabled 的值: - - 配置不存在或值不为 "false" → 放行 - - 值为 "false" 且不在白名单 → 检查开发者令牌,不匹配则返回 503 { code: "SITE_CLOSED" } - """ - - def __init__(self, app): - super().__init__(app) - self._cache_enabled = True # 缓存的站点状态(True=开启) - self._cache_at: float = 0.0 # 缓存时间戳 - - async def _is_site_enabled(self) -> bool: - """查询 site_enabled 配置,带短缓存避免每个请求查 DB。""" - now = time.time() - if now - self._cache_at < _CACHE_TTL: - return self._cache_enabled - - try: - from app.models.base import async_session - from app.models.system_config import SystemConfig - from sqlalchemy import select - - async with async_session() as db: - result = await db.execute( - select(SystemConfig.value).where(SystemConfig.key == "site_enabled").limit(1) - ) - value = result.scalar_one_or_none() - self._cache_enabled = value != "false" - self._cache_at = now - except Exception: - # 查询异常时默认放行,避免数据库故障导致全站不可用 - logger.exception("Failed to read site_enabled config, defaulting to enabled") - self._cache_enabled = True - self._cache_at = now - - return self._cache_enabled - - @staticmethod - def _is_whitelisted(path: str) -> bool: - """检查路径是否在白名单中。""" - if path in _WHITELIST_EXACT: - return True - return any(path.startswith(prefix) for prefix in _WHITELIST_PREFIXES) - - @staticmethod - async def _check_dev_token(token: str) -> bool: - """校验开发者访问令牌是否匹配配置。""" - try: - from app.models.base import async_session - from app.models.system_config import SystemConfig - from sqlalchemy import select - - async with async_session() as db: - result = await db.execute( - select(SystemConfig.value) - .where(SystemConfig.key == "site_dev_access_token") - .limit(1) - ) - expected = result.scalar_one_or_none() - return expected is not None and token == expected - except Exception: - logger.exception("Failed to verify dev access token") - return False - - async def dispatch( - self, request: Request, call_next: RequestResponseEndpoint - ) -> Response: - path = request.url.path - - # 白名单直接放行 - if self._is_whitelisted(path): - return await call_next(request) - - # 网站开启时放行 - if await self._is_site_enabled(): - return await call_next(request) - - # 网站已关闭 — 校验开发者令牌 - token = request.query_params.get("dev_access") or request.headers.get("x-dev-access") - if token and await self._check_dev_token(token): - return await call_next(request) - - # 拦截请求,返回维护状态 - return JSONResponse( - status_code=503, - content={ - "detail": { - "code": "SITE_CLOSED", - "message": "系统正在升级维护,请稍后再试", - } - }, - ) diff --git a/video-gen-app/src/App.tsx b/video-gen-app/src/App.tsx index f1abaee7..d166aefc 100644 --- a/video-gen-app/src/App.tsx +++ b/video-gen-app/src/App.tsx @@ -1,11 +1,10 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect } from 'react'; import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import { ConfigProvider, App as AntApp, Spin } from 'antd'; import zhCN from 'antd/locale/zh_CN'; import dayjs from 'dayjs'; import 'dayjs/locale/zh-cn'; dayjs.locale('zh-cn'); -import MaintenancePage from './pages/MaintenancePage'; import AppLayout from './components/Layout/AppLayout'; import LoginPage from './pages/LoginPage'; import ProjectsPage from './pages/ProjectsPage'; @@ -62,47 +61,11 @@ const ProtectedRoute = ({ children }: { children: React.ReactNode }) => { const App = () => { const { checkAuth } = useAuthStore(); - const [siteReady, setSiteReady] = useState(false); useEffect(() => { - // 处理 URL 中的开发者访问令牌 - const params = new URLSearchParams(window.location.search); - const devToken = params.get('dev_access'); - if (devToken) { - localStorage.setItem('dev_access_token', devToken); - // 清除 URL 中的令牌参数 - params.delete('dev_access'); - const newSearch = params.toString(); - window.history.replaceState({}, '', window.location.pathname + (newSearch ? `?${newSearch}` : '')); - } - - // 检查网站状态(仅当用户没有有效 dev token 时) checkAuth(); - if (!localStorage.getItem('dev_access_token')) { - import('./api').then(({ getSiteInfo }) => { - getSiteInfo() - .then((info) => { - if (!info.siteEnabled && !window.location.pathname.startsWith('/maintenance')) { - window.location.href = '/maintenance'; - } - }) - .catch(() => { /* 请求失败时默认放行 */ }) - .finally(() => setSiteReady(true)); - }); - } else { - setSiteReady(true); - } }, []); - // 网站状态检查中显示 loading - if (!siteReady && !localStorage.getItem('dev_access_token')) { - return ( -
- -
- ); - } - return ( { } /> - } /> } /> } /> (path: string, options: RequestOptions = {}): if (token) headers['Authorization'] = `Bearer ${token}`; } - // 开发者访问令牌(网站关闭时用于绕过限制) - const devAccessToken = localStorage.getItem('dev_access_token'); - if (devAccessToken) { - headers['X-Dev-Access'] = devAccessToken; - } - let bodyStr: string | undefined; if (body !== undefined) { const json = JSON.stringify(body); @@ -116,13 +110,6 @@ export async function apiRequest(path: string, options: RequestOptions = {}): // Handle error responses if (!res.ok) { - // 网站关闭 → 跳转到维护页面 - if (res.status === 503 && parsed?.detail?.code === 'SITE_CLOSED') { - if (!window.location.pathname.startsWith('/maintenance')) { - window.location.href = '/maintenance'; - } - throw new Error(parsed?.detail?.message || '系统正在升级维护'); - } let msg = parsed?.detail?.message || parsed?.message || `请求失败 (${res.status})`; if (typeof parsed?.detail === 'string') { msg = parsed.detail; diff --git a/video-gen-app/src/api/index.ts b/video-gen-app/src/api/index.ts index 6c591651..8e11da08 100644 --- a/video-gen-app/src/api/index.ts +++ b/video-gen-app/src/api/index.ts @@ -290,7 +290,7 @@ export async function verifyCaptcha(captchaId: string, x: number): Promise { +export async function getSiteInfo(): Promise<{ siteName: string; siteLogo: string; userAgreementPrivacyUrl: string; siteCopyright: string; operationManual: string }> { if (USE_MOCK) return { siteName: 'VideoGen.AI', siteLogo: '', userAgreementPrivacyUrl: '', siteCopyright: '© 2024 民众智创 版权所有', operationManual: '' }; return api.get('/auth/site-info', false); } diff --git a/video-gen-app/src/pages/MaintenancePage.tsx b/video-gen-app/src/pages/MaintenancePage.tsx deleted file mode 100644 index c0d8eef1..00000000 --- a/video-gen-app/src/pages/MaintenancePage.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { Button, Input, Space, Typography, message } from 'antd'; -import { ToolOutlined, SafetyOutlined } from '@ant-design/icons'; -import { getSiteInfo } from '../api'; - -const MaintenancePage: React.FC = () => { - const [devToken, setDevToken] = useState(''); - const [showDevInput, setShowDevInput] = useState(false); - const [siteName, setSiteName] = useState(''); - - useEffect(() => { - // site-info 在网站关闭时仍可访问(白名单) - getSiteInfo().then((info) => { - if (info.siteName) setSiteName(info.siteName); - }).catch(() => {}); - }, []); - - const handleDevAccess = () => { - const token = devToken.trim(); - if (!token) { - message.warning('请输入开发者访问令牌'); - return; - } - localStorage.setItem('dev_access_token', token); - message.success('开发者访问已启用,正在刷新...'); - window.location.href = '/'; - }; - - return ( -
- {/* 开发者入口 — 右下角 */} -
- {!showDevInput ? ( - setShowDevInput(true)} - style={{ fontSize: 12, color: '#94a3b8' }} - > - 开发者入口 - - ) : ( - - setDevToken(e.target.value)} - onPressEnter={handleDevAccess} - style={{ width: 200 }} - /> - - - )} -
- - {/* 主内容 */} -
- -
- - {siteName && ( - - {siteName} - - )} - - - 系统正在升级维护 - - - - 为了提供更好的服务,系统正在进行升级维护。 -
- 请稍后再来,感谢您的理解与支持! -
-
- ); -}; - -export default MaintenancePage; From 37588d0f0502c49c70487e0fb5286aa5b869c7c9 Mon Sep 17 00:00:00 2001 From: wwwwwwwww <526125649@qq.com> Date: Sat, 11 Jul 2026 11:31:30 +0800 Subject: [PATCH 06/12] =?UTF-8?q?=E5=89=8D=E5=8F=B0=E7=B4=A0=E6=9D=90?= =?UTF-8?q?=E4=BA=91=E9=A1=B5=E9=9D=A2=E6=A0=B7=E5=BC=8F=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/UnifiedFilterBar.tsx | 186 +++++++ video-gen-app/src/pages/ConsumePage.tsx | 35 +- video-gen-app/src/pages/GeneratedRecord.tsx | 455 ++++++------------ video-gen-app/src/pages/MaterialListPage.tsx | 47 +- 4 files changed, 374 insertions(+), 349 deletions(-) create mode 100644 video-gen-app/src/components/UnifiedFilterBar.tsx diff --git a/video-gen-app/src/components/UnifiedFilterBar.tsx b/video-gen-app/src/components/UnifiedFilterBar.tsx new file mode 100644 index 00000000..ed84c8cd --- /dev/null +++ b/video-gen-app/src/components/UnifiedFilterBar.tsx @@ -0,0 +1,186 @@ +import React from 'react'; +import { Button, DatePicker, Input, Space } from 'antd'; +import { + CheckOutlined, + ClearOutlined, + DeleteOutlined, + DownloadOutlined, + SearchOutlined, + UploadOutlined, +} from '@ant-design/icons'; +import type { Dayjs } from 'dayjs'; + +interface UnifiedFilterBarProps { + // 搜索 + searchValue: string; + searchPlaceholder?: string; + onSearchChange: (value: string) => void; + onSearch: () => void; + // 日期 + showDate?: boolean; + dateValue?: Dayjs | null; + onDateChange?: (date: Dayjs | null) => void; + datePlaceholder?: string; + // 批量操作 + batchCount: number; + totalCount: number; + onSelectAll: () => void; + onClearSelection: () => void; + onDelete: () => void; + onDownload?: () => void; + onPush?: () => void; + pushLoading?: boolean; + pushText?: string; + // 右侧额外操作 + extraActions?: React.ReactNode; +} + +const btnBase: React.CSSProperties = { + borderRadius: 8, + fontWeight: 600, + height: 36, +}; + +const btnSecondary: React.CSSProperties = { + ...btnBase, + background: '#f8f9fc', + border: '1px solid #e2e8f0', + color: '#334155', +}; + +const btnDanger: React.CSSProperties = { + ...btnBase, + background: 'linear-gradient(135deg, #ef4444, #dc2626)', + border: 'none', + color: '#fff', +}; + +const btnPrimary: React.CSSProperties = { + ...btnBase, + background: 'linear-gradient(135deg, #6366f1, #8b5cf6)', + border: 'none', + color: '#fff', +}; + +const UnifiedFilterBar: React.FC = ({ + searchValue, + searchPlaceholder = '搜索...', + onSearchChange, + onSearch, + showDate = false, + dateValue, + onDateChange, + datePlaceholder = '选择日期', + batchCount, + totalCount, + onSelectAll, + onClearSelection, + onDelete, + onDownload, + onPush, + pushLoading = false, + pushText = '推送至账户', + extraActions, +}) => { + const allSelected = batchCount === totalCount && totalCount > 0; + + return ( +
+ {/* 左侧:搜索 + 日期 */} + + { + const val = e.target.value; + onSearchChange(val); + if (!val) onSearch(); + }} + onSearch={onSearch} + style={{ width: 240, borderRadius: 8 }} + enterButton + /> + {showDate && ( + <> + onDateChange?.(date)} + format="YYYY-MM-DD" + placeholder={datePlaceholder} + style={{ width: 160, borderRadius: 8 }} + allowClear + /> + + )} + + + {/* 右侧:批量操作 + 额外操作 */} + + {/* 全选 / 取消选择 */} + + + {/* 删除 */} + + + {/* 下载 */} + {onDownload && ( + + )} + + {/* 推送 */} + {onPush && ( + + )} + + {/* 额外操作按钮 */} + {extraActions} + +
+ ); +}; + +export default UnifiedFilterBar; diff --git a/video-gen-app/src/pages/ConsumePage.tsx b/video-gen-app/src/pages/ConsumePage.tsx index c030cfc1..c79a8051 100644 --- a/video-gen-app/src/pages/ConsumePage.tsx +++ b/video-gen-app/src/pages/ConsumePage.tsx @@ -7,6 +7,9 @@ import dayjs from 'dayjs'; const { RangePicker } = DatePicker; +// 统一按钮样式 +const btnBase: React.CSSProperties = { borderRadius: 8, fontWeight: 600 }; + const toCamelCase = (str: string) => str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()); interface ConsumptionRecord { @@ -164,13 +167,24 @@ const ConsumePage: React.FC = () => { 消耗记录
-
-
+
+
setAdvertiserId(e.target.value)} - style={{ width: 180 }} + style={{ width: 180, borderRadius: 8 }} allowClear onPressEnter={() => { setCurrentPage(1); loadData(1, pageSize); }} /> @@ -183,25 +197,32 @@ const ConsumePage: React.FC = () => { setConsumeDateRange(undefined); } }} + style={{ borderRadius: 8 }} />
-
+
- +
diff --git a/video-gen-app/src/pages/GeneratedRecord.tsx b/video-gen-app/src/pages/GeneratedRecord.tsx index e6f663dc..ffa39602 100644 --- a/video-gen-app/src/pages/GeneratedRecord.tsx +++ b/video-gen-app/src/pages/GeneratedRecord.tsx @@ -17,6 +17,8 @@ import { DeleteOutlined, LoadingOutlined, UserOutlined, + CheckOutlined, + ClearOutlined, } from '@ant-design/icons'; import { useSearchParams } from 'react-router-dom'; import { PrivatePortraitLibraryPanel } from '../components/privatePortrait'; @@ -909,225 +911,86 @@ const GeneratedRecord: React.FC = () => { }, [filterType, filterMedia]); return (
- {/* 操作栏:筛选 + 推送按钮 */} + {/* 顶部:标签切换 + 批量操作按钮 */}
-
- - - - - - - - - -
+ { + setFilterType(key as typeof filterType); + setIsSelectionMode(false); + setSelectedItems(new Set()); + }} + items={[ + { key: 'project', label: 项目记录 }, + { key: 'creation', label: 创作记录 }, + { key: 'hot_opening_replicate', label: 爆款复刻 }, + { key: 'shot_replicate', label: 拆镜复刻 }, + { key: 'private_portrait', label: 私域素材库 }, + { key: 'upload_resource', label: 历史素材 }, + ]} + size="middle" + /> {filterType !== 'private_portrait' && filterType !== 'upload_resource' && ( -
- {/* 多选模式按钮 */} - {isSelectionMode ? ( - - - - - +
+ {isSelectionMode ? ( + + + + + + + + ) : ( - - ) : ( - - - )} -
+ )} +
)}
{filterType === 'private_portrait' ? ( @@ -1136,122 +999,75 @@ const GeneratedRecord: React.FC = () => { ) : ( <> - {/* Second row filter: 视频 / 图片 */} + {/* 统一筛选栏:搜索 + 日期 */}
-
- 媒体类型: - - - - handleDateChange(dateString || '')} - format="YYYY-MM-DD" - style={{ width: 160, borderRadius: 8, border: '1px solid #e2e8f0' }} - placeholder="选择日期" - /> - {selectedDate && ( - - )} - { - const val = e.target.value; - setSearchKeyword(val); - if (!val) { - setPagebreak(prev => ({ ...prev, page: 1 })); - loadRecordList(); - } - }} - onSearch={() => { +
+ 媒体类型: + + +
+ + handleDateChange(dateString || '')} + format="YYYY-MM-DD" + style={{ width: 160, borderRadius: 8, border: '1px solid #e2e8f0' }} + placeholder="选择日期" + allowClear + /> + { + const val = e.target.value; + setSearchKeyword(val); + if (!val) { setPagebreak(prev => ({ ...prev, page: 1 })); loadRecordList(); - }} - style={{ width: 240, borderRadius: 8 }} - /> - -
- + } + }} + onSearch={() => { + setPagebreak(prev => ({ ...prev, page: 1 })); + loadRecordList(); + }} + style={{ width: 240, borderRadius: 8 }} + enterButton + /> + +
{/* Content area */} {loading ? ( @@ -1408,18 +1224,20 @@ const GeneratedRecord: React.FC = () => {
{ e.stopPropagation(); @@ -1517,10 +1335,11 @@ const GeneratedRecord: React.FC = () => { left: 0, right: 0, bottom: 0, - border: '3px solid #10b981', + border: '3px solid #6366f1', borderRadius: 4, pointerEvents: 'none', zIndex: 5, + boxShadow: 'inset 0 0 0 1px rgba(99,102,241,0.2)', }} /> )}
diff --git a/video-gen-app/src/pages/MaterialListPage.tsx b/video-gen-app/src/pages/MaterialListPage.tsx index a7098272..8d81a160 100644 --- a/video-gen-app/src/pages/MaterialListPage.tsx +++ b/video-gen-app/src/pages/MaterialListPage.tsx @@ -383,13 +383,24 @@ const MaterialListPage: React.FC = () => { 素材列表
-
-
+
+
setSearchParams(prev => ({ ...prev, advertiser_id: e.target.value }))} - style={{ width: 160 }} + style={{ width: 150, borderRadius: 8 }} allowClear onPressEnter={() => { setCurrentPage(1); loadMaterialList(1, pageSize); }} /> @@ -397,7 +408,7 @@ const MaterialListPage: React.FC = () => { placeholder="素材ID" value={searchParams.material_id} onChange={(e) => setSearchParams(prev => ({ ...prev, material_id: e.target.value }))} - style={{ width: 160 }} + style={{ width: 150, borderRadius: 8 }} allowClear onPressEnter={() => { setCurrentPage(1); loadMaterialList(1, pageSize); }} /> @@ -405,7 +416,7 @@ const MaterialListPage: React.FC = () => { placeholder="上传ID" value={searchParams.upload_id} onChange={(e) => setSearchParams(prev => ({ ...prev, upload_id: e.target.value }))} - style={{ width: 160 }} + style={{ width: 150, borderRadius: 8 }} allowClear onPressEnter={() => { setCurrentPage(1); loadMaterialList(1, pageSize); }} /> @@ -413,7 +424,7 @@ const MaterialListPage: React.FC = () => { placeholder="文件名" value={searchParams.file_name} onChange={(e) => setSearchParams(prev => ({ ...prev, file_name: e.target.value }))} - style={{ width: 140 }} + style={{ width: 140, borderRadius: 8 }} allowClear onPressEnter={() => { setCurrentPage(1); loadMaterialList(1, pageSize); }} /> @@ -421,7 +432,7 @@ const MaterialListPage: React.FC = () => { placeholder="资源类型" value={searchParams.resource_type} onChange={(value) => setSearchParams(prev => ({ ...prev, resource_type: value }))} - style={{ width: 120 }} + style={{ width: 110, borderRadius: 8 }} allowClear options={[ { value: 'image', label: '图片' }, @@ -431,21 +442,16 @@ const MaterialListPage: React.FC = () => {
-
+
@@ -454,16 +460,9 @@ const MaterialListPage: React.FC = () => { loading={pushTemplatesLoading} onClick={handleOpenPushModal} disabled={selectedRows.size === 0} - style={{ - borderRadius: 12, - fontSize: 14, - background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', - border: 'none', - color: '#fff', - boxShadow: '0 4px 14px rgba(99,102,241,0.3)', - }} + style={{ borderRadius: 8, fontWeight: 600, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', border: 'none', color: '#fff' }} > - 推送前测 ({selectedRows.size}) + 推送前测 {selectedRows.size > 0 && `(${selectedRows.size})`}
From 742c8688bc0203069a336800b2b61be73a24ffbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=99=E4=BD=B3=E8=89=BA?= <13935794190@163.com> Date: Sat, 11 Jul 2026 11:32:20 +0800 Subject: [PATCH 07/12] =?UTF-8?q?=E2=80=9C=E5=8E=86=E5=8F=B2=E7=B4=A0?= =?UTF-8?q?=E6=9D=90=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{index-I3eif2op.js => index-B8KgQUTE.js} | 210 +++++++++--------- video-gen-app/dist/index.html | 2 +- .../src/components/UploadSelector.tsx | 3 + .../privatePortrait/library/LibraryPanel.tsx | 2 +- .../privatePortrait/library/ProjectList.tsx | 2 +- .../library/RealPersonLibraryPanel.tsx | 2 +- .../library/VirtualMaterialPanel.tsx | 4 +- .../privatePortrait/picker/AssetPicker.tsx | 57 ++--- .../UploadResourceHistoryPanel.tsx | 2 +- .../UploadResourceHistoryPicker.tsx | 132 +++++++++-- video-gen-app/src/pages/GenerateConver.tsx | 18 +- video-gen-app/src/pages/GeneratePage.tsx | 55 ++++- video-gen-app/src/pages/GeneratedRecord.tsx | 20 +- video-gen-app/src/pages/HomePage.tsx | 4 +- video-gen-app/src/pages/InitialInfo.tsx | 17 +- video-gen-app/src/pages/ProjectsPage.tsx | 2 +- video-gen-app/src/pages/RemoveInfo.tsx | 10 +- video-gen-app/src/pages/RemoveRw.tsx | 15 +- video-gen-app/vite.config.ts | 8 + 19 files changed, 377 insertions(+), 188 deletions(-) rename video-gen-app/dist/assets/{index-I3eif2op.js => index-B8KgQUTE.js} (51%) diff --git a/video-gen-app/dist/assets/index-I3eif2op.js b/video-gen-app/dist/assets/index-B8KgQUTE.js similarity index 51% rename from video-gen-app/dist/assets/index-I3eif2op.js rename to video-gen-app/dist/assets/index-B8KgQUTE.js index d743e86d..c9bf16b2 100644 --- a/video-gen-app/dist/assets/index-I3eif2op.js +++ b/video-gen-app/dist/assets/index-B8KgQUTE.js @@ -1,12 +1,12 @@ -var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports),s=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;li[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},c=(n,r,a)=>(a=n==null?{}:e(i(n)),s(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n)),l=(e=>typeof require<`u`?require:typeof Proxy<`u`?new Proxy(e,{get:(e,t)=>(typeof require<`u`?require:e)[t]}):e)(function(e){if(typeof require<`u`)return require.apply(this,arguments);throw Error('Calling `require` for "'+e+"\" in an environment that doesn't expose the `require` function. See https://rolldown.rs/in-depth/bundling-cjs#require-external-modules for more details.")});(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin===`use-credentials`?t.credentials=`include`:e.crossOrigin===`anonymous`?t.credentials=`omit`:t.credentials=`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var u=o((e=>{function t(e,t){var n=e.length;e.push(t);a:for(;0>>1,a=e[r];if(0>>1;ri(c,n))li(u,c)?(e[r]=u,e[l]=n,r=l):(e[r]=c,e[s]=n,r=s);else if(li(u,n))e[r]=u,e[l]=n,r=l;else break a}}return t}function i(e,t){var n=e.sortIndex-t.sortIndex;return n===0?e.id-t.id:n}if(e.unstable_now=void 0,typeof performance==`object`&&typeof performance.now==`function`){var a=performance;e.unstable_now=function(){return a.now()}}else{var o=Date,s=o.now();e.unstable_now=function(){return o.now()-s}}var c=[],l=[],u=1,d=null,f=3,p=!1,m=!1,h=!1,g=!1,_=typeof setTimeout==`function`?setTimeout:null,v=typeof clearTimeout==`function`?clearTimeout:null,y=typeof setImmediate<`u`?setImmediate:null;function b(e){for(var i=n(l);i!==null;){if(i.callback===null)r(l);else if(i.startTime<=e)r(l),i.sortIndex=i.expirationTime,t(c,i);else break;i=n(l)}}function x(e){if(h=!1,b(e),!m)if(n(c)!==null)m=!0,S||(S=!0,O());else{var t=n(l);t!==null&&j(x,t.startTime-e)}}var S=!1,C=-1,w=5,T=-1;function E(){return g?!0:!(e.unstable_now()-Tt&&E());){var o=d.callback;if(typeof o==`function`){d.callback=null,f=d.priorityLevel;var s=o(d.expirationTime<=t);if(t=e.unstable_now(),typeof s==`function`){d.callback=s,b(t),i=!0;break b}d===n(c)&&r(c),b(t)}else r(c);d=n(c)}if(d!==null)i=!0;else{var u=n(l);u!==null&&j(x,u.startTime-t),i=!1}}break a}finally{d=null,f=a,p=!1}i=void 0}}finally{i?O():S=!1}}}var O;if(typeof y==`function`)O=function(){y(D)};else if(typeof MessageChannel<`u`){var k=new MessageChannel,A=k.port2;k.port1.onmessage=D,O=function(){A.postMessage(null)}}else O=function(){_(D,0)};function j(t,n){C=_(function(){t(e.unstable_now())},n)}e.unstable_IdlePriority=5,e.unstable_ImmediatePriority=1,e.unstable_LowPriority=4,e.unstable_NormalPriority=3,e.unstable_Profiling=null,e.unstable_UserBlockingPriority=2,e.unstable_cancelCallback=function(e){e.callback=null},e.unstable_forceFrameRate=function(e){0>e||125o?(r.sortIndex=a,t(l,r),n(c)===null&&r===n(l)&&(h?(v(C),C=-1):h=!0,j(x,a-o))):(r.sortIndex=s,t(c,r),m||p||(m=!0,S||(S=!0,O()))),r},e.unstable_shouldYield=E,e.unstable_wrapCallback=function(e){var t=f;return function(){var n=f;f=t;try{return e.apply(this,arguments)}finally{f=n}}}})),d=o(((e,t)=>{t.exports=u()})),f=o((e=>{var t=Symbol.for(`react.transitional.element`),n=Symbol.for(`react.portal`),r=Symbol.for(`react.fragment`),i=Symbol.for(`react.strict_mode`),a=Symbol.for(`react.profiler`),o=Symbol.for(`react.consumer`),s=Symbol.for(`react.context`),c=Symbol.for(`react.forward_ref`),l=Symbol.for(`react.suspense`),u=Symbol.for(`react.memo`),d=Symbol.for(`react.lazy`),f=Symbol.for(`react.activity`),p=Symbol.iterator;function m(e){return typeof e!=`object`||!e?null:(e=p&&e[p]||e[`@@iterator`],typeof e==`function`?e:null)}var h={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},g=Object.assign,_={};function v(e,t,n){this.props=e,this.context=t,this.refs=_,this.updater=n||h}v.prototype.isReactComponent={},v.prototype.setState=function(e,t){if(typeof e!=`object`&&typeof e!=`function`&&e!=null)throw Error(`takes an object of state variables to update or a function which returns an object of state variables.`);this.updater.enqueueSetState(this,e,t,`setState`)},v.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,`forceUpdate`)};function y(){}y.prototype=v.prototype;function b(e,t,n){this.props=e,this.context=t,this.refs=_,this.updater=n||h}var x=b.prototype=new y;x.constructor=b,g(x,v.prototype),x.isPureReactComponent=!0;var S=Array.isArray;function C(){}var w={H:null,A:null,T:null,S:null},T=Object.prototype.hasOwnProperty;function E(e,n,r){var i=r.ref;return{$$typeof:t,type:e,key:n,ref:i===void 0?null:i,props:r}}function D(e,t){return E(e.type,t,e.props)}function O(e){return typeof e==`object`&&!!e&&e.$$typeof===t}function k(e){var t={"=":`=0`,":":`=2`};return`$`+e.replace(/[=:]/g,function(e){return t[e]})}var A=/\/+/g;function j(e,t){return typeof e==`object`&&e&&e.key!=null?k(``+e.key):t.toString(36)}function M(e){switch(e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason;default:switch(typeof e.status==`string`?e.then(C,C):(e.status=`pending`,e.then(function(t){e.status===`pending`&&(e.status=`fulfilled`,e.value=t)},function(t){e.status===`pending`&&(e.status=`rejected`,e.reason=t)})),e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason}}throw e}function N(e,r,i,a,o){var s=typeof e;(s===`undefined`||s===`boolean`)&&(e=null);var c=!1;if(e===null)c=!0;else switch(s){case`bigint`:case`string`:case`number`:c=!0;break;case`object`:switch(e.$$typeof){case t:case n:c=!0;break;case d:return c=e._init,N(c(e._payload),r,i,a,o)}}if(c)return o=o(e),c=a===``?`.`+j(e,0):a,S(o)?(i=``,c!=null&&(i=c.replace(A,`$&/`)+`/`),N(o,r,i,``,function(e){return e})):o!=null&&(O(o)&&(o=D(o,i+(o.key==null||e&&e.key===o.key?``:(``+o.key).replace(A,`$&/`)+`/`)+c)),r.push(o)),1;c=0;var l=a===``?`.`:a+`:`;if(S(e))for(var u=0;u{t.exports=f()})),m=o((e=>{var t=p();function n(e){var t=`https://react.dev/errors/`+e;if(1{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=m()})),g=o((e=>{var t=d(),n=p(),r=h();function i(e){var t=`https://react.dev/errors/`+e;if(1B||(e.current=z[B],z[B]=null,B--)}function U(e,t){B++,z[B]=e.current,e.current=t}var W=V(null),ee=V(null),G=V(null),te=V(null);function ne(e,t){switch(U(G,t),U(ee,e),U(W,null),t.nodeType){case 9:case 11:e=(e=t.documentElement)&&(e=e.namespaceURI)?Gd(e):0;break;default:if(e=t.tagName,t=t.namespaceURI)t=Gd(t),e=Kd(t,e);else switch(e){case`svg`:e=1;break;case`math`:e=2;break;default:e=0}}H(W),U(W,e)}function K(){H(W),H(ee),H(G)}function re(e){e.memoizedState!==null&&U(te,e);var t=W.current,n=Kd(t,e.type);t!==n&&(U(ee,e),U(W,n))}function ie(e){ee.current===e&&(H(W),H(ee)),te.current===e&&(H(te),np._currentValue=R)}var ae,q;function oe(e){if(ae===void 0)try{throw Error()}catch(e){var t=e.stack.trim().match(/\n( *(at )?)/);ae=t&&t[1]||``,q=-1()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports),s=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;li[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},c=(n,r,a)=>(a=n==null?{}:e(i(n)),s(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n)),l=(e=>typeof require<`u`?require:typeof Proxy<`u`?new Proxy(e,{get:(e,t)=>(typeof require<`u`?require:e)[t]}):e)(function(e){if(typeof require<`u`)return require.apply(this,arguments);throw Error('Calling `require` for "'+e+"\" in an environment that doesn't expose the `require` function. See https://rolldown.rs/in-depth/bundling-cjs#require-external-modules for more details.")});(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin===`use-credentials`?t.credentials=`include`:e.crossOrigin===`anonymous`?t.credentials=`omit`:t.credentials=`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var u=o((e=>{function t(e,t){var n=e.length;e.push(t);a:for(;0>>1,a=e[r];if(0>>1;ri(c,n))li(u,c)?(e[r]=u,e[l]=n,r=l):(e[r]=c,e[s]=n,r=s);else if(li(u,n))e[r]=u,e[l]=n,r=l;else break a}}return t}function i(e,t){var n=e.sortIndex-t.sortIndex;return n===0?e.id-t.id:n}if(e.unstable_now=void 0,typeof performance==`object`&&typeof performance.now==`function`){var a=performance;e.unstable_now=function(){return a.now()}}else{var o=Date,s=o.now();e.unstable_now=function(){return o.now()-s}}var c=[],l=[],u=1,d=null,f=3,p=!1,m=!1,h=!1,g=!1,_=typeof setTimeout==`function`?setTimeout:null,v=typeof clearTimeout==`function`?clearTimeout:null,y=typeof setImmediate<`u`?setImmediate:null;function b(e){for(var i=n(l);i!==null;){if(i.callback===null)r(l);else if(i.startTime<=e)r(l),i.sortIndex=i.expirationTime,t(c,i);else break;i=n(l)}}function x(e){if(h=!1,b(e),!m)if(n(c)!==null)m=!0,S||(S=!0,O());else{var t=n(l);t!==null&&j(x,t.startTime-e)}}var S=!1,C=-1,w=5,T=-1;function E(){return g?!0:!(e.unstable_now()-Tt&&E());){var o=d.callback;if(typeof o==`function`){d.callback=null,f=d.priorityLevel;var s=o(d.expirationTime<=t);if(t=e.unstable_now(),typeof s==`function`){d.callback=s,b(t),i=!0;break b}d===n(c)&&r(c),b(t)}else r(c);d=n(c)}if(d!==null)i=!0;else{var u=n(l);u!==null&&j(x,u.startTime-t),i=!1}}break a}finally{d=null,f=a,p=!1}i=void 0}}finally{i?O():S=!1}}}var O;if(typeof y==`function`)O=function(){y(D)};else if(typeof MessageChannel<`u`){var k=new MessageChannel,A=k.port2;k.port1.onmessage=D,O=function(){A.postMessage(null)}}else O=function(){_(D,0)};function j(t,n){C=_(function(){t(e.unstable_now())},n)}e.unstable_IdlePriority=5,e.unstable_ImmediatePriority=1,e.unstable_LowPriority=4,e.unstable_NormalPriority=3,e.unstable_Profiling=null,e.unstable_UserBlockingPriority=2,e.unstable_cancelCallback=function(e){e.callback=null},e.unstable_forceFrameRate=function(e){0>e||125o?(r.sortIndex=a,t(l,r),n(c)===null&&r===n(l)&&(h?(v(C),C=-1):h=!0,j(x,a-o))):(r.sortIndex=s,t(c,r),m||p||(m=!0,S||(S=!0,O()))),r},e.unstable_shouldYield=E,e.unstable_wrapCallback=function(e){var t=f;return function(){var n=f;f=t;try{return e.apply(this,arguments)}finally{f=n}}}})),d=o(((e,t)=>{t.exports=u()})),f=o((e=>{var t=Symbol.for(`react.transitional.element`),n=Symbol.for(`react.portal`),r=Symbol.for(`react.fragment`),i=Symbol.for(`react.strict_mode`),a=Symbol.for(`react.profiler`),o=Symbol.for(`react.consumer`),s=Symbol.for(`react.context`),c=Symbol.for(`react.forward_ref`),l=Symbol.for(`react.suspense`),u=Symbol.for(`react.memo`),d=Symbol.for(`react.lazy`),f=Symbol.for(`react.activity`),p=Symbol.iterator;function m(e){return typeof e!=`object`||!e?null:(e=p&&e[p]||e[`@@iterator`],typeof e==`function`?e:null)}var h={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},g=Object.assign,_={};function v(e,t,n){this.props=e,this.context=t,this.refs=_,this.updater=n||h}v.prototype.isReactComponent={},v.prototype.setState=function(e,t){if(typeof e!=`object`&&typeof e!=`function`&&e!=null)throw Error(`takes an object of state variables to update or a function which returns an object of state variables.`);this.updater.enqueueSetState(this,e,t,`setState`)},v.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,`forceUpdate`)};function y(){}y.prototype=v.prototype;function b(e,t,n){this.props=e,this.context=t,this.refs=_,this.updater=n||h}var x=b.prototype=new y;x.constructor=b,g(x,v.prototype),x.isPureReactComponent=!0;var S=Array.isArray;function C(){}var w={H:null,A:null,T:null,S:null},T=Object.prototype.hasOwnProperty;function E(e,n,r){var i=r.ref;return{$$typeof:t,type:e,key:n,ref:i===void 0?null:i,props:r}}function D(e,t){return E(e.type,t,e.props)}function O(e){return typeof e==`object`&&!!e&&e.$$typeof===t}function k(e){var t={"=":`=0`,":":`=2`};return`$`+e.replace(/[=:]/g,function(e){return t[e]})}var A=/\/+/g;function j(e,t){return typeof e==`object`&&e&&e.key!=null?k(``+e.key):t.toString(36)}function M(e){switch(e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason;default:switch(typeof e.status==`string`?e.then(C,C):(e.status=`pending`,e.then(function(t){e.status===`pending`&&(e.status=`fulfilled`,e.value=t)},function(t){e.status===`pending`&&(e.status=`rejected`,e.reason=t)})),e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason}}throw e}function N(e,r,i,a,o){var s=typeof e;(s===`undefined`||s===`boolean`)&&(e=null);var c=!1;if(e===null)c=!0;else switch(s){case`bigint`:case`string`:case`number`:c=!0;break;case`object`:switch(e.$$typeof){case t:case n:c=!0;break;case d:return c=e._init,N(c(e._payload),r,i,a,o)}}if(c)return o=o(e),c=a===``?`.`+j(e,0):a,S(o)?(i=``,c!=null&&(i=c.replace(A,`$&/`)+`/`),N(o,r,i,``,function(e){return e})):o!=null&&(O(o)&&(o=D(o,i+(o.key==null||e&&e.key===o.key?``:(``+o.key).replace(A,`$&/`)+`/`)+c)),r.push(o)),1;c=0;var l=a===``?`.`:a+`:`;if(S(e))for(var u=0;u{t.exports=f()})),m=o((e=>{var t=p();function n(e){var t=`https://react.dev/errors/`+e;if(1{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=m()})),g=o((e=>{var t=d(),n=p(),r=h();function i(e){var t=`https://react.dev/errors/`+e;if(1B||(e.current=z[B],z[B]=null,B--)}function U(e,t){B++,z[B]=e.current,e.current=t}var W=V(null),ee=V(null),te=V(null),G=V(null);function ne(e,t){switch(U(te,t),U(ee,e),U(W,null),t.nodeType){case 9:case 11:e=(e=t.documentElement)&&(e=e.namespaceURI)?Gd(e):0;break;default:if(e=t.tagName,t=t.namespaceURI)t=Gd(t),e=Kd(t,e);else switch(e){case`svg`:e=1;break;case`math`:e=2;break;default:e=0}}H(W),U(W,e)}function K(){H(W),H(ee),H(te)}function re(e){e.memoizedState!==null&&U(G,e);var t=W.current,n=Kd(t,e.type);t!==n&&(U(ee,e),U(W,n))}function ie(e){ee.current===e&&(H(W),H(ee)),G.current===e&&(H(G),np._currentValue=R)}var ae,q;function oe(e){if(ae===void 0)try{throw Error()}catch(e){var t=e.stack.trim().match(/\n( *(at )?)/);ae=t&&t[1]||``,q=-1)`:-1i||c[r]!==l[i]){var u=` `+c[r].replace(` at new `,` at `);return e.displayName&&u.includes(``)&&(u=u.replace(``,e.displayName)),u}while(1<=r&&0<=i);break}}}finally{se=!1,Error.prepareStackTrace=n}return(n=e?e.displayName||e.name:``)?oe(n):``}function le(e,t){switch(e.tag){case 26:case 27:case 5:return oe(e.type);case 16:return oe(`Lazy`);case 13:return e.child!==t&&t!==null?oe(`Suspense Fallback`):oe(`Suspense`);case 19:return oe(`SuspenseList`);case 0:case 15:return ce(e.type,!1);case 11:return ce(e.type.render,!1);case 1:return ce(e.type,!0);case 31:return oe(`Activity`);default:return``}}function ue(e){try{var t=``,n=null;do t+=le(e,n),n=e,e=e.return;while(e);return t}catch(e){return` Error generating stack: `+e.message+` -`+e.stack}}var de=Object.prototype.hasOwnProperty,fe=t.unstable_scheduleCallback,pe=t.unstable_cancelCallback,me=t.unstable_shouldYield,he=t.unstable_requestPaint,ge=t.unstable_now,_e=t.unstable_getCurrentPriorityLevel,ve=t.unstable_ImmediatePriority,ye=t.unstable_UserBlockingPriority,be=t.unstable_NormalPriority,xe=t.unstable_LowPriority,Se=t.unstable_IdlePriority,Ce=t.log,we=t.unstable_setDisableYieldValue,Te=null,Ee=null;function De(e){if(typeof Ce==`function`&&we(e),Ee&&typeof Ee.setStrictMode==`function`)try{Ee.setStrictMode(Te,e)}catch{}}var Oe=Math.clz32?Math.clz32:je,ke=Math.log,Ae=Math.LN2;function je(e){return e>>>=0,e===0?32:31-(ke(e)/Ae|0)|0}var Me=256,Ne=262144,Pe=4194304;function Fe(e){var t=e&42;if(t!==0)return t;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return e&261888;case 262144:case 524288:case 1048576:case 2097152:return e&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function Ie(e,t,n){var r=e.pendingLanes;if(r===0)return 0;var i=0,a=e.suspendedLanes,o=e.pingedLanes;e=e.warmLanes;var s=r&134217727;return s===0?(s=r&~a,s===0?o===0?n||(n=r&~e,n!==0&&(i=Fe(n))):i=Fe(o):i=Fe(s)):(r=s&~a,r===0?(o&=s,o===0?n||(n=s&~e,n!==0&&(i=Fe(n))):i=Fe(o)):i=Fe(r)),i===0?0:t!==0&&t!==i&&(t&a)===0&&(a=i&-i,n=t&-t,a>=n||a===32&&n&4194048)?t:i}function Le(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function Re(e,t){switch(e){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function ze(){var e=Pe;return Pe<<=1,!(Pe&62914560)&&(Pe=4194304),e}function Be(e){for(var t=[],n=0;31>n;n++)t.push(e);return t}function Ve(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function He(e,t,n,r,i,a){var o=e.pendingLanes;e.pendingLanes=n,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=n,e.entangledLanes&=n,e.errorRecoveryDisabledLanes&=n,e.shellSuspendCounter=0;var s=e.entanglements,c=e.expirationTimes,l=e.hiddenUpdates;for(n=o&~n;0`u`||window.document===void 0||window.document.createElement===void 0),en=!1;if($t)try{var tn={};Object.defineProperty(tn,`passive`,{get:function(){en=!0}}),window.addEventListener(`test`,tn,tn),window.removeEventListener(`test`,tn,tn)}catch{en=!1}var nn=null,rn=null,an=null;function on(){if(an)return an;var e,t=rn,n=t.length,r,i=`value`in nn?nn.value:nn.textContent,a=i.length;for(e=0;e=Rn),Vn=` `,Hn=!1;function Un(e,t){switch(e){case`keyup`:return In.indexOf(t.keyCode)!==-1;case`keydown`:return t.keyCode!==229;case`keypress`:case`mousedown`:case`focusout`:return!0;default:return!1}}function Wn(e){return e=e.detail,typeof e==`object`&&`data`in e?e.data:null}var Gn=!1;function Kn(e,t){switch(e){case`compositionend`:return Wn(t);case`keypress`:return t.which===32?(Hn=!0,Vn):null;case`textInput`:return e=t.data,e===Vn&&Hn?null:e;default:return null}}function qn(e,t){if(Gn)return e===`compositionend`||!Ln&&Un(e,t)?(e=on(),an=rn=nn=null,Gn=!1,e):null;switch(e){case`paste`:return null;case`keypress`:if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}a:{for(;n;){if(n.nextSibling){n=n.nextSibling;break a}n=n.parentNode}n=void 0}n=hr(n)}}function _r(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?_r(e,t.parentNode):`contains`in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function vr(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=Dt(e.document);t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href==`string`}catch{n=!1}if(n)e=t.contentWindow;else break;t=Dt(e.document)}return t}function yr(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t===`input`&&(e.type===`text`||e.type===`search`||e.type===`tel`||e.type===`url`||e.type===`password`)||t===`textarea`||e.contentEditable===`true`)}var br=$t&&`documentMode`in document&&11>=document.documentMode,xr=null,Sr=null,Cr=null,wr=!1;function Tr(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;wr||xr==null||xr!==Dt(r)||(r=xr,`selectionStart`in r&&yr(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),Cr&&mr(Cr,r)||(Cr=r,r=kd(Sr,`onSelect`),0>=o,i-=o,_i=1<<32-Oe(t)+i|n<h?(g=d,d=null):g=d.sibling;var _=p(i,d,s[h],c);if(_===null){d===null&&(d=g);break}e&&d&&_.alternate===null&&t(i,d),a=o(_,a,h),u===null?l=_:u.sibling=_,u=_,d=g}if(h===s.length)return n(i,d),Ei&&yi(i,h),l;if(d===null){for(;hg?(_=h,h=null):_=h.sibling;var y=p(a,h,v.value,l);if(y===null){h===null&&(h=_);break}e&&h&&y.alternate===null&&t(a,h),s=o(y,s,g),d===null?u=y:d.sibling=y,d=y,h=_}if(v.done)return n(a,h),Ei&&yi(a,g),u;if(h===null){for(;!v.done;g++,v=c.next())v=f(a,v.value,l),v!==null&&(s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return Ei&&yi(a,g),u}for(h=r(h);!v.done;g++,v=c.next())v=m(h,a,g,v.value,l),v!==null&&(e&&v.alternate!==null&&h.delete(v.key===null?g:v.key),s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return e&&h.forEach(function(e){return t(a,e)}),Ei&&yi(a,g),u}function b(e,r,o,c){if(typeof o==`object`&&o&&o.type===y&&o.key===null&&(o=o.props.children),typeof o==`object`&&o){switch(o.$$typeof){case _:a:{for(var l=o.key;r!==null;){if(r.key===l){if(l=o.type,l===y){if(r.tag===7){n(e,r.sibling),c=a(r,o.props.children),c.return=e,e=c;break a}}else if(r.elementType===l||typeof l==`object`&&l&&l.$$typeof===O&&ya(l)===r.type){n(e,r.sibling),c=a(r,o.props),Ea(c,o),c.return=e,e=c;break a}n(e,r);break}else t(e,r);r=r.sibling}o.type===y?(c=ii(o.props.children,e.mode,c,o.key),c.return=e,e=c):(c=ri(o.type,o.key,o.props,null,e.mode,c),Ea(c,o),c.return=e,e=c)}return s(e);case v:a:{for(l=o.key;r!==null;){if(r.key===l)if(r.tag===4&&r.stateNode.containerInfo===o.containerInfo&&r.stateNode.implementation===o.implementation){n(e,r.sibling),c=a(r,o.children||[]),c.return=e,e=c;break a}else{n(e,r);break}else t(e,r);r=r.sibling}c=si(o,e.mode,c),c.return=e,e=c}return s(e);case O:return o=ya(o),b(e,r,o,c)}if(F(o))return h(e,r,o,c);if(M(o)){if(l=M(o),typeof l!=`function`)throw Error(i(150));return o=l.call(o),g(e,r,o,c)}if(typeof o.then==`function`)return b(e,r,Ta(o),c);if(o.$$typeof===C)return b(e,r,Ji(e,o),c);Da(e,o)}return typeof o==`string`&&o!==``||typeof o==`number`||typeof o==`bigint`?(o=``+o,r!==null&&r.tag===6?(n(e,r.sibling),c=a(r,o),c.return=e,e=c):(n(e,r),c=ai(o,e.mode,c),c.return=e,e=c),s(e)):n(e,r)}return function(e,t,n,r){try{wa=0;var i=b(e,t,n,r);return Ca=null,i}catch(t){if(t===pa||t===ha)throw t;var a=$r(29,t,null,e.mode);return a.lanes=r,a.return=e,a}}}var ka=Oa(!0),Aa=Oa(!1),ja=!1;function Ma(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function Na(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,callbacks:null})}function Pa(e){return{lane:e,tag:0,payload:null,callback:null,next:null}}function Fa(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,Fl&2){var i=r.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),r.pending=t,t=Xr(e),Yr(e,null,n),t}return Kr(e,r,t,n),Xr(e)}function Ia(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,n&4194048)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,We(e,n)}}function La(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var i=null,a=null;if(n=n.firstBaseUpdate,n!==null){do{var o={lane:n.lane,tag:n.tag,payload:n.payload,callback:null,next:null};a===null?i=a=o:a=a.next=o,n=n.next}while(n!==null);a===null?i=a=t:a=a.next=t}else i=a=t;n={baseState:r.baseState,firstBaseUpdate:i,lastBaseUpdate:a,shared:r.shared,callbacks:r.callbacks},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}var Ra=!1;function za(){if(Ra){var e=aa;if(e!==null)throw e}}function Ba(e,t,n,r){Ra=!1;var i=e.updateQueue;ja=!1;var a=i.firstBaseUpdate,o=i.lastBaseUpdate,s=i.shared.pending;if(s!==null){i.shared.pending=null;var c=s,l=c.next;c.next=null,o===null?a=l:o.next=l,o=c;var u=e.alternate;u!==null&&(u=u.updateQueue,s=u.lastBaseUpdate,s!==o&&(s===null?u.firstBaseUpdate=l:s.next=l,u.lastBaseUpdate=c))}if(a!==null){var d=i.baseState;o=0,u=l=c=null,s=a;do{var f=s.lane&-536870913,p=f!==s.lane;if(p?(Rl&f)===f:(r&f)===f){f!==0&&f===ia&&(Ra=!0),u!==null&&(u=u.next={lane:0,tag:s.tag,payload:s.payload,callback:null,next:null});a:{var h=e,g=s;f=t;var _=n;switch(g.tag){case 1:if(h=g.payload,typeof h==`function`){d=h.call(_,d,f);break a}d=h;break a;case 3:h.flags=h.flags&-65537|128;case 0:if(h=g.payload,f=typeof h==`function`?h.call(_,d,f):h,f==null)break a;d=m({},d,f);break a;case 2:ja=!0}}f=s.callback,f!==null&&(e.flags|=64,p&&(e.flags|=8192),p=i.callbacks,p===null?i.callbacks=[f]:p.push(f))}else p={lane:f,tag:s.tag,payload:s.payload,callback:s.callback,next:null},u===null?(l=u=p,c=d):u=u.next=p,o|=f;if(s=s.next,s===null){if(s=i.shared.pending,s===null)break;p=s,s=p.next,p.next=null,i.lastBaseUpdate=p,i.shared.pending=null}}while(1);u===null&&(c=d),i.baseState=c,i.firstBaseUpdate=l,i.lastBaseUpdate=u,a===null&&(i.shared.lanes=0),Kl|=o,e.lanes=o,e.memoizedState=d}}function Va(e,t){if(typeof e!=`function`)throw Error(i(191,e));e.call(t)}function Ha(e,t){var n=e.callbacks;if(n!==null)for(e.callbacks=null,e=0;ea?a:8;var o=I.T,s={};I.T=s,Os(e,!1,t,n);try{var c=i(),l=I.S;l!==null&&l(s,c),typeof c==`object`&&c&&typeof c.then==`function`?Ds(e,t,J(c,r),mu(e)):Ds(e,t,r,mu(e))}catch(n){Ds(e,t,{then:function(){},status:`rejected`,reason:n},mu())}finally{L.p=a,o!==null&&s.types!==null&&(o.types=s.types),I.T=o}}function _s(){}function vs(e,t,n,r){if(e.tag!==5)throw Error(i(476));var a=ys(e).queue;gs(e,a,t,R,n===null?_s:function(){return bs(e),n(r)})}function ys(e){var t=e.memoizedState;if(t!==null)return t;t={memoizedState:R,baseState:R,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:ko,lastRenderedState:R},next:null};var n={};return t.next={memoizedState:n,baseState:n,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:ko,lastRenderedState:n},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function bs(e){var t=ys(e);t.next===null&&(t=e.alternate.memoizedState),Ds(e,t.next.queue,{},mu())}function xs(){return qi(np)}function Ss(){return wo().memoizedState}function Cs(){return wo().memoizedState}function ws(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var n=mu();e=Pa(n);var r=Fa(t,e,n);r!==null&&(gu(r,t,n),Ia(r,t,n)),t={cache:ea()},e.payload=t;return}t=t.return}}function Ts(e,t,n){var r=mu();n={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null},ks(e)?As(t,n):(n=qr(e,t,n,r),n!==null&&(gu(n,e,r),js(n,t,r)))}function Es(e,t,n){Ds(e,t,n,mu())}function Ds(e,t,n,r){var i={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null};if(ks(e))As(t,i);else{var a=e.alternate;if(e.lanes===0&&(a===null||a.lanes===0)&&(a=t.lastRenderedReducer,a!==null))try{var o=t.lastRenderedState,s=a(o,n);if(i.hasEagerState=!0,i.eagerState=s,pr(s,o))return Kr(e,t,i,0),Il===null&&Gr(),!1}catch{}if(n=qr(e,t,i,r),n!==null)return gu(n,e,r),js(n,t,r),!0}return!1}function Os(e,t,n,r){if(r={lane:2,revertLane:pd(),gesture:null,action:r,hasEagerState:!1,eagerState:null,next:null},ks(e)){if(t)throw Error(i(479))}else t=qr(e,n,r,2),t!==null&&gu(t,e,2)}function ks(e){var t=e.alternate;return e===ro||t!==null&&t===ro}function As(e,t){so=oo=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function js(e,t,n){if(n&4194048){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,We(e,n)}}var Ms={readContext:qi,use:Do,useCallback:mo,useContext:mo,useEffect:mo,useImperativeHandle:mo,useLayoutEffect:mo,useInsertionEffect:mo,useMemo:mo,useReducer:mo,useRef:mo,useState:mo,useDebugValue:mo,useDeferredValue:mo,useTransition:mo,useSyncExternalStore:mo,useId:mo,useHostTransitionStatus:mo,useFormState:mo,useActionState:mo,useOptimistic:mo,useMemoCache:mo,useCacheRefresh:mo};Ms.useEffectEvent=mo;var Ns={readContext:qi,use:Do,useCallback:function(e,t){return Co().memoizedState=[e,t===void 0?null:t],e},useContext:qi,useEffect:rs,useImperativeHandle:function(e,t,n){n=n==null?null:n.concat([e]),ts(4194308,4,ls.bind(null,t,e),n)},useLayoutEffect:function(e,t){return ts(4194308,4,e,t)},useInsertionEffect:function(e,t){ts(4,2,e,t)},useMemo:function(e,t){var n=Co();t=t===void 0?null:t;var r=e();if(co){De(!0);try{e()}finally{De(!1)}}return n.memoizedState=[r,t],r},useReducer:function(e,t,n){var r=Co();if(n!==void 0){var i=n(t);if(co){De(!0);try{n(t)}finally{De(!1)}}}else i=t;return r.memoizedState=r.baseState=i,e={pending:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:i},r.queue=e,e=e.dispatch=Ts.bind(null,ro,e),[r.memoizedState,e]},useRef:function(e){var t=Co();return e={current:e},t.memoizedState=e},useState:function(e){e=zo(e);var t=e.queue,n=Es.bind(null,ro,t);return t.dispatch=n,[e.memoizedState,n]},useDebugValue:ds,useDeferredValue:function(e,t){return ms(Co(),e,t)},useTransition:function(){var e=zo(!1);return e=gs.bind(null,ro,e.queue,!0,!1),Co().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,n){var r=ro,a=Co();if(Ei){if(n===void 0)throw Error(i(407));n=n()}else{if(n=t(),Il===null)throw Error(i(349));Rl&127||Po(r,t,n)}a.memoizedState=n;var o={value:n,getSnapshot:t};return a.queue=o,rs(Io.bind(null,r,o,e),[e]),r.flags|=2048,$o(9,{destroy:void 0},Fo.bind(null,r,o,n,t),null),n},useId:function(){var e=Co(),t=Il.identifierPrefix;if(Ei){var n=vi,r=_i;n=(r&~(1<<32-Oe(r)-1)).toString(32)+n,t=`_`+t+`R_`+n,n=lo++,0<\/script>`,o=o.removeChild(o.firstChild);break;case`select`:o=typeof r.is==`string`?s.createElement(`select`,{is:r.is}):s.createElement(`select`),r.multiple?o.multiple=!0:r.size&&(o.size=r.size);break;default:o=typeof r.is==`string`?s.createElement(a,{is:r.is}):s.createElement(a)}}o[Ze]=t,o[Qe]=r;a:for(s=t.child;s!==null;){if(s.tag===5||s.tag===6)o.appendChild(s.stateNode);else if(s.tag!==4&&s.tag!==27&&s.child!==null){s.child.return=s,s=s.child;continue}if(s===t)break a;for(;s.sibling===null;){if(s.return===null||s.return===t)break a;s=s.return}s.sibling.return=s.return,s=s.sibling}t.stateNode=o;a:switch(Rd(o,a,r),a){case`button`:case`input`:case`select`:case`textarea`:r=!!r.autoFocus;break a;case`img`:r=!0;break a;default:r=!1}r&&Dc(t)}}return Mc(t),Oc(t,t.type,e===null?null:e.memoizedProps,t.pendingProps,n),null;case 6:if(e&&t.stateNode!=null)e.memoizedProps!==r&&Dc(t);else{if(typeof r!=`string`&&t.stateNode===null)throw Error(i(166));if(e=G.current,Ni(t)){if(e=t.stateNode,n=t.memoizedProps,r=null,a=wi,a!==null)switch(a.tag){case 27:case 5:r=a.memoizedProps}e[Ze]=t,e=!!(e.nodeValue===n||r!==null&&!0===r.suppressHydrationWarning||Fd(e.nodeValue,n)),e||Ai(t,!0)}else e=Wd(e).createTextNode(r),e[Ze]=t,t.stateNode=e}return Mc(t),null;case 31:if(n=t.memoizedState,e===null||e.memoizedState!==null){if(r=Ni(t),n!==null){if(e===null){if(!r)throw Error(i(318));if(e=t.memoizedState,e=e===null?null:e.dehydrated,!e)throw Error(i(557));e[Ze]=t}else Pi(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;Mc(t),e=!1}else n=Fi(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=n),e=!0;if(!e)return t.flags&256?($a(t),t):($a(t),null);if(t.flags&128)throw Error(i(558))}return Mc(t),null;case 13:if(r=t.memoizedState,e===null||e.memoizedState!==null&&e.memoizedState.dehydrated!==null){if(a=Ni(t),r!==null&&r.dehydrated!==null){if(e===null){if(!a)throw Error(i(318));if(a=t.memoizedState,a=a===null?null:a.dehydrated,!a)throw Error(i(317));a[Ze]=t}else Pi(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;Mc(t),a=!1}else a=Fi(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=a),a=!0;if(!a)return t.flags&256?($a(t),t):($a(t),null)}return $a(t),t.flags&128?(t.lanes=n,t):(n=r!==null,e=e!==null&&e.memoizedState!==null,n&&(r=t.child,a=null,r.alternate!==null&&r.alternate.memoizedState!==null&&r.alternate.memoizedState.cachePool!==null&&(a=r.alternate.memoizedState.cachePool.pool),o=null,r.memoizedState!==null&&r.memoizedState.cachePool!==null&&(o=r.memoizedState.cachePool.pool),o!==a&&(r.flags|=2048)),n!==e&&n&&(t.child.flags|=8192),Ac(t,t.updateQueue),Mc(t),null);case 4:return K(),e===null&&Td(t.stateNode.containerInfo),Mc(t),null;case 10:return Vi(t.type),Mc(t),null;case 19:if(H(eo),r=t.memoizedState,r===null)return Mc(t),null;if(a=(t.flags&128)!=0,o=r.rendering,o===null)if(a)jc(r,!1);else{if(Gl!==0||e!==null&&e.flags&128)for(e=t.child;e!==null;){if(o=to(e),o!==null){for(t.flags|=128,jc(r,!1),e=o.updateQueue,t.updateQueue=e,Ac(t,e),t.subtreeFlags=0,e=n,n=t.child;n!==null;)ni(n,e),n=n.sibling;return U(eo,eo.current&1|2),Ei&&yi(t,r.treeForkCount),t.child}e=e.sibling}r.tail!==null&&ge()>nu&&(t.flags|=128,a=!0,jc(r,!1),t.lanes=4194304)}else{if(!a)if(e=to(o),e!==null){if(t.flags|=128,a=!0,e=e.updateQueue,t.updateQueue=e,Ac(t,e),jc(r,!0),r.tail===null&&r.tailMode===`hidden`&&!o.alternate&&!Ei)return Mc(t),null}else 2*ge()-r.renderingStartTime>nu&&n!==536870912&&(t.flags|=128,a=!0,jc(r,!1),t.lanes=4194304);r.isBackwards?(o.sibling=t.child,t.child=o):(e=r.last,e===null?t.child=o:e.sibling=o,r.last=o)}return r.tail===null?(Mc(t),null):(e=r.tail,r.rendering=e,r.tail=e.sibling,r.renderingStartTime=ge(),e.sibling=null,n=eo.current,U(eo,a?n&1|2:n&1),Ei&&yi(t,r.treeForkCount),e);case 22:case 23:return $a(t),Ka(),r=t.memoizedState!==null,e===null?r&&(t.flags|=8192):e.memoizedState!==null!==r&&(t.flags|=8192),r?n&536870912&&!(t.flags&128)&&(Mc(t),t.subtreeFlags&6&&(t.flags|=8192)):Mc(t),n=t.updateQueue,n!==null&&Ac(t,n.retryQueue),n=null,e!==null&&e.memoizedState!==null&&e.memoizedState.cachePool!==null&&(n=e.memoizedState.cachePool.pool),r=null,t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(r=t.memoizedState.cachePool.pool),r!==n&&(t.flags|=2048),e!==null&&H(la),null;case 24:return n=null,e!==null&&(n=e.memoizedState.cache),t.memoizedState.cache!==n&&(t.flags|=2048),Vi($i),Mc(t),null;case 25:return null;case 30:return null}throw Error(i(156,t.tag))}function Pc(e,t){switch(Si(t),t.tag){case 1:return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Vi($i),K(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 26:case 27:case 5:return ie(t),null;case 31:if(t.memoizedState!==null){if($a(t),t.alternate===null)throw Error(i(340));Pi()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 13:if($a(t),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(i(340));Pi()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return H(eo),null;case 4:return K(),null;case 10:return Vi(t.type),null;case 22:case 23:return $a(t),Ka(),e!==null&&H(la),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 24:return Vi($i),null;case 25:return null;default:return null}}function Fc(e,t){switch(Si(t),t.tag){case 3:Vi($i),K();break;case 26:case 27:case 5:ie(t);break;case 4:K();break;case 31:t.memoizedState!==null&&$a(t);break;case 13:$a(t);break;case 19:H(eo);break;case 10:Vi(t.type);break;case 22:case 23:$a(t),Ka(),e!==null&&H(la);break;case 24:Vi($i)}}function Ic(e,t){try{var n=t.updateQueue,r=n===null?null:n.lastEffect;if(r!==null){var i=r.next;n=i;do{if((n.tag&e)===e){r=void 0;var a=n.create,o=n.inst;r=a(),o.destroy=r}n=n.next}while(n!==i)}}catch(e){Ku(t,t.return,e)}}function Lc(e,t,n){try{var r=t.updateQueue,i=r===null?null:r.lastEffect;if(i!==null){var a=i.next;r=a;do{if((r.tag&e)===e){var o=r.inst,s=o.destroy;if(s!==void 0){o.destroy=void 0,i=t;var c=n,l=s;try{l()}catch(e){Ku(i,c,e)}}}r=r.next}while(r!==a)}}catch(e){Ku(t,t.return,e)}}function Rc(e){var t=e.updateQueue;if(t!==null){var n=e.stateNode;try{Ha(t,n)}catch(t){Ku(e,e.return,t)}}}function zc(e,t,n){n.props=Bs(e.type,e.memoizedProps),n.state=e.memoizedState;try{n.componentWillUnmount()}catch(n){Ku(e,t,n)}}function Bc(e,t){try{var n=e.ref;if(n!==null){switch(e.tag){case 26:case 27:case 5:var r=e.stateNode;break;case 30:r=e.stateNode;break;default:r=e.stateNode}typeof n==`function`?e.refCleanup=n(r):n.current=r}}catch(n){Ku(e,t,n)}}function Vc(e,t){var n=e.ref,r=e.refCleanup;if(n!==null)if(typeof r==`function`)try{r()}catch(n){Ku(e,t,n)}finally{e.refCleanup=null,e=e.alternate,e!=null&&(e.refCleanup=null)}else if(typeof n==`function`)try{n(null)}catch(n){Ku(e,t,n)}else n.current=null}function Hc(e){var t=e.type,n=e.memoizedProps,r=e.stateNode;try{a:switch(t){case`button`:case`input`:case`select`:case`textarea`:n.autoFocus&&r.focus();break a;case`img`:n.src?r.src=n.src:n.srcSet&&(r.srcset=n.srcSet)}}catch(t){Ku(e,e.return,t)}}function Uc(e,t,n){try{var r=e.stateNode;zd(r,e.type,n,t),r[Qe]=t}catch(t){Ku(e,e.return,t)}}function Wc(e){return e.tag===5||e.tag===3||e.tag===26||e.tag===27&&tf(e.type)||e.tag===4}function Gc(e){a:for(;;){for(;e.sibling===null;){if(e.return===null||Wc(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.tag===27&&tf(e.type)||e.flags&2||e.child===null||e.tag===4)continue a;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Kc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?(n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n).insertBefore(e,t):(t=n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n,t.appendChild(e),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=Wt));else if(r!==4&&(r===27&&tf(e.type)&&(n=e.stateNode,t=null),e=e.child,e!==null))for(Kc(e,t,n),e=e.sibling;e!==null;)Kc(e,t,n),e=e.sibling}function qc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(r===27&&tf(e.type)&&(n=e.stateNode),e=e.child,e!==null))for(qc(e,t,n),e=e.sibling;e!==null;)qc(e,t,n),e=e.sibling}function Jc(e){var t=e.stateNode,n=e.memoizedProps;try{for(var r=e.type,i=t.attributes;i.length;)t.removeAttributeNode(i[0]);Rd(t,r,n),t[Ze]=e,t[Qe]=n}catch(t){Ku(e,e.return,t)}}var Yc=!1,Xc=!1,Zc=!1,Qc=typeof WeakSet==`function`?WeakSet:Set,$c=null;function el(e,t){if(e=e.containerInfo,Hd=dp,e=vr(e),yr(e)){if(`selectionStart`in e)var n={start:e.selectionStart,end:e.selectionEnd};else a:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var a=r.anchorOffset,o=r.focusNode;r=r.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break a}var s=0,c=-1,l=-1,u=0,d=0,f=e,p=null;b:for(;;){for(var m;f!==n||a!==0&&f.nodeType!==3||(c=s+a),f!==o||r!==0&&f.nodeType!==3||(l=s+r),f.nodeType===3&&(s+=f.nodeValue.length),(m=f.firstChild)!==null;)p=f,f=m;for(;;){if(f===e)break b;if(p===n&&++u===a&&(c=s),p===o&&++d===r&&(l=s),(m=f.nextSibling)!==null)break;f=p,p=f.parentNode}f=m}n=c===-1||l===-1?null:{start:c,end:l}}else n=null}n||={start:0,end:0}}else n=null;for(Ud={focusedElem:e,selectionRange:n},dp=!1,$c=t;$c!==null;)if(t=$c,e=t.child,t.subtreeFlags&1028&&e!==null)e.return=t,$c=e;else for(;$c!==null;){switch(t=$c,o=t.alternate,e=t.flags,t.tag){case 0:if(e&4&&(e=t.updateQueue,e=e===null?null:e.events,e!==null))for(n=0;n title`))),Rd(o,r,n),o[Ze]=e,ut(o),r=o;break a;case`link`:var s=Gf(`link`,`href`,a).get(r+(n.href||``));if(s){for(var c=0;cg&&(o=g,g=h,h=o);var _=gr(s,h),v=gr(s,g);if(_&&v&&(p.rangeCount!==1||p.anchorNode!==_.node||p.anchorOffset!==_.offset||p.focusNode!==v.node||p.focusOffset!==v.offset)){var y=d.createRange();y.setStart(_.node,_.offset),p.removeAllRanges(),h>g?(p.addRange(y),p.extend(v.node,v.offset)):(y.setEnd(v.node,v.offset),p.addRange(y))}}}}for(d=[],p=s;p=p.parentNode;)p.nodeType===1&&d.push({element:p,left:p.scrollLeft,top:p.scrollTop});for(typeof s.focus==`function`&&s.focus(),s=0;sn?32:n,I.T=null,n=uu,uu=null;var o=ou,s=cu;if(au=0,su=ou=null,cu=0,Fl&6)throw Error(i(331));var c=Fl;if(Fl|=4,Al(o.current),Sl(o,o.current,s,n),Fl=c,od(0,!1),Ee&&typeof Ee.onPostCommitFiberRoot==`function`)try{Ee.onPostCommitFiberRoot(Te,o)}catch{}return!0}finally{L.p=a,I.T=r,Hu(e,t)}}function Gu(e,t,n){t=li(n,t),t=Ks(e.stateNode,t,2),e=Fa(e,t,2),e!==null&&(Ve(e,2),ad(e))}function Ku(e,t,n){if(e.tag===3)Gu(e,e,n);else for(;t!==null;){if(t.tag===3){Gu(t,e,n);break}else if(t.tag===1){var r=t.stateNode;if(typeof t.type.getDerivedStateFromError==`function`||typeof r.componentDidCatch==`function`&&(iu===null||!iu.has(r))){e=li(n,e),n=qs(2),r=Fa(t,n,2),r!==null&&(Js(n,r,t,e),Ve(r,2),ad(r));break}}t=t.return}}function qu(e,t,n){var r=e.pingCache;if(r===null){r=e.pingCache=new Pl;var i=new Set;r.set(t,i)}else i=r.get(t),i===void 0&&(i=new Set,r.set(t,i));i.has(n)||(Ul=!0,i.add(n),e=Ju.bind(null,e,t,n),t.then(e,e))}function Ju(e,t,n){var r=e.pingCache;r!==null&&r.delete(t),e.pingedLanes|=e.suspendedLanes&n,e.warmLanes&=~n,Il===e&&(Rl&n)===n&&(Gl===4||Gl===3&&(Rl&62914560)===Rl&&300>ge()-eu?!(Fl&2)&&Cu(e,0):Jl|=n,Xl===Rl&&(Xl=0)),ad(e)}function Yu(e,t){t===0&&(t=ze()),e=Jr(e,t),e!==null&&(Ve(e,t),ad(e))}function Xu(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),Yu(e,n)}function Zu(e,t){var n=0;switch(e.tag){case 31:case 13:var r=e.stateNode,a=e.memoizedState;a!==null&&(n=a.retryLane);break;case 19:r=e.stateNode;break;case 22:r=e.stateNode._retryCache;break;default:throw Error(i(314))}r!==null&&r.delete(t),Yu(e,n)}function Qu(e,t){return fe(e,t)}var $u=null,ed=null,td=!1,nd=!1,rd=!1,id=0;function ad(e){e!==ed&&e.next===null&&(ed===null?$u=ed=e:ed=ed.next=e),nd=!0,td||(td=!0,fd())}function od(e,t){if(!rd&&nd){rd=!0;do for(var n=!1,r=$u;r!==null;){if(!t)if(e!==0){var i=r.pendingLanes;if(i===0)var a=0;else{var o=r.suspendedLanes,s=r.pingedLanes;a=(1<<31-Oe(42|e)+1)-1,a&=i&~(o&~s),a=a&201326741?a&201326741|1:a?a|2:0}a!==0&&(n=!0,dd(r,a))}else a=Rl,a=Ie(r,r===Il?a:0,r.cancelPendingCommit!==null||r.timeoutHandle!==-1),!(a&3)||Le(r,a)||(n=!0,dd(r,a));r=r.next}while(n);rd=!1}}function sd(){cd()}function cd(){nd=td=!1;var e=0;id!==0&&Yd()&&(e=id);for(var t=ge(),n=null,r=$u;r!==null;){var i=r.next,a=ld(r,t);a===0?(r.next=null,n===null?$u=i:n.next=i,i===null&&(ed=n)):(n=r,(e!==0||a&3)&&(nd=!0)),r=i}au!==0&&au!==5||od(e,!1),id!==0&&(id=0)}function ld(e,t){for(var n=e.suspendedLanes,r=e.pingedLanes,i=e.expirationTimes,a=e.pendingLanes&-62914561;0s)break;var u=c.transferSize,d=c.initiatorType;u&&Bd(d)&&(c=c.responseEnd,o+=u*(c`u`?null:document;function Tf(e,t,n){var r=wf;if(r&&typeof t==`string`&&t){var i=kt(t);i=`link[rel="`+e+`"][href="`+i+`"]`,typeof n==`string`&&(i+=`[crossorigin="`+n+`"]`),yf.has(i)||(yf.add(i),e={rel:e,crossOrigin:n,href:t},r.querySelector(i)===null&&(t=r.createElement(`link`),Rd(t,`link`,e),ut(t),r.head.appendChild(t)))}}function Ef(e){xf.D(e),Tf(`dns-prefetch`,e,null)}function Df(e,t){xf.C(e,t),Tf(`preconnect`,e,t)}function Of(e,t,n){xf.L(e,t,n);var r=wf;if(r&&e&&t){var i=`link[rel="preload"][as="`+kt(t)+`"]`;t===`image`&&n&&n.imageSrcSet?(i+=`[imagesrcset="`+kt(n.imageSrcSet)+`"]`,typeof n.imageSizes==`string`&&(i+=`[imagesizes="`+kt(n.imageSizes)+`"]`)):i+=`[href="`+kt(e)+`"]`;var a=i;switch(t){case`style`:a=Pf(e);break;case`script`:a=Rf(e)}vf.has(a)||(e=m({rel:`preload`,href:t===`image`&&n&&n.imageSrcSet?void 0:e,as:t},n),vf.set(a,e),r.querySelector(i)!==null||t===`style`&&r.querySelector(Ff(a))||t===`script`&&r.querySelector(zf(a))||(t=r.createElement(`link`),Rd(t,`link`,e),ut(t),r.head.appendChild(t)))}}function kf(e,t){xf.m(e,t);var n=wf;if(n&&e){var r=t&&typeof t.as==`string`?t.as:`script`,i=`link[rel="modulepreload"][as="`+kt(r)+`"][href="`+kt(e)+`"]`,a=i;switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:a=Rf(e)}if(!vf.has(a)&&(e=m({rel:`modulepreload`,href:e},t),vf.set(a,e),n.querySelector(i)===null)){switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:if(n.querySelector(zf(a)))return}r=n.createElement(`link`),Rd(r,`link`,e),ut(r),n.head.appendChild(r)}}}function Af(e,t,n){xf.S(e,t,n);var r=wf;if(r&&e){var i=lt(r).hoistableStyles,a=Pf(e);t||=`default`;var o=i.get(a);if(!o){var s={loading:0,preload:null};if(o=r.querySelector(Ff(a)))s.loading=5;else{e=m({rel:`stylesheet`,href:e,"data-precedence":t},n),(n=vf.get(a))&&Hf(e,n);var c=o=r.createElement(`link`);ut(c),Rd(c,`link`,e),c._p=new Promise(function(e,t){c.onload=e,c.onerror=t}),c.addEventListener(`load`,function(){s.loading|=1}),c.addEventListener(`error`,function(){s.loading|=2}),s.loading|=4,Vf(o,t,r)}o={type:`stylesheet`,instance:o,count:1,state:s},i.set(a,o)}}}function jf(e,t){xf.X(e,t);var n=wf;if(n&&e){var r=lt(n).hoistableScripts,i=Rf(e),a=r.get(i);a||(a=n.querySelector(zf(i)),a||(e=m({src:e,async:!0},t),(t=vf.get(i))&&Uf(e,t),a=n.createElement(`script`),ut(a),Rd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function Mf(e,t){xf.M(e,t);var n=wf;if(n&&e){var r=lt(n).hoistableScripts,i=Rf(e),a=r.get(i);a||(a=n.querySelector(zf(i)),a||(e=m({src:e,async:!0,type:`module`},t),(t=vf.get(i))&&Uf(e,t),a=n.createElement(`script`),ut(a),Rd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function Nf(e,t,n,r){var a=(a=G.current)?bf(a):null;if(!a)throw Error(i(446));switch(e){case`meta`:case`title`:return null;case`style`:return typeof n.precedence==`string`&&typeof n.href==`string`?(t=Pf(n.href),n=lt(a).hoistableStyles,r=n.get(t),r||(r={type:`style`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};case`link`:if(n.rel===`stylesheet`&&typeof n.href==`string`&&typeof n.precedence==`string`){e=Pf(n.href);var o=lt(a).hoistableStyles,s=o.get(e);if(s||(a=a.ownerDocument||a,s={type:`stylesheet`,instance:null,count:0,state:{loading:0,preload:null}},o.set(e,s),(o=a.querySelector(Ff(e)))&&!o._p&&(s.instance=o,s.state.loading=5),vf.has(e)||(n={rel:`preload`,as:`style`,href:n.href,crossOrigin:n.crossOrigin,integrity:n.integrity,media:n.media,hrefLang:n.hrefLang,referrerPolicy:n.referrerPolicy},vf.set(e,n),o||Lf(a,e,n,s.state))),t&&r===null)throw Error(i(528,``));return s}if(t&&r!==null)throw Error(i(529,``));return null;case`script`:return t=n.async,n=n.src,typeof n==`string`&&t&&typeof t!=`function`&&typeof t!=`symbol`?(t=Rf(n),n=lt(a).hoistableScripts,r=n.get(t),r||(r={type:`script`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};default:throw Error(i(444,e))}}function Pf(e){return`href="`+kt(e)+`"`}function Ff(e){return`link[rel="stylesheet"][`+e+`]`}function If(e){return m({},e,{"data-precedence":e.precedence,precedence:null})}function Lf(e,t,n,r){e.querySelector(`link[rel="preload"][as="style"][`+t+`]`)?r.loading=1:(t=e.createElement(`link`),r.preload=t,t.addEventListener(`load`,function(){return r.loading|=1}),t.addEventListener(`error`,function(){return r.loading|=2}),Rd(t,`link`,n),ut(t),e.head.appendChild(t))}function Rf(e){return`[src="`+kt(e)+`"]`}function zf(e){return`script[async]`+e}function Bf(e,t,n){if(t.count++,t.instance===null)switch(t.type){case`style`:var r=e.querySelector(`style[data-href~="`+kt(n.href)+`"]`);if(r)return t.instance=r,ut(r),r;var a=m({},n,{"data-href":n.href,"data-precedence":n.precedence,href:null,precedence:null});return r=(e.ownerDocument||e).createElement(`style`),ut(r),Rd(r,`style`,a),Vf(r,n.precedence,e),t.instance=r;case`stylesheet`:a=Pf(n.href);var o=e.querySelector(Ff(a));if(o)return t.state.loading|=4,t.instance=o,ut(o),o;r=If(n),(a=vf.get(a))&&Hf(r,a),o=(e.ownerDocument||e).createElement(`link`),ut(o);var s=o;return s._p=new Promise(function(e,t){s.onload=e,s.onerror=t}),Rd(o,`link`,r),t.state.loading|=4,Vf(o,n.precedence,e),t.instance=o;case`script`:return o=Rf(n.src),(a=e.querySelector(zf(o)))?(t.instance=a,ut(a),a):(r=n,(a=vf.get(o))&&(r=m({},n),Uf(r,a)),e=e.ownerDocument||e,a=e.createElement(`script`),ut(a),Rd(a,`link`,r),e.head.appendChild(a),t.instance=a);case`void`:return null;default:throw Error(i(443,t.type))}else t.type===`stylesheet`&&!(t.state.loading&4)&&(r=t.instance,t.state.loading|=4,Vf(r,n.precedence,e));return t.instance}function Vf(e,t,n){for(var r=n.querySelectorAll(`link[rel="stylesheet"][data-precedence],style[data-precedence]`),i=r.length?r[r.length-1]:null,a=i,o=0;o title`):null)}function qf(e,t,n){if(n===1||t.itemProp!=null)return!1;switch(e){case`meta`:case`title`:return!0;case`style`:if(typeof t.precedence!=`string`||typeof t.href!=`string`||t.href===``)break;return!0;case`link`:if(typeof t.rel!=`string`||typeof t.href!=`string`||t.href===``||t.onLoad||t.onError)break;switch(t.rel){case`stylesheet`:return e=t.disabled,typeof t.precedence==`string`&&e==null;default:return!0}case`script`:if(t.async&&typeof t.async!=`function`&&typeof t.async!=`symbol`&&!t.onLoad&&!t.onError&&t.src&&typeof t.src==`string`)return!0}return!1}function Jf(e){return!(e.type===`stylesheet`&&!(e.state.loading&3))}function Yf(e,t,n,r){if(n.type===`stylesheet`&&(typeof r.media!=`string`||!1!==matchMedia(r.media).matches)&&!(n.state.loading&4)){if(n.instance===null){var i=Pf(r.href),a=t.querySelector(Ff(i));if(a){t=a._p,typeof t==`object`&&t&&typeof t.then==`function`&&(e.count++,e=Qf.bind(e),t.then(e,e)),n.state.loading|=4,n.instance=a,ut(a);return}a=t.ownerDocument||t,r=If(r),(i=vf.get(i))&&Hf(r,i),a=a.createElement(`link`),ut(a);var o=a;o._p=new Promise(function(e,t){o.onload=e,o.onerror=t}),Rd(a,`link`,r),n.instance=a}e.stylesheets===null&&(e.stylesheets=new Map),e.stylesheets.set(n,t),(t=n.state.preload)&&!(n.state.loading&3)&&(e.count++,n=Qf.bind(e),t.addEventListener(`load`,n),t.addEventListener(`error`,n))}}var Xf=0;function Zf(e,t){return e.stylesheets&&e.count===0&&ep(e,e.stylesheets),0Xf?50:800)+t);return e.unsuspend=n,function(){e.unsuspend=null,clearTimeout(r),clearTimeout(i)}}:null}function Qf(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)ep(this,this.stylesheets);else if(this.unsuspend){var e=this.unsuspend;this.unsuspend=null,e()}}}var $f=null;function ep(e,t){e.stylesheets=null,e.unsuspend!==null&&(e.count++,$f=new Map,t.forEach(tp,e),$f=null,Qf.call(e))}function tp(e,t){if(!(t.state.loading&4)){var n=$f.get(e);if(n)var r=n.get(null);else{n=new Map,$f.set(e,n);for(var i=e.querySelectorAll(`link[data-precedence],style[data-precedence]`),a=0;a{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=g()}))(),v=`modulepreload`,y=function(e){return`/`+e},b={},x=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}r=o(t.map(t=>{if(t=y(t,n),t in b)return;b[t]=!0;let r=t.endsWith(`.css`),i=r?`[rel="stylesheet"]`:``;if(n)for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}else if(document.querySelector(`link[href="${t}"]${i}`))return;let o=document.createElement(`link`);if(o.rel=r?`stylesheet`:v,r||(o.as=`script`),o.crossOrigin=``,o.href=t,a&&o.setAttribute(`nonce`,a),document.head.appendChild(o),r)return new Promise((e,n)=>{o.addEventListener(`load`,e),o.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})},S=c(p(),1),C=`popstate`;function w(e){return typeof e==`object`&&!!e&&`pathname`in e&&`search`in e&&`hash`in e&&`state`in e&&`key`in e}function T(e={}){function t(e,t){let n=t.state?.masked,{pathname:r,search:i,hash:a}=n||e.location;return A(``,{pathname:r,search:i,hash:a},t.state&&t.state.usr||null,t.state&&t.state.key||`default`,n?{pathname:e.location.pathname,search:e.location.search,hash:e.location.hash}:void 0)}function n(e,t){return typeof t==`string`?t:j(t)}return N(t,n,null,e)}function E(e,t){if(e===!1||e==null)throw Error(t)}function D(e,t){if(!e){typeof console<`u`&&console.warn(t);try{throw Error(t)}catch{}}}function O(){return Math.random().toString(36).substring(2,10)}function k(e,t){return{usr:e.state,key:e.key,idx:t,masked:e.mask?{pathname:e.pathname,search:e.search,hash:e.hash}:void 0}}function A(e,t,n=null,r,i){return{pathname:typeof e==`string`?e:e.pathname,search:``,hash:``,...typeof t==`string`?M(t):t,state:n,key:t&&t.key||r||O(),mask:i}}function j({pathname:e=`/`,search:t=``,hash:n=``}){return t&&t!==`?`&&(e+=t.charAt(0)===`?`?t:`?`+t),n&&n!==`#`&&(e+=n.charAt(0)===`#`?n:`#`+n),e}function M(e){let t={};if(e){let n=e.indexOf(`#`);n>=0&&(t.hash=e.substring(n),e=e.substring(0,n));let r=e.indexOf(`?`);r>=0&&(t.search=e.substring(r),e=e.substring(0,r)),e&&(t.pathname=e)}return t}function N(e,t,n,r={}){let{window:i=document.defaultView,v5Compat:a=!1}=r,o=i.history,s=`POP`,c=null,l=u();l??(l=0,o.replaceState({...o.state,idx:l},``));function u(){return(o.state||{idx:null}).idx}function d(){s=`POP`;let e=u(),t=e==null?null:e-l;l=e,c&&c({action:s,location:h.location,delta:t})}function f(e,t){s=`PUSH`;let r=w(e)?e:A(h.location,e,t);n&&n(r,e),l=u()+1;let d=k(r,l),f=h.createHref(r.mask||r);try{o.pushState(d,``,f)}catch(e){if(e instanceof DOMException&&e.name===`DataCloneError`)throw e;i.location.assign(f)}a&&c&&c({action:s,location:h.location,delta:1})}function p(e,t){s=`REPLACE`;let r=w(e)?e:A(h.location,e,t);n&&n(r,e),l=u();let i=k(r,l),d=h.createHref(r.mask||r);o.replaceState(i,``,d),a&&c&&c({action:s,location:h.location,delta:0})}function m(e){return P(e)}let h={get action(){return s},get location(){return e(i,o)},listen(e){if(c)throw Error(`A history only accepts one active listener`);return i.addEventListener(C,d),c=e,()=>{i.removeEventListener(C,d),c=null}},createHref(e){return t(i,e)},createURL:m,encodeLocation(e){let t=m(e);return{pathname:t.pathname,search:t.search,hash:t.hash}},push:f,replace:p,go(e){return o.go(e)}};return h}function P(e,t=!1){let n=`http://localhost`;typeof window<`u`&&(n=window.location.origin===`null`?window.location.href:window.location.origin),E(n,`No window.location.(origin|href) available to create URL`);let r=typeof e==`string`?e:j(e);return r=r.replace(/ $/,`%20`),!t&&r.startsWith(`//`)&&(r=n+r),new URL(r,n)}function F(e,t,n=`/`){return I(e,t,n,!1)}function I(e,t,n,r,i){let a=se((typeof t==`string`?M(t):t).pathname||`/`,n);if(a==null)return null;let o=i??R(e),s=null,c=oe(a);for(let e=0;s==null&&e{let c={relativePath:s===void 0?e.path||``:s,caseSensitive:e.caseSensitive===!0,childrenIndex:a,route:e};if(c.relativePath.startsWith(`/`)){if(!c.relativePath.startsWith(r)&&o)return;E(c.relativePath.startsWith(r),`Absolute route path "${c.relativePath}" nested under path "${r}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),c.relativePath=c.relativePath.slice(r.length)}let l=ge([r,c.relativePath]),u=n.concat(c);e.children&&e.children.length>0&&(E(e.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${l}".`),z(e.children,t,u,l,o)),!(e.path==null&&!e.index)&&t.push({path:l,score:K(l,e.index),routesMeta:u})};return e.forEach((e,t)=>{if(e.path===``||!e.path?.includes(`?`))a(e,t);else for(let n of B(e.path))a(e,t,!0,n)}),t}function B(e){let t=e.split(`/`);if(t.length===0)return[];let[n,...r]=t,i=n.endsWith(`?`),a=n.replace(/\?$/,``);if(r.length===0)return i?[a,``]:[a];let o=B(r.join(`/`)),s=[];return s.push(...o.map(e=>e===``?a:[a,e].join(`/`))),i&&s.push(...o),s.map(t=>e.startsWith(`/`)&&t===``?`/`:t)}function V(e){e.sort((e,t)=>e.score===t.score?re(e.routesMeta.map(e=>e.childrenIndex),t.routesMeta.map(e=>e.childrenIndex)):t.score-e.score)}var H=/^:[\w-]+$/,U=3,W=2,ee=1,G=10,te=-2,ne=e=>e===`*`;function K(e,t){let n=e.split(`/`),r=n.length;return n.some(ne)&&(r+=te),t&&(r+=W),n.filter(e=>!ne(e)).reduce((e,t)=>e+(H.test(t)?U:t===``?ee:G),r)}function re(e,t){return e.length===t.length&&e.slice(0,-1).every((e,n)=>e===t[n])?e[e.length-1]-t[t.length-1]:0}function ie(e,t,n=!1){let{routesMeta:r}=e,i={},a=`/`,o=[];for(let e=0;e{if(t===`*`){let e=s[r]||``;o=a.slice(0,a.length-e.length).replace(/(.)\/+$/,`$1`)}let i=s[r];return n&&!i?e[t]=void 0:e[t]=(i||``).replace(/%2F/g,`/`),e},{}),pathname:a,pathnameBase:o,pattern:e}}function q(e,t=!1,n=!0){D(e===`*`||!e.endsWith(`*`)||e.endsWith(`/*`),`Route path "${e}" will be treated as if it were "${e.replace(/\*$/,`/*`)}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${e.replace(/\*$/,`/*`)}".`);let r=[],i=`^`+e.replace(/\/*\*?$/,``).replace(/^\/*/,`/`).replace(/[\\.*+^${}|()[\]]/g,`\\$&`).replace(/\/:([\w-]+)(\?)?/g,(e,t,n,i,a)=>{if(r.push({paramName:t,isOptional:n!=null}),n){let t=a.charAt(i+e.length);return t&&t!==`/`?`/([^\\/]*)`:`(?:/([^\\/]*))?`}return`/([^\\/]+)`}).replace(/\/([\w-]+)\?(\/|$)/g,`(/$1)?$2`);return e.endsWith(`*`)?(r.push({paramName:`*`}),i+=e===`*`||e===`/*`?`(.*)$`:`(?:\\/(.+)|\\/*)$`):n?i+=`\\/*$`:e!==``&&e!==`/`&&(i+=`(?:(?=\\/|$))`),[new RegExp(i,t?void 0:`i`),r]}function oe(e){try{return e.split(`/`).map(e=>decodeURIComponent(e).replace(/\//g,`%2F`)).join(`/`)}catch(t){return D(!1,`The URL path "${e}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${t}).`),e}}function se(e,t){if(t===`/`)return e;if(!e.toLowerCase().startsWith(t.toLowerCase()))return null;let n=t.endsWith(`/`)?t.length-1:t.length,r=e.charAt(n);return r&&r!==`/`?null:e.slice(n)||`/`}var ce=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;function le(e,t=`/`){let{pathname:n,search:r=``,hash:i=``}=typeof e==`string`?M(e):e,a;return n?(n=he(n),a=n.startsWith(`/`)?ue(n.substring(1),`/`):ue(n,t)):a=t,{pathname:a,search:ye(r),hash:be(i)}}function ue(e,t){let n=_e(t).split(`/`);return e.split(`/`).forEach(e=>{e===`..`?n.length>1&&n.pop():e!==`.`&&n.push(e)}),n.length>1?n.join(`/`):`/`}function de(e,t,n,r){return`Cannot include a '${e}' character in a manually specified \`to.${t}\` field [${JSON.stringify(r)}]. Please separate it out to the \`to.${n}\` field. Alternatively you may provide the full path as a string in and the router will parse it for you.`}function fe(e){return e.filter((e,t)=>t===0||e.route.path&&e.route.path.length>0)}function pe(e){let t=fe(e);return t.map((e,n)=>n===t.length-1?e.pathname:e.pathnameBase)}function me(e,t,n,r=!1){let i;typeof e==`string`?i=M(e):(i={...e},E(!i.pathname||!i.pathname.includes(`?`),de(`?`,`pathname`,`search`,i)),E(!i.pathname||!i.pathname.includes(`#`),de(`#`,`pathname`,`hash`,i)),E(!i.search||!i.search.includes(`#`),de(`#`,`search`,`hash`,i)));let a=e===``||i.pathname===``,o=a?`/`:i.pathname,s;if(o==null)s=n;else{let e=t.length-1;if(!r&&o.startsWith(`..`)){let t=o.split(`/`);for(;t[0]===`..`;)t.shift(),--e;i.pathname=t.join(`/`)}s=e>=0?t[e]:`/`}let c=le(i,s),l=o&&o!==`/`&&o.endsWith(`/`),u=(a||o===`.`)&&n.endsWith(`/`);return!c.pathname.endsWith(`/`)&&(l||u)&&(c.pathname+=`/`),c}var he=e=>e.replace(/\/\/+/g,`/`),ge=e=>he(e.join(`/`)),_e=e=>e.replace(/\/+$/,``),ve=e=>_e(e).replace(/^\/*/,`/`),ye=e=>!e||e===`?`?``:e.startsWith(`?`)?e:`?`+e,be=e=>!e||e===`#`?``:e.startsWith(`#`)?e:`#`+e,xe=class{constructor(e,t,n,r=!1){this.status=e,this.statusText=t||``,this.internal=r,n instanceof Error?(this.data=n.toString(),this.error=n):this.data=n}};function Se(e){return e!=null&&typeof e.status==`number`&&typeof e.statusText==`string`&&typeof e.internal==`boolean`&&`data`in e}function Ce(e){return ge(e.map(e=>e.route.path).filter(Boolean))||`/`}var we=typeof window<`u`&&window.document!==void 0&&window.document.createElement!==void 0;function Te(e,t){let n=e;if(typeof n!=`string`||!ce.test(n))return{absoluteURL:void 0,isExternal:!1,to:n};let r=n,i=!1;if(we)try{let e=new URL(window.location.href),r=n.startsWith(`//`)?new URL(e.protocol+n):new URL(n),a=se(r.pathname,t);r.origin===e.origin&&a!=null?n=a+r.search+r.hash:i=!0}catch{D(!1,` contains an invalid URL which will probably break when clicked - please update to a valid URL path.`)}return{absoluteURL:r,isExternal:i,to:n}}Object.getOwnPropertyNames(Object.prototype).sort().join(`\0`);var Ee=[`POST`,`PUT`,`PATCH`,`DELETE`];new Set(Ee);var De=[`GET`,...Ee];new Set(De);var Oe=S.createContext(null);Oe.displayName=`DataRouter`;var ke=S.createContext(null);ke.displayName=`DataRouterState`;var Ae=S.createContext(!1);function je(){return S.useContext(Ae)}var Me=S.createContext({isTransitioning:!1});Me.displayName=`ViewTransition`;var Ne=S.createContext(new Map);Ne.displayName=`Fetchers`;var Pe=S.createContext(null);Pe.displayName=`Await`;var Fe=S.createContext(null);Fe.displayName=`Navigation`;var Ie=S.createContext(null);Ie.displayName=`Location`;var Le=S.createContext({outlet:null,matches:[],isDataRoute:!1});Le.displayName=`Route`;var Re=S.createContext(null);Re.displayName=`RouteError`;var ze=`REACT_ROUTER_ERROR`,Be=`REDIRECT`,Ve=`ROUTE_ERROR_RESPONSE`;function He(e){if(e.startsWith(`${ze}:${Be}:{`))try{let t=JSON.parse(e.slice(28));if(typeof t==`object`&&t&&typeof t.status==`number`&&typeof t.statusText==`string`&&typeof t.location==`string`&&typeof t.reloadDocument==`boolean`&&typeof t.replace==`boolean`)return t}catch{}}function Ue(e){if(e.startsWith(`${ze}:${Ve}:{`))try{let t=JSON.parse(e.slice(40));if(typeof t==`object`&&t&&typeof t.status==`number`&&typeof t.statusText==`string`)return new xe(t.status,t.statusText,t.data)}catch{}}function We(e,{relative:t}={}){E(Ge(),`useHref() may be used only in the context of a component.`);let{basename:n,navigator:r}=S.useContext(Fe),{hash:i,pathname:a,search:o}=et(e,{relative:t}),s=a;return n!==`/`&&(s=a===`/`?n:ge([n,a])),r.createHref({pathname:s,search:o,hash:i})}function Ge(){return S.useContext(Ie)!=null}function Ke(){return E(Ge(),`useLocation() may be used only in the context of a component.`),S.useContext(Ie).location}var qe=`You should call navigate() in a React.useEffect(), not when your component is first rendered.`;function Je(e){S.useContext(Fe).static||S.useLayoutEffect(e)}function Ye(){let{isDataRoute:e}=S.useContext(Le);return e?yt():Xe()}function Xe(){E(Ge(),`useNavigate() may be used only in the context of a component.`);let e=S.useContext(Oe),{basename:t,navigator:n}=S.useContext(Fe),{matches:r}=S.useContext(Le),{pathname:i}=Ke(),a=JSON.stringify(pe(r)),o=S.useRef(!1);return Je(()=>{o.current=!0}),S.useCallback((r,s={})=>{if(D(o.current,qe),!o.current)return;if(typeof r==`number`){n.go(r);return}let c=me(r,JSON.parse(a),i,s.relative===`path`);e==null&&t!==`/`&&(c.pathname=c.pathname===`/`?t:ge([t,c.pathname])),(s.replace?n.replace:n.push)(c,s.state,s)},[t,n,a,i,e])}var Ze=S.createContext(null);function Qe(e){let t=S.useContext(Le).outlet;return S.useMemo(()=>t&&S.createElement(Ze.Provider,{value:e},t),[t,e])}function $e(){let{matches:e}=S.useContext(Le);return e[e.length-1]?.params??{}}function et(e,{relative:t}={}){let{matches:n}=S.useContext(Le),{pathname:r}=Ke(),i=JSON.stringify(pe(n));return S.useMemo(()=>me(e,JSON.parse(i),r,t===`path`),[e,i,r,t])}function tt(e,t){return nt(e,t)}function nt(e,t,n){E(Ge(),`useRoutes() may be used only in the context of a component.`);let{navigator:r}=S.useContext(Fe),{matches:i}=S.useContext(Le),a=i[i.length-1],o=a?a.params:{},s=a?a.pathname:`/`,c=a?a.pathnameBase:`/`,l=a&&a.route;{let e=l&&l.path||``;xt(s,!l||e.endsWith(`*`)||e.endsWith(`*?`),`You rendered descendant (or called \`useRoutes()\`) at "${s}" (under ) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render. +`+e.stack}}var de=Object.prototype.hasOwnProperty,fe=t.unstable_scheduleCallback,pe=t.unstable_cancelCallback,me=t.unstable_shouldYield,he=t.unstable_requestPaint,ge=t.unstable_now,_e=t.unstable_getCurrentPriorityLevel,ve=t.unstable_ImmediatePriority,ye=t.unstable_UserBlockingPriority,be=t.unstable_NormalPriority,xe=t.unstable_LowPriority,Se=t.unstable_IdlePriority,Ce=t.log,we=t.unstable_setDisableYieldValue,Te=null,Ee=null;function De(e){if(typeof Ce==`function`&&we(e),Ee&&typeof Ee.setStrictMode==`function`)try{Ee.setStrictMode(Te,e)}catch{}}var Oe=Math.clz32?Math.clz32:je,ke=Math.log,Ae=Math.LN2;function je(e){return e>>>=0,e===0?32:31-(ke(e)/Ae|0)|0}var Me=256,Ne=262144,Pe=4194304;function Fe(e){var t=e&42;if(t!==0)return t;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return e&261888;case 262144:case 524288:case 1048576:case 2097152:return e&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function Ie(e,t,n){var r=e.pendingLanes;if(r===0)return 0;var i=0,a=e.suspendedLanes,o=e.pingedLanes;e=e.warmLanes;var s=r&134217727;return s===0?(s=r&~a,s===0?o===0?n||(n=r&~e,n!==0&&(i=Fe(n))):i=Fe(o):i=Fe(s)):(r=s&~a,r===0?(o&=s,o===0?n||(n=s&~e,n!==0&&(i=Fe(n))):i=Fe(o)):i=Fe(r)),i===0?0:t!==0&&t!==i&&(t&a)===0&&(a=i&-i,n=t&-t,a>=n||a===32&&n&4194048)?t:i}function Le(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function Re(e,t){switch(e){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function ze(){var e=Pe;return Pe<<=1,!(Pe&62914560)&&(Pe=4194304),e}function Be(e){for(var t=[],n=0;31>n;n++)t.push(e);return t}function Ve(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function He(e,t,n,r,i,a){var o=e.pendingLanes;e.pendingLanes=n,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=n,e.entangledLanes&=n,e.errorRecoveryDisabledLanes&=n,e.shellSuspendCounter=0;var s=e.entanglements,c=e.expirationTimes,l=e.hiddenUpdates;for(n=o&~n;0`u`||window.document===void 0||window.document.createElement===void 0),en=!1;if($t)try{var tn={};Object.defineProperty(tn,`passive`,{get:function(){en=!0}}),window.addEventListener(`test`,tn,tn),window.removeEventListener(`test`,tn,tn)}catch{en=!1}var nn=null,rn=null,an=null;function on(){if(an)return an;var e,t=rn,n=t.length,r,i=`value`in nn?nn.value:nn.textContent,a=i.length;for(e=0;e=Rn),Vn=` `,Hn=!1;function Un(e,t){switch(e){case`keyup`:return In.indexOf(t.keyCode)!==-1;case`keydown`:return t.keyCode!==229;case`keypress`:case`mousedown`:case`focusout`:return!0;default:return!1}}function Wn(e){return e=e.detail,typeof e==`object`&&`data`in e?e.data:null}var Gn=!1;function Kn(e,t){switch(e){case`compositionend`:return Wn(t);case`keypress`:return t.which===32?(Hn=!0,Vn):null;case`textInput`:return e=t.data,e===Vn&&Hn?null:e;default:return null}}function qn(e,t){if(Gn)return e===`compositionend`||!Ln&&Un(e,t)?(e=on(),an=rn=nn=null,Gn=!1,e):null;switch(e){case`paste`:return null;case`keypress`:if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}a:{for(;n;){if(n.nextSibling){n=n.nextSibling;break a}n=n.parentNode}n=void 0}n=hr(n)}}function _r(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?_r(e,t.parentNode):`contains`in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function vr(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=Dt(e.document);t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href==`string`}catch{n=!1}if(n)e=t.contentWindow;else break;t=Dt(e.document)}return t}function yr(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t===`input`&&(e.type===`text`||e.type===`search`||e.type===`tel`||e.type===`url`||e.type===`password`)||t===`textarea`||e.contentEditable===`true`)}var br=$t&&`documentMode`in document&&11>=document.documentMode,xr=null,Sr=null,Cr=null,wr=!1;function Tr(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;wr||xr==null||xr!==Dt(r)||(r=xr,`selectionStart`in r&&yr(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),Cr&&mr(Cr,r)||(Cr=r,r=kd(Sr,`onSelect`),0>=o,i-=o,_i=1<<32-Oe(t)+i|n<h?(g=d,d=null):g=d.sibling;var _=p(i,d,s[h],c);if(_===null){d===null&&(d=g);break}e&&d&&_.alternate===null&&t(i,d),a=o(_,a,h),u===null?l=_:u.sibling=_,u=_,d=g}if(h===s.length)return n(i,d),Ei&&yi(i,h),l;if(d===null){for(;hg?(_=h,h=null):_=h.sibling;var y=p(a,h,v.value,l);if(y===null){h===null&&(h=_);break}e&&h&&y.alternate===null&&t(a,h),s=o(y,s,g),d===null?u=y:d.sibling=y,d=y,h=_}if(v.done)return n(a,h),Ei&&yi(a,g),u;if(h===null){for(;!v.done;g++,v=c.next())v=f(a,v.value,l),v!==null&&(s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return Ei&&yi(a,g),u}for(h=r(h);!v.done;g++,v=c.next())v=m(h,a,g,v.value,l),v!==null&&(e&&v.alternate!==null&&h.delete(v.key===null?g:v.key),s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return e&&h.forEach(function(e){return t(a,e)}),Ei&&yi(a,g),u}function b(e,r,o,c){if(typeof o==`object`&&o&&o.type===y&&o.key===null&&(o=o.props.children),typeof o==`object`&&o){switch(o.$$typeof){case _:a:{for(var l=o.key;r!==null;){if(r.key===l){if(l=o.type,l===y){if(r.tag===7){n(e,r.sibling),c=a(r,o.props.children),c.return=e,e=c;break a}}else if(r.elementType===l||typeof l==`object`&&l&&l.$$typeof===O&&ya(l)===r.type){n(e,r.sibling),c=a(r,o.props),Ea(c,o),c.return=e,e=c;break a}n(e,r);break}else t(e,r);r=r.sibling}o.type===y?(c=ii(o.props.children,e.mode,c,o.key),c.return=e,e=c):(c=ri(o.type,o.key,o.props,null,e.mode,c),Ea(c,o),c.return=e,e=c)}return s(e);case v:a:{for(l=o.key;r!==null;){if(r.key===l)if(r.tag===4&&r.stateNode.containerInfo===o.containerInfo&&r.stateNode.implementation===o.implementation){n(e,r.sibling),c=a(r,o.children||[]),c.return=e,e=c;break a}else{n(e,r);break}else t(e,r);r=r.sibling}c=si(o,e.mode,c),c.return=e,e=c}return s(e);case O:return o=ya(o),b(e,r,o,c)}if(F(o))return h(e,r,o,c);if(M(o)){if(l=M(o),typeof l!=`function`)throw Error(i(150));return o=l.call(o),g(e,r,o,c)}if(typeof o.then==`function`)return b(e,r,Ta(o),c);if(o.$$typeof===C)return b(e,r,Ji(e,o),c);Da(e,o)}return typeof o==`string`&&o!==``||typeof o==`number`||typeof o==`bigint`?(o=``+o,r!==null&&r.tag===6?(n(e,r.sibling),c=a(r,o),c.return=e,e=c):(n(e,r),c=ai(o,e.mode,c),c.return=e,e=c),s(e)):n(e,r)}return function(e,t,n,r){try{wa=0;var i=b(e,t,n,r);return Ca=null,i}catch(t){if(t===pa||t===ha)throw t;var a=$r(29,t,null,e.mode);return a.lanes=r,a.return=e,a}}}var ka=Oa(!0),Aa=Oa(!1),ja=!1;function Ma(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function Na(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,callbacks:null})}function Pa(e){return{lane:e,tag:0,payload:null,callback:null,next:null}}function Fa(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,Fl&2){var i=r.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),r.pending=t,t=Xr(e),Yr(e,null,n),t}return Kr(e,r,t,n),Xr(e)}function Ia(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,n&4194048)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,We(e,n)}}function La(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var i=null,a=null;if(n=n.firstBaseUpdate,n!==null){do{var o={lane:n.lane,tag:n.tag,payload:n.payload,callback:null,next:null};a===null?i=a=o:a=a.next=o,n=n.next}while(n!==null);a===null?i=a=t:a=a.next=t}else i=a=t;n={baseState:r.baseState,firstBaseUpdate:i,lastBaseUpdate:a,shared:r.shared,callbacks:r.callbacks},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}var Ra=!1;function za(){if(Ra){var e=aa;if(e!==null)throw e}}function Ba(e,t,n,r){Ra=!1;var i=e.updateQueue;ja=!1;var a=i.firstBaseUpdate,o=i.lastBaseUpdate,s=i.shared.pending;if(s!==null){i.shared.pending=null;var c=s,l=c.next;c.next=null,o===null?a=l:o.next=l,o=c;var u=e.alternate;u!==null&&(u=u.updateQueue,s=u.lastBaseUpdate,s!==o&&(s===null?u.firstBaseUpdate=l:s.next=l,u.lastBaseUpdate=c))}if(a!==null){var d=i.baseState;o=0,u=l=c=null,s=a;do{var f=s.lane&-536870913,p=f!==s.lane;if(p?(Rl&f)===f:(r&f)===f){f!==0&&f===ia&&(Ra=!0),u!==null&&(u=u.next={lane:0,tag:s.tag,payload:s.payload,callback:null,next:null});a:{var h=e,g=s;f=t;var _=n;switch(g.tag){case 1:if(h=g.payload,typeof h==`function`){d=h.call(_,d,f);break a}d=h;break a;case 3:h.flags=h.flags&-65537|128;case 0:if(h=g.payload,f=typeof h==`function`?h.call(_,d,f):h,f==null)break a;d=m({},d,f);break a;case 2:ja=!0}}f=s.callback,f!==null&&(e.flags|=64,p&&(e.flags|=8192),p=i.callbacks,p===null?i.callbacks=[f]:p.push(f))}else p={lane:f,tag:s.tag,payload:s.payload,callback:s.callback,next:null},u===null?(l=u=p,c=d):u=u.next=p,o|=f;if(s=s.next,s===null){if(s=i.shared.pending,s===null)break;p=s,s=p.next,p.next=null,i.lastBaseUpdate=p,i.shared.pending=null}}while(1);u===null&&(c=d),i.baseState=c,i.firstBaseUpdate=l,i.lastBaseUpdate=u,a===null&&(i.shared.lanes=0),Kl|=o,e.lanes=o,e.memoizedState=d}}function Va(e,t){if(typeof e!=`function`)throw Error(i(191,e));e.call(t)}function Ha(e,t){var n=e.callbacks;if(n!==null)for(e.callbacks=null,e=0;ea?a:8;var o=I.T,s={};I.T=s,Os(e,!1,t,n);try{var c=i(),l=I.S;l!==null&&l(s,c),typeof c==`object`&&c&&typeof c.then==`function`?Ds(e,t,J(c,r),mu(e)):Ds(e,t,r,mu(e))}catch(n){Ds(e,t,{then:function(){},status:`rejected`,reason:n},mu())}finally{L.p=a,o!==null&&s.types!==null&&(o.types=s.types),I.T=o}}function _s(){}function vs(e,t,n,r){if(e.tag!==5)throw Error(i(476));var a=ys(e).queue;gs(e,a,t,R,n===null?_s:function(){return bs(e),n(r)})}function ys(e){var t=e.memoizedState;if(t!==null)return t;t={memoizedState:R,baseState:R,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:ko,lastRenderedState:R},next:null};var n={};return t.next={memoizedState:n,baseState:n,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:ko,lastRenderedState:n},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function bs(e){var t=ys(e);t.next===null&&(t=e.alternate.memoizedState),Ds(e,t.next.queue,{},mu())}function xs(){return qi(np)}function Ss(){return wo().memoizedState}function Cs(){return wo().memoizedState}function ws(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var n=mu();e=Pa(n);var r=Fa(t,e,n);r!==null&&(gu(r,t,n),Ia(r,t,n)),t={cache:ea()},e.payload=t;return}t=t.return}}function Ts(e,t,n){var r=mu();n={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null},ks(e)?As(t,n):(n=qr(e,t,n,r),n!==null&&(gu(n,e,r),js(n,t,r)))}function Es(e,t,n){Ds(e,t,n,mu())}function Ds(e,t,n,r){var i={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null};if(ks(e))As(t,i);else{var a=e.alternate;if(e.lanes===0&&(a===null||a.lanes===0)&&(a=t.lastRenderedReducer,a!==null))try{var o=t.lastRenderedState,s=a(o,n);if(i.hasEagerState=!0,i.eagerState=s,pr(s,o))return Kr(e,t,i,0),Il===null&&Gr(),!1}catch{}if(n=qr(e,t,i,r),n!==null)return gu(n,e,r),js(n,t,r),!0}return!1}function Os(e,t,n,r){if(r={lane:2,revertLane:pd(),gesture:null,action:r,hasEagerState:!1,eagerState:null,next:null},ks(e)){if(t)throw Error(i(479))}else t=qr(e,n,r,2),t!==null&&gu(t,e,2)}function ks(e){var t=e.alternate;return e===ro||t!==null&&t===ro}function As(e,t){so=oo=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function js(e,t,n){if(n&4194048){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,We(e,n)}}var Ms={readContext:qi,use:Do,useCallback:mo,useContext:mo,useEffect:mo,useImperativeHandle:mo,useLayoutEffect:mo,useInsertionEffect:mo,useMemo:mo,useReducer:mo,useRef:mo,useState:mo,useDebugValue:mo,useDeferredValue:mo,useTransition:mo,useSyncExternalStore:mo,useId:mo,useHostTransitionStatus:mo,useFormState:mo,useActionState:mo,useOptimistic:mo,useMemoCache:mo,useCacheRefresh:mo};Ms.useEffectEvent=mo;var Ns={readContext:qi,use:Do,useCallback:function(e,t){return Co().memoizedState=[e,t===void 0?null:t],e},useContext:qi,useEffect:rs,useImperativeHandle:function(e,t,n){n=n==null?null:n.concat([e]),ts(4194308,4,ls.bind(null,t,e),n)},useLayoutEffect:function(e,t){return ts(4194308,4,e,t)},useInsertionEffect:function(e,t){ts(4,2,e,t)},useMemo:function(e,t){var n=Co();t=t===void 0?null:t;var r=e();if(co){De(!0);try{e()}finally{De(!1)}}return n.memoizedState=[r,t],r},useReducer:function(e,t,n){var r=Co();if(n!==void 0){var i=n(t);if(co){De(!0);try{n(t)}finally{De(!1)}}}else i=t;return r.memoizedState=r.baseState=i,e={pending:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:i},r.queue=e,e=e.dispatch=Ts.bind(null,ro,e),[r.memoizedState,e]},useRef:function(e){var t=Co();return e={current:e},t.memoizedState=e},useState:function(e){e=zo(e);var t=e.queue,n=Es.bind(null,ro,t);return t.dispatch=n,[e.memoizedState,n]},useDebugValue:ds,useDeferredValue:function(e,t){return ms(Co(),e,t)},useTransition:function(){var e=zo(!1);return e=gs.bind(null,ro,e.queue,!0,!1),Co().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,n){var r=ro,a=Co();if(Ei){if(n===void 0)throw Error(i(407));n=n()}else{if(n=t(),Il===null)throw Error(i(349));Rl&127||Po(r,t,n)}a.memoizedState=n;var o={value:n,getSnapshot:t};return a.queue=o,rs(Io.bind(null,r,o,e),[e]),r.flags|=2048,$o(9,{destroy:void 0},Fo.bind(null,r,o,n,t),null),n},useId:function(){var e=Co(),t=Il.identifierPrefix;if(Ei){var n=vi,r=_i;n=(r&~(1<<32-Oe(r)-1)).toString(32)+n,t=`_`+t+`R_`+n,n=lo++,0<\/script>`,o=o.removeChild(o.firstChild);break;case`select`:o=typeof r.is==`string`?s.createElement(`select`,{is:r.is}):s.createElement(`select`),r.multiple?o.multiple=!0:r.size&&(o.size=r.size);break;default:o=typeof r.is==`string`?s.createElement(a,{is:r.is}):s.createElement(a)}}o[Ze]=t,o[Qe]=r;a:for(s=t.child;s!==null;){if(s.tag===5||s.tag===6)o.appendChild(s.stateNode);else if(s.tag!==4&&s.tag!==27&&s.child!==null){s.child.return=s,s=s.child;continue}if(s===t)break a;for(;s.sibling===null;){if(s.return===null||s.return===t)break a;s=s.return}s.sibling.return=s.return,s=s.sibling}t.stateNode=o;a:switch(Rd(o,a,r),a){case`button`:case`input`:case`select`:case`textarea`:r=!!r.autoFocus;break a;case`img`:r=!0;break a;default:r=!1}r&&Dc(t)}}return Mc(t),Oc(t,t.type,e===null?null:e.memoizedProps,t.pendingProps,n),null;case 6:if(e&&t.stateNode!=null)e.memoizedProps!==r&&Dc(t);else{if(typeof r!=`string`&&t.stateNode===null)throw Error(i(166));if(e=te.current,Ni(t)){if(e=t.stateNode,n=t.memoizedProps,r=null,a=wi,a!==null)switch(a.tag){case 27:case 5:r=a.memoizedProps}e[Ze]=t,e=!!(e.nodeValue===n||r!==null&&!0===r.suppressHydrationWarning||Fd(e.nodeValue,n)),e||Ai(t,!0)}else e=Wd(e).createTextNode(r),e[Ze]=t,t.stateNode=e}return Mc(t),null;case 31:if(n=t.memoizedState,e===null||e.memoizedState!==null){if(r=Ni(t),n!==null){if(e===null){if(!r)throw Error(i(318));if(e=t.memoizedState,e=e===null?null:e.dehydrated,!e)throw Error(i(557));e[Ze]=t}else Pi(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;Mc(t),e=!1}else n=Fi(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=n),e=!0;if(!e)return t.flags&256?($a(t),t):($a(t),null);if(t.flags&128)throw Error(i(558))}return Mc(t),null;case 13:if(r=t.memoizedState,e===null||e.memoizedState!==null&&e.memoizedState.dehydrated!==null){if(a=Ni(t),r!==null&&r.dehydrated!==null){if(e===null){if(!a)throw Error(i(318));if(a=t.memoizedState,a=a===null?null:a.dehydrated,!a)throw Error(i(317));a[Ze]=t}else Pi(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;Mc(t),a=!1}else a=Fi(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=a),a=!0;if(!a)return t.flags&256?($a(t),t):($a(t),null)}return $a(t),t.flags&128?(t.lanes=n,t):(n=r!==null,e=e!==null&&e.memoizedState!==null,n&&(r=t.child,a=null,r.alternate!==null&&r.alternate.memoizedState!==null&&r.alternate.memoizedState.cachePool!==null&&(a=r.alternate.memoizedState.cachePool.pool),o=null,r.memoizedState!==null&&r.memoizedState.cachePool!==null&&(o=r.memoizedState.cachePool.pool),o!==a&&(r.flags|=2048)),n!==e&&n&&(t.child.flags|=8192),Ac(t,t.updateQueue),Mc(t),null);case 4:return K(),e===null&&Td(t.stateNode.containerInfo),Mc(t),null;case 10:return Vi(t.type),Mc(t),null;case 19:if(H(eo),r=t.memoizedState,r===null)return Mc(t),null;if(a=(t.flags&128)!=0,o=r.rendering,o===null)if(a)jc(r,!1);else{if(Gl!==0||e!==null&&e.flags&128)for(e=t.child;e!==null;){if(o=to(e),o!==null){for(t.flags|=128,jc(r,!1),e=o.updateQueue,t.updateQueue=e,Ac(t,e),t.subtreeFlags=0,e=n,n=t.child;n!==null;)ni(n,e),n=n.sibling;return U(eo,eo.current&1|2),Ei&&yi(t,r.treeForkCount),t.child}e=e.sibling}r.tail!==null&&ge()>nu&&(t.flags|=128,a=!0,jc(r,!1),t.lanes=4194304)}else{if(!a)if(e=to(o),e!==null){if(t.flags|=128,a=!0,e=e.updateQueue,t.updateQueue=e,Ac(t,e),jc(r,!0),r.tail===null&&r.tailMode===`hidden`&&!o.alternate&&!Ei)return Mc(t),null}else 2*ge()-r.renderingStartTime>nu&&n!==536870912&&(t.flags|=128,a=!0,jc(r,!1),t.lanes=4194304);r.isBackwards?(o.sibling=t.child,t.child=o):(e=r.last,e===null?t.child=o:e.sibling=o,r.last=o)}return r.tail===null?(Mc(t),null):(e=r.tail,r.rendering=e,r.tail=e.sibling,r.renderingStartTime=ge(),e.sibling=null,n=eo.current,U(eo,a?n&1|2:n&1),Ei&&yi(t,r.treeForkCount),e);case 22:case 23:return $a(t),Ka(),r=t.memoizedState!==null,e===null?r&&(t.flags|=8192):e.memoizedState!==null!==r&&(t.flags|=8192),r?n&536870912&&!(t.flags&128)&&(Mc(t),t.subtreeFlags&6&&(t.flags|=8192)):Mc(t),n=t.updateQueue,n!==null&&Ac(t,n.retryQueue),n=null,e!==null&&e.memoizedState!==null&&e.memoizedState.cachePool!==null&&(n=e.memoizedState.cachePool.pool),r=null,t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(r=t.memoizedState.cachePool.pool),r!==n&&(t.flags|=2048),e!==null&&H(la),null;case 24:return n=null,e!==null&&(n=e.memoizedState.cache),t.memoizedState.cache!==n&&(t.flags|=2048),Vi($i),Mc(t),null;case 25:return null;case 30:return null}throw Error(i(156,t.tag))}function Pc(e,t){switch(Si(t),t.tag){case 1:return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Vi($i),K(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 26:case 27:case 5:return ie(t),null;case 31:if(t.memoizedState!==null){if($a(t),t.alternate===null)throw Error(i(340));Pi()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 13:if($a(t),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(i(340));Pi()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return H(eo),null;case 4:return K(),null;case 10:return Vi(t.type),null;case 22:case 23:return $a(t),Ka(),e!==null&&H(la),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 24:return Vi($i),null;case 25:return null;default:return null}}function Fc(e,t){switch(Si(t),t.tag){case 3:Vi($i),K();break;case 26:case 27:case 5:ie(t);break;case 4:K();break;case 31:t.memoizedState!==null&&$a(t);break;case 13:$a(t);break;case 19:H(eo);break;case 10:Vi(t.type);break;case 22:case 23:$a(t),Ka(),e!==null&&H(la);break;case 24:Vi($i)}}function Ic(e,t){try{var n=t.updateQueue,r=n===null?null:n.lastEffect;if(r!==null){var i=r.next;n=i;do{if((n.tag&e)===e){r=void 0;var a=n.create,o=n.inst;r=a(),o.destroy=r}n=n.next}while(n!==i)}}catch(e){Ku(t,t.return,e)}}function Lc(e,t,n){try{var r=t.updateQueue,i=r===null?null:r.lastEffect;if(i!==null){var a=i.next;r=a;do{if((r.tag&e)===e){var o=r.inst,s=o.destroy;if(s!==void 0){o.destroy=void 0,i=t;var c=n,l=s;try{l()}catch(e){Ku(i,c,e)}}}r=r.next}while(r!==a)}}catch(e){Ku(t,t.return,e)}}function Rc(e){var t=e.updateQueue;if(t!==null){var n=e.stateNode;try{Ha(t,n)}catch(t){Ku(e,e.return,t)}}}function zc(e,t,n){n.props=Bs(e.type,e.memoizedProps),n.state=e.memoizedState;try{n.componentWillUnmount()}catch(n){Ku(e,t,n)}}function Bc(e,t){try{var n=e.ref;if(n!==null){switch(e.tag){case 26:case 27:case 5:var r=e.stateNode;break;case 30:r=e.stateNode;break;default:r=e.stateNode}typeof n==`function`?e.refCleanup=n(r):n.current=r}}catch(n){Ku(e,t,n)}}function Vc(e,t){var n=e.ref,r=e.refCleanup;if(n!==null)if(typeof r==`function`)try{r()}catch(n){Ku(e,t,n)}finally{e.refCleanup=null,e=e.alternate,e!=null&&(e.refCleanup=null)}else if(typeof n==`function`)try{n(null)}catch(n){Ku(e,t,n)}else n.current=null}function Hc(e){var t=e.type,n=e.memoizedProps,r=e.stateNode;try{a:switch(t){case`button`:case`input`:case`select`:case`textarea`:n.autoFocus&&r.focus();break a;case`img`:n.src?r.src=n.src:n.srcSet&&(r.srcset=n.srcSet)}}catch(t){Ku(e,e.return,t)}}function Uc(e,t,n){try{var r=e.stateNode;zd(r,e.type,n,t),r[Qe]=t}catch(t){Ku(e,e.return,t)}}function Wc(e){return e.tag===5||e.tag===3||e.tag===26||e.tag===27&&tf(e.type)||e.tag===4}function Gc(e){a:for(;;){for(;e.sibling===null;){if(e.return===null||Wc(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.tag===27&&tf(e.type)||e.flags&2||e.child===null||e.tag===4)continue a;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Kc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?(n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n).insertBefore(e,t):(t=n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n,t.appendChild(e),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=Wt));else if(r!==4&&(r===27&&tf(e.type)&&(n=e.stateNode,t=null),e=e.child,e!==null))for(Kc(e,t,n),e=e.sibling;e!==null;)Kc(e,t,n),e=e.sibling}function qc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(r===27&&tf(e.type)&&(n=e.stateNode),e=e.child,e!==null))for(qc(e,t,n),e=e.sibling;e!==null;)qc(e,t,n),e=e.sibling}function Jc(e){var t=e.stateNode,n=e.memoizedProps;try{for(var r=e.type,i=t.attributes;i.length;)t.removeAttributeNode(i[0]);Rd(t,r,n),t[Ze]=e,t[Qe]=n}catch(t){Ku(e,e.return,t)}}var Yc=!1,Xc=!1,Zc=!1,Qc=typeof WeakSet==`function`?WeakSet:Set,$c=null;function el(e,t){if(e=e.containerInfo,Hd=dp,e=vr(e),yr(e)){if(`selectionStart`in e)var n={start:e.selectionStart,end:e.selectionEnd};else a:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var a=r.anchorOffset,o=r.focusNode;r=r.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break a}var s=0,c=-1,l=-1,u=0,d=0,f=e,p=null;b:for(;;){for(var m;f!==n||a!==0&&f.nodeType!==3||(c=s+a),f!==o||r!==0&&f.nodeType!==3||(l=s+r),f.nodeType===3&&(s+=f.nodeValue.length),(m=f.firstChild)!==null;)p=f,f=m;for(;;){if(f===e)break b;if(p===n&&++u===a&&(c=s),p===o&&++d===r&&(l=s),(m=f.nextSibling)!==null)break;f=p,p=f.parentNode}f=m}n=c===-1||l===-1?null:{start:c,end:l}}else n=null}n||={start:0,end:0}}else n=null;for(Ud={focusedElem:e,selectionRange:n},dp=!1,$c=t;$c!==null;)if(t=$c,e=t.child,t.subtreeFlags&1028&&e!==null)e.return=t,$c=e;else for(;$c!==null;){switch(t=$c,o=t.alternate,e=t.flags,t.tag){case 0:if(e&4&&(e=t.updateQueue,e=e===null?null:e.events,e!==null))for(n=0;n title`))),Rd(o,r,n),o[Ze]=e,ut(o),r=o;break a;case`link`:var s=Gf(`link`,`href`,a).get(r+(n.href||``));if(s){for(var c=0;cg&&(o=g,g=h,h=o);var _=gr(s,h),v=gr(s,g);if(_&&v&&(p.rangeCount!==1||p.anchorNode!==_.node||p.anchorOffset!==_.offset||p.focusNode!==v.node||p.focusOffset!==v.offset)){var y=d.createRange();y.setStart(_.node,_.offset),p.removeAllRanges(),h>g?(p.addRange(y),p.extend(v.node,v.offset)):(y.setEnd(v.node,v.offset),p.addRange(y))}}}}for(d=[],p=s;p=p.parentNode;)p.nodeType===1&&d.push({element:p,left:p.scrollLeft,top:p.scrollTop});for(typeof s.focus==`function`&&s.focus(),s=0;sn?32:n,I.T=null,n=uu,uu=null;var o=ou,s=cu;if(au=0,su=ou=null,cu=0,Fl&6)throw Error(i(331));var c=Fl;if(Fl|=4,Al(o.current),Sl(o,o.current,s,n),Fl=c,od(0,!1),Ee&&typeof Ee.onPostCommitFiberRoot==`function`)try{Ee.onPostCommitFiberRoot(Te,o)}catch{}return!0}finally{L.p=a,I.T=r,Hu(e,t)}}function Gu(e,t,n){t=li(n,t),t=Ks(e.stateNode,t,2),e=Fa(e,t,2),e!==null&&(Ve(e,2),ad(e))}function Ku(e,t,n){if(e.tag===3)Gu(e,e,n);else for(;t!==null;){if(t.tag===3){Gu(t,e,n);break}else if(t.tag===1){var r=t.stateNode;if(typeof t.type.getDerivedStateFromError==`function`||typeof r.componentDidCatch==`function`&&(iu===null||!iu.has(r))){e=li(n,e),n=qs(2),r=Fa(t,n,2),r!==null&&(Js(n,r,t,e),Ve(r,2),ad(r));break}}t=t.return}}function qu(e,t,n){var r=e.pingCache;if(r===null){r=e.pingCache=new Pl;var i=new Set;r.set(t,i)}else i=r.get(t),i===void 0&&(i=new Set,r.set(t,i));i.has(n)||(Ul=!0,i.add(n),e=Ju.bind(null,e,t,n),t.then(e,e))}function Ju(e,t,n){var r=e.pingCache;r!==null&&r.delete(t),e.pingedLanes|=e.suspendedLanes&n,e.warmLanes&=~n,Il===e&&(Rl&n)===n&&(Gl===4||Gl===3&&(Rl&62914560)===Rl&&300>ge()-eu?!(Fl&2)&&Cu(e,0):Jl|=n,Xl===Rl&&(Xl=0)),ad(e)}function Yu(e,t){t===0&&(t=ze()),e=Jr(e,t),e!==null&&(Ve(e,t),ad(e))}function Xu(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),Yu(e,n)}function Zu(e,t){var n=0;switch(e.tag){case 31:case 13:var r=e.stateNode,a=e.memoizedState;a!==null&&(n=a.retryLane);break;case 19:r=e.stateNode;break;case 22:r=e.stateNode._retryCache;break;default:throw Error(i(314))}r!==null&&r.delete(t),Yu(e,n)}function Qu(e,t){return fe(e,t)}var $u=null,ed=null,td=!1,nd=!1,rd=!1,id=0;function ad(e){e!==ed&&e.next===null&&(ed===null?$u=ed=e:ed=ed.next=e),nd=!0,td||(td=!0,fd())}function od(e,t){if(!rd&&nd){rd=!0;do for(var n=!1,r=$u;r!==null;){if(!t)if(e!==0){var i=r.pendingLanes;if(i===0)var a=0;else{var o=r.suspendedLanes,s=r.pingedLanes;a=(1<<31-Oe(42|e)+1)-1,a&=i&~(o&~s),a=a&201326741?a&201326741|1:a?a|2:0}a!==0&&(n=!0,dd(r,a))}else a=Rl,a=Ie(r,r===Il?a:0,r.cancelPendingCommit!==null||r.timeoutHandle!==-1),!(a&3)||Le(r,a)||(n=!0,dd(r,a));r=r.next}while(n);rd=!1}}function sd(){cd()}function cd(){nd=td=!1;var e=0;id!==0&&Yd()&&(e=id);for(var t=ge(),n=null,r=$u;r!==null;){var i=r.next,a=ld(r,t);a===0?(r.next=null,n===null?$u=i:n.next=i,i===null&&(ed=n)):(n=r,(e!==0||a&3)&&(nd=!0)),r=i}au!==0&&au!==5||od(e,!1),id!==0&&(id=0)}function ld(e,t){for(var n=e.suspendedLanes,r=e.pingedLanes,i=e.expirationTimes,a=e.pendingLanes&-62914561;0s)break;var u=c.transferSize,d=c.initiatorType;u&&Bd(d)&&(c=c.responseEnd,o+=u*(c`u`?null:document;function Tf(e,t,n){var r=wf;if(r&&typeof t==`string`&&t){var i=kt(t);i=`link[rel="`+e+`"][href="`+i+`"]`,typeof n==`string`&&(i+=`[crossorigin="`+n+`"]`),yf.has(i)||(yf.add(i),e={rel:e,crossOrigin:n,href:t},r.querySelector(i)===null&&(t=r.createElement(`link`),Rd(t,`link`,e),ut(t),r.head.appendChild(t)))}}function Ef(e){xf.D(e),Tf(`dns-prefetch`,e,null)}function Df(e,t){xf.C(e,t),Tf(`preconnect`,e,t)}function Of(e,t,n){xf.L(e,t,n);var r=wf;if(r&&e&&t){var i=`link[rel="preload"][as="`+kt(t)+`"]`;t===`image`&&n&&n.imageSrcSet?(i+=`[imagesrcset="`+kt(n.imageSrcSet)+`"]`,typeof n.imageSizes==`string`&&(i+=`[imagesizes="`+kt(n.imageSizes)+`"]`)):i+=`[href="`+kt(e)+`"]`;var a=i;switch(t){case`style`:a=Pf(e);break;case`script`:a=Rf(e)}vf.has(a)||(e=m({rel:`preload`,href:t===`image`&&n&&n.imageSrcSet?void 0:e,as:t},n),vf.set(a,e),r.querySelector(i)!==null||t===`style`&&r.querySelector(Ff(a))||t===`script`&&r.querySelector(zf(a))||(t=r.createElement(`link`),Rd(t,`link`,e),ut(t),r.head.appendChild(t)))}}function kf(e,t){xf.m(e,t);var n=wf;if(n&&e){var r=t&&typeof t.as==`string`?t.as:`script`,i=`link[rel="modulepreload"][as="`+kt(r)+`"][href="`+kt(e)+`"]`,a=i;switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:a=Rf(e)}if(!vf.has(a)&&(e=m({rel:`modulepreload`,href:e},t),vf.set(a,e),n.querySelector(i)===null)){switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:if(n.querySelector(zf(a)))return}r=n.createElement(`link`),Rd(r,`link`,e),ut(r),n.head.appendChild(r)}}}function Af(e,t,n){xf.S(e,t,n);var r=wf;if(r&&e){var i=lt(r).hoistableStyles,a=Pf(e);t||=`default`;var o=i.get(a);if(!o){var s={loading:0,preload:null};if(o=r.querySelector(Ff(a)))s.loading=5;else{e=m({rel:`stylesheet`,href:e,"data-precedence":t},n),(n=vf.get(a))&&Hf(e,n);var c=o=r.createElement(`link`);ut(c),Rd(c,`link`,e),c._p=new Promise(function(e,t){c.onload=e,c.onerror=t}),c.addEventListener(`load`,function(){s.loading|=1}),c.addEventListener(`error`,function(){s.loading|=2}),s.loading|=4,Vf(o,t,r)}o={type:`stylesheet`,instance:o,count:1,state:s},i.set(a,o)}}}function jf(e,t){xf.X(e,t);var n=wf;if(n&&e){var r=lt(n).hoistableScripts,i=Rf(e),a=r.get(i);a||(a=n.querySelector(zf(i)),a||(e=m({src:e,async:!0},t),(t=vf.get(i))&&Uf(e,t),a=n.createElement(`script`),ut(a),Rd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function Mf(e,t){xf.M(e,t);var n=wf;if(n&&e){var r=lt(n).hoistableScripts,i=Rf(e),a=r.get(i);a||(a=n.querySelector(zf(i)),a||(e=m({src:e,async:!0,type:`module`},t),(t=vf.get(i))&&Uf(e,t),a=n.createElement(`script`),ut(a),Rd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function Nf(e,t,n,r){var a=(a=te.current)?bf(a):null;if(!a)throw Error(i(446));switch(e){case`meta`:case`title`:return null;case`style`:return typeof n.precedence==`string`&&typeof n.href==`string`?(t=Pf(n.href),n=lt(a).hoistableStyles,r=n.get(t),r||(r={type:`style`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};case`link`:if(n.rel===`stylesheet`&&typeof n.href==`string`&&typeof n.precedence==`string`){e=Pf(n.href);var o=lt(a).hoistableStyles,s=o.get(e);if(s||(a=a.ownerDocument||a,s={type:`stylesheet`,instance:null,count:0,state:{loading:0,preload:null}},o.set(e,s),(o=a.querySelector(Ff(e)))&&!o._p&&(s.instance=o,s.state.loading=5),vf.has(e)||(n={rel:`preload`,as:`style`,href:n.href,crossOrigin:n.crossOrigin,integrity:n.integrity,media:n.media,hrefLang:n.hrefLang,referrerPolicy:n.referrerPolicy},vf.set(e,n),o||Lf(a,e,n,s.state))),t&&r===null)throw Error(i(528,``));return s}if(t&&r!==null)throw Error(i(529,``));return null;case`script`:return t=n.async,n=n.src,typeof n==`string`&&t&&typeof t!=`function`&&typeof t!=`symbol`?(t=Rf(n),n=lt(a).hoistableScripts,r=n.get(t),r||(r={type:`script`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};default:throw Error(i(444,e))}}function Pf(e){return`href="`+kt(e)+`"`}function Ff(e){return`link[rel="stylesheet"][`+e+`]`}function If(e){return m({},e,{"data-precedence":e.precedence,precedence:null})}function Lf(e,t,n,r){e.querySelector(`link[rel="preload"][as="style"][`+t+`]`)?r.loading=1:(t=e.createElement(`link`),r.preload=t,t.addEventListener(`load`,function(){return r.loading|=1}),t.addEventListener(`error`,function(){return r.loading|=2}),Rd(t,`link`,n),ut(t),e.head.appendChild(t))}function Rf(e){return`[src="`+kt(e)+`"]`}function zf(e){return`script[async]`+e}function Bf(e,t,n){if(t.count++,t.instance===null)switch(t.type){case`style`:var r=e.querySelector(`style[data-href~="`+kt(n.href)+`"]`);if(r)return t.instance=r,ut(r),r;var a=m({},n,{"data-href":n.href,"data-precedence":n.precedence,href:null,precedence:null});return r=(e.ownerDocument||e).createElement(`style`),ut(r),Rd(r,`style`,a),Vf(r,n.precedence,e),t.instance=r;case`stylesheet`:a=Pf(n.href);var o=e.querySelector(Ff(a));if(o)return t.state.loading|=4,t.instance=o,ut(o),o;r=If(n),(a=vf.get(a))&&Hf(r,a),o=(e.ownerDocument||e).createElement(`link`),ut(o);var s=o;return s._p=new Promise(function(e,t){s.onload=e,s.onerror=t}),Rd(o,`link`,r),t.state.loading|=4,Vf(o,n.precedence,e),t.instance=o;case`script`:return o=Rf(n.src),(a=e.querySelector(zf(o)))?(t.instance=a,ut(a),a):(r=n,(a=vf.get(o))&&(r=m({},n),Uf(r,a)),e=e.ownerDocument||e,a=e.createElement(`script`),ut(a),Rd(a,`link`,r),e.head.appendChild(a),t.instance=a);case`void`:return null;default:throw Error(i(443,t.type))}else t.type===`stylesheet`&&!(t.state.loading&4)&&(r=t.instance,t.state.loading|=4,Vf(r,n.precedence,e));return t.instance}function Vf(e,t,n){for(var r=n.querySelectorAll(`link[rel="stylesheet"][data-precedence],style[data-precedence]`),i=r.length?r[r.length-1]:null,a=i,o=0;o title`):null)}function qf(e,t,n){if(n===1||t.itemProp!=null)return!1;switch(e){case`meta`:case`title`:return!0;case`style`:if(typeof t.precedence!=`string`||typeof t.href!=`string`||t.href===``)break;return!0;case`link`:if(typeof t.rel!=`string`||typeof t.href!=`string`||t.href===``||t.onLoad||t.onError)break;switch(t.rel){case`stylesheet`:return e=t.disabled,typeof t.precedence==`string`&&e==null;default:return!0}case`script`:if(t.async&&typeof t.async!=`function`&&typeof t.async!=`symbol`&&!t.onLoad&&!t.onError&&t.src&&typeof t.src==`string`)return!0}return!1}function Jf(e){return!(e.type===`stylesheet`&&!(e.state.loading&3))}function Yf(e,t,n,r){if(n.type===`stylesheet`&&(typeof r.media!=`string`||!1!==matchMedia(r.media).matches)&&!(n.state.loading&4)){if(n.instance===null){var i=Pf(r.href),a=t.querySelector(Ff(i));if(a){t=a._p,typeof t==`object`&&t&&typeof t.then==`function`&&(e.count++,e=Qf.bind(e),t.then(e,e)),n.state.loading|=4,n.instance=a,ut(a);return}a=t.ownerDocument||t,r=If(r),(i=vf.get(i))&&Hf(r,i),a=a.createElement(`link`),ut(a);var o=a;o._p=new Promise(function(e,t){o.onload=e,o.onerror=t}),Rd(a,`link`,r),n.instance=a}e.stylesheets===null&&(e.stylesheets=new Map),e.stylesheets.set(n,t),(t=n.state.preload)&&!(n.state.loading&3)&&(e.count++,n=Qf.bind(e),t.addEventListener(`load`,n),t.addEventListener(`error`,n))}}var Xf=0;function Zf(e,t){return e.stylesheets&&e.count===0&&ep(e,e.stylesheets),0Xf?50:800)+t);return e.unsuspend=n,function(){e.unsuspend=null,clearTimeout(r),clearTimeout(i)}}:null}function Qf(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)ep(this,this.stylesheets);else if(this.unsuspend){var e=this.unsuspend;this.unsuspend=null,e()}}}var $f=null;function ep(e,t){e.stylesheets=null,e.unsuspend!==null&&(e.count++,$f=new Map,t.forEach(tp,e),$f=null,Qf.call(e))}function tp(e,t){if(!(t.state.loading&4)){var n=$f.get(e);if(n)var r=n.get(null);else{n=new Map,$f.set(e,n);for(var i=e.querySelectorAll(`link[data-precedence],style[data-precedence]`),a=0;a{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=g()}))(),v=`modulepreload`,y=function(e){return`/`+e},b={},x=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}r=o(t.map(t=>{if(t=y(t,n),t in b)return;b[t]=!0;let r=t.endsWith(`.css`),i=r?`[rel="stylesheet"]`:``;if(n)for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}else if(document.querySelector(`link[href="${t}"]${i}`))return;let o=document.createElement(`link`);if(o.rel=r?`stylesheet`:v,r||(o.as=`script`),o.crossOrigin=``,o.href=t,a&&o.setAttribute(`nonce`,a),document.head.appendChild(o),r)return new Promise((e,n)=>{o.addEventListener(`load`,e),o.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})},S=c(p(),1),C=`popstate`;function w(e){return typeof e==`object`&&!!e&&`pathname`in e&&`search`in e&&`hash`in e&&`state`in e&&`key`in e}function T(e={}){function t(e,t){let n=t.state?.masked,{pathname:r,search:i,hash:a}=n||e.location;return A(``,{pathname:r,search:i,hash:a},t.state&&t.state.usr||null,t.state&&t.state.key||`default`,n?{pathname:e.location.pathname,search:e.location.search,hash:e.location.hash}:void 0)}function n(e,t){return typeof t==`string`?t:j(t)}return N(t,n,null,e)}function E(e,t){if(e===!1||e==null)throw Error(t)}function D(e,t){if(!e){typeof console<`u`&&console.warn(t);try{throw Error(t)}catch{}}}function O(){return Math.random().toString(36).substring(2,10)}function k(e,t){return{usr:e.state,key:e.key,idx:t,masked:e.mask?{pathname:e.pathname,search:e.search,hash:e.hash}:void 0}}function A(e,t,n=null,r,i){return{pathname:typeof e==`string`?e:e.pathname,search:``,hash:``,...typeof t==`string`?M(t):t,state:n,key:t&&t.key||r||O(),mask:i}}function j({pathname:e=`/`,search:t=``,hash:n=``}){return t&&t!==`?`&&(e+=t.charAt(0)===`?`?t:`?`+t),n&&n!==`#`&&(e+=n.charAt(0)===`#`?n:`#`+n),e}function M(e){let t={};if(e){let n=e.indexOf(`#`);n>=0&&(t.hash=e.substring(n),e=e.substring(0,n));let r=e.indexOf(`?`);r>=0&&(t.search=e.substring(r),e=e.substring(0,r)),e&&(t.pathname=e)}return t}function N(e,t,n,r={}){let{window:i=document.defaultView,v5Compat:a=!1}=r,o=i.history,s=`POP`,c=null,l=u();l??(l=0,o.replaceState({...o.state,idx:l},``));function u(){return(o.state||{idx:null}).idx}function d(){s=`POP`;let e=u(),t=e==null?null:e-l;l=e,c&&c({action:s,location:h.location,delta:t})}function f(e,t){s=`PUSH`;let r=w(e)?e:A(h.location,e,t);n&&n(r,e),l=u()+1;let d=k(r,l),f=h.createHref(r.mask||r);try{o.pushState(d,``,f)}catch(e){if(e instanceof DOMException&&e.name===`DataCloneError`)throw e;i.location.assign(f)}a&&c&&c({action:s,location:h.location,delta:1})}function p(e,t){s=`REPLACE`;let r=w(e)?e:A(h.location,e,t);n&&n(r,e),l=u();let i=k(r,l),d=h.createHref(r.mask||r);o.replaceState(i,``,d),a&&c&&c({action:s,location:h.location,delta:0})}function m(e){return P(e)}let h={get action(){return s},get location(){return e(i,o)},listen(e){if(c)throw Error(`A history only accepts one active listener`);return i.addEventListener(C,d),c=e,()=>{i.removeEventListener(C,d),c=null}},createHref(e){return t(i,e)},createURL:m,encodeLocation(e){let t=m(e);return{pathname:t.pathname,search:t.search,hash:t.hash}},push:f,replace:p,go(e){return o.go(e)}};return h}function P(e,t=!1){let n=`http://localhost`;typeof window<`u`&&(n=window.location.origin===`null`?window.location.href:window.location.origin),E(n,`No window.location.(origin|href) available to create URL`);let r=typeof e==`string`?e:j(e);return r=r.replace(/ $/,`%20`),!t&&r.startsWith(`//`)&&(r=n+r),new URL(r,n)}function F(e,t,n=`/`){return I(e,t,n,!1)}function I(e,t,n,r,i){let a=se((typeof t==`string`?M(t):t).pathname||`/`,n);if(a==null)return null;let o=i??R(e),s=null,c=oe(a);for(let e=0;s==null&&e{let c={relativePath:s===void 0?e.path||``:s,caseSensitive:e.caseSensitive===!0,childrenIndex:a,route:e};if(c.relativePath.startsWith(`/`)){if(!c.relativePath.startsWith(r)&&o)return;E(c.relativePath.startsWith(r),`Absolute route path "${c.relativePath}" nested under path "${r}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),c.relativePath=c.relativePath.slice(r.length)}let l=ge([r,c.relativePath]),u=n.concat(c);e.children&&e.children.length>0&&(E(e.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${l}".`),z(e.children,t,u,l,o)),!(e.path==null&&!e.index)&&t.push({path:l,score:K(l,e.index),routesMeta:u})};return e.forEach((e,t)=>{if(e.path===``||!e.path?.includes(`?`))a(e,t);else for(let n of B(e.path))a(e,t,!0,n)}),t}function B(e){let t=e.split(`/`);if(t.length===0)return[];let[n,...r]=t,i=n.endsWith(`?`),a=n.replace(/\?$/,``);if(r.length===0)return i?[a,``]:[a];let o=B(r.join(`/`)),s=[];return s.push(...o.map(e=>e===``?a:[a,e].join(`/`))),i&&s.push(...o),s.map(t=>e.startsWith(`/`)&&t===``?`/`:t)}function V(e){e.sort((e,t)=>e.score===t.score?re(e.routesMeta.map(e=>e.childrenIndex),t.routesMeta.map(e=>e.childrenIndex)):t.score-e.score)}var H=/^:[\w-]+$/,U=3,W=2,ee=1,te=10,G=-2,ne=e=>e===`*`;function K(e,t){let n=e.split(`/`),r=n.length;return n.some(ne)&&(r+=G),t&&(r+=W),n.filter(e=>!ne(e)).reduce((e,t)=>e+(H.test(t)?U:t===``?ee:te),r)}function re(e,t){return e.length===t.length&&e.slice(0,-1).every((e,n)=>e===t[n])?e[e.length-1]-t[t.length-1]:0}function ie(e,t,n=!1){let{routesMeta:r}=e,i={},a=`/`,o=[];for(let e=0;e{if(t===`*`){let e=s[r]||``;o=a.slice(0,a.length-e.length).replace(/(.)\/+$/,`$1`)}let i=s[r];return n&&!i?e[t]=void 0:e[t]=(i||``).replace(/%2F/g,`/`),e},{}),pathname:a,pathnameBase:o,pattern:e}}function q(e,t=!1,n=!0){D(e===`*`||!e.endsWith(`*`)||e.endsWith(`/*`),`Route path "${e}" will be treated as if it were "${e.replace(/\*$/,`/*`)}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${e.replace(/\*$/,`/*`)}".`);let r=[],i=`^`+e.replace(/\/*\*?$/,``).replace(/^\/*/,`/`).replace(/[\\.*+^${}|()[\]]/g,`\\$&`).replace(/\/:([\w-]+)(\?)?/g,(e,t,n,i,a)=>{if(r.push({paramName:t,isOptional:n!=null}),n){let t=a.charAt(i+e.length);return t&&t!==`/`?`/([^\\/]*)`:`(?:/([^\\/]*))?`}return`/([^\\/]+)`}).replace(/\/([\w-]+)\?(\/|$)/g,`(/$1)?$2`);return e.endsWith(`*`)?(r.push({paramName:`*`}),i+=e===`*`||e===`/*`?`(.*)$`:`(?:\\/(.+)|\\/*)$`):n?i+=`\\/*$`:e!==``&&e!==`/`&&(i+=`(?:(?=\\/|$))`),[new RegExp(i,t?void 0:`i`),r]}function oe(e){try{return e.split(`/`).map(e=>decodeURIComponent(e).replace(/\//g,`%2F`)).join(`/`)}catch(t){return D(!1,`The URL path "${e}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${t}).`),e}}function se(e,t){if(t===`/`)return e;if(!e.toLowerCase().startsWith(t.toLowerCase()))return null;let n=t.endsWith(`/`)?t.length-1:t.length,r=e.charAt(n);return r&&r!==`/`?null:e.slice(n)||`/`}var ce=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;function le(e,t=`/`){let{pathname:n,search:r=``,hash:i=``}=typeof e==`string`?M(e):e,a;return n?(n=he(n),a=n.startsWith(`/`)?ue(n.substring(1),`/`):ue(n,t)):a=t,{pathname:a,search:ye(r),hash:be(i)}}function ue(e,t){let n=_e(t).split(`/`);return e.split(`/`).forEach(e=>{e===`..`?n.length>1&&n.pop():e!==`.`&&n.push(e)}),n.length>1?n.join(`/`):`/`}function de(e,t,n,r){return`Cannot include a '${e}' character in a manually specified \`to.${t}\` field [${JSON.stringify(r)}]. Please separate it out to the \`to.${n}\` field. Alternatively you may provide the full path as a string in and the router will parse it for you.`}function fe(e){return e.filter((e,t)=>t===0||e.route.path&&e.route.path.length>0)}function pe(e){let t=fe(e);return t.map((e,n)=>n===t.length-1?e.pathname:e.pathnameBase)}function me(e,t,n,r=!1){let i;typeof e==`string`?i=M(e):(i={...e},E(!i.pathname||!i.pathname.includes(`?`),de(`?`,`pathname`,`search`,i)),E(!i.pathname||!i.pathname.includes(`#`),de(`#`,`pathname`,`hash`,i)),E(!i.search||!i.search.includes(`#`),de(`#`,`search`,`hash`,i)));let a=e===``||i.pathname===``,o=a?`/`:i.pathname,s;if(o==null)s=n;else{let e=t.length-1;if(!r&&o.startsWith(`..`)){let t=o.split(`/`);for(;t[0]===`..`;)t.shift(),--e;i.pathname=t.join(`/`)}s=e>=0?t[e]:`/`}let c=le(i,s),l=o&&o!==`/`&&o.endsWith(`/`),u=(a||o===`.`)&&n.endsWith(`/`);return!c.pathname.endsWith(`/`)&&(l||u)&&(c.pathname+=`/`),c}var he=e=>e.replace(/\/\/+/g,`/`),ge=e=>he(e.join(`/`)),_e=e=>e.replace(/\/+$/,``),ve=e=>_e(e).replace(/^\/*/,`/`),ye=e=>!e||e===`?`?``:e.startsWith(`?`)?e:`?`+e,be=e=>!e||e===`#`?``:e.startsWith(`#`)?e:`#`+e,xe=class{constructor(e,t,n,r=!1){this.status=e,this.statusText=t||``,this.internal=r,n instanceof Error?(this.data=n.toString(),this.error=n):this.data=n}};function Se(e){return e!=null&&typeof e.status==`number`&&typeof e.statusText==`string`&&typeof e.internal==`boolean`&&`data`in e}function Ce(e){return ge(e.map(e=>e.route.path).filter(Boolean))||`/`}var we=typeof window<`u`&&window.document!==void 0&&window.document.createElement!==void 0;function Te(e,t){let n=e;if(typeof n!=`string`||!ce.test(n))return{absoluteURL:void 0,isExternal:!1,to:n};let r=n,i=!1;if(we)try{let e=new URL(window.location.href),r=n.startsWith(`//`)?new URL(e.protocol+n):new URL(n),a=se(r.pathname,t);r.origin===e.origin&&a!=null?n=a+r.search+r.hash:i=!0}catch{D(!1,` contains an invalid URL which will probably break when clicked - please update to a valid URL path.`)}return{absoluteURL:r,isExternal:i,to:n}}Object.getOwnPropertyNames(Object.prototype).sort().join(`\0`);var Ee=[`POST`,`PUT`,`PATCH`,`DELETE`];new Set(Ee);var De=[`GET`,...Ee];new Set(De);var Oe=S.createContext(null);Oe.displayName=`DataRouter`;var ke=S.createContext(null);ke.displayName=`DataRouterState`;var Ae=S.createContext(!1);function je(){return S.useContext(Ae)}var Me=S.createContext({isTransitioning:!1});Me.displayName=`ViewTransition`;var Ne=S.createContext(new Map);Ne.displayName=`Fetchers`;var Pe=S.createContext(null);Pe.displayName=`Await`;var Fe=S.createContext(null);Fe.displayName=`Navigation`;var Ie=S.createContext(null);Ie.displayName=`Location`;var Le=S.createContext({outlet:null,matches:[],isDataRoute:!1});Le.displayName=`Route`;var Re=S.createContext(null);Re.displayName=`RouteError`;var ze=`REACT_ROUTER_ERROR`,Be=`REDIRECT`,Ve=`ROUTE_ERROR_RESPONSE`;function He(e){if(e.startsWith(`${ze}:${Be}:{`))try{let t=JSON.parse(e.slice(28));if(typeof t==`object`&&t&&typeof t.status==`number`&&typeof t.statusText==`string`&&typeof t.location==`string`&&typeof t.reloadDocument==`boolean`&&typeof t.replace==`boolean`)return t}catch{}}function Ue(e){if(e.startsWith(`${ze}:${Ve}:{`))try{let t=JSON.parse(e.slice(40));if(typeof t==`object`&&t&&typeof t.status==`number`&&typeof t.statusText==`string`)return new xe(t.status,t.statusText,t.data)}catch{}}function We(e,{relative:t}={}){E(Ge(),`useHref() may be used only in the context of a component.`);let{basename:n,navigator:r}=S.useContext(Fe),{hash:i,pathname:a,search:o}=et(e,{relative:t}),s=a;return n!==`/`&&(s=a===`/`?n:ge([n,a])),r.createHref({pathname:s,search:o,hash:i})}function Ge(){return S.useContext(Ie)!=null}function Ke(){return E(Ge(),`useLocation() may be used only in the context of a component.`),S.useContext(Ie).location}var qe=`You should call navigate() in a React.useEffect(), not when your component is first rendered.`;function Je(e){S.useContext(Fe).static||S.useLayoutEffect(e)}function Ye(){let{isDataRoute:e}=S.useContext(Le);return e?yt():Xe()}function Xe(){E(Ge(),`useNavigate() may be used only in the context of a component.`);let e=S.useContext(Oe),{basename:t,navigator:n}=S.useContext(Fe),{matches:r}=S.useContext(Le),{pathname:i}=Ke(),a=JSON.stringify(pe(r)),o=S.useRef(!1);return Je(()=>{o.current=!0}),S.useCallback((r,s={})=>{if(D(o.current,qe),!o.current)return;if(typeof r==`number`){n.go(r);return}let c=me(r,JSON.parse(a),i,s.relative===`path`);e==null&&t!==`/`&&(c.pathname=c.pathname===`/`?t:ge([t,c.pathname])),(s.replace?n.replace:n.push)(c,s.state,s)},[t,n,a,i,e])}var Ze=S.createContext(null);function Qe(e){let t=S.useContext(Le).outlet;return S.useMemo(()=>t&&S.createElement(Ze.Provider,{value:e},t),[t,e])}function $e(){let{matches:e}=S.useContext(Le);return e[e.length-1]?.params??{}}function et(e,{relative:t}={}){let{matches:n}=S.useContext(Le),{pathname:r}=Ke(),i=JSON.stringify(pe(n));return S.useMemo(()=>me(e,JSON.parse(i),r,t===`path`),[e,i,r,t])}function tt(e,t){return nt(e,t)}function nt(e,t,n){E(Ge(),`useRoutes() may be used only in the context of a component.`);let{navigator:r}=S.useContext(Fe),{matches:i}=S.useContext(Le),a=i[i.length-1],o=a?a.params:{},s=a?a.pathname:`/`,c=a?a.pathnameBase:`/`,l=a&&a.route;{let e=l&&l.path||``;xt(s,!l||e.endsWith(`*`)||e.endsWith(`*?`),`You rendered descendant (or called \`useRoutes()\`) at "${s}" (under ) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render. Please change the parent to .`)}let u=Ke(),d;if(t){let e=typeof t==`string`?M(t):t;E(c===`/`||e.pathname?.startsWith(c),`When overriding the location using \`\` or \`useRoutes(routes, location)\`, the location pathname must begin with the portion of the URL pathname that was matched by all parent routes. The current pathname base is "${c}" but pathname "${e.pathname}" was given in the \`location\` prop.`),d=e}else d=u;let f=d.pathname||`/`,p=f;if(c!==`/`){let e=c.replace(/^\//,``).split(`/`);p=`/`+f.replace(/^\//,``).split(`/`).slice(e.length).join(`/`)}let m=n&&n.state.matches.length?n.state.matches.map(e=>Object.assign(e,{route:n.manifest[e.route.id]||e.route})):F(e,{pathname:p});D(l||m!=null,`No routes matched location "${d.pathname}${d.search}${d.hash}" `),D(m==null||m[m.length-1].route.element!==void 0||m[m.length-1].route.Component!==void 0||m[m.length-1].route.lazy!==void 0,`Matched leaf route at location "${d.pathname}${d.search}${d.hash}" does not have an element or Component. This means it will render an with a null value by default resulting in an "empty" page.`);let h=lt(m&&m.map(e=>Object.assign({},e,{params:Object.assign({},o,e.params),pathname:ge([c,r.encodeLocation?r.encodeLocation(e.pathname.replace(/%/g,`%25`).replace(/\?/g,`%3F`).replace(/#/g,`%23`)).pathname:e.pathname]),pathnameBase:e.pathnameBase===`/`?c:ge([c,r.encodeLocation?r.encodeLocation(e.pathnameBase.replace(/%/g,`%25`).replace(/\?/g,`%3F`).replace(/#/g,`%23`)).pathname:e.pathnameBase])})),i,n);return t&&h?S.createElement(Ie.Provider,{value:{location:{pathname:`/`,search:``,hash:``,state:null,key:`default`,mask:void 0,...d},navigationType:`POP`}},h):h}function rt(){let e=vt(),t=Se(e)?`${e.status} ${e.statusText}`:e instanceof Error?e.message:JSON.stringify(e),n=e instanceof Error?e.stack:null,r=`rgba(200,200,200, 0.5)`,i={padding:`0.5rem`,backgroundColor:r},a={padding:`2px 4px`,backgroundColor:r},o=null;return console.error(`Error handled by React Router default ErrorBoundary:`,e),o=S.createElement(S.Fragment,null,S.createElement(`p`,null,`💿 Hey developer 👋`),S.createElement(`p`,null,`You can provide a way better UX than this when your app throws errors by providing your own `,S.createElement(`code`,{style:a},`ErrorBoundary`),` or`,` `,S.createElement(`code`,{style:a},`errorElement`),` prop on your route.`)),S.createElement(S.Fragment,null,S.createElement(`h2`,null,`Unexpected Application Error!`),S.createElement(`h3`,{style:{fontStyle:`italic`}},t),n?S.createElement(`pre`,{style:i},n):null,o)}var it=S.createElement(rt,null),at=class extends S.Component{constructor(e){super(e),this.state={location:e.location,revalidation:e.revalidation,error:e.error}}static getDerivedStateFromError(e){return{error:e}}static getDerivedStateFromProps(e,t){return t.location!==e.location||t.revalidation!==`idle`&&e.revalidation===`idle`?{error:e.error,location:e.location,revalidation:e.revalidation}:{error:e.error===void 0?t.error:e.error,location:t.location,revalidation:e.revalidation||t.revalidation}}componentDidCatch(e,t){this.props.onError?this.props.onError(e,t):console.error(`React Router caught the following error during render`,e)}render(){let e=this.state.error;if(this.context&&typeof e==`object`&&e&&`digest`in e&&typeof e.digest==`string`){let t=Ue(e.digest);t&&(e=t)}let t=e===void 0?this.props.children:S.createElement(Le.Provider,{value:this.props.routeContext},S.createElement(Re.Provider,{value:e,children:this.props.component}));return this.context?S.createElement(st,{error:e},t):t}};at.contextType=Ae;var ot=new WeakMap;function st({children:e,error:t}){let{basename:n}=S.useContext(Fe);if(typeof t==`object`&&t&&`digest`in t&&typeof t.digest==`string`){let e=He(t.digest);if(e){let r=ot.get(t);if(r)throw r;let i=Te(e.location,n);if(we&&!ot.get(t))if(i.isExternal||e.reloadDocument)window.location.href=i.absoluteURL||i.to;else{let n=Promise.resolve().then(()=>window.__reactRouterDataRouter.navigate(i.to,{replace:e.replace}));throw ot.set(t,n),n}return S.createElement(`meta`,{httpEquiv:`refresh`,content:`0;url=${i.absoluteURL||i.to}`})}}return e}function ct({routeContext:e,match:t,children:n}){let r=S.useContext(Oe);return r&&r.static&&r.staticContext&&(t.route.errorElement||t.route.ErrorBoundary)&&(r.staticContext._deepestRenderedBoundaryId=t.route.id),S.createElement(Le.Provider,{value:e},n)}function lt(e,t=[],n){let r=n?.state;if(e==null){if(!r)return null;if(r.errors)e=r.matches;else if(t.length===0&&!r.initialized&&r.matches.length>0)e=r.matches;else return null}let i=e,a=r?.errors;if(a!=null){let e=i.findIndex(e=>e.route.id&&a?.[e.route.id]!==void 0);E(e>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(a).join(`,`)}`),i=i.slice(0,Math.min(i.length,e+1))}let o=!1,s=-1;if(n&&r){o=r.renderFallback;for(let e=0;e=0?i.slice(0,s+1):[i[0]];break}}}}let c=n?.onError,l=r&&c?(e,t)=>{c(e,{location:r.location,params:r.matches?.[0]?.params??{},pattern:Ce(r.matches),errorInfo:t})}:void 0;return i.reduceRight((e,n,c)=>{let u,d=!1,f=null,p=null;r&&(u=a&&n.route.id?a[n.route.id]:void 0,f=n.route.errorElement||it,o&&(s<0&&c===0?(xt(`route-fallback`,!1,"No `HydrateFallback` element provided to render during initial hydration"),d=!0,p=null):s===c&&(d=!0,p=n.route.hydrateFallbackElement||null)));let m=t.concat(i.slice(0,c+1)),h=()=>{let t;return t=u?f:d?p:n.route.Component?S.createElement(n.route.Component,null):n.route.element?n.route.element:e,S.createElement(ct,{match:n,routeContext:{outlet:e,matches:m,isDataRoute:r!=null},children:t})};return r&&(n.route.ErrorBoundary||n.route.errorElement||c===0)?S.createElement(at,{location:r.location,revalidation:r.revalidation,component:f,error:u,children:h(),routeContext:{outlet:null,matches:m,isDataRoute:!0},onError:l}):h()},null)}function ut(e){return`${e} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function dt(e){let t=S.useContext(Oe);return E(t,ut(e)),t}function ft(e){let t=S.useContext(ke);return E(t,ut(e)),t}function pt(e){let t=S.useContext(Le);return E(t,ut(e)),t}function mt(e){let t=pt(e),n=t.matches[t.matches.length-1];return E(n.route.id,`${e} can only be used on routes that contain a unique "id"`),n.route.id}function ht(){return mt(`useRouteId`)}function gt(){return ft(`useNavigation`).navigation}function _t(){let{matches:e,loaderData:t}=ft(`useMatches`);return S.useMemo(()=>e.map(e=>L(e,t)),[e,t])}function vt(){let e=S.useContext(Re),t=ft(`useRouteError`),n=mt(`useRouteError`);return e===void 0?t.errors?.[n]:e}function yt(){let{router:e}=dt(`useNavigate`),t=mt(`useNavigate`),n=S.useRef(!1);return Je(()=>{n.current=!0}),S.useCallback(async(r,i={})=>{D(n.current,qe),n.current&&(typeof r==`number`?await e.navigate(r):await e.navigate(r,{fromRouteId:t,...i}))},[e,t])}var bt={};function xt(e,t,n){!t&&!bt[e]&&(bt[e]=!0,D(!1,n))}S.memo(St);function St({routes:e,manifest:t,future:n,state:r,isStatic:i,onError:a}){return nt(e,void 0,{manifest:t,state:r,isStatic:i,onError:a,future:n})}function Ct({to:e,replace:t,state:n,relative:r}){E(Ge(),` may be used only in the context of a component.`);let{static:i}=S.useContext(Fe);D(!i,` must not be used on the initial render in a . This is a no-op, but you should modify your code so the is only ever rendered in response to some user interaction or state change.`);let{matches:a}=S.useContext(Le),{pathname:o}=Ke(),s=Ye(),c=me(e,pe(a),o,r===`path`),l=JSON.stringify(c);return S.useEffect(()=>{s(JSON.parse(l),{replace:t,state:n,relative:r})},[s,l,r,t,n]),null}function wt(e){return Qe(e.context)}function Tt(e){E(!1,`A is only ever to be used as the child of element, never rendered directly. Please wrap your in a .`)}function Et({basename:e=`/`,children:t=null,location:n,navigationType:r=`POP`,navigator:i,static:a=!1,useTransitions:o}){E(!Ge(),`You cannot render a inside another . You should never have more than one in your app.`);let s=e.replace(/^\/*/,`/`),c=S.useMemo(()=>({basename:s,navigator:i,static:a,useTransitions:o,future:{}}),[s,i,a,o]);typeof n==`string`&&(n=M(n));let{pathname:l=`/`,search:u=``,hash:d=``,state:f=null,key:p=`default`,mask:m}=n,h=S.useMemo(()=>{let e=se(l,s);return e==null?null:{location:{pathname:e,search:u,hash:d,state:f,key:p,mask:m},navigationType:r}},[s,l,u,d,f,p,r,m]);return D(h!=null,` is not able to match the URL "${l}${u}${d}" because it does not start with the basename, so the won't render anything.`),h==null?null:S.createElement(Fe.Provider,{value:c},S.createElement(Ie.Provider,{children:t,value:h}))}function Dt({children:e,location:t}){return tt(Ot(e),t)}S.Component;function Ot(e,t=[]){let n=[];return S.Children.forEach(e,(e,r)=>{if(!S.isValidElement(e))return;let i=[...t,r];if(e.type===S.Fragment){n.push.apply(n,Ot(e.props.children,i));return}E(e.type===Tt,`[${typeof e.type==`string`?e.type:e.type.name}] is not a component. All component children of must be a or `),E(!e.props.index||!e.props.children,`An index route cannot have child routes.`);let a={id:e.props.id||i.join(`-`),caseSensitive:e.props.caseSensitive,element:e.props.element,Component:e.props.Component,index:e.props.index,path:e.props.path,middleware:e.props.middleware,loader:e.props.loader,action:e.props.action,hydrateFallbackElement:e.props.hydrateFallbackElement,HydrateFallback:e.props.HydrateFallback,errorElement:e.props.errorElement,ErrorBoundary:e.props.ErrorBoundary,hasErrorBoundary:e.props.hasErrorBoundary===!0||e.props.ErrorBoundary!=null||e.props.errorElement!=null,shouldRevalidate:e.props.shouldRevalidate,handle:e.props.handle,lazy:e.props.lazy};e.props.children&&(a.children=Ot(e.props.children,i)),n.push(a)}),n}var kt=`get`,At=`application/x-www-form-urlencoded`;function jt(e){return typeof HTMLElement<`u`&&e instanceof HTMLElement}function Mt(e){return jt(e)&&e.tagName.toLowerCase()===`button`}function Nt(e){return jt(e)&&e.tagName.toLowerCase()===`form`}function Pt(e){return jt(e)&&e.tagName.toLowerCase()===`input`}function Ft(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function It(e,t){return e.button===0&&(!t||t===`_self`)&&!Ft(e)}function Lt(e=``){return new URLSearchParams(typeof e==`string`||Array.isArray(e)||e instanceof URLSearchParams?e:Object.keys(e).reduce((t,n)=>{let r=e[n];return t.concat(Array.isArray(r)?r.map(e=>[n,e]):[[n,r]])},[]))}function Rt(e,t){let n=Lt(e);return t&&t.forEach((e,r)=>{n.has(r)||t.getAll(r).forEach(e=>{n.append(r,e)})}),n}var zt=null;function Bt(){if(zt===null)try{new FormData(document.createElement(`form`),0),zt=!1}catch{zt=!0}return zt}var Vt=new Set([`application/x-www-form-urlencoded`,`multipart/form-data`,`text/plain`]);function Ht(e){return e!=null&&!Vt.has(e)?(D(!1,`"${e}" is not a valid \`encType\` for \`\`/\`\` and will default to "${At}"`),null):e}function Ut(e,t){let n,r,i,a,o;if(Nt(e)){let o=e.getAttribute(`action`);r=o?se(o,t):null,n=e.getAttribute(`method`)||kt,i=Ht(e.getAttribute(`enctype`))||At,a=new FormData(e)}else if(Mt(e)||Pt(e)&&(e.type===`submit`||e.type===`image`)){let o=e.form;if(o==null)throw Error(`Cannot submit a -
- - 还可选取图片 {selectedImages}/{maxImageCount !== undefined && usedImageCount !== undefined ? maxImageCount - usedImageCount : '-'} 张 - - - 视频还可选取 {selectedVideos}/{maxVideoCount !== undefined && usedVideoCount !== undefined ? maxVideoCount - usedVideoCount : '-'} 个({selectedVideoDuration.toFixed(1)}/{maxVideoDuration !== undefined && usedVideoDuration !== undefined ? (maxVideoDuration - usedVideoDuration).toFixed(1) : '-'}秒) - - {maxAudioCount !== undefined && ( - - 音频还可选取 {selectedAudios}/{maxAudioCount !== undefined && usedAudioCount !== undefined ? maxAudioCount - usedAudioCount : '-'} 个({selectedAudioDuration.toFixed(1)}/{maxAudioDuration !== undefined && usedAudioDuration !== undefined ? (maxAudioDuration - usedAudioDuration).toFixed(1) : '-'}秒) + {!hideLimitHint && ( +
+ + 还可选取图片 {selectedImages}/{maxImageCount !== undefined && usedImageCount !== undefined ? maxImageCount - usedImageCount : '-'} 张 - )} -
+ + 视频还可选取 {selectedVideos}/{maxVideoCount !== undefined && usedVideoCount !== undefined ? maxVideoCount - usedVideoCount : '-'} 个({selectedVideoDuration.toFixed(1)}/{maxVideoDuration !== undefined && usedVideoDuration !== undefined ? (maxVideoDuration - usedVideoDuration).toFixed(1) : '-'}秒) + + {maxAudioCount !== undefined && ( + + 音频还可选取 {selectedAudios}/{maxAudioCount !== undefined && usedAudioCount !== undefined ? maxAudioCount - usedAudioCount : '-'} 个({selectedAudioDuration.toFixed(1)}/{maxAudioDuration !== undefined && usedAudioDuration !== undefined ? (maxAudioDuration - usedAudioDuration).toFixed(1) : '-'}秒) + + )} +
+ )}
@@ -310,7 +401,9 @@ const UploadResourceHistoryPicker: React.FC =
{items.map((item) => { const checked = checkedMap.has(item.id); - const disabled = selectedIdSet.has(item.id); + const selectedDisabled = selectedIdSet.has(item.id); + const validationError = getItemValidationError(item); + const disabled = selectedDisabled || !!validationError; const preview = buildPreviewUrl(item.previewUrl || item.displayUrl || item.resourceUrl); return (
= overflow: 'hidden', boxShadow: checked ? '0 10px 24px rgba(139, 92, 246, 0.18)' : '0 6px 18px rgba(15,23,42,0.06)', }} + title={validationError ? validationError : undefined} >
{item.resourceType === 'image' ? ( @@ -340,6 +434,7 @@ const UploadResourceHistoryPicker: React.FC = {typeLabel(item.resourceType)} {item.durationSeconds ? {Number(item.durationSeconds).toFixed(1)}s : null} + {validationError && 尺寸不符} {item.fileName || item.id} @@ -352,7 +447,12 @@ const UploadResourceHistoryPicker: React.FC =
)} - {disabled &&
已选
} + {selectedDisabled &&
已选
} + {validationError && ( +
+ {validationError} +
+ )}
); })} diff --git a/video-gen-app/src/pages/GenerateConver.tsx b/video-gen-app/src/pages/GenerateConver.tsx index 5951fd67..58cd61ec 100644 --- a/video-gen-app/src/pages/GenerateConver.tsx +++ b/video-gen-app/src/pages/GenerateConver.tsx @@ -978,7 +978,7 @@ const AIChatPage: React.FC = () => { } - console.log(mediaReferences); + // console.log(mediaReferences); // 创建用户消息对象 const newMessage: Message = { @@ -1240,7 +1240,7 @@ const AIChatPage: React.FC = () => { try { const { width, height } = await getImageDimensions(file); - const error = validateVideoDimensions(width, height); + const error = validateImageDimensions(width, height); if (error) { antdMessage.error(error); return false; @@ -1316,7 +1316,7 @@ const AIChatPage: React.FC = () => { if (isImage) { try { const { width, height } = await getImageDimensions(file); - const error = validateVideoDimensions(width, height); + const error = validateImageDimensions(width, height); if (error) { antdMessage.error(error); return false; @@ -1456,7 +1456,7 @@ const AIChatPage: React.FC = () => { if (isImage) { try { const { width, height } = await getImageDimensions(file); - const error = validateVideoDimensions(width, height); + const error = validateImageDimensions(width, height); if (error) { antdMessage.error(error); return false; @@ -2183,13 +2183,16 @@ const AIChatPage: React.FC = () => { onClick={(e) => { e.stopPropagation(); setInputValue(msg.originalPrompt || ''); + if (msg.genType) { + setMediaType(msg.genType); + } if (msg.mediaReferences && msg.mediaReferences.length > 0) { const hasFirstLastFrame = msg.mediaReferences.some((ref: any) => ref.role === 'first_frame' || ref.role === 'last_frame'); if (hasFirstLastFrame && msg.genType === 'video') { const first = msg.mediaReferences.find((ref: any) => ref.role === 'first_frame'); const last = msg.mediaReferences.find((ref: any) => ref.role === 'last_frame'); - setFirstFrame(first ? { ...first, label: first.label || '' } : null); - setLastFrame(last ? { ...last, label: last.label || '' } : null); + setFirstFrame(first ? { ...first, label: first.label || '', duration: first.duration } : null); + setLastFrame(last ? { ...last, label: last.label || '', duration: last.duration } : null); setCurrentMedia([]); setReferenceMode('first_last_frame'); } else { @@ -2199,6 +2202,7 @@ const AIChatPage: React.FC = () => { url: ref.url, label: ref.label || '', role: ref.role, + duration: ref.duration, }))); setFirstFrame(null); setLastFrame(null); @@ -2887,6 +2891,7 @@ const AIChatPage: React.FC = () => { usedAudioCount={currentMedia.filter(m => m.type === 'audio').length} maxAudioDuration={15} usedAudioDuration={currentMedia.filter(m => m.type === 'audio').reduce((sum, m) => sum + (m.duration || 0), 0)} + hideLimitHint={mediaType === 'image'} >
{ usedAudioCount={currentMedia.filter(m => m.type === 'audio').length} maxAudioDuration={15} usedAudioDuration={currentMedia.filter(m => m.type === 'audio').reduce((sum, m) => sum + (m.duration || 0), 0)} + hideLimitHint={mediaType === 'image'} >
{ const [creditRatios, setCreditRatios] = useState([]); const [cimage, setCimage] = useState([]); + const validateImageDimensions = (width: number, height: number): string | null => { + if (width < 300 || width > 6000) return `图片宽度需在 300~6000px 之间,当前为 ${width}px`; + if (height < 300 || height > 6000) return `图片高度需在 300~6000px 之间,当前为 ${height}px`; + const ratio = width / height; + if (ratio < 0.4 || ratio > 2.5) return `图片宽高比需在 0.4~2.5 之间,当前为 ${ratio.toFixed(2)}`; + return null; + }; + + const validateVideoDimensions = (width: number, height: number): string | null => { + if (width < 300 || width > 6000) return `视频宽度需在 300~6000px 之间,当前为 ${width}px`; + if (height < 300 || height > 6000) return `视频高度需在 300~6000px 之间,当前为 ${height}px`; + const ratio = width / height; + if (ratio < 0.4 || ratio > 2.5) return `视频宽高比需在 0.4~2.5 之间,当前为 ${ratio.toFixed(2)}`; + const totalPixels = width * height; + if (totalPixels < 409600) return `视频总像素数过小(${width}×${height}=${totalPixels}),需 ≥ 640×640=409600`; + if (totalPixels > 8295044) return `视频总像素数过大(${width}×${height}=${totalPixels}),需 ≤ 3326×2494=8295044`; + return null; + }; + const handlePasteUpload = async (file: File) => { const isImage = file.type.startsWith("image/"); const isVideo = file.type.startsWith("video/"); @@ -299,23 +318,51 @@ const GeneratePage: React.FC = () => { } let fileDuration = 0; if (isVideo) { - fileDuration = await new Promise((resolve) => { + const videoInfo = await new Promise<{ duration: number; width: number; height: number }>((resolve) => { const video = document.createElement("video"); video.preload = "metadata"; video.onloadedmetadata = () => { - resolve(video.duration || 0); + resolve({ duration: video.duration || 0, width: video.videoWidth || 0, height: video.videoHeight || 0 }); video.remove(); }; video.onerror = () => { - resolve(0); + resolve({ duration: 0, width: 0, height: 0 }); video.remove(); }; video.src = URL.createObjectURL(file); }); + fileDuration = videoInfo.duration; + if (videoInfo.width > 0 && videoInfo.height > 0) { + const error = validateVideoDimensions(videoInfo.width, videoInfo.height); + if (error) { + message.error(`${error}`); + return false; + } + } if (videoDuration + fileDuration > MAX_VIDEO_DURATION) { message.error(`视频总时长不能超过${MAX_VIDEO_DURATION}秒`); return false; } + } else { + const imageInfo = await new Promise<{ width: number; height: number }>((resolve) => { + const img = document.createElement('img'); + img.onload = () => { + resolve({ width: img.width, height: img.height }); + URL.revokeObjectURL(img.src); + }; + img.onerror = () => { + resolve({ width: 0, height: 0 }); + URL.revokeObjectURL(img.src); + }; + img.src = URL.createObjectURL(file); + }); + if (imageInfo.width > 0 && imageInfo.height > 0) { + const error = validateImageDimensions(imageInfo.width, imageInfo.height); + if (error) { + message.error(`${error}`); + return false; + } + } } setUploading(true); try { @@ -1968,7 +2015,7 @@ const GeneratePage: React.FC = () => { } }} rows={3} - placeholder="上传参考素材、输入文字,自由组合图、文多元素。输入 @ 可引用参考内容..." + placeholder="上传参考素材(只用做模型理解,不参与生成)、输入文字,自由组合图、文多元素。输入 @ 可引用参考内容..." maxLength={500} bordered={false} autoSize={{ minRows: 2, maxRows: 6 }} diff --git a/video-gen-app/src/pages/GeneratedRecord.tsx b/video-gen-app/src/pages/GeneratedRecord.tsx index e6f663dc..05e5a605 100644 --- a/video-gen-app/src/pages/GeneratedRecord.tsx +++ b/video-gen-app/src/pages/GeneratedRecord.tsx @@ -299,7 +299,7 @@ const GeneratedRecord: React.FC = () => { folder?.file(filename, blob); successCount++; } catch (error) { - console.warn(`文件下载失败(CORS限制): ${filename},将使用备用方式下载`); + // console.warn(`文件下载失败(CORS限制): ${filename},将使用备用方式下载`); hasError = true; break; } @@ -448,7 +448,7 @@ const GeneratedRecord: React.FC = () => { const res = await getPreTestList({ page: 1, pageSize: 100 }); setPreTestTemplates(res.data?.data || res.data || []); } catch (error) { - console.error('加载前测模板失败:', error); + // console.error('加载前测模板失败:', error); } finally { setPreTestTemplatesLoading(false); } @@ -489,7 +489,7 @@ const GeneratedRecord: React.FC = () => { setOauthList(data || []); setOauthTotal(res.pagination.total || 0); } catch (error) { - console.error('加载授权列表失败:', error); + // console.error('加载授权列表失败:', error); setOauthList([]); setOauthTotal(0); } finally { @@ -513,7 +513,7 @@ const GeneratedRecord: React.FC = () => { }); setOpenTypeMap(map); } catch (error) { - console.error('加载历史授权账户列表失败:', error); + // console.error('加载历史授权账户列表失败:', error); setHistoryOAuthList([]); setOpenTypeMap({}); } finally { @@ -532,7 +532,7 @@ const GeneratedRecord: React.FC = () => { setUploadHistoryList(data || []); setUploadHistoryTotal(res.pagination?.total || res.total || 0); } catch (error) { - console.error('加载推送历史失败:', error); + // console.error('加载推送历史失败:', error); setUploadHistoryList([]); setUploadHistoryTotal(0); } finally { @@ -669,7 +669,7 @@ const GeneratedRecord: React.FC = () => { setIsPreTest('2'); setPreTestTemplate(''); } catch (error: any) { - console.error('批量推送失败:', error); + // console.error('批量推送失败:', error); message.error(error.message || '批量推送失败'); } finally { setUploading(false); @@ -699,7 +699,7 @@ const GeneratedRecord: React.FC = () => { }); message.success('文件名更新成功'); } catch (error: any) { - console.error('文件名更新失败:', error); + // console.error('文件名更新失败:', error); message.error(error.message || '文件名更新失败'); } }; @@ -733,11 +733,11 @@ const GeneratedRecord: React.FC = () => { }), })); }); - console.log(response); + // console.log(response); const successCount = response?.successCount || 0; message.success(`已更新 ${successCount} 个文件名`); } catch (error: any) { - console.error('文件名更新失败:', error); + // console.error('文件名更新失败:', error); message.error(error.message || '文件名更新失败'); } }; @@ -1263,7 +1263,7 @@ const GeneratedRecord: React.FC = () => { }}> } - tip="加载中..." + description="加载中..." size="large" />
diff --git a/video-gen-app/src/pages/HomePage.tsx b/video-gen-app/src/pages/HomePage.tsx index ff413f8d..578039c9 100644 --- a/video-gen-app/src/pages/HomePage.tsx +++ b/video-gen-app/src/pages/HomePage.tsx @@ -206,7 +206,7 @@ const HomePage: React.FC = () => { setActiveCaseTab(firstId); // 初始请求第一个 tab 的数据 getHomeCaseButton(firstId).then((btnRes: any) => { - console.log('caseButton:', btnRes); + // console.log('caseButton:', btnRes); if (btnRes?.categories?.[0]?.assets) { setCaseAssets(btnRes.categories[0].assets); } @@ -357,7 +357,7 @@ const HomePage: React.FC = () => { { icon: , title: '爆款复刻', - description: '上传参考视频与产品图片,一键复刻爆款视频开头', + description: '上传参考视频与产品图片,一键复刻爆款', action: '立即创作', path: '/initial', }, diff --git a/video-gen-app/src/pages/InitialInfo.tsx b/video-gen-app/src/pages/InitialInfo.tsx index be2b9051..a5618b8c 100644 --- a/video-gen-app/src/pages/InitialInfo.tsx +++ b/video-gen-app/src/pages/InitialInfo.tsx @@ -14,6 +14,13 @@ function clonePlain(value: T): T { return value === undefined ? value : JSON.parse(JSON.stringify(value)); } +const buildMediaUrl = (url: string): string => { + if (!url) return ''; + if (/^https?:\/\//i.test(url)) return url; + const base = (import.meta.env.VITE_API_BASE || 'http://localhost:8000').replace(/\/$/, ''); + return `${base}${url.startsWith('/') ? '' : '/'}${url}`; +}; + function InitialInfo() { const navigate = useNavigate(); const { creatID } = useParams<{ creatID: string }>(); @@ -101,7 +108,7 @@ function InitialInfo() { }, [previewVisible, previewType]); const openPreview = (url: string, type: 'image' | 'video') => { - console.log(url, type); + // console.log(url, type); setPreviewUrl(url); setPreviewType(type); setPreviewVisible(true); @@ -587,7 +594,7 @@ function InitialInfo() { {taskDetail?.material?.materialVideoUrl ? (
- ); -}; - -export default UnifiedFilterBar; diff --git a/video-gen-app/src/components/UploadSelector.tsx b/video-gen-app/src/components/UploadSelector.tsx index be8ce326..a2b3953c 100644 --- a/video-gen-app/src/components/UploadSelector.tsx +++ b/video-gen-app/src/components/UploadSelector.tsx @@ -30,7 +30,6 @@ interface UploadSelectorProps { usedAudioCount?: number; maxAudioDuration?: number; usedAudioDuration?: number; - hideLimitHint?: boolean; } const UploadSelector: React.FC = ({ @@ -53,7 +52,6 @@ const UploadSelector: React.FC = ({ usedAudioCount, maxAudioDuration, usedAudioDuration, - hideLimitHint, }) => { const fileInputRef = useRef(null); const [portraitPickerOpen, setPortraitPickerOpen] = useState(false); @@ -245,7 +243,6 @@ const UploadSelector: React.FC = ({ usedAudioCount={usedAudioCount} maxAudioDuration={maxAudioDuration} usedAudioDuration={usedAudioDuration} - hideLimitHint={hideLimitHint} /> { activeKey={activeKey} onChange={handleTabChange} items={items} - destroyOnHidden={false} + destroyInactiveTabPane={false} tabBarExtraContent={
{quotaText}。真人/虚拟共用,图片/视频共用;音频暂不开放。
} diff --git a/video-gen-app/src/components/privatePortrait/library/ProjectList.tsx b/video-gen-app/src/components/privatePortrait/library/ProjectList.tsx index ce31d022..45e9d992 100644 --- a/video-gen-app/src/components/privatePortrait/library/ProjectList.tsx +++ b/video-gen-app/src/components/privatePortrait/library/ProjectList.tsx @@ -13,7 +13,7 @@ interface Props { const PrivatePortraitProjectList: React.FC = ({ items, selectedId, onSelect }) => { if (!items.length) return ; return ( - + {items.map((project) => { const active = selectedId === project.id; return ( diff --git a/video-gen-app/src/components/privatePortrait/library/RealPersonLibraryPanel.tsx b/video-gen-app/src/components/privatePortrait/library/RealPersonLibraryPanel.tsx index 37479cf4..46bd87cc 100644 --- a/video-gen-app/src/components/privatePortrait/library/RealPersonLibraryPanel.tsx +++ b/video-gen-app/src/components/privatePortrait/library/RealPersonLibraryPanel.tsx @@ -122,14 +122,7 @@ const RealPersonLibraryPanel: React.FC = () => { 真人素材库 创建项目组时先完成真人认证,认证成功后项目组才会正式创建并可上传素材。
- +
@@ -162,7 +155,7 @@ const RealPersonLibraryPanel: React.FC = () => { onOk={validateSession ? undefined : handleCreate} okText="开始认证并创建" confirmLoading={creating} - mask={{ closable: !polling }} + maskClosable={!polling} > {!validateSession ? ( diff --git a/video-gen-app/src/components/privatePortrait/library/VirtualMaterialPanel.tsx b/video-gen-app/src/components/privatePortrait/library/VirtualMaterialPanel.tsx index 0fb86e6b..5a558de6 100644 --- a/video-gen-app/src/components/privatePortrait/library/VirtualMaterialPanel.tsx +++ b/video-gen-app/src/components/privatePortrait/library/VirtualMaterialPanel.tsx @@ -338,7 +338,7 @@ const VirtualMaterialPanel: React.FC = () => { @@ -392,14 +392,7 @@ const VirtualMaterialPanel: React.FC = () => { 虚拟素材库 可提前上传虚拟人像素材,后续生成视频时可直接使用。
- +
@@ -453,24 +446,11 @@ const VirtualMaterialPanel: React.FC = () => { title={selectedProject ? selectedProject.name : '素材资产'} extra={( - + {/* */} + {selectedProjectId && ( - + )} @@ -484,15 +464,14 @@ const VirtualMaterialPanel: React.FC = () => { value={keyword} onChange={(e) => setKeyword(e.target.value)} onSearch={() => loadAssets(1, assetPageSize)} - style={{ width: 240, borderRadius: 8 }} - enterButton + style={{ width: 240 }} /> { + setResourceType(value); + setPage(1); + }} + /> + } placeholder="搜索文件名或URL" + style={{ width: 240 }} onChange={(e) => setKeyword(e.target.value)} - onSearch={() => { setPage(1); load(); }} - style={{ width: 240, borderRadius: 8 }} - enterButton + onPressEnter={() => { + setPage(1); + load(); + }} /> + {/* */} + + {batchMode ? ( <> - - - + + + ) : ( - + )}
@@ -280,7 +232,7 @@ const UploadResourceHistoryPanel: React.FC = () => { const checked = selectedIds.has(item.id); return (
-
batchMode && toggle(item.id)}> +
{renderMedia(item)} {batchMode && toggle(item.id)} style={{ position: 'absolute', top: 10, left: 10, background: '#fff', borderRadius: 6, padding: 4 }} />} {typeLabel(item.resourceType)} diff --git a/video-gen-app/src/components/uploadResource/UploadResourceHistoryPicker.tsx b/video-gen-app/src/components/uploadResource/UploadResourceHistoryPicker.tsx index b061a44f..ab8cf210 100644 --- a/video-gen-app/src/components/uploadResource/UploadResourceHistoryPicker.tsx +++ b/video-gen-app/src/components/uploadResource/UploadResourceHistoryPicker.tsx @@ -26,72 +26,15 @@ interface UploadResourceHistoryPickerProps { usedAudioCount?: number; maxAudioDuration?: number; usedAudioDuration?: number; - hideLimitHint?: boolean; } const buildPreviewUrl = (url: string) => { if (!url) return ''; - if (/^data:|blob:/i.test(url)) return url; - if (/^https?:\/\//i.test(url)) { - const parsed = new URL(url); - if (parsed.pathname.startsWith('/uploads')) { - return parsed.pathname + parsed.search; - } - return url; - } + if (/^(https?:|data:|blob:)/i.test(url)) return url; const base = (import.meta.env.VITE_API_BASE || '').replace(/\/$/, ''); return `${base}${url.startsWith('/') ? '' : '/'}${url}`; }; -const validateImageDimensions = (width: number, height: number): string | null => { - if (width < 300 || width > 6000) return `图片宽度需在 300~6000px 之间,当前为 ${width}px`; - if (height < 300 || height > 6000) return `图片高度需在 300~6000px 之间,当前为 ${height}px`; - const ratio = width / height; - if (ratio < 0.4 || ratio > 2.5) return `图片宽高比需在 0.4~2.5 之间,当前为 ${ratio.toFixed(2)}`; - return null; -}; - -const validateVideoDimensions = (width: number, height: number): string | null => { - if (width < 300 || width > 6000) return `视频宽度需在 300~6000px 之间,当前为 ${width}px`; - if (height < 300 || height > 6000) return `视频高度需在 300~6000px 之间,当前为 ${height}px`; - const ratio = width / height; - if (ratio < 0.4 || ratio > 2.5) return `视频宽高比需在 0.4~2.5 之间,当前为 ${ratio.toFixed(2)}`; - const totalPixels = width * height; - if (totalPixels < 409600) return `视频总像素数过小(${width}×${height}=${totalPixels}),需 ≥ 640×640=409600`; - if (totalPixels > 8295044) return `视频总像素数过大(${width}×${height}=${totalPixels}),需 ≤ 3326×2494=8295044`; - return null; -}; - -const getItemValidationError = (item: UploadResourceHistoryItem): string | null => { - const itemAny = item as any; - const width = itemAny.width || itemAny.videoWidth || itemAny.imageWidth || 0; - const height = itemAny.height || itemAny.videoHeight || itemAny.imageHeight || 0; - - if (item.resourceType === 'image') { - if (width > 0 && height > 0) { - return validateImageDimensions(width, height); - } - } else if (item.resourceType === 'video') { - if (width > 0 && height > 0) { - return validateVideoDimensions(width, height); - } - } - return null; -}; - -const getImageDimensions = (url: string): Promise<{ width: number; height: number }> => { - return new Promise((resolve) => { - const img = new Image(); - img.onload = () => { - resolve({ width: img.width, height: img.height }); - }; - img.onerror = () => { - resolve({ width: 0, height: 0 }); - }; - img.src = url; - }); -}; - const typeIcon = (type: string) => { if (type === 'video') return ; if (type === 'audio') return ; @@ -121,7 +64,6 @@ const UploadResourceHistoryPicker: React.FC = usedAudioCount, maxAudioDuration, usedAudioDuration, - hideLimitHint, }) => { const [resourceType, setResourceType] = useState(''); const [keyword, setKeyword] = useState(''); @@ -233,42 +175,11 @@ const UploadResourceHistoryPicker: React.FC = if (open) setCheckedMap(new Map()); }, [open]); - const toggle = async (item: UploadResourceHistoryItem) => { + const toggle = (item: UploadResourceHistoryItem) => { if (selectedIdSet.has(item.id)) { message.warning('该素材已经在参考内容中'); return; } - - const itemAny = item as any; - let width = itemAny.width || itemAny.videoWidth || itemAny.imageWidth || 0; - let height = itemAny.height || itemAny.videoHeight || itemAny.imageHeight || 0; - - if (item.resourceType === 'image') { - if (width <= 0 || height <= 0) { - const preview = buildPreviewUrl(item.previewUrl || item.displayUrl || item.resourceUrl); - if (preview) { - const dims = await getImageDimensions(preview); - width = dims.width; - height = dims.height; - } - } - if (width > 0 && height > 0) { - const error = validateImageDimensions(width, height); - if (error) { - message.error(`${item.fileName || '图片'}:${error}`); - return; - } - } - } else if (item.resourceType === 'video') { - if (width > 0 && height > 0) { - const error = validateVideoDimensions(width, height); - if (error) { - message.error(`${item.fileName || '视频'}:${error}`); - return; - } - } - } - setCheckedMap((prev) => { const next = new Map(prev); if (next.has(item.id)) next.delete(item.id); @@ -338,21 +249,19 @@ const UploadResourceHistoryPicker: React.FC = /> - {!hideLimitHint && ( -
- - 还可选取图片 {selectedImages}/{maxImageCount !== undefined && usedImageCount !== undefined ? maxImageCount - usedImageCount : '-'} 张 +
+ + 还可选取图片 {selectedImages}/{maxImageCount !== undefined && usedImageCount !== undefined ? maxImageCount - usedImageCount : '-'} 张 + + + 视频还可选取 {selectedVideos}/{maxVideoCount !== undefined && usedVideoCount !== undefined ? maxVideoCount - usedVideoCount : '-'} 个({selectedVideoDuration.toFixed(1)}/{maxVideoDuration !== undefined && usedVideoDuration !== undefined ? (maxVideoDuration - usedVideoDuration).toFixed(1) : '-'}秒) + + {maxAudioCount !== undefined && ( + + 音频还可选取 {selectedAudios}/{maxAudioCount !== undefined && usedAudioCount !== undefined ? maxAudioCount - usedAudioCount : '-'} 个({selectedAudioDuration.toFixed(1)}/{maxAudioDuration !== undefined && usedAudioDuration !== undefined ? (maxAudioDuration - usedAudioDuration).toFixed(1) : '-'}秒) - - 视频还可选取 {selectedVideos}/{maxVideoCount !== undefined && usedVideoCount !== undefined ? maxVideoCount - usedVideoCount : '-'} 个({selectedVideoDuration.toFixed(1)}/{maxVideoDuration !== undefined && usedVideoDuration !== undefined ? (maxVideoDuration - usedVideoDuration).toFixed(1) : '-'}秒) - - {maxAudioCount !== undefined && ( - - 音频还可选取 {selectedAudios}/{maxAudioCount !== undefined && usedAudioCount !== undefined ? maxAudioCount - usedAudioCount : '-'} 个({selectedAudioDuration.toFixed(1)}/{maxAudioDuration !== undefined && usedAudioDuration !== undefined ? (maxAudioDuration - usedAudioDuration).toFixed(1) : '-'}秒) - - )} -
- )} + )} +
@@ -401,9 +310,7 @@ const UploadResourceHistoryPicker: React.FC =
{items.map((item) => { const checked = checkedMap.has(item.id); - const selectedDisabled = selectedIdSet.has(item.id); - const validationError = getItemValidationError(item); - const disabled = selectedDisabled || !!validationError; + const disabled = selectedIdSet.has(item.id); const preview = buildPreviewUrl(item.previewUrl || item.displayUrl || item.resourceUrl); return (
= overflow: 'hidden', boxShadow: checked ? '0 10px 24px rgba(139, 92, 246, 0.18)' : '0 6px 18px rgba(15,23,42,0.06)', }} - title={validationError ? validationError : undefined} >
{item.resourceType === 'image' ? ( @@ -434,7 +340,6 @@ const UploadResourceHistoryPicker: React.FC = {typeLabel(item.resourceType)} {item.durationSeconds ? {Number(item.durationSeconds).toFixed(1)}s : null} - {validationError && 尺寸不符} {item.fileName || item.id} @@ -447,12 +352,7 @@ const UploadResourceHistoryPicker: React.FC =
)} - {selectedDisabled &&
已选
} - {validationError && ( -
- {validationError} -
- )} + {disabled &&
已选
}
); })} diff --git a/video-gen-app/src/pages/ConsumePage.tsx b/video-gen-app/src/pages/ConsumePage.tsx index c79a8051..c030cfc1 100644 --- a/video-gen-app/src/pages/ConsumePage.tsx +++ b/video-gen-app/src/pages/ConsumePage.tsx @@ -7,9 +7,6 @@ import dayjs from 'dayjs'; const { RangePicker } = DatePicker; -// 统一按钮样式 -const btnBase: React.CSSProperties = { borderRadius: 8, fontWeight: 600 }; - const toCamelCase = (str: string) => str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()); interface ConsumptionRecord { @@ -167,24 +164,13 @@ const ConsumePage: React.FC = () => { 消耗记录
-
-
+
+
setAdvertiserId(e.target.value)} - style={{ width: 180, borderRadius: 8 }} + style={{ width: 180 }} allowClear onPressEnter={() => { setCurrentPage(1); loadData(1, pageSize); }} /> @@ -197,32 +183,25 @@ const ConsumePage: React.FC = () => { setConsumeDateRange(undefined); } }} - style={{ borderRadius: 8 }} />
-
+
- +
diff --git a/video-gen-app/src/pages/GenerateConver.tsx b/video-gen-app/src/pages/GenerateConver.tsx index 58cd61ec..5951fd67 100644 --- a/video-gen-app/src/pages/GenerateConver.tsx +++ b/video-gen-app/src/pages/GenerateConver.tsx @@ -978,7 +978,7 @@ const AIChatPage: React.FC = () => { } - // console.log(mediaReferences); + console.log(mediaReferences); // 创建用户消息对象 const newMessage: Message = { @@ -1240,7 +1240,7 @@ const AIChatPage: React.FC = () => { try { const { width, height } = await getImageDimensions(file); - const error = validateImageDimensions(width, height); + const error = validateVideoDimensions(width, height); if (error) { antdMessage.error(error); return false; @@ -1316,7 +1316,7 @@ const AIChatPage: React.FC = () => { if (isImage) { try { const { width, height } = await getImageDimensions(file); - const error = validateImageDimensions(width, height); + const error = validateVideoDimensions(width, height); if (error) { antdMessage.error(error); return false; @@ -1456,7 +1456,7 @@ const AIChatPage: React.FC = () => { if (isImage) { try { const { width, height } = await getImageDimensions(file); - const error = validateImageDimensions(width, height); + const error = validateVideoDimensions(width, height); if (error) { antdMessage.error(error); return false; @@ -2183,16 +2183,13 @@ const AIChatPage: React.FC = () => { onClick={(e) => { e.stopPropagation(); setInputValue(msg.originalPrompt || ''); - if (msg.genType) { - setMediaType(msg.genType); - } if (msg.mediaReferences && msg.mediaReferences.length > 0) { const hasFirstLastFrame = msg.mediaReferences.some((ref: any) => ref.role === 'first_frame' || ref.role === 'last_frame'); if (hasFirstLastFrame && msg.genType === 'video') { const first = msg.mediaReferences.find((ref: any) => ref.role === 'first_frame'); const last = msg.mediaReferences.find((ref: any) => ref.role === 'last_frame'); - setFirstFrame(first ? { ...first, label: first.label || '', duration: first.duration } : null); - setLastFrame(last ? { ...last, label: last.label || '', duration: last.duration } : null); + setFirstFrame(first ? { ...first, label: first.label || '' } : null); + setLastFrame(last ? { ...last, label: last.label || '' } : null); setCurrentMedia([]); setReferenceMode('first_last_frame'); } else { @@ -2202,7 +2199,6 @@ const AIChatPage: React.FC = () => { url: ref.url, label: ref.label || '', role: ref.role, - duration: ref.duration, }))); setFirstFrame(null); setLastFrame(null); @@ -2891,7 +2887,6 @@ const AIChatPage: React.FC = () => { usedAudioCount={currentMedia.filter(m => m.type === 'audio').length} maxAudioDuration={15} usedAudioDuration={currentMedia.filter(m => m.type === 'audio').reduce((sum, m) => sum + (m.duration || 0), 0)} - hideLimitHint={mediaType === 'image'} >
{ usedAudioCount={currentMedia.filter(m => m.type === 'audio').length} maxAudioDuration={15} usedAudioDuration={currentMedia.filter(m => m.type === 'audio').reduce((sum, m) => sum + (m.duration || 0), 0)} - hideLimitHint={mediaType === 'image'} >
{ const [creditRatios, setCreditRatios] = useState([]); const [cimage, setCimage] = useState([]); - const validateImageDimensions = (width: number, height: number): string | null => { - if (width < 300 || width > 6000) return `图片宽度需在 300~6000px 之间,当前为 ${width}px`; - if (height < 300 || height > 6000) return `图片高度需在 300~6000px 之间,当前为 ${height}px`; - const ratio = width / height; - if (ratio < 0.4 || ratio > 2.5) return `图片宽高比需在 0.4~2.5 之间,当前为 ${ratio.toFixed(2)}`; - return null; - }; - - const validateVideoDimensions = (width: number, height: number): string | null => { - if (width < 300 || width > 6000) return `视频宽度需在 300~6000px 之间,当前为 ${width}px`; - if (height < 300 || height > 6000) return `视频高度需在 300~6000px 之间,当前为 ${height}px`; - const ratio = width / height; - if (ratio < 0.4 || ratio > 2.5) return `视频宽高比需在 0.4~2.5 之间,当前为 ${ratio.toFixed(2)}`; - const totalPixels = width * height; - if (totalPixels < 409600) return `视频总像素数过小(${width}×${height}=${totalPixels}),需 ≥ 640×640=409600`; - if (totalPixels > 8295044) return `视频总像素数过大(${width}×${height}=${totalPixels}),需 ≤ 3326×2494=8295044`; - return null; - }; - const handlePasteUpload = async (file: File) => { const isImage = file.type.startsWith("image/"); const isVideo = file.type.startsWith("video/"); @@ -318,51 +299,23 @@ const GeneratePage: React.FC = () => { } let fileDuration = 0; if (isVideo) { - const videoInfo = await new Promise<{ duration: number; width: number; height: number }>((resolve) => { + fileDuration = await new Promise((resolve) => { const video = document.createElement("video"); video.preload = "metadata"; video.onloadedmetadata = () => { - resolve({ duration: video.duration || 0, width: video.videoWidth || 0, height: video.videoHeight || 0 }); + resolve(video.duration || 0); video.remove(); }; video.onerror = () => { - resolve({ duration: 0, width: 0, height: 0 }); + resolve(0); video.remove(); }; video.src = URL.createObjectURL(file); }); - fileDuration = videoInfo.duration; - if (videoInfo.width > 0 && videoInfo.height > 0) { - const error = validateVideoDimensions(videoInfo.width, videoInfo.height); - if (error) { - message.error(`${error}`); - return false; - } - } if (videoDuration + fileDuration > MAX_VIDEO_DURATION) { message.error(`视频总时长不能超过${MAX_VIDEO_DURATION}秒`); return false; } - } else { - const imageInfo = await new Promise<{ width: number; height: number }>((resolve) => { - const img = document.createElement('img'); - img.onload = () => { - resolve({ width: img.width, height: img.height }); - URL.revokeObjectURL(img.src); - }; - img.onerror = () => { - resolve({ width: 0, height: 0 }); - URL.revokeObjectURL(img.src); - }; - img.src = URL.createObjectURL(file); - }); - if (imageInfo.width > 0 && imageInfo.height > 0) { - const error = validateImageDimensions(imageInfo.width, imageInfo.height); - if (error) { - message.error(`${error}`); - return false; - } - } } setUploading(true); try { @@ -2015,7 +1968,7 @@ const GeneratePage: React.FC = () => { } }} rows={3} - placeholder="上传参考素材(只用做模型理解,不参与生成)、输入文字,自由组合图、文多元素。输入 @ 可引用参考内容..." + placeholder="上传参考素材、输入文字,自由组合图、文多元素。输入 @ 可引用参考内容..." maxLength={500} bordered={false} autoSize={{ minRows: 2, maxRows: 6 }} diff --git a/video-gen-app/src/pages/GeneratedRecord.tsx b/video-gen-app/src/pages/GeneratedRecord.tsx index 7fb271c6..e6f663dc 100644 --- a/video-gen-app/src/pages/GeneratedRecord.tsx +++ b/video-gen-app/src/pages/GeneratedRecord.tsx @@ -17,8 +17,6 @@ import { DeleteOutlined, LoadingOutlined, UserOutlined, - CheckOutlined, - ClearOutlined, } from '@ant-design/icons'; import { useSearchParams } from 'react-router-dom'; import { PrivatePortraitLibraryPanel } from '../components/privatePortrait'; @@ -301,7 +299,7 @@ const GeneratedRecord: React.FC = () => { folder?.file(filename, blob); successCount++; } catch (error) { - // console.warn(`文件下载失败(CORS限制): ${filename},将使用备用方式下载`); + console.warn(`文件下载失败(CORS限制): ${filename},将使用备用方式下载`); hasError = true; break; } @@ -450,7 +448,7 @@ const GeneratedRecord: React.FC = () => { const res = await getPreTestList({ page: 1, pageSize: 100 }); setPreTestTemplates(res.data?.data || res.data || []); } catch (error) { - // console.error('加载前测模板失败:', error); + console.error('加载前测模板失败:', error); } finally { setPreTestTemplatesLoading(false); } @@ -491,7 +489,7 @@ const GeneratedRecord: React.FC = () => { setOauthList(data || []); setOauthTotal(res.pagination.total || 0); } catch (error) { - // console.error('加载授权列表失败:', error); + console.error('加载授权列表失败:', error); setOauthList([]); setOauthTotal(0); } finally { @@ -515,7 +513,7 @@ const GeneratedRecord: React.FC = () => { }); setOpenTypeMap(map); } catch (error) { - // console.error('加载历史授权账户列表失败:', error); + console.error('加载历史授权账户列表失败:', error); setHistoryOAuthList([]); setOpenTypeMap({}); } finally { @@ -534,7 +532,7 @@ const GeneratedRecord: React.FC = () => { setUploadHistoryList(data || []); setUploadHistoryTotal(res.pagination?.total || res.total || 0); } catch (error) { - // console.error('加载推送历史失败:', error); + console.error('加载推送历史失败:', error); setUploadHistoryList([]); setUploadHistoryTotal(0); } finally { @@ -671,7 +669,7 @@ const GeneratedRecord: React.FC = () => { setIsPreTest('2'); setPreTestTemplate(''); } catch (error: any) { - // console.error('批量推送失败:', error); + console.error('批量推送失败:', error); message.error(error.message || '批量推送失败'); } finally { setUploading(false); @@ -701,7 +699,7 @@ const GeneratedRecord: React.FC = () => { }); message.success('文件名更新成功'); } catch (error: any) { - // console.error('文件名更新失败:', error); + console.error('文件名更新失败:', error); message.error(error.message || '文件名更新失败'); } }; @@ -735,11 +733,11 @@ const GeneratedRecord: React.FC = () => { }), })); }); - // console.log(response); + console.log(response); const successCount = response?.successCount || 0; message.success(`已更新 ${successCount} 个文件名`); } catch (error: any) { - // console.error('文件名更新失败:', error); + console.error('文件名更新失败:', error); message.error(error.message || '文件名更新失败'); } }; @@ -911,36 +909,10 @@ const GeneratedRecord: React.FC = () => { }, [filterType, filterMedia]); return (
- {/* 顶部:标签切换 */} - { - setFilterType(key as typeof filterType); - setIsSelectionMode(false); - setSelectedItems(new Set()); - }} - items={[ - { key: 'project', label: 项目记录 }, - { key: 'creation', label: 创作记录 }, - { key: 'hot_opening_replicate', label: 爆款复刻 }, - { key: 'shot_replicate', label: 拆镜复刻 }, - { key: 'private_portrait', label: 私域素材库 }, - { key: 'upload_resource', label: 历史素材 }, - ]} - size="middle" - style={{ marginBottom: 12 }} - /> - {filterType === 'private_portrait' ? ( - - ) : filterType === 'upload_resource' ? ( - - ) : ( - <> - {/* 统一筛选栏:搜索 + 日期 */} + {/* 操作栏:筛选 + 推送按钮 */}
{ borderRadius: 12, background: '#fff', border: '1px solid #f0f0f5', + justifyContent: 'space-between', }}> -
- 媒体类型: - - +
+ + + + + + + + +
- - handleDateChange(dateString || '')} - format="YYYY-MM-DD" - style={{ width: 160, borderRadius: 8, border: '1px solid #e2e8f0' }} - placeholder="选择日期" - allowClear - /> - { - const val = e.target.value; - setSearchKeyword(val); - if (!val) { - setPagebreak(prev => ({ ...prev, page: 1 })); - loadRecordList(); - } - }} - onSearch={() => { - setPagebreak(prev => ({ ...prev, page: 1 })); - loadRecordList(); - }} - style={{ width: 240, borderRadius: 8 }} - enterButton - /> - - {/* 批量操作按钮 */} + {filterType !== 'private_portrait' && filterType !== 'upload_resource' && ( +
+ {/* 多选模式按钮 */} {isSelectionMode ? ( - <> - - - - - - - ) : ( + + + + + + + ) : ( + + + )} +
+ )} +
+ {filterType === 'private_portrait' ? ( + + ) : filterType === 'upload_resource' ? ( + + ) : ( + <> + {/* Second row filter: 视频 / 图片 */} +
+
+ 媒体类型: + + + + handleDateChange(dateString || '')} + format="YYYY-MM-DD" + style={{ width: 160, borderRadius: 8, border: '1px solid #e2e8f0' }} + placeholder="选择日期" + /> + {selectedDate && ( + )} - + { + const val = e.target.value; + setSearchKeyword(val); + if (!val) { + setPagebreak(prev => ({ ...prev, page: 1 })); + loadRecordList(); + } + }} + onSearch={() => { + setPagebreak(prev => ({ ...prev, page: 1 })); + loadRecordList(); + }} + style={{ width: 240, borderRadius: 8 }} + /> + +
+
{/* Content area */} {loading ? ( @@ -1063,7 +1263,7 @@ const GeneratedRecord: React.FC = () => { }}> } - description="加载中..." + tip="加载中..." size="large" />
@@ -1208,20 +1408,18 @@ const GeneratedRecord: React.FC = () => {
{ e.stopPropagation(); @@ -1319,11 +1517,10 @@ const GeneratedRecord: React.FC = () => { left: 0, right: 0, bottom: 0, - border: '3px solid #6366f1', + border: '3px solid #10b981', borderRadius: 4, pointerEvents: 'none', zIndex: 5, - boxShadow: 'inset 0 0 0 1px rgba(99,102,241,0.2)', }} /> )}
diff --git a/video-gen-app/src/pages/HomePage.tsx b/video-gen-app/src/pages/HomePage.tsx index 578039c9..ff413f8d 100644 --- a/video-gen-app/src/pages/HomePage.tsx +++ b/video-gen-app/src/pages/HomePage.tsx @@ -206,7 +206,7 @@ const HomePage: React.FC = () => { setActiveCaseTab(firstId); // 初始请求第一个 tab 的数据 getHomeCaseButton(firstId).then((btnRes: any) => { - // console.log('caseButton:', btnRes); + console.log('caseButton:', btnRes); if (btnRes?.categories?.[0]?.assets) { setCaseAssets(btnRes.categories[0].assets); } @@ -357,7 +357,7 @@ const HomePage: React.FC = () => { { icon: , title: '爆款复刻', - description: '上传参考视频与产品图片,一键复刻爆款', + description: '上传参考视频与产品图片,一键复刻爆款视频开头', action: '立即创作', path: '/initial', }, diff --git a/video-gen-app/src/pages/InitialInfo.tsx b/video-gen-app/src/pages/InitialInfo.tsx index a5618b8c..be2b9051 100644 --- a/video-gen-app/src/pages/InitialInfo.tsx +++ b/video-gen-app/src/pages/InitialInfo.tsx @@ -14,13 +14,6 @@ function clonePlain(value: T): T { return value === undefined ? value : JSON.parse(JSON.stringify(value)); } -const buildMediaUrl = (url: string): string => { - if (!url) return ''; - if (/^https?:\/\//i.test(url)) return url; - const base = (import.meta.env.VITE_API_BASE || 'http://localhost:8000').replace(/\/$/, ''); - return `${base}${url.startsWith('/') ? '' : '/'}${url}`; -}; - function InitialInfo() { const navigate = useNavigate(); const { creatID } = useParams<{ creatID: string }>(); @@ -108,7 +101,7 @@ function InitialInfo() { }, [previewVisible, previewType]); const openPreview = (url: string, type: 'image' | 'video') => { - // console.log(url, type); + console.log(url, type); setPreviewUrl(url); setPreviewType(type); setPreviewVisible(true); @@ -594,7 +587,7 @@ function InitialInfo() { {taskDetail?.material?.materialVideoUrl ? (