From dc2403d6950193b1a0265e76b7979910037b2a7e Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Sun, 19 Jul 2026 14:56:27 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat(P1):=20#581=20=E6=A0=87=E9=A2=98?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E4=BB=8E=E5=89=AA=E8=BE=91=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E5=99=A8=E8=BF=81=E7=A7=BB=E5=88=B0=E6=99=BA=E8=83=BD=E5=89=AA?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 智能剪辑「选择标题」步骤新增完整标题样式设置 - AI自动选择开关 - 标题库选择 + 手动输入 - 位置、字体、字号滑块 - 8种预设样式卡片 - 粗体/斜体/描边/阴影样式按钮 - 标题设置随title_config传递给后端 - 剪辑编辑器移除标题设置UI和相关state - 编辑器保留titleConfig只读状态(从模板/计划继承) - 预览组件titleSettings → titleConfig - 新增标题设置相关CSS样式 --- .../pages/editing-planner/EditingPlanner.tsx | 76 ++-- .../components/ClipPropertiesPanel.tsx | 312 +------------- .../components/PreviewPlayer.tsx | 32 +- apps/web/src/pages/generate/GeneratePage.tsx | 402 +++++++++++++++--- apps/web/src/pages/generate/generate.css | 201 +++++++++ 5 files changed, 595 insertions(+), 428 deletions(-) mode change 100644 => 100755 apps/web/src/pages/generate/generate.css diff --git a/apps/web/src/pages/editing-planner/EditingPlanner.tsx b/apps/web/src/pages/editing-planner/EditingPlanner.tsx index ca96d6d22..5fad2865f 100755 --- a/apps/web/src/pages/editing-planner/EditingPlanner.tsx +++ b/apps/web/src/pages/editing-planner/EditingPlanner.tsx @@ -26,6 +26,7 @@ import type { GeneratedVideo, MediaAsset, TransitionEffect, + TitleConfig, } from "@/api/editPlans" import { getMediaAssets, @@ -61,7 +62,6 @@ import type { ChromaKeyConfig, StickerConfig, CoverConfig, - TitleSettings, } from "./types" import { DEFAULT_TRANSITION, @@ -157,18 +157,14 @@ const EditingPlanner: React.FC = () => { const [currentFilter, setCurrentFilter] = useState("全部") const [searchQuery, setSearchQuery] = useState("") - /* ── 标题/字幕/BGM 设置 ── */ - const [titleSettings, setTitleSettings] = useState({ - aiAutoSelect: false, - title: "", - position: "top", - font: "思源黑体", - size: 24, - bold: false, - italic: false, - stroke: true, - shadow: true, - color: "#ffffff", + /* ── 标题配置(只读,从模板/计划继承) ── */ + const [titleConfig, setTitleConfig] = useState({ + ai_auto_select: false, + content: "", + position: "bottom", + font_preset: "思源黑体", + font_color: "#ffffff", + font_size: 28, }) const [subtitleSettings, setSubtitleSettings] = useState({ @@ -368,15 +364,14 @@ const EditingPlanner: React.FC = () => { })) resetClips(mapped) - setTitleSettings((prev) => ({ - ...prev, - aiAutoSelect: tpl.title_config.ai_auto_select, - title: tpl.title_config.content, + setTitleConfig({ + ai_auto_select: tpl.title_config.ai_auto_select, + content: tpl.title_config.content, position: tpl.title_config.position, - font: tpl.title_config.font_preset, - size: tpl.title_config.font_size, - color: tpl.title_config.font_color || "#ffffff", - })) + font_preset: tpl.title_config.font_preset, + font_size: tpl.title_config.font_size, + font_color: tpl.title_config.font_color || "#ffffff", + }) setSubtitleSettings((prev) => ({ ...prev, enabled: tpl.subtitle_config.enabled, @@ -423,15 +418,14 @@ const EditingPlanner: React.FC = () => { // 还原 config 中的编辑器状态 const cfg = plan.config if (cfg.title_config) { - setTitleSettings((prev) => ({ - ...prev, - aiAutoSelect: cfg.title_config!.ai_auto_select, - title: cfg.title_config!.content, + setTitleConfig({ + ai_auto_select: cfg.title_config!.ai_auto_select, + content: cfg.title_config!.content, position: cfg.title_config!.position, - font: cfg.title_config!.font_preset, - size: cfg.title_config!.font_size, - color: cfg.title_config!.font_color || "#ffffff", - })) + font_preset: cfg.title_config!.font_preset, + font_size: cfg.title_config!.font_size, + font_color: cfg.title_config!.font_color || "#ffffff", + }) } if (cfg.subtitle_config) { setSubtitleSettings((prev) => ({ @@ -824,14 +818,7 @@ const EditingPlanner: React.FC = () => { /** 构建剪辑计划 config(编辑器状态 → API config) */ const buildPlanConfig = (): EditPlanConfig => ({ - title_config: { - ai_auto_select: titleSettings.aiAutoSelect, - content: titleSettings.title, - position: titleSettings.position, - font_preset: titleSettings.font, - font_color: titleSettings.color, - font_size: titleSettings.size, - }, + title_config: titleConfig, subtitle_config: { enabled: subtitleSettings.enabled, position: subtitleSettings.position, @@ -901,14 +888,7 @@ const EditingPlanner: React.FC = () => { .split(",") .map((t) => t.trim()) .filter(Boolean), - title_config: { - ai_auto_select: titleSettings.aiAutoSelect, - content: titleSettings.title, - font_preset: titleSettings.font, - font_color: titleSettings.color, - font_size: titleSettings.size, - position: titleSettings.position, - }, + title_config: titleConfig, subtitle_config: { enabled: subtitleSettings.enabled, position: subtitleSettings.position, @@ -1298,7 +1278,7 @@ const EditingPlanner: React.FC = () => { currentCoverScheme={currentCoverScheme} coverSchemes={COVER_SCHEMES} aiCoverLoading={aiCoverLoading} - titleSettings={titleSettings} + titleConfig={titleConfig} subtitleSettings={{ enabled: subtitleSettings.enabled, position: subtitleSettings.position, @@ -1355,15 +1335,11 @@ const EditingPlanner: React.FC = () => {
- setTitleSettings((prev) => ({ ...prev, ...partial })) - } onSubtitleSettingsChange={(partial) => setSubtitleSettings((prev) => ({ ...prev, ...partial }) as SubtitleStyleConfig) } diff --git a/apps/web/src/pages/editing-planner/components/ClipPropertiesPanel.tsx b/apps/web/src/pages/editing-planner/components/ClipPropertiesPanel.tsx index 65cfa5e78..c92a5842b 100755 --- a/apps/web/src/pages/editing-planner/components/ClipPropertiesPanel.tsx +++ b/apps/web/src/pages/editing-planner/components/ClipPropertiesPanel.tsx @@ -1,11 +1,11 @@ /** * 右栏设置面板 — V8 原型 1:1 还原 - * 标题设置(AI toggle) + 字幕设置 + BGM设置 + 片段详情 + * 字幕设置 + BGM设置 + 片段详情 */ import React, { useRef, useState, useCallback } from "react" import { useNavigate } from "react-router-dom" import type { TemplateMode } from "@/api/editingPlanner" -import type { ClipData, ClipType, TitleSettings } from "../types" +import type { ClipData, ClipType } from "../types" import { TRANSITION_OPTIONS } from "@/api/editPlans" import type { AssetItem } from "@/api/assets" @@ -33,13 +33,11 @@ interface BgmSettings { interface ClipPropertiesPanelProps { selectedClip: ClipData | null - titleSettings: TitleSettings subtitleSettings: SubtitleSettings bgmSettings: BgmSettings clipsCount: number totalDuration: number currentMode: TemplateMode - onTitleSettingsChange: (partial: Partial) => void onSubtitleSettingsChange: (partial: Partial) => void onBgmSettingsChange: (partial: Partial) => void onClipUpdate: (clipId: string, data: Partial) => void @@ -92,174 +90,6 @@ const ANIMATION_OPTIONS = [ { value: "typewriter", label: "打字机" }, ] -/** - * 标题样式预设 — 纯样式组合(颜色+描边+阴影+字重+字号) - * 不绑定字体,用户可自由搭配任意字体 - * 预览统一用系统字体展示效果 - */ -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", - }, - }, -] - -/** 判断当前设置匹配哪个预设(比较 size + color + bold/italic/stroke/shadow,不比较字体) */ -function getActivePreset(settings: TitleSettings): string | null { - for (const p of TITLE_PRESETS) { - if ( - settings.size === p.style.size && - settings.color === p.style.color && - settings.bold === p.style.bold && - settings.italic === p.style.italic && - settings.stroke === p.style.stroke && - settings.shadow === p.style.shadow - ) { - return p.key - } - } - return null -} - /** 片段类型图标/标签 */ const CLIP_TYPE_ICONS: Record = { voice: "🎙️", pip: "🖼️" } const CLIP_TYPE_LABELS: Record = { @@ -269,13 +99,11 @@ const CLIP_TYPE_LABELS: Record = { const ClipPropertiesPanel: React.FC = ({ selectedClip, - titleSettings, subtitleSettings, bgmSettings, clipsCount, totalDuration, currentMode, - onTitleSettingsChange, onSubtitleSettingsChange, onBgmSettingsChange: _onBgmSettingsChange, onClipUpdate, @@ -333,142 +161,6 @@ const ClipPropertiesPanel: React.FC = ({ } return (
- {/* ═══ 标题设置 ═══ */} -
-
- 📝 - 标题设置 -
- -
- AI 自动选择 -
- onTitleSettingsChange({ - aiAutoSelect: !titleSettings.aiAutoSelect, - }) - } - > -
-
-
- - {!titleSettings.aiAutoSelect && ( - <> -
- - -
- -
- - -
- -
- -
- onTitleSettingsChange({ size: Number(e.target.value) })} - /> - {titleSettings.size}px -
-
- -
- -
- {TITLE_PRESETS.map((p) => { - const isActive = getActivePreset(titleSettings) === p.key - return ( - - ) - })} -
-
- -
- -
- - - - -
-
- - )} -
- {/* ═══ 字幕设置 ═══ */}
diff --git a/apps/web/src/pages/editing-planner/components/PreviewPlayer.tsx b/apps/web/src/pages/editing-planner/components/PreviewPlayer.tsx index 9ae424cf6..3ff6c0e30 100755 --- a/apps/web/src/pages/editing-planner/components/PreviewPlayer.tsx +++ b/apps/web/src/pages/editing-planner/components/PreviewPlayer.tsx @@ -4,7 +4,8 @@ * 封面右侧竖排4个方案按钮 */ import React from "react" -import type { ClipData, ClipType, TitleSettings } from "../types" +import type { ClipData, ClipType } from "../types" +import type { TitleConfig } from "@/api/editPlans" interface CoverScheme { key: string @@ -26,7 +27,7 @@ interface PreviewPlayerProps { currentCoverScheme: string coverSchemes: CoverScheme[] aiCoverLoading: boolean - titleSettings?: TitleSettings + titleConfig?: TitleConfig subtitleSettings?: SubtitleSettings onClipSelect: (clipId: string) => void onCoverSchemeChange: (scheme: string) => void @@ -51,7 +52,7 @@ const PreviewPlayer: React.FC = ({ currentCoverScheme, coverSchemes, aiCoverLoading, - titleSettings, + titleConfig, subtitleSettings, onCoverSchemeChange, onPlayPause, @@ -90,27 +91,28 @@ const PreviewPlayer: React.FC = ({ )} {/* 标题实时预览 */} - {titleSettings && !titleSettings.aiAutoSelect && titleSettings.title && ( + {titleConfig && !titleConfig.ai_auto_select && titleConfig.content && (
- {titleSettings.title} + {titleConfig.content}
)} diff --git a/apps/web/src/pages/generate/GeneratePage.tsx b/apps/web/src/pages/generate/GeneratePage.tsx index ff3baa14e..4e6a6484d 100755 --- a/apps/web/src/pages/generate/GeneratePage.tsx +++ b/apps/web/src/pages/generate/GeneratePage.tsx @@ -80,7 +80,107 @@ const STEPS = [ { key: 5, label: "确认生成" }, ] -/* ── 标题选项从 API 加载,不再硬编码 ── */ +/* ── 标题设置常量 ── */ +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: "", + position: "bottom", + font: "思源黑体", + size: 28, + bold: true, + italic: false, + stroke: true, + shadow: false, + 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" }, + }, +] + +function getActivePreset(settings: TitleSettings): string | null { + for (const p of TITLE_PRESETS) { + if ( + settings.size === p.style.size && + settings.color === p.style.color && + settings.bold === p.style.bold && + settings.italic === p.style.italic && + settings.stroke === p.style.stroke && + settings.shadow === p.style.shadow + ) { + return p.key + } + } + return null +} /* ================================================================ 组件 @@ -111,8 +211,8 @@ const GeneratePage: React.FC = () => { /* 素材选择模式:手动选择 / 自动匹配 */ const [materialMode, setMaterialMode] = useState<"manual" | "auto">("manual") - /* ── 标题 ── */ - const [title, setTitle] = useState("") + /* ── 标题设置 ── */ + const [titleSettings, setTitleSettings] = useState(DEFAULT_TITLE_SETTINGS) const { data: userTitles = [] } = useQuery({ queryKey: ["titles"], queryFn: () => getTitles(), @@ -121,8 +221,16 @@ const GeneratePage: React.FC = () => { /* 当选中模板开启了「AI自动匹配标题」,自动填入模板预设标题 */ useEffect(() => { const tpl = userTemplates.find((t) => t.id === selectedTemplate) - if (tpl?.title_config?.ai_auto_select && tpl.title_config.content) { - setTitle(tpl.title_config.content) + if (tpl?.title_config) { + setTitleSettings((prev) => ({ + ...prev, + aiAutoSelect: tpl.title_config!.ai_auto_select, + title: tpl.title_config!.content || prev.title, + position: tpl.title_config!.position || prev.position, + font: tpl.title_config!.font_preset || prev.font, + size: tpl.title_config!.font_size || prev.size, + color: tpl.title_config!.font_color || prev.color, + })) } }, [selectedTemplate, userTemplates]) @@ -184,8 +292,16 @@ const GeneratePage: React.FC = () => { segments?: Array<{ media_asset_id?: string; material_type?: string }> } - if (config.title_config?.content) { - setTitle(config.title_config.content) + if (config.title_config) { + setTitleSettings((prev) => ({ + ...prev, + title: config.title_config!.content || "", + aiAutoSelect: config.title_config!.ai_auto_select || false, + position: config.title_config!.position || prev.position, + font: (config.title_config as any)?.font_preset || prev.font, + size: (config.title_config as any)?.font_size || prev.size, + color: (config.title_config as any)?.font_color || prev.color, + })) } if (config.segments && config.segments.length > 0) { const assetIds = config.segments @@ -206,8 +322,19 @@ const GeneratePage: React.FC = () => { const loadPlanConfig = async () => { try { const plan = await getEditPlan(editPlanId) - if (plan.name) setTitle(plan.name) + if (plan.name) setTitleSettings((prev) => ({ ...prev, title: plan.name })) const cfg = plan.config + if (cfg?.title_config) { + setTitleSettings((prev) => ({ + ...prev, + aiAutoSelect: cfg.title_config!.ai_auto_select, + title: cfg.title_config!.content || prev.title, + position: cfg.title_config!.position || prev.position, + font: cfg.title_config!.font_preset || prev.font, + size: cfg.title_config!.font_size || prev.size, + color: cfg.title_config!.font_color || prev.color, + })) + } if (cfg?.asset_ids) { setSelectedMaterials(cfg.asset_ids.filter((v): v is string => typeof v === "string")) } @@ -454,12 +581,12 @@ const GeneratePage: React.FC = () => { const handleGenerate = useCallback(async () => { console.log("[handleGenerate] 开始生成, 参数:", { - title, + titleSettings, selectedTemplate, selectedMaterials, voiceMode, }) - if (!title.trim()) { + if (!titleSettings.title.trim()) { message.warning("请先选择或输入标题") return } @@ -504,9 +631,17 @@ const GeneratePage: React.FC = () => { const plan = await createEditPlan({ template_id: selectedTemplate, - name: title.trim(), + name: titleSettings.title.trim(), config: { asset_ids: selectedMaterials, + title_config: { + ai_auto_select: titleSettings.aiAutoSelect, + content: titleSettings.title, + position: titleSettings.position, + font_preset: titleSettings.font, + font_color: titleSettings.color, + font_size: titleSettings.size, + }, ...voiceConfig, ratio: videoRatio, style, @@ -689,7 +824,7 @@ const GeneratePage: React.FC = () => { message.error(finalMsg) } }, [ - title, + titleSettings, selectedMaterials, selectedVoice, voiceMode, @@ -755,14 +890,14 @@ const GeneratePage: React.FC = () => { message.warning("请至少选择一个素材") return } - if (currentStep === 3 && !title.trim()) { + if (currentStep === 3 && !titleSettings.title.trim()) { message.warning("请选择或输入标题") return } if (currentStep < 5) { setCurrentStep((s) => s + 1) } - }, [currentStep, selectedTemplate, selectedMaterials.length, title, materialMode]) + }, [currentStep, selectedTemplate, selectedMaterials.length, titleSettings, materialMode]) const goPrev = useCallback(() => { if (currentStep > 1) { @@ -1022,48 +1157,209 @@ const GeneratePage: React.FC = () => {
) - /** 步骤 3:选择标题 */ + /** 步骤 3:选择标题 + 标题样式设置 */ const renderStep3 = () => (

📝 选择标题

-
- - setTitle(e.target.value)} - maxLength={50} - /> -
- {userTitles.length === 0 && ( -

- 标题库为空,请前往「标题管理」添加标题,或手动输入 -

+ + {!titleSettings.aiAutoSelect && ( + <> + {/* 标题内容选择 */} +
+ + + setTitleSettings((prev) => ({ ...prev, title: e.target.value })) + } + maxLength={50} + /> +
+ + {/* 标题样式设置区 */} +
+

标题样式

+ + {/* 位置 + 字体 一行 */} +
+
+ + +
+
+ + +
+
+ + {/* 字号滑块 */} +
+
+ + {titleSettings.size}px +
+ + setTitleSettings((prev) => ({ + ...prev, + size: Number(e.target.value), + })) + } + /> +
+ + {/* 预设样式 */} +
+ +
+ {TITLE_PRESETS.map((p) => { + const isActive = getActivePreset(titleSettings) === p.key + return ( + + ) + })} +
+
+ + {/* 样式按钮:粗体/斜体/描边/阴影 */} +
+ +
+ + + + +
+
+
+ )}
) @@ -1594,7 +1890,7 @@ const GeneratePage: React.FC = () => {
标题 - {title || "未选择"} + {titleSettings.title || "未选择"}
配音 @@ -1845,7 +2141,7 @@ const GeneratePage: React.FC = () => {
{/* 字幕预览 */} -
{title || "3秒抓住注意力,30秒讲清卖点"}
+
{titleSettings.title || "3秒抓住注意力,30秒讲清卖点"}
{/* 时间线标题 */}
剪辑计划预览
diff --git a/apps/web/src/pages/generate/generate.css b/apps/web/src/pages/generate/generate.css old mode 100644 new mode 100755 index f07f221c1..ac0d6876e --- a/apps/web/src/pages/generate/generate.css +++ b/apps/web/src/pages/generate/generate.css @@ -1136,3 +1136,204 @@ border: 1px solid var(--border-light, #f1f5f9); border-radius: 20px; } + +/* ============================================================ + 标题设置(选择标题步骤) + ============================================================ */ +.xx-title-ai-toggle { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 16px; + background: var(--bg-secondary); + border-radius: var(--radius-sm); + margin-bottom: 18px; +} + +.xx-toggle-label { + font-size: 14px; + font-weight: 500; + color: var(--text-primary); +} + +.xx-switch { + width: 44px; + height: 24px; + background: var(--border-color); + border-radius: 12px; + position: relative; + cursor: pointer; + transition: background 0.2s; +} + +.xx-switch.active { + background: var(--primary-color); +} + +.xx-switch-knob { + position: absolute; + top: 2px; + left: 2px; + width: 20px; + height: 20px; + background: #fff; + border-radius: 50%; + transition: transform 0.2s; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15); +} + +.xx-switch.active .xx-switch-knob { + transform: translateX(20px); +} + +.xx-title-style-section { + margin-top: 22px; + padding-top: 20px; + border-top: 1px solid var(--border-light); +} + +.xx-section-subtitle { + font-size: 14px; + font-weight: 600; + color: var(--text-primary); + margin: 0 0 16px; +} + +.xx-title-style-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 14px; + margin-bottom: 14px; +} + +.xx-half-field { + margin-bottom: 0; +} + +.xx-field-label-row { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; +} + +.xx-field-label-row label { + margin-bottom: 0; +} + +.xx-field-value { + font-size: 13px; + font-weight: 600; + color: var(--primary-color); +} + +.xx-slider { + width: 100%; + height: 6px; + -webkit-appearance: none; + appearance: none; + background: var(--border-color); + border-radius: 3px; + outline: none; + cursor: pointer; +} + +.xx-slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 18px; + height: 18px; + background: var(--primary-color); + border-radius: 50%; + cursor: pointer; + box-shadow: 0 2px 6px rgba(79, 70, 229, 0.3); +} + +.xx-slider::-moz-range-thumb { + width: 18px; + height: 18px; + background: var(--primary-color); + border-radius: 50%; + cursor: pointer; + border: none; + box-shadow: 0 2px 6px rgba(79, 70, 229, 0.3); +} + +/* 标题预设卡片网格 */ +.xx-title-presets-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 10px; +} + +.xx-title-preset-card { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 14px 8px; + background: var(--bg-secondary); + border: 2px solid transparent; + border-radius: var(--radius-sm); + cursor: pointer; + transition: all 0.15s; + text-align: center; +} + +.xx-title-preset-card:hover { + border-color: var(--primary-200); + background: var(--bg-primary); +} + +.xx-title-preset-card.active { + border-color: var(--primary-color); + background: var(--primary-50); +} + +.xx-title-preset-preview-text { + line-height: 1.4; + margin-bottom: 6px; + user-select: none; +} + +.xx-title-preset-card-label { + font-size: 11px; + color: var(--text-secondary); +} + +.xx-title-preset-card.active .xx-title-preset-card-label { + color: var(--primary-color); + font-weight: 500; +} + +/* 样式按钮组 */ +.xx-style-btns { + display: flex; + gap: 8px; +} + +.xx-style-btn { + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + background: var(--bg-primary); + cursor: pointer; + font-size: 15px; + color: var(--text-secondary); + transition: all 0.15s; +} + +.xx-style-btn:hover { + border-color: var(--primary-300); + color: var(--primary-color); +} + +.xx-style-btn.active { + background: var(--primary-color); + border-color: var(--primary-color); + color: #fff; +} \ No newline at end of file -- 2.54.0 From ee7eba0a852e70458bbebfe25dfe3a78c1e17b54 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Sun, 19 Jul 2026 14:59:35 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20=E7=A7=BB=E9=99=A4any=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=EF=BC=8C=E4=BD=BF=E7=94=A8TitleConfig=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E6=B3=A8=E8=A7=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/pages/generate/GeneratePage.tsx | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/apps/web/src/pages/generate/GeneratePage.tsx b/apps/web/src/pages/generate/GeneratePage.tsx index 4e6a6484d..5cb90e367 100755 --- a/apps/web/src/pages/generate/GeneratePage.tsx +++ b/apps/web/src/pages/generate/GeneratePage.tsx @@ -31,7 +31,7 @@ import { updateEditPlan, getGenerationTaskResults, } from "@/api/editPlans" -import type { GeneratedVideo, EditPlanConfig } from "@/api/editPlans" +import type { GeneratedVideo, EditPlanConfig, TitleConfig } from "@/api/editPlans" import { getEditingTemplates } from "@/api/editingPlanner" import { getTitles } from "@/api/titles" import apiClient from "@/api/client" @@ -293,14 +293,15 @@ const GeneratePage: React.FC = () => { } if (config.title_config) { + const tc = config.title_config as TitleConfig setTitleSettings((prev) => ({ ...prev, - title: config.title_config!.content || "", - aiAutoSelect: config.title_config!.ai_auto_select || false, - position: config.title_config!.position || prev.position, - font: (config.title_config as any)?.font_preset || prev.font, - size: (config.title_config as any)?.font_size || prev.size, - color: (config.title_config as any)?.font_color || prev.color, + title: tc.content || "", + aiAutoSelect: tc.ai_auto_select || false, + position: tc.position || prev.position, + font: tc.font_preset || prev.font, + size: tc.font_size || prev.size, + color: tc.font_color || prev.color, })) } if (config.segments && config.segments.length > 0) { -- 2.54.0 From 91554623c34c818fc1327a27ab59ec443cf123e1 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Sun, 19 Jul 2026 15:10:19 +0800 Subject: [PATCH 3/3] fix: prettier formatting --- apps/web/src/pages/generate/GeneratePage.tsx | 79 ++++++++++++-------- apps/web/src/pages/generate/generate.css | 2 +- 2 files changed, 49 insertions(+), 32 deletions(-) diff --git a/apps/web/src/pages/generate/GeneratePage.tsx b/apps/web/src/pages/generate/GeneratePage.tsx index 5cb90e367..5b10bb11f 100755 --- a/apps/web/src/pages/generate/GeneratePage.tsx +++ b/apps/web/src/pages/generate/GeneratePage.tsx @@ -120,13 +120,23 @@ 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" }, + 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" }, + previewStyle: { + fontWeight: 700, + color: "#d4a843", + textShadow: "1px 1px 3px rgba(0,0,0,0.8)", + fontSize: "20px", + }, }, { key: "fresh_minimal", @@ -138,7 +148,13 @@ const TITLE_PRESETS = [ 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" }, + previewStyle: { + fontWeight: 900, + color: "#ff4081", + WebkitTextStroke: "1.5px #ffffff", + textShadow: "2px 2px 4px rgba(0,0,0,0.5)", + fontSize: "22px", + }, }, { key: "business", @@ -150,19 +166,34 @@ const TITLE_PRESETS = [ 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" }, + 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" }, + 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" }, + previewStyle: { + fontWeight: 400, + color: "#333333", + textShadow: "1px 1px 2px rgba(0,0,0,0.3)", + fontSize: "20px", + }, }, ] @@ -1187,17 +1218,13 @@ const GeneratePage: React.FC = () => { showSearch style={{ width: "100%" }} value={titleSettings.title || undefined} - onChange={(val) => - setTitleSettings((prev) => ({ ...prev, title: val || "" })) - } + onChange={(val) => setTitleSettings((prev) => ({ ...prev, title: val || "" }))} options={userTitles.map((t) => ({ label: t.content, value: t.content, }))} filterOption={(input, option) => - ((option?.label as string) || "") - .toLowerCase() - .includes(input.toLowerCase()) + ((option?.label as string) || "").toLowerCase().includes(input.toLowerCase()) } notFoundContent={ userTitles.length === 0 ? ( @@ -1213,9 +1240,7 @@ const GeneratePage: React.FC = () => { - setTitleSettings((prev) => ({ ...prev, title: e.target.value })) - } + onChange={(e) => setTitleSettings((prev) => ({ ...prev, title: e.target.value }))} maxLength={50} />
@@ -1247,9 +1272,7 @@ const GeneratePage: React.FC = () => {