From 9a852a4a125f5187e4307d6778423bcd79331500 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Thu, 23 Jul 2026 23:15:38 +0800 Subject: [PATCH 01/11] =?UTF-8?q?docs(#780):=20=E8=A1=A5=E9=BD=90.env.exam?= =?UTF-8?q?ple=E7=BC=BA=E5=A4=B1=E7=9A=8430+=E9=85=8D=E7=BD=AE=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 梳理所有配置文件(shared/api/worker),完整列出全部环境变量: - 新增:数据库连接池配置(pool_size/max_overflow/timeout/recycle) - 新增:Celery broker/backend 配置 - 新增:Worker 并发/子进程数 配置 - 新增:OSS 直传大小/过期时间 配置 - 新增:豆包大模型(Doubao)配置 - 新增:CosyVoice 克隆模型配置 - 新增:渲染引擎选择(RENDER_ENGINE) - 新增:邮件投递开关 / SMTP TLS / API 端口等零散配置 - 每个配置补充用途注释和默认值说明 - 顶部增加配置读取优先级说明 --- .env.example | 222 +++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 180 insertions(+), 42 deletions(-) diff --git a/.env.example b/.env.example index 60efca990..01139df98 100755 --- a/.env.example +++ b/.env.example @@ -1,60 +1,198 @@ -# 小虾 SaaS 环境变量配置 +# ============================================================ +# 小虾 SaaS 环境变量完整配置 +# ============================================================ +# 本文件列出所有可配置的环境变量及默认值。 +# 复制为 .env 后按需修改;生产环境务必覆盖所有密钥类配置。 +# +# 配置读取规则(pydantic-settings,大小写不敏感): +# 1. 系统环境变量(最高优先级) +# 2. .env.{APP_ENV} 文件(如 .env.staging) +# 3. .env 文件 +# 4. 代码中的默认值(最低优先级) +# ============================================================ -# ==================== 应用配置 ==================== -APP_NAME=小虾 SaaS -APP_BASE_URL=http://localhost:3000 + +# ==================== 应用基本配置 ==================== + +# 应用名称 +APP_NAME=xiaoxia-saas + +# 应用版本号(展示用,代码中已内置默认) +APP_VERSION=0.1.61 + +# 环境标识:development / staging / production +# 决定读取 .env.{APP_ENV} 还是 .env,也影响部分配置的严格校验 APP_ENV=development -# ==================== 数据库配置 ==================== -DATABASE_URL=postgresql://xiaoxia_user:your_password@localhost:5432/xiaoxia_saas - -# 开发环境:使用内存数据库(不需要 PostgreSQL) -USE_IN_MEMORY_DB=true - -# 生产环境:使用 PostgreSQL -# USE_IN_MEMORY_DB=false - -# ==================== Redis 配置 ==================== -REDIS_URL=redis://localhost:6379/0 - -# ==================== JWT 配置 ==================== -JWT_SECRET_KEY=your-super-secret-key-change-this-in-production-min-32-chars -JWT_ALGORITHM=HS256 -JWT_ACCESS_TOKEN_EXPIRE_MINUTES=30 -JWT_REFRESH_TOKEN_EXPIRE_DAYS=30 - -# ==================== 邮件配置 ==================== -SMTP_HOST=smtp.gmail.com -SMTP_PORT=587 -SMTP_USER=your-email@gmail.com -SMTP_PASSWORD=your-app-specific-password -SMTP_FROM_EMAIL=noreply@xiaoxia-saas.com -SMTP_FROM_NAME=小虾 SaaS - -# ==================== 环境配置 ==================== -ENVIRONMENT=development +# 是否开启 Debug 模式(开发环境 true,生产环境 false) DEBUG=true -# ==================== CORS 配置 ==================== -# 逗号分隔的域名列表(Settings 读取 CORS_ORIGINS_RAW) -CORS_ORIGINS_RAW=http://localhost:3000,http://localhost:5173 +# 应用基础 URL,用于生成认证邮件、回调链接等 +APP_BASE_URL=http://localhost:3000 + +# API 服务监听地址(容器内绑定,外部暴露由 Docker/Nginx 控制) +API_HOST=0.0.0.0 + +# API 服务监听端口 +API_PORT=8000 + +# 是否自动创建数据库表结构(开发环境可开启,生产环境用 alembic migration) +AUTO_CREATE_SCHEMA=false + + +# ==================== 数据库配置 ==================== + +# 数据库连接串(格式:postgresql+psycopg://user:password@host:port/dbname) +DATABASE_URL=postgresql+psycopg://postgres:postgres@localhost:5432/xiaoxia_saas + +# 连接池大小(常驻连接数) +DATABASE_POOL_SIZE=20 + +# 连接池最大溢出连接数(pool_size + max_overflow = 最大并发连接数) +DATABASE_MAX_OVERFLOW=10 + +# 获取连接超时时间(秒) +DATABASE_POOL_TIMEOUT=30 + +# 连接回收时间(秒),防止数据库端主动断开导致的死连接 +DATABASE_POOL_RECYCLE=3600 + +# 是否使用内存数据库(SQLite,仅开发/测试可用;生产务必 false) +USE_IN_MEMORY_DB=false + + +# ==================== Redis 配置 ==================== + +# Redis 连接 URL(格式:redis://[:password@]host:port/db) +REDIS_URL=redis://localhost:6379/0 + +# 是否使用 Redis 存储 Session(多实例部署时必须开启;开发可用内存存储) +ENABLE_REDIS_SESSIONS=false + + +# ==================== Celery 任务队列 ==================== + +# Celery Broker(任务分发),默认用 Redis db0 +CELERY_BROKER_URL=redis://localhost:6379/0 + +# Celery Result Backend(任务结果存储),默认用 Redis db1 +CELERY_RESULT_BACKEND=redis://localhost:6379/1 + + +# ==================== Worker 配置 ==================== + +# Worker 进程名称 +WORKER_NAME=xiaoxia-saas-worker + +# Worker 并发数(同时执行的任务数) +WORKER_CONCURRENCY=4 + +# 每个子进程最多处理多少任务后重启(防止内存泄漏) +WORKER_MAX_TASKS_PER_CHILD=1000 + + +# ==================== JWT 认证配置 ==================== + +# JWT 签名密钥 — 生产环境必须设置为强随机字符串(至少32字符) +# 内置不安全值会被拒绝:secret / changeme / password / your-secret-key 等 +JWT_SECRET_KEY=your-super-secret-key-change-this-in-production-min-32-chars + +# JWT 签名算法 +JWT_ALGORITHM=HS256 + +# Access Token 过期时间(分钟) +JWT_ACCESS_TOKEN_EXPIRE_MINUTES=30 + +# Refresh Token 过期时间(天) +JWT_REFRESH_TOKEN_EXPIRE_DAYS=30 + + +# ==================== 邮件配置 ==================== + +# 是否启用邮件投递(关闭时邮件内容打印到日志,开发调试用) +ENABLE_EMAIL_DELIVERY=false + +# SMTP 服务器地址 +SMTP_HOST=smtp.gmail.com + +# SMTP 端口 +SMTP_PORT=587 + +# SMTP 用户名 +SMTP_USER=your-email@gmail.com + +# SMTP 密码 / 应用专用密码 +SMTP_PASSWORD=your-app-specific-password + +# 发件人邮箱 +SMTP_FROM_EMAIL=noreply@xiaoxia-saas.com + +# 发件人显示名称 +SMTP_FROM_NAME=小虾 SaaS + +# 是否启用 TLS +SMTP_USE_TLS=true + # ==================== 阿里云 OSS 配置 ==================== + +# OSS 区域 endpoint OSS_ENDPOINT=oss-cn-hangzhou.aliyuncs.com + +# OSS Access Key ID — 非开发环境必须设置 OSS_ACCESS_KEY_ID=your-access-key-id + +# OSS Access Key Secret — 非开发环境必须设置 OSS_ACCESS_KEY_SECRET=your-access-key-secret + +# OSS Bucket 名称 OSS_BUCKET_NAME=xiaoxia-autocut -# ==================== CosyVoice 语音合成配置 ==================== -# 注意:base_url 只需写到 /api/v1,具体路径由代码拼接 -# 模型: cosyvoice-v3-flash (推荐,支持系统音色,性价比高) -# cosyvoice-v3-plus (高质量,系统音色少) -# cosyvoice-v3.5-flash / cosyvoice-v3.5-plus (仅支持克隆/设计音色,无系统音色) -# 音色: v3系列系统音色带 _v3 后缀,如 longxiaochun_v3, longxiaoxia_v3, longanyang (无后缀) -# 注意:COSYVOICE_* 变量由 packages/shared/config.py 的 SharedSettings 读取 +# 直传最大文件大小(MB) +OSS_DIRECT_UPLOAD_MAX_MB=2000 + +# 直传签名有效期(秒) +OSS_DIRECT_UPLOAD_EXPIRE_SECONDS=900 + + +# ==================== CORS 配置 ==================== + +# 允许跨域的前端域名列表,逗号分隔 +CORS_ORIGINS_RAW=http://localhost:3000,http://localhost:5173,http://localhost:8000 + + +# ==================== 渲染引擎配置 ==================== + +# 渲染引擎选择: +# legacy — 旧 VideoComposeService(稳定,功能完整) +# unified — 新 UnifiedRenderService(新架构,部分场景仍在验证) +RENDER_ENGINE=legacy + + +# ==================== CosyVoice 语音合成 ==================== +# 阿里云百灵语音合成服务 +# 模型选择: +# cosyvoice-v3-flash — 推荐,系统音色多,性价比高 +# cosyvoice-v3-plus — 高质量,系统音色少 +# cosyvoice-v3.5-flash / cosyvoice-v3.5-plus — 仅支持克隆/设计音色,无系统音色 +# 音色:v3 系列系统音色带 _v3 后缀,如 longxiaochun_v3 / longxiaoxia_v3 / longanyang + COSYVOICE_API_KEY=your-cosyvoice-api-key COSYVOICE_BASE_URL=https://dashscope.aliyuncs.com/api/v1 COSYVOICE_MODEL=cosyvoice-v3-flash COSYVOICE_VOICE=longxiaochun_v3 COSYVOICE_SAMPLE_RATE=22050 COSYVOICE_FORMAT=mp3 + +# 音色克隆模型名(固定为 voice-enrollment,通常不需修改) +COSYVOICE_CLONE_MODEL=voice-enrollment + + +# ==================== 豆包大模型(火山引擎方舟) ==================== +# 用于 AI 文案生成、智能剪辑等需要大模型能力的场景 + +DOUBAO_API_KEY=your-doubao-api-key +DOUBAO_MODEL=doubao-seed-1-6-250615 +DOUBAO_BASE_URL=https://ark.cn-beijing.volces.com/api/v3 +DOUBAO_TIMEOUT=30 +DOUBAO_MAX_RETRIES=2 -- 2.54.0 From e9d4885f7a5464b5d9edd6866a40bc48e836b34a Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Fri, 24 Jul 2026 00:14:05 +0800 Subject: [PATCH 02/11] =?UTF-8?q?refactor:=20GeneratePage=20Phase=201=20-?= =?UTF-8?q?=20=E6=8A=BD=E5=8F=96=E5=B8=B8=E9=87=8F/=E7=B1=BB=E5=9E=8B/UI?= =?UTF-8?q?=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 3036 行的 GeneratePage.tsx 按 Phase 1 计划拆分: 新增文件: - constants.ts — 12组常量(STEPS/TITLE_PRESETS/AI标题模板/智能匹配理由/渐变色/图标等) - types.ts — TitleSettings/SmartMatchResult/AiTitleResult 等类型定义 - components/GenerateHeader.tsx — 页头组件 - components/GenerateStepsBar.tsx — 步骤条组件 - components/GenerateResultPanel.tsx — 右侧生成结果面板 GeneratePage.tsx 从 3036 行 → 2682 行(-354行) 纯结构重构,业务逻辑零改动 --- apps/web/src/pages/generate/GeneratePage.tsx | 423 ++---------------- .../generate/components/GenerateHeader.tsx | 40 ++ .../components/GenerateResultPanel.tsx | 187 ++++++++ .../generate/components/GenerateStepsBar.tsx | 47 ++ apps/web/src/pages/generate/constants.ts | 200 +++++++++ apps/web/src/pages/generate/types.ts | 74 +++ 6 files changed, 582 insertions(+), 389 deletions(-) create mode 100644 apps/web/src/pages/generate/components/GenerateHeader.tsx create mode 100644 apps/web/src/pages/generate/components/GenerateResultPanel.tsx create mode 100644 apps/web/src/pages/generate/components/GenerateStepsBar.tsx create mode 100644 apps/web/src/pages/generate/constants.ts create mode 100644 apps/web/src/pages/generate/types.ts diff --git a/apps/web/src/pages/generate/GeneratePage.tsx b/apps/web/src/pages/generate/GeneratePage.tsx index 7e8c2a886..2237051d9 100644 --- a/apps/web/src/pages/generate/GeneratePage.tsx +++ b/apps/web/src/pages/generate/GeneratePage.tsx @@ -15,8 +15,7 @@ import { LoadingOutlined, PlayCircleOutlined, PauseCircleOutlined, - DownloadOutlined, - ShareAltOutlined, + SaveOutlined, PlusOutlined, MinusOutlined, @@ -44,65 +43,27 @@ import { synthesizeSpeech, getTTSJobStatus, saveTtsToLibrary } from "@/api/tts" import { getTags, createTag } from "@/api/tags" import { useCloneProgress } from "@/hooks/useCloneProgress" import { useSearchParams, useNavigate } from "react-router-dom" +import GenerateHeader from "./components/GenerateHeader" +import GenerateStepsBar from "./components/GenerateStepsBar" +import GenerateResultPanel from "./components/GenerateResultPanel" +import { + CLONE_STATUS_CONFIG, + MODE_GRADIENTS, + VOICE_GENDER_ICON, + POSITION_OPTIONS, + FONT_OPTIONS, + TITLE_PRESETS, + COVER_MODE_LABELS, + COVER_MODE_ICONS, + DEFAULT_COVER_SETTINGS, + SMART_MATCH_REASONS, + AI_TITLE_TEMPLATES, +} from "./constants" +import type { TitleSettings, SmartMatchResult, AiTitleResult } from "./types" import "./generate.css" const { Text } = Typography -/* ── 克隆声音状态配置 ── */ -const CLONE_STATUS_CONFIG: Record = { - ready: { label: "就绪", color: "var(--secondary-color, #10b981)" }, - processing: { label: "克隆中", color: "var(--accent-color, #f59e0b)" }, - failed: { label: "失败", color: "var(--error-color, #ef4444)" }, -} - -/* ── 模板渐变色映射(根据 mode 分配视觉样式) ── */ -const MODE_GRADIENTS: Record = { - pip: "linear-gradient(135deg, #fbbf24, #f59e0b)", - one_take: "linear-gradient(135deg, #3b82f6, #1d4ed8)", - voice_over: "linear-gradient(135deg, #6366f1, #4f46e5)", - voice_pip: "linear-gradient(135deg, #10b981, #059669)", -} -/* ── 配音预设卡片:从 API 动态生成,不再硬编码 ── */ -const VOICE_GENDER_ICON: Record = { - female: "🎀", - male: "🎙️", - child: "🧒", - neutral: "✨", -} - -/* ── 步骤定义 ── */ -const STEPS = [ - { key: 1, label: "选择模板" }, - { key: 2, label: "选择素材" }, - { key: 3, label: "生成预览" }, - { key: 4, label: "选择标题" }, - { key: 5, label: "选择配音" }, - { key: 6, label: "选择封面" }, - { key: 7, label: "确认生成" }, -] - -/* ── 标题设置常量 ── */ -const POSITION_OPTIONS = [ - { value: "top", label: "顶部" }, - { value: "center", label: "居中" }, - { value: "bottom", label: "底部" }, -] - -const FONT_OPTIONS = ["思源黑体", "思源宋体", "苹方", "PingFang", "微软雅黑", "楷体", "华康俪金黑"] - -interface TitleSettings { - aiAutoSelect: boolean - title: string - position: string - font: string - size: number - bold: boolean - italic: boolean - stroke: boolean - shadow: boolean - color: string -} - const DEFAULT_TITLE_SETTINGS: TitleSettings = { aiAutoSelect: false, title: "", @@ -116,110 +77,6 @@ const DEFAULT_TITLE_SETTINGS: TitleSettings = { color: "#ffffff", } -const TITLE_PRESETS = [ - { - key: "classic_white", - label: "经典白字", - style: { size: 28, color: "#ffffff", bold: true, italic: false, stroke: true, shadow: false }, - previewStyle: { - fontWeight: 700, - color: "#ffffff", - WebkitTextStroke: "1px #000000", - fontSize: "20px", - }, - }, - { - key: "black_gold", - label: "黑金质感", - style: { size: 32, color: "#d4a843", bold: true, italic: false, stroke: false, shadow: true }, - previewStyle: { - fontWeight: 700, - color: "#d4a843", - textShadow: "1px 1px 3px rgba(0,0,0,0.8)", - fontSize: "20px", - }, - }, - { - key: "fresh_minimal", - label: "清新简约", - style: { size: 24, color: "#333333", bold: false, italic: false, stroke: false, shadow: false }, - previewStyle: { fontWeight: 400, color: "#333333", fontSize: "18px" }, - }, - { - key: "variety_show", - label: "综艺花字", - style: { size: 36, color: "#ff4081", bold: true, italic: false, stroke: true, shadow: true }, - previewStyle: { - fontWeight: 900, - color: "#ff4081", - WebkitTextStroke: "1.5px #ffffff", - textShadow: "2px 2px 4px rgba(0,0,0,0.5)", - fontSize: "22px", - }, - }, - { - key: "business", - label: "商务极简", - style: { size: 24, color: "#1a1a1a", bold: false, italic: false, stroke: false, shadow: false }, - previewStyle: { fontWeight: 400, color: "#1a1a1a", fontSize: "17px" }, - }, - { - key: "retro_film", - label: "复古胶片", - style: { size: 28, color: "#e8d5b7", bold: false, italic: false, stroke: false, shadow: true }, - previewStyle: { - fontWeight: 400, - color: "#e8d5b7", - textShadow: "2px 2px 6px rgba(0,0,0,0.7)", - fontSize: "18px", - }, - }, - { - key: "neon_glow", - label: "霓虹发光", - style: { size: 32, color: "#00e5ff", bold: true, italic: false, stroke: false, shadow: true }, - previewStyle: { - fontWeight: 700, - color: "#00e5ff", - textShadow: "0 0 4px #00e5ff, 0 0 8px #00e5ff, 0 0 16px rgba(0,229,255,0.5)", - fontSize: "20px", - }, - }, - { - key: "handwriting", - label: "手写字", - style: { size: 28, color: "#333333", bold: false, italic: false, stroke: false, shadow: true }, - previewStyle: { - fontWeight: 400, - color: "#333333", - textShadow: "1px 1px 2px rgba(0,0,0,0.3)", - fontSize: "20px", - }, - }, -] - -/* ── 封面设置常量 ── */ -const COVER_MODE_LABELS: Record = { - auto: "智能封面", - frame: "抽帧选封面", - upload: "上传封面", -} - -const COVER_MODE_ICONS: Record = { - auto: "🤖", - frame: "🎞️", - upload: "📤", -} - -const DEFAULT_COVER_SETTINGS: CoverConfig = { - enabled: true, - mode: "auto", - frame_time: 0, - upload_url: "", - ai_suggested_time: null, - thumbnail_url: "", -} - function getActivePreset(settings: TitleSettings): string | null { for (const p of TITLE_PRESETS) { if ( @@ -236,45 +93,6 @@ function getActivePreset(settings: TitleSettings): string | null { return null } -/* ================================================================ - 常量 - ================================================================ */ - -const SMART_MATCH_REASONS = [ - "画面清晰度高,构图专业", - "与描述场景高度契合", - "时长适中,适合剪辑节奏", - "色彩风格统一", - "包含关键动作镜头", - "镜头运动流畅自然", - "光影效果出色", - "人物表情生动", -] - -const AI_TITLE_TEMPLATES: Record = { - catchy: [ - "震惊!{topic}居然还能这样操作", - "99%的人都不知道的{topic}秘诀", - "{topic}的终极指南,看完直接封神", - "别再走弯路了!{topic}看这一篇就够", - "一个视频讲透{topic},建议收藏", - ], - emotional: [ - "致每一个在{topic}路上坚持的人", - "关于{topic},我想说句真心话", - "{topic}背后的故事,看完沉默了", - "为什么我劝你一定要了解{topic}", - "这才是{topic}最动人的样子", - ], - informative: [ - "{topic}完整科普:从入门到精通", - "深度解析{topic}的核心原理", - "{topic}行业趋势报告|2026最新版", - "三分钟带你全面了解{topic}", - "{topic}常见问题与解决方案汇总", - ], -} - /* ================================================================ 组件 ================================================================ */ @@ -2771,57 +2589,10 @@ const GeneratePage: React.FC = () => { return (
{/* ── 页头 ── */} -
-
-

- - 智能剪辑 -

-

快速生成短视频,支持多种风格和素材组合

-
- {editPlanId && ( - - 🎬 来自模板草稿 - - )} -
+ {/* ── 步骤条 ── */} -
- {STEPS.map((step, idx) => { - const isActive = currentStep === step.key - const isDone = currentStep > step.key - const cls = ["xx-step-item", isActive ? "active" : "", isDone ? "done" : ""] - .filter(Boolean) - .join(" ") - return ( - - {idx > 0 && } -
{ - // 允许点击已完成的步骤回退 - if (isDone) setCurrentStep(step.key) - }} - role="button" - tabIndex={0} - > -
{isDone ? "✓" : step.key}
- {step.label} -
-
- ) - })} -
+ {/* ── 主布局 ── */}
@@ -2859,146 +2630,20 @@ const GeneratePage: React.FC = () => {
{/* ════ 右侧:生成结果 ════ */} -
-
-

生成结果

- {generated && generatedVideos.length > 0 && ( - {generatedVideos.length} 个视频 - )} -
- - {/* 生成中进度 */} - {generating && ( -
-
- - - - - {Math.round(progress)}% -
-
- - 正在生成视频 - - - AI 正在处理素材,请稍候… - -
-
- )} - - {/* 生成失败 */} - {generateError && !generating && ( -
- - - 生成失败 - - - {typeof generateError === "string" ? generateError : "请重试"} - -
- )} - - {/* 空状态 */} - {!generated && !generating && !generateError && ( -
- - - 完成配置后点击「确认生成」 - - - 生成的视频将在这里展示 - -
- )} - - {/* 生成结果卡片列表 */} - {generated && generatedVideos.length > 0 && ( -
- {generatedVideos.map((video, idx) => ( -
{ - setPreviewVideo(video) - setPreviewModalOpen(true) - }} - > -
- {video.thumbnail_url ? ( - - ) : ( -
- -
- )} -
- -
- {video.duration && ( - {formatDuration(video.duration)} - )} -
-
-
视频 {idx + 1}
-
- - -
-
-
- ))} -
- )} - - {generated && ( -
- -
- )} -
+ { + setPreviewVideo(video) + setPreviewModalOpen(true) + }} + onDownload={handleDownload} + onShare={handleShare} + onGoToLibrary={() => navigate("/app/products")} + />
{/* ── 视频预览弹窗 ── */} diff --git a/apps/web/src/pages/generate/components/GenerateHeader.tsx b/apps/web/src/pages/generate/components/GenerateHeader.tsx new file mode 100644 index 000000000..669e50b01 --- /dev/null +++ b/apps/web/src/pages/generate/components/GenerateHeader.tsx @@ -0,0 +1,40 @@ +/** + * 智能剪辑页头组件 + */ +import React from "react" +import { ThunderboltOutlined } from "@ant-design/icons" + +interface GenerateHeaderProps { + /** 是否来自模板草稿(URL 带 edit_plan_id) */ + fromEditPlan?: boolean +} + +const GenerateHeader: React.FC = ({ fromEditPlan }) => { + return ( +
+
+

+ + 智能剪辑 +

+

快速生成短视频,支持多种风格和素材组合

+
+ {fromEditPlan && ( + + 🎬 来自模板草稿 + + )} +
+ ) +} + +export default GenerateHeader diff --git a/apps/web/src/pages/generate/components/GenerateResultPanel.tsx b/apps/web/src/pages/generate/components/GenerateResultPanel.tsx new file mode 100644 index 000000000..bae3110d8 --- /dev/null +++ b/apps/web/src/pages/generate/components/GenerateResultPanel.tsx @@ -0,0 +1,187 @@ +/** + * 智能剪辑右侧生成结果面板 + */ +import React from "react" +import { Typography } from "antd" +import { + PlayCircleOutlined, + CloseCircleOutlined, + DownloadOutlined, + ShareAltOutlined, +} from "@ant-design/icons" +import type { GeneratedVideo } from "@/api/template-editor" +import { formatDuration } from "@/api/voice-clone" + +const { Text } = Typography + +interface GenerateResultPanelProps { + /** 是否已生成完成 */ + generated: boolean + /** 是否正在生成中 */ + generating: boolean + /** 生成进度(0-100) */ + progress: number + /** 生成错误信息 */ + generateError: string | null + /** 生成的视频列表 */ + generatedVideos: GeneratedVideo[] + /** 点击视频卡片预览回调 */ + onVideoPreview: (video: GeneratedVideo) => void + /** 下载回调 */ + onDownload: () => void + /** 分享回调 */ + onShare: () => void + /** 前往成片库回调 */ + onGoToLibrary: () => void +} + +const GenerateResultPanel: React.FC = ({ + generated, + generating, + progress, + generateError, + generatedVideos, + onVideoPreview, + onDownload, + onShare, + onGoToLibrary, +}) => { + return ( +
+
+

生成结果

+ {generated && generatedVideos.length > 0 && ( + {generatedVideos.length} 个视频 + )} +
+ + {/* 生成中进度 */} + {generating && ( +
+
+ + + + + {Math.round(progress)}% +
+
+ + 正在生成视频 + + + AI 正在处理素材,请稍候… + +
+
+ )} + + {/* 生成失败 */} + {generateError && !generating && ( +
+ + + 生成失败 + + + {typeof generateError === "string" ? generateError : "请重试"} + +
+ )} + + {/* 空状态 */} + {!generated && !generating && !generateError && ( +
+ + + 完成配置后点击「确认生成」 + + + 生成的视频将在这里展示 + +
+ )} + + {/* 生成结果卡片列表 */} + {generated && generatedVideos.length > 0 && ( +
+ {generatedVideos.map((video, idx) => ( +
onVideoPreview(video)} + > +
+ {video.thumbnail_url ? ( + + ) : ( +
+ +
+ )} +
+ +
+ {video.duration && ( + {formatDuration(video.duration)} + )} +
+
+
视频 {idx + 1}
+
+ + +
+
+
+ ))} +
+ )} + + {generated && ( +
+ +
+ )} +
+ ) +} + +export default GenerateResultPanel diff --git a/apps/web/src/pages/generate/components/GenerateStepsBar.tsx b/apps/web/src/pages/generate/components/GenerateStepsBar.tsx new file mode 100644 index 000000000..7f4945f1c --- /dev/null +++ b/apps/web/src/pages/generate/components/GenerateStepsBar.tsx @@ -0,0 +1,47 @@ +/** + * 智能剪辑步骤条组件 + */ +import React from "react" +import { STEPS } from "../constants" + +interface GenerateStepsBarProps { + /** 当前步骤(1-based) */ + currentStep: number + /** 点击已完成步骤的回调(用于回退) */ + onStepClick?: (step: number) => void +} + +const GenerateStepsBar: React.FC = ({ currentStep, onStepClick }) => { + return ( +
+ {STEPS.map((step, idx) => { + const isActive = currentStep === step.key + const isDone = currentStep > step.key + const cls = ["xx-step-item", isActive ? "active" : "", isDone ? "done" : ""] + .filter(Boolean) + .join(" ") + return ( + + {idx > 0 && } +
{ + // 允许点击已完成的步骤回退 + if (isDone && onStepClick) { + onStepClick(step.key) + } + }} + role="button" + tabIndex={0} + > +
{isDone ? "✓" : step.key}
+ {step.label} +
+
+ ) + })} +
+ ) +} + +export default GenerateStepsBar diff --git a/apps/web/src/pages/generate/constants.ts b/apps/web/src/pages/generate/constants.ts new file mode 100644 index 000000000..cd27a0bf3 --- /dev/null +++ b/apps/web/src/pages/generate/constants.ts @@ -0,0 +1,200 @@ +/** + * 智能剪辑页面 — 常量定义 + */ + +import type { CoverConfig } from "../editing-planner/types" + +/* ── 克隆声音状态配置 ── */ +export const CLONE_STATUS_CONFIG: Record = { + ready: { label: "就绪", color: "var(--secondary-color, #10b981)" }, + processing: { label: "克隆中", color: "var(--accent-color, #f59e0b)" }, + failed: { label: "失败", color: "var(--error-color, #ef4444)" }, +} + +/* ── 模板渐变色映射(根据 mode 分配视觉样式) ── */ +export const MODE_GRADIENTS: Record = { + pip: "linear-gradient(135deg, #fbbf24, #f59e0b)", + one_take: "linear-gradient(135deg, #3b82f6, #1d4ed8)", + voice_over: "linear-gradient(135deg, #6366f1, #4f46e5)", + voice_pip: "linear-gradient(135deg, #10b981, #059669)", +} + +/* ── 配音性别图标 ── */ +export const VOICE_GENDER_ICON: Record = { + female: "🎀", + male: "🎙️", + child: "🧒", + neutral: "✨", +} + +/* ── 步骤定义 ── */ +export const STEPS = [ + { key: 1, label: "选择模板" }, + { key: 2, label: "选择素材" }, + { key: 3, label: "生成预览" }, + { key: 4, label: "选择标题" }, + { key: 5, label: "选择配音" }, + { key: 6, label: "选择封面" }, + { key: 7, label: "确认生成" }, +] + +/* ── 标题位置选项 ── */ +export const POSITION_OPTIONS = [ + { value: "top", label: "顶部" }, + { value: "center", label: "居中" }, + { value: "bottom", label: "底部" }, +] + +/* ── 标题字体选项 ── */ +export const FONT_OPTIONS = [ + "思源黑体", + "思源宋体", + "苹方", + "PingFang", + "微软雅黑", + "楷体", + "华康俪金黑", +] + +/* ── 标题样式预设 ── */ +export const TITLE_PRESETS = [ + { + key: "classic_white", + label: "经典白字", + style: { size: 28, color: "#ffffff", bold: true, italic: false, stroke: true, shadow: false }, + previewStyle: { + fontWeight: 700, + color: "#ffffff", + WebkitTextStroke: "1px #000000", + fontSize: "20px", + }, + }, + { + key: "black_gold", + label: "黑金质感", + style: { size: 32, color: "#d4a843", bold: true, italic: false, stroke: false, shadow: true }, + previewStyle: { + fontWeight: 700, + color: "#d4a843", + textShadow: "1px 1px 3px rgba(0,0,0,0.8)", + fontSize: "20px", + }, + }, + { + key: "fresh_minimal", + label: "清新简约", + style: { size: 24, color: "#333333", bold: false, italic: false, stroke: false, shadow: false }, + previewStyle: { fontWeight: 400, color: "#333333", fontSize: "18px" }, + }, + { + key: "variety_show", + label: "综艺花字", + style: { size: 36, color: "#ff4081", bold: true, italic: false, stroke: true, shadow: true }, + previewStyle: { + fontWeight: 900, + color: "#ff4081", + WebkitTextStroke: "1.5px #ffffff", + textShadow: "2px 2px 4px rgba(0,0,0,0.5)", + fontSize: "22px", + }, + }, + { + key: "business", + label: "商务极简", + style: { size: 24, color: "#1a1a1a", bold: false, italic: false, stroke: false, shadow: false }, + previewStyle: { fontWeight: 400, color: "#1a1a1a", fontSize: "17px" }, + }, + { + key: "retro_film", + label: "复古胶片", + style: { size: 28, color: "#e8d5b7", bold: false, italic: false, stroke: false, shadow: true }, + previewStyle: { + fontWeight: 400, + color: "#e8d5b7", + textShadow: "2px 2px 6px rgba(0,0,0,0.7)", + fontSize: "18px", + }, + }, + { + key: "neon_glow", + label: "霓虹发光", + style: { size: 32, color: "#00e5ff", bold: true, italic: false, stroke: false, shadow: true }, + previewStyle: { + fontWeight: 700, + color: "#00e5ff", + textShadow: "0 0 4px #00e5ff, 0 0 8px #00e5ff, 0 0 16px rgba(0,229,255,0.5)", + fontSize: "20px", + }, + }, + { + key: "handwriting", + label: "手写字", + style: { size: 28, color: "#333333", bold: false, italic: false, stroke: false, shadow: true }, + previewStyle: { + fontWeight: 400, + color: "#333333", + textShadow: "1px 1px 2px rgba(0,0,0,0.3)", + fontSize: "20px", + }, + }, +] + +/* ── 封面模式 ── */ +export const COVER_MODE_LABELS: Record = { + auto: "智能封面", + frame: "抽帧选封面", + upload: "上传封面", +} + +export const COVER_MODE_ICONS: Record = { + auto: "🤖", + frame: "🎞️", + upload: "📤", +} + +/* ── 智能匹配推荐理由 ── */ +export const SMART_MATCH_REASONS = [ + "画面清晰度高,构图专业", + "与描述场景高度契合", + "时长适中,适合剪辑节奏", + "色彩风格统一", + "包含关键动作镜头", + "镜头运动流畅自然", + "光影效果出色", + "人物表情生动", +] + +/* ── AI 标题模板 ── */ +export const AI_TITLE_TEMPLATES: Record = { + catchy: [ + "震惊!{topic}居然还能这样操作", + "99%的人都不知道的{topic}秘诀", + "{topic}的终极指南,看完直接封神", + "别再走弯路了!{topic}看这一篇就够", + "一个视频讲透{topic},建议收藏", + ], + emotional: [ + "致每一个在{topic}路上坚持的人", + "关于{topic},我想说句真心话", + "{topic}背后的故事,看完沉默了", + "为什么我劝你一定要了解{topic}", + "这才是{topic}最动人的样子", + ], + informative: [ + "{topic}完整科普:从入门到精通", + "深度解析{topic}的核心原理", + "{topic}行业趋势报告|2026最新版", + "三分钟带你全面了解{topic}", + "{topic}常见问题与解决方案汇总", + ], +} + +/* ── 默认封面设置 ── */ +export const DEFAULT_COVER_SETTINGS: CoverConfig = { + enabled: true, + mode: "auto", + frame_time: 0, + upload_url: "", + ai_suggested_time: null, + thumbnail_url: "", +} diff --git a/apps/web/src/pages/generate/types.ts b/apps/web/src/pages/generate/types.ts new file mode 100644 index 000000000..175f6fc4e --- /dev/null +++ b/apps/web/src/pages/generate/types.ts @@ -0,0 +1,74 @@ +/** + * 智能剪辑页面 — 类型定义 + */ + +import type { AssetItem } from "@/api/assets" +import type { PresetVoiceItem } from "@/api/voices" + +/* ── 标题设置 ── */ +export interface TitleSettings { + aiAutoSelect: boolean + title: string + position: string + font: string + size: number + bold: boolean + italic: boolean + stroke: boolean + shadow: boolean + color: string +} + +/* ── 智能匹配结果 ── */ +export interface SmartMatchResult { + asset: AssetItem + matchScore: number + reasons: string[] +} + +/* ── AI 标题结果 ── */ +export interface AiTitleResult { + title: string + style: string + styleLabel: string + highlights: string[] +} + +/* ── 配音推荐结果 ── */ +export interface VoiceRecommendation { + voiceId: string + voiceName: string + reason: string +} + +/* ── 步骤定义 ── */ +export interface StepDef { + key: number + label: string +} + +/* ── 标题预设样式 ── */ +export interface TitlePresetStyle { + size: number + color: string + bold: boolean + italic: boolean + stroke: boolean + shadow: boolean +} + +export interface TitlePreset { + key: string + label: string + style: TitlePresetStyle + previewStyle: Record +} + +/* ── 生成结果视频 ── */ +export interface GeneratedVideoResult { + id: string + url: string + thumbnail: string + duration: number + title: string +} -- 2.54.0 From fe62730401b98627f67e791386fb0d617ec5a735 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Fri, 24 Jul 2026 00:45:00 +0800 Subject: [PATCH 03/11] feat(generate): add Step1-3 components and hooks - Step1TemplateSelect + useStep1Template: template selection UI and logic - Step2MaterialSelect + useStep2Materials: material selection with manual/auto modes - Step3GeneratePreview + useStep3Preview: generation preview summary Part of GeneratePage Phase 2 refactoring. --- .../components/Step1TemplateSelect.tsx | 86 ++++ .../components/Step2MaterialSelect.tsx | 310 ++++++++++++++ .../components/Step3GeneratePreview.tsx | 57 +++ .../pages/generate/hooks/useStep1Template.ts | 44 ++ .../pages/generate/hooks/useStep2Materials.ts | 205 +++++++++ .../pages/generate/hooks/useStep3Preview.ts | 47 +++ .../src/pages/generate/hooks/useStep4Title.ts | 251 +++++++++++ .../src/pages/generate/hooks/useStep5Voice.ts | 397 ++++++++++++++++++ .../src/pages/generate/hooks/useStep6Cover.ts | 77 ++++ .../pages/generate/hooks/useStep7Generate.ts | 121 ++++++ 10 files changed, 1595 insertions(+) create mode 100644 apps/web/src/pages/generate/components/Step1TemplateSelect.tsx create mode 100644 apps/web/src/pages/generate/components/Step2MaterialSelect.tsx create mode 100644 apps/web/src/pages/generate/components/Step3GeneratePreview.tsx create mode 100644 apps/web/src/pages/generate/hooks/useStep1Template.ts create mode 100644 apps/web/src/pages/generate/hooks/useStep2Materials.ts create mode 100644 apps/web/src/pages/generate/hooks/useStep3Preview.ts create mode 100644 apps/web/src/pages/generate/hooks/useStep4Title.ts create mode 100644 apps/web/src/pages/generate/hooks/useStep5Voice.ts create mode 100644 apps/web/src/pages/generate/hooks/useStep6Cover.ts create mode 100644 apps/web/src/pages/generate/hooks/useStep7Generate.ts diff --git a/apps/web/src/pages/generate/components/Step1TemplateSelect.tsx b/apps/web/src/pages/generate/components/Step1TemplateSelect.tsx new file mode 100644 index 000000000..d39a864b3 --- /dev/null +++ b/apps/web/src/pages/generate/components/Step1TemplateSelect.tsx @@ -0,0 +1,86 @@ +/** + * Step 1 模板选择组件 + */ +import React from "react" +import type { EditingTemplate } from "@/api/editing-planner" +import { MODE_GRADIENTS } from "../constants" +import { useStep1Template } from "../hooks/useStep1Template" + +interface Step1TemplateSelectProps { + templates: EditingTemplate[] + selectedTemplate: string + onSelectTemplate: (id: string) => void +} + +const Step1TemplateSelect: React.FC = (props) => { + const { templates, selectedTemplate, handleSelect, handleKeySelect } = useStep1Template(props) + + return ( +
+

🎨 选择模板

+ {templates.length === 0 ? ( +
+

暂无可用模板

+

+ 请先在「模板编辑器」中创建模板 +

+
+ ) : ( +
+ {templates.map((tpl) => ( +
handleSelect(tpl.id)} + role="button" + tabIndex={0} + aria-pressed={selectedTemplate === tpl.id} + onKeyDown={(e) => handleKeySelect(e, tpl.id)} + > + +
+ 🎬 +
+

{tpl.name}

+

+ {tpl.estimated_duration}s · {tpl.segments.length}片段 +

+ {tpl.tags.length > 0 && ( +
+ {tpl.tags.map((tag) => ( + + {tag} + + ))} +
+ )} +
+ ))} +
+ )} +
+ ) +} + +export default Step1TemplateSelect diff --git a/apps/web/src/pages/generate/components/Step2MaterialSelect.tsx b/apps/web/src/pages/generate/components/Step2MaterialSelect.tsx new file mode 100644 index 000000000..983351d2a --- /dev/null +++ b/apps/web/src/pages/generate/components/Step2MaterialSelect.tsx @@ -0,0 +1,310 @@ +/** + * Step 2 素材选择组件 + */ +import React from "react" +import { Typography } from "antd" +import { LoadingOutlined, PlayCircleOutlined, CheckCircleFilled } from "@ant-design/icons" +import { useStep2Materials } from "../hooks/useStep2Materials" + +const { Text } = Typography + +interface Step2MaterialSelectProps { + materialMode: "manual" | "auto" + onMaterialModeChange: (mode: "manual" | "auto") => void + selectedMaterials: string[] + onSelectedMaterialsChange: (ids: string[]) => void + smartSelectedIds: string[] + onSmartSelectedIdsChange: (ids: string[]) => void +} + +const Step2MaterialSelect: React.FC = (props) => { + const { + libraries, + selectedLibraryId, + setSelectedLibraryId, + materials, + materialsLoading, + materialMode, + onMaterialModeChange, + selectedMaterials, + handleToggleMaterial, + smartMatchInput, + setSmartMatchInput, + smartMatching, + smartMatchedResults, + hasMatched, + smartSelectedIds, + handleSmartMatch, + handleToggleSmartSelect, + handleRefreshMatch, + handleSelectAllMatched, + handleClearSmartSelect, + smartSelectedTotalDuration, + formatDuration, + } = useStep2Materials(props) + + return ( +
+

📦 选择素材

+ + {/* ── 模式切换 Tab ── */} +
+ + +
+ + {/* ── 视频库选择(两种模式共用) ── */} +
+ + +
+ + {/* ── 手动选择模式 ── */} + {materialMode === "manual" && ( + <> +
+ 已选 {selectedMaterials.length} 个素材 +
+ + {/* 素材列表 */} +
+ {materialsLoading ? ( + 加载素材中… + ) : materials.items.length === 0 ? ( + + 暂无素材,请先在视频库中上传 + + ) : ( +
+ {materials.items.map((m) => { + const checked = selectedMaterials.includes(m.id) + return ( + + ) + })} +
+ )} +
+ + )} + + {/* ── 自动匹配模式 ── */} + {materialMode === "auto" && ( +
+ {/* 描述输入区 */} +
+ +