diff --git a/video-gen-api/app/api/v1/__init__.py b/video-gen-api/app/api/v1/__init__.py index d26dd612..017aa2fe 100644 --- a/video-gen-api/app/api/v1/__init__.py +++ b/video-gen-api/app/api/v1/__init__.py @@ -17,9 +17,11 @@ from app.api.v1.image_engines import router as image_engines_router from app.api.v1.generation_ai import router as generation_ai_router from app.api.v1.hot_opening_replicate import router as hot_opening_replicate_router from app.api.v1.shot_replicate import router as shot_replicate_router +from app.api.v1.recent_generation import router as recent_generation_router from app.api.v1.test import router as test_router from app.api.v1.user_oauth import router as user_oauth_router from app.api.v1.user_oauth_app import router as user_oauth_app_router +from app.api.v1.user_oauth_account import router as user_oauth_account_router from app.api.v1.upload_material import router as upload_material_router from app.api.v1.pre_test_template import router as pre_test_template_router from app.api.v1.material_consumption import router as material_consumption_router @@ -46,9 +48,11 @@ api_router.include_router(image_engines_router) api_router.include_router(generation_ai_router) api_router.include_router(hot_opening_replicate_router) api_router.include_router(shot_replicate_router) +api_router.include_router(recent_generation_router) api_router.include_router(test_router) api_router.include_router(user_oauth_router) api_router.include_router(user_oauth_app_router) +api_router.include_router(user_oauth_account_router) api_router.include_router(upload_material_router) api_router.include_router(pre_test_template_router) api_router.include_router(material_consumption_router) diff --git a/video-gen-api/app/api/v1/notifications.py b/video-gen-api/app/api/v1/notifications.py index 9f52748f..114c0232 100644 --- a/video-gen-api/app/api/v1/notifications.py +++ b/video-gen-api/app/api/v1/notifications.py @@ -8,7 +8,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.dependencies import get_db, get_current_user from app.models.user import User from app.models.notification import Notification -from app.schemas.notification import NotificationOut, NotificationListOut, UnreadCountOut +from app.schemas.notification import NotificationListOut, NotificationCreditsOut, UnreadCountOut from app.services.auth import decode_access_token from app.services.notification import ( get_notifications, @@ -112,7 +112,8 @@ async def list_notifications( db: AsyncSession = Depends(get_db), ): items, total = await get_notifications(db, current_user.id, page, page_size, is_read) - return {"items": items, "total": total} + credits = NotificationCreditsOut(balance=round(float(current_user.credits or 0.0), 2)) + return {"items": items, "total": total, "credits": credits} @router.put("/{notification_id}/read") diff --git a/video-gen-api/app/api/v1/recent_generation.py b/video-gen-api/app/api/v1/recent_generation.py new file mode 100644 index 00000000..f7b7550d --- /dev/null +++ b/video-gen-api/app/api/v1/recent_generation.py @@ -0,0 +1,104 @@ +from __future__ import annotations + +from typing import Annotated + +from fastapi import APIRouter, Depends, Query +from sqlalchemy.ext.asyncio import AsyncSession + +from app.dependencies import get_current_user, get_db +from app.enums.recent_generation import RecentGenerationModuleEnum +from app.models.user import User +from app.schemas.recent_generation import RecentGenerationGroupOut +from app.services.recent_generation_service import ( + DEFAULT_RECENT_GENERATION_LIMIT, + MAX_RECENT_GENERATION_LIMIT, + list_recent_generations, +) + +router = APIRouter( + prefix="/recent-generations", + tags=["recent-generations"], +) + + +@router.get( + "", + response_model=RecentGenerationGroupOut, + summary="获取当前用户各模块最近生成记录", + description=( + "获取当前登录用户在多个生成模块下最近生成成功的图片/视频记录。" + "返回结构固定为 project、chat_ai、hot_opening_replicate、shot_replicate 四个数组。" + "modules 不传时查询全部模块;modules 可重复传参指定一个或多个模块,例如 " + "?modules=project&modules=chat_ai。" + "limit 表示每个模块最多返回多少条,默认 5 条,最大 100 条。" + "接口只查询和返回展示所需轻量字段,不返回 prompt、engine_snapshot、provider_response_json 等大字段。" + ), + responses={ + 200: { + "description": "查询成功,固定返回四个模块数组;没有数据的模块返回空数组。", + "content": { + "application/json": { + "example": { + "project": [], + "chat_ai": [], + "hot_opening_replicate": [], + "shot_replicate": [ + { + "generated_time": "2026-06-26T14:30:00", + "result_url": "https://example.com/generate/video/demo.mp4?exp=1780000000&sign=xxxx", + "cover_url": "https://example.com/generate/cover/demo.jpg?exp=1780000000&sign=xxxx", + "module": "shot_replicate", + "shot_task_set_id": "0019ef0000000000001", + "shot_segment_id": "0019ef0000000000002", + "module_project_id": "0019ef0000000000003", + "module_step_id": "0019ef0000000000004", + "generation_id": "0019ef0000000000005", + "resource_type": "video", + } + ], + } + } + }, + }, + 401: {"description": "未登录或 Token 无效"}, + 403: {"description": "账号需要先设置登录密码或无权限"}, + 422: {"description": "参数校验失败,例如 limit 超出范围或 modules 枚举值非法"}, + }, +) +async def get_recent_generations( + limit: Annotated[ + int, + Query( + ge=1, + le=MAX_RECENT_GENERATION_LIMIT, + description=( + "每个模块返回的最近生成记录数量,默认 5,最大 100。" + "例如 limit=10 表示 project/chat_ai/hot_opening_replicate/shot_replicate 每个模块最多返回 10 条。" + ), + examples=[DEFAULT_RECENT_GENERATION_LIMIT], + ), + ] = DEFAULT_RECENT_GENERATION_LIMIT, + modules: Annotated[ + list[RecentGenerationModuleEnum] | None, + Query( + description=( + "模块枚举,可不传或重复传参。" + "不传表示查询全部模块。" + "可选值:" + "project=项目生成 GenerationRecord;" + "chat_ai=AI创作 ChatGenerationTask.generation_mode=chatapi_async;" + "hot_opening_replicate=爆款开头复刻 ChatGenerationTask.generation_mode=hot_opening_replicate;" + "shot_replicate=拆镜复刻 ChatGenerationTask.generation_mode=shot_replicate。" + ), + examples=[["project", "chat_ai"]], + ), + ] = None, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> RecentGenerationGroupOut: + return await list_recent_generations( + db, + user_id=current_user.id, + modules=modules, + limit=limit, + ) \ No newline at end of file diff --git a/video-gen-api/app/api/v1/upload_material.py b/video-gen-api/app/api/v1/upload_material.py index 761dce8b..b8e6c6a2 100644 --- a/video-gen-api/app/api/v1/upload_material.py +++ b/video-gen-api/app/api/v1/upload_material.py @@ -46,6 +46,30 @@ class UpdateFileName(BaseModel): class FileNameUpdateRequest(BaseModel): filenames: list[UpdateFileName] = Field(..., description="批量修改文件名列表,格式: [{\"source_id\":\"资源id\",\"file_name\":\"文件名称\"}]") +# @router.post( +# "/batch-upload", +# summary="批量上传素材到平台", +# description="支持批量上传多个授权账户下的资源到素材库,预留下前测功能", +# ) +# async def batch_upload_material( +# current_user: User = Depends(get_current_user), +# db: AsyncSession = Depends(get_db), +# ) -> Any | dict: +# try: +# result = await upload_material_to_platform( +# ["0019ef7700c503991c9"], +# ["1856633793022992"], +# "0019f018b4b3612e184", +# db, +# current_user.id, +# None, +# ) +# return result +# except Exception as e: +# return { +# "code": 0, +# "message": str(e), +# } @router.post( "/async-batch-upload", diff --git a/video-gen-api/app/api/v1/user_oauth_account.py b/video-gen-api/app/api/v1/user_oauth_account.py new file mode 100644 index 00000000..1d29bdfd --- /dev/null +++ b/video-gen-api/app/api/v1/user_oauth_account.py @@ -0,0 +1,100 @@ +from fastapi import APIRouter, Depends, HTTPException, Query, status +from sqlalchemy.ext.asyncio import AsyncSession + +from app.dependencies import get_current_user, get_db +from app.models.user import User +from app.schemas.user_oauth_account import ( + OAuthAccountListResponse, + DeleteOAuthAccountRequest, +) +from app.services.user_oauth_account_service import ( + get_oauth_account_list, + delete_oauth_account, +) + +router = APIRouter(prefix="/oauth-account", tags=["oauth-account"]) + + +@router.get( + "/list", + summary="获取授权账户列表", + description="获取授权账户列表,支持按广告主ID、授权ID、广告账户名称筛选", +) +async def oauth_account_list( + advertiser_id: str | None = Query(None, description="广告主账户ID"), + oauth_id: str | None = Query(None, description="授权ID"), + advertiser_name: str | None = Query(None, description="广告账户名称(模糊查询)"), + page: int = Query(1, ge=1, description="页码"), + page_size: int = Query(10, ge=1, le=100, description="每页数量"), + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> dict: + """ + 获取授权账户列表 + + - **advertiser_id**: 广告主账户ID(可选) + - **oauth_id**: 授权ID(可选) + - **advertiser_name**: 广告账户名称,支持模糊查询(可选) + - **page**: 页码,默认为1 + - **page_size**: 每页数量,默认为10,最大100 + """ + try: + result = await get_oauth_account_list( + db=db, + page=page, + page_size=page_size, + advertiser_id=advertiser_id, + oauth_id=oauth_id, + advertiser_name=advertiser_name, + user_id=current_user.id, + ) + + return { + "code": 0, + "message": "查询成功", + "data": result["data"], + "pagination": { + "page": result["page"], + "page_size": result["page_size"], + "total": result["total"], + "total_pages": result["total_pages"], + }, + } + except Exception as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e), + ) + + +@router.get( + "/delete", + summary="删除授权账户", + description="软删除授权账户", +) +async def delete_oauth_account_api( + id: str = Query(..., description="授权账户表id"), + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> dict: + try: + await delete_oauth_account( + db=db, + account_id=id, + user_id=current_user.id, + ) + + return { + "code": 0, + "message": "删除成功", + } + except ValueError as e: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=str(e), + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=str(e), + ) diff --git a/video-gen-api/app/enums/__init__.py b/video-gen-api/app/enums/__init__.py index 730424e5..cbf1eff1 100644 --- a/video-gen-api/app/enums/__init__.py +++ b/video-gen-api/app/enums/__init__.py @@ -7,3 +7,6 @@ from app.enums.user import * from app.enums.credit_record import * from app.enums.token_usage import * from app.enums.generation_task import * +from app.enums.generation_status import * +from app.enums.sms import * +from app.enums.notification import * diff --git a/video-gen-api/app/enums/generation.py b/video-gen-api/app/enums/generation.py new file mode 100644 index 00000000..c4502eaf --- /dev/null +++ b/video-gen-api/app/enums/generation.py @@ -0,0 +1,49 @@ +from enum import Enum + + +class GenerationMode(str, Enum): + """生成模式。""" + STANDARD = "standard" + FAST = "fast" + + +class GenerationType(str, Enum): + """生成类型。""" + video = "video" + image = "image" + + +class ChatGenerationTaskStatus(str, Enum): + """聊天生成任务状态。""" + pending = "pending" + processing = "processing" + completed = "completed" + failed = "failed" + + +class ChatGenerationPipelineStage(str, Enum): + """聊天生成任务阶段。""" + waiting = "waiting" + prompt_optimization = "prompt_optimization" + video_generation = "video_generation" + post_processing = "post_processing" + completed = "completed" + failed = "failed" + + +class ChatGenerationTaskEventType(str, Enum): + """聊天生成任务事件类型。""" + TASK_CREATED = "TASK_CREATED" + TASK_DELETED = "TASK_DELETED" + TASK_CANCELLED = "TASK_CANCELLED" + PROMPT_OPT_STARTED = "PROMPT_OPT_STARTED" + PROMPT_OPT_COMPLETED = "PROMPT_OPT_COMPLETED" + PROMPT_OPT_FAILED = "PROMPT_OPT_FAILED" + VIDEO_GEN_STARTED = "VIDEO_GEN_STARTED" + VIDEO_GEN_COMPLETED = "VIDEO_GEN_COMPLETED" + VIDEO_GEN_FAILED = "VIDEO_GEN_FAILED" + POST_PROCESSING_STARTED = "POST_PROCESSING_STARTED" + POST_PROCESSING_COMPLETED = "POST_PROCESSING_COMPLETED" + POST_PROCESSING_FAILED = "POST_PROCESSING_FAILED" + TASK_COMPLETED = "TASK_COMPLETED" + TASK_FAILED = "TASK_FAILED" \ No newline at end of file diff --git a/video-gen-api/app/enums/generation_status.py b/video-gen-api/app/enums/generation_status.py new file mode 100644 index 00000000..81dcb791 --- /dev/null +++ b/video-gen-api/app/enums/generation_status.py @@ -0,0 +1,22 @@ +from enum import Enum + + +class GenerationStatus(str, Enum): + """生成状态。""" + prompt_optimized = "prompt_optimized" + generating = "generating" + completed = "completed" + failed = "failed" + + +class GenerationType(str, Enum): + """生成类型。""" + video = "video" + image = "image" + + +# 生成配置常量 +DURATIONS = [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] +ASPECT_RATIOS = ["16:9", "9:16", "1:1", "4:3", "3:4", "21:9"] +RESOLUTIONS = ["480p", "720p", "1080p"] +IMAGE_SIZES = ["2K", "4K"] \ No newline at end of file diff --git a/video-gen-api/app/enums/notification.py b/video-gen-api/app/enums/notification.py new file mode 100644 index 00000000..ab8be2ce --- /dev/null +++ b/video-gen-api/app/enums/notification.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +from enum import StrEnum + + +class NotificationType(StrEnum): + """通知类型。""" + + SYSTEM = "system" + CREDIT = "credit" + VIDEO = "video" + GENERATION = "generation" + PAYMENT = "payment" + RECHARGE = "recharge" + UNKNOWN = "unknown" + + +NOTIFICATION_TYPE_LABELS = { + NotificationType.SYSTEM.value: "系统通知", + NotificationType.CREDIT.value: "积分通知", + NotificationType.VIDEO.value: "视频通知", + NotificationType.GENERATION.value: "生成通知", + NotificationType.PAYMENT.value: "支付通知", + NotificationType.RECHARGE.value: "充值通知", + NotificationType.UNKNOWN.value: "未知通知", +} diff --git a/video-gen-api/app/enums/recent_generation.py b/video-gen-api/app/enums/recent_generation.py new file mode 100644 index 00000000..c568e332 --- /dev/null +++ b/video-gen-api/app/enums/recent_generation.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +from enum import StrEnum + +from app.enums.generation_task import ChatGenerationTaskStatus, GenerationMode + + +class RecentGenerationModuleEnum(StrEnum): + """最近生成记录接口支持的模块分组枚举。""" + + PROJECT = "project" + CHAT_AI = "chat_ai" + HOT_OPENING_REPLICATE = "hot_opening_replicate" + SHOT_REPLICATE = "shot_replicate" + + +class RecentGenerationResourceTypeEnum(StrEnum): + """最近生成记录接口返回的资源类型枚举。""" + + IMAGE = "image" + VIDEO = "video" + + +RECENT_GENERATION_ALL_MODULES: tuple[RecentGenerationModuleEnum, ...] = ( + RecentGenerationModuleEnum.PROJECT, + RecentGenerationModuleEnum.CHAT_AI, + RecentGenerationModuleEnum.HOT_OPENING_REPLICATE, + RecentGenerationModuleEnum.SHOT_REPLICATE, +) +"""最近生成记录接口默认查询的全部模块。""" + + +RECENT_GENERATION_CHAT_TASK_MODULES: tuple[RecentGenerationModuleEnum, ...] = ( + RecentGenerationModuleEnum.CHAT_AI, + RecentGenerationModuleEnum.HOT_OPENING_REPLICATE, + RecentGenerationModuleEnum.SHOT_REPLICATE, +) +"""来自 chat_generation_tasks 表的模块集合。""" + + +RECENT_GENERATION_MODULE_TO_TASK_MODE: dict[RecentGenerationModuleEnum, GenerationMode] = { + RecentGenerationModuleEnum.CHAT_AI: GenerationMode.CHATAPI_ASYNC, + RecentGenerationModuleEnum.HOT_OPENING_REPLICATE: GenerationMode.HOT_OPENING_REPLICATE, + RecentGenerationModuleEnum.SHOT_REPLICATE: GenerationMode.SHOT_REPLICATE, +} +"""最近生成记录模块枚举到 ChatGenerationTask.generation_mode 的映射。""" + + +RECENT_GENERATION_TASK_MODE_VALUE_TO_MODULE: dict[str, RecentGenerationModuleEnum] = { + task_mode.value: module + for module, task_mode in RECENT_GENERATION_MODULE_TO_TASK_MODE.items() +} +"""ChatGenerationTask.generation_mode 字符串值到最近生成记录模块枚举的映射。""" + + +RECENT_GENERATION_COMPLETED_STATUS = ChatGenerationTaskStatus.COMPLETED.value +"""最近生成记录只展示生成成功的数据,状态值与 ChatGenerationTaskStatus.COMPLETED 保持一致。""" \ No newline at end of file diff --git a/video-gen-api/app/enums/sms.py b/video-gen-api/app/enums/sms.py new file mode 100644 index 00000000..d364df0d --- /dev/null +++ b/video-gen-api/app/enums/sms.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class SmsScene(str, Enum): + """短信场景。""" + register = "register" + login = "login" + common = "common" + set_password = "set_password" \ No newline at end of file diff --git a/video-gen-api/app/schemas/generation.py b/video-gen-api/app/schemas/generation.py index 51d71ce5..fca8db5c 100644 --- a/video-gen-api/app/schemas/generation.py +++ b/video-gen-api/app/schemas/generation.py @@ -1,29 +1,17 @@ -from enum import Enum - from pydantic import BaseModel, Field +from app.enums.generation_status import ( + GenerationStatus, + GenerationType, + DURATIONS, + ASPECT_RATIOS, + RESOLUTIONS, + IMAGE_SIZES, +) from app.schemas.common import NaiveDatetime, NaiveDatetimeOptional from app.services.operation_log import log_operation -class GenerationStatus(str, Enum): - prompt_optimized = "prompt_optimized" - generating = "generating" - completed = "completed" - failed = "failed" - - -class GenerationType(str, Enum): - video = "video" - image = "image" - - -DURATIONS = [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] -ASPECT_RATIOS = ["16:9", "9:16", "1:1", "4:3", "3:4", "21:9"] -RESOLUTIONS = ["480p", "720p", "1080p"] -IMAGE_SIZES = ["2K", "4K"] - - class OptimizeParams(BaseModel): project_id: str prompt: str = Field(..., max_length=500) diff --git a/video-gen-api/app/schemas/notification.py b/video-gen-api/app/schemas/notification.py index 93d2f272..4ccf374f 100644 --- a/video-gen-api/app/schemas/notification.py +++ b/video-gen-api/app/schemas/notification.py @@ -1,22 +1,45 @@ -from pydantic import BaseModel +from __future__ import annotations +from pydantic import BaseModel, Field, field_validator + +from app.enums.notification import NotificationType from app.schemas.common import NaiveDatetime +class NotificationCreditsOut(BaseModel): + """通知轮询接口携带的当前用户积分信息。""" + + balance: float = Field(..., description="当前用户最新积分余额") + + class NotificationOut(BaseModel): id: str title: str content: str - type: str + type: NotificationType is_read: bool created_at: NaiveDatetime + @field_validator("type", mode="before") + @classmethod + def normalize_type(cls, value): + """兼容历史数据或后台自定义通知类型,避免响应模型校验失败。""" + if isinstance(value, NotificationType): + return value + if value is None: + return NotificationType.UNKNOWN + try: + return NotificationType(str(value)) + except ValueError: + return NotificationType.UNKNOWN + model_config = {"from_attributes": True} class NotificationListOut(BaseModel): items: list[NotificationOut] total: int + credits: NotificationCreditsOut class UnreadCountOut(BaseModel): diff --git a/video-gen-api/app/schemas/recent_generation.py b/video-gen-api/app/schemas/recent_generation.py new file mode 100644 index 00000000..b33c7069 --- /dev/null +++ b/video-gen-api/app/schemas/recent_generation.py @@ -0,0 +1,119 @@ +from __future__ import annotations + +from pydantic import BaseModel, ConfigDict, Field + +from app.enums.recent_generation import RecentGenerationModuleEnum, RecentGenerationResourceTypeEnum +from app.schemas.common import NaiveDatetimeOptional + + +class RecentGenerationItemOut(BaseModel): + """最近生成记录响应项。""" + + model_config = ConfigDict( + json_schema_extra={ + "example": { + "generated_time": "2026-06-26T14:30:00", + "result_url": "https://example.com/generate/video/demo.mp4?exp=1780000000&sign=xxxx", + "cover_url": "https://example.com/generate/cover/demo.jpg?exp=1780000000&sign=xxxx", + "module": "shot_replicate", + "shot_task_set_id": "0019ef0000000000001", + "shot_segment_id": "0019ef0000000000002", + "module_project_id": "0019ef0000000000003", + "module_step_id": "0019ef0000000000004", + "generation_id": "0019ef0000000000005", + "resource_type": "video", + } + } + ) + + generated_time: NaiveDatetimeOptional = Field( + None, + description="生成完成时间。优先使用 generated_at;历史数据 generated_at 为空时回退 updated_at,再回退 created_at。", + ) + result_url: str | None = Field( + None, + description="生成结果链接。resource_type=image 时为图片链接;resource_type=video 时为视频链接。返回前会按项目资源签名规则追加 exp/sign。", + ) + cover_url: str | None = Field( + None, + description="视频封面链接。仅视频资源通常有值;图片资源或历史无封面数据时返回 null。返回前会按项目资源签名规则追加 exp/sign。", + ) + module: RecentGenerationModuleEnum = Field( + ..., + description=( + "数据模块枚举:" + "project=项目生成 GenerationRecord;" + "chat_ai=AI创作 ChatGenerationTask.generation_mode=chatapi_async;" + "hot_opening_replicate=爆款开头复刻 ChatGenerationTask.generation_mode=hot_opening_replicate;" + "shot_replicate=拆镜复刻 ChatGenerationTask.generation_mode=shot_replicate。" + ), + ) + shot_task_set_id: str | None = Field( + None, + description="关联拆镜总任务ID。仅 shot_replicate 模块可能有值,来源 shot_replicate_segments.task_set_id;其他模块返回 null。", + ) + shot_segment_id: str | None = Field( + None, + description="关联拆镜片段ID。仅 shot_replicate 模块可能有值,来源 shot_replicate_segments.id;其他模块返回 null。", + ) + module_project_id: str | None = Field( + None, + description="通用模块项目ID。hot_opening_replicate/shot_replicate 模块可能有值,来源 module_generation_steps.project_id;project/chat_ai 返回 null。", + ) + module_step_id: str | None = Field( + None, + description="通用模块步骤ID。hot_opening_replicate/shot_replicate 模块可能有值,来源 module_generation_steps.id;project/chat_ai 返回 null。", + ) + generation_id: str = Field( + ..., + description="生成ID。project 模块为 generation_records.id;chat_ai/hot_opening_replicate/shot_replicate 模块为 chat_generation_tasks.id。", + ) + resource_type: RecentGenerationResourceTypeEnum = Field( + ..., + description="资源类型枚举:image=图片资源;video=视频资源。前端可据此决定预览组件。", + ) + + +class RecentGenerationGroupOut(BaseModel): + """最近生成记录固定分组响应。""" + + model_config = ConfigDict( + json_schema_extra={ + "example": { + "project": [], + "chat_ai": [ + { + "generated_time": "2026-06-26T14:30:00", + "result_url": "https://example.com/generate/image/demo.png?exp=1780000000&sign=xxxx", + "cover_url": None, + "module": "chat_ai", + "shot_task_set_id": None, + "shot_segment_id": None, + "module_project_id": None, + "module_step_id": None, + "generation_id": "0019ef0000000000010", + "resource_type": "image", + } + ], + "hot_opening_replicate": [], + "shot_replicate": [], + } + } + ) + + project: list[RecentGenerationItemOut] = Field( + default_factory=list, + description="项目生成最近记录数组,来源 generation_records。未查询该模块或无数据时返回空数组。", + ) + chat_ai: list[RecentGenerationItemOut] = Field( + default_factory=list, + description="AI创作最近记录数组,来源 chat_generation_tasks,条件 generation_mode=chatapi_async。未查询该模块或无数据时返回空数组。", + ) + hot_opening_replicate: list[RecentGenerationItemOut] = Field( + default_factory=list, + description="爆款开头复刻最近记录数组,来源 chat_generation_tasks,条件 generation_mode=hot_opening_replicate。未查询该模块或无数据时返回空数组。", + ) + shot_replicate: list[RecentGenerationItemOut] = Field( + default_factory=list, + description="拆镜复刻最近记录数组,来源 chat_generation_tasks,条件 generation_mode=shot_replicate。未查询该模块或无数据时返回空数组。", + ) \ No newline at end of file diff --git a/video-gen-api/app/schemas/sms.py b/video-gen-api/app/schemas/sms.py index 1c84396a..5b8dc793 100644 --- a/video-gen-api/app/schemas/sms.py +++ b/video-gen-api/app/schemas/sms.py @@ -1,13 +1,6 @@ -from enum import Enum - from pydantic import BaseModel, Field - -class SmsScene(str, Enum): - register = "register" - login = "login" - common = "common" - set_password = "set_password" +from app.enums.sms import SmsScene class SmsSendRequest(BaseModel): diff --git a/video-gen-api/app/schemas/user_oauth_account.py b/video-gen-api/app/schemas/user_oauth_account.py new file mode 100644 index 00000000..bf718830 --- /dev/null +++ b/video-gen-api/app/schemas/user_oauth_account.py @@ -0,0 +1,32 @@ +from datetime import datetime +from typing import List, Optional + +from pydantic import BaseModel, Field + + +class UserOAuthAccountOut(BaseModel): + id: str = Field(..., description="主键") + oauth_id: str = Field(..., description="授权表中的id") + advertiser_id: Optional[str] = Field(None, description="广告主账户id") + advertiser_name: Optional[str] = Field(None, description="广告账户名") + advertiser_role: Optional[str] = Field(None, description="广告账户类型") + created_at: datetime = Field(..., description="创建时间") + updated_at: datetime = Field(..., description="更新时间") + + +class PaginationInfo(BaseModel): + page: int = Field(..., description="当前页码") + page_size: int = Field(..., description="每页数量") + total: int = Field(..., description="总记录数") + total_pages: int = Field(..., description="总页数") + + +class OAuthAccountListResponse(BaseModel): + code: int = Field(0, description="返回码,0表示成功") + message: str = Field("查询成功", description="返回消息") + data: List[UserOAuthAccountOut] = Field(..., description="授权账户列表数据") + pagination: PaginationInfo = Field(..., description="分页信息") + + +class DeleteOAuthAccountRequest(BaseModel): + id: str = Field(..., description="授权账户表id") diff --git a/video-gen-api/app/services/recent_generation_service.py b/video-gen-api/app/services/recent_generation_service.py new file mode 100644 index 00000000..ee27fdca --- /dev/null +++ b/video-gen-api/app/services/recent_generation_service.py @@ -0,0 +1,407 @@ +from __future__ import annotations + +from collections.abc import Iterable +from datetime import datetime +from typing import Any, TypedDict + +from sqlalchemy import and_, func, or_, select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.enums.generation_task import GenerationType +from app.enums.recent_generation import ( + RECENT_GENERATION_ALL_MODULES, + RECENT_GENERATION_CHAT_TASK_MODULES, + RECENT_GENERATION_COMPLETED_STATUS, + RECENT_GENERATION_MODULE_TO_TASK_MODE, + RECENT_GENERATION_TASK_MODE_VALUE_TO_MODULE, + RecentGenerationModuleEnum, + RecentGenerationResourceTypeEnum, +) +from app.models.chat_generation_task import ChatGenerationTask +from app.models.generation_record import GenerationRecord +from app.models.module_generation_step import ModuleGenerationStep +from app.models.shot_replicate_segment import ShotReplicateSegment +from app.schemas.recent_generation import RecentGenerationGroupOut, RecentGenerationItemOut +from app.services.resource_signed_url_service import build_resource_signed_url + +DEFAULT_RECENT_GENERATION_LIMIT = 5 +MAX_RECENT_GENERATION_LIMIT = 100 + + +class _StepLinkInfo(TypedDict): + module_project_id: str | None + module_step_id: str | None + module: str | None + + +class _ShotLinkInfo(TypedDict): + shot_task_set_id: str | None + shot_segment_id: str | None + + +def _normalize_limit(limit: int | None) -> int: + if limit is None: + return DEFAULT_RECENT_GENERATION_LIMIT + return min(max(int(limit), 1), MAX_RECENT_GENERATION_LIMIT) + + +def _normalize_modules( + modules: Iterable[RecentGenerationModuleEnum] | None, +) -> list[RecentGenerationModuleEnum]: + if not modules: + return list(RECENT_GENERATION_ALL_MODULES) + + normalized: list[RecentGenerationModuleEnum] = [] + seen: set[RecentGenerationModuleEnum] = set() + for module in modules: + module_enum = RecentGenerationModuleEnum(module) + if module_enum not in seen: + normalized.append(module_enum) + seen.add(module_enum) + return normalized + + +def _has_url(column) -> Any: + return and_(column.is_not(None), column != "") + + +def _detect_resource_type( + gen_type: str | None, + image_url: str | None, + video_url: str | None, +) -> RecentGenerationResourceTypeEnum: + gen_type_value = (gen_type or "").strip().lower() + + if gen_type_value == GenerationType.IMAGE.value and image_url: + return RecentGenerationResourceTypeEnum.IMAGE + if gen_type_value == GenerationType.VIDEO.value and video_url: + return RecentGenerationResourceTypeEnum.VIDEO + if video_url: + return RecentGenerationResourceTypeEnum.VIDEO + return RecentGenerationResourceTypeEnum.IMAGE + + +def _sign_url(url: str | None) -> str | None: + if not url: + return None + return build_resource_signed_url(url) + + +def _build_item( + *, + generation_id: str, + module: RecentGenerationModuleEnum, + gen_type: str | None, + image_url: str | None, + video_url: str | None, + video_cover_url: str | None, + generated_time: datetime | None, + step_info: _StepLinkInfo | None = None, + shot_info: _ShotLinkInfo | None = None, +) -> RecentGenerationItemOut: + resource_type = _detect_resource_type( + gen_type=gen_type, + image_url=image_url, + video_url=video_url, + ) + + raw_result_url = video_url if resource_type == RecentGenerationResourceTypeEnum.VIDEO else image_url + raw_cover_url = video_cover_url if resource_type == RecentGenerationResourceTypeEnum.VIDEO else None + + return RecentGenerationItemOut( + generated_time=generated_time, + result_url=_sign_url(raw_result_url), + cover_url=_sign_url(raw_cover_url), + module=module, + shot_task_set_id=shot_info["shot_task_set_id"] if shot_info else None, + shot_segment_id=shot_info["shot_segment_id"] if shot_info else None, + module_project_id=step_info["module_project_id"] if step_info else None, + module_step_id=step_info["module_step_id"] if step_info else None, + generation_id=generation_id, + resource_type=resource_type, + ) + + +async def _list_project_recent_items( + db: AsyncSession, + *, + user_id: str, + limit: int, +) -> list[RecentGenerationItemOut]: + generated_time_expr = func.coalesce( + GenerationRecord.generated_at, + GenerationRecord.updated_at, + GenerationRecord.created_at, + ).label("generated_time") + + stmt = ( + select( + GenerationRecord.id.label("generation_id"), + GenerationRecord.gen_type.label("gen_type"), + GenerationRecord.image_url.label("image_url"), + GenerationRecord.video_url.label("video_url"), + GenerationRecord.video_cover_url.label("video_cover_url"), + generated_time_expr, + ) + .where( + GenerationRecord.user_id == user_id, + GenerationRecord.deleted_at.is_(None), + GenerationRecord.status == RECENT_GENERATION_COMPLETED_STATUS, + or_(_has_url(GenerationRecord.image_url), _has_url(GenerationRecord.video_url)), + ) + .order_by(generated_time_expr.desc(), GenerationRecord.created_at.desc()) + .limit(limit) + ) + + rows = (await db.execute(stmt)).mappings().all() + return [ + _build_item( + generation_id=row["generation_id"], + module=RecentGenerationModuleEnum.PROJECT, + gen_type=row["gen_type"], + image_url=row["image_url"], + video_url=row["video_url"], + video_cover_url=row["video_cover_url"], + generated_time=row["generated_time"], + ) + for row in rows + ] + + +async def _list_chat_task_recent_rows( + db: AsyncSession, + *, + user_id: str, + modules: list[RecentGenerationModuleEnum], + limit: int, +) -> list[dict[str, Any]]: + task_mode_values = [ + RECENT_GENERATION_MODULE_TO_TASK_MODE[module].value + for module in modules + if module in RECENT_GENERATION_CHAT_TASK_MODULES + ] + if not task_mode_values: + return [] + + generated_time_expr = func.coalesce( + ChatGenerationTask.generated_at, + ChatGenerationTask.updated_at, + ChatGenerationTask.created_at, + ) + + ranked_subquery = ( + select( + ChatGenerationTask.id.label("generation_id"), + ChatGenerationTask.generation_mode.label("generation_mode"), + ChatGenerationTask.gen_type.label("gen_type"), + ChatGenerationTask.image_url.label("image_url"), + ChatGenerationTask.video_url.label("video_url"), + ChatGenerationTask.video_cover_url.label("video_cover_url"), + generated_time_expr.label("generated_time"), + func.row_number() + .over( + partition_by=ChatGenerationTask.generation_mode, + order_by=(generated_time_expr.desc(), ChatGenerationTask.created_at.desc()), + ) + .label("row_num"), + ) + .where( + ChatGenerationTask.user_id == user_id, + ChatGenerationTask.deleted_at.is_(None), + ChatGenerationTask.status == RECENT_GENERATION_COMPLETED_STATUS, + ChatGenerationTask.generation_mode.in_(task_mode_values), + or_(_has_url(ChatGenerationTask.image_url), _has_url(ChatGenerationTask.video_url)), + ) + .subquery() + ) + + stmt = ( + select(ranked_subquery) + .where(ranked_subquery.c.row_num <= limit) + .order_by(ranked_subquery.c.generation_mode.asc(), ranked_subquery.c.generated_time.desc()) + ) + + return [dict(row) for row in (await db.execute(stmt)).mappings().all()] + + +async def _load_step_link_map( + db: AsyncSession, + *, + chat_task_ids: list[str], +) -> dict[str, _StepLinkInfo]: + if not chat_task_ids: + return {} + + stmt = ( + select( + ModuleGenerationStep.chat_task_id.label("chat_task_id"), + ModuleGenerationStep.id.label("module_step_id"), + ModuleGenerationStep.project_id.label("module_project_id"), + ModuleGenerationStep.module.label("module"), + ModuleGenerationStep.is_current.label("is_current"), + ModuleGenerationStep.updated_at.label("updated_at"), + ) + .where( + ModuleGenerationStep.deleted_at.is_(None), + ModuleGenerationStep.chat_task_id.in_(chat_task_ids), + ModuleGenerationStep.module.in_( + [ + RecentGenerationModuleEnum.HOT_OPENING_REPLICATE.value, + RecentGenerationModuleEnum.SHOT_REPLICATE.value, + ] + ), + ) + .order_by( + ModuleGenerationStep.chat_task_id.asc(), + ModuleGenerationStep.is_current.desc(), + ModuleGenerationStep.updated_at.desc(), + ) + ) + + link_map: dict[str, _StepLinkInfo] = {} + rows = (await db.execute(stmt)).mappings().all() + for row in rows: + chat_task_id = row["chat_task_id"] + if not chat_task_id or chat_task_id in link_map: + continue + link_map[chat_task_id] = { + "module_project_id": row["module_project_id"], + "module_step_id": row["module_step_id"], + "module": row["module"], + } + return link_map + + +async def _load_shot_link_map( + db: AsyncSession, + *, + module_project_ids: list[str], +) -> dict[str, _ShotLinkInfo]: + if not module_project_ids: + return {} + + stmt = ( + select( + ShotReplicateSegment.module_project_id.label("module_project_id"), + ShotReplicateSegment.id.label("shot_segment_id"), + ShotReplicateSegment.task_set_id.label("shot_task_set_id"), + ) + .where( + ShotReplicateSegment.deleted_at.is_(None), + ShotReplicateSegment.module_project_id.in_(module_project_ids), + ) + .order_by(ShotReplicateSegment.updated_at.desc()) + ) + + link_map: dict[str, _ShotLinkInfo] = {} + rows = (await db.execute(stmt)).mappings().all() + for row in rows: + module_project_id = row["module_project_id"] + if not module_project_id or module_project_id in link_map: + continue + link_map[module_project_id] = { + "shot_task_set_id": row["shot_task_set_id"], + "shot_segment_id": row["shot_segment_id"], + } + return link_map + + +async def _build_chat_task_group_items( + db: AsyncSession, + *, + rows: list[dict[str, Any]], +) -> dict[RecentGenerationModuleEnum, list[RecentGenerationItemOut]]: + grouped: dict[RecentGenerationModuleEnum, list[RecentGenerationItemOut]] = { + RecentGenerationModuleEnum.CHAT_AI: [], + RecentGenerationModuleEnum.HOT_OPENING_REPLICATE: [], + RecentGenerationModuleEnum.SHOT_REPLICATE: [], + } + if not rows: + return grouped + + module_task_rows: list[dict[str, Any]] = [] + module_chat_task_ids: list[str] = [] + for row in rows: + module = RECENT_GENERATION_TASK_MODE_VALUE_TO_MODULE.get(row["generation_mode"]) + if module in ( + RecentGenerationModuleEnum.HOT_OPENING_REPLICATE, + RecentGenerationModuleEnum.SHOT_REPLICATE, + ): + module_task_rows.append(row) + module_chat_task_ids.append(row["generation_id"]) + + step_link_map = await _load_step_link_map(db, chat_task_ids=module_chat_task_ids) + + shot_project_ids = [ + step_info["module_project_id"] + for row in module_task_rows + if (step_info := step_link_map.get(row["generation_id"])) + and step_info.get("module") == RecentGenerationModuleEnum.SHOT_REPLICATE.value + and step_info.get("module_project_id") + ] + shot_link_map = await _load_shot_link_map(db, module_project_ids=shot_project_ids) + + for row in rows: + module = RECENT_GENERATION_TASK_MODE_VALUE_TO_MODULE.get(row["generation_mode"]) + if not module: + continue + + step_info = step_link_map.get(row["generation_id"]) + shot_info = None + if module == RecentGenerationModuleEnum.SHOT_REPLICATE and step_info: + module_project_id = step_info.get("module_project_id") + if module_project_id: + shot_info = shot_link_map.get(module_project_id) + + grouped[module].append( + _build_item( + generation_id=row["generation_id"], + module=module, + gen_type=row["gen_type"], + image_url=row["image_url"], + video_url=row["video_url"], + video_cover_url=row["video_cover_url"], + generated_time=row["generated_time"], + step_info=step_info, + shot_info=shot_info, + ) + ) + + return grouped + + +async def list_recent_generations( + db: AsyncSession, + *, + user_id: str, + modules: Iterable[RecentGenerationModuleEnum] | None = None, + limit: int | None = None, +) -> RecentGenerationGroupOut: + """获取当前用户各模块最近生成成功的图片/视频记录。""" + + normalized_limit = _normalize_limit(limit) + normalized_modules = _normalize_modules(modules) + + response = RecentGenerationGroupOut() + + if RecentGenerationModuleEnum.PROJECT in normalized_modules: + response.project = await _list_project_recent_items( + db, + user_id=user_id, + limit=normalized_limit, + ) + + chat_modules = [module for module in normalized_modules if module in RECENT_GENERATION_CHAT_TASK_MODULES] + if chat_modules: + chat_rows = await _list_chat_task_recent_rows( + db, + user_id=user_id, + modules=chat_modules, + limit=normalized_limit, + ) + chat_grouped = await _build_chat_task_group_items(db, rows=chat_rows) + response.chat_ai = chat_grouped[RecentGenerationModuleEnum.CHAT_AI] + response.hot_opening_replicate = chat_grouped[RecentGenerationModuleEnum.HOT_OPENING_REPLICATE] + response.shot_replicate = chat_grouped[RecentGenerationModuleEnum.SHOT_REPLICATE] + + return response \ No newline at end of file diff --git a/video-gen-api/app/services/upload_material_service.py b/video-gen-api/app/services/upload_material_service.py index f571e4a9..d2019f86 100644 --- a/video-gen-api/app/services/upload_material_service.py +++ b/video-gen-api/app/services/upload_material_service.py @@ -334,8 +334,14 @@ async def _upload_to_juliang( file_content = f.read() image_signature = hashlib.md5(file_content).hexdigest() + # data = { + # "advertiser_id": advertiser_id, + # "upload_type": "UPLOAD_BY_FILE", + # "image_signature": image_signature, + # "filename": filename, + # } data = { - "advertiser_id": advertiser_id, + "local_account_id": advertiser_id, "upload_type": "UPLOAD_BY_FILE", "image_signature": image_signature, "filename": filename, @@ -345,7 +351,9 @@ async def _upload_to_juliang( "image_file": (filename, file_content, "image/png"), } - response = await douyin_api.upload_image_material(oauth_id, data, files) + # 上传本地推图片 + response = await douyin_api.upload_local_image_material(oauth_id, data, files) + #response = await douyin_api.upload_image_material(oauth_id, data, files) if response["code"] != 0: return { @@ -416,8 +424,14 @@ async def _upload_to_juliang( file_content = f.read() video_signature = hashlib.md5(file_content).hexdigest() + # data = { + # "advertiser_id": advertiser_id, + # "upload_type": "UPLOAD_BY_FILE", + # "video_signature": video_signature, + # "filename": filename, + # } data = { - "advertiser_id": advertiser_id, + "local_account_id": advertiser_id, "upload_type": "UPLOAD_BY_FILE", "video_signature": video_signature, "filename": filename, @@ -428,6 +442,7 @@ async def _upload_to_juliang( } response = await douyin_api.upload_video_material(oauth_id, data, files) + #response = await douyin_api.upload_local_video_material(oauth_id, data, files) if response["code"] != 0: return { diff --git a/video-gen-api/app/services/upload_queue.py b/video-gen-api/app/services/upload_queue.py index 73ce61f0..016bdbf3 100644 --- a/video-gen-api/app/services/upload_queue.py +++ b/video-gen-api/app/services/upload_queue.py @@ -15,6 +15,7 @@ from app.models.pre_test_template import PreTestTemplate from app.utils.id_gen import generate_id from app.utils.douyinApi import DouyinApi from app.utils.logger import get_logger +import os import hashlib import json @@ -27,7 +28,6 @@ class UploadQueue: def __init__(self): self.queue: asyncio.Queue[str] = asyncio.Queue() self.running = False - async def enqueue(self, task_id: str): """Add a task to the queue.""" await self.queue.put(task_id) @@ -277,6 +277,20 @@ async def _upload_to_juliang( else: filename = os.path.basename(storage_path) + #查询授权记录 + oauth = await db.execute( + select(UserOAuth).where( + UserOAuth.id == oauth_id, + ) + ) + oauth = oauth.scalar_one_or_none() + if not oauth: + return { + "success": False, + "error": "授权记录不存在" + } + account_role = oauth.account_role + if resource_type == "image": if resource.file_size_bytes > 5 * 1024 * 1024: return { @@ -292,17 +306,21 @@ async def _upload_to_juliang( image_signature = hashlib.md5(file_content).hexdigest() data = { - "advertiser_id": advertiser_id, "upload_type": "UPLOAD_BY_FILE", "image_signature": image_signature, "filename": filename, } - files = { "image_file": (filename, file_content, "image/png"), } - response = await douyin_api.upload_image_material(oauth_id, data, files) + #如果account_role授权角色包含:LOCAL,那么就是本地推接口,其他是广告千川接口 + if "LOCAL" in account_role: + data["local_account_id"] = advertiser_id + response = await douyin_api.upload_local_image_material(oauth_id, data, files) + else: + data["advertiser_id"] = advertiser_id + response = await douyin_api.upload_image_material(oauth_id, data, files) if response["code"] != 0: return { @@ -375,17 +393,20 @@ async def _upload_to_juliang( video_signature = hashlib.md5(file_content).hexdigest() data = { - "advertiser_id": advertiser_id, "upload_type": "UPLOAD_BY_FILE", "video_signature": video_signature, "filename": filename, } - files = { "video_file": (filename, file_content, "video/mp4"), } - response = await douyin_api.upload_video_material(oauth_id, data, files) + if "LOCAL" in account_role: + data["local_account_id"] = advertiser_id + response = await douyin_api.upload_local_video_material(oauth_id, data, files) + else: + data["advertiser_id"] = advertiser_id + response = await douyin_api.upload_video_material(oauth_id, data, files) if response["code"] != 0: return { @@ -622,4 +643,4 @@ async def _update_material_pre_test_status( ) -upload_queue = UploadQueue() \ No newline at end of file +upload_queue = UploadQueue() diff --git a/video-gen-api/app/services/user_oauth_account_service.py b/video-gen-api/app/services/user_oauth_account_service.py new file mode 100644 index 00000000..c3c0aa2b --- /dev/null +++ b/video-gen-api/app/services/user_oauth_account_service.py @@ -0,0 +1,111 @@ +from sqlalchemy import select, func +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models.user_oauth_account import UserOAuthAccount +from app.models.user_oauth import UserOAuth + + +async def get_oauth_account_list( + db: AsyncSession, + page: int = 1, + page_size: int = 20, + advertiser_id: str | None = None, + oauth_id: str | None = None, + advertiser_name: str | None = None, + user_id: str | None = None, +) -> dict: + # 构建连表查询 + query = select( + UserOAuthAccount.id, + UserOAuthAccount.advertiser_id, + UserOAuthAccount.advertiser_name, + UserOAuthAccount.advertiser_role, + UserOAuthAccount.oauth_id, + UserOAuthAccount.created_at, + UserOAuth.account_id, + UserOAuth.account_name, + UserOAuth.account_role, + UserOAuth.account_username, + UserOAuth.account_userid, + UserOAuth.open_type, + ).join( + UserOAuth, + UserOAuth.id == UserOAuthAccount.oauth_id + ).where( + UserOAuth.deleted_at.is_(None), + UserOAuthAccount.deleted_at.is_(None), + UserOAuth.user_id == user_id, # 过滤当前登录用户 + ) + + # 添加筛选条件 + if advertiser_id: + query = query.where(UserOAuthAccount.advertiser_id == advertiser_id) + if oauth_id: + query = query.where(UserOAuthAccount.oauth_id == oauth_id) + if advertiser_name: + query = query.where(UserOAuthAccount.advertiser_name.like(f"%{advertiser_name}%")) + + # 查询总数 + count_query = select(func.count()).select_from(query.subquery()) + total_result = await db.execute(count_query) + total = total_result.scalar() or 0 + + # 查询分页数据 + offset = (page - 1) * page_size + query = query.offset(offset).limit(page_size).order_by(UserOAuthAccount.created_at.desc()) + + result = await db.execute(query) + accounts = result.all() + + total_pages = (total + page_size - 1) // page_size if total > 0 else 0 + # 转换为字典列表(或 Pydantic 实例列表) + data = [ + { + "id": row.id, + "advertiser_id": row.advertiser_id, + "advertiser_name": row.advertiser_name, + "advertiser_role": row.advertiser_role, + "oauth_id": row.oauth_id, + "account_id": row.account_id, + "account_name": row.account_name, + "account_role": row.account_role, + "account_username": row.account_username, + "account_userid": row.account_userid, + "open_type": row.open_type, + "created_at": row.created_at, + } + for row in accounts + ] + return { + "data": data, + "page": page, + "page_size": page_size, + "total": total, + "total_pages": total_pages, + } + + +async def delete_oauth_account( + db: AsyncSession, + account_id: str, + user_id: str, +) -> bool: + result = await db.execute( + select(UserOAuthAccount).join( + UserOAuth, + UserOAuth.id == UserOAuthAccount.oauth_id + ).where( + UserOAuthAccount.id == account_id, + UserOAuthAccount.deleted_at.is_(None), + UserOAuth.user_id == user_id, # 过滤当前登录用户 + ) + ) + account = result.scalar_one_or_none() + if not account: + raise ValueError("授权账户不存在") + + # 软删除 + account.deleted_at = func.now() + await db.commit() + + return True diff --git a/video-gen-api/app/utils/douyinApi.py b/video-gen-api/app/utils/douyinApi.py index b09212b4..ab777ee0 100644 --- a/video-gen-api/app/utils/douyinApi.py +++ b/video-gen-api/app/utils/douyinApi.py @@ -19,6 +19,24 @@ class DouyinApi: 'GET', {'params': params or {}} ) + #上传本地推图片 + async def upload_local_image_material(self, oauth_id: str, data: Optional[Dict[str, Any]] = None, files: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + if not oauth_id: + raise RuntimeError('OAuth ID is not set.') + + url = "https://api.oceanengine.com/open_api/v3.0/local/image/upload/" + options: Dict[str, Any] = {} + if data: + options['data'] = data + if files: + options['files'] = files + return await self.request.request_with_token_with_context( + oauth_id, + url, + 'POST', + options, + request_count = 3 + ) async def upload_image_material(self, oauth_id: str, data: Optional[Dict[str, Any]] = None, files: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: if not oauth_id: @@ -57,6 +75,26 @@ class DouyinApi: request_count = 3 ) + #上传本地推视频素材 + async def upload_local_video_material(self, oauth_id: str, data: Optional[Dict[str, Any]] = None, files: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + if not oauth_id: + raise RuntimeError('OAuth ID is not set.') + + url = "https://api.oceanengine.com/open_api/v3.0/local/file/video/upload/" + options: Dict[str, Any] = {} + if data: + options['data'] = data + if files: + options['files'] = files + return await self.request.request_with_token_with_context( + oauth_id, + url, + 'POST', + options, + request_count = 3 + ) + + #获取区域信息 async def get_area(self, oauth_id: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: if not oauth_id: diff --git a/video-gen-app/dist/assets/homebtn1-DjQgGq1F.png b/video-gen-app/dist/assets/homebtn1-DjQgGq1F.png new file mode 100644 index 00000000..e384a801 Binary files /dev/null and b/video-gen-app/dist/assets/homebtn1-DjQgGq1F.png differ diff --git a/video-gen-app/dist/assets/homebtn2-BNQvBUFh.png b/video-gen-app/dist/assets/homebtn2-BNQvBUFh.png new file mode 100644 index 00000000..c9e5b320 Binary files /dev/null and b/video-gen-app/dist/assets/homebtn2-BNQvBUFh.png differ diff --git a/video-gen-app/dist/assets/homebtn3-CaXf7CQb.png b/video-gen-app/dist/assets/homebtn3-CaXf7CQb.png new file mode 100644 index 00000000..3fed85d9 Binary files /dev/null and b/video-gen-app/dist/assets/homebtn3-CaXf7CQb.png differ diff --git a/video-gen-app/dist/assets/index-Cp_h_kxu.js b/video-gen-app/dist/assets/index-Cp_h_kxu.js new file mode 100644 index 00000000..4d68a593 --- /dev/null +++ b/video-gen-app/dist/assets/index-Cp_h_kxu.js @@ -0,0 +1,653 @@ +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 ee=V(null),te=V(null),ne=V(null),W=V(null);function re(e,t){switch(U(ne,t),U(te,e),U(ee,null),t.nodeType){case 9:case 11:e=(e=t.documentElement)&&(e=e.namespaceURI)?Wd(e):0;break;default:if(e=t.tagName,t=t.namespaceURI)t=Wd(t),e=Gd(t,e);else switch(e){case`svg`:e=1;break;case`math`:e=2;break;default:e=0}}H(ee),U(ee,e)}function G(){H(ee),H(te),H(ne)}function K(e){e.memoizedState!==null&&U(W,e);var t=ee.current,n=Gd(t,e.type);t!==n&&(U(te,e),U(ee,n))}function q(e){te.current===e&&(H(ee),H(te)),W.current===e&&(H(W),tp._currentValue=R)}var ie,J;function ae(e){if(ie===void 0)try{throw Error()}catch(e){var t=e.stack.trim().match(/\n( *(at )?)/);ie=t&&t[1]||``,J=-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{oe=!1,Error.prepareStackTrace=n}return(n=e?e.displayName||e.name:``)?ae(n):``}function ce(e,t){switch(e.tag){case 26:case 27:case 5:return ae(e.type);case 16:return ae(`Lazy`);case 13:return e.child!==t&&t!==null?ae(`Suspense Fallback`):ae(`Suspense`);case 19:return ae(`SuspenseList`);case 0:case 15:return se(e.type,!1);case 11:return se(e.type.render,!1);case 1:return se(e.type,!0);case 31:return ae(`Activity`);default:return``}}function le(e){try{var t=``,n=null;do t+=ce(e,n),n=e,e=e.return;while(e);return t}catch(e){return` +Error generating stack: `+e.message+` +`+e.stack}}var ue=Object.prototype.hasOwnProperty,de=t.unstable_scheduleCallback,fe=t.unstable_cancelCallback,pe=t.unstable_shouldYield,me=t.unstable_requestPaint,he=t.unstable_now,ge=t.unstable_getCurrentPriorityLevel,_e=t.unstable_ImmediatePriority,ve=t.unstable_UserBlockingPriority,ye=t.unstable_NormalPriority,be=t.unstable_LowPriority,xe=t.unstable_IdlePriority,Se=t.log,Ce=t.unstable_setDisableYieldValue,we=null,Te=null;function Ee(e){if(typeof Se==`function`&&Ce(e),Te&&typeof Te.setStrictMode==`function`)try{Te.setStrictMode(we,e)}catch{}}var De=Math.clz32?Math.clz32:Ae,Oe=Math.log,ke=Math.LN2;function Ae(e){return e>>>=0,e===0?32:31-(Oe(e)/ke|0)|0}var je=256,Me=262144,Ne=4194304;function Pe(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 Fe(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=Pe(n))):i=Pe(o):i=Pe(s)):(r=s&~a,r===0?(o&=s,o===0?n||(n=s&~e,n!==0&&(i=Pe(n))):i=Pe(o)):i=Pe(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 Ie(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function Le(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 Re(){var e=Ne;return Ne<<=1,!(Ne&62914560)&&(Ne=4194304),e}function ze(e){for(var t=[],n=0;31>n;n++)t.push(e);return t}function Be(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function Ve(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),$t=!1;if(Qt)try{var en={};Object.defineProperty(en,`passive`,{get:function(){$t=!0}}),window.addEventListener(`test`,en,en),window.removeEventListener(`test`,en,en)}catch{$t=!1}var tn=null,nn=null,rn=null;function an(){if(rn)return rn;var e,t=nn,n=t.length,r,i=`value`in tn?tn.value:tn.textContent,a=i.length;for(e=0;e=Ln),Bn=` `,Vn=!1;function Hn(e,t){switch(e){case`keyup`:return Fn.indexOf(t.keyCode)!==-1;case`keydown`:return t.keyCode!==229;case`keypress`:case`mousedown`:case`focusout`:return!0;default:return!1}}function Un(e){return e=e.detail,typeof e==`object`&&`data`in e?e.data:null}var Wn=!1;function Gn(e,t){switch(e){case`compositionend`:return Un(t);case`keypress`:return t.which===32?(Vn=!0,Bn):null;case`textInput`:return e=t.data,e===Bn&&Vn?null:e;default:return null}}function Kn(e,t){if(Wn)return e===`compositionend`||!In&&Hn(e,t)?(e=an(),rn=nn=tn=null,Wn=!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=mr(n)}}function gr(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?gr(e,t.parentNode):`contains`in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function _r(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=Et(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=Et(e.document)}return t}function vr(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 yr=Qt&&`documentMode`in document&&11>=document.documentMode,br=null,xr=null,Sr=null,Cr=!1;function wr(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Cr||br==null||br!==Et(r)||(r=br,`selectionStart`in r&&vr(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}),Sr&&pr(Sr,r)||(Sr=r,r=Od(xr,`onSelect`),0>=o,i-=o,gi=1<<32-De(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),Ti&&vi(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),Ti&&vi(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 Ti&&vi(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)}),Ti&&vi(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&&va(l)===r.type){n(e,r.sibling),c=a(r,o.props),Ta(c,o),c.return=e,e=c;break a}n(e,r);break}else t(e,r);r=r.sibling}o.type===y?(c=ri(o.props.children,e.mode,c,o.key),c.return=e,e=c):(c=ni(o.type,o.key,o.props,null,e.mode,c),Ta(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=oi(o,e.mode,c),c.return=e,e=c}return s(e);case O:return o=va(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,wa(o),c);if(o.$$typeof===C)return b(e,r,qi(e,o),c);Ea(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=ii(o,e.mode,c),c.return=e,e=c),s(e)):n(e,r)}return function(e,t,n,r){try{Ca=0;var i=b(e,t,n,r);return Sa=null,i}catch(t){if(t===fa||t===ma)throw t;var a=Qr(29,t,null,e.mode);return a.lanes=r,a.return=e,a}}}var Oa=Da(!0),ka=Da(!1),Aa=!1;function ja(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function Ma(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 Na(e){return{lane:e,tag:0,payload:null,callback:null,next:null}}function Pa(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,Pl&2){var i=r.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),r.pending=t,t=Yr(e),Jr(e,null,n),t}return Gr(e,r,t,n),Yr(e)}function Fa(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,Ue(e,n)}}function Ia(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 La=!1;function Ra(){if(La){var e=ia;if(e!==null)throw e}}function za(e,t,n,r){La=!1;var i=e.updateQueue;Aa=!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?(Ll&f)===f:(r&f)===f){f!==0&&f===ra&&(La=!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:Aa=!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),Gl|=o,e.lanes=o,e.memoizedState=d}}function Ba(e,t){if(typeof e!=`function`)throw Error(i(191,e));e.call(t)}function X(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,Ds(e,!1,t,n);try{var c=i(),l=I.S;l!==null&&l(s,c),typeof c==`object`&&c&&typeof c.then==`function`?Es(e,t,oa(c,r),pu(e)):Es(e,t,r,pu(e))}catch(n){Es(e,t,{then:function(){},status:`rejected`,reason:n},pu())}finally{L.p=a,o!==null&&s.types!==null&&(o.types=s.types),I.T=o}}function gs(){}function _s(e,t,n,r){if(e.tag!==5)throw Error(i(476));var a=vs(e).queue;hs(e,a,t,R,n===null?gs:function(){return ys(e),n(r)})}function vs(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:Oo,lastRenderedState:R},next:null};var n={};return t.next={memoizedState:n,baseState:n,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Oo,lastRenderedState:n},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function ys(e){var t=vs(e);t.next===null&&(t=e.alternate.memoizedState),Es(e,t.next.queue,{},pu())}function bs(){return Ki(tp)}function xs(){return Co().memoizedState}function Ss(){return Co().memoizedState}function Cs(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var n=pu();e=Na(n);var r=Pa(t,e,n);r!==null&&(hu(r,t,n),Fa(r,t,n)),t={cache:$i()},e.payload=t;return}t=t.return}}function ws(e,t,n){var r=pu();n={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null},Os(e)?ks(t,n):(n=Kr(e,t,n,r),n!==null&&(hu(n,e,r),As(n,t,r)))}function Ts(e,t,n){Es(e,t,n,pu())}function Es(e,t,n,r){var i={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null};if(Os(e))ks(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,fr(s,o))return Gr(e,t,i,0),Fl===null&&Wr(),!1}catch{}if(n=Kr(e,t,i,r),n!==null)return hu(n,e,r),As(n,t,r),!0}return!1}function Ds(e,t,n,r){if(r={lane:2,revertLane:fd(),gesture:null,action:r,hasEagerState:!1,eagerState:null,next:null},Os(e)){if(t)throw Error(i(479))}else t=Kr(e,n,r,2),t!==null&&hu(t,e,2)}function Os(e){var t=e.alternate;return e===no||t!==null&&t===no}function ks(e,t){oo=ao=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function As(e,t,n){if(n&4194048){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,Ue(e,n)}}var js={readContext:Ki,use:Eo,useCallback:po,useContext:po,useEffect:po,useImperativeHandle:po,useLayoutEffect:po,useInsertionEffect:po,useMemo:po,useReducer:po,useRef:po,useState:po,useDebugValue:po,useDeferredValue:po,useTransition:po,useSyncExternalStore:po,useId:po,useHostTransitionStatus:po,useFormState:po,useActionState:po,useOptimistic:po,useMemoCache:po,useCacheRefresh:po};js.useEffectEvent=po;var Ms={readContext:Ki,use:Eo,useCallback:function(e,t){return So().memoizedState=[e,t===void 0?null:t],e},useContext:Ki,useEffect:ns,useImperativeHandle:function(e,t,n){n=n==null?null:n.concat([e]),es(4194308,4,cs.bind(null,t,e),n)},useLayoutEffect:function(e,t){return es(4194308,4,e,t)},useInsertionEffect:function(e,t){es(4,2,e,t)},useMemo:function(e,t){var n=So();t=t===void 0?null:t;var r=e();if(so){Ee(!0);try{e()}finally{Ee(!1)}}return n.memoizedState=[r,t],r},useReducer:function(e,t,n){var r=So();if(n!==void 0){var i=n(t);if(so){Ee(!0);try{n(t)}finally{Ee(!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=ws.bind(null,no,e),[r.memoizedState,e]},useRef:function(e){var t=So();return e={current:e},t.memoizedState=e},useState:function(e){e=Ro(e);var t=e.queue,n=Ts.bind(null,no,t);return t.dispatch=n,[e.memoizedState,n]},useDebugValue:us,useDeferredValue:function(e,t){return ps(So(),e,t)},useTransition:function(){var e=Ro(!1);return e=hs.bind(null,no,e.queue,!0,!1),So().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,n){var r=no,a=So();if(Ti){if(n===void 0)throw Error(i(407));n=n()}else{if(n=t(),Fl===null)throw Error(i(349));Ll&127||No(r,t,n)}a.memoizedState=n;var o={value:n,getSnapshot:t};return a.queue=o,ns(Fo.bind(null,r,o,e),[e]),r.flags|=2048,Qo(9,{destroy:void 0},Po.bind(null,r,o,n,t),null),n},useId:function(){var e=So(),t=Fl.identifierPrefix;if(Ti){var n=_i,r=gi;n=(r&~(1<<32-De(r)-1)).toString(32)+n,t=`_`+t+`R_`+n,n=co++,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[Xe]=t,o[Ze]=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(Ld(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&&Ec(t)}}return jc(t),Dc(t,t.type,e===null?null:e.memoizedProps,t.pendingProps,n),null;case 6:if(e&&t.stateNode!=null)e.memoizedProps!==r&&Ec(t);else{if(typeof r!=`string`&&t.stateNode===null)throw Error(i(166));if(e=ne.current,Mi(t)){if(e=t.stateNode,n=t.memoizedProps,r=null,a=Ci,a!==null)switch(a.tag){case 27:case 5:r=a.memoizedProps}e[Xe]=t,e=!!(e.nodeValue===n||r!==null&&!0===r.suppressHydrationWarning||Pd(e.nodeValue,n)),e||ki(t,!0)}else e=Ud(e).createTextNode(r),e[Xe]=t,t.stateNode=e}return jc(t),null;case 31:if(n=t.memoizedState,e===null||e.memoizedState!==null){if(r=Mi(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[Xe]=t}else Ni(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;jc(t),e=!1}else n=Pi(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=n),e=!0;if(!e)return t.flags&256?(Qa(t),t):(Qa(t),null);if(t.flags&128)throw Error(i(558))}return jc(t),null;case 13:if(r=t.memoizedState,e===null||e.memoizedState!==null&&e.memoizedState.dehydrated!==null){if(a=Mi(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[Xe]=t}else Ni(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;jc(t),a=!1}else a=Pi(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=a),a=!0;if(!a)return t.flags&256?(Qa(t),t):(Qa(t),null)}return Qa(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),kc(t,t.updateQueue),jc(t),null);case 4:return G(),e===null&&wd(t.stateNode.containerInfo),jc(t),null;case 10:return Bi(t.type),jc(t),null;case 19:if(H($a),r=t.memoizedState,r===null)return jc(t),null;if(a=(t.flags&128)!=0,o=r.rendering,o===null)if(a)Ac(r,!1);else{if(Wl!==0||e!==null&&e.flags&128)for(e=t.child;e!==null;){if(o=eo(e),o!==null){for(t.flags|=128,Ac(r,!1),e=o.updateQueue,t.updateQueue=e,kc(t,e),t.subtreeFlags=0,e=n,n=t.child;n!==null;)ti(n,e),n=n.sibling;return U($a,$a.current&1|2),Ti&&vi(t,r.treeForkCount),t.child}e=e.sibling}r.tail!==null&&he()>tu&&(t.flags|=128,a=!0,Ac(r,!1),t.lanes=4194304)}else{if(!a)if(e=eo(o),e!==null){if(t.flags|=128,a=!0,e=e.updateQueue,t.updateQueue=e,kc(t,e),Ac(r,!0),r.tail===null&&r.tailMode===`hidden`&&!o.alternate&&!Ti)return jc(t),null}else 2*he()-r.renderingStartTime>tu&&n!==536870912&&(t.flags|=128,a=!0,Ac(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?(jc(t),null):(e=r.tail,r.rendering=e,r.tail=e.sibling,r.renderingStartTime=he(),e.sibling=null,n=$a.current,U($a,a?n&1|2:n&1),Ti&&vi(t,r.treeForkCount),e);case 22:case 23:return Qa(t),Ga(),r=t.memoizedState!==null,e===null?r&&(t.flags|=8192):e.memoizedState!==null!==r&&(t.flags|=8192),r?n&536870912&&!(t.flags&128)&&(jc(t),t.subtreeFlags&6&&(t.flags|=8192)):jc(t),n=t.updateQueue,n!==null&&kc(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(ca),null;case 24:return n=null,e!==null&&(n=e.memoizedState.cache),t.memoizedState.cache!==n&&(t.flags|=2048),Bi(Qi),jc(t),null;case 25:return null;case 30:return null}throw Error(i(156,t.tag))}function Nc(e,t){switch(xi(t),t.tag){case 1:return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Bi(Qi),G(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 26:case 27:case 5:return q(t),null;case 31:if(t.memoizedState!==null){if(Qa(t),t.alternate===null)throw Error(i(340));Ni()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 13:if(Qa(t),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(i(340));Ni()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return H($a),null;case 4:return G(),null;case 10:return Bi(t.type),null;case 22:case 23:return Qa(t),Ga(),e!==null&&H(ca),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 24:return Bi(Qi),null;case 25:return null;default:return null}}function Pc(e,t){switch(xi(t),t.tag){case 3:Bi(Qi),G();break;case 26:case 27:case 5:q(t);break;case 4:G();break;case 31:t.memoizedState!==null&&Qa(t);break;case 13:Qa(t);break;case 19:H($a);break;case 10:Bi(t.type);break;case 22:case 23:Qa(t),Ga(),e!==null&&H(ca);break;case 24:Bi(Qi)}}function Fc(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){Gu(t,t.return,e)}}function Ic(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){Gu(i,c,e)}}}r=r.next}while(r!==a)}}catch(e){Gu(t,t.return,e)}}function Lc(e){var t=e.updateQueue;if(t!==null){var n=e.stateNode;try{X(t,n)}catch(t){Gu(e,e.return,t)}}}function Rc(e,t,n){n.props=zs(e.type,e.memoizedProps),n.state=e.memoizedState;try{n.componentWillUnmount()}catch(n){Gu(e,t,n)}}function zc(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){Gu(e,t,n)}}function Bc(e,t){var n=e.ref,r=e.refCleanup;if(n!==null)if(typeof r==`function`)try{r()}catch(n){Gu(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){Gu(e,t,n)}else n.current=null}function Vc(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){Gu(e,e.return,t)}}function Hc(e,t,n){try{var r=e.stateNode;Rd(r,e.type,n,t),r[Ze]=t}catch(t){Gu(e,e.return,t)}}function Uc(e){return e.tag===5||e.tag===3||e.tag===26||e.tag===27&&ef(e.type)||e.tag===4}function Wc(e){a:for(;;){for(;e.sibling===null;){if(e.return===null||Uc(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&&ef(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 Gc(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=Ut));else if(r!==4&&(r===27&&ef(e.type)&&(n=e.stateNode,t=null),e=e.child,e!==null))for(Gc(e,t,n),e=e.sibling;e!==null;)Gc(e,t,n),e=e.sibling}function Kc(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&&ef(e.type)&&(n=e.stateNode),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){var t=e.stateNode,n=e.memoizedProps;try{for(var r=e.type,i=t.attributes;i.length;)t.removeAttributeNode(i[0]);Ld(t,r,n),t[Xe]=e,t[Ze]=n}catch(t){Gu(e,e.return,t)}}var Jc=!1,Yc=!1,Xc=!1,Zc=typeof WeakSet==`function`?WeakSet:Set,Qc=null;function $c(e,t){if(e=e.containerInfo,Vd=up,e=_r(e),vr(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(Hd={focusedElem:e,selectionRange:n},up=!1,Qc=t;Qc!==null;)if(t=Qc,e=t.child,t.subtreeFlags&1028&&e!==null)e.return=t,Qc=e;else for(;Qc!==null;){switch(t=Qc,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`))),Ld(o,r,n),o[Xe]=e,lt(o),r=o;break a;case`link`:var s=Wf(`link`,`href`,a).get(r+(n.href||``));if(s){for(var c=0;cg&&(o=g,g=h,h=o);var _=hr(s,h),v=hr(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=lu,lu=null;var o=au,s=su;if(iu=0,ou=au=null,su=0,Pl&6)throw Error(i(331));var c=Pl;if(Pl|=4,kl(o.current),xl(o,o.current,s,n),Pl=c,ad(0,!1),Te&&typeof Te.onPostCommitFiberRoot==`function`)try{Te.onPostCommitFiberRoot(we,o)}catch{}return!0}finally{L.p=a,I.T=r,Vu(e,t)}}function Wu(e,t,n){t=ci(n,t),t=Gs(e.stateNode,t,2),e=Pa(e,t,2),e!==null&&(Be(e,2),id(e))}function Gu(e,t,n){if(e.tag===3)Wu(e,e,n);else for(;t!==null;){if(t.tag===3){Wu(t,e,n);break}else if(t.tag===1){var r=t.stateNode;if(typeof t.type.getDerivedStateFromError==`function`||typeof r.componentDidCatch==`function`&&(ru===null||!ru.has(r))){e=ci(n,e),n=Ks(2),r=Pa(t,n,2),r!==null&&(qs(n,r,t,e),Be(r,2),id(r));break}}t=t.return}}function Ku(e,t,n){var r=e.pingCache;if(r===null){r=e.pingCache=new Nl;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)||(Hl=!0,i.add(n),e=qu.bind(null,e,t,n),t.then(e,e))}function qu(e,t,n){var r=e.pingCache;r!==null&&r.delete(t),e.pingedLanes|=e.suspendedLanes&n,e.warmLanes&=~n,Fl===e&&(Ll&n)===n&&(Wl===4||Wl===3&&(Ll&62914560)===Ll&&300>he()-$l?!(Pl&2)&&Su(e,0):ql|=n,Yl===Ll&&(Yl=0)),id(e)}function Ju(e,t){t===0&&(t=Re()),e=qr(e,t),e!==null&&(Be(e,t),id(e))}function Yu(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),Ju(e,n)}function Xu(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),Ju(e,n)}function Zu(e,t){return de(e,t)}var Qu=null,$u=null,ed=!1,td=!1,nd=!1,rd=0;function id(e){e!==$u&&e.next===null&&($u===null?Qu=$u=e:$u=$u.next=e),td=!0,ed||(ed=!0,dd())}function ad(e,t){if(!nd&&td){nd=!0;do for(var n=!1,r=Qu;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-De(42|e)+1)-1,a&=i&~(o&~s),a=a&201326741?a&201326741|1:a?a|2:0}a!==0&&(n=!0,ud(r,a))}else a=Ll,a=Fe(r,r===Fl?a:0,r.cancelPendingCommit!==null||r.timeoutHandle!==-1),!(a&3)||Ie(r,a)||(n=!0,ud(r,a));r=r.next}while(n);nd=!1}}function od(){sd()}function sd(){td=ed=!1;var e=0;rd!==0&&Jd()&&(e=rd);for(var t=he(),n=null,r=Qu;r!==null;){var i=r.next,a=cd(r,t);a===0?(r.next=null,n===null?Qu=i:n.next=i,i===null&&($u=n)):(n=r,(e!==0||a&3)&&(td=!0)),r=i}iu!==0&&iu!==5||ad(e,!1),rd!==0&&(rd=0)}function cd(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&&zd(d)&&(c=c.responseEnd,o+=u*(c`u`?null:document;function wf(e,t,n){var r=Cf;if(r&&typeof t==`string`&&t){var i=Ot(t);i=`link[rel="`+e+`"][href="`+i+`"]`,typeof n==`string`&&(i+=`[crossorigin="`+n+`"]`),vf.has(i)||(vf.add(i),e={rel:e,crossOrigin:n,href:t},r.querySelector(i)===null&&(t=r.createElement(`link`),Ld(t,`link`,e),lt(t),r.head.appendChild(t)))}}function Tf(e){bf.D(e),wf(`dns-prefetch`,e,null)}function Ef(e,t){bf.C(e,t),wf(`preconnect`,e,t)}function Df(e,t,n){bf.L(e,t,n);var r=Cf;if(r&&e&&t){var i=`link[rel="preload"][as="`+Ot(t)+`"]`;t===`image`&&n&&n.imageSrcSet?(i+=`[imagesrcset="`+Ot(n.imageSrcSet)+`"]`,typeof n.imageSizes==`string`&&(i+=`[imagesizes="`+Ot(n.imageSizes)+`"]`)):i+=`[href="`+Ot(e)+`"]`;var a=i;switch(t){case`style`:a=Nf(e);break;case`script`:a=Lf(e)}_f.has(a)||(e=m({rel:`preload`,href:t===`image`&&n&&n.imageSrcSet?void 0:e,as:t},n),_f.set(a,e),r.querySelector(i)!==null||t===`style`&&r.querySelector(Pf(a))||t===`script`&&r.querySelector(Rf(a))||(t=r.createElement(`link`),Ld(t,`link`,e),lt(t),r.head.appendChild(t)))}}function Of(e,t){bf.m(e,t);var n=Cf;if(n&&e){var r=t&&typeof t.as==`string`?t.as:`script`,i=`link[rel="modulepreload"][as="`+Ot(r)+`"][href="`+Ot(e)+`"]`,a=i;switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:a=Lf(e)}if(!_f.has(a)&&(e=m({rel:`modulepreload`,href:e},t),_f.set(a,e),n.querySelector(i)===null)){switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:if(n.querySelector(Rf(a)))return}r=n.createElement(`link`),Ld(r,`link`,e),lt(r),n.head.appendChild(r)}}}function kf(e,t,n){bf.S(e,t,n);var r=Cf;if(r&&e){var i=ct(r).hoistableStyles,a=Nf(e);t||=`default`;var o=i.get(a);if(!o){var s={loading:0,preload:null};if(o=r.querySelector(Pf(a)))s.loading=5;else{e=m({rel:`stylesheet`,href:e,"data-precedence":t},n),(n=_f.get(a))&&Vf(e,n);var c=o=r.createElement(`link`);lt(c),Ld(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,Bf(o,t,r)}o={type:`stylesheet`,instance:o,count:1,state:s},i.set(a,o)}}}function Af(e,t){bf.X(e,t);var n=Cf;if(n&&e){var r=ct(n).hoistableScripts,i=Lf(e),a=r.get(i);a||(a=n.querySelector(Rf(i)),a||(e=m({src:e,async:!0},t),(t=_f.get(i))&&Hf(e,t),a=n.createElement(`script`),lt(a),Ld(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function jf(e,t){bf.M(e,t);var n=Cf;if(n&&e){var r=ct(n).hoistableScripts,i=Lf(e),a=r.get(i);a||(a=n.querySelector(Rf(i)),a||(e=m({src:e,async:!0,type:`module`},t),(t=_f.get(i))&&Hf(e,t),a=n.createElement(`script`),lt(a),Ld(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function Mf(e,t,n,r){var a=(a=ne.current)?yf(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=Nf(n.href),n=ct(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=Nf(n.href);var o=ct(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(Pf(e)))&&!o._p&&(s.instance=o,s.state.loading=5),_f.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},_f.set(e,n),o||If(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=Lf(n),n=ct(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 Nf(e){return`href="`+Ot(e)+`"`}function Pf(e){return`link[rel="stylesheet"][`+e+`]`}function Ff(e){return m({},e,{"data-precedence":e.precedence,precedence:null})}function If(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}),Ld(t,`link`,n),lt(t),e.head.appendChild(t))}function Lf(e){return`[src="`+Ot(e)+`"]`}function Rf(e){return`script[async]`+e}function zf(e,t,n){if(t.count++,t.instance===null)switch(t.type){case`style`:var r=e.querySelector(`style[data-href~="`+Ot(n.href)+`"]`);if(r)return t.instance=r,lt(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`),lt(r),Ld(r,`style`,a),Bf(r,n.precedence,e),t.instance=r;case`stylesheet`:a=Nf(n.href);var o=e.querySelector(Pf(a));if(o)return t.state.loading|=4,t.instance=o,lt(o),o;r=Ff(n),(a=_f.get(a))&&Vf(r,a),o=(e.ownerDocument||e).createElement(`link`),lt(o);var s=o;return s._p=new Promise(function(e,t){s.onload=e,s.onerror=t}),Ld(o,`link`,r),t.state.loading|=4,Bf(o,n.precedence,e),t.instance=o;case`script`:return o=Lf(n.src),(a=e.querySelector(Rf(o)))?(t.instance=a,lt(a),a):(r=n,(a=_f.get(o))&&(r=m({},n),Hf(r,a)),e=e.ownerDocument||e,a=e.createElement(`script`),lt(a),Ld(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,Bf(r,n.precedence,e));return t.instance}function Bf(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 Kf(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 qf(e){return!(e.type===`stylesheet`&&!(e.state.loading&3))}function Jf(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=Nf(r.href),a=t.querySelector(Pf(i));if(a){t=a._p,typeof t==`object`&&t&&typeof t.then==`function`&&(e.count++,e=Zf.bind(e),t.then(e,e)),n.state.loading|=4,n.instance=a,lt(a);return}a=t.ownerDocument||t,r=Ff(r),(i=_f.get(i))&&Vf(r,i),a=a.createElement(`link`),lt(a);var o=a;o._p=new Promise(function(e,t){o.onload=e,o.onerror=t}),Ld(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=Zf.bind(e),t.addEventListener(`load`,n),t.addEventListener(`error`,n))}}var Yf=0;function Xf(e,t){return e.stylesheets&&e.count===0&&$f(e,e.stylesheets),0Yf?50:800)+t);return e.unsuspend=n,function(){e.unsuspend=null,clearTimeout(r),clearTimeout(i)}}:null}function Zf(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)$f(this,this.stylesheets);else if(this.unsuspend){var e=this.unsuspend;this.unsuspend=null,e()}}}var Qf=null;function $f(e,t){e.stylesheets=null,e.unsuspend!==null&&(e.count++,Qf=new Map,t.forEach(ep,e),Qf=null,Zf.call(e))}function ep(e,t){if(!(t.state.loading&4)){var n=Qf.get(e);if(n)var r=n.get(null);else{n=new Map,Qf.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=oe((typeof t==`string`?M(t):t).pathname||`/`,n);if(a==null)return null;let o=i??R(e),s=null,c=ae(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=he([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:G(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?K(e.routesMeta.map(e=>e.childrenIndex),t.routesMeta.map(e=>e.childrenIndex)):t.score-e.score)}var H=/^:[\w-]+$/,U=3,ee=2,te=1,ne=10,W=-2,re=e=>e===`*`;function G(e,t){let n=e.split(`/`),r=n.length;return n.some(re)&&(r+=W),t&&(r+=ee),n.filter(e=>!re(e)).reduce((e,t)=>e+(H.test(t)?U:t===``?te:ne),r)}function K(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 q(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 J(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 ae(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 oe(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 se=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;function ce(e,t=`/`){let{pathname:n,search:r=``,hash:i=``}=typeof e==`string`?M(e):e,a;return n?(n=me(n),a=n.startsWith(`/`)?le(n.substring(1),`/`):le(n,t)):a=t,{pathname:a,search:ve(r),hash:ye(i)}}function le(e,t){let n=ge(t).split(`/`);return e.split(`/`).forEach(e=>{e===`..`?n.length>1&&n.pop():e!==`.`&&n.push(e)}),n.length>1?n.join(`/`):`/`}function ue(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 de(e){return e.filter((e,t)=>t===0||e.route.path&&e.route.path.length>0)}function fe(e){let t=de(e);return t.map((e,n)=>n===t.length-1?e.pathname:e.pathnameBase)}function pe(e,t,n,r=!1){let i;typeof e==`string`?i=M(e):(i={...e},E(!i.pathname||!i.pathname.includes(`?`),ue(`?`,`pathname`,`search`,i)),E(!i.pathname||!i.pathname.includes(`#`),ue(`#`,`pathname`,`hash`,i)),E(!i.search||!i.search.includes(`#`),ue(`#`,`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=ce(i,s),l=o&&o!==`/`&&o.endsWith(`/`),u=(a||o===`.`)&&n.endsWith(`/`);return!c.pathname.endsWith(`/`)&&(l||u)&&(c.pathname+=`/`),c}var me=e=>e.replace(/\/\/+/g,`/`),he=e=>me(e.join(`/`)),ge=e=>e.replace(/\/+$/,``),_e=e=>ge(e).replace(/^\/*/,`/`),ve=e=>!e||e===`?`?``:e.startsWith(`?`)?e:`?`+e,ye=e=>!e||e===`#`?``:e.startsWith(`#`)?e:`#`+e,be=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 xe(e){return e!=null&&typeof e.status==`number`&&typeof e.statusText==`string`&&typeof e.internal==`boolean`&&`data`in e}function Se(e){return he(e.map(e=>e.route.path).filter(Boolean))||`/`}var Ce=typeof window<`u`&&window.document!==void 0&&window.document.createElement!==void 0;function we(e,t){let n=e;if(typeof n!=`string`||!se.test(n))return{absoluteURL:void 0,isExternal:!1,to:n};let r=n,i=!1;if(Ce)try{let e=new URL(window.location.href),r=n.startsWith(`//`)?new URL(e.protocol+n):new URL(n),a=oe(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 Te=[`POST`,`PUT`,`PATCH`,`DELETE`];new Set(Te);var Ee=[`GET`,...Te];new Set(Ee);var De=S.createContext(null);De.displayName=`DataRouter`;var Oe=S.createContext(null);Oe.displayName=`DataRouterState`;var ke=S.createContext(!1);function Ae(){return S.useContext(ke)}var je=S.createContext({isTransitioning:!1});je.displayName=`ViewTransition`;var Me=S.createContext(new Map);Me.displayName=`Fetchers`;var Ne=S.createContext(null);Ne.displayName=`Await`;var Pe=S.createContext(null);Pe.displayName=`Navigation`;var Fe=S.createContext(null);Fe.displayName=`Location`;var Ie=S.createContext({outlet:null,matches:[],isDataRoute:!1});Ie.displayName=`Route`;var Le=S.createContext(null);Le.displayName=`RouteError`;var Re=`REACT_ROUTER_ERROR`,ze=`REDIRECT`,Be=`ROUTE_ERROR_RESPONSE`;function Ve(e){if(e.startsWith(`${Re}:${ze}:{`))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 He(e){if(e.startsWith(`${Re}:${Be}:{`))try{let t=JSON.parse(e.slice(40));if(typeof t==`object`&&t&&typeof t.status==`number`&&typeof t.statusText==`string`)return new be(t.status,t.statusText,t.data)}catch{}}function Ue(e,{relative:t}={}){E(We(),`useHref() may be used only in the context of a component.`);let{basename:n,navigator:r}=S.useContext(Pe),{hash:i,pathname:a,search:o}=$e(e,{relative:t}),s=a;return n!==`/`&&(s=a===`/`?n:he([n,a])),r.createHref({pathname:s,search:o,hash:i})}function We(){return S.useContext(Fe)!=null}function Ge(){return E(We(),`useLocation() may be used only in the context of a component.`),S.useContext(Fe).location}var Ke=`You should call navigate() in a React.useEffect(), not when your component is first rendered.`;function qe(e){S.useContext(Pe).static||S.useLayoutEffect(e)}function Je(){let{isDataRoute:e}=S.useContext(Ie);return e?vt():Ye()}function Ye(){E(We(),`useNavigate() may be used only in the context of a component.`);let e=S.useContext(De),{basename:t,navigator:n}=S.useContext(Pe),{matches:r}=S.useContext(Ie),{pathname:i}=Ge(),a=JSON.stringify(fe(r)),o=S.useRef(!1);return qe(()=>{o.current=!0}),S.useCallback((r,s={})=>{if(D(o.current,Ke),!o.current)return;if(typeof r==`number`){n.go(r);return}let c=pe(r,JSON.parse(a),i,s.relative===`path`);e==null&&t!==`/`&&(c.pathname=c.pathname===`/`?t:he([t,c.pathname])),(s.replace?n.replace:n.push)(c,s.state,s)},[t,n,a,i,e])}var Xe=S.createContext(null);function Ze(e){let t=S.useContext(Ie).outlet;return S.useMemo(()=>t&&S.createElement(Xe.Provider,{value:e},t),[t,e])}function Qe(){let{matches:e}=S.useContext(Ie);return e[e.length-1]?.params??{}}function $e(e,{relative:t}={}){let{matches:n}=S.useContext(Ie),{pathname:r}=Ge(),i=JSON.stringify(fe(n));return S.useMemo(()=>pe(e,JSON.parse(i),r,t===`path`),[e,i,r,t])}function et(e,t){return tt(e,t)}function tt(e,t,n){E(We(),`useRoutes() may be used only in the context of a component.`);let{navigator:r}=S.useContext(Pe),{matches:i}=S.useContext(Ie),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||``;bt(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=Ge(),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=ct(m&&m.map(e=>Object.assign({},e,{params:Object.assign({},o,e.params),pathname:he([c,r.encodeLocation?r.encodeLocation(e.pathname.replace(/%/g,`%25`).replace(/\?/g,`%3F`).replace(/#/g,`%23`)).pathname:e.pathname]),pathnameBase:e.pathnameBase===`/`?c:he([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(Fe.Provider,{value:{location:{pathname:`/`,search:``,hash:``,state:null,key:`default`,mask:void 0,...d},navigationType:`POP`}},h):h}function nt(){let e=_t(),t=xe(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 rt=S.createElement(nt,null),it=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=He(e.digest);t&&(e=t)}let t=e===void 0?this.props.children:S.createElement(Ie.Provider,{value:this.props.routeContext},S.createElement(Le.Provider,{value:e,children:this.props.component}));return this.context?S.createElement(ot,{error:e},t):t}};it.contextType=ke;var at=new WeakMap;function ot({children:e,error:t}){let{basename:n}=S.useContext(Pe);if(typeof t==`object`&&t&&`digest`in t&&typeof t.digest==`string`){let e=Ve(t.digest);if(e){let r=at.get(t);if(r)throw r;let i=we(e.location,n);if(Ce&&!at.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 at.set(t,n),n}return S.createElement(`meta`,{httpEquiv:`refresh`,content:`0;url=${i.absoluteURL||i.to}`})}}return e}function st({routeContext:e,match:t,children:n}){let r=S.useContext(De);return r&&r.static&&r.staticContext&&(t.route.errorElement||t.route.ErrorBoundary)&&(r.staticContext._deepestRenderedBoundaryId=t.route.id),S.createElement(Ie.Provider,{value:e},n)}function ct(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:Se(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||rt,o&&(s<0&&c===0?(bt(`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(st,{match:n,routeContext:{outlet:e,matches:m,isDataRoute:r!=null},children:t})};return r&&(n.route.ErrorBoundary||n.route.errorElement||c===0)?S.createElement(it,{location:r.location,revalidation:r.revalidation,component:f,error:u,children:h(),routeContext:{outlet:null,matches:m,isDataRoute:!0},onError:l}):h()},null)}function lt(e){return`${e} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function ut(e){let t=S.useContext(De);return E(t,lt(e)),t}function dt(e){let t=S.useContext(Oe);return E(t,lt(e)),t}function ft(e){let t=S.useContext(Ie);return E(t,lt(e)),t}function pt(e){let t=ft(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 mt(){return pt(`useRouteId`)}function ht(){return dt(`useNavigation`).navigation}function gt(){let{matches:e,loaderData:t}=dt(`useMatches`);return S.useMemo(()=>e.map(e=>L(e,t)),[e,t])}function _t(){let e=S.useContext(Le),t=dt(`useRouteError`),n=pt(`useRouteError`);return e===void 0?t.errors?.[n]:e}function vt(){let{router:e}=ut(`useNavigate`),t=pt(`useNavigate`),n=S.useRef(!1);return qe(()=>{n.current=!0}),S.useCallback(async(r,i={})=>{D(n.current,Ke),n.current&&(typeof r==`number`?await e.navigate(r):await e.navigate(r,{fromRouteId:t,...i}))},[e,t])}var yt={};function bt(e,t,n){!t&&!yt[e]&&(yt[e]=!0,D(!1,n))}S.memo(xt);function xt({routes:e,manifest:t,future:n,state:r,isStatic:i,onError:a}){return tt(e,void 0,{manifest:t,state:r,isStatic:i,onError:a,future:n})}function St({to:e,replace:t,state:n,relative:r}){E(We(),` may be used only in the context of a component.`);let{static:i}=S.useContext(Pe);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(Ie),{pathname:o}=Ge(),s=Je(),c=pe(e,fe(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 Ct(e){return Ze(e.context)}function wt(e){E(!1,`A is only ever to be used as the child of element, never rendered directly. Please wrap your in a .`)}function Tt({basename:e=`/`,children:t=null,location:n,navigationType:r=`POP`,navigator:i,static:a=!1,useTransitions:o}){E(!We(),`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=oe(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(Pe.Provider,{value:c},S.createElement(Fe.Provider,{children:t,value:h}))}function Et({children:e,location:t}){return et(Dt(e),t)}S.Component;function Dt(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,Dt(e.props.children,i));return}E(e.type===wt,`[${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=Dt(e.props.children,i)),n.push(a)}),n}var Ot=`get`,kt=`application/x-www-form-urlencoded`;function At(e){return typeof HTMLElement<`u`&&e instanceof HTMLElement}function jt(e){return At(e)&&e.tagName.toLowerCase()===`button`}function Mt(e){return At(e)&&e.tagName.toLowerCase()===`form`}function Nt(e){return At(e)&&e.tagName.toLowerCase()===`input`}function Pt(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function Ft(e,t){return e.button===0&&(!t||t===`_self`)&&!Pt(e)}function It(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 Lt(e,t){let n=It(e);return t&&t.forEach((e,r)=>{n.has(r)||t.getAll(r).forEach(e=>{n.append(r,e)})}),n}var Rt=null;function zt(){if(Rt===null)try{new FormData(document.createElement(`form`),0),Rt=!1}catch{Rt=!0}return Rt}var Bt=new Set([`application/x-www-form-urlencoded`,`multipart/form-data`,`text/plain`]);function Vt(e){return e!=null&&!Bt.has(e)?(D(!1,`"${e}" is not a valid \`encType\` for \`
\`/\`\` and will default to "${kt}"`),null):e}function Ht(e,t){let n,r,i,a,o;if(Mt(e)){let o=e.getAttribute(`action`);r=o?oe(o,t):null,n=e.getAttribute(`method`)||Ot,i=Vt(e.getAttribute(`enctype`))||kt,a=new FormData(e)}else if(jt(e)||Nt(e)&&(e.type===`submit`||e.type===`image`)){let o=e.form;if(o==null)throw Error(`Cannot submit a - {/* Contact Modal */} 联系我们} open={contactModalOpen} onCancel={() => { setContactModalOpen(false); contactForm.resetFields(); }} footer={null} width={480} + className="contact-modal" >
diff --git a/video-gen-app/src/components/NotificationPopup.tsx b/video-gen-app/src/components/NotificationPopup.tsx index 6c198f2a..2d435bc6 100644 --- a/video-gen-app/src/components/NotificationPopup.tsx +++ b/video-gen-app/src/components/NotificationPopup.tsx @@ -24,18 +24,30 @@ const NotificationPopup: React.FC = () => { const [notifications, setNotifications] = useState([]); const [currentIndex, setCurrentIndex] = useState(0); const refreshUser = useAuthStore((state) => state.refreshUser); + const setUserCredits = useAuthStore((state) => state.setUserCredits); const fetchNotifications = useCallback(async () => { try { const result = await getNotifications(1, 20, false); + // 接口返回结构:{ credits: { balance: 4317.23 }, items: [...], total: N } const data = result.items || []; + + // 同步积分:result.credits.balance 与当前 user.credits 不一致时,直接替换 + const newBalance = result?.credits?.balance; + if (typeof newBalance === 'number') { + const currentUser = useAuthStore.getState().user; + if (currentUser && currentUser.credits !== newBalance) { + setUserCredits(newBalance); + } + } + setNotifications(data); if (data.length > 0 && !visible) { setVisible(true); setCurrentIndex(0); } } catch { /* ignore */ } - }, [visible]); + }, [visible, setUserCredits]); useEffect(() => { fetchNotifications(); diff --git a/video-gen-app/src/index.css b/video-gen-app/src/index.css index 53195dae..2c679d73 100644 --- a/video-gen-app/src/index.css +++ b/video-gen-app/src/index.css @@ -200,6 +200,30 @@ html, body { .recharge-card .recharge-price { transition: all 0.3s ease; } .recharge-card:hover .recharge-price { transform: scale(1.08); } +/* ── Homepage tabs ────────────────────── */ +.homepage-tabs .ant-tabs-nav { + margin-bottom: 8px !important; +} +.homepage-tabs .ant-tabs-tab { + padding: 8px 16px !important; + font-size: 13px !important; + font-weight: 500 !important; + color: #6b7280 !important; + transition: all 0.25s !important; +} +.homepage-tabs .ant-tabs-tab:hover { + color: #6366f1 !important; +} +.homepage-tabs .ant-tabs-tab-active .ant-tabs-tab-btn { + color: #6366f1 !important; + font-weight: 600 !important; +} +.homepage-tabs .ant-tabs-ink-bar { + background: linear-gradient(90deg, #6366f1, #a855f7) !important; + height: 3px !important; + border-radius: 2px !important; +} + /* ── Project card hover ────────────────── */ .project-card { transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1) !important; @@ -440,3 +464,19 @@ html, body { /* Card grid */ .card-grid { grid-template-columns: repeat(3, 1fr) !important; } } + +/* ── HomePage Tabs ────────────── */ +.homepage-tabs .ant-tabs-nav { margin-bottom: 0; } +.homepage-tabs .ant-tabs-tab { + font-size: 14px; + font-weight: 500; + padding: 8px 20px; + margin-right: 4px; +} +.homepage-tabs .ant-tabs-tab-active { + color: #6366f1; + font-weight: 600; +} +.homepage-tabs .ant-tabs-ink-bar { + background: linear-gradient(135deg, #6366f1, #8b5cf6); +} diff --git a/video-gen-app/src/pages/GenerateConver.tsx b/video-gen-app/src/pages/GenerateConver.tsx index 1917c916..5e27e1b9 100644 --- a/video-gen-app/src/pages/GenerateConver.tsx +++ b/video-gen-app/src/pages/GenerateConver.tsx @@ -1603,14 +1603,15 @@ const AIChatPage: React.FC = () => { }} size="middle" > - + + {/*
*/} diff --git a/video-gen-app/src/pages/GeneratePage.tsx b/video-gen-app/src/pages/GeneratePage.tsx index e1fb35ca..6ef240bb 100644 --- a/video-gen-app/src/pages/GeneratePage.tsx +++ b/video-gen-app/src/pages/GeneratePage.tsx @@ -247,7 +247,7 @@ const GeneratePage: React.FC = () => { const [lastTextTokens, setLastTextTokens] = useState(0); // Media type: 1 for image, 2 for video - const [mediaType, setMediaType] = useState("image"); + const [mediaType, setMediaType] = useState("video"); // Image parameters const [selectedRatio, setSelectedRatio] = useState("1:1"); @@ -1400,7 +1400,14 @@ const GeneratePage: React.FC = () => { }; return ( -
+
{/* Header */}
{ {/* 单选按钮组 */} - {/* 图片选项 */} - - {" "} - {/* 图片图标 */} - 图片 - {/* 视频选项 */} { {/* 视频图标 */} 视频 + {/* 图片选项 */} + + {" "} + {/* 图片图标 */} + 图片 + +
{mediaType === "video" && diff --git a/video-gen-app/src/pages/HomePage.tsx b/video-gen-app/src/pages/HomePage.tsx new file mode 100644 index 00000000..7e5ced8e --- /dev/null +++ b/video-gen-app/src/pages/HomePage.tsx @@ -0,0 +1,832 @@ +import React, { useEffect, useState } from 'react'; +import { Button, Input, Tabs, Tag, Upload, message } from 'antd'; +import { + FileTextOutlined, + ScissorOutlined, + RobotOutlined, + ArrowRightOutlined, + UploadOutlined, + VideoCameraOutlined, + PictureOutlined, + ThunderboltOutlined, +} from '@ant-design/icons'; +import { useNavigate } from 'react-router-dom'; +import { getmedit } from '../api'; + +import hot from '../assets/homebtn1.png'; +import mashup from '../assets/homebtn2.png'; +import aicreate from '../assets/homebtn3.png'; + + +// 把 ISO 时间格式化成 MM-DD HH:mm(与图片一致) +function formatShortDate(iso?: string | null): string { + if (!iso) return '-'; + let s = String(iso).trim(); + if (!s.includes('T')) s = s.replace(' ', 'T'); + const dotIdx = s.indexOf('.'); + if (dotIdx > 0) s = s.slice(0, dotIdx); + s = s.replace(/[+-]\d{2}:?\d{0,2}$/, '').replace(/Z$/, ''); + // s 形如 2026-06-18T17:17:00 + const [datePart = '', timePart = ''] = s.split('T'); + const [, month = '', day = ''] = datePart.split('-'); + const hhmm = timePart.slice(0, 5); + if (!month || !day) return '-'; + return `${month}-${day} ${hhmm}`; +} + +const HomePage: React.FC = () => { + const navigate = useNavigate(); + const [activeTab, setActiveTab] = useState('project'); + const [inputValue, setInputValue] = useState(''); + const [mockVideos, setMockVideos] = useState([]); + + useEffect(() => { + const fetchAll = async () => { + try { + + getmedit(5).then((res) => { + console.log('getmedit:', res); + for (let item in res) { + res[item].forEach(element => { + // 给每条 element 标记它所属的模块(item 是接口返回的 key) + element.type = item; + // 依次追加到 mockVideos(函数式更新,保证拿到最新 state) + setMockVideos(prev => [...prev, element]); + }); + } + // 最终再打印一次汇总(state 异步更新,这里打印的是本次追加的累计长度) + setMockVideos(prev => { + console.log('mockVideos 最终条数:', prev.length); + return prev; + }); + }); + + } catch (err) { + console.error('HomePage fetch error:', err); + } + }; + fetchAll(); + }, []); + + const handleTabChange = (key: string) => { + setActiveTab(key); + }; + + + + const aiEntries = [ + { + icon: , + title: '爆款开头复刻', + description: '一键复刻热门视频开篇,快速替换自有商品素材', + action: '立即创作', + path: '/initial', + }, + { + icon: , + title: '批量混剪', + description: '多素材批量自动剪辑,智能筛选高清优质镜头片段', + action: '开始混剪', + path: '/removelens', + }, + { + icon: , + title: 'AI成片', + description: 'AI全自动快速出片,支持文案生成/上传参考素材制作', + action: '立即生成', + path: '/conversation', + }, + + ]; + + const tabs = [ + { key: 'all', label: '全部' }, + { key: 'project', label: '项目媒体' }, + { key: 'chatAi', label: 'AI成片' }, + { key: 'hotOpeningReplicate', label: '爆款复刻' }, + { key: 'shotReplicate', label: '拆镜复刻' }, + ]; + + const filteredVideos = activeTab === 'all' + ? mockVideos + : mockVideos.filter(v => v.type === activeTab); + + const materialCases = [ + 'https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=modern%20city%20skyline%20night%20view&image_size=landscape_16_9', + 'https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=nature%20forest%20landscape%20sunlight&image_size=landscape_16_9', + 'https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=abstract%20technology%20background%20digital&image_size=landscape_16_9', + 'https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=food%20cooking%20kitchen%20delicious&image_size=landscape_16_9', + 'https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=fashion%20clothing%20style%20elegant&image_size=landscape_16_9', + ]; + + return ( +
+ {/* ========== 顶部工作台引导区域(三步流程) ========== */} +
+ {/* 区域标题 */} +
+
+
+
+
+ 我的项目 +
+
+
+
+ { + navigate('/authorization') + }} + >一键授权 + + + +
+ +
+ +
+
+
+ 第1步 +
+ {/* 插图占位:项目卡(标题/描述输入框 + 行业分类 chip) */} +
+ {/* 项目名称占位 */} +
+
+
+ 项目名称... +
+
+ {/* 行业分类 chip */} +
+
美妆
+
美食
+
3C数码
+
服饰
+
+ {/* 描述占位行 */} +
+
+
+
+ 创建新项目,自定义项目名称并选择对应行业分类 +
+
+ +
+ +
+ +
+
+ 第2步 +
+ {/* 插图占位:图片/视频切换 + 尺寸/时长参数 */} +
+ {/* 图片 / 视频 切换 */} +
+
+ 视频 +
+
+ 图片 +
+
+ {/* 尺寸参数 */} +
+
尺寸比例
+
+
9:16
+
16:9
+
1:1
+
+
+ {/* 时长参数 */} +
+
时长
+
+
5s
+
10s
+
15s
+
+
+
+
+ 选择生成图片或视频,设置尺寸、时长等参数,点击生成即可一键优化提示词 +
+
+ +
+ +
+ +
+
+ 第3步 +
+ {/* 插图占位:核对清单(✓ 项)+ 一键生成按钮 */} +
+
+
+
+
提示词已智能优化
+
+
+
+
尺寸 9:16 · 时长 5s
+
+ {/*
+
!
+
参考素材 0/3
+
*/} +
+ {/* 一键生成按钮 */} +
+ + 确认并生成素材 +
+
+
+ 核对图片 / 视频参数与优化后的提示词,确认后一键生成素材 +
+
+
navigate('/projects')} + onMouseEnter={(e) => { + e.currentTarget.style.transform = 'translateY(-2px) scale(1.05)'; + e.currentTarget.style.boxShadow = '0 14px 32px rgba(99,102,241,0.45)'; + const arrow = e.currentTarget.querySelector('.round-arrow') as HTMLElement | null; + if (arrow) arrow.style.transform = 'translateX(3px)'; + }} + onMouseLeave={(e) => { + e.currentTarget.style.transform = 'translateY(0) scale(1)'; + e.currentTarget.style.boxShadow = '0 8px 20px rgba(99,102,241,0.30)'; + const arrow = e.currentTarget.querySelector('.round-arrow') as HTMLElement | null; + if (arrow) arrow.style.transform = 'translateX(0)'; + }} + style={{ + cursor: 'pointer', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + gap: 4, + width: 64, + height: 64, + borderRadius: '50%', + textAlign: 'center', + background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a855f7 100%)', + color: '#fff', + fontSize: 14, + fontWeight: 600, + letterSpacing: 0.5, + boxShadow: '0 8px 20px rgba(99,102,241,0.30)', + transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)', + userSelect: 'none', + position: 'relative', + }} + > + {/* 高光层(hover 时轻微变亮) */} + + 前往 + +
+
+ +
+ + {/* ========== AI 创作入口区域 ========== */} +
+
+
+
+ AI 创作入口 +
+
+ 一键开启智能创作 +
+
+ +
+ {aiEntries.map((entry, index) => { + // 三个入口用三种不同色调的渐变光晕作为视觉区分,但都保持白底卡片 + const accentMap = [ + { color: '#6366f1', light: 'rgba(99,102,241,0.10)', tag: '复刻', bg: hot }, + { color: '#f97316', light: 'rgba(249,115,22,0.10)', tag: '混剪', bg: mashup }, + { color: '#10b981', light: 'rgba(16,185,129,0.10)', tag: '云创', bg: aicreate }, + ]; + const accent = accentMap[index] || accentMap[0]; + return ( +
navigate(entry.path)} + className="project-card" + style={{ + flex: 1, + padding: '20px 22px', + borderRadius: 14, + background: '#fff', + border: '1px solid #e2e8f0', + cursor: 'pointer', + transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)', + position: 'relative', + overflow: 'hidden', + backgroundImage: `url(${accent.bg})`, + backgroundRepeat: 'no-repeat', + backgroundSize: '100% 100%', + backgroundPosition: 'center', + }} + onMouseEnter={(e) => { + e.currentTarget.style.borderColor = accent.color; + e.currentTarget.style.boxShadow = `0 12px 32px ${accent.light}`; + }} + onMouseLeave={(e) => { + e.currentTarget.style.borderColor = '#e2e8f0'; + e.currentTarget.style.boxShadow = 'none'; + }} + > + {/* 顶部装饰光带 */} +
+
+
+ {entry.icon} +
+
{accent.tag}
+
+
+ {entry.title} +
+
+ {entry.description} +
+
+ {entry.action} + +
+
+ ); + })} +
+
+ + {/* ========== 近期作品区域 ========== */} +
+
+
+
+
+ 近期作品 +
+
+ +
+ {/* Tab切换 */} +
+ ({ + key: tab.key, + label: tab.label, + }))} + className="homepage-tabs" + /> +
+ + {/* 视频网格 */} +
+ {filteredVideos.length === 0 ? ( +
+ +
暂无作品
+
+ ) : filteredVideos.map((video) => ( +
{ + // 按模块分发跳转: + // - 爆款复刻(hot)→ 复刻详情页 + // - AI 成片(ai)→ 对话/生成页 + // - 项目记录(project)→ 项目详情页 + const id = video.moduleProjectId; + if (video.type === 'hotOpeningReplicate' && id != null) { + navigate(`/initial/${id}/initialinfo`); + } else if (video.type === 'shotReplicate' && id != null) { + navigate(`/removelens/${id}/removefenbu`); + } else if (video.type === 'chatAi') { + navigate(`/conversation`); + } else if (video.type === 'project') { + navigate(`/project`); + } + }} + style={{ + borderRadius: 12, + overflow: 'hidden', + cursor: 'pointer', + background: '#fff', + border: '1px solid #e2e8f0', + }} + > +
+ {(() => { + // 1) 读取后端 API 基础地址;环境变量未配置时降级到本地 8000 + const apiBase = (import.meta.env.VITE_API_BASE as string) || 'http://localhost:8000'; + + // 2) 判断当前作品是否为"图片": + // - 爆款复刻(type === 'hot')始终是视频,不参与图片判断 + // - 其他模块(项目记录 / AI 成片)根据 genType 判定 + // - genType 可能是字符串 'image',也可能是数字 1(兼容两种后端约定) + const isImage = video.type !== 'hotOpeningReplicate' + && ( + String(video.resourceType ?? '').toLowerCase() === 'image' + || video.resourceType === 1 + || String(video.resourceType ?? '') === '1' + ); + + // 3) 根据媒体类型选择对应的资源路径: + // - 图片:后端返回的 imageUrl 已经是带签名的完整相对路径 + // 形如 /static/generate/images/2026/06/26/0019f017f5924df4123.png?exp=...&sign=...&w=300&p=50 + // - 视频:使用视频封面 videoCoverUrl(这是视频作品的静态缩略图) + // - 爆款复刻(type === 'hot')特殊处理:使用 finalVideoCoverUrl + let rawPath = ''; + if (isImage) { + rawPath = '/static' + video.resultUrl + '&w=300&p=50' || ''; + } else if (video.type === 'hot') { + rawPath = video.coverUrl || video.resultUrl || video.resultUrl || ''; + } else { + rawPath = video.coverUrl || video.resultUrl || video.resultUrl || ''; + } + + // 4) 拼装最终 src: + // - rawPath 为空 → 用空串(让 走 onError 兜底) + // - 已经是 http(s) 完整 URL → 直接使用(OSS / CDN 场景) + // - 否则视为后端相对路径,前面拼 apiBase + const src = rawPath + ? (rawPath.startsWith('http') ? rawPath : apiBase + rawPath) + : ''; + + return ( + {video.title + ); + })()} + {(() => { + const isImage = String(video.resourceType ?? '').toLowerCase() === 'image'; + if (isImage) return null; + return ( +
+ +
+ ); + })()} +
+
+ {/*
+ {video.title || video.name || video.originalPrompt || video.prompt || '未命名作品'} +
*/} +
+ {(() => { + // 显示所属模块,而非媒体类型 + const moduleMap: Record = { + project: '项目媒体', + chatAi: 'AI成片', + hotOpeningReplicate: '爆款复刻', + shotReplicate: '拆镜复刻', + }; + const moduleLabel = moduleMap[video.type] || '其他'; + return ( + + {moduleLabel} + + ); + })()} + · + {formatShortDate(video.generatedTime)} +
+
+
+ ))} +
+
+ + + + + {/* ========== 素材案例区域 ========== */} +
+
+
+
+
+ 素材案例 +
+
+ 精选优质作品参考 +
+
+
+ 更多案例 + +
+
+ +
+ {materialCases.map((caseUrl, index) => ( +
{ + const overlay = e.currentTarget.querySelector('.case-overlay') as HTMLElement | null; + if (overlay) overlay.style.opacity = '1'; + }} + onMouseLeave={(e) => { + const overlay = e.currentTarget.querySelector('.case-overlay') as HTMLElement | null; + if (overlay) overlay.style.opacity = '0'; + }} + > + {`素材案例 + {/* 案例悬停遮罩(由父级 hover 触发) */} +
+ 案例 {index + 1} +
+
+ ))} +
+
+
+ ); +}; + +export default HomePage; diff --git a/video-gen-app/src/pages/LoginPage.css b/video-gen-app/src/pages/LoginPage.css new file mode 100644 index 00000000..eb63c84d --- /dev/null +++ b/video-gen-app/src/pages/LoginPage.css @@ -0,0 +1,377 @@ +.login-page { + min-height: 100vh; + display: flex; + flex-direction: column; + background-image: url(/backimage.png); + background-size: cover; + background-position: center; + background-repeat: no-repeat; + background-attachment: fixed; + position: relative; + overflow-x: hidden; +} + +@media (min-width: 900px) { + .login-page { + flex-direction: row; + } +} + +.login-bg-overlay { + position: absolute; + inset: 0; + background: linear-gradient(135deg, rgba(240,244,255,0.9) 0%, rgba(232,236,248,0.85) 40%, rgba(240,240,255,0.9) 70%, rgba(248,249,255,0.95) 100%); + z-index: 0; +} + +.login-decoration { + position: absolute; + border-radius: 50%; + filter: blur(40px); + z-index: 0; +} + +.login-decoration-1 { + width: 500px; + height: 500px; + background: radial-gradient(circle, rgba(99,102,241,0.08) 0%, transparent 70%); + top: -150px; + right: -100px; +} + +.login-decoration-2 { + width: 400px; + height: 400px; + background: radial-gradient(circle, rgba(139,92,246,0.06) 0%, transparent 70%); + bottom: -100px; + left: -80px; + filter: blur(50px); +} + +.login-left-section { + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + padding: 32px 16px; + z-index: 1; +} + +@media (min-width: 900px) { + .login-left-section { + padding: 0 80px; + } +} + +.login-left-content { + width: 100%; +} + +.login-logo-row { + display: flex; + align-items: center; + gap: 14px; + margin-bottom: 20px; +} + +@media (max-width: 480px) { + .login-logo-row { + justify-content: center; + margin-bottom: 16px; + } +} + +.login-logo-img { + width: 52px; + height: 52px; + border-radius: 14px; + objectFit: contain; +} + +.login-logo-placeholder { + width: 52px; + height: 52px; + border-radius: 14px; + background: linear-gradient(135deg, #6366f1, #8b5cf6); + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 8px 24px rgba(99,102,241,0.25); +} + +.login-site-name { + color: #1e293b; + font-size: 28px; + font-weight: 800; + letter-spacing: -0.5px; +} + +@media (max-width: 480px) { + .login-site-name { + font-size: 24px; + } +} + +.login-desc { + color: #64748b !important; + font-size: 17px !important; + max-width: 480px; + line-height: 1.8 !important; + margin: 0 !important; +} + +@media (max-width: 899px) { + .login-desc { + display: none !important; + } +} + +.login-features-list { + display: none; + flex-direction: column; + gap: 20px; +} + +@media (min-width: 900px) { + .login-features-list { + display: flex; + } +} + +.login-feature-card { + position: relative; + display: flex; + gap: 16px; + align-items: flex-start; + padding: 18px 22px; + border-radius: 14px; + background: rgba(255,255,255,0.85); + backdrop-filter: blur(12px); + overflow: hidden; +} + +.login-feature-icon { + width: 44px; + height: 44px; + border-radius: 12px; + flex-shrink: 0; + background: linear-gradient(135deg, rgba(99,102,241,0.15), rgba(139,92,246,0.15)); + display: flex; + align-items: center; + justify-content: center; + color: #6366f1; + font-size: 20px; +} + +.login-feature-title { + color: #1e293b !important; + font-size: 15px !important; + font-weight: 600 !important; + display: block !important; + margin-bottom: 4px !important; +} + +.login-feature-desc { + color: #64748b !important; + font-size: 13px !important; + line-height: 1.6 !important; +} + +.login-right-section { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + z-index: 1; + padding: 16px 16px 32px; +} + +@media (min-width: 900px) { + .login-right-section { + padding: 40px 24px; + } +} + +.login-card { + width: 100%; + max-width: 440px; + border-radius: 20px !important; + box-shadow: 0 20px 60px rgba(0,0,0,0.08) !important; + border: 1px solid #e2e8f0 !important; + background: #fff !important; +} + +@media (max-width: 480px) { + .login-card { + border-radius: 16px !important; + } + .login-card .ant-card-body { + padding: 24px 20px !important; + } +} + +.login-card-title { + text-align: center !important; + margin-bottom: 6px !important; + color: #1e293b !important; + font-weight: 700 !important; +} + +.login-card-subtitle { + display: block; + text-align: center; + margin-bottom: 28px; + color: #94a3b8; + font-size: 14px; +} + +.login-tabs { + display: flex; + gap: 0; + margin-bottom: 24px; + background: #f1f5f9; + border-radius: 10px; + padding: 4px; + border: 1px solid #e2e8f0; +} + +.login-tab { + flex: 1; + text-align: center; + padding: 10px 0; + border-radius: 8px; + cursor: pointer; + font-size: 14px; + font-weight: 400; + color: #64748b; + background: transparent; + border: 1px solid transparent; + transition: all 0.3s ease; +} + +.login-tab-active { + font-weight: 600 !important; + color: #6366f1 !important; + background: #fff !important; + border: 1px solid rgba(99,102,241,0.2) !important; + box-shadow: 0 2px 8px rgba(99,102,241,0.1) !important; +} + +.login-submit-btn { + height: 48px !important; + border-radius: 10px !important; + font-size: 16px !important; + font-weight: 600 !important; + background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%) !important; + border: none !important; + box-shadow: 0 8px 24px rgba(99,102,241,0.25) !important; +} + +.login-code-btn { + height: 48px !important; + border-radius: 0 10px 10px 0 !important; + border: 1.5px solid #e2e8f0 !important; + border-left: none !important; + font-weight: 600 !important; + min-width: 100px !important; +} + +.login-agreement { + margin-bottom: 16px; +} + +.login-agreement-text { + font-size: 13px; + color: #64748b; +} + +.login-link { + color: #6366f1; + cursor: pointer; +} + +.login-footer { + display: flex; + justify-content: center; +} + +@media (min-width: 480px) { + .login-footer { + justify-content: flex-start; + } +} + +.login-switch-btn { + font-size: 13px !important; + color: #6366f1 !important; + cursor: pointer; +} + +.login-page .ant-input-affix-wrapper { + padding: 0 11px !important; + height: 48px !important; +} + +.login-page .ant-input-affix-wrapper .ant-input-prefix { + margin-right: 10px !important; + margin-left: 0 !important; +} + +.login-page .ant-input { + padding-left: 11px !important; + height: 48px !important; +} + +.login-code-btn { + height: 48px !important; +} + +@media (max-width: 767px) { + .login-left-section { + padding: 28px 16px 12px; + } + + .login-right-section { + padding: 12px 16px 32px; + } + + .login-card { + max-width: 100%; + } +} + +@media (max-width: 480px) { + .login-left-section { + padding: 24px 12px 8px; + text-align: center; + } + + .login-right-section { + padding: 8px 12px 24px; + } + + .shiny-text-container { + text-align: center; + } + + .login-page .ant-input, + .login-page .ant-input-affix-wrapper { + height: 44px !important; + font-size: 14px !important; + } + + .login-page .ant-input-affix-wrapper .ant-input { + height: 100% !important; + } + + .login-code-btn { + height: 44px !important; + min-width: 90px !important; + font-size: 13px !important; + } + + .login-submit-btn { + height: 44px !important; + font-size: 15px !important; + } +} diff --git a/video-gen-app/src/pages/LoginPage.tsx b/video-gen-app/src/pages/LoginPage.tsx index c143a2d5..92cfd43c 100644 --- a/video-gen-app/src/pages/LoginPage.tsx +++ b/video-gen-app/src/pages/LoginPage.tsx @@ -8,6 +8,7 @@ import { import { useNavigate } from 'react-router-dom'; import { useAuthStore } from '../store/useAuthStore'; import { sendSms,phonelogin, getSiteInfo, register } from '../api'; +import './LoginPage.css'; const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8000/api'; @@ -73,16 +74,14 @@ const LoginPage: React.FC = () => { if (!checkAgreed()) return; try { const values = await pwdForm.validateFields(); - // setLoading(true); await login(values.phone, values.password, undefined, values.rememberMe); message.success('登录成功,欢迎回来'); await checkAuth(); - navigate('/projects'); + navigate('/home'); } catch (error: any) { const errorMsg = error?.response?.data?.detail || error?.response?.data?.message || error?.message || '登录失败'; message.error(errorMsg); } finally { - // setLoading(false); } }; @@ -98,7 +97,7 @@ const LoginPage: React.FC = () => { await phonelogin(values.phone, values.code); message.success('登录成功,欢迎回来'); await checkAuth(); - navigate('/projects'); + navigate('/home'); } catch (error: any) { const errorMsg = error?.response?.data?.detail || error?.response?.data?.message || error?.message || '登录失败'; message.error(errorMsg); @@ -115,7 +114,7 @@ const LoginPage: React.FC = () => { const user = await register(values.phone, values.regCode, values.password); message.success('注册成功'); await checkAuth(); - navigate('/projects'); + navigate('/home'); } catch (error: any) { const errorMsg = error?.response?.data?.detail || error?.response?.data?.message || error?.message || '注册失败'; message.error(errorMsg); @@ -131,7 +130,6 @@ const LoginPage: React.FC = () => { if (c <= 1) { clearInterval(timer); setShowSliderVerify(false); - // 倒计时结束后,设置重新发送状态 if (isReg) { setShowResend(true); } else { @@ -144,7 +142,6 @@ const LoginPage: React.FC = () => { }, 1000); }; - // 登录模式倒计时结束后重置验证状态 useEffect(() => { if (countdown === 0 && mode === 'phone') { setLoginSliderVerified(false); @@ -158,14 +155,12 @@ const LoginPage: React.FC = () => { return; } - // 注册时需要滑动验证 if (isReg) { setShowSliderVerify(true); setSliderVerified(false); return; } - // 登录时也需要滑动验证 if (!loginSliderVerified) { setShowSliderVerify(true); setLoginSliderVerified(false); @@ -185,7 +180,6 @@ const LoginPage: React.FC = () => { } catch (error: any) { const errorMsg = error?.response?.data?.detail || error?.response?.data?.message || error?.message || '登录失败'; message.error(errorMsg); - // 发送失败,重置验证状态、倒计时和滑块组件 if (mode === 'register') { setSliderVerified(false); setRegCountdown(0); @@ -193,18 +187,13 @@ const LoginPage: React.FC = () => { setLoginSliderVerified(false); setCountdown(0); } - // 通过更新 key 强制刷新滑块组件 setSliderKey(prev => prev + 1); } }; - // 滑动验证成功后的回调 const handleSliderSuccess = async (isResend = false) => { - // 根据当前模式获取手机号 const phone = mode === 'register' ? regForm.getFieldValue('phone') : phoneForm.getFieldValue('phone'); try { - - let mode2 = ''; if (mode === 'register') { mode2 = 'register'; @@ -213,7 +202,6 @@ const LoginPage: React.FC = () => { } await sendSms(phone,mode2); - // 设置对应的验证状态 if (mode === 'register') { setSliderVerified(true); startCountdown(setRegCountdown, true); @@ -225,7 +213,6 @@ const LoginPage: React.FC = () => { } catch (error: any) { const errorMsg = error?.response?.data?.detail || error?.response?.data?.message || error?.message || '登录失败'; message.error(errorMsg); - // 发送失败,重置验证状态、倒计时和滑块组件 if (mode === 'register') { setSliderVerified(false); setRegCountdown(0); @@ -235,7 +222,6 @@ const LoginPage: React.FC = () => { setCountdown(0); setLoginShowResend(true); } - // 通过更新 key 强制刷新滑块组件 setSliderKey(prev => prev + 1); } }; @@ -250,9 +236,7 @@ const LoginPage: React.FC = () => { setShowSliderVerify(false); setShowResend(false); setLoginShowResend(false); - // 刷新滑动验证组件 setSliderKey(prev => prev + 1); - // 清空当前模式相关的表单 if (t === 'password') { pwdForm.resetFields(); } else { @@ -270,7 +254,6 @@ const LoginPage: React.FC = () => { background: '#fff', border: '1.5px solid #e2e8f0', color: '#1e293b', - height: 48, borderRadius: 10, fontSize: 14, }; @@ -280,93 +263,45 @@ const LoginPage: React.FC = () => { }; return ( -
-
-
-
+
+
+
+
- {/* Left side - features */} -
- +
+
-
+
{siteLogo ? ( - logo + logo ) : ( -
+
)} - - {siteName} - + {siteName}
AI赋能创意,素材触手可及
- + 专业的 AI 素材生成平台,通过智能提示词优化,
让您的创意快速转化为精美视频、图片
-
+
{features.map((f, i) => (
-
{f.icon}
+
{f.icon}
- {f.title} - {f.desc} + {f.title} + {f.desc}
))} @@ -374,48 +309,26 @@ const LoginPage: React.FC = () => {
- {/* Right side - login/register form */} -
- - +
+ + {mode === 'register' ? '创建账号' : '欢迎回来'} - + {mode === 'register' ? '注册新账号,开始创作视频' : '登录您的账号,开始创作视频'} - {/* Tab switcher - only for login modes */} {mode !== 'register' && ( -
+
{(['password', 'phone'] as const).map((t) => ( -
switchTab(t)} style={{ - flex: 1, textAlign: 'center', padding: '10px 0', - borderRadius: 8, cursor: 'pointer', fontSize: 14, - fontWeight: tab === t ? 600 : 400, - color: tab === t ? '#6366f1' : '#64748b', - background: tab === t ? '#fff' : 'transparent', - border: tab === t ? '1px solid rgba(99,102,241,0.2)' : '1px solid transparent', - boxShadow: tab === t ? '0 2px 8px rgba(99,102,241,0.1)' : 'none', - transition: 'all 0.3s ease', - }}> +
switchTab(t)} + className={`login-tab ${tab === t ? 'login-tab-active' : ''}`}> {t === 'password' ? '密码登录' : '验证码登录'}
))}
)} - {/* Password Login */} {mode === 'password' && ( @@ -428,16 +341,11 @@ const LoginPage: React.FC = () => { 记住我的登录状态 - + )} - {/* Phone Login */} {mode === 'phone' && (
@@ -454,27 +362,18 @@ const LoginPage: React.FC = () => { - {/* 滑动验证 - 获取验证码后显示 */} {showSliderVerify && ( { )} - + )} - {/* Register */} {mode === 'register' && (
@@ -511,27 +405,18 @@ const LoginPage: React.FC = () => { - {/* 滑动验证 - 获取验证码后显示 */} {showSliderVerify && ( { } placeholder="请设置密码(至少6位)" style={inputStyle} /> - - - +
)} - {/* Agreement checkbox */} -
+
setAgreed(e.target.checked)}> - + 我已阅读并同意 { e.stopPropagation(); openPdf(agreementUrl); }} - style={{ color: '#6366f1', cursor: 'pointer' }} + className="login-link" >《用户协议》 { e.stopPropagation(); openPdf(policyUrl); }} - style={{ color: '#6366f1', cursor: 'pointer' }} + className="login-link" >《隐私政策》
- {/* Bottom left: switch between login and register */} -
+
{mode === 'register' ? ( { setMode('password'); setTab('password'); @@ -594,7 +471,7 @@ const LoginPage: React.FC = () => { ) : ( { setMode('register'); setShowResend(false); @@ -614,7 +491,6 @@ const LoginPage: React.FC = () => { ); }; -// 滑动验证组件 const SliderVerify: React.FC<{ onSuccess: () => void; isVerified: boolean; @@ -623,13 +499,12 @@ const SliderVerify: React.FC<{ const sliderRef = React.useRef(null); const trackRef = React.useRef(null); const positionRef = React.useRef(0); - const successRef = React.useRef(false); // 防止重复触发成功回调 + const successRef = React.useRef(false); - const [containerWidth, setContainerWidth] = React.useState(360); // 容器宽度,自适应 - const sliderWidth = 50; // 滑块宽度 + const [containerWidth, setContainerWidth] = React.useState(360); + const sliderWidth = 50; const maxPosition = containerWidth - sliderWidth; - // 监听容器尺寸变化,使其自适应父容器宽度 React.useEffect(() => { const updateWidth = () => { if (containerRef.current) { @@ -656,13 +531,11 @@ const SliderVerify: React.FC<{ }, []); const updatePosition = (x: number) => { - // 如果已经成功,不再处理 if (successRef.current || isVerified) return; const newPosition = Math.max(0, Math.min(x, maxPosition)); positionRef.current = newPosition; - // 直接操作DOM,避免React状态更新的开销 if (sliderRef.current) { sliderRef.current.style.left = `${newPosition}px`; } @@ -670,12 +543,9 @@ const SliderVerify: React.FC<{ trackRef.current.style.width = `${newPosition + sliderWidth}px`; } - // 验证成功 if (newPosition >= maxPosition - 5) { - // 设置成功标志,防止重复触发 successRef.current = true; - // 设置验证成功状态 if (sliderRef.current) { sliderRef.current.style.background = '#22c55e'; sliderRef.current.innerHTML = ``; @@ -685,7 +555,6 @@ const SliderVerify: React.FC<{ trackRef.current.style.background = '#dcfce7'; } - // 调用成功回调 onSuccess(); } }; @@ -708,7 +577,6 @@ const SliderVerify: React.FC<{ document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); - // 如果没有滑到终点,重置位置 if (!successRef.current && positionRef.current < maxPosition - 5) { positionRef.current = 0; if (sliderRef.current) { @@ -723,11 +591,49 @@ const SliderVerify: React.FC<{ document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); }; + + const handleTouchStart = (e: React.TouchEvent) => { + if (successRef.current || isVerified) return; + e.preventDefault(); + + const container = containerRef.current; + if (!container) return; + + const rect = container.getBoundingClientRect(); + const touch = e.touches[0]; + const startX = touch.clientX - rect.left - sliderWidth / 2; + updatePosition(startX); + + const handleTouchMove = (moveEvent: TouchEvent) => { + const moveTouch = moveEvent.touches[0]; + const x = moveTouch.clientX - rect.left - sliderWidth / 2; + updatePosition(x); + }; + + const handleTouchEnd = () => { + document.removeEventListener('touchmove', handleTouchMove); + document.removeEventListener('touchend', handleTouchEnd); + + if (!successRef.current && positionRef.current < maxPosition - 5) { + positionRef.current = 0; + if (sliderRef.current) { + sliderRef.current.style.left = '0px'; + } + if (trackRef.current) { + trackRef.current.style.width = `${sliderWidth}px`; + } + } + }; + + document.addEventListener('touchmove', handleTouchMove, { passive: false }); + document.addEventListener('touchend', handleTouchEnd); + }; return (
- {/* 已滑动部分背景 - 包含阴影弧度 */}
- {/* 文字提示 */}
- {/* 滑块 */}
{ }; return ( -
+
{/* Header banner */}
{ background: 'rgba(255,255,255,0.15)', border: '1px solid rgba(255,255,255,0.3)', color: '#fff', fontWeight: 600, backdropFilter: 'blur(10px)', borderRadius: 10, height: 42, }}>新建项目 +
{projects.length === 0 ? ( diff --git a/video-gen-app/src/store/useAppStore.ts b/video-gen-app/src/store/useAppStore.ts index e1ab0b3d..e6a2f2dc 100644 --- a/video-gen-app/src/store/useAppStore.ts +++ b/video-gen-app/src/store/useAppStore.ts @@ -65,7 +65,7 @@ export const useAppStore = create((set, get) => ({ loading: false, // 生成配置状态初始值 - mediaType: 'image', + mediaType: 'video', countType: '请选择', selectedRatio: '1:1', selectedResolution: '2K', diff --git a/video-gen-app/src/store/useAuthStore.ts b/video-gen-app/src/store/useAuthStore.ts index 62fe9745..0a23c065 100644 --- a/video-gen-app/src/store/useAuthStore.ts +++ b/video-gen-app/src/store/useAuthStore.ts @@ -10,6 +10,7 @@ interface AuthState { checkAuth: () => Promise; changePassword: (oldPwd: string, newPwd: string) => Promise; refreshUser: () => Promise; + setUserCredits: (credits: number) => void; } export const useAuthStore = create((set) => ({ @@ -52,4 +53,10 @@ export const useAuthStore = create((set) => ({ console.error('Failed to refresh user:', error); } }, + + // 用通知接口返回的 credits.balance 直接覆盖当前 user.credits + // 避免再请求一次 getUser 接口 + setUserCredits: (credits: number) => { + set((state) => (state.user ? { user: { ...state.user, credits } } : {})); + }, }));