diff --git a/.gitignore b/.gitignore index 4c3b48dc..a212a307 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,7 @@ __pycache__/ .claude/ .vscode/ .trae/ -video-gen-app/dist/ +# video-gen-app/dist/ video-gen-api/dist/ # 使用通配符 diff --git a/video-gen-api/alembic/env.py b/video-gen-api/alembic/env.py index a18798e8..481b66e3 100644 --- a/video-gen-api/alembic/env.py +++ b/video-gen-api/alembic/env.py @@ -40,6 +40,8 @@ from app.models.material_cost import MaterialCost # noqa: F401 from app.models.generated_resource import GeneratedResource # noqa: F401 from app.models.pre_test_template import PreTestTemplate # noqa: F401 from app.models.upload_task import UploadTask # noqa: F401 +from app.models.open_type import OpenType # noqa: F401 + config = context.config diff --git a/video-gen-api/alembic/versions/2bafebb4be14_新增开户方式管理表.py b/video-gen-api/alembic/versions/2bafebb4be14_新增开户方式管理表.py new file mode 100644 index 00000000..387e633a --- /dev/null +++ b/video-gen-api/alembic/versions/2bafebb4be14_新增开户方式管理表.py @@ -0,0 +1,29 @@ +"""新增开户方式管理表 + +Revision ID: 2bafebb4be14 +Revises: 08749ddd1414 +Create Date: 2026-06-23 13:50:26.577410 +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '2bafebb4be14' +down_revision: Union[str, None] = '08749ddd1414' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/video-gen-api/app/api/v1/__init__.py b/video-gen-api/app/api/v1/__init__.py index 6833e4a7..06f60775 100644 --- a/video-gen-api/app/api/v1/__init__.py +++ b/video-gen-api/app/api/v1/__init__.py @@ -22,6 +22,8 @@ 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.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 +from app.api.v1.open_type import router as open_type_router from app.api.admin import router as admin_module_router api_router = APIRouter() @@ -47,4 +49,6 @@ api_router.include_router(user_oauth_router) api_router.include_router(user_oauth_app_router) api_router.include_router(upload_material_router) api_router.include_router(pre_test_template_router) +api_router.include_router(material_consumption_router) +api_router.include_router(open_type_router) api_router.include_router(admin_module_router) diff --git a/video-gen-api/app/api/v1/material_consumption.py b/video-gen-api/app/api/v1/material_consumption.py new file mode 100644 index 00000000..59f5e3b4 --- /dev/null +++ b/video-gen-api/app/api/v1/material_consumption.py @@ -0,0 +1,254 @@ +from typing import Any, Optional, List +from datetime import datetime, timedelta, timezone + +from fastapi import APIRouter, Query, Depends, Body +from sqlalchemy import select, func +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models.user import User +from app.models.resources_material import ResourcesMaterial +from app.models.material_cost import MaterialCost +from app.models.user_oauth import UserOAuth +from app.models.user_oauth_account import UserOAuthAccount +from app.dependencies import get_current_user, get_db, get_admin_user +from app.services.material_consumption_queue import sync_all_advertisers_consumption, _fetch_and_save_consumption +from app.services.material_consumption_service import get_consumption_list, format_consumption_response + +router = APIRouter(prefix="/material-consumption", tags=["material-consumption"]) + + +@router.get( + "/list", + summary="查询素材消耗列表", + description="查询当前用户的素材消耗列表", +) +async def user_consumption_list( + page: int = Query(1, ge=1, description="页码"), + page_size: int = Query(20, ge=1, le=100, description="每页数量"), + advertiser_id: Optional[str] = Query(None, description="广告主ID筛选"), + consume_date: Optional[List[str]] = Query(None, description="消耗日期范围,格式: ['开始日期','结束日期']"), + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> Any | dict: + consumptions, total = await get_consumption_list( + db=db, + page=page, + page_size=page_size, + advertiser_id=advertiser_id, + consume_date=consume_date, + current_user=current_user, + ) + + return { + "code": 0, + "data": format_consumption_response(consumptions), + "pagination": { + "page": page, + "page_size": page_size, + "total": total, + }, + } + + +@router.get( + "/sync", + summary="手动同步素材消耗", + description="手动触发当前用户授权的广告主的素材消耗同步任务,加入队列顺序执行", +) +async def sync_consumption( + date: str = Query(None, description="同步日期,默认为昨天"), + advertiser_id: Optional[str] = Query(None, description="指定广告主ID,不指定则同步所有授权的广告主"), + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> Any | dict: + if date is None: + date = (datetime.now(timezone.utc) - timedelta(days=1)).strftime("%Y-%m-%d") + + from app.services.material_consumption_queue import material_consumption_queue + + query = ( + select(UserOAuth.id, UserOAuthAccount.advertiser_id) + .join(UserOAuthAccount, UserOAuth.id == UserOAuthAccount.oauth_id) + .join(ResourcesMaterial, UserOAuthAccount.advertiser_id == ResourcesMaterial.advertiser_id) + .where( + UserOAuth.user_id == current_user.id, + UserOAuth.deleted_at.is_(None), + UserOAuthAccount.deleted_at.is_(None), + ResourcesMaterial.deleted_at.is_(None), + ResourcesMaterial.material_id.is_not_(None), + ) + .distinct() + ) + + if advertiser_id: + query = query.where(UserOAuthAccount.advertiser_id == advertiser_id) + + result = await db.execute(query) + oauth_advertiser_pairs = result.all() + + if not oauth_advertiser_pairs: + return { + "code": 0, + "message": "没有找到可同步的广告主", + } + + for oauth_id, adv_id in oauth_advertiser_pairs: + await material_consumption_queue.enqueue({ + "oauth_id": oauth_id, + "advertiser_id": adv_id, + "date": date, + }) + + return { + "code": 0, + "message": f"已将 {len(oauth_advertiser_pairs)} 个广告主的消耗更新任务加入队列", + } + + +@router.get( + "/fields", + summary="获取素材消耗字段描述", + description="返回素材消耗表每个字段对应的中文描述,帮助前端理解字段含义", +) +async def get_consumption_fields() -> Any | dict: + fields = [ + {"field": "id", "description": "主键"}, + {"field": "advertiser_id", "description": "广告主id"}, + {"field": "material_id", "description": "素材id"}, + {"field": "consume_date", "description": "消耗日期"}, + {"field": "stat_cost", "description": "消耗金额"}, + {"field": "show_cnt", "description": "展示数"}, + {"field": "cpm_platform", "description": "平均千次展现费用(元)"}, + {"field": "click_cnt", "description": "点击数"}, + {"field": "ctr", "description": "点击率"}, + {"field": "cpc_platform", "description": "平均点击单价(元)"}, + {"field": "convert_cnt", "description": "转化数"}, + {"field": "conversion_cost", "description": "平均转化成本(元)"}, + {"field": "conversion_rate", "description": "转化率"}, + {"field": "deep_convert_cnt", "description": "深度转化数"}, + {"field": "deep_convert_cost", "description": "深度转化成本(元)"}, + {"field": "deep_convert_rate", "description": "深度转化率"}, + {"field": "active", "description": "激活数"}, + {"field": "active_cost", "description": "激活成本(元)"}, + {"field": "active_rate", "description": "激活率"}, + {"field": "active_register", "description": "注册数"}, + {"field": "active_register_cost", "description": "注册成本(元)"}, + {"field": "active_register_rate", "description": "注册率"}, + {"field": "attribution_next_day_open_cnt", "description": "次留数"}, + {"field": "attribution_next_day_open_cost", "description": "次留成本"}, + {"field": "attribution_next_day_open_rate", "description": "次留率"}, + {"field": "active_pay", "description": "首次付费数"}, + {"field": "active_pay_cost", "description": "首次付费成本(元)"}, + {"field": "active_pay_rate", "description": "首次付费率"}, + {"field": "phone", "description": "点击电话按钮"}, + {"field": "form", "description": "用户在门店落地页多线沟通提交表单的次数"}, + {"field": "download_start", "description": "用户点击下载开始的次数"}, + {"field": "form_submit", "description": "用户查看附加创意后,提交表单的次数"}, + {"field": "button", "description": "用户点击按钮button的次数"}, + {"field": "view", "description": "用户在关键页面的浏览次数"}, + {"field": "message", "description": "用户点击短信咨询的次数"}, + {"field": "consult", "description": "用户点击在线咨询按钮的次数"}, + {"field": "consult_effective", "description": "用户在门店落地页多线沟通的在线咨询中有效咨询的次数"}, + {"field": "shopping", "description": "用户购买商品的次数"}, + {"field": "customer_effective", "description": "有效获客"}, + {"field": "attribution_game_in_app_ltv_1day", "description": "当日付费金额"}, + {"field": "attribution_game_in_app_roi_1day", "description": "当日付费ROI"}, + {"field": "loan_completion", "description": "完件数(互联网金融-贷款行业中,用户成功提交贷款额度申请的行为)"}, + {"field": "loan_completion_cost", "description": "完件成本(元)"}, + {"field": "loan_completion_rate", "description": "完件率"}, + {"field": "loan_credit", "description": "授信数(互联网金融-贷款行业中,用户提交贷款额度申请后,客户审批通过,给予用户可贷款的额度)"}, + {"field": "loan_credit_cost", "description": "授信成本(元)"}, + {"field": "loan_credit_rate", "description": "授信率"}, + {"field": "in_app_order_gmv", "description": "引流电商订单GMV(当您使用\"in_app_order\"事件回传订单金额时,对应的GMV金额)"}, + {"field": "in_app_order_roi", "description": "引流电商订单ROI"}, + {"field": "in_app_pay_gmv", "description": "引流电商支付GMV"}, + {"field": "in_app_pay_roi", "description": "引流电商支付ROI"}, + {"field": "total_play", "description": "播放量(播放时间大于0S的数量,在某些蜂窝网络环境下,需要您手动点击开始才会开始播放,因此有时播放数小于展示数)"}, + {"field": "valid_play", "description": "有效播放数"}, + {"field": "valid_play_cost", "description": "有效播放成本(元)"}, + {"field": "valid_play_rate", "description": "有效播放率"}, + {"field": "valid_play_of_mille", "description": "千次有效播放数"}, + {"field": "valid_play_cost_of_mille", "description": "千次有效播放成本(元)"}, + {"field": "average_play_time_per_play", "description": "平均单次播放时长"}, + {"field": "play_over_rate", "description": "完播率"}, + {"field": "dy_like", "description": "点赞数"}, + {"field": "dy_comment", "description": "评论量"}, + {"field": "dy_share", "description": "分享量"}, + {"field": "report_cnt", "description": "举报数"}, + {"field": "created_at", "description": "创建时间"}, + ] + + return { + "code": 0, + "data": fields, + } + + +@router.get( + "/advertisers", + summary="获取用户的广告主列表", + description="获取当前用户有权限的广告主列表,用于筛选", +) +async def get_advertisers( + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> Any | dict: + result = await db.execute( + select(UserOAuthAccount.advertiser_id) + .join(UserOAuth, UserOAuthAccount.oauth_id == UserOAuth.id) + .where( + UserOAuth.user_id == current_user.id, + UserOAuth.deleted_at.is_(None), + UserOAuthAccount.deleted_at.is_(None), + ) + .distinct() + ) + advertisers = result.scalars().all() + + return { + "code": 0, + "data": [{"advertiser_id": str(aid)} for aid in advertisers], + } + + +@router.get( + "/admin/list", + summary="管理员查询所有素材消耗列表", + description="管理员可查看系统中所有用户的素材消耗数据", +) +async def admin_consumption_list( + page: int = Query(1, ge=1, description="页码"), + page_size: int = Query(20, ge=1, le=100, description="每页数量"), + advertiser_id: Optional[str] = Query(None, description="广告主ID筛选"), + user_id: Optional[str] = Query(None, description="用户ID筛选"), + consume_date: Optional[List[str]] = Query(None, description="消耗日期范围,格式: ['开始日期','结束日期']"), + current_user: User = Depends(get_admin_user), + db: AsyncSession = Depends(get_db), +) -> Any | dict: + #需要判断管理员吗 + if not current_user.is_admin: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="只有管理员才能查询所有素材消耗列表", + ) + + + consumptions, total = await get_consumption_list( + db=db, + page=page, + page_size=page_size, + advertiser_id=advertiser_id, + user_id=user_id, + consume_date=consume_date, + current_user=None, + ) + + return { + "code": 0, + "data": format_consumption_response(consumptions, include_oauth_id=True), + "pagination": { + "page": page, + "page_size": page_size, + "total": total, + }, + } \ No newline at end of file diff --git a/video-gen-api/app/api/v1/open_type.py b/video-gen-api/app/api/v1/open_type.py new file mode 100644 index 00000000..c8a0bf2e --- /dev/null +++ b/video-gen-api/app/api/v1/open_type.py @@ -0,0 +1,293 @@ +from typing import Any, Optional, List + +from fastapi import APIRouter, Body, Depends, HTTPException, Query, status +from sqlalchemy import select, func +from sqlalchemy.ext.asyncio import AsyncSession + +from app.dependencies import get_admin_user, get_db +from app.models.user import User +from app.models.open_type import OpenType +from app.schemas.open_type import ( + OpenTypeCreate, + OpenTypeUpdate, + OpenTypeListResponse, + OpenTypeResponse, +) +from app.utils.id_gen import generate_id + +router = APIRouter(prefix="/open-type", tags=["open-type"]) + + +# 公共接口 - 无需登录 +@router.get( + "/open_type_all", + summary="查询所有开户方式", + description="查询所有开户方式,前端使用select框选择,无需登录", + dependencies=[], +) +async def get_open_type_public( + db: AsyncSession = Depends(get_db), +) -> Any | dict: + + result = await db.execute( + select(OpenType).where(OpenType.deleted_at.is_(None)) + ) + open_types = result.scalars().all() + return { + "code": 0, + "message": "查询成功", + "data": [ + { + "id": ot.id, + "type_name": ot.type_name, + "open_type": ot.open_type, + "description": ot.description, + "thumb": ot.thumb, + } + for ot in open_types + ], + } + + +@router.get( + "/list", + summary="获取开户方式列表", + description="获取开户方式列表,支持分页和筛选", + response_model=OpenTypeListResponse, +) +async def get_open_type_list( + page: int = Query(1, ge=1, description="页码"), + page_size: int = Query(20, ge=1, le=100, description="每页数量"), + type_name: Optional[str] = Query(None, description="标题名称筛选"), + open_type: Optional[int] = Query(None, description="开户方式id筛选"), + admin: User = Depends(get_admin_user), + db: AsyncSession = Depends(get_db), +) -> Any | dict: + offset = (page - 1) * page_size + + query = select(OpenType).where(OpenType.deleted_at.is_(None)) + + if type_name: + query = query.where(OpenType.type_name.like(f"%{type_name}%")) + + if open_type: + query = query.where(OpenType.open_type == open_type) + + result = await db.execute( + query.order_by(OpenType.open_type.asc()) + .offset(offset) + .limit(page_size) + ) + open_types = result.scalars().all() + + count_query = select(func.count(OpenType.id)).where(OpenType.deleted_at.is_(None)) + if type_name: + count_query = count_query.where(OpenType.type_name.like(f"%{type_name}%")) + if open_type: + count_query = count_query.where(OpenType.open_type == open_type) + + total_result = await db.execute(count_query) + total = total_result.scalar_one() + + return { + "code": 0, + "message": "查询成功", + "data": [ + { + "id": item.id, + "type_name": item.type_name, + "open_type": item.open_type, + "description": item.description, + "thumb": item.thumb, + "created_at": item.created_at, + "updated_at": item.updated_at, + } + for item in open_types + ], + "pagination": { + "page": page, + "page_size": page_size, + "total": total, + "total_pages": (total + page_size - 1) // page_size, + }, + } + + +@router.get( + "/select/{id}", + summary="获取开户方式详情", + description="根据ID获取开户方式详情", + response_model=OpenTypeResponse, +) +async def get_open_type_detail( + id: str, + admin: User = Depends(get_admin_user), + db: AsyncSession = Depends(get_db), +) -> Any | dict: + result = await db.execute( + select(OpenType).where(OpenType.id == id, OpenType.deleted_at.is_(None)) + ) + open_type = result.scalar_one_or_none() + + if not open_type: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="开户方式不存在", + ) + + return { + "code": 0, + "message": "查询成功", + "data": { + "id": open_type.id, + "type_name": open_type.type_name, + "open_type": open_type.open_type, + "description": open_type.description, + "thumb": open_type.thumb, + "created_at": open_type.created_at, + "updated_at": open_type.updated_at, + }, + } + + +@router.post( + "/create", + summary="创建开户方式", + description="创建新的开户方式", + response_model=OpenTypeResponse, +) +async def create_open_type( + req: OpenTypeCreate = Body(...), + admin: User = Depends(get_admin_user), + db: AsyncSession = Depends(get_db), +) -> Any | dict: + existing = await db.execute( + select(OpenType).where(OpenType.open_type == req.open_type, OpenType.deleted_at.is_(None)) + ) + if existing.scalar_one_or_none(): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"开户方式ID {req.open_type} 已存在", + ) + + open_type = OpenType( + id=generate_id(), + type_name=req.type_name, + open_type=req.open_type, + description=req.description, + thumb=req.thumb, + ) + + db.add(open_type) + await db.commit() + await db.refresh(open_type) + + return { + "code": 0, + "message": "创建成功", + "data": { + "id": open_type.id, + "type_name": open_type.type_name, + "open_type": open_type.open_type, + "description": open_type.description, + "thumb": open_type.thumb, + "created_at": open_type.created_at, + "updated_at": open_type.updated_at, + }, + } + + +@router.put( + "/update/{id}", + summary="更新开户方式", + description="更新指定的开户方式", + response_model=OpenTypeResponse, +) +async def update_open_type( + id: str, + req: OpenTypeUpdate = Body(...), + admin: User = Depends(get_admin_user), + db: AsyncSession = Depends(get_db), +) -> Any | dict: + result = await db.execute( + select(OpenType).where(OpenType.id == id, OpenType.deleted_at.is_(None)) + ) + open_type = result.scalar_one_or_none() + + if not open_type: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="开户方式不存在", + ) + + if req.open_type is not None and req.open_type != open_type.open_type: + existing = await db.execute( + select(OpenType).where( + OpenType.open_type == req.open_type, + OpenType.id != id, + OpenType.deleted_at.is_(None), + ) + ) + if existing.scalar_one_or_none(): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"开户方式ID {req.open_type} 已存在", + ) + + if req.type_name is not None: + open_type.type_name = req.type_name + if req.open_type is not None: + open_type.open_type = req.open_type + if req.description is not None: + open_type.description = req.description + if req.thumb is not None: + open_type.thumb = req.thumb + + await db.commit() + await db.refresh(open_type) + + return { + "code": 0, + "message": "更新成功", + "data": { + "id": open_type.id, + "type_name": open_type.type_name, + "open_type": open_type.open_type, + "description": open_type.description, + "thumb": open_type.thumb, + "created_at": open_type.created_at, + "updated_at": open_type.updated_at, + }, + } + + +@router.delete( + "/delete/{id}", + summary="删除开户方式", + description="软删除指定的开户方式", + response_model=OpenTypeResponse, +) +async def delete_open_type( + id: str, + admin: User = Depends(get_admin_user), + db: AsyncSession = Depends(get_db), +) -> Any | dict: + result = await db.execute( + select(OpenType).where(OpenType.id == id, OpenType.deleted_at.is_(None)) + ) + open_type = result.scalar_one_or_none() + + if not open_type: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="开户方式不存在", + ) + + open_type.deleted_at = func.now() + await db.commit() + + return { + "code": 0, + "message": "删除成功", + "data": None, + } diff --git a/video-gen-api/app/api/v1/upload_material.py b/video-gen-api/app/api/v1/upload_material.py index 509eb757..30116a8d 100644 --- a/video-gen-api/app/api/v1/upload_material.py +++ b/video-gen-api/app/api/v1/upload_material.py @@ -222,6 +222,14 @@ async def async_batch_upload_material( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ) -> Any | dict: + + # param = { + # "account_ids" : json.dumps([1836693172153543]), + # } + + # account_info = await DouyinApi().get_account_info("0019eb9f130027c05b8", param) + # return account_info + if not req.tasks: return { "code": 0, diff --git a/video-gen-api/app/main.py b/video-gen-api/app/main.py index 9fd46e74..c486f832 100644 --- a/video-gen-api/app/main.py +++ b/video-gen-api/app/main.py @@ -68,10 +68,13 @@ async def lifespan(app: FastAPI): await upload_queue.recover() upload_queue_task = asyncio.create_task(upload_queue.run()) - # 启动上传队列(异步处理素材上传) - from app.services.upload_queue import upload_queue - await upload_queue.recover() - upload_queue_task = asyncio.create_task(upload_queue.run()) + # 启动素材消耗队列 + from app.services.material_consumption_queue import material_consumption_queue + consumption_queue_task = asyncio.create_task(material_consumption_queue.run()) + + # 启动素材消耗计划任务(每天9点自动同步) + from app.tasks.material_consumption_task import schedule_daily_sync + consumption_schedule_task = asyncio.create_task(schedule_daily_sync()) # 启动时立即同步一次未支付订单 asyncio.create_task(asyncio.sleep(5)) # 等待5秒后再同步,让系统完全启动 @@ -97,10 +100,11 @@ async def lifespan(app: FastAPI): await queue_task upload_queue.stop() await upload_queue_task + material_consumption_queue.stop() + await consumption_queue_task + consumption_schedule_task.cancel() expiry_task.cancel() token_refresh_task.cancel() - upload_queue.stop() - await upload_queue_task await close_database() await close_redis() diff --git a/video-gen-api/app/models/open_type.py b/video-gen-api/app/models/open_type.py new file mode 100644 index 00000000..c5d6154c --- /dev/null +++ b/video-gen-api/app/models/open_type.py @@ -0,0 +1,24 @@ +from sqlalchemy import Integer, String, Text +from sqlalchemy.orm import Mapped, mapped_column + +from app.models.base import Base, TimestampMixin, SoftDeleteMixin + + +class OpenType(Base, TimestampMixin, SoftDeleteMixin): + __tablename__ = "open_type" + + id: Mapped[str] = mapped_column( + String(32), primary_key=True, comment="主键" + ) + type_name: Mapped[str] = mapped_column( + String(128), nullable=False, comment="标题名称" + ) + open_type: Mapped[int] = mapped_column( + Integer, nullable=False, unique=True, index=True, comment="开户方式id" + ) + description: Mapped[str | None] = mapped_column( + Text, nullable=True, comment="开户方式描述" + ) + thumb: Mapped[str | None] = mapped_column( + String(255), nullable=True, comment="缩略图" + ) \ No newline at end of file diff --git a/video-gen-api/app/schemas/open_type.py b/video-gen-api/app/schemas/open_type.py new file mode 100644 index 00000000..0553e432 --- /dev/null +++ b/video-gen-api/app/schemas/open_type.py @@ -0,0 +1,48 @@ +from datetime import datetime +from typing import List, Optional + +from pydantic import BaseModel, Field + + +class OpenTypeCreate(BaseModel): + type_name: str = Field(..., description="标题名称") + open_type: int = Field(..., description="开户方式id") + description: Optional[str] = Field(None, description="开户方式描述") + thumb: Optional[str] = Field(None, description="缩略图") + + +class OpenTypeUpdate(BaseModel): + type_name: Optional[str] = Field(None, description="标题名称") + open_type: Optional[int] = Field(None, description="开户方式id") + description: Optional[str] = Field(None, description="开户方式描述") + thumb: Optional[str] = Field(None, description="缩略图") + + +class OpenTypeOut(BaseModel): + id: str = Field(..., description="主键") + type_name: str = Field(..., description="标题名称") + open_type: int = Field(..., description="开户方式id") + description: Optional[str] = Field(None, description="开户方式描述") + thumb: 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 OpenTypeListResponse(BaseModel): + code: int = Field(0, description="返回码,0表示成功") + message: str = Field("查询成功", description="返回消息") + data: List[OpenTypeOut] = Field(..., description="开户方式列表") + pagination: PaginationInfo = Field(..., description="分页信息") + + +class OpenTypeResponse(BaseModel): + code: int = Field(0, description="返回码,0表示成功") + message: str = Field("操作成功", description="返回消息") + data: Optional[OpenTypeOut] = Field(None, description="开户方式信息") \ No newline at end of file diff --git a/video-gen-api/app/services/material_consumption_queue.py b/video-gen-api/app/services/material_consumption_queue.py new file mode 100644 index 00000000..1028d7ad --- /dev/null +++ b/video-gen-api/app/services/material_consumption_queue.py @@ -0,0 +1,334 @@ +import asyncio +import logging +from datetime import datetime, timezone, timedelta + +from sqlalchemy import select, delete +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models.base import async_session +from app.models.material_cost import MaterialCost +from app.models.user_oauth import UserOAuth +from app.models.user_oauth_account import UserOAuthAccount +from app.models.resources_material import ResourcesMaterial +from app.utils.id_gen import generate_id +from app.utils.douyinApi import DouyinApi + +import os +import json + +LOG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "logs") +os.makedirs(LOG_DIR, exist_ok=True) + +logger = logging.getLogger("material_consumption_task") +logger.setLevel(logging.INFO) + + +class DailyRotatingFileHandler(logging.FileHandler): + def __init__(self, directory, encoding=None): + self.directory = directory + filename = self._get_log_filename() + super().__init__(filename, encoding=encoding) + + def _get_log_filename(self): + return os.path.join(self.directory, f"material_consumption-{datetime.now(timezone.utc).strftime('%Y-%m-%d')}.log") + + def emit(self, record): + current_filename = self._get_log_filename() + if self.baseFilename != current_filename: + self.close() + self.baseFilename = current_filename + self.stream = self._open() + super().emit(record) + + +if not logger.handlers: + handler = DailyRotatingFileHandler(LOG_DIR, encoding="utf-8") + handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s", "%Y-%m-%d %H:%M:%S")) + logger.addHandler(handler) + +douyin_api = DouyinApi() + + +def _convert_numeric(value): + """Convert string numeric values to appropriate Python types.""" + if value is None or value == "": + return None + try: + # Try int first + return int(value) + except ValueError: + try: + # Then try float + return float(value) + except ValueError: + # Return original value if conversion fails + return None + + +class MaterialConsumptionQueue: + def __init__(self): + self.queue: asyncio.Queue[dict] = asyncio.Queue() + self.running = False + + async def enqueue(self, task_data: dict): + """Add a task to the queue.""" + await self.queue.put(task_data) + + async def run(self): + """Main processing loop.""" + self.running = True + logger.info("Material consumption queue started") + + while self.running: + try: + task_data = await asyncio.wait_for(self.queue.get(), timeout=5.0) + except asyncio.TimeoutError: + continue + + try: + await self._process(task_data) + except Exception as e: + logger.error(f"Error processing material consumption task: {e}") + finally: + self.queue.task_done() + + logger.info("Material consumption queue stopped") + + async def _process(self, task_data: dict): + """Process a single consumption update task.""" + oauth_id = task_data.get("oauth_id") + advertiser_id = task_data.get("advertiser_id") + date = task_data.get("date") + + logger.info(f"Processing consumption update for oauth_id={oauth_id}, advertiser_id={advertiser_id}, date={date}") + + try: + result = await _fetch_and_save_consumption(oauth_id, advertiser_id, date) + if result.get("success"): + logger.info(f"Successfully updated consumption for advertiser {advertiser_id} on {date}") + else: + logger.error(f"Failed to update consumption for advertiser {advertiser_id} on {date}: {result.get('error')}") + except Exception as e: + logger.error(f"Exception processing consumption for advertiser {advertiser_id} on {date}: {e}") + + def stop(self): + """Stop the queue.""" + self.running = False + + +async def _fetch_and_save_consumption(oauth_id: str, advertiser_id: str, date: str) -> dict: + async with async_session() as db: + try: + consume_date_obj = datetime.strptime(date, "%Y-%m-%d").date() + + # 查询当前授权下系统上传的素材ID列表 + material_result = await db.execute( + select(ResourcesMaterial.upload_id) + .where( + ResourcesMaterial.oauth_id == oauth_id, + ResourcesMaterial.advertiser_id == advertiser_id, + ResourcesMaterial.deleted_at.is_(None), + ResourcesMaterial.upload_id.isnot(None), + ResourcesMaterial.upload_id != "", + ) + ) + system_material_ids = [str(row[0]) for row in material_result.all()] + + if not system_material_ids: + return { + "success": True, + "message": "没有系统上传的素材" + } + + #强制删除旧数据,真实删除,不要软删除 + await db.execute( + delete(MaterialCost).where( + MaterialCost.oauth_id == oauth_id, + MaterialCost.advertiser_id == advertiser_id, + MaterialCost.consume_date == consume_date_obj, + MaterialCost.deleted_at.is_(None), + ) + ) + + BATCH_SIZE = 50 + total_saved = 0 + total_requests = 0 + + # 分批请求,每次请求50个素材 + for i in range(0, len(system_material_ids), BATCH_SIZE): + batch_materials = system_material_ids[i:i+BATCH_SIZE] + total_requests += 1 + + params = { + "dimensions": json.dumps(["ad_platform_material_name","image_mode","material_id","stat_time_day"]), + "advertiser_id": int(advertiser_id), + "metrics": json.dumps([ + "stat_cost","show_cnt","cpm_platform","click_cnt","ctr","cpc_platform","convert_cnt","conversion_cost","conversion_rate","deep_convert_cnt","deep_convert_cost","deep_convert_rate","click_start_cnt","click_start_cost","download_finish_rate", + "install_finish_cnt","install_finish_cost","install_finish_rate","active","active_cost", + "active_rate","active_register","active_register_cost","active_register_rate","game_addiction", + "game_addiction_cost","game_addiction_rate","attribution_next_day_open_cnt","attribution_next_day_open_cost","attribution_next_day_open_rate","next_day_open","active_pay","active_pay_cost","active_pay_rate", + "game_pay_count","game_pay_cost","in_app_uv","in_app_detail_uv","in_app_cart","in_app_pay", + "in_app_order","attribution_billing_game_in_app_ltv_1day","attribution_billing_game_in_app_roi_1day","phone","form", + "form_submit","map","button","view","download_start","qq","vote","lottery","message", + "redirect","shopping","consult","consult_effective","phone_confirm","phone_connect","phone_effective", + "redirect_to_shop","coupon_single_page","poi_address_click","poi_collect","customer_effective", + "attribution_customer_effective","attribution_customer_effective_cost","attribution_clue_pay_succeed", + "attribution_clue_pay_succeed_cost","attribution_clue_interflow","attribution_clue_interflow_cost","attribution_clue_high_intention","attribution_clue_high_intention_cost", + "consult_clue","clue_message_count","attribution_work_wechat_unfriend_count","attribution_clue_connected_count","attribution_clue_connected_cost","attribution_clue_connected_rate","attribution_form","form_and_submit_count","form_and_submit_cost","intention_form_and_submit_count","intention_form_and_submit_cost","attribution_micro_game_0d_ltv","attribution_micro_game_3d_ltv","attribution_micro_game_7d_ltv","attribution_micro_game_0d_roi","attribution_micro_game_3d_roi","attribution_micro_game_7d_roi","attribution_game_in_app_ltv_1day","attribution_game_in_app_roi_1day","active_pay_intra_day_count","active_pay_intra_day_cost","active_pay_intra_day_rate","first_pay_intra_24hour_amount","loan_completion","loan_completion_cost","loan_completion_rate","pre_loan_credit","pre_loan_credit_cost","loan_credit_cost","loan_credit","loan_credit_rate","in_wechat_pay_count","unfollow_in_wechat_count","loan","loan_cost","loan_rate","open_account_count","withdraw_m2_count","in_app_order_gmv","in_app_order_roi","in_app_pay_gmv","in_app_pay_roi","first_rental_order_count","commute_first_pay_count","first_order_count","submit_certification_count","approval_count","total_play","play_duration_3s","valid_play","valid_play_cost","valid_play_rate","valid_play_of_mille","valid_play_cost_of_mille","average_play_time_per_play","play_over_rate","dy_like","dy_comment","dy_share","report_cnt","location_click","dislike_cnt","dy_home_visited","dy_follow","message_action","click_landing_page","click_shopwindow","click_website","click_call_dy","click_download","luban_live_enter_cnt","luban_live_follow_cnt","luban_live_share_cnt","luban_live_comment_cnt","luban_live_gift_cnt","luban_live_gift_amount","click_call_cnt","click_counsel","a3_ask_count","a3_ask_cost" + ]), + "filters": json.dumps([ + { + "field": "stat_cost", + "type": 3, + "operator": 4, + "values": ["0"] + }, + { + "field": "material_id", + "type": 2, + "operator": 7, + "values": batch_materials + } + ]), + "start_time": date, + "end_time": date, + "order_by": json.dumps([ + { + "field": "stat_cost", + "type": "ASC" + } + ]) + } + + response = await douyin_api.get_material_cost(oauth_id, params, request_count=3) + + if response.get("code") != 0: + logger.warning(f"获取素材消耗失败(批次 {i//BATCH_SIZE + 1}): {response.get('message', '未知错误')}") + continue + + data = response.get("data", {}) + list_data = data.get("rows", []) + + for item in list_data: + dimensions = item.get("dimensions", {}) + metrics = item.get("metrics", {}) + material_id = dimensions.get("material_id", "") + + cost = MaterialCost( + id=generate_id(), + oauth_id=oauth_id, + advertiser_id=str(advertiser_id), + material_id=material_id, + consume_date=consume_date_obj, + stat_cost=_convert_numeric(metrics.get("stat_cost")), + show_cnt=_convert_numeric(metrics.get("show_cnt")), + cpm_platform=_convert_numeric(metrics.get("cpm_platform")), + click_cnt=_convert_numeric(metrics.get("click_cnt")), + ctr=_convert_numeric(metrics.get("ctr")), + cpc_platform=_convert_numeric(metrics.get("cpc_platform")), + convert_cnt=_convert_numeric(metrics.get("convert_cnt")), + conversion_cost=_convert_numeric(metrics.get("conversion_cost")), + conversion_rate=_convert_numeric(metrics.get("conversion_rate")), + deep_convert_cnt=_convert_numeric(metrics.get("deep_convert_cnt")), + deep_convert_cost=_convert_numeric(metrics.get("deep_convert_cost")), + deep_convert_rate=_convert_numeric(metrics.get("deep_convert_rate")), + active=_convert_numeric(metrics.get("active")), + active_cost=_convert_numeric(metrics.get("active_cost")), + active_rate=_convert_numeric(metrics.get("active_rate")), + active_register=_convert_numeric(metrics.get("active_register")), + active_register_cost=_convert_numeric(metrics.get("active_register_cost")), + active_register_rate=_convert_numeric(metrics.get("active_register_rate")), + attribution_next_day_open_cnt=_convert_numeric(metrics.get("attribution_next_day_open_cnt")), + attribution_next_day_open_cost=_convert_numeric(metrics.get("attribution_next_day_open_cost")), + attribution_next_day_open_rate=_convert_numeric(metrics.get("attribution_next_day_open_rate")), + active_pay=_convert_numeric(metrics.get("active_pay")), + active_pay_cost=_convert_numeric(metrics.get("active_pay_cost")), + active_pay_rate=_convert_numeric(metrics.get("active_pay_rate")), + phone=_convert_numeric(metrics.get("phone")), + form=_convert_numeric(metrics.get("form")), + download_start=_convert_numeric(metrics.get("download_start")), + form_submit=_convert_numeric(metrics.get("form_submit")), + button=_convert_numeric(metrics.get("button")), + view=_convert_numeric(metrics.get("view")), + message=_convert_numeric(metrics.get("message")), + consult=_convert_numeric(metrics.get("consult")), + consult_effective=_convert_numeric(metrics.get("consult_effective")), + shopping=_convert_numeric(metrics.get("shopping")), + customer_effective=_convert_numeric(metrics.get("customer_effective")), + attribution_game_in_app_ltv_1day=_convert_numeric(metrics.get("attribution_game_in_app_ltv_1day")), + attribution_game_in_app_roi_1day=_convert_numeric(metrics.get("attribution_game_in_app_roi_1day")), + loan_completion=_convert_numeric(metrics.get("loan_completion")), + loan_completion_cost=_convert_numeric(metrics.get("loan_completion_cost")), + loan_completion_rate=_convert_numeric(metrics.get("loan_completion_rate")), + loan_credit=_convert_numeric(metrics.get("loan_credit")), + loan_credit_cost=_convert_numeric(metrics.get("loan_credit_cost")), + loan_credit_rate=_convert_numeric(metrics.get("loan_credit_rate")), + in_app_order_gmv=_convert_numeric(metrics.get("in_app_order_gmv")), + in_app_order_roi=_convert_numeric(metrics.get("in_app_order_roi")), + in_app_pay_gmv=_convert_numeric(metrics.get("in_app_pay_gmv")), + in_app_pay_roi=_convert_numeric(metrics.get("in_app_pay_roi")), + total_play=_convert_numeric(metrics.get("total_play")), + valid_play=_convert_numeric(metrics.get("valid_play")), + valid_play_cost=_convert_numeric(metrics.get("valid_play_cost")), + valid_play_rate=_convert_numeric(metrics.get("valid_play_rate")), + valid_play_of_mille=_convert_numeric(metrics.get("valid_play_of_mille")), + valid_play_cost_of_mille=_convert_numeric(metrics.get("valid_play_cost_of_mille")), + average_play_time_per_play=_convert_numeric(metrics.get("average_play_time_per_play")), + play_over_rate=_convert_numeric(metrics.get("play_over_rate")), + dy_like=_convert_numeric(metrics.get("dy_like")), + dy_comment=_convert_numeric(metrics.get("dy_comment")), + dy_share=_convert_numeric(metrics.get("dy_share")), + report_cnt=_convert_numeric(metrics.get("report_cnt")), + ) + db.add(cost) + total_saved += 1 + + await db.commit() + + return { + "success": True, + "message": f"成功更新 {total_saved} 条记录,共请求 {total_requests} 批次,系统素材总数 {len(system_material_ids)}", + "count": total_saved + } + + except Exception as e: + await db.rollback() + return { + "success": False, + "error": str(e) + } + +#异步获取所有要更新的广告主,素材消耗 +async def sync_all_advertisers_consumption(date: str = None): + """Sync consumption data for all advertisers.""" + if date is None: + date = (datetime.now(timezone.utc) - timedelta(days=1)).strftime("%Y-%m-%d") + + async with async_session() as db: + result = await db.execute( + select(UserOAuth.id, UserOAuthAccount.advertiser_id) + .join(UserOAuthAccount, UserOAuth.id == UserOAuthAccount.oauth_id) + .join(ResourcesMaterial, UserOAuthAccount.oauth_id == ResourcesMaterial.oauth_id) + .where( + UserOAuth.deleted_at.is_(None), + UserOAuthAccount.deleted_at.is_(None), + ResourcesMaterial.deleted_at.is_(None), + ResourcesMaterial.material_id.isnot(None), + ResourcesMaterial.material_id != "", + ) + .distinct() + ) + oauth_advertiser_pairs = result.all() + + for oauth_id, advertiser_id in oauth_advertiser_pairs: + await material_consumption_queue.enqueue({ + "oauth_id": oauth_id, + "advertiser_id": advertiser_id, + "date": date, + }) + logger.info(f"Enqueued consumption sync for oauth_id={oauth_id}, advertiser_id={advertiser_id}, date={date}") + + return {"message": f"已将 {len(oauth_advertiser_pairs)} 个广告主的消耗更新任务加入队列"} + + +material_consumption_queue = MaterialConsumptionQueue() \ No newline at end of file diff --git a/video-gen-api/app/services/material_consumption_service.py b/video-gen-api/app/services/material_consumption_service.py new file mode 100644 index 00000000..5fd3eb00 --- /dev/null +++ b/video-gen-api/app/services/material_consumption_service.py @@ -0,0 +1,160 @@ +from typing import Any, Optional, Tuple, List +from datetime import datetime + +from sqlalchemy import select, func +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models.user import User +from app.models.material_cost import MaterialCost +from app.models.user_oauth import UserOAuth + + +async def get_consumption_list( + db: AsyncSession, + page: int = 1, + page_size: int = 20, + advertiser_id: Optional[str] = None, + user_id: Optional[str] = None, + consume_date: Optional[List[str]] = None, + current_user: Optional[User] = None, +) -> Tuple[List[MaterialCost], int]: + offset = (page - 1) * page_size + + query = select(MaterialCost).where(MaterialCost.deleted_at.is_(None)) + + if current_user: + query = query.join( + UserOAuth, + MaterialCost.oauth_id == UserOAuth.id, + ).where( + UserOAuth.user_id == current_user.id, + UserOAuth.deleted_at.is_(None), + ) + + if user_id: + query = query.join( + UserOAuth, + MaterialCost.oauth_id == UserOAuth.id, + ).where(UserOAuth.user_id == user_id) + + if advertiser_id: + query = query.where(MaterialCost.advertiser_id == advertiser_id) + + if consume_date and len(consume_date) >= 2: + start_date_obj = datetime.strptime(consume_date[0], "%Y-%m-%d").date() + end_date_obj = datetime.strptime(consume_date[1], "%Y-%m-%d").date() + query = query.where(MaterialCost.consume_date >= start_date_obj) + query = query.where(MaterialCost.consume_date <= end_date_obj) + + result = await db.execute( + query.order_by(MaterialCost.consume_date.desc()) + .offset(offset) + .limit(page_size) + ) + consumptions = result.scalars().all() + + count_query = select(func.count(MaterialCost.id)).where(MaterialCost.deleted_at.is_(None)) + + if current_user: + count_query = count_query.join( + UserOAuth, + MaterialCost.oauth_id == UserOAuth.id, + ).where( + UserOAuth.user_id == current_user.id, + UserOAuth.deleted_at.is_(None), + ) + + if user_id: + count_query = count_query.join( + UserOAuth, + MaterialCost.oauth_id == UserOAuth.id, + ).where(UserOAuth.user_id == user_id) + + if advertiser_id: + count_query = count_query.where(MaterialCost.advertiser_id == advertiser_id) + + if consume_date and len(consume_date) >= 2: + start_date_obj = datetime.strptime(consume_date[0], "%Y-%m-%d").date() + end_date_obj = datetime.strptime(consume_date[1], "%Y-%m-%d").date() + count_query = count_query.where(MaterialCost.consume_date >= start_date_obj) + count_query = count_query.where(MaterialCost.consume_date <= end_date_obj) + + total_result = await db.execute(count_query) + total = total_result.scalar_one() + + return consumptions, total + + +def format_consumption_response(consumptions: List[MaterialCost], include_oauth_id: bool = False) -> List[dict]: + result = [] + for consumption in consumptions: + item = { + "id": consumption.id, + "advertiser_id": consumption.advertiser_id, + "material_id": consumption.material_id, + "consume_date": consumption.consume_date.isoformat() if consumption.consume_date else None, + "stat_cost": consumption.stat_cost, + "show_cnt": consumption.show_cnt, + "cpm_platform": consumption.cpm_platform, + "click_cnt": consumption.click_cnt, + "ctr": consumption.ctr, + "cpc_platform": consumption.cpc_platform, + "convert_cnt": consumption.convert_cnt, + "conversion_cost": consumption.conversion_cost, + "conversion_rate": consumption.conversion_rate, + "deep_convert_cnt": consumption.deep_convert_cnt, + "deep_convert_cost": consumption.deep_convert_cost, + "deep_convert_rate": consumption.deep_convert_rate, + "active": consumption.active, + "active_cost": consumption.active_cost, + "active_rate": consumption.active_rate, + "active_register": consumption.active_register, + "active_register_cost": consumption.active_register_cost, + "active_register_rate": consumption.active_register_rate, + "attribution_next_day_open_cnt": consumption.attribution_next_day_open_cnt, + "attribution_next_day_open_cost": consumption.attribution_next_day_open_cost, + "attribution_next_day_open_rate": consumption.attribution_next_day_open_rate, + "active_pay": consumption.active_pay, + "active_pay_cost": consumption.active_pay_cost, + "active_pay_rate": consumption.active_pay_rate, + "phone": consumption.phone, + "form": consumption.form, + "download_start": consumption.download_start, + "form_submit": consumption.form_submit, + "button": consumption.button, + "view": consumption.view, + "message": consumption.message, + "consult": consumption.consult, + "consult_effective": consumption.consult_effective, + "shopping": consumption.shopping, + "customer_effective": consumption.customer_effective, + "attribution_game_in_app_ltv_1day": consumption.attribution_game_in_app_ltv_1day, + "attribution_game_in_app_roi_1day": consumption.attribution_game_in_app_roi_1day, + "loan_completion": consumption.loan_completion, + "loan_completion_cost": consumption.loan_completion_cost, + "loan_completion_rate": consumption.loan_completion_rate, + "loan_credit": consumption.loan_credit, + "loan_credit_cost": consumption.loan_credit_cost, + "loan_credit_rate": consumption.loan_credit_rate, + "in_app_order_gmv": consumption.in_app_order_gmv, + "in_app_order_roi": consumption.in_app_order_roi, + "in_app_pay_gmv": consumption.in_app_pay_gmv, + "in_app_pay_roi": consumption.in_app_pay_roi, + "total_play": consumption.total_play, + "valid_play": consumption.valid_play, + "valid_play_cost": consumption.valid_play_cost, + "valid_play_rate": consumption.valid_play_rate, + "valid_play_of_mille": consumption.valid_play_of_mille, + "valid_play_cost_of_mille": consumption.valid_play_cost_of_mille, + "average_play_time_per_play": consumption.average_play_time_per_play, + "play_over_rate": consumption.play_over_rate, + "dy_like": consumption.dy_like, + "dy_comment": consumption.dy_comment, + "dy_share": consumption.dy_share, + "report_cnt": consumption.report_cnt, + "created_at": consumption.created_at, + } + if include_oauth_id: + item["oauth_id"] = consumption.oauth_id + result.append(item) + return result \ No newline at end of file diff --git a/video-gen-api/app/services/upload_queue.py b/video-gen-api/app/services/upload_queue.py index 145bd5f2..267b1ebf 100644 --- a/video-gen-api/app/services/upload_queue.py +++ b/video-gen-api/app/services/upload_queue.py @@ -10,11 +10,13 @@ from app.models.upload_task import UploadTask from app.models.generated_resource import GeneratedResource from app.models.user_oauth import UserOAuth from app.models.resources_material import ResourcesMaterial +from app.models.user_oauth_account import UserOAuthAccount from app.utils.id_gen import generate_id from app.utils.douyinApi import DouyinApi import os import hashlib +import json LOG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "logs") @@ -133,6 +135,51 @@ class UploadQueue: note=result.get("message", "上传成功"), ) ) + + #请求巨量接口获取账户信息,如果存在则更新,否则插入新记录 + param = { + "account_ids" : json.dumps([int(task.advertiser_id)]), + } + try: + account_info = await douyin_api.get_account_info(task.oauth_id, param) + + code = account_info.get("code", 0) + try: + code = int(code) + except (ValueError, TypeError): + code = -1 + + if code != 0: + logger.error(f"Error getting account info: {json.dumps(account_info)}") + else: + existing_account = await db.execute( + select(UserOAuthAccount).where( + UserOAuthAccount.oauth_id == task.oauth_id, + UserOAuthAccount.advertiser_id == task.advertiser_id, + ) + ) + existing_account = existing_account.scalar_one_or_none() + + if existing_account: + await db.execute( + update(UserOAuthAccount).where( + UserOAuthAccount.id == existing_account.id, + ).values( + deleted_at=None, + advertiser_name= account_info.get("data", {}).get("account_detail_list", [{}])[0].get("advertiser_name", ""), + advertiser_role= "", + ) + ) + else: + db.add(UserOAuthAccount( + id=generate_id(), + oauth_id=task.oauth_id, + advertiser_id=task.advertiser_id, + advertiser_name= account_info.get("data", {}).get("account_detail_list", [{}])[0].get("advertiser_name", ""), + advertiser_role= "", + )) + except Exception as e: + logger.error(f"Exception getting account info: {e}") else: await db.execute( update(UploadTask).where(UploadTask.id == task_id).values( diff --git a/video-gen-api/app/tasks/material_consumption_task.py b/video-gen-api/app/tasks/material_consumption_task.py new file mode 100644 index 00000000..28dd1580 --- /dev/null +++ b/video-gen-api/app/tasks/material_consumption_task.py @@ -0,0 +1,68 @@ +from datetime import datetime, timedelta, timezone +import asyncio +import os +import logging + +from app.services.material_consumption_queue import sync_all_advertisers_consumption + +LOG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "logs") +os.makedirs(LOG_DIR, exist_ok=True) + +logger = logging.getLogger("material_consumption_task") +logger.setLevel(logging.INFO) + + +class DailyRotatingFileHandler(logging.FileHandler): + def __init__(self, directory, encoding=None): + self.directory = directory + filename = self._get_log_filename() + super().__init__(filename, encoding=encoding) + + def _get_log_filename(self): + return os.path.join(self.directory, f"material_consumption_task-{datetime.now(timezone.utc).strftime('%Y-%m-%d')}.log") + + def emit(self, record): + current_filename = self._get_log_filename() + if self.baseFilename != current_filename: + self.close() + self.baseFilename = current_filename + self.stream = self._open() + super().emit(record) + + +if not logger.handlers: + handler = DailyRotatingFileHandler(LOG_DIR, encoding="utf-8") + handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s", "%Y-%m-%d %H:%M:%S")) + logger.addHandler(handler) + + +async def schedule_daily_sync(): + """每天9点自动同步素材消耗数据""" + logger.info("Daily material consumption sync task started") + + while True: + try: + now = datetime.now(timezone.utc) + target_time = now.replace(hour=9, minute=0, second=0, microsecond=0) + + if now >= target_time: + target_time += timedelta(days=1) + + wait_seconds = (target_time - now).total_seconds() + logger.info(f"Waiting {wait_seconds/3600:.1f} hours until next sync at {target_time}") + + await asyncio.sleep(wait_seconds) + + logger.info("Starting daily material consumption sync...") + try: + result = await sync_all_advertisers_consumption() + logger.info(f"Daily sync completed: {result['message']}") + except Exception as e: + logger.error(f"Daily sync failed: {e}") + + except asyncio.CancelledError: + logger.info("Daily sync task cancelled") + break + except Exception as e: + logger.error(f"Error in schedule_daily_sync: {e}") + await asyncio.sleep(60) \ No newline at end of file diff --git a/video-gen-api/app/utils/douyinApi.py b/video-gen-api/app/utils/douyinApi.py index e7bbbaca..87d2b7fa 100644 --- a/video-gen-api/app/utils/douyinApi.py +++ b/video-gen-api/app/utils/douyinApi.py @@ -78,4 +78,28 @@ class DouyinApi: url, 'POST', {"json": params or {}} + ) + + #获取素材消耗 + async def get_material_cost(self, oauth_id: str, params: any, request_count: int = 3) -> Dict[str, Any]: + if not oauth_id: + raise RuntimeError('OAuth ID is not set.') + url = "https://api.oceanengine.com/open_api/v3.0/report/custom/get/" + return await self.request.request_with_token_with_context( + oauth_id, + url, + 'GET', + {'params': params or {}}, + request_count=request_count + ) + #获取账户信息 + async def get_account_info(self, oauth_id: str, params: any) -> Dict[str, Any]: + if not oauth_id: + raise RuntimeError('OAuth ID is not set.') + url = "https://api.oceanengine.com/open_api/2/agent/advertiser_info/query/" + return await self.request.request_with_token_with_context( + oauth_id, + url, + 'GET', + {'params': params or {}} ) \ No newline at end of file diff --git a/video-gen-api/app/utils/douyinRequest.py b/video-gen-api/app/utils/douyinRequest.py index 5a066b5a..5136ffca 100644 --- a/video-gen-api/app/utils/douyinRequest.py +++ b/video-gen-api/app/utils/douyinRequest.py @@ -187,11 +187,13 @@ class DouyinRequest: url: str, method: str = 'GET', options: any = None, + request_count: int = 1, ) -> Any: options = options or {} token = await self.get_access_token(oauth_id) - for i in range(1, 2): + for i in range(1, request_count+1): + # 有一些错误是触发频次管理的,需要重试 try: headers = options.get('headers', {}).copy() headers['Access-Token'] = token diff --git a/video-gen-app/.gitignore b/video-gen-app/.gitignore index 9cac2492..6f8391d9 100644 --- a/video-gen-app/.gitignore +++ b/video-gen-app/.gitignore @@ -8,7 +8,7 @@ pnpm-debug.log* lerna-debug.log* node_modules -dist +#dist dist-ssr *.local diff --git a/video-gen-app/dist/assets/bg1-5_KeKgjy.png b/video-gen-app/dist/assets/bg1-5_KeKgjy.png new file mode 100644 index 00000000..57d4ce8e Binary files /dev/null and b/video-gen-app/dist/assets/bg1-5_KeKgjy.png differ diff --git a/video-gen-app/dist/assets/bg2-DR6Q-s5B.png b/video-gen-app/dist/assets/bg2-DR6Q-s5B.png new file mode 100644 index 00000000..ac1692f4 Binary files /dev/null and b/video-gen-app/dist/assets/bg2-DR6Q-s5B.png differ diff --git a/video-gen-app/dist/assets/index-DTCz7Wbv.js b/video-gen-app/dist/assets/index-DTCz7Wbv.js new file mode 100644 index 00000000..97f553c0 --- /dev/null +++ b/video-gen-app/dist/assets/index-DTCz7Wbv.js @@ -0,0 +1,440 @@ +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));(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 l=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}}}})),u=o(((e,t)=>{t.exports=l()})),d=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=d()})),p=o((e=>{var t=f();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=p()})),h=o((e=>{var t=u(),n=f(),r=m();function i(e){var t=`https://react.dev/errors/`+e;if(1B||(e.current=z[B],z[B]=null,B--)}function U(e,t){B++,z[B]=e.current,e.current=t}var W=V(null),ee=V(null),G=V(null),K=V(null);function te(e,t){switch(U(G,t),U(ee,e),U(W,null),t.nodeType){case 9:case 11:e=(e=t.documentElement)&&(e=e.namespaceURI)?Hd(e):0;break;default:if(e=t.tagName,t=t.namespaceURI)t=Hd(t),e=Ud(t,e);else switch(e){case`svg`:e=1;break;case`math`:e=2;break;default:e=0}}H(W),U(W,e)}function q(){H(W),H(ee),H(G)}function J(e){e.memoizedState!==null&&U(K,e);var t=W.current,n=Ud(t,e.type);t!==n&&(U(ee,e),U(W,n))}function Y(e){ee.current===e&&(H(W),H(ee)),K.current===e&&(H(K),$f._currentValue=R)}var ne,X;function re(e){if(ne===void 0)try{throw Error()}catch(e){var t=e.stack.trim().match(/\n( *(at )?)/);ne=t&&t[1]||``,X=-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{ie=!1,Error.prepareStackTrace=n}return(n=e?e.displayName||e.name:``)?re(n):``}function oe(e,t){switch(e.tag){case 26:case 27:case 5:return re(e.type);case 16:return re(`Lazy`);case 13:return e.child!==t&&t!==null?re(`Suspense Fallback`):re(`Suspense`);case 19:return re(`SuspenseList`);case 0:case 15:return ae(e.type,!1);case 11:return ae(e.type.render,!1);case 1:return ae(e.type,!0);case 31:return re(`Activity`);default:return``}}function se(e){try{var t=``,n=null;do t+=oe(e,n),n=e,e=e.return;while(e);return t}catch(e){return` +Error generating stack: `+e.message+` +`+e.stack}}var ce=Object.prototype.hasOwnProperty,le=t.unstable_scheduleCallback,ue=t.unstable_cancelCallback,de=t.unstable_shouldYield,fe=t.unstable_requestPaint,pe=t.unstable_now,me=t.unstable_getCurrentPriorityLevel,he=t.unstable_ImmediatePriority,ge=t.unstable_UserBlockingPriority,_e=t.unstable_NormalPriority,ve=t.unstable_LowPriority,ye=t.unstable_IdlePriority,be=t.log,xe=t.unstable_setDisableYieldValue,Se=null,Ce=null;function we(e){if(typeof be==`function`&&xe(e),Ce&&typeof Ce.setStrictMode==`function`)try{Ce.setStrictMode(Se,e)}catch{}}var Te=Math.clz32?Math.clz32:Oe,Ee=Math.log,De=Math.LN2;function Oe(e){return e>>>=0,e===0?32:31-(Ee(e)/De|0)|0}var ke=256,Ae=262144,je=4194304;function Me(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 Ne(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=Me(n))):i=Me(o):i=Me(s)):(r=s&~a,r===0?(o&=s,o===0?n||(n=s&~e,n!==0&&(i=Me(n))):i=Me(o)):i=Me(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 Pe(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function Fe(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 Ie(){var e=je;return je<<=1,!(je&62914560)&&(je=4194304),e}function Le(e){for(var t=[],n=0;31>n;n++)t.push(e);return t}function Re(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function ze(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),Zt=!1;if(Xt)try{var Qt={};Object.defineProperty(Qt,`passive`,{get:function(){Zt=!0}}),window.addEventListener(`test`,Qt,Qt),window.removeEventListener(`test`,Qt,Qt)}catch{Zt=!1}var $t=null,en=null,tn=null;function nn(){if(tn)return tn;var e,t=en,n=t.length,r,i=`value`in $t?$t.value:$t.textContent,a=i.length;for(e=0;e=Fn),Rn=` `,zn=!1;function Bn(e,t){switch(e){case`keyup`:return Nn.indexOf(t.keyCode)!==-1;case`keydown`:return t.keyCode!==229;case`keypress`:case`mousedown`:case`focusout`:return!0;default:return!1}}function Vn(e){return e=e.detail,typeof e==`object`&&`data`in e?e.data:null}var Hn=!1;function Un(e,t){switch(e){case`compositionend`:return Vn(t);case`keypress`:return t.which===32?(zn=!0,Rn):null;case`textInput`:return e=t.data,e===Rn&&zn?null:e;default:return null}}function Wn(e,t){if(Hn)return e===`compositionend`||!Pn&&Bn(e,t)?(e=nn(),tn=en=$t=null,Hn=!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=fr(n)}}function mr(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?mr(e,t.parentNode):`contains`in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function hr(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=wt(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=wt(e.document)}return t}function gr(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 _r=Xt&&`documentMode`in document&&11>=document.documentMode,vr=null,yr=null,br=null,xr=!1;function Sr(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;xr||vr==null||vr!==wt(r)||(r=vr,`selectionStart`in r&&gr(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}),br&&dr(br,r)||(br=r,r=Ed(yr,`onSelect`),0>=o,i-=o,pi=1<<32-Te(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),Si&&hi(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),Si&&hi(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 Si&&hi(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)}),Si&&hi(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&&ha(l)===r.type){n(e,r.sibling),c=a(r,o.props),Sa(c,o),c.return=e,e=c;break a}n(e,r);break}else t(e,r);r=r.sibling}o.type===y?(c=ei(o.props.children,e.mode,c,o.key),c.return=e,e=c):(c=$r(o.type,o.key,o.props,null,e.mode,c),Sa(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=ri(o,e.mode,c),c.return=e,e=c}return s(e);case O:return o=ha(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,xa(o),c);if(o.$$typeof===C)return b(e,r,Wi(e,o),c);Ca(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=ti(o,e.mode,c),c.return=e,e=c),s(e)):n(e,r)}return function(e,t,n,r){try{ba=0;var i=b(e,t,n,r);return ya=null,i}catch(t){if(t===la||t===da)throw t;var a=Yr(29,t,null,e.mode);return a.lanes=r,a.return=e,a}}}var Ta=wa(!0),Ea=wa(!1),Da=!1;function Oa(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function ka(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 Aa(e){return{lane:e,tag:0,payload:null,callback:null,next:null}}function ja(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,Ml&2){var i=r.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),r.pending=t,t=Kr(e),Gr(e,null,n),t}return Ur(e,r,t,n),Kr(e)}function Ma(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,Ve(e,n)}}function Na(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 Pa=!1;function Fa(){if(Pa){var e=ea;if(e!==null)throw e}}function Ia(e,t,n,r){Pa=!1;var i=e.updateQueue;Da=!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?(Fl&f)===f:(r&f)===f){f!==0&&f===$i&&(Pa=!0),u!==null&&(u=u.next={lane:0,tag:s.tag,payload:s.payload,callback:null,next:null});a:{var m=e,g=s;f=t;var _=n;switch(g.tag){case 1:if(m=g.payload,typeof m==`function`){d=m.call(_,d,f);break a}d=m;break a;case 3:m.flags=m.flags&-65537|128;case 0:if(m=g.payload,f=typeof m==`function`?m.call(_,d,f):m,f==null)break a;d=h({},d,f);break a;case 2:Da=!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),Ul|=o,e.lanes=o,e.memoizedState=d}}function La(e,t){if(typeof e!=`function`)throw Error(i(191,e));e.call(t)}function Ra(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,Ts(e,!1,t,n);try{var c=i(),l=I.S;l!==null&&l(s,c),typeof c==`object`&&c&&typeof c.then==`function`?ws(e,t,ra(c,r),du(e)):ws(e,t,r,du(e))}catch(n){ws(e,t,{then:function(){},status:`rejected`,reason:n},du())}finally{L.p=a,o!==null&&s.types!==null&&(o.types=s.types),I.T=o}}function ms(){}function hs(e,t,n,r){if(e.tag!==5)throw Error(i(476));var a=gs(e).queue;ps(e,a,t,R,n===null?ms:function(){return _s(e),n(r)})}function gs(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:Eo,lastRenderedState:R},next:null};var n={};return t.next={memoizedState:n,baseState:n,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Eo,lastRenderedState:n},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function _s(e){var t=gs(e);t.next===null&&(t=e.alternate.memoizedState),ws(e,t.next.queue,{},du())}function vs(){return Ui($f)}function ys(){return xo().memoizedState}function bs(){return xo().memoizedState}function xs(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var n=du();e=Aa(n);var r=ja(t,e,n);r!==null&&(pu(r,t,n),Ma(r,t,n)),t={cache:Yi()},e.payload=t;return}t=t.return}}function Ss(e,t,n){var r=du();n={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null},Es(e)?Ds(t,n):(n=Wr(e,t,n,r),n!==null&&(pu(n,e,r),Os(n,t,r)))}function Cs(e,t,n){ws(e,t,n,du())}function ws(e,t,n,r){var i={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null};if(Es(e))Ds(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,ur(s,o))return Ur(e,t,i,0),Nl===null&&Hr(),!1}catch{}if(n=Wr(e,t,i,r),n!==null)return pu(n,e,r),Os(n,t,r),!0}return!1}function Ts(e,t,n,r){if(r={lane:2,revertLane:ud(),gesture:null,action:r,hasEagerState:!1,eagerState:null,next:null},Es(e)){if(t)throw Error(i(479))}else t=Wr(e,n,r,2),t!==null&&pu(t,e,2)}function Es(e){var t=e.alternate;return e===eo||t!==null&&t===eo}function Ds(e,t){io=ro=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Os(e,t,n){if(n&4194048){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,Ve(e,n)}}var ks={readContext:Ui,use:wo,useCallback:uo,useContext:uo,useEffect:uo,useImperativeHandle:uo,useLayoutEffect:uo,useInsertionEffect:uo,useMemo:uo,useReducer:uo,useRef:uo,useState:uo,useDebugValue:uo,useDeferredValue:uo,useTransition:uo,useSyncExternalStore:uo,useId:uo,useHostTransitionStatus:uo,useFormState:uo,useActionState:uo,useOptimistic:uo,useMemoCache:uo,useCacheRefresh:uo};ks.useEffectEvent=uo;var As={readContext:Ui,use:wo,useCallback:function(e,t){return bo().memoizedState=[e,t===void 0?null:t],e},useContext:Ui,useEffect:es,useImperativeHandle:function(e,t,n){n=n==null?null:n.concat([e]),Qo(4194308,4,os.bind(null,t,e),n)},useLayoutEffect:function(e,t){return Qo(4194308,4,e,t)},useInsertionEffect:function(e,t){Qo(4,2,e,t)},useMemo:function(e,t){var n=bo();t=t===void 0?null:t;var r=e();if(ao){we(!0);try{e()}finally{we(!1)}}return n.memoizedState=[r,t],r},useReducer:function(e,t,n){var r=bo();if(n!==void 0){var i=n(t);if(ao){we(!0);try{n(t)}finally{we(!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=Ss.bind(null,eo,e),[r.memoizedState,e]},useRef:function(e){var t=bo();return e={current:e},t.memoizedState=e},useState:function(e){e=Io(e);var t=e.queue,n=Cs.bind(null,eo,t);return t.dispatch=n,[e.memoizedState,n]},useDebugValue:cs,useDeferredValue:function(e,t){return ds(bo(),e,t)},useTransition:function(){var e=Io(!1);return e=ps.bind(null,eo,e.queue,!0,!1),bo().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,n){var r=eo,a=bo();if(Si){if(n===void 0)throw Error(i(407));n=n()}else{if(n=t(),Nl===null)throw Error(i(349));Fl&127||jo(r,t,n)}a.memoizedState=n;var o={value:n,getSnapshot:t};return a.queue=o,es(No.bind(null,r,o,e),[e]),r.flags|=2048,Xo(9,{destroy:void 0},Mo.bind(null,r,o,n,t),null),n},useId:function(){var e=bo(),t=Nl.identifierPrefix;if(Si){var n=mi,r=pi;n=(r&~(1<<32-Te(r)-1)).toString(32)+n,t=`_`+t+`R_`+n,n=oo++,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[Je]=t,o[Ye]=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(Fd(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&&wc(t)}}return kc(t),Tc(t,t.type,e===null?null:e.memoizedProps,t.pendingProps,n),null;case 6:if(e&&t.stateNode!=null)e.memoizedProps!==r&&wc(t);else{if(typeof r!=`string`&&t.stateNode===null)throw Error(i(166));if(e=G.current,ki(t)){if(e=t.stateNode,n=t.memoizedProps,r=null,a=bi,a!==null)switch(a.tag){case 27:case 5:r=a.memoizedProps}e[Je]=t,e=!!(e.nodeValue===n||r!==null&&!0===r.suppressHydrationWarning||Md(e.nodeValue,n)),e||Ei(t,!0)}else e=Vd(e).createTextNode(r),e[Je]=t,t.stateNode=e}return kc(t),null;case 31:if(n=t.memoizedState,e===null||e.memoizedState!==null){if(r=ki(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[Je]=t}else Ai(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;kc(t),e=!1}else n=ji(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=n),e=!0;if(!e)return t.flags&256?(Xa(t),t):(Xa(t),null);if(t.flags&128)throw Error(i(558))}return kc(t),null;case 13:if(r=t.memoizedState,e===null||e.memoizedState!==null&&e.memoizedState.dehydrated!==null){if(a=ki(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[Je]=t}else Ai(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;kc(t),a=!1}else a=ji(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=a),a=!0;if(!a)return t.flags&256?(Xa(t),t):(Xa(t),null)}return Xa(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),Dc(t,t.updateQueue),kc(t),null);case 4:return q(),e===null&&Sd(t.stateNode.containerInfo),kc(t),null;case 10:return Li(t.type),kc(t),null;case 19:if(H(Za),r=t.memoizedState,r===null)return kc(t),null;if(a=(t.flags&128)!=0,o=r.rendering,o===null)if(a)Oc(r,!1);else{if(Hl!==0||e!==null&&e.flags&128)for(e=t.child;e!==null;){if(o=Qa(e),o!==null){for(t.flags|=128,Oc(r,!1),e=o.updateQueue,t.updateQueue=e,Dc(t,e),t.subtreeFlags=0,e=n,n=t.child;n!==null;)Qr(n,e),n=n.sibling;return U(Za,Za.current&1|2),Si&&hi(t,r.treeForkCount),t.child}e=e.sibling}r.tail!==null&&pe()>$l&&(t.flags|=128,a=!0,Oc(r,!1),t.lanes=4194304)}else{if(!a)if(e=Qa(o),e!==null){if(t.flags|=128,a=!0,e=e.updateQueue,t.updateQueue=e,Dc(t,e),Oc(r,!0),r.tail===null&&r.tailMode===`hidden`&&!o.alternate&&!Si)return kc(t),null}else 2*pe()-r.renderingStartTime>$l&&n!==536870912&&(t.flags|=128,a=!0,Oc(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?(kc(t),null):(e=r.tail,r.rendering=e,r.tail=e.sibling,r.renderingStartTime=pe(),e.sibling=null,n=Za.current,U(Za,a?n&1|2:n&1),Si&&hi(t,r.treeForkCount),e);case 22:case 23:return Xa(t),Ua(),r=t.memoizedState!==null,e===null?r&&(t.flags|=8192):e.memoizedState!==null!==r&&(t.flags|=8192),r?n&536870912&&!(t.flags&128)&&(kc(t),t.subtreeFlags&6&&(t.flags|=8192)):kc(t),n=t.updateQueue,n!==null&&Dc(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(aa),null;case 24:return n=null,e!==null&&(n=e.memoizedState.cache),t.memoizedState.cache!==n&&(t.flags|=2048),Li(Ji),kc(t),null;case 25:return null;case 30:return null}throw Error(i(156,t.tag))}function jc(e,t){switch(vi(t),t.tag){case 1:return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Li(Ji),q(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 26:case 27:case 5:return Y(t),null;case 31:if(t.memoizedState!==null){if(Xa(t),t.alternate===null)throw Error(i(340));Ai()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 13:if(Xa(t),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(i(340));Ai()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return H(Za),null;case 4:return q(),null;case 10:return Li(t.type),null;case 22:case 23:return Xa(t),Ua(),e!==null&&H(aa),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 24:return Li(Ji),null;case 25:return null;default:return null}}function Mc(e,t){switch(vi(t),t.tag){case 3:Li(Ji),q();break;case 26:case 27:case 5:Y(t);break;case 4:q();break;case 31:t.memoizedState!==null&&Xa(t);break;case 13:Xa(t);break;case 19:H(Za);break;case 10:Li(t.type);break;case 22:case 23:Xa(t),Ua(),e!==null&&H(aa);break;case 24:Li(Ji)}}function Nc(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){Uu(t,t.return,e)}}function Pc(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){Uu(i,c,e)}}}r=r.next}while(r!==a)}}catch(e){Uu(t,t.return,e)}}function Fc(e){var t=e.updateQueue;if(t!==null){var n=e.stateNode;try{Ra(t,n)}catch(t){Uu(e,e.return,t)}}}function Ic(e,t,n){n.props=Ls(e.type,e.memoizedProps),n.state=e.memoizedState;try{n.componentWillUnmount()}catch(n){Uu(e,t,n)}}function Lc(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){Uu(e,t,n)}}function Rc(e,t){var n=e.ref,r=e.refCleanup;if(n!==null)if(typeof r==`function`)try{r()}catch(n){Uu(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){Uu(e,t,n)}else n.current=null}function zc(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){Uu(e,e.return,t)}}function Bc(e,t,n){try{var r=e.stateNode;Id(r,e.type,n,t),r[Ye]=t}catch(t){Uu(e,e.return,t)}}function Vc(e){return e.tag===5||e.tag===3||e.tag===26||e.tag===27&&Qd(e.type)||e.tag===4}function Hc(e){a:for(;;){for(;e.sibling===null;){if(e.return===null||Vc(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&&Qd(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 Uc(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=Vt));else if(r!==4&&(r===27&&Qd(e.type)&&(n=e.stateNode,t=null),e=e.child,e!==null))for(Uc(e,t,n),e=e.sibling;e!==null;)Uc(e,t,n),e=e.sibling}function Wc(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&&Qd(e.type)&&(n=e.stateNode),e=e.child,e!==null))for(Wc(e,t,n),e=e.sibling;e!==null;)Wc(e,t,n),e=e.sibling}function Gc(e){var t=e.stateNode,n=e.memoizedProps;try{for(var r=e.type,i=t.attributes;i.length;)t.removeAttributeNode(i[0]);Fd(t,r,n),t[Je]=e,t[Ye]=n}catch(t){Uu(e,e.return,t)}}var Kc=!1,qc=!1,Jc=!1,Yc=typeof WeakSet==`function`?WeakSet:Set,Xc=null;function Zc(e,t){if(e=e.containerInfo,zd=cp,e=hr(e),gr(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(Bd={focusedElem:e,selectionRange:n},cp=!1,Xc=t;Xc!==null;)if(t=Xc,e=t.child,t.subtreeFlags&1028&&e!==null)e.return=t,Xc=e;else for(;Xc!==null;){switch(t=Xc,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`))),Fd(o,r,n),o[Je]=e,st(o),r=o;break a;case`link`:var s=Hf(`link`,`href`,a).get(r+(n.href||``));if(s){for(var c=0;cg&&(o=g,g=h,h=o);var _=pr(s,h),v=pr(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=su,su=null;var o=ru,s=au;if(nu=0,iu=ru=null,au=0,Ml&6)throw Error(i(331));var c=Ml;if(Ml|=4,Dl(o.current),yl(o,o.current,s,n),Ml=c,rd(0,!1),Ce&&typeof Ce.onPostCommitFiberRoot==`function`)try{Ce.onPostCommitFiberRoot(Se,o)}catch{}return!0}finally{L.p=a,I.T=r,zu(e,t)}}function Hu(e,t,n){t=ai(n,t),t=Us(e.stateNode,t,2),e=ja(e,t,2),e!==null&&(Re(e,2),nd(e))}function Uu(e,t,n){if(e.tag===3)Hu(e,e,n);else for(;t!==null;){if(t.tag===3){Hu(t,e,n);break}else if(t.tag===1){var r=t.stateNode;if(typeof t.type.getDerivedStateFromError==`function`||typeof r.componentDidCatch==`function`&&(tu===null||!tu.has(r))){e=ai(n,e),n=Ws(2),r=ja(t,n,2),r!==null&&(Gs(n,r,t,e),Re(r,2),nd(r));break}}t=t.return}}function Wu(e,t,n){var r=e.pingCache;if(r===null){r=e.pingCache=new jl;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)||(Bl=!0,i.add(n),e=Gu.bind(null,e,t,n),t.then(e,e))}function Gu(e,t,n){var r=e.pingCache;r!==null&&r.delete(t),e.pingedLanes|=e.suspendedLanes&n,e.warmLanes&=~n,Nl===e&&(Fl&n)===n&&(Hl===4||Hl===3&&(Fl&62914560)===Fl&&300>pe()-Zl?!(Ml&2)&&bu(e,0):Gl|=n,ql===Fl&&(ql=0)),nd(e)}function Ku(e,t){t===0&&(t=Ie()),e=Z(e,t),e!==null&&(Re(e,t),nd(e))}function qu(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),Ku(e,n)}function Ju(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),Ku(e,n)}function Yu(e,t){return le(e,t)}var Xu=null,Zu=null,Qu=!1,$u=!1,ed=!1,td=0;function nd(e){e!==Zu&&e.next===null&&(Zu===null?Xu=Zu=e:Zu=Zu.next=e),$u=!0,Qu||(Qu=!0,ld())}function rd(e,t){if(!ed&&$u){ed=!0;do for(var n=!1,r=Xu;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-Te(42|e)+1)-1,a&=i&~(o&~s),a=a&201326741?a&201326741|1:a?a|2:0}a!==0&&(n=!0,cd(r,a))}else a=Fl,a=Ne(r,r===Nl?a:0,r.cancelPendingCommit!==null||r.timeoutHandle!==-1),!(a&3)||Pe(r,a)||(n=!0,cd(r,a));r=r.next}while(n);ed=!1}}function id(){ad()}function ad(){$u=Qu=!1;var e=0;td!==0&&Kd()&&(e=td);for(var t=pe(),n=null,r=Xu;r!==null;){var i=r.next,a=od(r,t);a===0?(r.next=null,n===null?Xu=i:n.next=i,i===null&&(Zu=n)):(n=r,(e!==0||a&3)&&($u=!0)),r=i}nu!==0&&nu!==5||rd(e,!1),td!==0&&(td=0)}function od(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&&Ld(d)&&(c=c.responseEnd,o+=u*(c`u`?null:document;function Sf(e,t,n){var r=xf;if(r&&typeof t==`string`&&t){var i=Et(t);i=`link[rel="`+e+`"][href="`+i+`"]`,typeof n==`string`&&(i+=`[crossorigin="`+n+`"]`),gf.has(i)||(gf.add(i),e={rel:e,crossOrigin:n,href:t},r.querySelector(i)===null&&(t=r.createElement(`link`),Fd(t,`link`,e),st(t),r.head.appendChild(t)))}}function Cf(e){vf.D(e),Sf(`dns-prefetch`,e,null)}function wf(e,t){vf.C(e,t),Sf(`preconnect`,e,t)}function Tf(e,t,n){vf.L(e,t,n);var r=xf;if(r&&e&&t){var i=`link[rel="preload"][as="`+Et(t)+`"]`;t===`image`&&n&&n.imageSrcSet?(i+=`[imagesrcset="`+Et(n.imageSrcSet)+`"]`,typeof n.imageSizes==`string`&&(i+=`[imagesizes="`+Et(n.imageSizes)+`"]`)):i+=`[href="`+Et(e)+`"]`;var a=i;switch(t){case`style`:a=jf(e);break;case`script`:a=Ff(e)}hf.has(a)||(e=h({rel:`preload`,href:t===`image`&&n&&n.imageSrcSet?void 0:e,as:t},n),hf.set(a,e),r.querySelector(i)!==null||t===`style`&&r.querySelector(Mf(a))||t===`script`&&r.querySelector(If(a))||(t=r.createElement(`link`),Fd(t,`link`,e),st(t),r.head.appendChild(t)))}}function Ef(e,t){vf.m(e,t);var n=xf;if(n&&e){var r=t&&typeof t.as==`string`?t.as:`script`,i=`link[rel="modulepreload"][as="`+Et(r)+`"][href="`+Et(e)+`"]`,a=i;switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:a=Ff(e)}if(!hf.has(a)&&(e=h({rel:`modulepreload`,href:e},t),hf.set(a,e),n.querySelector(i)===null)){switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:if(n.querySelector(If(a)))return}r=n.createElement(`link`),Fd(r,`link`,e),st(r),n.head.appendChild(r)}}}function Df(e,t,n){vf.S(e,t,n);var r=xf;if(r&&e){var i=ot(r).hoistableStyles,a=jf(e);t||=`default`;var o=i.get(a);if(!o){var s={loading:0,preload:null};if(o=r.querySelector(Mf(a)))s.loading=5;else{e=h({rel:`stylesheet`,href:e,"data-precedence":t},n),(n=hf.get(a))&&zf(e,n);var c=o=r.createElement(`link`);st(c),Fd(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,Rf(o,t,r)}o={type:`stylesheet`,instance:o,count:1,state:s},i.set(a,o)}}}function Of(e,t){vf.X(e,t);var n=xf;if(n&&e){var r=ot(n).hoistableScripts,i=Ff(e),a=r.get(i);a||(a=n.querySelector(If(i)),a||(e=h({src:e,async:!0},t),(t=hf.get(i))&&Bf(e,t),a=n.createElement(`script`),st(a),Fd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function kf(e,t){vf.M(e,t);var n=xf;if(n&&e){var r=ot(n).hoistableScripts,i=Ff(e),a=r.get(i);a||(a=n.querySelector(If(i)),a||(e=h({src:e,async:!0,type:`module`},t),(t=hf.get(i))&&Bf(e,t),a=n.createElement(`script`),st(a),Fd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function Af(e,t,n,r){var a=(a=G.current)?_f(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=jf(n.href),n=ot(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=jf(n.href);var o=ot(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(Mf(e)))&&!o._p&&(s.instance=o,s.state.loading=5),hf.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},hf.set(e,n),o||Pf(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=Ff(n),n=ot(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 jf(e){return`href="`+Et(e)+`"`}function Mf(e){return`link[rel="stylesheet"][`+e+`]`}function Nf(e){return h({},e,{"data-precedence":e.precedence,precedence:null})}function Pf(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}),Fd(t,`link`,n),st(t),e.head.appendChild(t))}function Ff(e){return`[src="`+Et(e)+`"]`}function If(e){return`script[async]`+e}function Lf(e,t,n){if(t.count++,t.instance===null)switch(t.type){case`style`:var r=e.querySelector(`style[data-href~="`+Et(n.href)+`"]`);if(r)return t.instance=r,st(r),r;var a=h({},n,{"data-href":n.href,"data-precedence":n.precedence,href:null,precedence:null});return r=(e.ownerDocument||e).createElement(`style`),st(r),Fd(r,`style`,a),Rf(r,n.precedence,e),t.instance=r;case`stylesheet`:a=jf(n.href);var o=e.querySelector(Mf(a));if(o)return t.state.loading|=4,t.instance=o,st(o),o;r=Nf(n),(a=hf.get(a))&&zf(r,a),o=(e.ownerDocument||e).createElement(`link`),st(o);var s=o;return s._p=new Promise(function(e,t){s.onload=e,s.onerror=t}),Fd(o,`link`,r),t.state.loading|=4,Rf(o,n.precedence,e),t.instance=o;case`script`:return o=Ff(n.src),(a=e.querySelector(If(o)))?(t.instance=a,st(a),a):(r=n,(a=hf.get(o))&&(r=h({},n),Bf(r,a)),e=e.ownerDocument||e,a=e.createElement(`script`),st(a),Fd(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,Rf(r,n.precedence,e));return t.instance}function Rf(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 Wf(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 Gf(e){return!(e.type===`stylesheet`&&!(e.state.loading&3))}function Kf(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=jf(r.href),a=t.querySelector(Mf(i));if(a){t=a._p,typeof t==`object`&&t&&typeof t.then==`function`&&(e.count++,e=Yf.bind(e),t.then(e,e)),n.state.loading|=4,n.instance=a,st(a);return}a=t.ownerDocument||t,r=Nf(r),(i=hf.get(i))&&zf(r,i),a=a.createElement(`link`),st(a);var o=a;o._p=new Promise(function(e,t){o.onload=e,o.onerror=t}),Fd(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=Yf.bind(e),t.addEventListener(`load`,n),t.addEventListener(`error`,n))}}var qf=0;function Jf(e,t){return e.stylesheets&&e.count===0&&Zf(e,e.stylesheets),0qf?50:800)+t);return e.unsuspend=n,function(){e.unsuspend=null,clearTimeout(r),clearTimeout(i)}}:null}function Yf(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)Zf(this,this.stylesheets);else if(this.unsuspend){var e=this.unsuspend;this.unsuspend=null,e()}}}var Xf=null;function Zf(e,t){e.stylesheets=null,e.unsuspend!==null&&(e.count++,Xf=new Map,t.forEach(Qf,e),Xf=null,Yf.call(e))}function Qf(e,t){if(!(t.state.loading&4)){var n=Xf.get(e);if(n)var r=n.get(null);else{n=new Map,Xf.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=h()}))(),_=`modulepreload`,v=function(e){return`/`+e},y={},b=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=v(t,n),t in y)return;y[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`:_,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)})},x=c(f(),1),S=`popstate`;function C(e){return typeof e==`object`&&!!e&&`pathname`in e&&`search`in e&&`hash`in e&&`state`in e&&`key`in e}function w(e={}){function t(e,t){let n=t.state?.masked,{pathname:r,search:i,hash:a}=n||e.location;return k(``,{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:A(t)}return M(t,n,null,e)}function T(e,t){if(e===!1||e==null)throw Error(t)}function E(e,t){if(!e){typeof console<`u`&&console.warn(t);try{throw Error(t)}catch{}}}function D(){return Math.random().toString(36).substring(2,10)}function O(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 k(e,t,n=null,r,i){return{pathname:typeof e==`string`?e:e.pathname,search:``,hash:``,...typeof t==`string`?j(t):t,state:n,key:t&&t.key||r||D(),mask:i}}function A({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 j(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 M(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=C(e)?e:k(h.location,e,t);n&&n(r,e),l=u()+1;let d=O(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=C(e)?e:k(h.location,e,t);n&&n(r,e),l=u();let i=O(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 N(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(S,d),c=e,()=>{i.removeEventListener(S,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 N(e,t=!1){let n=`http://localhost`;typeof window<`u`&&(n=window.location.origin===`null`?window.location.href:window.location.origin),T(n,`No window.location.(origin|href) available to create URL`);let r=typeof e==`string`?e:A(e);return r=r.replace(/ $/,`%20`),!t&&r.startsWith(`//`)&&(r=n+r),new URL(r,n)}function P(e,t,n=`/`){return F(e,t,n,!1)}function F(e,t,n,r,i){let a=re((typeof t==`string`?j(t):t).pathname||`/`,n);if(a==null)return null;let o=i??L(e),s=null,c=X(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;T(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=fe([r,c.relativePath]),u=n.concat(c);e.children&&e.children.length>0&&(T(e.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${l}".`),R(e.children,t,u,l,o)),!(e.path==null&&!e.index)&&t.push({path:l,score:te(l,e.index),routesMeta:u})};return e.forEach((e,t)=>{if(e.path===``||!e.path?.includes(`?`))a(e,t);else for(let n of z(e.path))a(e,t,!0,n)}),t}function z(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=z(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 B(e){e.sort((e,t)=>e.score===t.score?q(e.routesMeta.map(e=>e.childrenIndex),t.routesMeta.map(e=>e.childrenIndex)):t.score-e.score)}var V=/^:[\w-]+$/,H=3,U=2,W=1,ee=10,G=-2,K=e=>e===`*`;function te(e,t){let n=e.split(`/`),r=n.length;return n.some(K)&&(r+=G),t&&(r+=U),n.filter(e=>!K(e)).reduce((e,t)=>e+(V.test(t)?H:t===``?W:ee),r)}function q(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 J(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 ne(e,t=!1,n=!0){E(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 X(e){try{return e.split(`/`).map(e=>decodeURIComponent(e).replace(/\//g,`%2F`)).join(`/`)}catch(t){return E(!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 re(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 ie=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;function ae(e,t=`/`){let{pathname:n,search:r=``,hash:i=``}=typeof e==`string`?j(e):e,a;return n?(n=de(n),a=n.startsWith(`/`)?oe(n.substring(1),`/`):oe(n,t)):a=t,{pathname:a,search:he(r),hash:ge(i)}}function oe(e,t){let n=pe(t).split(`/`);return e.split(`/`).forEach(e=>{e===`..`?n.length>1&&n.pop():e!==`.`&&n.push(e)}),n.length>1?n.join(`/`):`/`}function se(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 ce(e){return e.filter((e,t)=>t===0||e.route.path&&e.route.path.length>0)}function le(e){let t=ce(e);return t.map((e,n)=>n===t.length-1?e.pathname:e.pathnameBase)}function ue(e,t,n,r=!1){let i;typeof e==`string`?i=j(e):(i={...e},T(!i.pathname||!i.pathname.includes(`?`),se(`?`,`pathname`,`search`,i)),T(!i.pathname||!i.pathname.includes(`#`),se(`#`,`pathname`,`hash`,i)),T(!i.search||!i.search.includes(`#`),se(`#`,`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=ae(i,s),l=o&&o!==`/`&&o.endsWith(`/`),u=(a||o===`.`)&&n.endsWith(`/`);return!c.pathname.endsWith(`/`)&&(l||u)&&(c.pathname+=`/`),c}var de=e=>e.replace(/\/\/+/g,`/`),fe=e=>de(e.join(`/`)),pe=e=>e.replace(/\/+$/,``),me=e=>pe(e).replace(/^\/*/,`/`),he=e=>!e||e===`?`?``:e.startsWith(`?`)?e:`?`+e,ge=e=>!e||e===`#`?``:e.startsWith(`#`)?e:`#`+e,_e=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 ve(e){return e!=null&&typeof e.status==`number`&&typeof e.statusText==`string`&&typeof e.internal==`boolean`&&`data`in e}function ye(e){return fe(e.map(e=>e.route.path).filter(Boolean))||`/`}var be=typeof window<`u`&&window.document!==void 0&&window.document.createElement!==void 0;function xe(e,t){let n=e;if(typeof n!=`string`||!ie.test(n))return{absoluteURL:void 0,isExternal:!1,to:n};let r=n,i=!1;if(be)try{let e=new URL(window.location.href),r=n.startsWith(`//`)?new URL(e.protocol+n):new URL(n),a=re(r.pathname,t);r.origin===e.origin&&a!=null?n=a+r.search+r.hash:i=!0}catch{E(!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 Se=[`POST`,`PUT`,`PATCH`,`DELETE`];new Set(Se);var Ce=[`GET`,...Se];new Set(Ce);var we=x.createContext(null);we.displayName=`DataRouter`;var Te=x.createContext(null);Te.displayName=`DataRouterState`;var Ee=x.createContext(!1);function De(){return x.useContext(Ee)}var Oe=x.createContext({isTransitioning:!1});Oe.displayName=`ViewTransition`;var ke=x.createContext(new Map);ke.displayName=`Fetchers`;var Ae=x.createContext(null);Ae.displayName=`Await`;var je=x.createContext(null);je.displayName=`Navigation`;var Me=x.createContext(null);Me.displayName=`Location`;var Ne=x.createContext({outlet:null,matches:[],isDataRoute:!1});Ne.displayName=`Route`;var Pe=x.createContext(null);Pe.displayName=`RouteError`;var Fe=`REACT_ROUTER_ERROR`,Ie=`REDIRECT`,Le=`ROUTE_ERROR_RESPONSE`;function Re(e){if(e.startsWith(`${Fe}:${Ie}:{`))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 ze(e){if(e.startsWith(`${Fe}:${Le}:{`))try{let t=JSON.parse(e.slice(40));if(typeof t==`object`&&t&&typeof t.status==`number`&&typeof t.statusText==`string`)return new _e(t.status,t.statusText,t.data)}catch{}}function Be(e,{relative:t}={}){T(Ve(),`useHref() may be used only in the context of a component.`);let{basename:n,navigator:r}=x.useContext(je),{hash:i,pathname:a,search:o}=Xe(e,{relative:t}),s=a;return n!==`/`&&(s=a===`/`?n:fe([n,a])),r.createHref({pathname:s,search:o,hash:i})}function Ve(){return x.useContext(Me)!=null}function He(){return T(Ve(),`useLocation() may be used only in the context of a component.`),x.useContext(Me).location}var Ue=`You should call navigate() in a React.useEffect(), not when your component is first rendered.`;function We(e){x.useContext(je).static||x.useLayoutEffect(e)}function Ge(){let{isDataRoute:e}=x.useContext(Ne);return e?ht():Ke()}function Ke(){T(Ve(),`useNavigate() may be used only in the context of a component.`);let e=x.useContext(we),{basename:t,navigator:n}=x.useContext(je),{matches:r}=x.useContext(Ne),{pathname:i}=He(),a=JSON.stringify(le(r)),o=x.useRef(!1);return We(()=>{o.current=!0}),x.useCallback((r,s={})=>{if(E(o.current,Ue),!o.current)return;if(typeof r==`number`){n.go(r);return}let c=ue(r,JSON.parse(a),i,s.relative===`path`);e==null&&t!==`/`&&(c.pathname=c.pathname===`/`?t:fe([t,c.pathname])),(s.replace?n.replace:n.push)(c,s.state,s)},[t,n,a,i,e])}var qe=x.createContext(null);function Je(e){let t=x.useContext(Ne).outlet;return x.useMemo(()=>t&&x.createElement(qe.Provider,{value:e},t),[t,e])}function Ye(){let{matches:e}=x.useContext(Ne);return e[e.length-1]?.params??{}}function Xe(e,{relative:t}={}){let{matches:n}=x.useContext(Ne),{pathname:r}=He(),i=JSON.stringify(le(n));return x.useMemo(()=>ue(e,JSON.parse(i),r,t===`path`),[e,i,r,t])}function Ze(e,t){return Qe(e,t)}function Qe(e,t,n){T(Ve(),`useRoutes() may be used only in the context of a component.`);let{navigator:r}=x.useContext(je),{matches:i}=x.useContext(Ne),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||``;_t(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=He(),d;if(t){let e=typeof t==`string`?j(t):t;T(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})):P(e,{pathname:p});E(l||m!=null,`No routes matched location "${d.pathname}${d.search}${d.hash}" `),E(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=at(m&&m.map(e=>Object.assign({},e,{params:Object.assign({},o,e.params),pathname:fe([c,r.encodeLocation?r.encodeLocation(e.pathname.replace(/%/g,`%25`).replace(/\?/g,`%3F`).replace(/#/g,`%23`)).pathname:e.pathname]),pathnameBase:e.pathnameBase===`/`?c:fe([c,r.encodeLocation?r.encodeLocation(e.pathnameBase.replace(/%/g,`%25`).replace(/\?/g,`%3F`).replace(/#/g,`%23`)).pathname:e.pathnameBase])})),i,n);return t&&h?x.createElement(Me.Provider,{value:{location:{pathname:`/`,search:``,hash:``,state:null,key:`default`,mask:void 0,...d},navigationType:`POP`}},h):h}function $e(){let e=mt(),t=ve(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=x.createElement(x.Fragment,null,x.createElement(`p`,null,`💿 Hey developer 👋`),x.createElement(`p`,null,`You can provide a way better UX than this when your app throws errors by providing your own `,x.createElement(`code`,{style:a},`ErrorBoundary`),` or`,` `,x.createElement(`code`,{style:a},`errorElement`),` prop on your route.`)),x.createElement(x.Fragment,null,x.createElement(`h2`,null,`Unexpected Application Error!`),x.createElement(`h3`,{style:{fontStyle:`italic`}},t),n?x.createElement(`pre`,{style:i},n):null,o)}var et=x.createElement($e,null),tt=class extends x.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=ze(e.digest);t&&(e=t)}let t=e===void 0?this.props.children:x.createElement(Ne.Provider,{value:this.props.routeContext},x.createElement(Pe.Provider,{value:e,children:this.props.component}));return this.context?x.createElement(rt,{error:e},t):t}};tt.contextType=Ee;var nt=new WeakMap;function rt({children:e,error:t}){let{basename:n}=x.useContext(je);if(typeof t==`object`&&t&&`digest`in t&&typeof t.digest==`string`){let e=Re(t.digest);if(e){let r=nt.get(t);if(r)throw r;let i=xe(e.location,n);if(be&&!nt.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 nt.set(t,n),n}return x.createElement(`meta`,{httpEquiv:`refresh`,content:`0;url=${i.absoluteURL||i.to}`})}}return e}function it({routeContext:e,match:t,children:n}){let r=x.useContext(we);return r&&r.static&&r.staticContext&&(t.route.errorElement||t.route.ErrorBoundary)&&(r.staticContext._deepestRenderedBoundaryId=t.route.id),x.createElement(Ne.Provider,{value:e},n)}function at(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);T(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:ye(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||et,o&&(s<0&&c===0?(_t(`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?x.createElement(n.route.Component,null):n.route.element?n.route.element:e,x.createElement(it,{match:n,routeContext:{outlet:e,matches:m,isDataRoute:r!=null},children:t})};return r&&(n.route.ErrorBoundary||n.route.errorElement||c===0)?x.createElement(tt,{location:r.location,revalidation:r.revalidation,component:f,error:u,children:h(),routeContext:{outlet:null,matches:m,isDataRoute:!0},onError:l}):h()},null)}function ot(e){return`${e} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function st(e){let t=x.useContext(we);return T(t,ot(e)),t}function ct(e){let t=x.useContext(Te);return T(t,ot(e)),t}function lt(e){let t=x.useContext(Ne);return T(t,ot(e)),t}function ut(e){let t=lt(e),n=t.matches[t.matches.length-1];return T(n.route.id,`${e} can only be used on routes that contain a unique "id"`),n.route.id}function dt(){return ut(`useRouteId`)}function ft(){return ct(`useNavigation`).navigation}function pt(){let{matches:e,loaderData:t}=ct(`useMatches`);return x.useMemo(()=>e.map(e=>I(e,t)),[e,t])}function mt(){let e=x.useContext(Pe),t=ct(`useRouteError`),n=ut(`useRouteError`);return e===void 0?t.errors?.[n]:e}function ht(){let{router:e}=st(`useNavigate`),t=ut(`useNavigate`),n=x.useRef(!1);return We(()=>{n.current=!0}),x.useCallback(async(r,i={})=>{E(n.current,Ue),n.current&&(typeof r==`number`?await e.navigate(r):await e.navigate(r,{fromRouteId:t,...i}))},[e,t])}var gt={};function _t(e,t,n){!t&&!gt[e]&&(gt[e]=!0,E(!1,n))}x.memo(vt);function vt({routes:e,manifest:t,future:n,state:r,isStatic:i,onError:a}){return Qe(e,void 0,{manifest:t,state:r,isStatic:i,onError:a,future:n})}function yt({to:e,replace:t,state:n,relative:r}){T(Ve(),` may be used only in the context of a component.`);let{static:i}=x.useContext(je);E(!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}=x.useContext(Ne),{pathname:o}=He(),s=Ge(),c=ue(e,le(a),o,r===`path`),l=JSON.stringify(c);return x.useEffect(()=>{s(JSON.parse(l),{replace:t,state:n,relative:r})},[s,l,r,t,n]),null}function bt(e){return Je(e.context)}function xt(e){T(!1,`A is only ever to be used as the child of element, never rendered directly. Please wrap your in a .`)}function St({basename:e=`/`,children:t=null,location:n,navigationType:r=`POP`,navigator:i,static:a=!1,useTransitions:o}){T(!Ve(),`You cannot render a inside another . You should never have more than one in your app.`);let s=e.replace(/^\/*/,`/`),c=x.useMemo(()=>({basename:s,navigator:i,static:a,useTransitions:o,future:{}}),[s,i,a,o]);typeof n==`string`&&(n=j(n));let{pathname:l=`/`,search:u=``,hash:d=``,state:f=null,key:p=`default`,mask:m}=n,h=x.useMemo(()=>{let e=re(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 E(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:x.createElement(je.Provider,{value:c},x.createElement(Me.Provider,{children:t,value:h}))}function Ct({children:e,location:t}){return Ze(wt(e),t)}x.Component;function wt(e,t=[]){let n=[];return x.Children.forEach(e,(e,r)=>{if(!x.isValidElement(e))return;let i=[...t,r];if(e.type===x.Fragment){n.push.apply(n,wt(e.props.children,i));return}T(e.type===xt,`[${typeof e.type==`string`?e.type:e.type.name}] is not a component. All component children of must be a or `),T(!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=wt(e.props.children,i)),n.push(a)}),n}var Tt=`get`,Et=`application/x-www-form-urlencoded`;function Dt(e){return typeof HTMLElement<`u`&&e instanceof HTMLElement}function Ot(e){return Dt(e)&&e.tagName.toLowerCase()===`button`}function kt(e){return Dt(e)&&e.tagName.toLowerCase()===`form`}function At(e){return Dt(e)&&e.tagName.toLowerCase()===`input`}function jt(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function Mt(e,t){return e.button===0&&(!t||t===`_self`)&&!jt(e)}function Nt(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 Pt(e,t){let n=Nt(e);return t&&t.forEach((e,r)=>{n.has(r)||t.getAll(r).forEach(e=>{n.append(r,e)})}),n}var Ft=null;function It(){if(Ft===null)try{new FormData(document.createElement(`form`),0),Ft=!1}catch{Ft=!0}return Ft}var Lt=new Set([`application/x-www-form-urlencoded`,`multipart/form-data`,`text/plain`]);function Rt(e){return e!=null&&!Lt.has(e)?(E(!1,`"${e}" is not a valid \`encType\` for \`
\`/\`\` and will default to "${Et}"`),null):e}function zt(e,t){let n,r,i,a,o;if(kt(e)){let o=e.getAttribute(`action`);r=o?re(o,t):null,n=e.getAttribute(`method`)||Tt,i=Rt(e.getAttribute(`enctype`))||Et,a=new FormData(e)}else if(Ot(e)||At(e)&&(e.type===`submit`||e.type===`image`)){let o=e.form;if(o==null)throw Error(`Cannot submit a , - , ]} >
-
+ +
-
+
@@ -528,8 +541,9 @@ const VideoTrimPicker: React.FC = ({ gridTemplateColumns: `repeat(${Math.max(frames.length, 1)}, minmax(42px, 1fr))`, height: 86, overflow: 'hidden', - borderRadius: 10, - background: '#dbe2ff', + borderRadius: 12, + background: '#fff', + position: 'relative', }} > {frameLoading && frames.length === 0 ? ( @@ -538,23 +552,27 @@ const VideoTrimPicker: React.FC = ({ 正在抽取每秒帧...
) : frames.length > 0 ? ( - frames.map((frame) => ( -
) : ( -
+
{frame.second}s 无有效帧
@@ -580,15 +598,17 @@ const VideoTrimPicker: React.FC = ({ {frame.second}s - )) + ); + }) ) : (
暂无帧预览,请确认视频是否加载完成
)} +
-
+
= ({ disabled={!integerDuration || disabledByDuration} styles={{ track: { - background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', - height: 6, + background: '#6969dd63', + height: 70, borderRadius: 3, + margin: '0 ', }, rail: { - background: '#e2e8f0', - height: 6, + // background: '#0e447e70', + height: 70, borderRadius: 3, }, handle: { - width: 18, - height: 18, + width: 20, + height: 70, marginTop: 0, // backgroundColor: '#fff', - // border: '3px solid #6366f1', - // boxShadow: '0 2px 8px rgba(99, 102, 241, 0.3)', - transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)', + // border: '2px solid #6366f1', + borderRadius: '50%', }, }} />
-
+
已选取 {selectedDuration}s - - 最小 {minDuration}s,最大 {maxDuration}s -
{localError && ( -
+
{localError}
)} diff --git a/video-gen-app/src/pages/GenerateConver.tsx b/video-gen-app/src/pages/GenerateConver.tsx index 1d0fb3de..e97a3a3d 100644 --- a/video-gen-app/src/pages/GenerateConver.tsx +++ b/video-gen-app/src/pages/GenerateConver.tsx @@ -15,6 +15,10 @@ import { Modal, } from 'antd'; +import bg1 from '../assets/bg1.png'; +import bg2 from '../assets/bg2.png'; + + import { getParameters, createGenerationTask, getgen_list, getEngine, uploadImage, uploadVideo, getCreditRatios, deleteHistory, calculateCredits @@ -809,7 +813,7 @@ const AIChatPage: React.FC = () => { const handleClosePreview = () => { setPreviewVisible(false); - setPreviewUrl(''); + setPreviewUrl(''); if (videoRef.current) { videoRef.current.pause(); } @@ -830,7 +834,16 @@ const AIChatPage: React.FC = () => { // ==================== 渲染 ==================== return ( - + {/* 左侧边栏 - 对话列表(已隐藏,保留代码) */} {false && ( { collapsed={collapsed} width={220} style={{ - background: '#fff', - borderRight: '1px solid #f0f0f0', + background: 'rgba(255,255,255,0.7)', + backdropFilter: 'blur(20px)', + borderRight: '1px solid rgba(99, 102, 241, 0.08)', }} >
@@ -917,38 +931,46 @@ const AIChatPage: React.FC = () => { )} {/* 主内容区 */} - + {/* 头部 - 显示对话标题和模型信息 */}
-
- - {'开启创作'} - +
+
+ +
+
+ + 开启创作 + + {/*
*/} +
- {/*
- 模型: Gemini 3 Pro - 比例: 9:16 - 消耗 15 积分 -
*/}
{/* 消息区域 */} {/* 空状态 - 没有对话或当前对话没有消息时显示 */} @@ -982,10 +1004,15 @@ const AIChatPage: React.FC = () => {
{/* 加载更多按钮 - 在列表顶部 */} @@ -1015,81 +1042,56 @@ const AIChatPage: React.FC = () => {
-
+
{/* 头像 */}
- +
{/* 消息内容 */} -
+
{/* 时间戳和参数信息 */} -
- {msg.createdAt?.replace('T', ' ').split('.')[0]} - {/* 引擎标签 */} - - {/* */} - {msg.engineSnapshot.name} - - {/* 参数标签 */} - - {/* */} - {msg.genType === 'image' - ? `${msg.imageProportion || ''} · ${msg.imagePx || ''} · ${msg.imageSize || ''}` - : `${msg.duration || ''}s · ${msg.aspectRatio || ''} · ${msg.resolution || ''}` - } - - 消耗积分:{msg.creditsCost} - {msg.mediaReferences && msg.mediaReferences.length > 0 && ( - { - e.stopPropagation(); - const target = e.currentTarget as HTMLElement; - const rect = target.getBoundingClientRect(); - setAttachmentPopupPosition({ - x: rect.left, - y: rect.top - 10 - }); - setAttachmentPopupMessageId(msg.id); - setAttachmentPopupVisible(true); - }} - > - 附件详情 - - )} -
{/* 消息气泡 */}
+
+ {msg.createdAt?.replace('T', ' ').split('.')[0]} + +
{/* 删除按钮 - 右上角 */} -
+
{ @@ -1115,8 +1117,8 @@ const AIChatPage: React.FC = () => { height: 28, borderRadius: 8, border: 'none', - background: 'rgba(146, 144, 144, 1)', - color: '#ffffffff', + background: 'rgba(99, 102, 241, 0.08)', + color: '#6366f1', cursor: 'pointer', display: 'flex', alignItems: 'center', @@ -1125,35 +1127,36 @@ const AIChatPage: React.FC = () => { padding: 0, }} onMouseEnter={(e) => { - e.currentTarget.style.background = '#fff2f0'; - e.currentTarget.style.color = '#ff4d4f'; + e.currentTarget.style.background = 'rgba(239, 68, 68, 0.1)'; + e.currentTarget.style.color = '#ef4444'; }} onMouseLeave={(e) => { - e.currentTarget.style.background = 'rgba(0,0,0,0.05)'; - e.currentTarget.style.color = '#999'; + e.currentTarget.style.background = 'rgba(99, 102, 241, 0.08)'; + e.currentTarget.style.color = '#6366f1'; }} > - +
{/* 文本内容 */} -
{ setExpandedPrompts(prev => { @@ -1170,8 +1173,9 @@ const AIChatPage: React.FC = () => { }); }} > - {/* 默认显示:一行省略 */} + 默认显示:一行省略
{ }}> {msg.originalPrompt}
- - {/* 鼠标移入显示:完整内容 */} + + 鼠标移入显示:完整内容
{ }}> {msg.originalPrompt}
-
+
*/} {/* 根据 status 显示不同内容 */} {/* 生成中 - 显示加载动画 */} {msg.status === 'generating' && ( -
@@ -1231,13 +1236,13 @@ const AIChatPage: React.FC = () => { )} {/* 生成失败 - 显示失败提示 */} {msg.status === 'failed' && ( -
@@ -1268,14 +1273,14 @@ const AIChatPage: React.FC = () => { )} {/* 已完成 - 显示媒体内容 */} {msg.status === 'completed' && ( -
+
{ setPreviewUrl(msg.genType === 'image' ? msg.imageUrl : msg.videoUrl); setPreviewType(msg.genType === 'image' ? 'image' : 'video'); setPreviewVisible(true); }} - style={{ cursor: 'pointer', overflow: 'hidden', borderRadius: 8, position: 'relative', height: 200 }} + style={{ width: '50%', cursor: 'pointer', overflow: 'hidden', borderRadius: 8, position: 'relative', height: 200 }} > {msg.genType === 'image' ? ( { )}
+
+

+ {msg.originalPrompt} +

+
+ {/* 引擎标签 */} + + {/* */} + {msg.engineSnapshot.name} + + {/* 参数标签 */} + + {/* */} + {msg.genType === 'image' + ? `${msg.imageProportion || ''} · ${msg.imagePx || ''} · ${msg.imageSize || ''}` + : `${msg.duration || ''}s · ${msg.aspectRatio || ''} · ${msg.resolution || ''}` + } + + 消耗积分:{msg.creditsCost} + {msg.mediaReferences && msg.mediaReferences.length > 0 && ( + { + e.stopPropagation(); + const target = e.currentTarget as HTMLElement; + const rect = target.getBoundingClientRect(); + setAttachmentPopupPosition({ + x: rect.left, + y: rect.top - 10 + }); + setAttachmentPopupMessageId(msg.id); + setAttachmentPopupVisible(true); + }} + > + 附件详情 + + + )} +
+
)} @@ -1445,11 +1490,14 @@ const AIChatPage: React.FC = () => { {/* 输入区域 - 始终显示 */}
{/* 已上传媒体预览 */} @@ -1499,12 +1547,13 @@ const AIChatPage: React.FC = () => { {/* 输入框区域 */}
{/* 上传按钮 */} @@ -1518,15 +1567,15 @@ const AIChatPage: React.FC = () => { style={{ width: 48, height: 48, - borderRadius: 12, - border: '2px dashed #cbd5e1', + borderRadius: 14, + border: '2px dashed rgba(99, 102, 241, 0.2)', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', transition: 'all 0.25s ease', flexShrink: 0, - backgroundColor: '#f8fafc', + backgroundColor: 'rgba(255,255,255,0.7)', }} onMouseEnter={(e) => { e.currentTarget.style.borderColor = '#6366f1'; @@ -1534,8 +1583,8 @@ const AIChatPage: React.FC = () => { e.currentTarget.style.transform = 'scale(1.05)'; }} onMouseLeave={(e) => { - e.currentTarget.style.borderColor = '#cbd5e1'; - e.currentTarget.style.backgroundColor = '#f8fafc'; + e.currentTarget.style.borderColor = 'rgba(99, 102, 241, 0.2)'; + e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.7)'; e.currentTarget.style.transform = 'scale(1)'; }} > @@ -1557,6 +1606,7 @@ const AIChatPage: React.FC = () => { autoSize={{ minRows: 1, maxRows: 4 }} style={{ flex: 1, + height: '100%', borderRadius: 12, border: 'none', outline: 'none', @@ -1564,7 +1614,10 @@ const AIChatPage: React.FC = () => { fontSize: 14, lineHeight: 1.5, color: '#1e293b', - resize: 'none', + // resize: 'none', + + // boxShadow: 'none', + // padding: '8px 12px', }} disabled={loading} /> @@ -1582,14 +1635,16 @@ const AIChatPage: React.FC = () => { width: 48, height: 48, borderRadius: 14, - boxShadow: '0 4px 12px rgba(99, 102, 241, 0.35)', + background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', + boxShadow: '0 4px 16px rgba(99, 102, 241, 0.4)', transition: 'all 0.2s ease', + border: 'none', }} />
{/* 底部选择器 - 媒体类型和数量 */} -
+
{/* 文件类型选择器 */} {/*
{ onChange={(val) => setMediaType(val)} style={{ width: 100, - outline: 'none', - background: '#f1f5f9', - border: 'none', - borderRadius: 8, height: 34, + outline: 'none', + border: 'none', + backgroundColor: '#f1f5f9', + borderRadius: 10, }} size="middle" > @@ -1634,16 +1689,26 @@ const AIChatPage: React.FC = () => { className="image-settings-trigger" style={{ minWidth: 200, - padding: '4px 12px', + padding: '6px 14px', height: 34, - borderRadius: 8, + borderRadius: 10, border: 'none', backgroundColor: '#f1f5f9', + cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 6, transition: 'all 0.2s', + boxShadow: '0 2px 8px rgba(99, 102, 241, 0.04)', + }} + onMouseEnter={(e) => { + e.currentTarget.style.borderColor = 'rgba(99, 102, 241, 0.3)'; + e.currentTarget.style.boxShadow = '0 4px 12px rgba(99, 102, 241, 0.08)'; + }} + onMouseLeave={(e) => { + e.currentTarget.style.borderColor = 'rgba(99, 102, 241, 0.15)'; + e.currentTarget.style.boxShadow = '0 2px 8px rgba(99, 102, 241, 0.04)'; }} > @@ -1664,11 +1729,12 @@ const AIChatPage: React.FC = () => { bottom: 'calc(100% + 8px)', left: 0, width: 360, - backgroundColor: '#fff', - borderRadius: 16, - boxShadow: '0 10px 40px rgba(0,0,0,0.15)', + backgroundColor: 'rgba(255,255,255,0.95)', + backdropFilter: 'blur(20px)', + borderRadius: 14, + boxShadow: '0 12px 48px rgba(99, 102, 241, 0.15)', padding: 16, - border: 'none', + border: '1px solid rgba(99, 102, 241, 0.1)', zIndex: 9999, }} onClick={(e) => e.stopPropagation()} @@ -1679,8 +1745,8 @@ const AIChatPage: React.FC = () => { display: 'block', marginBottom: 8, fontSize: 12, - fontWeight: 500, - color: '#666666', + fontWeight: 600, + color: '#475569', }}> 选择引擎 @@ -1793,11 +1859,12 @@ const AIChatPage: React.FC = () => { bottom: 'calc(100% + 8px)', left: 0, width: 480, - backgroundColor: '#fff', - borderRadius: 16, - boxShadow: '0 10px 40px rgba(0,0,0,0.15)', + backgroundColor: 'rgba(255,255,255,0.95)', + backdropFilter: 'blur(20px)', + borderRadius: 14, + boxShadow: '0 12px 48px rgba(99, 102, 241, 0.15)', padding: 16, - border: 'none', + border: '1px solid rgba(99, 102, 241, 0.1)', zIndex: 9999, }} onClick={(e) => e.stopPropagation()} @@ -2074,11 +2141,12 @@ const AIChatPage: React.FC = () => { bottom: 'calc(100% + 8px)', left: 0, width: 400, - backgroundColor: '#fff', - borderRadius: 16, - boxShadow: '0 10px 40px rgba(0,0,0,0.15)', + backgroundColor: 'rgba(255,255,255,0.95)', + backdropFilter: 'blur(20px)', + borderRadius: 14, + boxShadow: '0 12px 48px rgba(99, 102, 241, 0.15)', padding: 16, - border: 'none', + border: '1px solid rgba(99, 102, 241, 0.1)', zIndex: 9999, }} onClick={(e) => e.stopPropagation()} @@ -2265,22 +2333,12 @@ const AIChatPage: React.FC = () => {
)} {/* 预估积分 */} -
- 预估积分 - {getEstimatedCredits()} +
+ 预估积分 + {getEstimatedCredits()}
- {/* 底部信息 */} - {/*
- GPT Video 2 - - - {mediaType === 'image' ? (selectedRatio === 'auto' ? '智能' : selectedRatio) : videoAspectRatio} - - {countType} - -
*/}
@@ -2299,7 +2357,7 @@ const AIChatPage: React.FC = () => { open={previewVisible} onCancel={handleClosePreview} footer={[ - , ]} @@ -2309,14 +2367,14 @@ const AIChatPage: React.FC = () => { } + style={{ borderRadius: 16 }} styles={{ body: { display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '400px', - } + }, }} >
@@ -2388,7 +2449,7 @@ const AIChatPage: React.FC = () => { document.body.appendChild(link); link.click(); document.body.removeChild(link); - }}> + }} style={{ background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', borderRadius: 10, border: 'none' }}> 下载 , ]} @@ -2398,14 +2459,14 @@ const AIChatPage: React.FC = () => { } + style={{ borderRadius: 16 }} styles={{ body: { display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '400px', - } + }, }} >
diff --git a/video-gen-app/src/pages/InitialInfo.tsx b/video-gen-app/src/pages/InitialInfo.tsx index 3d713738..6dcc0501 100644 --- a/video-gen-app/src/pages/InitialInfo.tsx +++ b/video-gen-app/src/pages/InitialInfo.tsx @@ -52,10 +52,13 @@ function InitialInfo() { const [currentPage, setCurrentPage] = useState(1); const [pageSize, setPageSize] = useState(20); const [total, setTotal] = useState(0); + const [searchKeyword, setSearchKeyword] = useState(''); // 任务详情数据 const [taskDetail, setTaskDetail] = useState({}); // API 返回的步骤数据 const [apiSteps, setApiSteps] = useState([]); + // 当前展开的步骤 + const [activeKey, setActiveKey] = useState([]); // const baseSteps = [ @@ -75,6 +78,21 @@ function InitialInfo() { engineId: apiSteps[index]?.input?.payload?.videoConfig?.engineId || '', })); + // 当apiSteps更新时,逆向遍历找到第一个已完成或失败的步骤并展开 + useEffect(() => { + for (let i = steps.length - 1; i >= 0; i--) { + if (steps[i].status === 'completed' || steps[i].status === 'failed') { + setActiveKey([String(steps[i].childId)]); + return; + } + } + setActiveKey([]); + }, [apiSteps]); + + // console.log('steps', steps); + + + @@ -82,8 +100,8 @@ function InitialInfo() { // 获取列表数据的函数 - const fetchList = (page: number, size: number) => { - getReplicationList(page, size).then((res: any) => { + const fetchList = (page: number, size: number, keyword?: string) => { + getReplicationList(page, size, keyword).then((res: any) => { if (res.items) { setTableData(res.items); } @@ -98,7 +116,13 @@ function InitialInfo() { const handlePageChange = (page: number, size: number) => { setCurrentPage(page); setPageSize(size); - fetchList(page, size); + fetchList(page, size, searchKeyword); + }; + + // 搜索处理 + const handleSearch = () => { + setCurrentPage(1); + fetchList(1, pageSize, searchKeyword); }; // 获取复刻列表数据 @@ -360,7 +384,7 @@ function InitialInfo() { } const agincreatevideo = () => { let params = { - engine_id: steps[2].engineId, + engine_id: steps[3].engineId, } @@ -546,13 +570,14 @@ function InitialInfo() {
({ - key: String(step.id), + key: String(step.childId), label: (
@@ -1072,7 +1097,7 @@ function InitialInfo() { <>
- 视频提词已生成,可点击按钮查看并修改后台 Schema 允许编辑的字段。 + 视频提词已生成,可点击按钮查看/修改
@@ -1083,7 +1108,7 @@ function InitialInfo() { style={{ flex: 1, borderRadius: 10, borderColor: 'rgba(99, 102, 241, 0.3)', color: '#6366f1', height: 36, fontWeight: 500, background: 'rgba(99, 102, 241, 0.04)' }} disabled={step.status !== 'completed' || !taskDetail?.videoGeneration?.promptSchema || !taskDetail?.videoGeneration?.schemaConfigSnapshot} > - 查看视频提词 + 查看/修改视频提词
diff --git a/video-gen-app/src/pages/InitialReplication.tsx b/video-gen-app/src/pages/InitialReplication.tsx index b6e01bfd..344d4d4a 100644 --- a/video-gen-app/src/pages/InitialReplication.tsx +++ b/video-gen-app/src/pages/InitialReplication.tsx @@ -34,6 +34,7 @@ const GenerateConver: React.FC = () => { const [currentPage, setCurrentPage] = useState(1); const [pageSize, setPageSize] = useState(20); const [total, setTotal] = useState(0); + const [searchKeyword, setSearchKeyword] = useState(''); // 卡片列表专用状态 const [cardData, setCardData] = useState([]); @@ -114,8 +115,8 @@ const GenerateConver: React.FC = () => { }; // 获取列表数据的函数 - const fetchList = (page: number, size: number, isPolling = false) => { - getReplicationList(page, size).then((res: any) => { + const fetchList = (page: number, size: number, isPolling = false, keyword?: string) => { + getReplicationList(page, size, keyword).then((res: any) => { if (res.items) { if (isPolling) { // 轮询时:增量更新,只更新状态发生变化的项目 @@ -139,7 +140,7 @@ const GenerateConver: React.FC = () => { // 如果有 processing 状态且轮询未启动,启动轮询 if (hasProcessing && !tablePollingTimer.current) { tablePollingTimer.current = setInterval(() => { - fetchList(currentPage, pageSize, true); + fetchList(currentPage, pageSize, true, searchKeyword); }, 5000); } // 如果没有 processing 状态且轮询正在运行,停止轮询 @@ -177,7 +178,13 @@ const GenerateConver: React.FC = () => { const handlePageChange = (page: number, size: number) => { setCurrentPage(page); setPageSize(size); - fetchList(page, size); + fetchList(page, size, false, searchKeyword); + }; + + // 搜索处理 + const handleSearch = () => { + setCurrentPage(1); + fetchList(1, pageSize, false, searchKeyword); }; // 文件上传前校验 @@ -205,7 +212,7 @@ const GenerateConver: React.FC = () => { resolve(false); return; } - if (video.duration > 15) { + if (video.duration >= 16) { message.error('视频时长不能超过15秒'); URL.revokeObjectURL(video.src); resolve(false); @@ -489,13 +496,14 @@ const GenerateConver: React.FC = () => { key={item.id} style={{ background: 'rgba(255,255,255,0.9)', + border: '1px solid rgba(99, 102, 241, 0.6)', backdropFilter: 'blur(10px)', borderRadius: 16, overflow: 'hidden', boxShadow: '0 4px 16px rgba(99, 102, 241, 0.06)', cursor: 'pointer', transition: 'transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.25s cubic-bezier(0.4, 0, 0.2, 1)', - border: '1px solid rgba(99, 102, 241, 0.06)', + // border: '1px solid rgba(99, 102, 241, 0.06)', }} onClick={() => navigate(`/initial/${item.id}/initialinfo`)} onMouseEnter={(e) => { @@ -1146,6 +1154,9 @@ const GenerateConver: React.FC = () => {
setSearchKeyword(e.target.value)} + onPressEnter={handleSearch} style={{ width: 220, borderRadius: 10, @@ -1156,6 +1167,7 @@ const GenerateConver: React.FC = () => { />
({ - key: String(step.id), + key: String(step.childId), label: (
@@ -1075,7 +1095,7 @@ function InitialInfo() { <>
- 视频提词已生成,可点击按钮查看并修改后台 Schema 允许编辑的字段。 + 视频提词已生成,可点击按钮查看/修改
@@ -1086,7 +1106,7 @@ function InitialInfo() { style={{ flex: 1, borderRadius: 10, borderColor: 'rgba(99, 102, 241, 0.3)', color: '#6366f1', height: 36, fontWeight: 500, background: 'rgba(99, 102, 241, 0.04)' }} disabled={step.status !== 'completed' || !taskDetail?.videoGeneration?.promptSchema || !taskDetail?.videoGeneration?.schemaConfigSnapshot} > - 查看视频提词 + 查看/修改视频提词