From b86a9c2af210e19a6d2ca61c8f4bcd286e1bef79 Mon Sep 17 00:00:00 2001 From: DevOps Bot Date: Mon, 6 Jul 2026 09:12:54 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E7=BB=9F=E4=B8=80=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=20-=20black=20+=20prettier=20=E5=85=A8?= =?UTF-8?q?=E9=87=8F=E6=A0=BC=E5=BC=8F=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../versions/025_add_user_wechat_fields.py | 5 +- apps/api/app/api/routes/asset_diagnosis.py | 11 +- apps/api/app/api/routes/edit_plans.py | 3 +- apps/web/src/api/assets.ts | 4 +- apps/web/src/components/layout/Header.tsx | 6 +- apps/web/src/components/layout/Sidebar.tsx | 4 +- apps/web/src/components/voice/CloneModal.tsx | 46 ++++-- apps/web/src/components/voice/clone-modal.css | 61 +++++-- apps/web/src/config/navigation.tsx | 14 +- apps/web/src/pages/assets/AssetLibrary.tsx | 32 +++- apps/web/src/pages/assets/assets.css | 13 +- apps/web/src/pages/auth/Login.css | 6 +- apps/web/src/pages/auth/Login.tsx | 18 +- apps/web/src/pages/auth/Register.css | 6 +- apps/web/src/pages/dashboard/Dashboard.tsx | 1 - apps/web/src/pages/dashboard/dashboard.css | 7 +- .../pages/editing-planner/EditingPlanner.css | 64 ++++++-- .../pages/editing-planner/EditingPlanner.tsx | 15 +- .../components/ClipPropertiesPanel.tsx | 12 +- .../components/GenerationHistoryModal.tsx | 18 +- .../components/PreviewPlayer.tsx | 4 +- .../components/TimelinePanel.tsx | 13 +- .../editing-planner/hooks/useUndoRedo.ts | 24 ++- apps/web/src/pages/generate/GeneratePage.tsx | 154 +++++++++++++----- apps/web/src/pages/generate/generate.css | 47 ++++-- .../src/pages/my-templates/MyTemplates.tsx | 5 +- .../src/pages/templates/TemplateLibrary.tsx | 20 ++- apps/web/src/pages/templates/templates.css | 1 - apps/web/src/pages/voice-clone/VoiceClone.tsx | 19 ++- .../web/src/pages/voice-clone/voice-clone.css | 10 +- packages/domain/config_schemas.py | 1 - tests/integration/test_duplication_api.py | 1 - .../test_duplication_upload_error_handling.py | 1 - tests/integration/test_subscription_api.py | 12 +- tests/unit/test_strenum_deserialization.py | 18 +- 35 files changed, 467 insertions(+), 209 deletions(-) diff --git a/alembic/versions/025_add_user_wechat_fields.py b/alembic/versions/025_add_user_wechat_fields.py index 2e0ef4ee0..4338ffd5d 100644 --- a/alembic/versions/025_add_user_wechat_fields.py +++ b/alembic/versions/025_add_user_wechat_fields.py @@ -23,10 +23,7 @@ def _column_exists(table: str, column: str) -> bool: """检查 PostgreSQL 表中某列是否已存在。""" conn = op.get_bind() result = conn.execute( - sa.text( - "SELECT 1 FROM information_schema.columns " - "WHERE table_name = :table AND column_name = :column" - ), + sa.text("SELECT 1 FROM information_schema.columns " "WHERE table_name = :table AND column_name = :column"), {"table": table, "column": column}, ) return result.scalar() is not None diff --git a/apps/api/app/api/routes/asset_diagnosis.py b/apps/api/app/api/routes/asset_diagnosis.py index f005a1b63..25b879869 100644 --- a/apps/api/app/api/routes/asset_diagnosis.py +++ b/apps/api/app/api/routes/asset_diagnosis.py @@ -198,13 +198,10 @@ def _build_single_asset_diagnosis(project_id: str, asset: Asset) -> ProjectAsset kind = _asset_kind(asset) is_ready = asset.status == AssetStatus.READY is_problem = asset.status in {AssetStatus.ERROR, AssetStatus.UPLOADING, AssetStatus.PROCESSING} - is_risky = ( - is_ready - and ( - (asset.quality_score is not None and asset.quality_score < 60) - or asset.metadata.get("review_status") == "rejected" - or asset.status == AssetStatus.ERROR - ) + is_risky = is_ready and ( + (asset.quality_score is not None and asset.quality_score < 60) + or asset.metadata.get("review_status") == "rejected" + or asset.status == AssetStatus.ERROR ) is_unclassified = is_ready and asset.classification_status.value in {"pending", "failed"} diff --git a/apps/api/app/api/routes/edit_plans.py b/apps/api/app/api/routes/edit_plans.py index fb859ac6f..84cc516b6 100644 --- a/apps/api/app/api/routes/edit_plans.py +++ b/apps/api/app/api/routes/edit_plans.py @@ -39,7 +39,6 @@ from packages.application.generation_tasks import ( from packages.domain.config_schemas import normalize_plan_config from packages.domain.edit_plan import EditPlan, EditPlanStatus - logger = logging.getLogger(__name__) router = APIRouter() @@ -662,6 +661,7 @@ def ai_recommend_clips( # 调用 AI 推荐服务(同步调用 stub,后续改为 Celery 异步) from apps.worker.worker_app.tasks.ai_tasks import run_ai_recommend + result = run_ai_recommend( plan_id=plan_id, template_id=plan.template_id, @@ -783,6 +783,7 @@ def generate_cover( # 调用 AI 封面生成服务 from apps.worker.worker_app.tasks.ai_tasks import run_generate_cover + cover_data = run_generate_cover( plan_id=plan_id, asset_ids=body.asset_ids, diff --git a/apps/web/src/api/assets.ts b/apps/web/src/api/assets.ts index bc7437207..2e7a6de59 100644 --- a/apps/web/src/api/assets.ts +++ b/apps/web/src/api/assets.ts @@ -83,7 +83,9 @@ export interface AssetDiagnosis { // ─── 素材诊断 ────────────────────────────────────────────── /** 获取素材诊断信息(可选 asset_id 查单素材,否则全局诊断) */ -export const getAssetDiagnosis = async (assetId?: string): Promise => { +export const getAssetDiagnosis = async ( + assetId?: string, +): Promise => { const params: Record = {}; if (assetId) params.asset_id = assetId; const response = await apiClient.get("/asset-diagnosis", { params }); diff --git a/apps/web/src/components/layout/Header.tsx b/apps/web/src/components/layout/Header.tsx index 3bb2e9aff..dacfec862 100644 --- a/apps/web/src/components/layout/Header.tsx +++ b/apps/web/src/components/layout/Header.tsx @@ -51,7 +51,11 @@ const Header: React.FC = () => { const isActive = (path: string) => { // 首页特殊处理:/ 和 /app/dashboard 都算激活 if (path === "/app/dashboard") { - return location.pathname === "/" || location.pathname === "/app" || location.pathname === "/app/dashboard"; + return ( + location.pathname === "/" || + location.pathname === "/app" || + location.pathname === "/app/dashboard" + ); } return location.pathname.startsWith(path); }; diff --git a/apps/web/src/components/layout/Sidebar.tsx b/apps/web/src/components/layout/Sidebar.tsx index 038db1b77..f0e6ad8c9 100644 --- a/apps/web/src/components/layout/Sidebar.tsx +++ b/apps/web/src/components/layout/Sidebar.tsx @@ -19,7 +19,9 @@ import "./Sidebar.css"; const isMenuItemActive = (pathname: string, path: string): boolean => { // 首页特殊处理:/ 和 /app/dashboard 都算激活 if (path === "/app/dashboard") { - return pathname === "/" || pathname === "/app" || pathname === "/app/dashboard"; + return ( + pathname === "/" || pathname === "/app" || pathname === "/app/dashboard" + ); } return pathname.startsWith(path); }; diff --git a/apps/web/src/components/voice/CloneModal.tsx b/apps/web/src/components/voice/CloneModal.tsx index 29d4d0b81..b962f2938 100644 --- a/apps/web/src/components/voice/CloneModal.tsx +++ b/apps/web/src/components/voice/CloneModal.tsx @@ -86,7 +86,10 @@ const CloneModal: React.FC = ({ return () => { if (timerRef.current) clearTimeout(timerRef.current); if (recordTimerRef.current) clearInterval(recordTimerRef.current); - if (mediaRecorderRef.current && mediaRecorderRef.current.state !== "inactive") { + if ( + mediaRecorderRef.current && + mediaRecorderRef.current.state !== "inactive" + ) { mediaRecorderRef.current.stop(); } }; @@ -108,7 +111,10 @@ const CloneModal: React.FC = ({ clearInterval(recordTimerRef.current); recordTimerRef.current = null; } - if (mediaRecorderRef.current && mediaRecorderRef.current.state !== "inactive") { + if ( + mediaRecorderRef.current && + mediaRecorderRef.current.state !== "inactive" + ) { mediaRecorderRef.current.stop(); } mediaRecorderRef.current = null; @@ -207,13 +213,18 @@ const CloneModal: React.FC = ({ clearInterval(recordTimerRef.current); recordTimerRef.current = null; } - if (mediaRecorderRef.current && mediaRecorderRef.current.state !== "inactive") { + if ( + mediaRecorderRef.current && + mediaRecorderRef.current.state !== "inactive" + ) { mediaRecorderRef.current.stop(); } } else { // 开始录制 try { - const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + const stream = await navigator.mediaDevices.getUserMedia({ + audio: true, + }); const mediaRecorder = new MediaRecorder(stream); mediaRecorderRef.current = mediaRecorder; audioChunksRef.current = []; @@ -250,7 +261,10 @@ const CloneModal: React.FC = ({ clearInterval(recordTimerRef.current); recordTimerRef.current = null; } - if (mediaRecorderRef.current && mediaRecorderRef.current.state !== "inactive") { + if ( + mediaRecorderRef.current && + mediaRecorderRef.current.state !== "inactive" + ) { mediaRecorderRef.current.stop(); } setErrorMessage("已达最长录制时长(5分钟),已自动停止"); @@ -311,9 +325,13 @@ const CloneModal: React.FC = ({ fileToUpload = selectedFile; } else { // 将录音 Blob 转为 File - fileToUpload = new File([recordedBlob!], `recorded-${Date.now()}.webm`, { - type: "audio/webm", - }); + fileToUpload = new File( + [recordedBlob!], + `recorded-${Date.now()}.webm`, + { + type: "audio/webm", + }, + ); } const formData = new FormData(); @@ -345,9 +363,7 @@ const CloneModal: React.FC = ({ /* ── 计算属性 ──────────────────────────────────────── */ const canSubmit = - voiceName.trim().length >= 2 && - voiceName.trim().length <= 20 && - hasAudio; + voiceName.trim().length >= 2 && voiceName.trim().length <= 20 && hasAudio; const isProcessing = phase === "uploading" || phase === "cloning"; @@ -564,7 +580,9 @@ const CloneModal: React.FC = ({
{isDone ? "✓" : step.icon}
- {step.label} + + {step.label} + ); @@ -585,7 +603,9 @@ const CloneModal: React.FC = ({ {phase === "cloning" && ( <>
-

AI 正在克隆你的声音…

+

+ AI 正在克隆你的声音… +

正在分析声音特征,生成专属音色模型

diff --git a/apps/web/src/components/voice/clone-modal.css b/apps/web/src/components/voice/clone-modal.css index 0332c9b5f..cbdb8ee08 100644 --- a/apps/web/src/components/voice/clone-modal.css +++ b/apps/web/src/components/voice/clone-modal.css @@ -345,15 +345,34 @@ animation: xx-clonemodal-wave 0.8s ease-in-out infinite alternate; } -.xx-clonemodal-record-wave-bar:nth-child(1) { height: 40%; animation-delay: 0s; } -.xx-clonemodal-record-wave-bar:nth-child(2) { height: 70%; animation-delay: 0.15s; } -.xx-clonemodal-record-wave-bar:nth-child(3) { height: 100%; animation-delay: 0.3s; } -.xx-clonemodal-record-wave-bar:nth-child(4) { height: 60%; animation-delay: 0.45s; } -.xx-clonemodal-record-wave-bar:nth-child(5) { height: 30%; animation-delay: 0.6s; } +.xx-clonemodal-record-wave-bar:nth-child(1) { + height: 40%; + animation-delay: 0s; +} +.xx-clonemodal-record-wave-bar:nth-child(2) { + height: 70%; + animation-delay: 0.15s; +} +.xx-clonemodal-record-wave-bar:nth-child(3) { + height: 100%; + animation-delay: 0.3s; +} +.xx-clonemodal-record-wave-bar:nth-child(4) { + height: 60%; + animation-delay: 0.45s; +} +.xx-clonemodal-record-wave-bar:nth-child(5) { + height: 30%; + animation-delay: 0.6s; +} @keyframes xx-clonemodal-wave { - from { transform: scaleY(0.4); } - to { transform: scaleY(1); } + from { + transform: scaleY(0.4); + } + to { + transform: scaleY(1); + } } /* 录制按钮 */ @@ -366,7 +385,10 @@ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); font-size: 20px; cursor: pointer; - transition: background 0.2s ease, transform 0.15s ease, box-shadow 0.2s ease; + transition: + background 0.2s ease, + transform 0.15s ease, + box-shadow 0.2s ease; display: flex; align-items: center; justify-content: center; @@ -390,8 +412,13 @@ } @keyframes xx-clonemodal-pulse { - 0%, 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4); } - 50% { box-shadow: 0 0 0 8px rgba(239, 68, 68, 0); } + 0%, + 100% { + box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4); + } + 50% { + box-shadow: 0 0 0 8px rgba(239, 68, 68, 0); + } } /* ── 进度阶段(上传中 / 克隆中 / 完成) ─────────────── */ @@ -522,9 +549,17 @@ } @keyframes xx-clonemodal-bounce { - 0% { transform: scale(0); opacity: 0; } - 60% { transform: scale(1.2); opacity: 1; } - 100% { transform: scale(1); } + 0% { + transform: scale(0); + opacity: 0; + } + 60% { + transform: scale(1.2); + opacity: 1; + } + 100% { + transform: scale(1); + } } .xx-clonemodal-success-title { diff --git a/apps/web/src/config/navigation.tsx b/apps/web/src/config/navigation.tsx index a66ef3fd9..e5e09a150 100644 --- a/apps/web/src/config/navigation.tsx +++ b/apps/web/src/config/navigation.tsx @@ -43,14 +43,24 @@ export const NAV_ITEMS: NavItem[] = [ path: "/app/dashboard", icon: , }, - { key: "assets", label: "素材库", path: "/app/assets", icon: }, + { + key: "assets", + label: "素材库", + path: "/app/assets", + icon: , + }, { key: "titles", label: "标题库", path: "/app/titles", icon: , }, - { key: "voices", label: "配音库", path: "/app/voices", icon: }, + { + key: "voices", + label: "配音库", + path: "/app/voices", + icon: , + }, { key: "voice-clone", label: "我的音色", diff --git a/apps/web/src/pages/assets/AssetLibrary.tsx b/apps/web/src/pages/assets/AssetLibrary.tsx index 4b6c7a588..e7129fff7 100644 --- a/apps/web/src/pages/assets/AssetLibrary.tsx +++ b/apps/web/src/pages/assets/AssetLibrary.tsx @@ -194,9 +194,23 @@ const SkeletonCard: React.FC = () => (
-
-
-
+
+
+
); @@ -668,8 +682,12 @@ const AssetLibrary: React.FC = () => {
) : assetsError ? (
-
-

{assetsErrorObj?.message || "加载失败"}

+
+ +
+

+ {assetsErrorObj?.message || "加载失败"} +

)}
diff --git a/apps/web/src/pages/assets/assets.css b/apps/web/src/pages/assets/assets.css index 115ade2ed..a27ab08a4 100644 --- a/apps/web/src/pages/assets/assets.css +++ b/apps/web/src/pages/assets/assets.css @@ -401,9 +401,15 @@ 骨架屏(素材列表加载占位) ============================================================ */ @keyframes xx-skeleton-pulse { - 0% { opacity: 1; } - 50% { opacity: 0.4; } - 100% { opacity: 1; } + 0% { + opacity: 1; + } + 50% { + opacity: 0.4; + } + 100% { + opacity: 1; + } } .xx-asset-skeleton { @@ -464,7 +470,6 @@ font-weight: var(--font-weight-semibold); } - /* ============================================================ 内联样式迁移类 ============================================================ */ diff --git a/apps/web/src/pages/auth/Login.css b/apps/web/src/pages/auth/Login.css index 67c0b5bf9..81d7fe6cd 100755 --- a/apps/web/src/pages/auth/Login.css +++ b/apps/web/src/pages/auth/Login.css @@ -33,13 +33,15 @@ border-radius: var(--radius-sm); background: linear-gradient(135deg, #6366f1, #4f46e5); border: none; - box-shadow: 0 4px 14px color-mix(in srgb, var(--primary-color) 30%, transparent); + box-shadow: 0 4px 14px + color-mix(in srgb, var(--primary-color) 30%, transparent); transition: all 0.2s; } .xx-auth-card .xx-btn-primary:hover { transform: translateY(-1px); - box-shadow: 0 6px 20px color-mix(in srgb, var(--primary-color) 40%, transparent); + box-shadow: 0 6px 20px + color-mix(in srgb, var(--primary-color) 40%, transparent); } /* ── 忘记密码链接 ───────────────────────────────────────── */ diff --git a/apps/web/src/pages/auth/Login.tsx b/apps/web/src/pages/auth/Login.tsx index 5e84a0ece..3801bae1c 100755 --- a/apps/web/src/pages/auth/Login.tsx +++ b/apps/web/src/pages/auth/Login.tsx @@ -71,11 +71,19 @@ const Login: React.FC = () => { -
+
记住我 - 忘记密码? + + 忘记密码? +
@@ -95,7 +103,11 @@ const Login: React.FC = () => {
- diff --git a/apps/web/src/pages/auth/Register.css b/apps/web/src/pages/auth/Register.css index 9fac6e57d..f7601780f 100755 --- a/apps/web/src/pages/auth/Register.css +++ b/apps/web/src/pages/auth/Register.css @@ -33,11 +33,13 @@ border-radius: var(--radius-sm); background: linear-gradient(135deg, #6366f1, #4f46e5); border: none; - box-shadow: 0 4px 14px color-mix(in srgb, var(--primary-color) 30%, transparent); + box-shadow: 0 4px 14px + color-mix(in srgb, var(--primary-color) 30%, transparent); transition: all 0.2s; } .xx-auth-card .xx-btn-primary:hover { transform: translateY(-1px); - box-shadow: 0 6px 20px color-mix(in srgb, var(--primary-color) 40%, transparent); + box-shadow: 0 6px 20px + color-mix(in srgb, var(--primary-color) 40%, transparent); } diff --git a/apps/web/src/pages/dashboard/Dashboard.tsx b/apps/web/src/pages/dashboard/Dashboard.tsx index 5a20e2cbc..f278b112e 100644 --- a/apps/web/src/pages/dashboard/Dashboard.tsx +++ b/apps/web/src/pages/dashboard/Dashboard.tsx @@ -245,7 +245,6 @@ const formatDate = () => { return `${d.getFullYear()}年${d.getMonth() + 1}月${d.getDate()}日 星期${weekDays[d.getDay()]}`; }; - /** 图标名称 → Ant Design 组件映射 */ const iconMap: Record = { video: , diff --git a/apps/web/src/pages/dashboard/dashboard.css b/apps/web/src/pages/dashboard/dashboard.css index 6a4f55770..c9899c2be 100755 --- a/apps/web/src/pages/dashboard/dashboard.css +++ b/apps/web/src/pages/dashboard/dashboard.css @@ -44,11 +44,7 @@ } .xx-kpi-card { - background: linear-gradient( - 180deg, - var(--bg-primary), - var(--bg-secondary) - ); + background: linear-gradient(180deg, var(--bg-primary), var(--bg-secondary)); border: 1px solid var(--border-color); border-radius: var(--radius-lg); padding: 20px; @@ -484,7 +480,6 @@ margin-top: var(--space-xs); } - /* ============================================================ 迁移自内联样式的工具类 ============================================================ */ diff --git a/apps/web/src/pages/editing-planner/EditingPlanner.css b/apps/web/src/pages/editing-planner/EditingPlanner.css index 78402d9c5..96cd0c06d 100755 --- a/apps/web/src/pages/editing-planner/EditingPlanner.css +++ b/apps/web/src/pages/editing-planner/EditingPlanner.css @@ -504,7 +504,11 @@ display: flex; align-items: center; justify-content: center; - background: linear-gradient(135deg, var(--ep-bg-card-hover, #1a1a2e), var(--ep-bg-deepest, #0a0a14)); + background: linear-gradient( + 135deg, + var(--ep-bg-card-hover, #1a1a2e), + var(--ep-bg-deepest, #0a0a14) + ); height: calc(100% - 20px); position: relative; } @@ -572,7 +576,11 @@ .ep-cover-image { width: 100%; height: 100%; - background: linear-gradient(135deg, var(--ep-bg-card-hover, #1a1a2e), var(--ep-bg-card, #13131f)); + background: linear-gradient( + 135deg, + var(--ep-bg-card-hover, #1a1a2e), + var(--ep-bg-card, #13131f) + ); display: flex; align-items: center; justify-content: center; @@ -805,7 +813,9 @@ .ep-clip-card.selected { border-color: var(--primary, #6366f1); - box-shadow: 0 0 0 2px var(--primary-soft, #eef2ff), 0 4px 12px rgba(99, 102, 241, 0.2); + box-shadow: + 0 0 0 2px var(--primary-soft, #eef2ff), + 0 4px 12px rgba(99, 102, 241, 0.2); } .ep-clip-card.drag-over { @@ -819,7 +829,11 @@ .ep-clip-thumbnail { height: 52px; - background: linear-gradient(135deg, var(--bg-secondary, #f8fafc), var(--border-light, #f1f5f9)); + background: linear-gradient( + 135deg, + var(--bg-secondary, #f8fafc), + var(--border-light, #f1f5f9) + ); display: flex; align-items: center; justify-content: center; @@ -1124,7 +1138,10 @@ width: 100%; height: 36px; padding: 0 12px; - background: var(--gradient-primary, linear-gradient(135deg, #6366f1, #4f46e5)); + background: var( + --gradient-primary, + linear-gradient(135deg, #6366f1, #4f46e5) + ); border: none; border-radius: var(--radius-xs, 8px); color: #fff; @@ -1333,7 +1350,9 @@ stroke: var(--primary, #6366f1); stroke-width: 8; stroke-linecap: round; - transition: stroke-dashoffset 0.5s ease, stroke 0.3s; + transition: + stroke-dashoffset 0.5s ease, + stroke 0.3s; } .ep-gen-progress-pct { @@ -1475,7 +1494,9 @@ font-size: 11px; padding: 2px 4px; border-radius: 3px; - transition: color 0.2s, background 0.2s; + transition: + color 0.2s, + background 0.2s; display: inline-flex; align-items: center; gap: 3px; @@ -1490,11 +1511,15 @@ color: #5a4bd6; } - /* Skeleton loading */ @keyframes ep-skeleton-pulse { - 0%, 100% { opacity: 0.4; } - 50% { opacity: 0.8; } + 0%, + 100% { + opacity: 0.4; + } + 50% { + opacity: 0.8; + } } .ep-skeleton { @@ -1654,8 +1679,12 @@ } @keyframes ep-fade-in { - from { opacity: 0; } - to { opacity: 1; } + from { + opacity: 0; + } + to { + opacity: 1; + } } .ep-modal { @@ -1668,8 +1697,14 @@ } @keyframes ep-scale-in { - from { opacity: 0; transform: scale(0.95); } - to { opacity: 1; transform: scale(1); } + from { + opacity: 0; + transform: scale(0.95); + } + to { + opacity: 1; + transform: scale(1); + } } .ep-modal-header { @@ -1751,4 +1786,3 @@ text-align: center; min-height: 80px; } - diff --git a/apps/web/src/pages/editing-planner/EditingPlanner.tsx b/apps/web/src/pages/editing-planner/EditingPlanner.tsx index 3ab13a5f6..50cf96ac8 100755 --- a/apps/web/src/pages/editing-planner/EditingPlanner.tsx +++ b/apps/web/src/pages/editing-planner/EditingPlanner.tsx @@ -25,7 +25,11 @@ import { generateFromTemplate, MODE_LABELS, } from "@/api/editingPlanner"; -import type { EditPlanClip, EditPlanGeneration, MediaAsset } from "@/api/editPlans"; +import type { + EditPlanClip, + EditPlanGeneration, + MediaAsset, +} from "@/api/editPlans"; import { getMediaAssets, getEditPlanGenerations, @@ -387,7 +391,9 @@ const EditingPlanner: React.FC = () => { asset_ids: assetIds, cover_type: coverType, }); - setCurrentCoverScheme(coverType === "ai_frame" ? "ai_frame" : "ai_reselect"); + setCurrentCoverScheme( + coverType === "ai_frame" ? "ai_frame" : "ai_reselect", + ); msg.success("AI 封面生成成功"); } catch { msg.error("AI 封面生成失败"); @@ -750,10 +756,7 @@ const EditingPlanner: React.FC = () => { | 📐 模板片段: {currentTemplate?.segments.length || 0} | -
diff --git a/apps/web/src/pages/editing-planner/components/ClipPropertiesPanel.tsx b/apps/web/src/pages/editing-planner/components/ClipPropertiesPanel.tsx index 9b27c9cbe..2533c8781 100644 --- a/apps/web/src/pages/editing-planner/components/ClipPropertiesPanel.tsx +++ b/apps/web/src/pages/editing-planner/components/ClipPropertiesPanel.tsx @@ -287,7 +287,9 @@ const ClipPropertiesPanel: React.FC = ({ onSubtitleSettingsChange({ size: Number(e.target.value) }) } /> - {subtitleSettings.size}px + + {subtitleSettings.size}px +
@@ -323,9 +325,7 @@ const ClipPropertiesPanel: React.FC = ({
-
+
已选 {selectedMaterials.length} 个素材 @@ -586,7 +599,9 @@ const GeneratePage: React.FC = () => { alignItems: "center", gap: 10, padding: "8px 12px", - background: checked ? "var(--primary-soft, #eef2ff)" : "#f8fafc", + background: checked + ? "var(--primary-soft, #eef2ff)" + : "#f8fafc", borderRadius: 10, cursor: "pointer", border: checked @@ -607,10 +622,21 @@ const GeneratePage: React.FC = () => { }} style={{ accentColor: "var(--primary-color, #4f46e5)" }} /> - + {m.name} - + {m.mime_type.split("/")[1].toUpperCase()} @@ -628,10 +654,7 @@ const GeneratePage: React.FC = () => {

📝 选择标题

- setTitle(e.target.value)}> {TITLE_OPTIONS.map((t) => (
{/* 试听按钮 */} {presetVoices.length > 0 && ( -
+
{presetVoices.slice(0, 4).map((v) => ( ))} @@ -782,16 +816,29 @@ const GeneratePage: React.FC = () => { disabled={!customVoiceText.trim() || synthesizeMutation.isPending} onClick={handleSynthesizeVoice} > - {synthesizeMutation.isPending ? "合成中…" : "合成语音"} + {" "} + {synthesizeMutation.isPending ? "合成中…" : "合成语音"}
{ttsError && ( - + {ttsError} )} {customAudioUrl && ( - + ✓ 语音合成完成 )} @@ -802,7 +849,9 @@ const GeneratePage: React.FC = () => { {voiceMode === "clone" && (

克隆我的声音

- + 上传一段您的语音样本,AI 将克隆您的声音用于视频配音 @@ -856,14 +905,21 @@ const GeneratePage: React.FC = () => { {statusCfg.label} {isReady && ( - + {formatDuration(cv.duration_seconds)} )}
{selected && isReady && ( - + )}
); @@ -872,12 +928,26 @@ const GeneratePage: React.FC = () => { )} {clonedVoices.length === 0 && ( - + 暂无克隆音色,点击「克隆新声音」开始 )} - + 💡 提示:录音环境越安静,克隆效果越好。
@@ -896,7 +966,9 @@ const GeneratePage: React.FC = () => {
素材 - {selectedMaterials.length} 个素材 + + {selectedMaterials.length} 个素材 +
标题 @@ -928,12 +1000,18 @@ const GeneratePage: React.FC = () => { /** 渲染当前步骤 */ const renderCurrentStep = () => { switch (currentStep) { - case 1: return renderStep1(); - case 2: return renderStep2(); - case 3: return renderStep3(); - case 4: return renderStep4(); - case 5: return renderStep5(); - default: return null; + case 1: + return renderStep1(); + case 2: + return renderStep2(); + case 3: + return renderStep3(); + case 4: + return renderStep4(); + case 5: + return renderStep5(); + default: + return null; } }; @@ -992,9 +1070,7 @@ const GeneratePage: React.FC = () => { role="button" tabIndex={0} > -
- {isDone ? "✓" : step.key} -
+
{isDone ? "✓" : step.key}
{step.label}
diff --git a/apps/web/src/pages/generate/generate.css b/apps/web/src/pages/generate/generate.css index 4fc3fc773..a09c32208 100644 --- a/apps/web/src/pages/generate/generate.css +++ b/apps/web/src/pages/generate/generate.css @@ -424,8 +424,13 @@ } @keyframes xx-clone-pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.6; } + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.6; + } } .xx-clone-info { @@ -474,8 +479,10 @@ width: 100%; background: repeating-linear-gradient( 90deg, - #f59e0b 0%, #f59e0b 25%, - #10b981 25%, #10b981 50%, + #f59e0b 0%, + #f59e0b 25%, + #10b981 25%, + #10b981 50%, #f59e0b 50% ); background-size: 60px 100%; @@ -483,8 +490,12 @@ } @keyframes xx-clone-progress-flow { - from { background-position: 0 0; } - to { background-position: 60px 0; } + from { + background-position: 0 0; + } + to { + background-position: 60px 0; + } } .xx-clone-progress-text { @@ -518,13 +529,23 @@ } @keyframes xx-fade { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.7; } + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.7; + } } @keyframes xx-blink { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.3; } + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.3; + } } /* ============================================================ @@ -654,7 +675,11 @@ content: ""; position: absolute; inset: 0; - background: radial-gradient(circle at 72% 28%, rgba(255, 255, 255, 0.2), transparent 40%); + background: radial-gradient( + circle at 72% 28%, + rgba(255, 255, 255, 0.2), + transparent 40% + ); pointer-events: none; } diff --git a/apps/web/src/pages/my-templates/MyTemplates.tsx b/apps/web/src/pages/my-templates/MyTemplates.tsx index b2d961e44..7891d8c43 100644 --- a/apps/web/src/pages/my-templates/MyTemplates.tsx +++ b/apps/web/src/pages/my-templates/MyTemplates.tsx @@ -178,7 +178,10 @@ const MyTemplates: React.FC = () => { description="还没有模板,点击右上角「新建模板」开始创建" style={{ padding: 80 }} > - diff --git a/apps/web/src/pages/templates/TemplateLibrary.tsx b/apps/web/src/pages/templates/TemplateLibrary.tsx index 740461b69..d5ea837df 100644 --- a/apps/web/src/pages/templates/TemplateLibrary.tsx +++ b/apps/web/src/pages/templates/TemplateLibrary.tsx @@ -231,9 +231,7 @@ const TemplatePreviewModal: React.FC = ({ {TEMPLATE_TYPES.find((t) => t.type === template.type)?.icon ?? "📋"} - - {template.name} - + {template.name}
@@ -522,7 +520,9 @@ const TemplateLibrary: React.FC = () => { return (
-
+
+ +

加载模板中...

@@ -534,7 +534,9 @@ const TemplateLibrary: React.FC = () => { return (
-
+
+ +

加载失败

{error?.message || "网络异常,请稍后重试"}

@@ -562,7 +564,9 @@ const TemplateLibrary: React.FC = () => { {/* ── 工具栏:搜索 + 类型按钮组 ─────────────────────────── */}
- + + + { {/* ── 模板展示区 ────────────────────────────────────────── */} {filtered.length === 0 ? (
-
+
+ +

{searchText || activeType !== "全部" ? "未找到匹配的模板" diff --git a/apps/web/src/pages/templates/templates.css b/apps/web/src/pages/templates/templates.css index b967ca61e..0d0207906 100644 --- a/apps/web/src/pages/templates/templates.css +++ b/apps/web/src/pages/templates/templates.css @@ -675,7 +675,6 @@ gap: 12px; } - /* ============================================================ 内联样式迁移类 ============================================================ */ diff --git a/apps/web/src/pages/voice-clone/VoiceClone.tsx b/apps/web/src/pages/voice-clone/VoiceClone.tsx index 68a78003d..4fad8a6f8 100644 --- a/apps/web/src/pages/voice-clone/VoiceClone.tsx +++ b/apps/web/src/pages/voice-clone/VoiceClone.tsx @@ -119,11 +119,15 @@ const VoiceCloneCard: React.FC = ({ {/* 元信息 */}
- + + + 时长:{formatDuration(voice.duration_seconds)}
- + + + 创建于:{createdDate}
@@ -291,7 +295,9 @@ const VoiceClone: React.FC = () => { {/* 空状态 */} {!isLoading && voices.length === 0 && (
-
+
+ +

还没有克隆音色

上传你的声音,AI将克隆你的专属音色